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: Unvirtualize importer paths in webpack and rspack #430

Merged
merged 2 commits into from
Oct 28, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
})