diff --git a/.github/actions/override-dependency/action.yml b/.github/actions/override-dependency/action.yml new file mode 100644 index 0000000..4d53360 --- /dev/null +++ b/.github/actions/override-dependency/action.yml @@ -0,0 +1,27 @@ +name: Override dependency +description: Override the pinned version of a specified dependency +inputs: + name: + description: The name of the dependency to override. + required: true + version: + description: The version of the dependency to install. + required: true +runs: + using: composite + steps: + - name: Print package.json file + shell: bash + run: cat package.json + - name: Generate package.overrides.json file + shell: bash + run: jq '.pnpm.overrides["${{ inputs.name }}"] = "${{ inputs.version }}"' package.json > package.overrides.json + - name: Replace package.json with package.overrides.json + shell: bash + run: mv package.overrides.json package.json + - name: Print package.json file + shell: bash + run: cat package.json + - name: Install overrides + shell: bash + run: pnpm install --no-frozen-lockfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7491ef1..2f29bc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: branches: - main workflow_dispatch: +permissions: + contents: none jobs: lint: runs-on: ubuntu-latest @@ -14,7 +16,7 @@ jobs: - name: Run the linter run: pnpm lint test-e2e: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install-dependencies @@ -23,18 +25,17 @@ jobs: - run: pnpm playwright install --with-deps - run: pnpm -r test:e2e test-e2e-vite-8: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install-dependencies - run: pnpm build:core - run: pnpm build:shims - run: pnpm playwright install --with-deps - - name: update overrides to use vite 8 and re-install - run: | - jq '.pnpm.overrides.vite = "8.0.1"' package.json > package.tmp.json - mv package.tmp.json package.json - pnpm i --no-frozen-lockfile + - uses: ./.github/actions/override-dependency + with: + name: vite + version: 8.0.1 - run: pnpm -r test:e2e test-unit: runs-on: ubuntu-latest @@ -44,6 +45,22 @@ jobs: - run: pnpm build:core - run: pnpm build:shims - run: pnpm -r test + test-unit-vite-8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-dependencies + - run: pnpm build:core + - run: pnpm build:shims + - uses: ./.github/actions/override-dependency + with: + name: vite + version: 8.0.1 + - uses: ./.github/actions/override-dependency + with: + name: vitest + version: ^4.1.2 + - run: pnpm -r test typecheck: runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index b2b6442..be7c0c2 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "node-stdlib-browser": "^1.3.1" }, "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.60.0", "@types/node": "^18.18.8", "buffer": "6.0.3", "esbuild": "^0.19.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0763d84..79c3ed2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: version: 1.3.1 devDependencies: '@playwright/test': - specifier: ^1.40.1 - version: 1.40.1 + specifier: ^1.60.0 + version: 1.60.0 '@types/node': specifier: ^18.18.8 version: 18.18.8 @@ -548,9 +548,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@playwright/test@1.40.1': - resolution: {integrity: sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==} - engines: {node: '>=16'} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} hasBin: true '@polka/url@1.0.0-next.23': @@ -1178,8 +1178,8 @@ packages: destr@1.2.2: resolution: {integrity: sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} diff-sequences@29.6.3: @@ -2388,14 +2388,14 @@ packages: pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} - playwright-core@1.40.1: - resolution: {integrity: sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==} - engines: {node: '>=16'} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} hasBin: true - playwright@1.40.1: - resolution: {integrity: sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==} - engines: {node: '>=16'} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} hasBin: true pluralize@8.0.0: @@ -3416,9 +3416,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - '@playwright/test@1.40.1': + '@playwright/test@1.60.0': dependencies: - playwright: 1.40.1 + playwright: 1.60.0 '@polka/url@1.0.0-next.23': {} @@ -4154,7 +4154,7 @@ snapshots: destr@1.2.2: {} - detect-libc@2.0.4: + detect-libc@2.1.2: optional: true diff-sequences@29.6.3: {} @@ -5118,7 +5118,7 @@ snapshots: lightningcss@1.30.1: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optionalDependencies: lightningcss-darwin-arm64: 1.30.1 lightningcss-darwin-x64: 1.30.1 @@ -5532,11 +5532,11 @@ snapshots: mlly: 1.4.2 pathe: 1.1.1 - playwright-core@1.40.1: {} + playwright-core@1.60.0: {} - playwright@1.40.1: + playwright@1.60.0: dependencies: - playwright-core: 1.40.1 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 0000000..fa2cd66 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,24 @@ +import { + type BooleanOrBuildTarget, + type BuildTarget, + isEnabled, +} from './utils' + +export type GlobalName = typeof globals[number] + +export const globals = [ + 'buffer', + 'global', + 'process', +] as const + +export const getGlobalsToHandle = (options: { + globals: { [Name in GlobalName]: BooleanOrBuildTarget }, + target: BuildTarget, +}): GlobalName[] => { + return globals.filter((global) => { + const value = options.globals[global] + + return isEnabled(value, options.target) + }) +} diff --git a/src/index.ts b/src/index.ts index 3216043..8e0ad6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,14 +4,20 @@ import stdLibBrowser from 'node-stdlib-browser' import { handleCircularDependancyWarning } from 'node-stdlib-browser/helpers/rollup/plugin' import esbuildPlugin from 'node-stdlib-browser/helpers/esbuild/plugin' import type { Plugin } from 'vite' -import { compareModuleNames, isEnabled, isNodeProtocolImport, toRegExp, withoutNodeProtocol } from './utils' - -type TransformHook = Extract - -export type BuildTarget = 'build' | 'dev' -export type BooleanOrBuildTarget = boolean | BuildTarget -export type ModuleName = keyof typeof stdLibBrowser -export type ModuleNameWithoutNodePrefix = T extends `node:${infer P}` ? P : never +import { getModulesToPolyfill } from './modules' +import { buildTrailingSlashNormalizer } from './plugins' +import { + type BooleanOrBuildTarget, + type ModuleName, + type ModuleNameWithoutNodePrefix, + type TransformHook, + compareModuleNames, + globalShimBanners, + isEnabled, + isNodeProtocolImport, + toRegExp, + withoutNodeProtocol, +} from './utils' export type PolyfillOptions = { /** @@ -89,21 +95,6 @@ export type PolyfillOptionsResolved = { protocolImports: boolean, } -const globalShimBanners = { - buffer: [ - `import __buffer_polyfill from 'vite-plugin-node-polyfills/shims/buffer'`, - `globalThis.Buffer = globalThis.Buffer || __buffer_polyfill`, - ], - global: [ - `import __global_polyfill from 'vite-plugin-node-polyfills/shims/global'`, - `globalThis.global = globalThis.global || __global_polyfill`, - ], - process: [ - `import __process_polyfill from 'vite-plugin-node-polyfills/shims/process'`, - `globalThis.process = globalThis.process || __process_polyfill`, - ], -} - /** * Returns a Vite plugin to polyfill Node's Core Modules for browser environments. Supports `node:` protocol imports. * @@ -147,6 +138,16 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin[] => { }, } + const modulesToPolyfill = getModulesToPolyfill({ + modulesToExclude: optionsResolved.exclude, + modulesToInclude: optionsResolved.include, + }) + + const trailingSlashNormalizer = buildTrailingSlashNormalizer({ + modules: modulesToPolyfill, + protocolImports: optionsResolved.protocolImports, + }) + const isExcluded = (moduleName: ModuleName) => { if (optionsResolved.include.length > 0) { return !optionsResolved.include.some((includedName) => compareModuleNames(moduleName, includedName)) @@ -220,9 +221,11 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin[] => { const plugin: Plugin = { name: 'vite-plugin-node-polyfills', config(config, env) { + const isBuild = env.command === 'build' const isDev = env.command === 'serve' // @ts-expect-error - this.meta.rolldownVersion only exists with rolldown-vite 7+ const isRolldownVite = !!this?.meta?.rolldownVersion + const isNativeInjectAvailable = isBuild && isRolldownVite // https://github.com/niksy/node-stdlib-browser/blob/3e7cd7f3d115ac5c4593b550e7d8c4a82a0d4ac4/README.md?plain=1#L203-L209 const defines = { @@ -238,7 +241,6 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin[] => { ...(isEnabled(optionsResolved.globals.process, 'build') ? { process: 'vite-plugin-node-polyfills/shims/process' } : {}), } - const isNativeInjectAvailable = env.command === 'build' && isRolldownVite rawInjectPlugin = (Object.keys(shimsToInject).length > 0 && !isNativeInjectAvailable) ? inject(shimsToInject) as Plugin : false if (rawInjectPlugin === false) { delete injectPlugin.transform @@ -283,6 +285,7 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin[] => { define: defines, }, plugins: [ + trailingSlashNormalizer, { name: 'vite-plugin-node-polyfills:optimizer', banner: isDev ? globalShimsBanner : undefined, @@ -331,8 +334,10 @@ export const nodePolyfills = (options: PolyfillOptions = {}): Plugin[] => { } }, } + return [ + trailingSlashNormalizer, injectPlugin, plugin, - ] + ].flat() } diff --git a/src/modules.ts b/src/modules.ts new file mode 100644 index 0000000..bbeba9a --- /dev/null +++ b/src/modules.ts @@ -0,0 +1,65 @@ +export type ModuleName = typeof modules[number] +export type ModuleNameWithNodePrefix = `node:${ModuleName}` + +export const modules = [ + '_stream_duplex', + '_stream_passthrough', + '_stream_readable', + '_stream_transform', + '_stream_writable', + 'assert', + 'buffer', + 'child_process', + 'cluster', + 'console', + 'constants', + 'crypto', + 'dgram', + 'dns', + 'domain', + 'events', + 'fs', + 'http', + 'http2', + 'https', + 'module', + 'net', + 'os', + 'path', + 'process', + 'punycode', + 'querystring', + 'readline', + 'repl', + 'stream', + 'string_decoder', + 'sys', + 'timers', + 'timers/promises', + 'tls', + 'tty', + 'url', + 'util', + 'vm', + 'zlib', +] as const + +export const getModulesToPolyfill = ({ + modulesToExclude, + modulesToInclude, +}: { + modulesToExclude: ModuleName[], + modulesToInclude: ModuleName[], +}): ModuleName[] => { + return modules.filter((module) => { + if (modulesToInclude.length > 0) { + return modulesToInclude.includes(module) + } + + return !modulesToExclude.includes(module) + }) +} + +export const isModuleName = (name: string): name is ModuleName => { + return modules.includes(name as ModuleName) +} diff --git a/src/plugins.ts b/src/plugins.ts new file mode 100644 index 0000000..d64aa24 --- /dev/null +++ b/src/plugins.ts @@ -0,0 +1,55 @@ +import { type Plugin } from 'vite' +import { globals } from './globals' +import { type ModuleName } from './modules' +import * as regex from './regex' + +export const buildTrailingSlashNormalizer = ({ + modules, + protocolImports, +}: { + modules: ModuleName[], + protocolImports: boolean, +}): Plugin[] => { + const trailingSlashNormalizerRegex = regex.compose([ + regex.any([ + regex.join([ + regex.when( + protocolImports, + regex.optional('node:'), + ), + regex.any(modules), + ]), + regex.join([ + 'vite-plugin-node-polyfills/shims/', + regex.any(globals), + ]), + ]), + '/', + ]) + + return [ + { + // Vite v8.0.0+ does not support trailing slashes on package subpaths. + name: 'vite-plugin-node-polyfills:trailing-slash-normalizer', + enforce: 'pre', + resolveId: { + // @ts-expect-error This property is only supported in Vite v6.3.0+, so + // we must run the check inside the handler to maintain compatibility + // with older Vite versions. + filter: { id: trailingSlashNormalizerRegex }, + async handler(source, importer, options) { + if (!trailingSlashNormalizerRegex.test(source)) { + return + } + + const sourceWithoutTrailingSlash = source.replace(/\/$/, '') + + return this.resolve(sourceWithoutTrailingSlash, importer, { + ...options, + skipSelf: true, + }) + }, + }, + }, + ] +} diff --git a/src/regex.ts b/src/regex.ts new file mode 100644 index 0000000..af71945 --- /dev/null +++ b/src/regex.ts @@ -0,0 +1,29 @@ +export const any = (patterns: readonly string[]) => { + return group(patterns.join('|')) +} + +export const compose = (patterns: readonly string[]) => { + const pattern = patterns.join('') + + return new RegExp(`^${pattern}$`) +} + +export const group = (pattern: string) => { + return `(?:${pattern})` +} + +export const join = (patterns: readonly string[]) => { + return patterns.join('') +} + +export const optional = (pattern: string) => { + return `${group(pattern)}?` +} + +export const when = (condition: boolean, pattern: string, fallback = '') => { + if (condition) { + return pattern + } + + return fallback +} diff --git a/src/utils.ts b/src/utils.ts index b72d360..b74fbbe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,14 +1,40 @@ -import type { BooleanOrBuildTarget, ModuleName, ModuleNameWithoutNodePrefix } from './index' +import stdLibBrowser from 'node-stdlib-browser' +import { type Plugin } from 'vite' + +export type BuildTarget = 'build' | 'dev' +export type BooleanOrBuildTarget = boolean | BuildTarget +export type ModuleName = keyof typeof stdLibBrowser +export type ModuleNameWithoutNodePrefix = T extends `node:${infer P}` ? P : never +export type TransformHook = Extract export const compareModuleNames = (moduleA: ModuleName, moduleB: ModuleName) => { return withoutNodeProtocol(moduleA) === withoutNodeProtocol(moduleB) } -export const isEnabled = (value: BooleanOrBuildTarget, mode: 'build' | 'dev') => { +export const globalShimBanners = { + buffer: [ + `import __buffer_polyfill from 'vite-plugin-node-polyfills/shims/buffer'`, + `globalThis.Buffer = globalThis.Buffer || __buffer_polyfill`, + ], + global: [ + `import __global_polyfill from 'vite-plugin-node-polyfills/shims/global'`, + `globalThis.global = globalThis.global || __global_polyfill`, + ], + process: [ + `import __process_polyfill from 'vite-plugin-node-polyfills/shims/process'`, + `globalThis.process = globalThis.process || __process_polyfill`, + ], +} + +export const isEnabled = (value: BooleanOrBuildTarget, target: BuildTarget) => { if (!value) return false if (value === true) return true - return value === mode + return value === target +} + +export const isModuleName = (name: string): name is ModuleName => { + return name in stdLibBrowser } export const isNodeProtocolImport = (name: string) => {