Skip to content

Commit ea71048

Browse files
committed
Better handling imports of builtins
1 parent bf0dfee commit ea71048

File tree

2 files changed

+69
-60
lines changed

2 files changed

+69
-60
lines changed

src/index.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,7 @@ test('prefixedBuiltins === false', async t => {
6363
await plugin.buildStart(fakeInputOptions)
6464

6565
for (const source of [ 'node:path', 'path' ]) {
66-
t.deepEqual(
67-
await plugin.resolveId(source),
68-
{ id: source, external: true }
69-
)
66+
t.false(await plugin.resolveId(source))
7067
}
7168
})
7269

@@ -77,7 +74,7 @@ test('prefixedBuiltins === true (default)', async t => {
7774
for (const source of [ 'node:path', 'path' ]) {
7875
t.deepEqual(
7976
await plugin.resolveId(source),
80-
{ id: source, external: true }
77+
{ id: 'node:path', external: true }
8178
)
8279
}
8380
})

src/index.ts

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import path from 'node:path'
2-
import type { Plugin } from 'rollup'
1+
import path from 'path'
32
import { builtinModules } from 'module'
43
import { findPackagePaths, findDependencies } from './dependencies'
4+
import type { Plugin } from 'rollup'
55

66
export interface ExternalsOptions {
77
/** Mark node built-in modules like `path`, `fs`... as external. Defaults to `true`. */
88
builtins?: boolean
99
/** How to treat prefixed builtins. Defaults to `true` (prefixed are considered the same as unprefixed). */
10-
prefixedBuiltins?: boolean | 'strip' | 'add'
10+
builtinsPrefix?: 'add' | 'strip'
1111
/**
1212
* Path/to/your/package.json file (or array of paths).
1313
* Defaults to all package.json files found in parent directories recursively.
@@ -26,82 +26,92 @@ export interface ExternalsOptions {
2626
include?: string | RegExp | (string | RegExp)[]
2727
/** Exclude these deps from the list of externals, regardless of other settings. Defaults to `[]` */
2828
exclude?: string | RegExp | (string | RegExp)[]
29+
/**
30+
* @deprecated - Please use `builtinsPrefix`instead.
31+
*/
32+
prefixedBuiltins?: boolean | 'strip' | 'add'
2933
}
3034

