diff --git a/packages/dev-server-core/src/index.ts b/packages/dev-server-core/src/index.ts index 63c596468..2bcfb0c8d 100644 --- a/packages/dev-server-core/src/index.ts +++ b/packages/dev-server-core/src/index.ts @@ -6,7 +6,7 @@ import WebSocket from 'ws'; export { WebSocket }; export { DevServer } from './server/DevServer'; -export { Plugin, ServerStartParams } from './plugins/Plugin'; +export { Plugin, ServerStartParams, ResolveOptions } from './plugins/Plugin'; export { DevServerCoreConfig, MimeTypeMappings } from './server/DevServerCoreConfig'; export { WebSocketsManager, WebSocketData } from './web-sockets/WebSocketsManager'; export { diff --git a/packages/dev-server-core/src/plugins/Plugin.ts b/packages/dev-server-core/src/plugins/Plugin.ts index d4616e213..54cb52b76 100644 --- a/packages/dev-server-core/src/plugins/Plugin.ts +++ b/packages/dev-server-core/src/plugins/Plugin.ts @@ -26,6 +26,12 @@ export interface ServerStartParams { webSockets?: WebSocketsManager; } +export interface ResolveOptions { + isEntry?: boolean; + skipSelf?: boolean; + [key: string]: unknown; +} + export interface Plugin { name: string; injectWebSocket?: boolean; @@ -40,7 +46,9 @@ export interface Plugin { code?: string; column?: number; line?: number; + resolveOptions?: ResolveOptions; }): ResolveResult | Promise; + resolveImportSkip?(context: Context, source: string, importer: string): void; transformImport?(args: { source: string; context: Context; diff --git a/packages/dev-server-rollup/src/createRollupPluginContextAdapter.ts b/packages/dev-server-rollup/src/createRollupPluginContextAdapter.ts index f0936148e..ff88d90c9 100644 --- a/packages/dev-server-rollup/src/createRollupPluginContextAdapter.ts +++ b/packages/dev-server-rollup/src/createRollupPluginContextAdapter.ts @@ -1,5 +1,11 @@ import path from 'path'; -import { DevServerCoreConfig, FSWatcher, Plugin as WdsPlugin, Context } from '@web/dev-server-core'; +import { + DevServerCoreConfig, + FSWatcher, + Plugin as WdsPlugin, + Context, + ResolveOptions, +} from '@web/dev-server-core'; import { PluginContext, MinimalPluginContext, @@ -70,15 +76,20 @@ export function createRollupPluginContextAdapter< throw new Error('Emitting files is not yet supported'); }, - async resolve(source: string, importer: string, options: { skipSelf: boolean }) { + async resolve(source: string, importer: string, options: ResolveOptions) { if (!context) throw new Error('Context is required.'); + const { skipSelf, ...resolveOptions } = options; + + if (skipSelf) wdsPlugin.resolveImportSkip?.(context, source, importer); + for (const pl of config.plugins ?? []) { - if ( - pl.resolveImport && - (!options.skipSelf || pl.resolveImport !== wdsPlugin.resolveImport) - ) { - const result = await pl.resolveImport({ source, context }); + if (pl.resolveImport && (!skipSelf || pl !== wdsPlugin)) { + const result = await pl.resolveImport({ + source, + context, + resolveOptions, + }); let resolvedId: string | undefined; if (typeof result === 'string') { resolvedId = result; @@ -97,7 +108,11 @@ export function createRollupPluginContextAdapter< } }, - async resolveId(source: string, importer: string, options: { skipSelf: boolean }) { + async resolveId( + source: string, + importer: string, + options: { isEntry: boolean; skipSelf: boolean; custom: Record }, + ) { const resolveResult = await this.resolve(source, importer, options); if (typeof resolveResult === 'string') { return resolveResult; diff --git a/packages/dev-server-rollup/src/rollupAdapter.ts b/packages/dev-server-rollup/src/rollupAdapter.ts index fcac5f77e..74403b6ec 100644 --- a/packages/dev-server-rollup/src/rollupAdapter.ts +++ b/packages/dev-server-rollup/src/rollupAdapter.ts @@ -17,7 +17,12 @@ import { setTextContent, } from '@web/dev-server-core/dist/dom5'; import { parse as parseHtml, serialize as serializeHtml } from 'parse5'; -import { CustomPluginOptions, Plugin as RollupPlugin, TransformPluginContext } from 'rollup'; +import { + CustomPluginOptions, + Plugin as RollupPlugin, + TransformPluginContext, + ResolveIdHook, +} from 'rollup'; import { InputOptions } from 'rollup'; import { red, cyan } from 'nanocolors'; @@ -55,6 +60,8 @@ export interface RollupAdapterOptions { throwOnUnresolvedImport?: boolean; } +const resolverCache = new WeakMap>>(); + export function rollupAdapter( rollupPlugin: RollupPlugin, rollupInputOptions: Partial = {}, @@ -70,6 +77,7 @@ export function rollupAdapter( let fileWatcher: FSWatcher; let config: DevServerCoreConfig; let rootDir: string; + let idResolvers: ResolveIdHook[] = []; function savePluginMeta( id: string, @@ -89,16 +97,43 @@ export function rollupAdapter( ({ rootDir } = config); rollupPluginContexts = await createRollupPluginContexts(rollupInputOptions); + idResolvers = []; + // call the options and buildStart hooks - rollupPlugin.options?.call(rollupPluginContexts.minimalPluginContext, rollupInputOptions) ?? - rollupInputOptions; + const transformedOptions = + (await rollupPlugin.options?.call( + rollupPluginContexts.minimalPluginContext, + rollupInputOptions, + )) ?? rollupInputOptions; rollupPlugin.buildStart?.call( rollupPluginContexts.pluginContext, rollupPluginContexts.normalizedInputOptions, ); + + if (transformedOptions && transformedOptions.plugins) { + for (const subPlugin of transformedOptions.plugins) { + if (subPlugin && subPlugin.resolveId) { + idResolvers.push(subPlugin.resolveId); + } + } + } + + if (rollupPlugin.resolveId) { + idResolvers.push(rollupPlugin.resolveId); + } }, - async resolveImport({ source, context, code, column, line }) { + resolveImportSkip(context: Context, source: string, importer: string) { + const resolverCacheForContext = + resolverCache.get(context) ?? new WeakMap>(); + resolverCache.set(context, resolverCacheForContext); + const resolverCacheForPlugin = resolverCacheForContext.get(wdsPlugin) ?? new Set(); + resolverCacheForContext.set(wdsPlugin, resolverCacheForPlugin); + + resolverCacheForPlugin.add(`${source}:${importer}`); + }, + + async resolveImport({ source, context, code, column, line, resolveOptions }) { if (context.response.is('html') && source.startsWith('�')) { // when serving HTML a null byte gets parsed as an unknown character // we remap it to a null byte here so that it is handled properly downstream @@ -109,7 +144,7 @@ export function rollupAdapter( // if we just transformed this file and the import is an absolute file path // we need to rewrite it to a browser path const injectedFilePath = path.normalize(source).startsWith(rootDir); - if (!injectedFilePath && !rollupPlugin.resolveId) { + if (!injectedFilePath && idResolvers.length === 0) { return; } @@ -146,12 +181,28 @@ export function rollupAdapter( } } - let result = await rollupPlugin.resolveId?.call( - rollupPluginContext, - resolvableImport, - filePath, - { isEntry: false }, - ); + let result = null; + + const resolverCacheForContext = + resolverCache.get(context) ?? new WeakMap>(); + resolverCache.set(context, resolverCacheForContext); + const resolverCacheForPlugin = resolverCacheForContext.get(wdsPlugin) ?? new Set(); + resolverCacheForContext.set(wdsPlugin, resolverCacheForPlugin); + + if (resolverCacheForPlugin.has(`${source}:${filePath}`)) { + return undefined; + } + + for (const idResolver of idResolvers) { + result = await idResolver.call(rollupPluginContext, resolvableImport, filePath, { + ...resolveOptions, + isEntry: false, + }); + + if (result) { + break; + } + } if (!result && injectedFilePath) { // the import is a file path but it was not resolved by this plugin diff --git a/packages/dev-server-rollup/test/node/plugins/commonjs.test.ts b/packages/dev-server-rollup/test/node/plugins/commonjs.test.ts index a5c3c138f..e13aed548 100644 --- a/packages/dev-server-rollup/test/node/plugins/commonjs.test.ts +++ b/packages/dev-server-rollup/test/node/plugins/commonjs.test.ts @@ -3,8 +3,10 @@ import { runTests } from '@web/test-runner-core/test-helpers'; import { resolve } from 'path'; import { chromeLauncher } from '@web/test-runner-chrome'; +import * as path from 'path'; import { createTestServer, fetchText, expectIncludes } from '../test-helpers'; import { fromRollup } from '../../../src/index'; +import { nodeResolvePlugin } from '@web/dev-server'; const commonjs = fromRollup(rollupCommonjs); @@ -135,6 +137,35 @@ exports.default = _default;`; } }); + it('can transform modules which require node-resolved modules', async () => { + const rootDir = path.resolve(__dirname, '..', 'fixtures', 'basic'); + const { server, host } = await createTestServer({ + plugins: [ + { + name: 'test', + serve(context) { + if (context.path === '/foo.js') { + return 'import {expect} from "chai"; export {expect};'; + } + }, + }, + commonjs(), + nodeResolvePlugin(rootDir, false, {}), + ], + }); + + try { + const text = await fetchText(`${host}/foo.js`); + expectIncludes( + text, + 'import {expect} from "/__wds-outside-root__/6/node_modules/chai/index.mjs"', + ); + expectIncludes(text, 'export {expect};'); + } finally { + server.stop(); + } + }); + it('can transform modules which require other modules', async () => { const { server, host } = await createTestServer({ plugins: [