diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index bc9ac4d27..7cacab99e 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -18,7 +18,8 @@ import { } from './virtual-content'; import { Memoize } from 'typescript-memoize'; import { describeExports } from './describe-exports'; -import { readFileSync } from 'fs'; +import { existsSync, readFileSync } from 'fs'; +import { readJSONSync } from 'fs-extra'; const debug = makeDebug('embroider:resolver'); function logTransition(reason: string, before: R, after: R = before): R { @@ -151,6 +152,7 @@ export class Resolver { request = this.handleFastbootCompat(request); request = this.handleGlobalsCompat(request); + request = this.handleLegacyAddons(request); request = this.handleRenaming(request); return this.preHandleExternal(request); } @@ -177,9 +179,7 @@ export class Resolver { // synchronous alternative to resolve() above. Because our own internals are // all synchronous, you can use this if your defaultResolve function is - // synchronous. At present, we need this for the case where we are compiling - // non-strict templates and doing component resolutions inside the template - // compiler inside babel, which is a synchronous context. + // synchronous. resolveSync( request: Req, defaultResolve: SyncResolverFunction @@ -596,6 +596,68 @@ export class Resolver { return owningEngine; } + private handleLegacyAddons(request: R): R { + let packageCache = PackageCache.shared('embroider-stage3', this.options.appRoot); + + // first we handle output requests from moved packages + let pkg = this.owningPackage(request.fromFile); + if (!pkg) { + return request; + } + let originalRoot = this.legacyAddonsIndex.v2toV1.get(pkg.root); + if (originalRoot) { + request = logTransition( + 'outbound from moved v1 addon', + request, + request.rehome(resolve(originalRoot, 'package.json')) + ); + pkg = packageCache.get(originalRoot)!; + } + + // then we handle inbound requests to moved packages + let packageName = getPackageName(request.specifier); + if (packageName && packageName !== pkg.name) { + // non-relative, non-self request, so check if it aims at a rewritten addon + try { + let target = PackageCache.shared('embroider-stage3', this.options.appRoot).resolve(packageName, pkg); + if (target) { + let movedRoot = this.legacyAddonsIndex.v1ToV2.get(target.root); + if (movedRoot) { + request = logTransition( + 'inbound to moved v1 addon', + request, + this.resolveWithinPackage(request, packageCache.get(movedRoot)) + ); + } + } + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } + } + } + + return request; + } + + @Memoize() + private get legacyAddonsIndex(): { v1ToV2: Map; v2toV1: Map } { + let addonsDir = resolve(this.options.appRoot, 'node_modules', '.embroider', 'addons'); + let indexFile = resolve(addonsDir, 'v1-addon-index.json'); + if (existsSync(indexFile)) { + let { v1Addons } = readJSONSync(indexFile) as { v1Addons: Record }; + return { + v1ToV2: new Map( + Object.entries(v1Addons).map(([oldRoot, relativeNewRoot]) => [oldRoot, resolve(addonsDir, relativeNewRoot)]) + ), + v2toV1: new Map( + Object.entries(v1Addons).map(([oldRoot, relativeNewRoot]) => [resolve(addonsDir, relativeNewRoot), oldRoot]) + ), + }; + } + return { v1ToV2: new Map(), v2toV1: new Map() }; + } + private handleRenaming(request: R): R { if (request.isVirtual) { return request; diff --git a/tests/scenarios/core-resolver-test.ts b/tests/scenarios/core-resolver-test.ts index 410a0f560..d2da534fc 100644 --- a/tests/scenarios/core-resolver-test.ts +++ b/tests/scenarios/core-resolver-test.ts @@ -2,7 +2,7 @@ import { AddonMeta, AppMeta } from '@embroider/shared-internals'; import { outputFileSync } from 'fs-extra'; import { resolve } from 'path'; import QUnit from 'qunit'; -import { Project, Scenarios } from 'scenario-tester'; +import { PreparedApp, Project, Scenarios } from 'scenario-tester'; import { CompatResolverOptions } from '@embroider/compat/src/resolver-transform'; import { ExpectAuditResults } from '@embroider/test-support/audit-assertions'; import { installAuditAssertions } from '@embroider/test-support/audit-assertions'; @@ -55,10 +55,26 @@ Scenarios.fromProject(() => new Project()) } let configure: (opts?: ConfigureOpts) => Promise; + let app: PreparedApp; + + function addonPackageJSON(addonMeta?: Partial) { + return JSON.stringify( + (() => { + let meta: AddonMeta = { type: 'addon', version: 2, 'auto-upgraded': true, ...(addonMeta ?? {}) }; + return { + name: 'my-addon', + keywords: ['ember-addon'], + 'ember-addon': meta, + }; + })(), + null, + 2 + ); + } hooks.beforeEach(async assert => { installAuditAssertions(assert); - let app = await scenario.prepare(); + app = await scenario.prepare(); givenFiles = function (files: Record) { for (let [filename, contents] of Object.entries(files)) { @@ -110,18 +126,7 @@ Scenarios.fromProject(() => new Project()) module.exports = function(filename) { return true } `, '.embroider/resolver.json': JSON.stringify(resolverOptions), - 'node_modules/my-addon/package.json': JSON.stringify( - (() => { - let meta: AddonMeta = { type: 'addon', version: 2, 'auto-upgraded': true, ...(opts?.addonMeta ?? {}) }; - return { - name: 'my-addon', - keywords: ['ember-addon'], - 'ember-addon': meta, - }; - })(), - null, - 2 - ), + 'node_modules/my-addon/package.json': addonPackageJSON(opts?.addonMeta), }); expectAudit = await assert.audit({ outputDir: app.dir }); @@ -594,5 +599,48 @@ Scenarios.fromProject(() => new Project()) switcherModule.resolves('./browser').to('./node_modules/my-addon/_app_/hello-world.js'); }); }); + + Qmodule('legacy-addons', function () { + test('app can resolve file in rewritten addon', async function () { + givenFiles({ + 'node_modules/.embroider/addons/v1-addon-index.json': JSON.stringify({ + v1Addons: { + [resolve(app.dir, 'node_modules/my-addon')]: 'my-addon.1234', + }, + }), + 'node_modules/.embroider/addons/my-addon.1234/hello-world.js': ``, + 'node_modules/.embroider/addons/my-addon.1234/package.json': addonPackageJSON(), + 'app.js': `import "my-addon/hello-world"`, + }); + + await configure({}); + + expectAudit + .module('./app.js') + .resolves('my-addon/hello-world') + .to('./node_modules/.embroider/addons/my-addon.1234/hello-world.js'); + }); + + test('moved addon resolves dependencies from its original location', async function () { + givenFiles({ + 'node_modules/my-addon/node_modules/inner-dep/index.js': '', + 'node_modules/.embroider/addons/v1-addon-index.json': JSON.stringify({ + v1Addons: { + [resolve(app.dir, 'node_modules/my-addon')]: 'my-addon.1234', + }, + }), + 'node_modules/.embroider/addons/my-addon.1234/hello-world.js': `import "inner-dep"`, + 'node_modules/.embroider/addons/my-addon.1234/package.json': addonPackageJSON(), + 'app.js': `import "my-addon/hello-world"`, + }); + + await configure({}); + + expectAudit + .module('./node_modules/.embroider/addons/my-addon.1234/hello-world.js') + .resolves('inner-dep') + .to('./node_modules/my-addon/node_modules/inner-dep/index.js'); + }); + }); }); });