diff --git a/packages/compat/src/babel-plugin-adjust-imports.ts b/packages/compat/src/babel-plugin-adjust-imports.ts index 9fe846aa3..bb1e8c1f8 100644 --- a/packages/compat/src/babel-plugin-adjust-imports.ts +++ b/packages/compat/src/babel-plugin-adjust-imports.ts @@ -53,6 +53,19 @@ export default function main(babel: typeof Babel) { } return { + manipulateOptions(opts: any, parserOpts: any) { + let filename: string = opts.filename || parserOpts.sourceFileName; + if (!filename) return; + filename = cleanUrl(filename); + if (filename.includes('__embroider_appjs_match__')) { + filename = filename.split('__embroider_appjs_match__')[0]; + if (filename.includes('embroider_virtual:')) { + filename = filename.split('embroider_virtual:')[1]; + } + opts.filename = filename; + parserOpts.sourceFileName = filename; + } + }, visitor: { Program: { enter(path: NodePath, state: State) { diff --git a/packages/compat/src/module-visitor.ts b/packages/compat/src/module-visitor.ts index e1da5b15a..27edfafb6 100644 --- a/packages/compat/src/module-visitor.ts +++ b/packages/compat/src/module-visitor.ts @@ -309,6 +309,7 @@ class ModuleVisitor { transpiledContent: result.transpiledContent, }; } catch (err) { + console.error(err); if (['BABEL_PARSE_ERROR', 'BABEL_TRANSFORM_ERROR'].includes(err.code)) { return [ { diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index 44369c4dc..2e184c2c0 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -20,6 +20,8 @@ import { fastbootSwitch, decodeFastbootSwitch, decodeImplicitModules, + decodeAppJsMatch, + encodeAppJsMatch, } from './virtual-content'; import { Memoize } from 'typescript-memoize'; import { describeExports } from './describe-exports'; @@ -168,6 +170,7 @@ export class Resolver { request = this.handleFastbootSwitch(request); request = await this.handleGlobalsCompat(request); + request = this.handleEncodedAppJsMatch(request); request = this.handleImplicitModules(request); request = this.handleImplicitTestScripts(request); request = this.handleVendorStyles(request); @@ -184,6 +187,7 @@ export class Resolver { // rehome requests to their un-rewritten locations, and for the most part we // want to be dealing with the rewritten packages. request = this.handleRewrittenPackages(request); + request = this.handleAppJsMatch(request); return request; } @@ -372,6 +376,27 @@ export class Resolver { return logTransition('failed to match in fastboot switch', request); } + private handleAppJsMatch(request: R): R { + if (request.meta?.isAppJsMatch) { + return request.virtualize(encodeAppJsMatch(request.specifier, request.fromFile)); + } + return request; + } + + private handleEncodedAppJsMatch(request: R): R { + if (isTerminal(request)) { + return request; + } + let match = decodeAppJsMatch(request.fromFile); + if (!match) { + return request; + } + if (match.virtual) { + return request.rehome('./package.json'); + } + return request.rehome(match.filename); + } + private handleImplicitModules(request: R): R { if (isTerminal(request)) { return request; @@ -919,7 +944,7 @@ export class Resolver { request // setting meta here because if this fails, we want the fallback // logic to revert our rehome and continue from the *moved* package. - .withMeta({ originalFromFile: request.fromFile }) + .withMeta({ originalFromFile: request.fromFile, isAppJsMatch: request.meta?.isAppJsMatch }) .rehome(resolve(originalRequestingPkg.root, 'package.json')) ); } else { @@ -1055,7 +1080,7 @@ export class Resolver { // setting meta because if this fails, we want the fallback to pick up back // in the original requesting package. - return newRequest.withMeta({ originalFromFile }); + return newRequest.withMeta({ originalFromFile, isAppJsMatch: request.meta?.isAppJsMatch }); } private preHandleExternal(request: R): R { @@ -1425,7 +1450,13 @@ export class Resolver { case undefined: return undefined; case 'app-only': - return request.alias(matched.entry['app-js'].specifier).rehome(matched.entry['app-js'].fromFile); + return request + .alias(matched.entry['app-js'].specifier) + .rehome(matched.entry['app-js'].fromFile) + .withMeta({ + ...request.meta, + isAppJsMatch: true, + }); case 'fastboot-only': return request.alias(matched.entry['fastboot-js'].specifier).rehome(matched.entry['fastboot-js'].fromFile); case 'both': diff --git a/packages/core/src/virtual-content.ts b/packages/core/src/virtual-content.ts index 10fde4b9b..0ab50a2b0 100644 --- a/packages/core/src/virtual-content.ts +++ b/packages/core/src/virtual-content.ts @@ -1,5 +1,5 @@ -import { dirname, basename, resolve, posix, sep, join } from 'path'; -import type { Resolver, AddonPackage, Package } from '.'; +import { basename, dirname, join, posix, resolve, sep, extname } from 'path'; +import type { AddonPackage, Package, Resolver } from '.'; import { explicitRelative, extensionsPattern } from '.'; import { compile } from './js-handlebars'; import { decodeImplicitTestScripts, renderImplicitTestScripts } from './virtual-test-support'; @@ -9,6 +9,7 @@ import { decodeVirtualVendorStyles, renderVendorStyles } from './virtual-vendor- import { decodeEntrypoint, renderEntrypoint } from './virtual-entrypoint'; import { decodeRouteEntrypoint, renderRouteEntrypoint } from './virtual-route-entrypoint'; +import { readFileSync } from 'fs-extra'; const externalESPrefix = '/@embroider/ext-es/'; const externalCJSPrefix = '/@embroider/ext-cjs/'; @@ -42,6 +43,15 @@ export function virtualContent(filename: string, resolver: Resolver): VirtualCon if (extern) { return renderESExternalShim(extern); } + + let appjs = decodeAppJsMatch(filename); + if (appjs) { + if (!appjs.virtual) { + return renderAppJs(appjs.filename); + } + filename = appjs.filename; + } + let match = decodeVirtualPairComponent(filename); if (match) { return pairedComponentShim(match); @@ -175,8 +185,8 @@ const pairComponentMarker = '-embroider-pair-component'; const pairComponentPattern = /^(?.*)__vpc__(?[^\/]*)-embroider-pair-component$/; export function virtualPairComponent(hbsModule: string, jsModule: string | undefined): string { - let relativeJSModule = ''; - if (jsModule) { + let relativeJSModule = jsModule || ''; + if (jsModule && !jsModule.includes(appJsMatchMarker)) { relativeJSModule = explicitRelative(dirname(hbsModule), jsModule); } return `${hbsModule}__vpc__${encodeURIComponent(relativeJSModule)}${pairComponentMarker}`; @@ -195,7 +205,10 @@ function decodeVirtualPairComponent( } let { hbsModule, jsModule } = match.groups! as { hbsModule: string; jsModule: string }; // target our real hbs module from our virtual module - let relativeHBSModule = explicitRelative(dirname(filename), hbsModule); + let relativeHBSModule = hbsModule; + if (!hbsModule.includes(appJsMatchMarker)) { + relativeHBSModule = explicitRelative(dirname(filename), hbsModule); + } return { relativeHBSModule, relativeJSModule: decodeURIComponent(jsModule) || null, @@ -203,6 +216,45 @@ function decodeVirtualPairComponent( }; } +const appJsMatchMarker = '__embroider_appjs_match__'; +const appJsMatchPattern = /(?.+)__embroider_appjs_match__.{2,5}$/; +export function encodeAppJsMatch(specifier: string, from: string): string { + let to = require.resolve(specifier, { + paths: [resolve(dirname(from), 'node_modules')], + }); + return `${to}${appJsMatchMarker}${extname(to)}`; +} + +export function decodeAppJsMatch(filename: string) { + // Performance: avoid paying regex exec cost unless needed + if (!filename.includes(appJsMatchMarker)) { + return; + } + let match = appJsMatchPattern.exec(filename); + if (match) { + let to = match.groups!.to; + if (to.includes(pairComponentMarker)) { + if (to.includes('_vpc_')) { + to = filename; + } + return { + filename: to, + virtual: true, + }; + } + return { + filename: to, + }; + } +} + +function renderAppJs(filename: string) { + return { + src: readFileSync(filename).toString(), + watches: [filename], + }; +} + const fastbootSwitchSuffix = '/embroider_fastboot_switch'; const fastbootSwitchPattern = /(?.+)\/embroider_fastboot_switch(?:\?names=(?.+))?$/; export function fastbootSwitch(specifier: string, fromFile: string, names: Set): string { diff --git a/tests/scenarios/core-resolver-test.ts b/tests/scenarios/core-resolver-test.ts index 0279099d1..2f9468715 100644 --- a/tests/scenarios/core-resolver-test.ts +++ b/tests/scenarios/core-resolver-test.ts @@ -510,7 +510,7 @@ Scenarios.fromProject(() => new Project()) expectAudit .module('./app.js') .resolves('my-app/hello-world') - .to('./node_modules/my-addon/_app_/hello-world.js'); + .to('./node_modules/my-addon/_app_/hello-world.js__embroider_appjs_match__.js'); }); test('app-js module in addon can still do relative imports that escape its package', async function () { @@ -527,7 +527,7 @@ Scenarios.fromProject(() => new Project()) }); expectAudit - .module('./node_modules/my-addon/_app_/hello-world.js') + .module('./node_modules/my-addon/_app_/hello-world.js__embroider_appjs_match__.js') .resolves('../../extra.js') .to('./node_modules/extra.js'); }); @@ -547,7 +547,7 @@ Scenarios.fromProject(() => new Project()) expectAudit .module('./app.js') .resolves('my-app/templates/hello-world') - .to('./node_modules/my-addon/_app_/templates/hello-world.hbs'); + .to('./node_modules/my-addon/_app_/templates/hello-world.hbs__embroider_appjs_match__.hbs'); }); test(`relative import in addon's app tree resolves to app`, async function () { @@ -564,7 +564,7 @@ Scenarios.fromProject(() => new Project()) }); expectAudit - .module('./node_modules/my-addon/_app_/hello-world.js') + .module('./node_modules/my-addon/_app_/hello-world.js__embroider_appjs_match__.js') .resolves('./secondary') .to('./secondary.js'); }); @@ -584,7 +584,7 @@ Scenarios.fromProject(() => new Project()) }); expectAudit - .module('./node_modules/my-addon/_app_/hello-world.js') + .module('./node_modules/my-addon/_app_/hello-world.js__embroider_appjs_match__.js') .resolves('./secondary') .to('./secondary.js'); }); @@ -602,7 +602,7 @@ Scenarios.fromProject(() => new Project()) }); expectAudit - .module('./node_modules/my-addon/_app_/hello-world.js') + .module('./node_modules/my-addon/_app_/hello-world.js__embroider_appjs_match__.js') .resolves('the-apps-dep') .to('./node_modules/the-apps-dep/index.js'); }); @@ -621,7 +621,7 @@ Scenarios.fromProject(() => new Project()) }); expectAudit - .module('./node_modules/my-addon/_app_/hello-world.js') + .module('./node_modules/my-addon/_app_/hello-world.js__embroider_appjs_match__.js') .resolves('my-app/secondary') .to('./secondary.js'); }); diff --git a/tests/scenarios/vite-internals-test.ts b/tests/scenarios/vite-internals-test.ts index 60cb58542..4bb4204a0 100644 --- a/tests/scenarios/vite-internals-test.ts +++ b/tests/scenarios/vite-internals-test.ts @@ -219,20 +219,33 @@ appScenarios test(`dep optimization of a v2 addon`, async function (assert) { expectAudit - .module('./index.html') - .resolves(/\/index.html.*/) // in-html app-boot script - .toModule() - .resolves(/\/app\.js.*/) + .module(/\/app\.js.*/) + .resolves(/.*\/-embroider-entrypoint.js/) .toModule() + .withContents((_src, imports) => { + let pageTitleImports = imports.filter(imp => /page-title/.test(imp.source)); + assert.ok(pageTitleImports.length > 0, 'should not have at least one import from page-title'); + for (let pageTitleImport of pageTitleImports) { + assert.notOk( + /\.vite\/deps/.test(pageTitleImport.source), + `expected ${pageTitleImport.source} not to be in vite deps` + ); + } + return true; + }); + expectAudit + .module(/\/app\.js.*/) .resolves(/.*\/-embroider-entrypoint.js/) .toModule() + .resolves(/page-title/) + .toModule() .withContents((_src, imports) => { let pageTitleImports = imports.filter(imp => /page-title/.test(imp.source)); - assert.ok(pageTitleImports.length > 0, 'should have at least one import from page-title'); + assert.ok(pageTitleImports.length > 0, 'should not have at least one import from page-title'); for (let pageTitleImport of pageTitleImports) { - assert.ok( + assert.notOk( /\.vite\/deps/.test(pageTitleImport.source), - `expected ${pageTitleImport.source} to be in vite deps` + `expected ${pageTitleImport.source} not to be in vite deps` ); } return true;