diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts index d73280aa..dc10d629 100644 --- a/src/esbuild/index.ts +++ b/src/esbuild/index.ts @@ -94,7 +94,11 @@ export function getEsbuildPlugin ( if (plugin.resolveId) { onResolve({ filter: onResolveFilter }, async (args) => { - const result = await plugin.resolveId!(args.path, args.importer) + const result = await plugin.resolveId!( + args.path, + args.kind === 'entry-point' ? undefined : args.importer, + { isEntry: args.kind === 'entry-point' } + ) if (typeof result === 'string') { return { path: result, namespace: plugin.name } } else if (typeof result === 'object' && result !== null) { diff --git a/src/types.ts b/src/types.ts index 91c678c3..7b3d601d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,7 +32,7 @@ export interface UnpluginOptions { transformInclude?: (id: string) => boolean; transform?: (this: UnpluginBuildContext & UnpluginContext, code: string, id: string) => Thenable; load?: (this: UnpluginBuildContext & UnpluginContext, id: string) => Thenable - resolveId?: (id: string, importer?: string) => Thenable + resolveId?: (id: string, importer: string | undefined, options: { isEntry: boolean }) => Thenable watchChange?: (this: UnpluginBuildContext, id: string, change: {event: 'create' | 'update' | 'delete'}) => void // framework specify extends diff --git a/src/webpack/index.ts b/src/webpack/index.ts index 6d0450f3..65db6fa9 100644 --- a/src/webpack/index.ts +++ b/src/webpack/index.ts @@ -2,7 +2,7 @@ import fs from 'fs' import { fileURLToPath } from 'url' import { resolve, dirname } from 'path' import VirtualModulesPlugin from 'webpack-virtual-modules' -import type { Resolver, ResolveRequest } from 'enhanced-resolve' +import type { ResolvePluginInstance } from 'webpack' import type { UnpluginContextMeta, UnpluginInstance, UnpluginFactory, WebpackCompiler, ResolvedUnpluginOptions } from '../types' import { slash, backSlash } from './utils' import { createContext } from './context' @@ -79,58 +79,63 @@ export function getWebpackPlugin ( plugin.__vfsModules = new Set() plugin.__vfs = vfs - const resolver = { - apply (resolver: Resolver) { + const resolverPlugin: ResolvePluginInstance = { + apply (resolver) { const target = resolver.ensureHook('resolve') - const tap = () => async (request: ResolveRequest, resolveContext: any, callback: any) => { - if (!request.request) { - return callback() - } - - const id = backSlash(request.request) - - // filter out invalid requests - if (id.startsWith(plugin.__virtualModulePrefix)) { - return callback() - } - - // call hook - const result = await plugin.resolveId!(slash(id)) - if (result == null) { - return callback() - } - let resolved = typeof result === 'string' ? result : result.id - - // TODO: support external - // const isExternal = typeof result === 'string' ? false : result.external === true - - // if the resolved module is not exists, - // we treat it as a virtual module - if (!fs.existsSync(resolved)) { - resolved = plugin.__virtualModulePrefix + backSlash(resolved) - // webpack virtual module should pass in the correct path - plugin.__vfs!.writeModule(resolved, '') - plugin.__vfsModules!.add(resolved) - } - - // construct the new request - const newRequest = { - ...request, - request: resolved - } - - // redirect the resolver - resolver.doResolve(target, newRequest, null, resolveContext, callback) - } resolver .getHook('resolve') - .tapAsync('unplugin', tap()) + .tapAsync(plugin.name, async (request, resolveContext, callback) => { + if (!request.request) { + return callback() + } + + const id = backSlash(request.request) + + // filter out invalid requests + if (id.startsWith(plugin.__virtualModulePrefix)) { + return callback() + } + + const requestContext = (request as unknown as { context: { issuer: string } }).context + const importer = requestContext.issuer !== '' ? requestContext.issuer : undefined + const isEntry = requestContext.issuer === '' + + // call hook + const result = await plugin.resolveId!(slash(id), importer, { isEntry }) + + if (result == null) { + return callback() + } + + let resolved = typeof result === 'string' ? result : result.id + + // TODO: support external + // const isExternal = typeof result === 'string' ? false : result.external === true + + // If the resolved module does not exist, + // we treat it as a virtual module + if (!fs.existsSync(resolved)) { + resolved = plugin.__virtualModulePrefix + backSlash(resolved) + // webpack virtual module should pass in the correct path + plugin.__vfs!.writeModule(resolved, '') + plugin.__vfsModules!.add(resolved) + } + + // construct the new request + const newRequest = { + ...request, + request: resolved + } + + // redirect the resolver + resolver.doResolve(target, newRequest, null, resolveContext, callback) + }) } } compiler.options.resolve.plugins = compiler.options.resolve.plugins || [] - compiler.options.resolve.plugins.push(resolver) + compiler.options.resolve.plugins.push(resolverPlugin) } // load hook diff --git a/test/unit-tests/resolve-id/resolve-id.test.ts b/test/unit-tests/resolve-id/resolve-id.test.ts new file mode 100644 index 00000000..1ea6a790 --- /dev/null +++ b/test/unit-tests/resolve-id/resolve-id.test.ts @@ -0,0 +1,113 @@ +import * as path from 'path' +import { it, describe, expect, vi, afterEach } from 'vitest' +import * as vite from 'vite' +import * as rollup from 'rollup' +import { webpack } from 'webpack' +import * as esbuild from 'esbuild' +import { createUnplugin, UnpluginOptions } from '../../../src' + +const createUnpluginWithcallback = ( + resolveIdCallback: UnpluginOptions['resolveId'] +) => { + return createUnplugin(() => ({ + name: 'test-plugin', + resolveId: resolveIdCallback + })) +} + +// We extract this check because all bundlers should behave the same +function checkResolveIdHook (resolveIdCallback): void { + expect.assertions(4) + + expect(resolveIdCallback).toHaveBeenCalledWith( + expect.stringMatching(/\/entry\.js$/), + undefined, + expect.objectContaining({ isEntry: true }) + ) + + expect(resolveIdCallback).toHaveBeenCalledWith( + './proxy-export', + expect.stringMatching(/\/entry\.js$/), + expect.objectContaining({ isEntry: false }) + ) + + expect(resolveIdCallback).toHaveBeenCalledWith( + './default-export', + expect.stringMatching(/\/proxy-export\.js$/), + expect.objectContaining({ isEntry: false }) + ) + + expect(resolveIdCallback).toHaveBeenCalledWith( + './named-export', + expect.stringMatching(/\/proxy-export\.js$/), + expect.objectContaining({ isEntry: false }) + ) +} + +describe('resolveId hook', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('vite', async () => { + const mockResolveIdHook = vi.fn(() => undefined) + const plugin = createUnpluginWithcallback(mockResolveIdHook).vite + + await vite.build({ + clearScreen: false, + plugins: [{ ...plugin(), enforce: 'pre' }], // we need to define `enforce` here for the plugin to be run + build: { + lib: { + entry: path.resolve(__dirname, 'test-src/entry.js'), + name: 'TestLib' + }, + write: false // don't output anything + } + }) + + checkResolveIdHook(mockResolveIdHook) + }) + + it('rollup', async () => { + const mockResolveIdHook = vi.fn(() => undefined) + const plugin = createUnpluginWithcallback(mockResolveIdHook).rollup + + await rollup.rollup({ + input: path.resolve(__dirname, 'test-src/entry.js'), + plugins: [plugin()] + }) + + checkResolveIdHook(mockResolveIdHook) + }) + + it('webpack', async () => { + const mockResolveIdHook = vi.fn(() => undefined) + const plugin = createUnpluginWithcallback(mockResolveIdHook).webpack + + await new Promise((resolve) => { + webpack( + { + entry: path.resolve(__dirname, 'test-src/entry.js'), + plugins: [plugin()] + }, + resolve + ) + }) + + checkResolveIdHook(mockResolveIdHook) + }) + + it('esbuild', async () => { + const mockResolveIdHook = vi.fn(() => undefined) + const plugin = createUnpluginWithcallback(mockResolveIdHook).esbuild + + await esbuild.build({ + entryPoints: [path.resolve(__dirname, 'test-src/entry.js')], + plugins: [plugin()], + bundle: true, // actually traverse imports + write: false // don't pollute console + }) + + checkResolveIdHook(mockResolveIdHook) + }) +}) diff --git a/test/unit-tests/resolve-id/test-src/default-export.js b/test/unit-tests/resolve-id/test-src/default-export.js new file mode 100644 index 00000000..770ef173 --- /dev/null +++ b/test/unit-tests/resolve-id/test-src/default-export.js @@ -0,0 +1 @@ +export default 'some string' diff --git a/test/unit-tests/resolve-id/test-src/entry.js b/test/unit-tests/resolve-id/test-src/entry.js new file mode 100644 index 00000000..3b39dfe1 --- /dev/null +++ b/test/unit-tests/resolve-id/test-src/entry.js @@ -0,0 +1,3 @@ +import { named, proxiedDefault } from './proxy-export' + +process.stdout.write(JSON.stringify({ named, proxiedDefault })) diff --git a/test/unit-tests/resolve-id/test-src/named-export.js b/test/unit-tests/resolve-id/test-src/named-export.js new file mode 100644 index 00000000..1c6da59f --- /dev/null +++ b/test/unit-tests/resolve-id/test-src/named-export.js @@ -0,0 +1 @@ +export const named = 'named export' diff --git a/test/unit-tests/resolve-id/test-src/proxy-export.js b/test/unit-tests/resolve-id/test-src/proxy-export.js new file mode 100644 index 00000000..4e6de84a --- /dev/null +++ b/test/unit-tests/resolve-id/test-src/proxy-export.js @@ -0,0 +1,2 @@ +export { named } from './named-export' +export { default as proxiedDefault } from './default-export'