From 614989c9290500be6fb00b4493221d66bd3ea2af Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 27 Nov 2024 14:39:23 -0500 Subject: [PATCH 1/2] move module-request to its own module --- packages/core/src/index.ts | 3 ++- packages/core/src/module-request.ts | 33 ++++++++++++++++++++++++++ packages/core/src/module-resolver.ts | 35 +--------------------------- packages/core/src/node-resolve.ts | 3 ++- 4 files changed, 38 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/module-request.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ef8d1c435..9ddfd3707 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,7 +12,8 @@ export { default as Options, optionsWithDefaults } from './options'; export { default as WaitForTrees, OutputPaths } from './wait-for-trees'; export { compile as jsHandlebarsCompile } from './js-handlebars'; export { todo, unsupported, warn, debug, expectWarning, throwOnWarnings } from './messages'; -export { Resolver, ModuleRequest, Resolution } from './module-resolver'; +export { Resolver } from './module-resolver'; +export type { ModuleRequest, Resolution } from './module-request'; export type { Options as ResolverOptions } from './module-resolver-options'; export { ResolverLoader } from './resolver-loader'; export { virtualContent } from './virtual-content'; diff --git a/packages/core/src/module-request.ts b/packages/core/src/module-request.ts new file mode 100644 index 000000000..a40192355 --- /dev/null +++ b/packages/core/src/module-request.ts @@ -0,0 +1,33 @@ +// 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 } + + // used for requests that are special and don't represent real files that + // embroider can possibly do anything custom with. + // + // the motivating use case for introducing this is Vite's depscan which marks + // almost everything as "external" as a way to tell esbuild to stop traversing + // once it has been seen the first time. + | { type: 'ignored'; result: T } + + // the important thing about this Resolution is that embroider should do its + // fallback behaviors here. + | { type: 'not_found'; err: E }; + +export interface ModuleRequest { + readonly specifier: string; + readonly fromFile: string; + readonly isVirtual: boolean; + readonly meta: Record | undefined; + readonly debugType: string; + readonly isNotFound: boolean; + readonly resolvedTo: Res | undefined; + alias(newSpecifier: string): this; + rehome(newFromFile: string): this; + virtualize(virtualFilename: string): this; + withMeta(meta: Record | undefined): this; + notFound(): this; + defaultResolve(): Promise; + resolveTo(resolution: Res): this; +} diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index 534c1e19a..6845a3e49 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -21,6 +21,7 @@ import { nodeResolve } from './node-resolve'; import { decodePublicRouteEntrypoint, encodeRouteEntrypoint } from './virtual-route-entrypoint'; import type { Options, EngineConfig } from './module-resolver-options'; import { satisfies } from 'semver'; +import type { ModuleRequest, Resolution } from './module-request'; const debug = makeDebug('embroider:resolver'); @@ -110,40 +111,6 @@ type MergeMap = Map[^\/]+)\/(?.*)/; -export interface ModuleRequest { - readonly specifier: string; - readonly fromFile: string; - readonly isVirtual: boolean; - readonly meta: Record | undefined; - readonly debugType: string; - readonly isNotFound: boolean; - readonly resolvedTo: Res | undefined; - alias(newSpecifier: string): this; - rehome(newFromFile: string): this; - virtualize(virtualFilename: string): this; - withMeta(meta: Record | undefined): this; - notFound(): this; - defaultResolve(): Promise; - resolveTo(resolution: Res): this; -} - -// 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 } - - // used for requests that are special and don't represent real files that - // embroider can possibly do anything custom with. - // - // the motivating use case for introducing this is Vite's depscan which marks - // almost everything as "external" as a way to tell esbuild to stop traversing - // once it has been seen the first time. - | { type: 'ignored'; result: T } - - // the important thing about this Resolution is that embroider should do its - // fallback behaviors here. - | { type: 'not_found'; err: E }; - export class Resolver { constructor(readonly options: Options) {} diff --git a/packages/core/src/node-resolve.ts b/packages/core/src/node-resolve.ts index 8d6590cf0..427828942 100644 --- a/packages/core/src/node-resolve.ts +++ b/packages/core/src/node-resolve.ts @@ -4,7 +4,8 @@ import { explicitRelative } from '@embroider/shared-internals'; import assertNever from 'assert-never'; // these would be circular, but they're type-only so it's fine -import type { ModuleRequest, Resolution, Resolver } from './module-resolver'; +import type { ModuleRequest, Resolution } from './module-request'; +import type { Resolver } from './module-resolver'; export class NodeModuleRequest implements ModuleRequest { constructor( From d4ee663d370dffa75e24ba48d81d0960feba1d76 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Wed, 27 Nov 2024 17:02:44 -0500 Subject: [PATCH 2/2] refactor ModuleRequest to an adapter pattern --- packages/core/src/index.ts | 2 +- packages/core/src/module-request.ts | 141 ++++++++++++-- packages/core/src/node-resolve.ts | 84 ++------ packages/vite/src/esbuild-request.ts | 159 ++++----------- packages/vite/src/esbuild-resolver.ts | 12 +- packages/vite/src/request.ts | 171 +++++----------- packages/vite/src/resolver.ts | 11 +- .../webpack/src/webpack-resolver-plugin.ts | 183 +++++------------- 8 files changed, 293 insertions(+), 470 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9ddfd3707..ba9e22595 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,7 +13,7 @@ export { default as WaitForTrees, OutputPaths } from './wait-for-trees'; export { compile as jsHandlebarsCompile } from './js-handlebars'; export { todo, unsupported, warn, debug, expectWarning, throwOnWarnings } from './messages'; export { Resolver } from './module-resolver'; -export type { ModuleRequest, Resolution } from './module-request'; +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'; diff --git a/packages/core/src/module-request.ts b/packages/core/src/module-request.ts index a40192355..9c8630272 100644 --- a/packages/core/src/module-request.ts +++ b/packages/core/src/module-request.ts @@ -15,19 +15,136 @@ export type Resolution = // fallback behaviors here. | { type: 'not_found'; err: E }; -export interface ModuleRequest { +export type RequestAdapterCreate = ( + params: Init +) => { initialState: InitialRequestState; adapter: RequestAdapter } | undefined; + +export interface RequestAdapter { + readonly debugType: string; + resolve(request: ModuleRequest): Promise; +} + +export interface InitialRequestState { readonly specifier: string; readonly fromFile: string; - readonly isVirtual: boolean; readonly meta: Record | undefined; - readonly debugType: string; - readonly isNotFound: boolean; - readonly resolvedTo: Res | undefined; - alias(newSpecifier: string): this; - rehome(newFromFile: string): this; - virtualize(virtualFilename: string): this; - withMeta(meta: Record | undefined): this; - notFound(): this; - defaultResolve(): Promise; - resolveTo(resolution: Res): this; +} + +export class ModuleRequest implements ModuleRequest { + static create( + createAdapter: RequestAdapterCreate, + params: Init + ): ModuleRequest | undefined { + let result = createAdapter(params); + if (result) { + return new ModuleRequest(result.adapter, result.initialState); + } + } + + #adapter: RequestAdapter; + #specifier: string; + #fromFile: string; + #isVirtual: boolean; + #meta: Record | undefined; + #isNotFound: boolean; + #resolvedTo: Res | undefined; + + private constructor( + adapter: RequestAdapter, + initialize: InitialRequestState, + propagate?: { isVirtual: boolean; isNotFound: boolean; resolvedTo: Res | undefined } + ) { + this.#adapter = adapter; + this.#specifier = initialize.specifier; + this.#fromFile = initialize.fromFile; + this.#meta = initialize.meta; + this.#isVirtual = propagate?.isVirtual ?? false; + this.#isNotFound = propagate?.isNotFound ?? false; + this.#resolvedTo = propagate?.resolvedTo; + } + + get specifier(): string { + return this.#specifier; + } + + get fromFile(): string { + return this.#fromFile; + } + + get isVirtual(): boolean { + return this.#isVirtual; + } + + get debugType(): string { + return this.#adapter.debugType; + } + + get meta(): Record | undefined { + return this.#meta; + } + + get isNotFound(): boolean { + return this.#isNotFound; + } + + get resolvedTo(): Res | undefined { + return this.#resolvedTo; + } + + alias(newSpecifier: string): this { + if (this.#specifier === newSpecifier) { + return this; + } + let result = this.#clone(); + result.#specifier = newSpecifier; + result.#isNotFound = false; + result.#resolvedTo = undefined; + return result; + } + + rehome(newFromFile: string): this { + if (this.#fromFile === newFromFile) { + return this; + } + let result = this.#clone(); + result.#fromFile = newFromFile; + result.#isNotFound = false; + result.#resolvedTo = undefined; + return result; + } + + virtualize(virtualFileName: string): this { + let result = this.#clone(); + result.#specifier = virtualFileName; + result.#isVirtual = true; + result.#isNotFound = false; + result.#resolvedTo = undefined; + return result; + } + + withMeta(meta: Record | undefined): this { + let result = this.#clone(); + result.#meta = meta; + return result; + } + + notFound(): this { + let result = this.#clone(); + result.#isNotFound = true; + return result; + } + + resolveTo(res: Res): this { + let result = this.#clone(); + result.#resolvedTo = res; + return result; + } + + defaultResolve(): Promise { + return this.#adapter.resolve(this); + } + + #clone(): this { + return new ModuleRequest(this.#adapter, this, this) as this; + } } diff --git a/packages/core/src/node-resolve.ts b/packages/core/src/node-resolve.ts index 427828942..ce2f6a86e 100644 --- a/packages/core/src/node-resolve.ts +++ b/packages/core/src/node-resolve.ts @@ -4,74 +4,31 @@ import { explicitRelative } from '@embroider/shared-internals'; import assertNever from 'assert-never'; // these would be circular, but they're type-only so it's fine -import type { ModuleRequest, Resolution } from './module-request'; +import { ModuleRequest, type RequestAdapter, type RequestAdapterCreate, type Resolution } from './module-request'; import type { Resolver } from './module-resolver'; -export class NodeModuleRequest implements ModuleRequest { - constructor( - private resolver: Resolver, - readonly specifier: string, - readonly fromFile: string, - readonly isVirtual: boolean, - readonly meta: Record | undefined, - readonly isNotFound: boolean, - readonly resolvedTo: Resolution | undefined - ) {} +export class NodeRequestAdapter implements RequestAdapter> { + static create: RequestAdapterCreate< + { resolver: Resolver; specifier: string; fromFile: string }, + Resolution + > = ({ resolver, specifier, fromFile }) => { + return { + initialState: { + specifier, + fromFile, + meta: undefined, + }, + adapter: new NodeRequestAdapter(resolver), + }; + }; + + private constructor(private resolver: Resolver) {} get debugType() { return 'node'; } - alias(specifier: string): this { - return new NodeModuleRequest(this.resolver, specifier, this.fromFile, false, this.meta, false, undefined) as this; - } - rehome(fromFile: string): this { - if (this.fromFile === fromFile) { - return this; - } else { - return new NodeModuleRequest(this.resolver, this.specifier, fromFile, false, this.meta, false, undefined) as this; - } - } - virtualize(filename: string): this { - return new NodeModuleRequest(this.resolver, filename, this.fromFile, true, this.meta, false, undefined) as this; - } - withMeta(meta: Record | undefined): this { - return new NodeModuleRequest( - this.resolver, - this.specifier, - this.fromFile, - this.isVirtual, - meta, - this.isNotFound, - this.resolvedTo - ) as this; - } - notFound(): this { - return new NodeModuleRequest( - this.resolver, - this.specifier, - this.fromFile, - this.isVirtual, - this.meta, - true, - undefined - ) as this; - } - - resolveTo(resolution: Resolution): this { - return new NodeModuleRequest( - this.resolver, - this.specifier, - this.fromFile, - this.isVirtual, - this.meta, - this.isNotFound, - resolution - ) as this; - } - - async defaultResolve(): Promise> { - const request = this; + async resolve(request: ModuleRequest>): Promise> { if (request.isVirtual) { return { type: 'found', @@ -157,9 +114,8 @@ export async function nodeResolve( specifier: string, fromFile: string ): Promise { - let resolution = await resolver.resolve( - new NodeModuleRequest(resolver, specifier, fromFile, false, undefined, false, undefined) - ); + let request = ModuleRequest.create(NodeRequestAdapter.create, { resolver, fromFile, specifier }); + let resolution = await resolver.resolve(request!); switch (resolution.type) { case 'not_found': return resolution; diff --git a/packages/vite/src/esbuild-request.ts b/packages/vite/src/esbuild-request.ts index d9c028430..37a8cb614 100644 --- a/packages/vite/src/esbuild-request.ts +++ b/packages/vite/src/esbuild-request.ts @@ -3,40 +3,44 @@ const { cleanUrl, packageName } = core; import type { ImportKind, OnResolveResult, PluginBuild } from 'esbuild'; import { dirname } from 'path'; -import type { PackageCache as _PackageCache, Resolution, ModuleRequest } from '@embroider/core'; +import type { PackageCache as _PackageCache, Resolution, ModuleRequest, RequestAdapter } from '@embroider/core'; import { externalName } from '@embroider/reverse-exports'; type PublicAPI = { [K in keyof T]: T[K] }; type PackageCache = PublicAPI<_PackageCache>; -export class EsBuildModuleRequest implements ModuleRequest { - static from( - packageCache: PackageCache, - phase: 'bundling' | 'other', - context: PluginBuild, - kind: ImportKind, - source: string, - importer: string | undefined, - pluginData: Record | undefined - ): EsBuildModuleRequest | undefined { +export class EsBuildRequestAdapter implements RequestAdapter> { + static create({ + packageCache, + phase, + build, + kind, + path, + importer, + pluginData, + }: { + packageCache: PackageCache; + phase: 'bundling' | 'other'; + build: PluginBuild; + kind: ImportKind; + path: string; + importer: string | undefined; + pluginData: Record | undefined; + }) { if (!(pluginData?.embroider?.enableCustomResolver ?? true)) { return; } - if (source && importer && source[0] !== '\0' && !source.startsWith('virtual-module:')) { + if (path && importer && path[0] !== '\0' && !path.startsWith('virtual-module:')) { let fromFile = cleanUrl(importer); - return new EsBuildModuleRequest( - packageCache, - phase, - context, - kind, - source, - fromFile, - pluginData?.embroider?.meta, - false, - false, - undefined - ); + return { + initialState: { + specifier: path, + fromFile, + meta: pluginData?.embroider?.meta, + }, + adapter: new EsBuildRequestAdapter(packageCache, phase, build, kind), + }; } } @@ -44,117 +48,22 @@ export class EsBuildModuleRequest implements ModuleRequest { private packageCache: PackageCache, private phase: 'bundling' | 'other', private context: PluginBuild, - private kind: ImportKind, - readonly specifier: string, - readonly fromFile: string, - readonly meta: Record | undefined, - readonly isVirtual: boolean, - readonly isNotFound: boolean, - readonly resolvedTo: Resolution | undefined + private kind: ImportKind ) {} get debugType() { return 'esbuild'; } - alias(newSpecifier: string) { - return new EsBuildModuleRequest( - this.packageCache, - this.phase, - this.context, - this.kind, - newSpecifier, - this.fromFile, - this.meta, - this.isVirtual, - false, - undefined - ) as this; - } - rehome(newFromFile: string) { - if (this.fromFile === newFromFile) { - return this; - } else { - return new EsBuildModuleRequest( - this.packageCache, - this.phase, - this.context, - this.kind, - this.specifier, - newFromFile, - this.meta, - this.isVirtual, - false, - undefined - ) as this; - } - } - virtualize(filename: string) { - return new EsBuildModuleRequest( - this.packageCache, - this.phase, - this.context, - this.kind, - filename, - this.fromFile, - this.meta, - true, - false, - undefined - ) as this; - } - withMeta(meta: Record | undefined): this { - return new EsBuildModuleRequest( - this.packageCache, - this.phase, - this.context, - this.kind, - this.specifier, - this.fromFile, - meta, - this.isVirtual, - this.isNotFound, - this.resolvedTo - ) as this; - } - notFound(): this { - return new EsBuildModuleRequest( - this.packageCache, - this.phase, - this.context, - this.kind, - this.specifier, - this.fromFile, - this.meta, - this.isVirtual, - true, - undefined - ) as this; - } - - resolveTo(resolution: Resolution): this { - return new EsBuildModuleRequest( - this.packageCache, - this.phase, - this.context, - this.kind, - this.specifier, - this.fromFile, - this.meta, - this.isVirtual, - this.isNotFound, - resolution - ) as this; - } - - async defaultResolve(): Promise> { - const request = this; + async resolve( + request: ModuleRequest> + ): Promise> { if (request.isVirtual) { return { type: 'found', filename: request.specifier, result: { path: request.specifier, namespace: 'embroider-virtual' }, - isVirtual: this.isVirtual, + isVirtual: request.isVirtual, }; } if (request.isNotFound) { @@ -220,7 +129,7 @@ export class EsBuildModuleRequest implements ModuleRequest { }; } } - return { type: 'found', filename: result.path, result, isVirtual: this.isVirtual }; + return { type: 'found', filename: result.path, result, isVirtual: request.isVirtual }; } } } diff --git a/packages/vite/src/esbuild-resolver.ts b/packages/vite/src/esbuild-resolver.ts index 683864139..94097c94d 100644 --- a/packages/vite/src/esbuild-resolver.ts +++ b/packages/vite/src/esbuild-resolver.ts @@ -1,10 +1,10 @@ import type { Plugin as EsBuildPlugin, OnLoadResult, PluginBuild, ResolveResult } from 'esbuild'; import { transformAsync } from '@babel/core'; -import core from '@embroider/core'; +import core, { ModuleRequest } from '@embroider/core'; const { ResolverLoader, virtualContent, needsSyntheticComponentJS, isInComponents } = core; import fs from 'fs-extra'; const { readFileSync } = fs; -import { EsBuildModuleRequest } from './esbuild-request.js'; +import { EsBuildRequestAdapter } from './esbuild-request.js'; import { assertNever } from 'assert-never'; import { hbsToJS } from '@embroider/core'; import { Preprocessor } from 'content-tag'; @@ -52,15 +52,15 @@ export function esBuildResolver(): EsBuildPlugin { // Embroider Resolver build.onResolve({ filter: /./ }, async ({ path, importer, pluginData, kind }) => { - let request = EsBuildModuleRequest.from( - resolverLoader.resolver.packageCache, + let request = ModuleRequest.create(EsBuildRequestAdapter.create, { + packageCache: resolverLoader.resolver.packageCache, phase, build, kind, path, importer, - pluginData - ); + pluginData, + }); if (!request) { return null; } diff --git a/packages/vite/src/request.ts b/packages/vite/src/request.ts index e0e19d63d..f4f0f3a38 100644 --- a/packages/vite/src/request.ts +++ b/packages/vite/src/request.ts @@ -1,17 +1,24 @@ -import type { ModuleRequest, Resolution } from '@embroider/core'; +import type { ModuleRequest, RequestAdapter, RequestAdapterCreate, Resolution } from '@embroider/core'; import core from '@embroider/core'; const { cleanUrl, getUrlQueryParams } = core; import type { PluginContext, ResolveIdResult } from 'rollup'; export const virtualPrefix = 'embroider_virtual:'; -export class RollupModuleRequest implements ModuleRequest { - static from( - context: PluginContext, - source: string, - importer: string | undefined, - custom: Record | undefined - ): RollupModuleRequest | undefined { +interface Init { + context: PluginContext; + source: string; + importer: string | undefined; + custom: Record | undefined; +} + +export class RollupRequestAdapter implements RequestAdapter> { + static create: RequestAdapterCreate> = ({ + context, + source, + importer, + custom, + }: Init) => { if (!(custom?.embroider?.enableCustomResolver ?? true)) { return; } @@ -33,26 +40,15 @@ export class RollupModuleRequest implements ModuleRequest { let cleanSource = cleanUrl(source); let queryParams = getUrlQueryParams(source); - return new RollupModuleRequest( - context, - cleanSource, - fromFile, - custom?.embroider?.meta, - false, - undefined, - queryParams, - importerQueryParams - ); + return { + initialState: { specifier: cleanSource, fromFile, meta: custom?.embroider?.meta }, + adapter: new RollupRequestAdapter(context, queryParams, importerQueryParams), + }; } - } + }; private constructor( private context: PluginContext, - readonly specifier: string, - readonly fromFile: string, - readonly meta: Record | undefined, - readonly isNotFound: boolean, - readonly resolvedTo: Resolution | undefined, private queryParams: string, private importerQueryParams: string ) {} @@ -61,125 +57,52 @@ export class RollupModuleRequest implements ModuleRequest { return 'rollup'; } - get isVirtual(): boolean { - return this.specifier.startsWith(virtualPrefix); + private specifierWithQueryParams(specifier: string): string { + return `${specifier}${this.queryParams}`; } - private get specifierWithQueryParams(): string { - return `${this.specifier}${this.queryParams}`; + private fromFileWithQueryParams(fromFile: string): string { + return `${fromFile}${this.importerQueryParams}`; } - private get fromFileWithQueryParams(): string { - return `${this.fromFile}${this.importerQueryParams}`; - } - - alias(newSpecifier: string) { - return new RollupModuleRequest( - this.context, - newSpecifier, - this.fromFile, - this.meta, - false, - undefined, - this.queryParams, - this.importerQueryParams - ) as this; - } - rehome(newFromFile: string) { - if (this.fromFile === newFromFile) { - return this; - } else { - return new RollupModuleRequest( - this.context, - this.specifier, - newFromFile, - this.meta, - false, - undefined, - this.queryParams, - this.importerQueryParams - ) as this; - } - } - virtualize(filename: string) { - return new RollupModuleRequest( - this.context, - virtualPrefix + filename, - this.fromFile, - this.meta, - false, - undefined, - this.queryParams, - this.importerQueryParams - ) as this; - } - withMeta(meta: Record | undefined): this { - return new RollupModuleRequest( - this.context, - this.specifier, - this.fromFile, - meta, - this.isNotFound, - this.resolvedTo, - this.queryParams, - this.importerQueryParams - ) as this; - } - notFound(): this { - return new RollupModuleRequest( - this.context, - this.specifier, - this.fromFile, - this.meta, - true, - undefined, - this.queryParams, - this.importerQueryParams - ) as this; - } - async defaultResolve(): Promise> { - if (this.isVirtual) { + async resolve(request: ModuleRequest>): Promise> { + if (request.isVirtual) { + let specifier = virtualPrefix + request.specifier; return { type: 'found', - filename: this.specifier, - result: { id: this.specifierWithQueryParams, resolvedBy: this.fromFileWithQueryParams }, - isVirtual: this.isVirtual, + filename: specifier, + result: { + id: this.specifierWithQueryParams(specifier), + resolvedBy: this.fromFileWithQueryParams(request.fromFile), + }, + isVirtual: request.isVirtual, }; } - if (this.isNotFound) { + if (request.isNotFound) { // TODO: we can make sure this looks correct in rollup & vite output when a // user encounters it let err = new Error(`module not found ${this.specifierWithQueryParams}`); (err as any).code = 'MODULE_NOT_FOUND'; return { type: 'not_found', err }; } - let result = await this.context.resolve(this.specifierWithQueryParams, this.fromFileWithQueryParams, { - skipSelf: true, - custom: { - embroider: { - enableCustomResolver: false, - meta: this.meta, + let result = await this.context.resolve( + this.specifierWithQueryParams(request.specifier), + this.fromFileWithQueryParams(request.fromFile), + { + skipSelf: true, + custom: { + embroider: { + enableCustomResolver: false, + meta: request.meta, + }, }, - }, - }); + } + ); if (result) { let { pathname } = new URL(result.id, 'http://example.com'); - return { type: 'found', filename: pathname, result, isVirtual: this.isVirtual }; + return { type: 'found', filename: pathname, result, isVirtual: request.isVirtual }; } else { return { type: 'not_found', err: undefined }; } } - - resolveTo(resolution: Resolution): this { - return new RollupModuleRequest( - this.context, - this.specifier, - this.fromFile, - this.meta, - this.isNotFound, - resolution, - this.queryParams, - this.importerQueryParams - ) as this; - } } diff --git a/packages/vite/src/resolver.ts b/packages/vite/src/resolver.ts index 99cb56303..002e9d593 100644 --- a/packages/vite/src/resolver.ts +++ b/packages/vite/src/resolver.ts @@ -1,7 +1,7 @@ import type { Plugin, ViteDevServer } from 'vite'; -import core, { type Resolver } from '@embroider/core'; +import core, { ModuleRequest, type Resolver } from '@embroider/core'; const { virtualContent, ResolverLoader, explicitRelative, cleanUrl, tmpdir } = core; -import { RollupModuleRequest, virtualPrefix } from './request.js'; +import { RollupRequestAdapter, virtualPrefix } from './request.js'; import { assertNever } from 'assert-never'; import makeDebug from 'debug'; import { resolve, join } from 'path'; @@ -48,7 +48,12 @@ export function resolver(): Plugin { return await observeDepScan(this, source, importer, options); } - let request = RollupModuleRequest.from(this, source, importer, options.custom); + let request = ModuleRequest.create(RollupRequestAdapter.create, { + context: this, + source, + importer, + custom: options.custom, + }); if (!request) { // fallthrough to other rollup plugins return null; diff --git a/packages/webpack/src/webpack-resolver-plugin.ts b/packages/webpack/src/webpack-resolver-plugin.ts index a418c5385..7479eb5b3 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 type { ModuleRequest, Resolution } from '@embroider/core'; +import { ModuleRequest, 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'; @@ -44,7 +44,12 @@ export class EmbroiderPlugin { nmf.hooks.resolve.tapAsync( { name: '@embroider/webpack', stage: 50 }, (state: ExtendedResolveData, callback: CB) => { - let request = WebpackModuleRequest.from(defaultResolve, state, this.#babelLoaderPrefix, this.#appRoot); + let request = ModuleRequest.create(WebpackRequestAdapter.create, { + resolveFunction: defaultResolve, + state, + babelLoaderPrefix: this.#babelLoaderPrefix, + appRoot: this.#appRoot, + }); if (!request) { defaultResolve(state, callback); return; @@ -94,13 +99,18 @@ type ExtendedResolveData = ResolveData & { type WebpackResolution = Resolution; -class WebpackModuleRequest implements ModuleRequest { - static from( - resolveFunction: DefaultResolve, - state: ExtendedResolveData, - babelLoaderPrefix: string, - appRoot: string - ): WebpackModuleRequest | undefined { +class WebpackRequestAdapter implements RequestAdapter { + static create({ + resolveFunction, + state, + babelLoaderPrefix, + appRoot, + }: { + resolveFunction: DefaultResolve; + state: ExtendedResolveData; + babelLoaderPrefix: string; + appRoot: string; + }) { let specifier = state.request; if ( specifier.includes(virtualLoaderName) || // prevents recursion on requests we have already sent to our virtual loader @@ -129,30 +139,20 @@ class WebpackModuleRequest implements ModuleRequest { return; } - return new WebpackModuleRequest( - resolveFunction, - babelLoaderPrefix, - appRoot, - specifier, - fromFile, - state.contextInfo._embroiderMeta, - false, - false, - undefined, - state - ); + return { + initialState: { + specifier, + fromFile, + meta: state.contextInfo._embroiderMeta, + }, + adapter: new WebpackRequestAdapter(resolveFunction, babelLoaderPrefix, appRoot, state), + }; } private constructor( private resolveFunction: DefaultResolve, private babelLoaderPrefix: string, private appRoot: string, - readonly specifier: string, - readonly fromFile: string, - readonly meta: Record | undefined, - readonly isVirtual: boolean, - readonly isNotFound: boolean, - readonly resolvedTo: WebpackResolution | undefined, private originalState: ExtendedResolveData ) {} @@ -182,124 +182,37 @@ class WebpackModuleRequest implements ModuleRequest { // requests evolve through the module-resolver they aren't actually all // mutating the shared state. Only when a request is allowed to bubble back // out to webpack does that happen. - toWebpackResolveData(): ExtendedResolveData { - this.originalState.request = this.specifier; - this.originalState.context = dirname(this.fromFile); - this.originalState.contextInfo.issuer = this.fromFile; - this.originalState.contextInfo._embroiderMeta = this.meta; - if (this.resolvedTo) { - if (this.resolvedTo.type === 'found') { - this.originalState.createData = this.resolvedTo.result; - } + toWebpackResolveData(request: ModuleRequest): ExtendedResolveData { + let specifier = request.specifier; + if (request.isVirtual) { + let params = new URLSearchParams(); + params.set('f', specifier); + params.set('a', this.appRoot); + specifier = `${this.babelLoaderPrefix}${virtualLoaderName}?${params.toString()}!`; } - return this.originalState; - } - alias(newSpecifier: string) { - if (newSpecifier === this.specifier) { - return this; - } - return new WebpackModuleRequest( - this.resolveFunction, - this.babelLoaderPrefix, - this.appRoot, - newSpecifier, - this.fromFile, - this.meta, - this.isVirtual, - false, - undefined, - this.originalState - ) as this; - } - rehome(newFromFile: string) { - if (this.fromFile === newFromFile) { - return this; + this.originalState.request = specifier; + this.originalState.context = dirname(request.fromFile); + this.originalState.contextInfo.issuer = request.fromFile; + this.originalState.contextInfo._embroiderMeta = request.meta; + if (request.resolvedTo) { + if (request.resolvedTo.type === 'found') { + this.originalState.createData = request.resolvedTo.result; + } } - return new WebpackModuleRequest( - this.resolveFunction, - this.babelLoaderPrefix, - this.appRoot, - this.specifier, - newFromFile, - this.meta, - this.isVirtual, - false, - undefined, - this.originalState - ) as this; - } - virtualize(filename: string) { - let params = new URLSearchParams(); - params.set('f', filename); - params.set('a', this.appRoot); - return new WebpackModuleRequest( - this.resolveFunction, - this.babelLoaderPrefix, - this.appRoot, - `${this.babelLoaderPrefix}${virtualLoaderName}?${params.toString()}!`, - this.fromFile, - this.meta, - true, - false, - undefined, - this.originalState - ) as this; - } - withMeta(meta: Record | undefined): this { - return new WebpackModuleRequest( - this.resolveFunction, - this.babelLoaderPrefix, - this.appRoot, - this.specifier, - this.fromFile, - meta, - this.isVirtual, - this.isNotFound, - this.resolvedTo, - this.originalState - ) as this; - } - notFound(): this { - return new WebpackModuleRequest( - this.resolveFunction, - this.babelLoaderPrefix, - this.appRoot, - this.specifier, - this.fromFile, - this.meta, - this.isVirtual, - true, - undefined, - this.originalState - ) as this; - } - - resolveTo(resolution: WebpackResolution): this { - return new WebpackModuleRequest( - this.resolveFunction, - this.babelLoaderPrefix, - this.appRoot, - this.specifier, - this.fromFile, - this.meta, - this.isVirtual, - this.isNotFound, - resolution, - this.originalState - ) as this; + return this.originalState; } - async defaultResolve(): Promise { - if (this.isNotFound) { + async resolve(request: ModuleRequest): Promise { + if (request.isNotFound) { // TODO: we can make sure this looks correct in webpack output when a // user encounters it - let err = new Error(`module not found ${this.specifier}`); + let err = new Error(`module not found ${request.specifier}`); (err as any).code = 'MODULE_NOT_FOUND'; return { type: 'not_found', err }; } return await new Promise(resolve => - this.resolveFunction(this.toWebpackResolveData(), err => { + this.resolveFunction(this.toWebpackResolveData(request), err => { if (err) { // unfortunately webpack doesn't let us distinguish between Not Found // and other unexpected exceptions here. @@ -308,7 +221,7 @@ class WebpackModuleRequest implements ModuleRequest { resolve({ type: 'found', result: this.originalState.createData, - isVirtual: this.isVirtual, + isVirtual: request.isVirtual, filename: this.originalState.createData.resource!, }); }