Skip to content

Commit

Permalink
fix: Unvirtualize importer paths in webpack and rspack (#430)
Browse files Browse the repository at this point in the history
* Unvirtualize `importer` paths in webpack and rspack

* Add tests
  • Loading branch information
edemaine authored Oct 28, 2024
1 parent f6ceb50 commit 52f0b79
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 2 deletions.
5 changes: 4 additions & 1 deletion src/rspack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,12 @@ export function getRspackPlugin<UserOptions = Record<string, never>>(
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 = {
Expand Down
5 changes: 4 additions & 1 deletion src/webpack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@ export function getWebpackPlugin<UserOptions = Record<string, never>>(
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.
Expand Down
1 change: 1 addition & 0 deletions test/unit-tests/virtual-id/test-src/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './imported.js'
1 change: 1 addition & 0 deletions test/unit-tests/virtual-id/test-src/imported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'test'
157 changes: 157 additions & 0 deletions test/unit-tests/virtual-id/virtual-id.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit 52f0b79

Please sign in to comment.