Skip to content

Commit 6c2be6c

Browse files
committed
Add support for subpath polyfills
1 parent b8193a2 commit 6c2be6c

File tree

5 files changed

+104
-25
lines changed

5 files changed

+104
-25
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
@@ -93,10 +93,12 @@
9393
},
9494
"dependencies": {
9595
"@rollup/plugin-inject": "^5.0.5",
96+
"browser-resolve": "^2.0.0",
9697
"node-stdlib-browser": "^1.2.0"
9798
},
9899
"devDependencies": {
99100
"@playwright/test": "^1.40.1",
101+
"@types/browser-resolve": "^2.0.4",
100102
"@types/node": "^18.18.8",
101103
"buffer": "6.0.3",
102104
"esbuild": "^0.19.8",

pnpm-lock.yaml

Lines changed: 16 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: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { createRequire } from 'node:module'
21
import inject from '@rollup/plugin-inject'
2+
import browserResolve from 'browser-resolve'
33
import stdLibBrowser from 'node-stdlib-browser'
44
import { handleCircularDependancyWarning } from 'node-stdlib-browser/helpers/rollup/plugin'
55
import esbuildPlugin from 'node-stdlib-browser/helpers/esbuild/plugin'
66
import type { Plugin } from 'vite'
7-
import { compareModuleNames, isEnabled, isNodeProtocolImport, toRegExp, withoutNodeProtocol } from './utils'
7+
import { compareModuleNames, isEnabled, isNodeProtocolImport, resolvePolyfill, toEntries, toRegExp, withoutNodeProtocol } from './utils'
88

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

1418
export type PolyfillOptions = {
1519
/**
@@ -22,7 +26,7 @@ export type PolyfillOptions = {
2226
* })
2327
* ```
2428
*/
25-
include?: ModuleNameWithoutNodePrefix[],
29+
include?: BareModuleName[],
2630
/**
2731
* @example
2832
*
@@ -32,7 +36,7 @@ export type PolyfillOptions = {
3236
* })
3337
* ```
3438
*/
35-
exclude?: ModuleNameWithoutNodePrefix[],
39+
exclude?: BareModuleName[],
3640
/**
3741
* Specify whether specific globals should be polyfilled.
3842
*
@@ -66,7 +70,7 @@ export type PolyfillOptions = {
6670
* })
6771
* ```
6872
*/
69-
overrides?: { [Key in ModuleNameWithoutNodePrefix]?: string },
73+
overrides?: { [Key in BareModuleName | BareModuleNameWithSubpath]?: string },
7074
/**
7175
* Specify whether the Node protocol version of an import (e.g. `node:buffer`) should be polyfilled too.
7276
*
@@ -76,14 +80,14 @@ export type PolyfillOptions = {
7680
}
7781

7882
export type PolyfillOptionsResolved = {
79-
include: ModuleNameWithoutNodePrefix[],
80-
exclude: ModuleNameWithoutNodePrefix[],
83+
include: BareModuleName[],
84+
exclude: BareModuleName[],
8185
globals: {
8286
Buffer: BooleanOrBuildTarget,
8387
global: BooleanOrBuildTarget,
8488
process: BooleanOrBuildTarget,
8589
},
86-
overrides: { [Key in ModuleNameWithoutNodePrefix]?: string },
90+
overrides: { [Key in BareModuleName | BareModuleNameWithSubpath]?: string },
8791
protocolImports: boolean,
8892
}
8993

@@ -127,11 +131,10 @@ const globalShimsBanner = [
127131
* ```
128132
*/
129133
export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
130-
const require = createRequire(import.meta.url)
131134
const globalShimPaths = [
132-
require.resolve('vite-plugin-node-polyfills/shims/buffer'),
133-
require.resolve('vite-plugin-node-polyfills/shims/global'),
134-
require.resolve('vite-plugin-node-polyfills/shims/process'),
135+
'vite-plugin-node-polyfills/shims/buffer',
136+
'vite-plugin-node-polyfills/shims/global',
137+
'vite-plugin-node-polyfills/shims/process',
135138
]
136139
const optionsResolved: PolyfillOptionsResolved = {
137140
include: [],
@@ -155,16 +158,16 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
155158
return optionsResolved.exclude.some((excludedName) => compareModuleNames(moduleName, excludedName))
156159
}
157160

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

163-
if (isEnabled(optionsResolved.globals.global, 'dev') && /^global$/.test(name)) {
166+
if (/^global$/.test(name)) {
164167
return 'vite-plugin-node-polyfills/shims/global'
165168
}
166169

167-
if (isEnabled(optionsResolved.globals.process, 'dev') && /^process$/.test(name)) {
170+
if (/^process$/.test(name)) {
168171
return 'vite-plugin-node-polyfills/shims/process'
169172
}
170173

@@ -189,6 +192,7 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
189192

190193
return {
191194
name: 'vite-plugin-node-polyfills',
195+
enforce: 'pre',
192196
config: (config, env) => {
193197
const isDev = env.command === 'serve'
194198

@@ -236,21 +240,25 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
236240
...globalShimPaths,
237241
],
238242
plugins: [
239-
esbuildPlugin(polyfills),
243+
esbuildPlugin({
244+
...polyfills,
245+
}),
240246
// Supress the 'injected path "..." cannot be marked as external' error in Vite 4 (emitted by esbuild).
241247
// https://github.com/evanw/esbuild/blob/edede3c49ad6adddc6ea5b3c78c6ea7507e03020/internal/bundler/bundler.go#L1469
242248
{
243249
name: 'vite-plugin-node-polyfills-shims-resolver',
244-
setup(build) {
250+
setup: (build) => {
245251
for (const globalShimPath of globalShimPaths) {
246252
const globalShimsFilter = toRegExp(globalShimPath)
247253

248254
// https://esbuild.github.io/plugins/#on-resolve
249255
build.onResolve({ filter: globalShimsFilter }, () => {
256+
const resolved = browserResolve.sync(globalShimPath)
257+
250258
return {
251259
// https://github.com/evanw/esbuild/blob/edede3c49ad6adddc6ea5b3c78c6ea7507e03020/internal/bundler/bundler.go#L1468
252260
external: false,
253-
path: globalShimPath,
261+
path: resolved,
254262
}
255263
})
256264
}
@@ -267,5 +275,28 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin => {
267275
},
268276
}
269277
},
278+
async resolveId(id) {
279+
for (const [moduleName, modulePath] of toEntries(polyfills)) {
280+
if (id.startsWith(modulePath)) {
281+
// Grab the subpath without the forward slash. E.g. `path/posix` -> `posix`
282+
const moduleSubpath = id.slice(modulePath.length + 1)
283+
284+
if (moduleSubpath.length > 0) {
285+
const moduleNameWithoutProtocol = withoutNodeProtocol(moduleName)
286+
const overrideName = `${moduleNameWithoutProtocol}/${moduleSubpath}` as const
287+
const override = optionsResolved.overrides[overrideName]
288+
289+
if (!override) {
290+
// Todo: Maybe throw error?
291+
return undefined
292+
}
293+
294+
return await resolvePolyfill(this, override)
295+
}
296+
297+
return browserResolve.sync(modulePath)
298+
}
299+
}
300+
},
270301
}
271302
}

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)