From e3e9bd859472f32c0f33a552e476cb7f96f7d7d3 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Thu, 31 Oct 2024 14:28:48 -0400 Subject: [PATCH] Add a new option for addon-shim to pass config to ember-auto-import This is part of the plan to ship ember-source as a v2 addon. This piece makes it possible for a v2 addon to send additional configuration directly to the leading copy of ember-auto-import. This is different from how it worked before, which was that registerV2Addon calls all cascaded up the tree through intermediate copies of ember-auto-import, which might be any compatible minor version. Instead, now if you add a specific version of ember-auto-import as a dependency of your v2 addon, you will be guaranteed to be talking to that version (or newer) when you registerV2Addon, and thus you can send new options and expect them to work. --- packages/addon-shim/src/index.ts | 139 +++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/packages/addon-shim/src/index.ts b/packages/addon-shim/src/index.ts index 97d9b772b..340e4814a 100644 --- a/packages/addon-shim/src/index.ts +++ b/packages/addon-shim/src/index.ts @@ -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 { @@ -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') @@ -73,12 +93,7 @@ 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; @@ -86,7 +101,12 @@ export function addonV1Shim(directory: string, options: ShimOptions = {}) { 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); @@ -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>; + 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); + }, }; } @@ -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[]):