diff --git a/src/rspack/index.ts b/src/rspack/index.ts index 717ad625..70c7280a 100644 --- a/src/rspack/index.ts +++ b/src/rspack/index.ts @@ -75,9 +75,12 @@ export function getRspackPlugin>( const id = normalizeAbsolutePath(resolveData.request) const requestContext = resolveData.contextInfo - const importer = requestContext.issuer !== '' ? requestContext.issuer : undefined + let importer = requestContext.issuer !== '' ? requestContext.issuer : undefined const isEntry = requestContext.issuer === '' + if (importer?.startsWith(plugin.__virtualModulePrefix)) + importer = decodeURIComponent(importer.slice(plugin.__virtualModulePrefix.length)) + const context = createBuildContext(compiler, compilation) let error: Error | undefined const pluginContext: UnpluginContext = { diff --git a/src/webpack/index.ts b/src/webpack/index.ts index 169b4f0f..e215c962 100644 --- a/src/webpack/index.ts +++ b/src/webpack/index.ts @@ -84,9 +84,12 @@ export function getWebpackPlugin>( const id = normalizeAbsolutePath(request.request) const requestContext = (request as unknown as { context: { issuer: string } }).context - const importer = requestContext.issuer !== '' ? requestContext.issuer : undefined + let importer = requestContext.issuer !== '' ? requestContext.issuer : undefined const isEntry = requestContext.issuer === '' + if (importer?.startsWith(plugin.__virtualModulePrefix)) + importer = decodeURIComponent(importer.slice(plugin.__virtualModulePrefix.length)) + // call hook // resolveContext.fileDependencies is typed as a WriteOnlySet, so make our own copy here // so we can return it from getWatchFiles. diff --git a/test/unit-tests/virtual-id/test-src/entry.js b/test/unit-tests/virtual-id/test-src/entry.js new file mode 100644 index 00000000..dd7beb38 --- /dev/null +++ b/test/unit-tests/virtual-id/test-src/entry.js @@ -0,0 +1 @@ +import './imported.js' diff --git a/test/unit-tests/virtual-id/test-src/imported.js b/test/unit-tests/virtual-id/test-src/imported.js new file mode 100644 index 00000000..08d065f2 --- /dev/null +++ b/test/unit-tests/virtual-id/test-src/imported.js @@ -0,0 +1 @@ +export default 'test' diff --git a/test/unit-tests/virtual-id/virtual-id.test.ts b/test/unit-tests/virtual-id/virtual-id.test.ts new file mode 100644 index 00000000..3831bb6f --- /dev/null +++ b/test/unit-tests/virtual-id/virtual-id.test.ts @@ -0,0 +1,157 @@ +import type { UnpluginOptions, VitePlugin } from 'unplugin' +import type { Mock } from 'vitest' +import * as fs from 'fs' +import * as path from 'path' +import { createUnplugin } from 'unplugin' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { build, toArray } from '../utils' + +function createUnpluginWithCallbacks(resolveIdCallback: UnpluginOptions['resolveId'], loadCallback: UnpluginOptions['load']) { + return createUnplugin(() => ({ + name: 'test-plugin', + resolveId: resolveIdCallback, + load: loadCallback, + })) +} + +function createResolveIdHook(): Mock { + const mockResolveIdHook = vi.fn((id: string, importer: string | undefined): string => { + // rspack seems to generate paths of the form \C:\... on Windows. + // Remove the leading \ + if (importer && /^\\[A-Z]:\\/.test(importer)) + importer = importer.slice(1) + id = path.resolve(path.dirname(importer ?? ''), id) + return `${id}.js` + }) + return mockResolveIdHook +} + +function createLoadHook(): Mock { + const mockLoadHook = vi.fn((id: string): string => { + expect(id).toMatch(/\.js\.js$/) + id = id.slice(0, -3) + return fs.readFileSync(id, { encoding: 'utf-8' }) + }) + return mockLoadHook +} + +function checkResolveIdHook(resolveIdCallback: Mock): void { + expect(resolveIdCallback).toHaveBeenCalledWith( + expect.stringMatching(/(?:\/|\\)entry\.js$/), + undefined, + expect.objectContaining({ isEntry: true }), + ) + + expect(resolveIdCallback).toHaveBeenCalledWith( + './imported.js', + expect.stringMatching(/(?:\/|\\)entry\.js\.js$/), + expect.objectContaining({ isEntry: false }), + ) +} + +function checkLoadHook(loadCallback: Mock): void { + expect(loadCallback).toHaveBeenCalledWith( + expect.stringMatching(/(?:\/|\\)entry\.js\.js$/), + ) + + expect(loadCallback).toHaveBeenCalledWith( + expect.stringMatching(/(?:\/|\\)imported\.js\.js$/), + ) +} + +describe('virtual ids', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('vite', async () => { + const mockResolveIdHook = createResolveIdHook() + const mockLoadHook = createLoadHook() + const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).vite + // we need to define `enforce` here for the plugin to be run + const plugins = toArray(plugin()).map((plugin): VitePlugin => ({ ...plugin, enforce: 'pre' })) + + await build.vite({ + clearScreen: false, + plugins: [plugins], + build: { + lib: { + entry: path.resolve(__dirname, 'test-src/entry.js'), + name: 'TestLib', + }, + write: false, // don't output anything + }, + }) + + checkResolveIdHook(mockResolveIdHook) + checkLoadHook(mockLoadHook) + }) + + it('rollup', async () => { + const mockResolveIdHook = createResolveIdHook() + const mockLoadHook = createLoadHook() + const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).rollup + + await build.rollup({ + input: path.resolve(__dirname, 'test-src/entry.js'), + plugins: [plugin()], + }) + + checkResolveIdHook(mockResolveIdHook) + checkLoadHook(mockLoadHook) + }) + + it('webpack', async () => { + const mockResolveIdHook = createResolveIdHook() + const mockLoadHook = createLoadHook() + const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).webpack + + await new Promise((resolve) => { + build.webpack( + { + entry: path.resolve(__dirname, 'test-src/entry.js'), + plugins: [plugin()], + }, + resolve, + ) + }) + + checkResolveIdHook(mockResolveIdHook) + checkLoadHook(mockLoadHook) + }) + + it('rspack', async () => { + const mockResolveIdHook = createResolveIdHook() + const mockLoadHook = createLoadHook() + const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).rspack + + await new Promise((resolve) => { + build.rspack( + { + entry: path.resolve(__dirname, 'test-src/entry.js'), + plugins: [plugin()], + }, + resolve, + ) + }) + + checkResolveIdHook(mockResolveIdHook) + checkLoadHook(mockLoadHook) + }) + + it('esbuild', async () => { + const mockResolveIdHook = createResolveIdHook() + const mockLoadHook = createLoadHook() + const plugin = createUnpluginWithCallbacks(mockResolveIdHook, mockLoadHook).esbuild + + await build.esbuild({ + entryPoints: [path.resolve(__dirname, 'test-src/entry.js')], + plugins: [plugin()], + bundle: true, // actually traverse imports + write: false, // don't pollute console + }) + + checkResolveIdHook(mockResolveIdHook) + checkLoadHook(mockLoadHook) + }) +})