Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new option for addon-shim to pass config to ember-auto-import #2158

Merged
merged 2 commits into from
Oct 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 114 additions & 25 deletions packages/addon-shim/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ import { satisfies } from 'semver';

export interface ShimOptions {
disabled?: (options: any) => boolean;

// this part only applies when running under ember-auto-import. It's intended
// to let a V2 addon tweak how it's interpreted by ember-auto-import inside
// the classic build in order to achieve backward compatibility with how it
// behaved as a V1 addon.
autoImportCompat?: {
// can modify the `ember-addon` metadata that ember-auto-import is using to
// do resolution. Right now that means the `renamed-modules`.
customizeMeta?: (meta: AddonMeta) => AddonMeta;
};
}

function addonMeta(pkgJSON: PackageInfo): AddonMeta {
Expand All @@ -22,6 +32,16 @@ function addonMeta(pkgJSON: PackageInfo): AddonMeta {
return meta as AddonMeta;
}

type OwnType = AddonInstance & {
_eaiAssertions(): void;
_internalRegisterV2Addon(
name: string,
root: string,
autoImportCompat?: ShimOptions['autoImportCompat']
): void;
_parentName(): string;
};

export function addonV1Shim(directory: string, options: ShimOptions = {}) {
let pkg: PackageInfo = JSON.parse(
readFileSync(resolve(directory, './package.json'), 'utf8')
Expand Down Expand Up @@ -73,20 +93,20 @@ export function addonV1Shim(directory: string, options: ShimOptions = {}) {

return {
name: pkg.name,
included(
this: AddonInstance & {
registerV2Addon(name: string, dir: string): void;
},
...args: unknown[]
) {
included(this: OwnType, ...args: unknown[]) {
let parentOptions;
if (isDeepAddonInstance(this)) {
parentOptions = this.parent.options;
} else {
parentOptions = this.app.options;
}

this.registerV2Addon(this.name, directory);
this._eaiAssertions();
this._internalRegisterV2Addon(
this.name,
directory,
options.autoImportCompat
);

if (options.disabled) {
disabled = options.disabled(parentOptions);
Expand Down Expand Up @@ -139,38 +159,94 @@ export function addonV1Shim(directory: string, options: ShimOptions = {}) {
return isInside(directory, appInstance.project.root);
},

registerV2Addon(this: AddonInstance, name: string, root: string): void {
let parentName: string;
if (isDeepAddonInstance(this)) {
parentName = this.parent.name;
} else {
parentName = this.parent.name();
}

_eaiAssertions(this: OwnType) {
// if we're being used by a v1 package, that package needs ember-auto-import 2
if ((this.parent.pkg['ember-addon']?.version ?? 1) < 2) {
// important: here we're talking about the version of ember-auto-import
// declared by the package that is trying to use our V2 addon. Which is
// distinct from the version that may be installed in the top-level app,
// and which is also distinct from the elected ember-auto-import leader.
let autoImport = locateAutoImport(this.parent.addons);
if (!autoImport.present) {
throw new Error(
`${parentName} needs to depend on ember-auto-import in order to use ${this.name}`
`${this._parentName()} needs to depend on ember-auto-import in order to use ${
this.name
}`
);
}

if (!autoImport.satisfiesV2) {
throw new Error(
`${parentName} has ember-auto-import ${autoImport.version} which is not new enough to use ${this.name}. It needs to upgrade to >=2.0`
`${this._parentName()} has ember-auto-import ${
autoImport.version
} which is not new enough to use ${
this.name
}. It needs to upgrade to >=2.0`
);
}
autoImport.instance.registerV2Addon(name, root);
}
},

_internalRegisterV2Addon(
this: OwnType,
name: string,
root: string,
options?: ShimOptions['autoImportCompat']
) {
// this is searching the top-level app for ember-auto-import, which is
// different from how we searched above in _eaiAssertions. We're going
// straight to the top because we definitely want to locate EAI if it's
// present, but our addon's immediate parent won't necessarily have EAI if
// that parent is itself a V2 addon.
let autoImport = locateAutoImport(this.project.addons);
if (!autoImport.present || !autoImport.satisfiesV2) {
// We don't assert here because it's not our responsibility. In
// _eaiAssertions we check the condition of our immediate parent, which
// makes the error messages more actionable. If our parent has EAI>=2,
// its copy of EAI will in turn assert that the app has one as well.
//
// This case is actually fine for a v2 app under Embroider, where EAI is
// not needed.
return;
}

// we're not using autoImport.instance.registerV2Addon because not all 2.x
// versions will forward the third argument to the current leader. Whereas
// we can confidently ensure that the leader itself supports the third
// argument by adding it as a dependency of our V2 addon, since the newest
// copy that satisfies the app's requested semver range will win the
// election.

let leader: ReturnType<NonNullable<EAI2Instance['leader']>>;
if (autoImport.instance.leader) {
// sufficiently new EAI lets us directly ask for the leader
leader = autoImport.instance.leader();
} else {
// This should only be done if we're being consumed by an addon
if (this.parent.pkg['ember-addon'].type === 'addon') {
// if we're being used by a v2 addon, it also has this shim and will
// forward our registration onward to ember-auto-import
(this.parent as EAI2Instance).registerV2Addon(name, root);
}
// otherwise we need to reach inside
// eslint-disable-next-line @typescript-eslint/no-require-imports
let AutoImport = require(join(
autoImport.instance.root,
'auto-import.js'
)).default;
leader = AutoImport.lookup(autoImport.instance);
}

leader.registerV2Addon(name, root, options);
},

_parentName(this: OwnType): string {
if (isDeepAddonInstance(this)) {
return this.parent.name;
} else {
return this.parent.name();
}
},

// This continues to exist because there are earlier versions of addon-shim
// that forward v2 addon registration through their parent V2 addon, thus
// calling this method.
registerV2Addon(this: OwnType, name: string, root: string): void {
this._internalRegisterV2Addon(name, root);
},
};
}

Expand All @@ -180,7 +256,20 @@ function isInside(parentDir: string, otherDir: string): boolean {
}

type EAI2Instance = AddonInstance & {
// all 2.x versions of EAI have this method
registerV2Addon(name: string, root: string): void;

// EAI >= 2.10.0 offers this API, which is intended to be more extensible
// since it lets you talk directly to the current leader. That's better
// because the newest version of EAI present becomes the leader, so you can
// guarantee a minimum leader version by making it your own dependency.
leader?: () => {
registerV2Addon(
name: string,
root: string,
options?: ShimOptions['autoImportCompat']
): void;
};
};

function locateAutoImport(addons: AddonInstance[]):
Expand Down