Skip to content

Commit

Permalink
Merge pull request #2158 from embroider-build/eai-compat-options
Browse files Browse the repository at this point in the history
Add a new option for addon-shim to pass config to ember-auto-import
  • Loading branch information
ef4 authored Oct 31, 2024
2 parents f879e60 + 6827683 commit 79f2339
Showing 1 changed file with 114 additions and 25 deletions.
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

0 comments on commit 79f2339

Please sign in to comment.