Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix (dev-server-rollup): support child plugins in resolution algorithm #2050

Merged
merged 5 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/dev-server-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions packages/dev-server-core/src/plugins/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,7 +46,9 @@ export interface Plugin {
code?: string;
column?: number;
line?: number;
resolveOptions?: ResolveOptions;
}): ResolveResult | Promise<ResolveResult>;
resolveImportSkip?(context: Context, source: string, importer: string): void;
transformImport?(args: {
source: string;
context: Context;
Expand Down
31 changes: 23 additions & 8 deletions packages/dev-server-rollup/src/createRollupPluginContextAdapter.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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<string, unknown> },
) {
const resolveResult = await this.resolve(source, importer, options);
if (typeof resolveResult === 'string') {
return resolveResult;
Expand Down
73 changes: 62 additions & 11 deletions packages/dev-server-rollup/src/rollupAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -55,6 +60,8 @@ export interface RollupAdapterOptions {
throwOnUnresolvedImport?: boolean;
}

const resolverCache = new WeakMap<Context, WeakMap<WdsPlugin, Set<string>>>();

export function rollupAdapter(
rollupPlugin: RollupPlugin,
rollupInputOptions: Partial<InputOptions> = {},
Expand All @@ -70,6 +77,7 @@ export function rollupAdapter(
let fileWatcher: FSWatcher;
let config: DevServerCoreConfig;
let rootDir: string;
let idResolvers: ResolveIdHook[] = [];

function savePluginMeta(
id: string,
Expand All @@ -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<WdsPlugin, Set<string>>();
resolverCache.set(context, resolverCacheForContext);
const resolverCacheForPlugin = resolverCacheForContext.get(wdsPlugin) ?? new Set<string>();
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
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<WdsPlugin, Set<string>>();
resolverCache.set(context, resolverCacheForContext);
const resolverCacheForPlugin = resolverCacheForContext.get(wdsPlugin) ?? new Set<string>();
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
Expand Down
31 changes: 31 additions & 0 deletions packages/dev-server-rollup/test/node/plugins/commonjs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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: [
Expand Down