Skip to content

Commit 46059b1

Browse files
committed
Add support for subpath polyfills
1 parent 8d68d4f commit 46059b1

File tree

5 files changed

+104
-20
lines changed

5 files changed

+104
-20
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ export default defineConfig({
6969
// Override the default polyfills for specific modules.
7070
overrides: {
7171
// Since `fs` is not supported in browsers, we can use the `memfs` package to polyfill it.
72-
fs: 'memfs',
72+
'fs': 'memfs',
73+
// Subpaths can be specified as well.
74+
'path/posix': 'path-browserify',
7375
},
7476
// Whether to polyfill `node:` protocol imports.
7577
protocolImports: true,

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@
9595
},
9696
"dependencies": {
9797
"@rollup/plugin-inject": "^5.0.5",
98+
"browser-resolve": "^2.0.0",
9899
"node-stdlib-browser": "^1.2.0"
99100
},
100101
"devDependencies": {
101102
"@playwright/test": "^1.40.1",
103+
"@types/browser-resolve": "^2.0.4",
102104
"@types/node": "^18.18.8",
103105
"buffer": "6.0.3",
104106
"esbuild": "^0.19.8",

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { createRequire } from 'node:module'
22
import inject from '@rollup/plugin-inject'
3+
import browserResolve from 'browser-resolve'
34
import stdLibBrowser from 'node-stdlib-browser'
45
import { handleCircularDependancyWarning } from 'node-stdlib-browser/helpers/rollup/plugin'
56
import esbuildPlugin from 'node-stdlib-browser/helpers/esbuild/plugin'
67
import type { Plugin } from 'vite'
7-
import { compareModuleNames, isEnabled, isNodeProtocolImport, toRegExp, withoutNodeProtocol } from './utils'
8+
import { compareModuleNames, isEnabled, isNodeProtocolImport, resolvePolyfill, toEntries, toRegExp, withoutNodeProtocol } from './utils'
89

9-
export type BuildTarget = 'build' | 'dev'
10+
export type BareModuleName<T = ModuleName> = T extends `node:${infer P}` ? P : never
11+
export type BareModuleNameWithSubpath<T = ModuleName> = T extends `node:${infer P}` ? `${P}/${string}` : never
1012
export type BooleanOrBuildTarget = boolean | BuildTarget
13+
export type BuildTarget = 'build' | 'dev'
1114
export type ModuleName = keyof typeof stdLibBrowser
12-
export type ModuleNameWithoutNodePrefix<T = ModuleName> = T extends `node:${infer P}` ? P : never
15+
export type OverrideOptions = {
16+
17+
}
1318

1419
export type PolyfillOptions = {
1520
/**
@@ -22,7 +27,7 @@ export type PolyfillOptions = {
2227
* })
2328
* ```
2429
*/
25-
include?: ModuleNameWithoutNodePrefix[],
30+
include?: BareModuleName[],
2631
/**
2732
* @example
2833
*
@@ -32,7 +37,7 @@ export type PolyfillOptions = {
3237
* })
3338
* ```
3439
*/
35-
exclude?: ModuleNameWithoutNodePrefix[],
40+
exclude?: BareModuleName[],
3641
/**
3742
* Specify whether specific globals should be polyfilled.
3843
*
@@ -66,7 +71,7 @@ export type PolyfillOptions = {
6671
* })
6772
* ```
6873
*/
69-
overrides?: { [Key in ModuleNameWithoutNodePrefix]?: string },
74+
overrides?: { [Key in BareModuleName | BareModuleNameWithSubpath]?: string },
7075
/**
7176
* Specify whether the Node protocol version of an import (e.g. `node:buffer`) should be polyfilled too.
7277
*
@@ -76,14 +81,14 @@ export type PolyfillOptions = {
7681
}
7782

7883
export type PolyfillOptionsResolved = {
79-
include: ModuleNameWithoutNodePrefix[],
80-
exclude: ModuleNameWithoutNodePrefix[],
84+
include: BareModuleName[],
85+
exclude: BareModuleName[],
8186
globals: {
8287
Buffer: BooleanOrBuildTarget,
8388
global: BooleanOrBuildTarget,
8489
process: BooleanOrBuildTarget,
8590
},
86-
overrides: { [Key in ModuleNameWithoutNodePrefix]?: string },
91+
overrides: { [Key in BareModuleName | BareModuleNameWithSubpath]?: string },
8792
protocolImports: boolean,
8893
}
8994

@@ -153,16 +158,16 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
153158
return optionsResolved.exclude.some((excludedName) => compareModuleNames(moduleName, excludedName))
154159
}
155160

156-
const toOverride = (name: ModuleNameWithoutNodePrefix): string | void => {
157-
if (isEnabled(optionsResolved.globals.Buffer, 'dev') && /^buffer$/.test(name)) {
161+
const toOverride = (name: BareModuleName): string | void => {
162+
if (/^buffer$/.test(name)) {
158163
return 'vite-plugin-node-polyfills/shims/buffer'
159164
}
160165

161-
if (isEnabled(optionsResolved.globals.global, 'dev') && /^global$/.test(name)) {
166+
if (/^global$/.test(name)) {
162167
return 'vite-plugin-node-polyfills/shims/global'
163168
}
164169

165-
if (isEnabled(optionsResolved.globals.process, 'dev') && /^process$/.test(name)) {
170+
if (/^process$/.test(name)) {
166171
return 'vite-plugin-node-polyfills/shims/process'
167172
}
168173

@@ -201,6 +206,7 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
201206

202207
return {
203208
name: 'vite-plugin-node-polyfills',
209+
enforce: 'pre',
204210
config: (config, env) => {
205211
const isDev = env.command === 'serve'
206212

@@ -246,21 +252,25 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
246252
...globalShimPaths,
247253
],
248254
plugins: [
249-
esbuildPlugin(polyfills),
255+
esbuildPlugin({
256+
...polyfills,
257+
}),
250258
// Supress the 'injected path "..." cannot be marked as external' error in Vite 4 (emitted by esbuild).
251259
// https://github.com/evanw/esbuild/blob/edede3c49ad6adddc6ea5b3c78c6ea7507e03020/internal/bundler/bundler.go#L1469
252260
{
253261
name: 'vite-plugin-node-polyfills-shims-resolver',
254-
setup(build) {
262+
setup: (build) => {
255263
for (const globalShimPath of globalShimPaths) {
256264
const globalShimsFilter = toRegExp(globalShimPath)
257265

258266
// https://esbuild.github.io/plugins/#on-resolve
259267
build.onResolve({ filter: globalShimsFilter }, () => {
268+
const resolved = browserResolve.sync(globalShimPath)
269+
260270
return {
261271
// https://github.com/evanw/esbuild/blob/edede3c49ad6adddc6ea5b3c78c6ea7507e03020/internal/bundler/bundler.go#L1468
262272
external: false,
263-
path: globalShimPath,
273+
path: resolved,
264274
}
265275
})
266276
}
@@ -277,5 +287,28 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
277287
},
278288
}
279289
},
290+
async resolveId(id) {
291+
for (const [moduleName, modulePath] of toEntries(polyfills)) {
292+
if (id.startsWith(modulePath)) {
293+
// Grab the subpath without the forward slash. E.g. `path/posix` -> `posix`
294+
const moduleSubpath = id.slice(modulePath.length + 1)
295+
296+
if (moduleSubpath.length > 0) {
297+
const moduleNameWithoutProtocol = withoutNodeProtocol(moduleName)
298+
const overrideName = `${moduleNameWithoutProtocol}/${moduleSubpath}` as const
299+
const override = optionsResolved.overrides[overrideName]
300+
301+
if (!override) {
302+
// Todo: Maybe throw error?
303+
return undefined
304+
}
305+
306+
return await resolvePolyfill(this, override)
307+
}
308+
309+
return browserResolve.sync(modulePath)
310+
}
311+
}
312+
},
280313
}
281314
}

src/utils.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { BooleanOrBuildTarget, ModuleName, ModuleNameWithoutNodePrefix } from './index'
1+
import type { PluginContext } from 'rollup'
2+
import type { BareModuleName, BooleanOrBuildTarget, ModuleName } from './index'
3+
4+
export type Identity<T> = T
5+
export type ObjectToEntries<T> = Identity<{ [K in keyof T]: [K, T[K]] }[keyof T][]>
26

37
export const compareModuleNames = (moduleA: ModuleName, moduleB: ModuleName) => {
48
return withoutNodeProtocol(moduleA) === withoutNodeProtocol(moduleB)
@@ -15,13 +19,37 @@ export const isNodeProtocolImport = (name: string) => {
1519
return name.startsWith('node:')
1620
}
1721

22+
export const resolvePolyfill = async (context: PluginContext, name: string) => {
23+
const consumerResolved = await context.resolve(name)
24+
25+
if (consumerResolved) {
26+
return consumerResolved
27+
}
28+
29+
const provider = await context.resolve('vite-plugin-node-polyfills')
30+
const providerResolved = await context.resolve(name, provider!.id)
31+
32+
if (providerResolved) {
33+
return providerResolved
34+
}
35+
36+
const upstream = await context.resolve('node-stdlib-browser', provider!.id)
37+
const upstreamResolved = await context.resolve(name, upstream!.id)
38+
39+
return upstreamResolved
40+
}
41+
42+
export const toEntries = <T extends Record<PropertyKey, unknown>>(object: T): ObjectToEntries<T> => {
43+
return Object.entries(object) as ObjectToEntries<T>
44+
}
45+
1846
export const toRegExp = (text: string) => {
1947
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
2048
const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
2149

2250
return new RegExp(`^${escapedText}$`)
2351
}
2452

25-
export const withoutNodeProtocol = (name: ModuleName): ModuleNameWithoutNodePrefix => {
26-
return name.replace(/^node:/, '') as ModuleNameWithoutNodePrefix
53+
export const withoutNodeProtocol = (name: ModuleName): BareModuleName => {
54+
return name.replace(/^node:/, '') as BareModuleName
2755
}

0 commit comments

Comments
 (0)