|
| 1 | +import assert from "node:assert"; |
1 | 2 | import { builtinModules } from "node:module"; |
2 | 3 | import nodePath from "node:path"; |
3 | 4 | import dedent from "ts-dedent"; |
@@ -169,71 +170,65 @@ function handleNodeJSGlobals( |
169 | 170 | build: PluginBuild, |
170 | 171 | inject: Record<string, string | string[]> |
171 | 172 | ) { |
172 | | - const UNENV_GLOBALS_RE = /_virtual_unenv_global_polyfill-([^.]+)\.js$/; |
| 173 | + const UNENV_GLOBALS_RE = /_virtual_unenv_global_polyfill-(.+)$/; |
173 | 174 | const prefix = nodePath.resolve( |
174 | 175 | getBasePath(), |
175 | 176 | "_virtual_unenv_global_polyfill-" |
176 | 177 | ); |
177 | 178 |
|
| 179 | + /** |
| 180 | + * Map of module identifiers to |
| 181 | + * - `injectedName`: the name injected on `globalThis` |
| 182 | + * - `exportName`: the export name from the module |
| 183 | + * - `importName`: the imported name |
| 184 | + */ |
| 185 | + const injectsByModule = new Map< |
| 186 | + string, |
| 187 | + { injectedName: string; exportName: string; importName: string }[] |
| 188 | + >(); |
| 189 | + |
| 190 | + // Module specifier (i.e. `/unenv/runtime/node/...`) keyed by path (i.e. `/prefix/_virtual_unenv_global_polyfill-...`) |
| 191 | + const virtualModulePathToSpecifier = new Map<string, string>(); |
| 192 | + |
| 193 | + for (const [injectedName, moduleSpecifier] of Object.entries(inject)) { |
| 194 | + const [module, exportName, importName] = Array.isArray(moduleSpecifier) |
| 195 | + ? [moduleSpecifier[0], moduleSpecifier[1], moduleSpecifier[1]] |
| 196 | + : [moduleSpecifier, "default", "defaultExport"]; |
| 197 | + |
| 198 | + if (!injectsByModule.has(module)) { |
| 199 | + injectsByModule.set(module, []); |
| 200 | + virtualModulePathToSpecifier.set( |
| 201 | + prefix + module.replaceAll("/", "-"), |
| 202 | + module |
| 203 | + ); |
| 204 | + } |
| 205 | + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
| 206 | + injectsByModule.get(module)!.push({ injectedName, exportName, importName }); |
| 207 | + } |
| 208 | + |
| 209 | + // Inject the virtual modules |
178 | 210 | build.initialOptions.inject = [ |
179 | 211 | ...(build.initialOptions.inject ?? []), |
180 | | - //convert unenv's inject keys to absolute specifiers of custom virtual modules that will be provided via a custom onLoad |
181 | | - ...Object.keys(inject).map( |
182 | | - (globalName) => `${prefix}${encodeToLowerCase(globalName)}.js` |
183 | | - ), |
| 212 | + ...virtualModulePathToSpecifier.keys(), |
184 | 213 | ]; |
185 | 214 |
|
186 | 215 | build.onResolve({ filter: UNENV_GLOBALS_RE }, ({ path }) => ({ path })); |
187 | 216 |
|
188 | 217 | build.onLoad({ filter: UNENV_GLOBALS_RE }, ({ path }) => { |
189 | | - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
190 | | - const globalName = decodeFromLowerCase(path.match(UNENV_GLOBALS_RE)![1]); |
191 | | - const { importStatement, exportName } = getGlobalInject(inject[globalName]); |
| 218 | + const module = virtualModulePathToSpecifier.get(path); |
| 219 | + assert(module, `Expected ${path} to be mapped to a module specifier`); |
| 220 | + const injects = injectsByModule.get(module); |
| 221 | + assert(injects, `Expected ${module} to inject values`); |
| 222 | + |
| 223 | + const imports = injects.map(({ exportName, importName }) => |
| 224 | + importName === exportName ? exportName : `${exportName} as ${importName}` |
| 225 | + ); |
192 | 226 |
|
193 | 227 | return { |
194 | 228 | contents: dedent` |
195 | | - ${importStatement} |
196 | | - globalThis.${globalName} = ${exportName}; |
| 229 | + import { ${imports.join(", ")} } from "${module}"; |
| 230 | + ${injects.map(({ injectedName, importName }) => `globalThis.${injectedName} = ${importName};`).join("\n")} |
197 | 231 | `, |
198 | 232 | }; |
199 | 233 | }); |
200 | 234 | } |
201 | | - |
202 | | -/** |
203 | | - * Get the import statement and export name to be used for the given global inject setting. |
204 | | - */ |
205 | | -function getGlobalInject(globalInject: string | string[]) { |
206 | | - if (typeof globalInject === "string") { |
207 | | - // the mapping is a simple string, indicating a default export, so the string is just the module specifier. |
208 | | - return { |
209 | | - importStatement: `import globalVar from "${globalInject}";`, |
210 | | - exportName: "globalVar", |
211 | | - }; |
212 | | - } |
213 | | - // the mapping is a 2 item tuple, indicating a named export, made up of a module specifier and an export name. |
214 | | - const [moduleSpecifier, exportName] = globalInject; |
215 | | - return { |
216 | | - importStatement: `import { ${exportName} } from "${moduleSpecifier}";`, |
217 | | - exportName, |
218 | | - }; |
219 | | -} |
220 | | - |
221 | | -/** |
222 | | - * Encodes a case sensitive string to lowercase string. |
223 | | - * |
224 | | - * - Escape $ with another $ ("$" -> "$$") |
225 | | - * - Escape uppercase letters with $ and turn them into lowercase letters ("L" -> "$L") |
226 | | - * |
227 | | - * This function exists because ESBuild requires that all resolved paths are case insensitive. |
228 | | - * Without this transformation, ESBuild will clobber /foo/bar.js with /foo/Bar.js |
229 | | - */ |
230 | | -export function encodeToLowerCase(str: string): string { |
231 | | - return str.replace(/[A-Z$]/g, (escape) => `$${escape.toLowerCase()}`); |
232 | | -} |
233 | | - |
234 | | -/** |
235 | | - * Decodes a string lowercased using `encodeToLowerCase` to the original strings |
236 | | - */ |
237 | | -export function decodeFromLowerCase(str: string): string { |
238 | | - return str.replace(/\$[a-z$]/g, (escaped) => escaped[1].toUpperCase()); |
239 | | -} |
0 commit comments