Skip to content

Commit

Permalink
feat: Add isEntry flag to resolveId hook
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst authored Jul 10, 2022
1 parent 454d8ca commit 491c2ba
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 47 deletions.
6 changes: 5 additions & 1 deletion src/esbuild/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ export function getEsbuildPlugin <UserOptions = {}> (

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) {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface UnpluginOptions {
transformInclude?: (id: string) => boolean;
transform?: (this: UnpluginBuildContext & UnpluginContext, code: string, id: string) => Thenable<TransformResult>;
load?: (this: UnpluginBuildContext & UnpluginContext, id: string) => Thenable<TransformResult>
resolveId?: (id: string, importer?: string) => Thenable<string | ExternalIdResult | null | undefined>
resolveId?: (id: string, importer: string | undefined, options: { isEntry: boolean }) => Thenable<string | ExternalIdResult | null | undefined>
watchChange?: (this: UnpluginBuildContext, id: string, change: {event: 'create' | 'update' | 'delete'}) => void

// framework specify extends
Expand Down
95 changes: 50 additions & 45 deletions src/webpack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -79,58 +79,63 @@ export function getWebpackPlugin<UserOptions = {}> (
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
Expand Down
113 changes: 113 additions & 0 deletions test/unit-tests/resolve-id/resolve-id.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
1 change: 1 addition & 0 deletions test/unit-tests/resolve-id/test-src/default-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'some string'
3 changes: 3 additions & 0 deletions test/unit-tests/resolve-id/test-src/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { named, proxiedDefault } from './proxy-export'

process.stdout.write(JSON.stringify({ named, proxiedDefault }))
1 change: 1 addition & 0 deletions test/unit-tests/resolve-id/test-src/named-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const named = 'named export'
2 changes: 2 additions & 0 deletions test/unit-tests/resolve-id/test-src/proxy-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { named } from './named-export'
export { default as proxiedDefault } from './default-export'

0 comments on commit 491c2ba

Please sign in to comment.