|
| 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