31-
type IncludeExclude = keyof (ExternalsOptions['include'] | ExternalsOptions['exclude'])
32-
3335
/**
3436
* A Rollup plugin that automatically declares NodeJS built-in modules,
3537
* and optionally npm dependencies, as 'external'.
3638
*/
3739
function externals(options: ExternalsOptions = {}): Plugin {
3840

41+
// This will store all eventual warnings until we can display them.
42+
const warnings: string[] = []
43+
3944
// Consolidate options
4045
const config: Required<ExternalsOptions> = {
4146
builtins: true,
42-
prefixedBuiltins: 'strip',
47+
builtinsPrefix: 'add',
4348
packagePath: [],
4449
deps: true,
4550
devDeps: true,
4651
peerDeps: true,
4752
optDeps: true,
4853
include: [],
4954
exclude: [],
55+
56+
prefixedBuiltins: 'strip',
57+
5058
...options
5159
}
5260

53-
// This will store all eventual warnings until we can display them.
54-
const warnings: string[] = []
61+
if ('prefixedBuiltins' in options) {
62+
warnings.push("The 'prefixedBuiltins' option is now deprecated, " +
63+
"please use 'builtinsPrefix' instead to silent this warning.")
64+
}
65+
else if ('builtinsPrefix' in options) {
66+
config.prefixedBuiltins = options.builtinsPrefix
67+
}
5568

5669
// Map the include and exclude options to arrays of regexes.
57-
const [ include, exclude ] = [ 'include', 'exclude' ].map(option => new Array<string | RegExp>()
58-
.concat(config[option as IncludeExclude])
59-
.map((entry, index) => {
60-
if (entry instanceof RegExp)
61-
return entry
62-
63-
if (typeof entry === 'string')
64-
return new RegExp('^' + entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$')
65-
66-
if (entry) {
67-
warnings.push(`Ignoring wrong entry type #${index} in '${option}' option: '${entry}'`)
68-
}
69-
70-
return /(?=no)match/
71-
})
70+
const [ include, exclude ] = [ 'include', 'exclude' ].map(option =>
71+
([] as (string | RegExp)[])
72+
.concat(config[option as 'include' | 'exclude'])
73+
.reduce((result, entry, index) => {
74+
if (entry instanceof RegExp)
75+
result.push(entry)
76+
else if (typeof entry === 'string')
77+
result.push(new RegExp('^' + entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$'))
78+
else if (entry) {
79+
warnings.push(`Ignoring wrong entry type #${index} in '${option}' option: ${JSON.stringify(entry)}`)
80+
}
81+
return result
82+
}, [] as RegExp[])
7283
)
7384

7485
// A filter function to keep only non excluded dependencies.
7586
const isNotExcluded = (id: string) => !exclude.some(rx => rx.test(id))
7687

7788
// The array of the final regexes.
78-
const externals: RegExp[] = []
89+
let externals: RegExp[] = []
7990
const isExternal = (id: string) => externals.some(rx => rx.test(id))
8091

81-
// Support for nodejs: prefix and sub directory.
82-
const nodePrefixRx = /^(?:node(?:js)?:)?/
83-
84-
let builtins: Set<string>
92+
// Support for builtin modules.
93+
const builtins: Set<string> = new Set(),
94+
alwaysSchemed: Set<string> = new Set()
8595
if (config.builtins) {
86-
const filtered = builtinModules.filter(isNotExcluded)
87-
builtins = new Set([
88-
...filtered,
89-
...filtered.map(builtin => builtin.startsWith('node:') ? builtin : 'node:' + builtin)
90-
])
96+
const filtered = builtinModules.filter(b => isNotExcluded(b) && isNotExcluded('node:' + b))
97+
for (const builtin of filtered) {
98+
builtins.add(builtin)
99+
if (builtin.startsWith('node:'))
100+
alwaysSchemed.add(builtin)
101+
else
102+
builtins.add('node:' + builtin)
103+
}
91104
}
92-
else builtins = new Set()
93105

94106
return {
95107
name: 'node-externals',
96108

97109
async buildStart() {
98110

99-
// 1) Add the include option.
100-
if (include.length > 0) {
101-
externals.push(...include)
102-
}
111+
// Begin with the include option as it has precedence over the other options.
112+
externals = [ ...include ]
103113

104-
// 2) Find and filter dependencies, supporting potential import from a sub directory (e.g. 'lodash/map').
114+
// Find and filter dependencies, supporting potential import from a sub directory (e.g. 'lodash/map').
105115
const packagePaths: string[] = ([] as string[]).concat(config.packagePath)
106116
const dependencies = (await findDependencies({
107117
packagePaths: packagePaths.length > 0 ? packagePaths : findPackagePaths(),
@@ -118,31 +128,33 @@ function externals(options: ExternalsOptions = {}): Plugin {
118128
externals.push(new RegExp('^(?:' + dependencies.join('|') + ')(?:/.+)?$'))
119129
}
120130

121-
// All done. Issue the warnings we may have collected.
122-
while (warnings.length > 0) {
123-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
124-
this.warn(warnings.shift()!)
131+
// Issue the warnings we may have collected.
132+
let warning: string | undefined
133+
while ((warning = warnings.shift())) {
134+
this.warn(warning)
125135
}
126136
},
127137

128138
resolveId(importee) {
129139

130-
// Ignore already resolved ids and relative imports.
131-
if (path.isAbsolute(importee) || importee.startsWith('.') || importee.charCodeAt(0) === 0) {
140+
// Ignore already resolved ids, relative imports and virtual modules.
141+
if (path.isAbsolute(importee) || /^(?:\0|\.{1,2}[\\/])/.test(importee))
132142
return null
133-
}
134143

135-
// Handle builtins.
144+
// Handle builtins first.
145+
if (alwaysSchemed.has(importee))
146+
return false
147+
136148
if (builtins.has(importee)) {
137-
if (config.prefixedBuiltins) {
138-
let stripped = importee.replace(nodePrefixRx, '')
139-
if (config.prefixedBuiltins === 'strip')
140-
importee = stripped
141-
else if (config.prefixedBuiltins === 'add')
142-
importee = 'node:' + stripped
143-
}
149+
if (config.prefixedBuiltins === false)
150+
return false
151+
152+
const stripped = importee.replace(/^node:/, '')
153+
const prefixed = 'node:' + stripped
144154

145-
return { id: importee, external: true }
155+
return config.prefixedBuiltins === 'strip'
156+
? { id: stripped, external: true }
157+
: { id: prefixed, external: true }
146158
}
147159

148160
// Handle dependencies.

0 commit comments

Comments
 (0)