diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ba9e22595..05ae3099b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,7 +16,7 @@ export { Resolver } from './module-resolver'; export { ModuleRequest, type Resolution, type RequestAdapter, type RequestAdapterCreate } from './module-request'; export type { Options as ResolverOptions } from './module-resolver-options'; export { ResolverLoader } from './resolver-loader'; -export { virtualContent } from './virtual-content'; +export { virtualContent, type VirtualResponse } from './virtual-content'; export type { Engine } from './app-files'; // this is reexported because we already make users manage a peerDep from some diff --git a/packages/core/src/module-request.ts b/packages/core/src/module-request.ts index 4d67d1fd6..c439dcbbd 100644 --- a/packages/core/src/module-request.ts +++ b/packages/core/src/module-request.ts @@ -1,7 +1,9 @@ +import type { VirtualResponse } from './virtual-content'; + // This is generic because different build systems have different ways of // representing a found module, and we just pass those values through. export type Resolution = - | { type: 'found'; filename: string; isVirtual: boolean; result: T } + | { type: 'found'; filename: string; virtual: VirtualResponse | false; result: T } // the important thing about this Resolution is that embroider should do its // fallback behaviors here. @@ -19,7 +21,7 @@ export interface RequestAdapter { // plugins are a pain in the butt. Integrators are encouraged to use the plain // Response-returning variants in all sane build environments. notFoundResponse(request: ModuleRequest): Res | (() => Promise); - virtualResponse(request: ModuleRequest, virtualFileName: string): Res | (() => Promise); + virtualResponse(request: ModuleRequest, response: VirtualResponse): Res | (() => Promise); } export interface InitialRequestState { @@ -90,8 +92,8 @@ export class ModuleRequest implements Modul return result; } - virtualize(virtualFileName: string): this { - return this.resolveTo(this.#adapter.virtualResponse(this, virtualFileName)); + virtualize(virtualResponse: VirtualResponse): this { + return this.resolveTo(this.#adapter.virtualResponse(this, virtualResponse)); } withMeta(meta: Record | undefined): this { diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index 750980b76..2c57e556f 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -22,6 +22,7 @@ import { decodePublicRouteEntrypoint, encodeRouteEntrypoint } from './virtual-ro import type { Options, EngineConfig } from './module-resolver-options'; import { satisfies } from 'semver'; import type { ModuleRequest, Resolution } from './module-request'; +import { virtualEntrypoint } from './virtual-entrypoint'; const debug = makeDebug('embroider:resolver'); @@ -252,7 +253,11 @@ export class Resolver { if (switchFile === request.fromFile) { return logTransition('internal lookup from fastbootSwitch', request); } else { - return logTransition('shadowed app fastboot', request, request.virtualize(switchFile)); + return logTransition( + 'shadowed app fastboot', + request, + request.virtualize({ type: 'fastboot-switch', specifier: switchFile }) + ); } } else { return logTransition( @@ -347,13 +352,13 @@ export class Resolver { return logTransition( `dep's implicit modules`, request, - request.virtualize(resolve(dep.root, `-embroider-${im.type}.js`)) + request.virtualize({ type: im.type, specifier: resolve(dep.root, `-embroider-${im.type}.js`) }) ); } else { return logTransition( `own implicit modules`, request, - request.virtualize(resolve(pkg.root, `-embroider-${im.type}.js`)) + request.virtualize({ type: im.type, specifier: resolve(pkg.root, `-embroider-${im.type}.js`) }) ); } } @@ -363,48 +368,12 @@ export class Resolver { return request; } - //TODO move the extra forwardslash handling out into the vite plugin - const candidates = [ - '@embroider/virtual/compat-modules', - '/@embroider/virtual/compat-modules', - './@embroider/virtual/compat-modules', - ]; - - if (!candidates.some(c => request.specifier.startsWith(c + '/') || request.specifier === c)) { - return request; - } - - const result = /\.?\/?@embroider\/virtual\/compat-modules(?:\/(?.*))?/.exec(request.specifier); - - if (!result) { - // TODO make a better error - throw new Error('entrypoint does not match pattern' + request.specifier); - } - - const { packageName } = result.groups!; - - const requestingPkg = this.packageCache.ownerOfFile(request.fromFile); - - if (!requestingPkg?.isV2Ember()) { - throw new Error(`bug: found entrypoint import in non-ember package at ${request.fromFile}`); - } - - let pkg; - - if (packageName) { - pkg = this.packageCache.resolve(packageName, requestingPkg); + let virtualResponse = virtualEntrypoint(request, this.packageCache); + if (virtualResponse) { + return logTransition('entrypoint', request, request.virtualize(virtualResponse)); } else { - pkg = requestingPkg; + return request; } - let matched = resolveExports(pkg.packageJSON, '-embroider-entrypoint.js', { - browser: true, - conditions: ['default', 'imports'], - }); - return logTransition( - 'entrypoint', - request, - request.virtualize(resolve(pkg.root, matched?.[0] ?? '-embroider-entrypoint.js')) - ); } private handleRouteEntrypoint(request: R): R { @@ -424,15 +393,10 @@ export class Resolver { throw new Error(`bug: found entrypoint import in non-ember package at ${request.fromFile}`); } - let matched = resolveExports(pkg.packageJSON, '-embroider-route-entrypoint.js', { - browser: true, - conditions: ['default', 'imports'], - }); - return logTransition( 'route entrypoint', request, - request.virtualize(encodeRouteEntrypoint(pkg.root, matched?.[0], routeName)) + request.virtualize({ type: 'route-entrypoint', specifier: encodeRouteEntrypoint(pkg, routeName) }) ); } @@ -455,7 +419,11 @@ export class Resolver { ); } - return logTransition('test-support', request, request.virtualize(resolve(pkg.root, '-embroider-test-support.js'))); + return logTransition( + 'test-support', + request, + request.virtualize({ type: 'test-support-js', specifier: resolve(pkg.root, '-embroider-test-support.js') }) + ); } private handleTestSupportStyles(request: R): R { @@ -480,7 +448,10 @@ export class Resolver { return logTransition( 'test-support-styles', request, - request.virtualize(resolve(pkg.root, '-embroider-test-support-styles.css')) + request.virtualize({ + type: 'test-support-css', + specifier: resolve(pkg.root, '-embroider-test-support-styles.css'), + }) ); } @@ -535,7 +506,7 @@ export class Resolver { return logTransition( 'vendor-styles', request, - request.virtualize(resolve(pkg.root, '-embroider-vendor-styles.css')) + request.virtualize({ type: 'vendor-css', specifier: resolve(pkg.root, '-embroider-vendor-styles.css') }) ); } @@ -562,11 +533,7 @@ export class Resolver { for (let candidate of this.componentTemplateCandidates(target.packageName)) { let candidateSpecifier = `${target.packageName}${candidate.prefix}${target.memberName}${candidate.suffix}`; - let resolution = await this.resolve( - request.alias(candidateSpecifier).rehome(target.from).withMeta({ - runtimeFallback: false, - }) - ); + let resolution = await this.resolve(request.alias(candidateSpecifier).rehome(target.from)); if (resolution.type === 'found') { hbsModule = resolution; @@ -578,11 +545,7 @@ export class Resolver { for (let candidate of this.componentJSCandidates(target.packageName)) { let candidateSpecifier = `${target.packageName}${candidate.prefix}${target.memberName}${candidate.suffix}`; - let resolution = await this.resolve( - request.alias(candidateSpecifier).rehome(target.from).withMeta({ - runtimeFallback: false, - }) - ); + let resolution = await this.resolve(request.alias(candidateSpecifier).rehome(target.from)); // .hbs is a resolvable extension for us, so we need to exclude it here. // It matches as a priority lower than .js, so finding an .hbs means @@ -602,7 +565,10 @@ export class Resolver { return logTransition( `resolveComponent found legacy HBS`, request, - request.virtualize(virtualPairComponent(hbsModule.filename, jsModule?.filename)) + request.virtualize({ + type: 'component-pair', + specifier: virtualPairComponent(this.options.appRoot, hbsModule.filename, jsModule?.filename), + }) ); } else if (jsModule) { return logTransition(`resolving to resolveComponent found only JS`, request, request.resolveTo(jsModule)); @@ -620,11 +586,7 @@ export class Resolver { // component, so here to resolve the ambiguity we need to actually resolve // that candidate to see if it works. let helperCandidate = this.resolveHelper(path, inEngine, request); - let helperMatch = await this.resolve( - request.alias(helperCandidate.specifier).rehome(helperCandidate.fromFile).withMeta({ - runtimeFallback: false, - }) - ); + let helperMatch = await this.resolve(request.alias(helperCandidate.specifier).rehome(helperCandidate.fromFile)); if (helperMatch.type === 'found') { return logTransition('resolve to ambiguous case matched a helper', request, request.resolveTo(helperMatch)); @@ -1014,7 +976,11 @@ export class Resolver { ); } - return logTransition('vendor', request, request.virtualize(resolve(pkg.root, '-embroider-vendor.js'))); + return logTransition( + 'vendor', + request, + request.virtualize({ type: 'vendor-js', specifier: resolve(pkg.root, '-embroider-vendor.js') }) + ); } private resolveWithinMovedPackage(request: R, pkg: Package): R { @@ -1331,9 +1297,7 @@ export class Resolver { return request.alias(matched.entry['fastboot-js'].specifier).rehome(matched.entry['fastboot-js'].fromFile); case 'both': let foundAppJS = await this.resolve( - request.alias(matched.entry['app-js'].specifier).rehome(matched.entry['app-js'].fromFile).withMeta({ - runtimeFallback: false, - }) + request.alias(matched.entry['app-js'].specifier).rehome(matched.entry['app-js'].fromFile) ); if (foundAppJS.type !== 'found') { throw new Error( @@ -1341,7 +1305,10 @@ export class Resolver { ); } let { names } = describeExports(readFileSync(foundAppJS.filename, 'utf8'), { configFile: false }); - return request.virtualize(fastbootSwitch(matched.matched, resolve(engine.root, 'package.json'), names)); + return request.virtualize({ + type: 'fastboot-switch', + specifier: fastbootSwitch(matched.matched, resolve(engine.root, 'package.json'), names), + }); } } diff --git a/packages/core/src/node-resolve.ts b/packages/core/src/node-resolve.ts index 5b6a7fbaa..f69609663 100644 --- a/packages/core/src/node-resolve.ts +++ b/packages/core/src/node-resolve.ts @@ -1,4 +1,4 @@ -import { virtualContent } from './virtual-content'; +import { virtualContent, type VirtualResponse } from './virtual-content'; import { dirname, resolve, isAbsolute } from 'path'; import { explicitRelative } from '@embroider/shared-internals'; import assertNever from 'assert-never'; @@ -39,16 +39,16 @@ export class NodeRequestAdapter implements RequestAdapter>, - virtualFileName: string + virtual: VirtualResponse ): Resolution { return { type: 'found', - filename: virtualFileName, - isVirtual: true, + filename: virtual.specifier, + virtual, result: { - type: 'virtual' as 'virtual', - content: virtualContent(virtualFileName, this.resolver).src, - filename: virtualFileName, + type: 'virtual' as const, + content: virtualContent(virtual.specifier, this.resolver).src, + filename: virtual.specifier, }, }; } @@ -92,7 +92,7 @@ export class NodeRequestAdapter implements RequestAdapter string; -const pairComponentMarker = '-embroider-pair-component'; -const pairComponentPattern = /^(?.*)__vpc__(?[^\/]*)-embroider-pair-component$/; +const pairComponentMarker = '/embroider-pair-component/'; +const pairComponentPattern = /\/embroider-pair-component\/(?[^\/]*)\/__vpc__\/(?[^\/]*)$/; -export function virtualPairComponent(hbsModule: string, jsModule: string | undefined): string { - let relativeJSModule = ''; - if (jsModule) { - relativeJSModule = explicitRelative(dirname(hbsModule), jsModule); - } - return `${hbsModule}__vpc__${encodeURIComponent(relativeJSModule)}${pairComponentMarker}`; +export function virtualPairComponent(appRoot: string, hbsModule: string, jsModule: string | undefined): string { + return `${appRoot}/embroider-pair-component/${encodeURIComponent(hbsModule)}/__vpc__/${encodeURIComponent( + jsModule ?? '' + )}`; } function decodeVirtualPairComponent( filename: string -): { relativeHBSModule: string; relativeJSModule: string | null; debugName: string } | null { +): { hbsModule: string; jsModule: string | null; debugName: string } | null { // Performance: avoid paying regex exec cost unless needed if (!filename.includes(pairComponentMarker)) { return null; @@ -132,12 +151,10 @@ function decodeVirtualPairComponent( return null; } 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); return { - relativeHBSModule, - relativeJSModule: decodeURIComponent(jsModule) || null, - debugName: basename(relativeHBSModule).replace(/\.(js|hbs)$/, ''), + hbsModule: decodeURIComponent(hbsModule), + jsModule: jsModule ? decodeURIComponent(jsModule) : null, + debugName: basename(decodeURIComponent(hbsModule)).replace(/\.(js|hbs)$/, ''), }; } diff --git a/packages/core/src/virtual-entrypoint.ts b/packages/core/src/virtual-entrypoint.ts index 69689af90..cd5298033 100644 --- a/packages/core/src/virtual-entrypoint.ts +++ b/packages/core/src/virtual-entrypoint.ts @@ -3,16 +3,64 @@ import { compile } from './js-handlebars'; import type { Resolver } from './module-resolver'; import type { CompatResolverOptions } from '../../compat/src/resolver-transform'; import { flatten, partition } from 'lodash'; -import { join } from 'path'; -import { extensionsPattern } from '@embroider/shared-internals'; +import { join, resolve } from 'path'; +import { extensionsPattern, type PackageCachePublicAPI, type Package } from '@embroider/shared-internals'; import walkSync from 'walk-sync'; import type { V2AddonPackage } from '@embroider/shared-internals/src/package'; import { encodePublicRouteEntrypoint } from './virtual-route-entrypoint'; import escapeRegExp from 'escape-string-regexp'; import { optionsWithDefaults } from './options'; +import { type ModuleRequest } from './module-request'; +import { exports as resolveExports } from 'resolve.exports'; +import { type VirtualResponse } from './virtual-content'; const entrypointPattern = /(?.*)[\\/]-embroider-entrypoint.js/; +export function virtualEntrypoint( + request: ModuleRequest, + packageCache: PackageCachePublicAPI +): VirtualResponse | undefined { + //TODO move the extra forwardslash handling out into the vite plugin + const candidates = [ + '@embroider/virtual/compat-modules', + '/@embroider/virtual/compat-modules', + './@embroider/virtual/compat-modules', + ]; + + if (!candidates.some(c => request.specifier.startsWith(c + '/') || request.specifier === c)) { + return undefined; + } + + const result = /\.?\/?@embroider\/virtual\/compat-modules(?:\/(?.*))?/.exec(request.specifier); + + if (!result) { + throw new Error('bug: entrypoint does not match pattern' + request.specifier); + } + + const { packageName } = result.groups!; + + const requestingPkg = packageCache.ownerOfFile(request.fromFile); + + if (!requestingPkg?.isV2Ember()) { + throw new Error(`bug: found entrypoint import in non-ember package at ${request.fromFile}`); + } + let pkg: Package; + + if (packageName) { + pkg = packageCache.resolve(packageName, requestingPkg); + } else { + pkg = requestingPkg; + } + let matched = resolveExports(pkg.packageJSON, '-embroider-entrypoint.js', { + browser: true, + conditions: ['default', 'imports'], + }); + return { + type: 'entrypoint', + specifier: resolve(pkg.root, matched?.[0] ?? '-embroider-entrypoint.js'), + }; +} + export function decodeEntrypoint(filename: string): { fromDir: string } | undefined { // Performance: avoid paying regex exec cost unless needed if (!filename.includes('-embroider-entrypoint')) { diff --git a/packages/core/src/virtual-route-entrypoint.ts b/packages/core/src/virtual-route-entrypoint.ts index 603b326db..a73084aaf 100644 --- a/packages/core/src/virtual-route-entrypoint.ts +++ b/packages/core/src/virtual-route-entrypoint.ts @@ -3,17 +3,20 @@ import { AppFiles } from './app-files'; import type { Resolver } from './module-resolver'; import { resolve } from 'path'; import { compile } from './js-handlebars'; -import { extensionsPattern } from '@embroider/shared-internals'; +import { extensionsPattern, type Package } from '@embroider/shared-internals'; import { partition } from 'lodash'; import { getAppFiles, getFastbootFiles, importPaths, splitRoute, staticAppPathsPattern } from './virtual-entrypoint'; +import { exports as resolveExports } from 'resolve.exports'; const entrypointPattern = /(?.*)[\\/]-embroider-route-entrypoint.js:route=(?.*)/; -export function encodeRouteEntrypoint(packagePath: string, matched: string | undefined, routeName: string): string { - return resolve( - packagePath, - matched ? `${matched}:route=${routeName}` : `-embroider-route-entrypoint.js:route=${routeName}` - ); +export function encodeRouteEntrypoint(pkg: Package, routeName: string): string { + let matched = resolveExports(pkg.packageJSON, '-embroider-route-entrypoint.js', { + browser: true, + conditions: ['default', 'imports'], + }); + let target = matched ? `${matched}:route=${routeName}` : `-embroider-route-entrypoint.js:route=${routeName}`; + return resolve(pkg.root, target); } export function decodeRouteEntrypoint(filename: string): { fromDir: string; route: string } | undefined { diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index 47326abf0..fcd9b966e 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -9,7 +9,7 @@ export { } from './paths'; export { getOrCreate } from './get-or-create'; export { default as Package, V2AddonPackage as AddonPackage, V2AppPackage as AppPackage, V2Package } from './package'; -export { default as PackageCache } from './package-cache'; +export { default as PackageCache, type PackageCachePublicAPI } from './package-cache'; export type { RewrittenPackageIndex } from './rewritten-package-cache'; export { RewrittenPackageCache } from './rewritten-package-cache'; export { default as packageName } from './package-name'; diff --git a/packages/shared-internals/src/package-cache.ts b/packages/shared-internals/src/package-cache.ts index 6e1c467a8..73d77de18 100644 --- a/packages/shared-internals/src/package-cache.ts +++ b/packages/shared-internals/src/package-cache.ts @@ -68,11 +68,6 @@ export default class PackageCache { ownerOfFile(filename: string): Package | undefined { let candidate = filename; - const virtualPrefix = 'embroider_virtual:'; - - if (candidate.includes(virtualPrefix)) { - candidate = candidate.replace(/^.*embroider_virtual:/, ''); - } // first we look through our cached packages for any that are rooted right // at or above the file. @@ -111,3 +106,9 @@ export default class PackageCache { } const shared: Map = new Map(); + +// without this, using a class as an interface forces you to have the same +// private and protected methods too (since people trying to extend from you +// could see all of those) +type PublicAPI = { [K in keyof T]: T[K] }; +export type PackageCachePublicAPI = PublicAPI; diff --git a/packages/shared-internals/src/rewritten-package-cache.ts b/packages/shared-internals/src/rewritten-package-cache.ts index aa7d38af4..914111029 100644 --- a/packages/shared-internals/src/rewritten-package-cache.ts +++ b/packages/shared-internals/src/rewritten-package-cache.ts @@ -1,4 +1,4 @@ -import PackageCache from './package-cache'; +import PackageCache, { type PackageCachePublicAPI } from './package-cache'; import type { V2AddonPackage, V2AppPackage, V2Package } from './package'; import Package from './package'; import { existsSync, readJSONSync, realpathSync } from 'fs-extra'; @@ -25,12 +25,9 @@ export interface RewrittenPackageIndex { extraResolutions: Record; } -// without this, using a class as an interface forces you to have the same -// private and protected methods too (since people trying to extend from you -// could see all of those) type PublicAPI = { [K in keyof T]: T[K] }; -export class RewrittenPackageCache implements PublicAPI { +export class RewrittenPackageCache implements PackageCachePublicAPI { constructor(private plainCache: PackageCache) {} get appRoot(): string { diff --git a/packages/vite/src/esbuild-request.ts b/packages/vite/src/esbuild-request.ts index 41a39dd34..cfe49fc56 100644 --- a/packages/vite/src/esbuild-request.ts +++ b/packages/vite/src/esbuild-request.ts @@ -3,12 +3,15 @@ const { cleanUrl, packageName } = core; import type { ImportKind, OnResolveResult, PluginBuild } from 'esbuild'; import { dirname } from 'path'; -import type { PackageCache as _PackageCache, Resolution, ModuleRequest, RequestAdapter } from '@embroider/core'; +import type { + PackageCachePublicAPI as PackageCache, + Resolution, + ModuleRequest, + RequestAdapter, + VirtualResponse, +} from '@embroider/core'; import { externalName } from '@embroider/reverse-exports'; -type PublicAPI = { [K in keyof T]: T[K] }; -type PackageCache = PublicAPI<_PackageCache>; - export class EsBuildRequestAdapter implements RequestAdapter> { static create({ packageCache, @@ -56,7 +59,7 @@ export class EsBuildRequestAdapter implements RequestAdapter> + request: ModuleRequest> ): Resolution { return { type: 'not_found', @@ -67,14 +70,14 @@ export class EsBuildRequestAdapter implements RequestAdapter>, - virtualFileName: string + _request: ModuleRequest>, + virtual: VirtualResponse ): Resolution { return { type: 'found', - filename: virtualFileName, - result: { path: virtualFileName, namespace: 'embroider-virtual' }, - isVirtual: true, + filename: virtual.specifier, + result: { path: virtual.specifier, namespace: 'embroider-virtual' }, + virtual, }; } @@ -133,7 +136,7 @@ export class EsBuildRequestAdapter implements RequestAdapter | undefined; } +export interface ResponseMeta { + virtual: VirtualResponse; +} + export class RollupRequestAdapter implements RequestAdapter> { static create: RequestAdapterCreate> = ({ context, @@ -23,16 +26,9 @@ export class RollupRequestAdapter implements RequestAdapter>, - virtualFileName: string + virtual: VirtualResponse ): Resolution { - let specifier = virtualPrefix + virtualFileName; return { type: 'found', - filename: specifier, + filename: virtual.specifier, result: { - id: this.specifierWithQueryParams(specifier), + id: this.specifierWithQueryParams(virtual.specifier), resolvedBy: this.fromFileWithQueryParams(request.fromFile), + meta: { + 'embroider-resolver': { virtual } satisfies ResponseMeta, + }, }, - isVirtual: true, + virtual, }; } @@ -103,7 +101,7 @@ export class RollupRequestAdapter implements RequestAdapter = new Map(); const notViteDeps = new Set(); + const responseMetas: Map = new Map(); + + async function resolveId( + context: PluginContext, + source: string, + importer: string | undefined, + options: { custom?: Record } + ) { + if (options.custom?.depScan) { + return await observeDepScan(context, source, importer, options); + } + + let request = ModuleRequest.create(RollupRequestAdapter.create, { + context, + source, + importer, + custom: options.custom, + }); + if (!request) { + // fallthrough to other rollup plugins + return null; + } + let resolution = await resolverLoader.resolver.resolve(request); + switch (resolution.type) { + case 'found': + if (resolution.virtual) { + return resolution.result; + } else { + return await maybeCaptureNewOptimizedDep(context, resolverLoader.resolver, resolution.result, notViteDeps); + } + case 'not_found': + return null; + default: + throw assertNever(resolution); + } + } + + async function ensureResolve(context: PluginContext, specifier: string): Promise { + let result = await resolveId( + context, + specifier, + resolve(resolverLoader.resolver.options.appRoot, 'package.json'), + {} + ); + if (!result) { + throw new Error(`bug: expected to resolve ${specifier}`); + } + if (typeof result === 'string') { + return result; + } + return result.id; + } + + async function emitVirtualFile(context: PluginContext, fileName: string): Promise { + context.emitFile({ + type: 'asset', + fileName, + source: virtualContent(await ensureResolve(context, fileName), resolverLoader.resolver).src, + }); + } let mode = ''; @@ -64,77 +124,32 @@ export function resolver(): Plugin { }, async resolveId(source, importer, options) { - if (options.custom?.depScan) { - return await observeDepScan(this, source, importer, options); - } - - let request = ModuleRequest.create(RollupRequestAdapter.create, { - context: this, - source, - importer, - custom: options.custom, - }); - if (!request) { - // fallthrough to other rollup plugins - return null; + let resolution = await resolveId(this, source, importer, options); + if (typeof resolution === 'string') { + return resolution; } - let resolution = await resolverLoader.resolver.resolve(request); - switch (resolution.type) { - case 'found': - if (resolution.isVirtual) { - return resolution.result; - } else { - return await maybeCaptureNewOptimizedDep(this, resolverLoader.resolver, resolution.result, notViteDeps); - } - case 'not_found': - return null; - default: - throw assertNever(resolution); + if (resolution && resolution.meta?.['embroider-resolver']) { + responseMetas.set(normalizePath(resolution.id), resolution.meta['embroider-resolver'] as ResponseMeta); } + return resolution; }, + load(id) { - if (id.startsWith(virtualPrefix)) { - let { pathname } = new URL(id, 'http://example.com'); - let { src, watches } = virtualContent(pathname.slice(virtualPrefix.length + 1), resolverLoader.resolver); + let meta = responseMetas.get(normalizePath(id)); + if (meta?.virtual) { + let { src, watches } = virtualContent(cleanUrl(id), resolverLoader.resolver); virtualDeps.set(id, watches); server?.watcher.add(watches); return src; } }, - buildEnd() { - this.emitFile({ - type: 'asset', - fileName: '@embroider/virtual/vendor.js', - source: virtualContent( - resolve(resolverLoader.resolver.options.engines[0].root, '-embroider-vendor.js'), - resolverLoader.resolver - ).src, - }); - this.emitFile({ - type: 'asset', - fileName: '@embroider/virtual/vendor.css', - source: virtualContent( - resolve(resolverLoader.resolver.options.engines[0].root, '-embroider-vendor-styles.css'), - resolverLoader.resolver - ).src, - }); + async buildEnd() { + emitVirtualFile(this, '@embroider/virtual/vendor.js'); + emitVirtualFile(this, '@embroider/virtual/vendor.css'); + if (mode !== 'production') { - this.emitFile({ - type: 'asset', - fileName: '@embroider/virtual/test-support.js', - source: virtualContent( - resolve(resolverLoader.resolver.options.engines[0].root, '-embroider-test-support.js'), - resolverLoader.resolver - ).src, - }); - this.emitFile({ - type: 'asset', - fileName: '@embroider/virtual/test-support.css', - source: virtualContent( - resolve(resolverLoader.resolver.options.engines[0].root, '-embroider-test-support-styles.css'), - resolverLoader.resolver - ).src, - }); + emitVirtualFile(this, '@embroider/virtual/test-support.js'); + emitVirtualFile(this, '@embroider/virtual/test-support.css'); } }, }; diff --git a/packages/webpack/src/webpack-resolver-plugin.ts b/packages/webpack/src/webpack-resolver-plugin.ts index 1471b4457..24d6b5c45 100644 --- a/packages/webpack/src/webpack-resolver-plugin.ts +++ b/packages/webpack/src/webpack-resolver-plugin.ts @@ -1,5 +1,5 @@ import { dirname, resolve } from 'path'; -import { ModuleRequest, type RequestAdapter, type Resolution } from '@embroider/core'; +import { ModuleRequest, type VirtualResponse, type RequestAdapter, type Resolution } from '@embroider/core'; import { Resolver as EmbroiderResolver, ResolverOptions as EmbroiderResolverOptions } from '@embroider/core'; import type { Compiler, Module, ResolveData } from 'webpack'; import assertNever from 'assert-never'; @@ -213,36 +213,39 @@ class WebpackRequestAdapter implements RequestAdapter { virtualResponse( request: ModuleRequest, - virtualFileName: string + virtual: VirtualResponse ): () => Promise { return () => { - return this._resolve(request, virtualFileName); + return this._resolve(request, virtual); }; } async resolve(request: ModuleRequest): Promise { - return this._resolve(request, undefined); + return this._resolve(request, false); } async _resolve( request: ModuleRequest, - virtualFileName: string | undefined + virtualResponse: VirtualResponse | false ): Promise { return await new Promise(resolve => - this.resolveFunction(this.toWebpackResolveData(request, virtualFileName), err => { - if (err) { - // unfortunately webpack doesn't let us distinguish between Not Found - // and other unexpected exceptions here. - resolve({ type: 'not_found', err }); - } else { - resolve({ - type: 'found', - result: this.originalState.createData, - isVirtual: Boolean(virtualFileName), - filename: this.originalState.createData.resource!, - }); + this.resolveFunction( + this.toWebpackResolveData(request, virtualResponse ? virtualResponse.specifier : request.specifier), + err => { + if (err) { + // unfortunately webpack doesn't let us distinguish between Not Found + // and other unexpected exceptions here. + resolve({ type: 'not_found', err }); + } else { + resolve({ + type: 'found', + result: this.originalState.createData, + virtual: virtualResponse, + filename: this.originalState.createData.resource!, + }); + } } - }) + ) ); } } diff --git a/tests/scenarios/compat-route-split-test.ts b/tests/scenarios/compat-route-split-test.ts index 77a299ec3..af34bde18 100644 --- a/tests/scenarios/compat-route-split-test.ts +++ b/tests/scenarios/compat-route-split-test.ts @@ -226,43 +226,40 @@ splitScenarios }); test('dynamically imports the route entrypoint from the main entrypoint', function () { - inEntrypoint(/import\("\/@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + inEntrypoint(/import\("\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has split controllers in route entrypoint', function () { inEntrypoint( ['app/controllers/people', 'app/controllers/people/show'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has split route templates in route entrypoint', function () { inEntrypoint( ['app/templates/people', 'app/templates/people/index', 'app/templates/people/show'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has split routes in route entrypoint', function () { inEntrypoint( ['app/routes/people', 'app/routes/people/show'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has no components in route entrypoint', function () { - notInEntrypoint( - ['all-people', 'welcome', 'unused'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ - ); + notInEntrypoint(['all-people', 'welcome', 'unused'], /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no helpers in route entrypoint', function () { - notInEntrypoint('capitalize', /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + notInEntrypoint('capitalize', /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no helpers in route entrypoint', function () { - notInEntrypoint('auto-focus', /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + notInEntrypoint('auto-focus', /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no issues', function () { @@ -400,43 +397,40 @@ splitScenarios }); test('dynamically imports the route entrypoint from the main entrypoint', function () { - inEntrypoint(/import\("\/@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people\?import"\)/); + inEntrypoint(/import\("\/app\/-embroider-route-entrypoint.js:route=people\?import"\)/); }); test('has split controllers in route entrypoint', function () { inEntrypoint( ['pods/people/controller', 'pods/people/show/controller'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has split route templates in route entrypoint', function () { inEntrypoint( ['pods/people/template', 'pods/people/index/template', 'pods/people/show/template'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has split routes in route entrypoint', function () { inEntrypoint( ['pods/people/route', 'pods/people/show/route'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has no components in route entrypoint', function () { - notInEntrypoint( - ['all-people', 'welcome', 'unused'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ - ); + notInEntrypoint(['all-people', 'welcome', 'unused'], /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no helpers in route entrypoint', function () { - notInEntrypoint('capitalize', /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + notInEntrypoint('capitalize', /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no modifiers in route entrypoint', function () { - notInEntrypoint('auto-focus', /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + notInEntrypoint('auto-focus', /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no issues', function () { @@ -574,43 +568,40 @@ splitScenarios }); test('dynamically imports the route entrypoint from the main entrypoint', function () { - inEntrypoint(/import\("\/@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people\?import"\)/); + inEntrypoint(/import\("\/app\/-embroider-route-entrypoint.js:route=people\?import"\)/); }); test('has split controllers in route entrypoint', function () { inEntrypoint( ['routes/people/controller', 'routes/people/show/controller'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has split route templates in route entrypoint', function () { inEntrypoint( ['routes/people/template', 'routes/people/index/template', 'routes/people/show/template'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has split routes in route entrypoint', function () { inEntrypoint( ['routes/people/route', 'routes/people/show/route'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ + /\/app\/-embroider-route-entrypoint.js:route=people/ ); }); test('has no components in route entrypoint', function () { - notInEntrypoint( - ['all-people', 'welcome', 'unused'], - /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/ - ); + notInEntrypoint(['all-people', 'welcome', 'unused'], /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no helpers in route entrypoint', function () { - notInEntrypoint('capitalize', /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + notInEntrypoint('capitalize', /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no modifiers in route entrypoint', function () { - notInEntrypoint('auto-focus', /@id\/embroider_virtual:.*-embroider-route-entrypoint.js:route=people/); + notInEntrypoint('auto-focus', /\/app\/-embroider-route-entrypoint.js:route=people/); }); test('has no issues', function () { diff --git a/tests/scenarios/core-resolver-test.ts b/tests/scenarios/core-resolver-test.ts index 5e1970481..e270c89a3 100644 --- a/tests/scenarios/core-resolver-test.ts +++ b/tests/scenarios/core-resolver-test.ts @@ -240,9 +240,12 @@ Scenarios.fromProject(() => new Project()) .resolves('@embroider/virtual/components/hello-world') .toModule(); + let templateFile = normalizePath(`${app.dir}/templates/components/hello-world.hbs`); + let componentFile = normalizePath(`${app.dir}/components/hello-world.js`); + pairModule.codeEquals(` import { setComponentTemplate } from "@ember/component"; - import template from "./hello-world.hbs"; + import template from "${esc(templateFile)}"; import { deprecate } from "@ember/debug"; true && !false && deprecate( "Components with separately resolved templates are deprecated. Migrate to either co-located js/ts + hbs files or to gjs/gts. Tried to lookup 'hello-world'.", @@ -258,12 +261,12 @@ Scenarios.fromProject(() => new Project()) }, } ); - import component from "../../components/hello-world.js"; + import component from "${esc(componentFile)}"; export default setComponentTemplate(template, component); `); - pairModule.resolves('./hello-world.hbs').to('./templates/components/hello-world.hbs'); - pairModule.resolves('../../components/hello-world.js').to('./components/hello-world.js'); + pairModule.resolves(templateFile).to('./templates/components/hello-world.hbs'); + pairModule.resolves(componentFile).to('./components/hello-world.js'); }); test('hbs-only component', async function () { @@ -279,9 +282,10 @@ Scenarios.fromProject(() => new Project()) .resolves('@embroider/virtual/components/hello-world') .toModule(); + let templateFile = normalizePath(`${app.dir}/templates/components/hello-world.hbs`); pairModule.codeEquals(` import { setComponentTemplate } from "@ember/component"; - import template from "./hello-world.hbs"; + import template from "${esc(templateFile)}"; import { deprecate } from "@ember/debug"; true && !false && deprecate( "Components with separately resolved templates are deprecated. Migrate to either co-located js/ts + hbs files or to gjs/gts. Tried to lookup 'hello-world'.", @@ -301,7 +305,7 @@ Scenarios.fromProject(() => new Project()) export default setComponentTemplate(template, templateOnlyComponent(undefined, "hello-world")); `); - pairModule.resolves('./hello-world.hbs').to('./templates/components/hello-world.hbs'); + pairModule.resolves(templateFile).to('./templates/components/hello-world.hbs'); }); test('explicitly namedspaced component', async function () { @@ -392,9 +396,11 @@ Scenarios.fromProject(() => new Project()) .resolves('@embroider/virtual/components/hello-world') .toModule(); + let templateFile = normalizePath(`${app.dir}/components/hello-world/template.hbs`); + pairModule.codeEquals(` import { setComponentTemplate } from "@ember/component"; - import template from "./template.hbs"; + import template from "${esc(templateFile)}"; import { deprecate } from "@ember/debug"; true && !false && deprecate( "Components with separately resolved templates are deprecated. Migrate to either co-located js/ts + hbs files or to gjs/gts. Tried to lookup 'template'.", @@ -414,7 +420,7 @@ Scenarios.fromProject(() => new Project()) export default setComponentTemplate(template, templateOnlyComponent(undefined, "template")); `); - pairModule.resolves('./template.hbs').to('./components/hello-world/template.hbs'); + pairModule.resolves(templateFile).to('./components/hello-world/template.hbs'); }); test('podded hbs-only component with non-blank podModulePrefix', async function () { @@ -430,9 +436,11 @@ Scenarios.fromProject(() => new Project()) .resolves('@embroider/virtual/components/hello-world') .toModule(); + let templateFile = normalizePath(`${app.dir}/pods/components/hello-world/template.hbs`); + pairModule.codeEquals(` import { setComponentTemplate } from "@ember/component"; - import template from "./template.hbs"; + import template from "${esc(templateFile)}"; import { deprecate } from "@ember/debug"; true && !false && deprecate( "Components with separately resolved templates are deprecated. Migrate to either co-located js/ts + hbs files or to gjs/gts. Tried to lookup 'template'.", @@ -452,7 +460,7 @@ Scenarios.fromProject(() => new Project()) export default setComponentTemplate(template, templateOnlyComponent(undefined, "template")); `); - pairModule.resolves('./template.hbs').to('./pods/components/hello-world/template.hbs'); + pairModule.resolves(templateFile).to('./pods/components/hello-world/template.hbs'); }); test('podded js-and-hbs component with blank podModulePrefix', async function () { @@ -469,9 +477,12 @@ Scenarios.fromProject(() => new Project()) .resolves('@embroider/virtual/components/hello-world') .toModule(); + let templateFile = normalizePath(`${app.dir}/components/hello-world/template.hbs`); + let componentFile = normalizePath(`${app.dir}/components/hello-world/component.js`); + pairModule.codeEquals(` import { setComponentTemplate } from "@ember/component"; - import template from "./template.hbs"; + import template from "${esc(templateFile)}"; import { deprecate } from "@ember/debug"; true && !false && deprecate( "Components with separately resolved templates are deprecated. Migrate to either co-located js/ts + hbs files or to gjs/gts. Tried to lookup 'template'.", @@ -487,12 +498,12 @@ Scenarios.fromProject(() => new Project()) }, } ); - import component from "./component.js"; + import component from "${esc(componentFile)}"; export default setComponentTemplate(template, component); `); - pairModule.resolves('./template.hbs').to('./components/hello-world/template.hbs'); - pairModule.resolves('./component.js').to('./components/hello-world/component.js'); + pairModule.resolves(templateFile).to('./components/hello-world/template.hbs'); + pairModule.resolves(componentFile).to('./components/hello-world/component.js'); }); test('podded js-and-hbs component with non-blank podModulePrefix', async function () { @@ -509,9 +520,12 @@ Scenarios.fromProject(() => new Project()) .resolves('@embroider/virtual/components/hello-world') .toModule(); + let templateFile = normalizePath(`${app.dir}/pods/components/hello-world/template.hbs`); + let componentFile = normalizePath(`${app.dir}/pods/components/hello-world/component.js`); + pairModule.codeEquals(` import { setComponentTemplate } from "@ember/component"; - import template from "./template.hbs"; + import template from "${esc(templateFile)}"; import { deprecate } from "@ember/debug"; true && !false && deprecate( "Components with separately resolved templates are deprecated. Migrate to either co-located js/ts + hbs files or to gjs/gts. Tried to lookup 'template'.", @@ -527,12 +541,11 @@ Scenarios.fromProject(() => new Project()) }, } ); - import component from "./component.js"; + import component from "${esc(componentFile)}"; export default setComponentTemplate(template, component); `); - - pairModule.resolves('./template.hbs').to('./pods/components/hello-world/template.hbs'); - pairModule.resolves('./component.js').to('./pods/components/hello-world/component.js'); + pairModule.resolves(templateFile).to('./pods/components/hello-world/template.hbs'); + pairModule.resolves(componentFile).to('./pods/components/hello-world/component.js'); }); test('plain helper', async function () { @@ -987,3 +1000,15 @@ Scenarios.fromProject(() => new Project()) }); }); }); + +function normalizePath(s: string): string { + if (process.platform === 'win32') { + return s.replace(/\//g, '\\'); + } else { + return s; + } +} + +function esc(s: string): string { + return s.replace(/\\/g, '\\\\'); +}