@staticbolt/core 1.0.0-beta.32 → 1.0.0-beta.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,11 @@
1
1
  import { A as isTextAssetMetadata, B as checkJsonObject, C as isBinaryAssetMetadata, D as isScriptMetadata, E as isPackageMetadata, G as isTypeScriptScript, H as isEmptyElement, I as safeReadFileSync, K as TYPESCRIPT_TYPE, M as METADATA_TYPES, N as Resolver, O as isStyleMetadata, P as readJsonFile, R as valueOrError, S as filterStyleMetadata, T as isMarkdownMetadata, U as isJavaScript, V as isDynamic, X as splitHtmlLink, Y as isValidRelativePath, Z as DependencyTracker, _ as mergeMaps, b as printFmtError, c as downloadContent, d as hashContent, f as humanReadableBytes, h as isURL, j as isWebManifestMetadata, k as isSvgMetadata, l as escapeHtml, n as bytesToKB, q as isScriptType, s as cloneObject, w as isHtmlMetadata, x as filterScriptMetadata, y as PrintFormattedError, z as checkFileExists } from "../utilities-jK4uUZBV.mjs";
2
- import { _ as replaceExtension, a as basename, c as isAbsolute, d as join, f as normalize, g as relative, i as createLog, l as isPathMatch, m as parsePatterns, n as CUSTOM_ATTRIBUTES, o as dirname, p as parse$2, r as Log, s as extname, u as isSubpath, v as resolve } from "../common-D1QTZ8ra.mjs";
2
+ import { _ as replaceExtension, a as basename, c as isAbsolute, d as join, f as normalize, g as relative, i as createLog, l as isPathMatch, m as parsePatterns, n as CUSTOM_ATTRIBUTES, o as dirname, p as parse$5, r as Log, s as extname, u as isSubpath, v as resolve } from "../common-D1QTZ8ra.mjs";
3
3
  import { _ as minifyHtml, a as formatStyle, c as minifyStylePass, d as minifyScript, f as minifyScriptPass, g as formatHtmlPass, h as formatHtml, i as minifySvgPass, l as formatScript, m as formatMarkdownPass, n as formatSvgPass, o as formatStylePass, p as formatMarkdown, r as minifySvg, s as minifyStyle, t as formatSvg, u as formatScriptPass, v as minifyHtmlPass, y as formatCode } from "../deferred-DTj91vEg.mjs";
4
4
  import { t as loadConfigFile } from "../load-config-CsbiJ01A.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import nodePath from "node:path";
7
7
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
+ import { ResolverFactory } from "oxc-resolver";
8
9
  import chalk from "chalk";
9
10
  import json5 from "json5";
10
11
  import _generator from "@babel/generator";
@@ -15,6 +16,10 @@ import { globSync } from "glob";
15
16
  import * as esbuild from "esbuild";
16
17
  import _traverse from "@babel/traverse";
17
18
  import * as t from "@babel/types";
19
+ import { init, parse as parse$1 } from "cjs-module-lexer";
20
+ import { init as init$1, parse as parse$2 } from "es-module-lexer";
21
+ import * as babelParser from "@babel/parser";
22
+ import { parse as parse$3 } from "@babel/parser";
18
23
  import postcssrc from "postcss-load-config";
19
24
  import * as babel from "@babel/core";
20
25
  import babelPresetEnv from "@babel/preset-env";
@@ -35,8 +40,7 @@ import remarkRehype from "remark-rehype";
35
40
  import remarkSmartypants from "remark-smartypants";
36
41
  import { unified } from "unified";
37
42
  import { visit } from "unist-util-visit";
38
- import { parse as parse$1 } from "yaml";
39
- import * as babelParser from "@babel/parser";
43
+ import { parse as parse$4 } from "yaml";
40
44
  import fastifyStatic from "@fastify/static";
41
45
  import fastifyUrlData from "@fastify/url-data";
42
46
  import Fastify from "fastify";
@@ -137,7 +141,7 @@ function analyzeOutputPlugin(options = {}) {
137
141
  "workbox-*.js"
138
142
  ];
139
143
  const maxFileSize = options.maxFileSize ?? { ".js": 150 };
140
- const isLogUnusedFiles = options.logUnusedFiles ?? true;
144
+ const isLogUnusedFiles = options.logUnusedFiles ?? false;
141
145
  return {
142
146
  name: "analyze-output",
143
147
  async postBuild() {
@@ -158,21 +162,21 @@ function analyzeOutputPlugin(options = {}) {
158
162
  root: this.outdir
159
163
  });
160
164
  const missingFiles = [...usedFiles.difference(availableFiles)].filter((file) => !isExcluded(file));
161
- let missingFilesLog = chalk.bold("The following files are missing:");
162
- for (const missing of missingFiles) missingFilesLog += "\n" + chalk.red("File not found: ") + chalk.yellow(relative(this.root, missing));
163
- if (missingFiles.length > 0) this.log.error(missingFilesLog);
165
+ if (missingFiles.length > 0) {
166
+ const missingFilesLog = missingFiles.map((missing) => "\n" + chalk.red("File not found: ") + chalk.yellow(relative(this.root, missing)));
167
+ this.log.error(chalk.bold("The following files are missing:") + missingFilesLog.join(""));
168
+ }
164
169
  const unusedFiles = availableFiles.difference(usedFiles);
165
170
  const unusedFilesLog = [];
166
171
  for (const unused of unusedFiles) {
167
- if (extname(unused) === ".html") continue;
168
172
  if (isExcluded(unused)) continue;
169
173
  if (isRemoveUnused) unlinkSync(unused);
170
174
  unusedFilesLog.push(chalk.yellow(join(basename(this.outdir), relative(this.outdir, unused))));
171
175
  }
172
176
  const logTitle = isRemoveUnused ? chalk.bold(chalk.red("Removed"), "(" + chalk.yellow(unusedFilesLog.length) + ")", "unused files from the output directory") : chalk.bold("Found", "(" + chalk.yellow(unusedFilesLog.length) + ")", "unused files");
173
177
  if (unusedFilesLog.length > 0) this.log.warn(logTitle + (isLogUnusedFiles ? "\n" + unusedFilesLog.join("\n") : ""));
174
- const sortedDirectories = globSync(this.outdir + "/**/*/").toSorted((a, b) => a.localeCompare(b));
175
- for (const directory of sortedDirectories) {
178
+ const directories = globSync(this.outdir + "/**/*/");
179
+ for (const directory of directories) {
176
180
  let currentPath = directory;
177
181
  while (currentPath !== this.outdir) {
178
182
  if (!(readdirSync(currentPath).length === 0)) break;
@@ -181,9 +185,8 @@ function analyzeOutputPlugin(options = {}) {
181
185
  }
182
186
  }
183
187
  if (maxFileSize) {
184
- const extensions = Object.keys(maxFileSize).map((extension) => extension.slice(1));
185
- const patternExtensionPart = extensions.length > 1 ? `{${extensions.join(",")}}` : extensions[0];
186
- const filesStat = globSync(`**/*.${patternExtensionPart}`, {
188
+ const patterns = Object.keys(maxFileSize).map((extension) => `**/*${extension}`);
189
+ const filesStat = globSync(patterns, {
187
190
  ignore: exclude,
188
191
  nodir: true,
189
192
  cwd: this.outdir,
@@ -195,9 +198,9 @@ function analyzeOutputPlugin(options = {}) {
195
198
  for (const file of sortedFiles) {
196
199
  const maxSize = maxFileSize[extname(file.name)];
197
200
  if (typeof maxSize !== "number") continue;
198
- if (file.size === 0) continue;
199
- if (bytesToKB(file.size ?? 0) <= maxSize) continue;
200
- largeFilesLogs.push(chalk.yellow(file.relative()) + ` (${chalk.red.bold(humanReadableBytes(file.size ?? 0))} ${chalk.blue(">")} ${chalk.green(humanReadableBytes(maxSize * 1024))})`);
201
+ const size = file.size ?? 0;
202
+ if (bytesToKB(size) <= maxSize) continue;
203
+ largeFilesLogs.push(chalk.yellow(file.relative()) + ` (${chalk.red.bold(humanReadableBytes(size))} ${chalk.blue(">")} ${chalk.green(humanReadableBytes(maxSize * 1024))})`);
201
204
  }
202
205
  if (largeFilesLogs.length > 0) this.log.warn(chalk.bold("The following files may be large:\n") + largeFilesLogs.join("\n"));
203
206
  }
@@ -206,38 +209,135 @@ function analyzeOutputPlugin(options = {}) {
206
209
  }
207
210
 
208
211
  //#endregion
209
- //#region src/plugins/bundle-packages/bundle-esbuild.ts
212
+ //#region src/plugins/bundle-packages/shared/helpers.ts
213
+ /** Appends to the array at `key`, creating it when absent. */
214
+ function pushInto(map, key, value) {
215
+ const existing = map.get(key);
216
+ if (!existing) {
217
+ map.set(key, [value]);
218
+ return;
219
+ }
220
+ existing.push(value);
221
+ }
222
+ /**
223
+ * Replaces `.js` with `_js` in the directory parts of a package path, so a package like `highlight.js` does not need a folder
224
+ * with the same name as its own bundle.
225
+ *
226
+ * Example: `highlight.js/languages/javascript.js` -> `highlight_js/languages/javascript.js`
227
+ */
228
+ function normalizePackageName(originalSource) {
229
+ const parts = originalSource.split("/");
230
+ if (parts.length <= 1) return originalSource;
231
+ const fileName = parts.pop();
232
+ return [...parts.map((part) => part.endsWith(".js") ? part.slice(0, -3) + "_js" : part), fileName].join("/");
233
+ }
234
+ /** Extensions a package name can already end in, which are replaced rather than appended to. */
235
+ const JS_EXTENSIONS$1 = /* @__PURE__ */ new Set([
236
+ ".js",
237
+ ".mjs",
238
+ ".cjs"
239
+ ]);
240
+ /**
241
+ * Gives a package name the `.js` its bundle is written under.
242
+ *
243
+ * Only a real JavaScript extension is replaced. Treating the last dot-segment of any name as one puts `lodash.debounce` and
244
+ * `lodash.merge` in the same file, where the second overwrites the first and every import of the loser breaks.
245
+ */
246
+ function withJsExtension(name) {
247
+ const lastSegment = name.slice(name.lastIndexOf("/") + 1);
248
+ const dot = lastSegment.lastIndexOf(".");
249
+ if (dot === -1 || !JS_EXTENSIONS$1.has(lastSegment.slice(dot))) return name + ".js";
250
+ return name.slice(0, name.length - (lastSegment.length - dot)) + ".js";
251
+ }
252
+ /** Where a package's bundle is written. Example: `react-dom/client` -> `packages/react-dom/client.js` */
253
+ function calcPackageOutPath(packageName, packagesDirectory) {
254
+ return join(packagesDirectory, withJsExtension(normalizePackageName(packageName)));
255
+ }
256
+ /**
257
+ * Output path for an asset shipped by a package, keeping its own extension.
258
+ *
259
+ * Example: `node_modules/highlight.js/styles/default.css` -> `packages/highlight_js/styles/default.css`
260
+ */
261
+ function calcPackageAssetOutPath(nodeModulesPath, packagesDirectory) {
262
+ const markerIndex = nodeModulesPath.lastIndexOf("node_modules/");
263
+ const packagePath = markerIndex === -1 ? nodeModulesPath : nodeModulesPath.slice(markerIndex + 13);
264
+ return join(packagesDirectory, normalizePackageName(packagePath));
265
+ }
266
+ /** The part of a pattern before its wildcard, or nothing when it has none. */
267
+ function wildcardBase(pattern) {
268
+ if (pattern.endsWith("/**")) return pattern.slice(0, -3);
269
+ if (pattern.endsWith("/*")) return pattern.slice(0, -2);
270
+ }
271
+ /** Whether a package answers to any of the patterns written on a chunk. */
272
+ function matchesPackage(packageName, packages) {
273
+ for (const pattern of packages) {
274
+ if (pattern === packageName) return true;
275
+ const base = wildcardBase(pattern);
276
+ if (base === void 0) continue;
277
+ if (packageName === base || packageName.startsWith(base + "/")) return true;
278
+ }
279
+ return false;
280
+ }
281
+ /** Joins a package name with one of its entry points. Example: `packageJoin("react-dom", "./client") // => "react-dom/client"` */
282
+ function packageJoin(name, subPackage) {
283
+ return nodePath.join(name, subPackage).replace(/\\/g, "/").replace(/\/$/, "");
284
+ }
285
+ /** The `node_modules` directory a package was installed into, or nothing with the reason it could not be found. */
286
+ function getNodeModuleDirectory(packageName, root) {
287
+ try {
288
+ const entryPoint = createRequire(root.endsWith(nodePath.sep) ? root : root + nodePath.sep).resolve(packageName);
289
+ const parts = entryPoint.split(nodePath.sep);
290
+ const index = parts.lastIndexOf("node_modules");
291
+ if (index === -1) return { problem: `"${packageName}" resolves to "${entryPoint}", which is not inside a node_modules directory.` };
292
+ return { path: parts.slice(0, index + 1).join(nodePath.sep) };
293
+ } catch {
294
+ return { problem: `"${packageName}" could not be resolved from the project root.` };
295
+ }
296
+ }
297
+ /**
298
+ * The path a `resolvePackage` entry points a package export at, if one covers it.
299
+ *
300
+ * An entry that cannot be honoured goes to `onProblem` rather than being printed, so it reaches the build's own reporting.
301
+ */
302
+ function getCustomPackagePath(root, importPath, isProduction, resolvePackage, onProblem) {
303
+ const environment = isProduction ? "production" : "development";
304
+ for (const [packageName, exports] of Object.entries(resolvePackage)) for (const [entry, stringOrObject] of Object.entries(exports)) {
305
+ if (packageJoin(packageName, entry) !== importPath) continue;
306
+ const relativePath = typeof stringOrObject === "string" ? stringOrObject : stringOrObject[environment];
307
+ if (!relativePath) continue;
308
+ const directory = getNodeModuleDirectory(packageName, root);
309
+ if ("problem" in directory) {
310
+ onProblem(`The "resolvePackage" entry for "${importPath}" was skipped: ${directory.problem}`);
311
+ continue;
312
+ }
313
+ return join(directory.path, packageName, relativePath);
314
+ }
315
+ }
316
+ /** Whether a path reaches into `node_modules`. */
317
+ function isNodeModulesPath(path) {
318
+ return /(^|\/)node_modules\//.test(path);
319
+ }
320
+ /** Directory of the package a `node_modules` path belongs to, `undefined` for paths outside `node_modules`. */
321
+ function packageDirectory(path) {
322
+ return /^(.*?node_modules\/(?:@[^/]+\/)?[^/]+)(?:\/|$)/.exec(path)?.[1];
323
+ }
324
+
325
+ //#endregion
326
+ //#region src/plugins/bundle-packages/build/bundle-esbuild.ts
210
327
  const bundleFunction$1 = valueOrError(esbuild.build);
328
+ /** Bundles one generated entry into a single ES module, keeping the result in memory rather than writing it. */
211
329
  async function bundlePackage(options) {
212
- const { resolveDir, packageName, outfile, onPackageResolve, production, minify, treeShaking } = options;
213
- const plugins = [{
214
- name: "esbuild-plugin-packages-resolver",
215
- setup(build) {
216
- build.onResolve({ filter: /* @__PURE__ */ new RegExp("^[^./].*") }, async (arguments_) => {
217
- if (arguments_.path === packageName || arguments_.importer === "<stdin>") return;
218
- return await onPackageResolve(arguments_.path, arguments_.importer);
219
- });
220
- }
221
- }];
222
- if (options.onFileResolve) plugins.push({
223
- name: "esbuild-plugin-relative-imports-resolver",
224
- setup(build) {
225
- build.onResolve({ filter: /^\./ }, (arguments_) => {
226
- return options.onFileResolve?.(arguments_.path, arguments_.importer);
227
- });
228
- }
229
- });
330
+ const { contents, resolveDir, outfile, onPackageResolve, production } = options;
230
331
  const [bundleResult, bundleError] = await bundleFunction$1({
231
- stdin: options.contents ? {
232
- contents: options.contents,
332
+ stdin: {
333
+ contents,
233
334
  resolveDir
234
- } : void 0,
235
- entryPoints: options.entryPoint ? [options.entryPoint] : void 0,
335
+ },
236
336
  outfile,
237
337
  absWorkingDir: resolveDir,
238
338
  bundle: true,
239
- minify,
240
- treeShaking,
339
+ minify: production,
340
+ treeShaking: production,
241
341
  write: false,
242
342
  legalComments: "none",
243
343
  packages: "bundle",
@@ -246,106 +346,879 @@ async function bundlePackage(options) {
246
346
  target: "es2022",
247
347
  define: { "process.env.NODE_ENV": production ? "\"production\"" : "\"development\"" },
248
348
  logLevel: "silent",
249
- plugins
349
+ plugins: [{
350
+ name: "esbuild-plugin-packages-resolver",
351
+ setup(build) {
352
+ build.onResolve({ filter: /^[^./]/ }, (arguments_) => onPackageResolve(arguments_.path));
353
+ }
354
+ }]
250
355
  });
251
356
  if (bundleError) return [null, bundleError];
252
357
  const outCode = bundleResult?.outputFiles?.[0]?.text;
253
- if (typeof outCode !== "string") return [null, /* @__PURE__ */ new Error(`Failed to bundle the package "${packageName}"`)];
358
+ if (typeof outCode !== "string") return [null, /* @__PURE__ */ new Error(`esbuild produced no output for "${outfile}"`)];
254
359
  return [outCode, null];
255
360
  }
256
361
 
257
362
  //#endregion
258
- //#region src/plugins/bundle-packages/collect-specifiers.ts
363
+ //#region src/plugins/bundle-packages/shared/name-set.ts
364
+ /** A set of export names, or all of them. Grows only. */
365
+ var NameSet = class NameSet {
366
+ /** `undefined` means all names. */
367
+ #names;
368
+ constructor(names) {
369
+ this.#names = names;
370
+ }
371
+ static all() {
372
+ return new NameSet(void 0);
373
+ }
374
+ static of(names = []) {
375
+ return new NameSet(new Set(names));
376
+ }
377
+ get isAll() {
378
+ return this.#names === void 0;
379
+ }
380
+ has(name) {
381
+ return this.#names ? this.#names.has(name) : true;
382
+ }
383
+ list() {
384
+ return this.#names ? [...this.#names] : [];
385
+ }
386
+ clone() {
387
+ return this.#names ? NameSet.of(this.#names) : NameSet.all();
388
+ }
389
+ /**
390
+ * Adds another set's names.
391
+ *
392
+ * @returns Whether anything changed
393
+ */
394
+ widen(other) {
395
+ if (this.isAll) return false;
396
+ if (other.isAll) {
397
+ this.#names = void 0;
398
+ return true;
399
+ }
400
+ const names = this.#names;
401
+ let isWidened = false;
402
+ for (const name of other.list()) {
403
+ if (names.has(name)) continue;
404
+ names.add(name);
405
+ isWidened = true;
406
+ }
407
+ return isWidened;
408
+ }
409
+ /** @returns A stable string, for cache keys */
410
+ key() {
411
+ if (!this.#names) return "*";
412
+ return [...this.#names].toSorted((a, b) => a.localeCompare(b)).join(",");
413
+ }
414
+ };
415
+ /**
416
+ * Widens a map entry, inserting when absent.
417
+ *
418
+ * @returns Whether anything changed
419
+ */
420
+ function didWidenEntry(map, key, names) {
421
+ const existing = map.get(key);
422
+ if (!existing) {
423
+ map.set(key, names.clone());
424
+ return true;
425
+ }
426
+ return existing.widen(names);
427
+ }
428
+
429
+ //#endregion
430
+ //#region src/plugins/bundle-packages/plan/resolve-chunks.ts
431
+ /** The packages named directly by a chunk, whichever form it takes. */
432
+ function namedPackages(option) {
433
+ return Array.isArray(option) ? option : option.include;
434
+ }
435
+ /**
436
+ * Works out which packages each chunk holds.
437
+ *
438
+ * For the object form the walk starts at `include` and follows what those packages need, leaving out three kinds of package:
439
+ *
440
+ * - Ones the project imports itself, which keep their own file so a page needing only them does not pull the chunk.
441
+ * - Ones named in `exclude`.
442
+ * - Ones only ever reached through `import()`, since folding those into a chunk ends their laziness.
443
+ *
444
+ * A `require` overrules all three, since the two packages cannot be in separate files at all. An `exclude` naming such a package
445
+ * is refused rather than quietly producing a bundle that throws.
446
+ */
447
+ function resolveChunks(chunks, discovered, seeds, index, groups) {
448
+ const chunkOf = /* @__PURE__ */ new Map();
449
+ const refused = [];
450
+ const unmatched = [];
451
+ const everyPackage = discovered.keys().toArray();
452
+ const assign = (specifier, chunkName) => {
453
+ if (!chunkOf.get(specifier)?.includes(chunkName)) pushInto(chunkOf, specifier, chunkName);
454
+ };
455
+ for (const [chunkName, option] of Object.entries(chunks)) {
456
+ const named = namedPackages(option);
457
+ const excluded = Array.isArray(option) ? [] : option.exclude ?? [];
458
+ const roots = everyPackage.filter((specifier) => matchesPackage(specifier, named));
459
+ for (const root of roots) assign(root, chunkName);
460
+ unmatched.push(...findUnmatched(chunkName, named, excluded, everyPackage));
461
+ if (Array.isArray(option)) continue;
462
+ const queue = [...roots];
463
+ const seen = new Set(roots);
464
+ while (queue.length > 0) {
465
+ const specifier = queue.shift();
466
+ const closure = index.closureOf(specifier, discovered.get(specifier) ?? NameSet.of());
467
+ if (!closure) continue;
468
+ for (const edge of closure.crossPackage) {
469
+ if (seen.has(edge.specifier)) continue;
470
+ if (!discovered.has(edge.specifier)) continue;
471
+ const isRequired = edge.kind === "require" || edge.isStar;
472
+ if (!isRequired && isLeftOut(edge.specifier, edge.isLazyOnly, named, excluded, seeds)) continue;
473
+ if (isRequired && matchesPackage(edge.specifier, excluded)) refused.push({
474
+ chunk: chunkName,
475
+ specifier: edge.specifier,
476
+ requiredBy: specifier
477
+ });
478
+ seen.add(edge.specifier);
479
+ assign(edge.specifier, chunkName);
480
+ queue.push(edge.specifier);
481
+ }
482
+ }
483
+ }
484
+ pullInWholeGroups(chunkOf, groups, assign);
485
+ const duplicated = [];
486
+ for (const [specifier, names] of chunkOf) {
487
+ if (names.length < 2) continue;
488
+ duplicated.push({
489
+ specifier,
490
+ chunks: [...names]
491
+ });
492
+ }
493
+ const clashes = [];
494
+ for (const name of Object.keys(chunks)) {
495
+ if (!discovered.has(name)) continue;
496
+ const members = chunkOf.entries().filter(([, chunkNames]) => chunkNames.includes(name)).toArray();
497
+ const hasItself = members.some(([specifier]) => specifier === name);
498
+ clashes.push({
499
+ name,
500
+ otherMembers: hasItself ? members.length - 1 : members.length
501
+ });
502
+ }
503
+ return {
504
+ chunkOf,
505
+ duplicated,
506
+ refused,
507
+ clashes,
508
+ unmatched,
509
+ pulled: findPulledIn(chunks, chunkOf, seeds)
510
+ };
511
+ }
512
+ /**
513
+ * Finds packages the project imports that ended up in a chunk without being named by it.
514
+ *
515
+ * The walk skips those so a page wanting only that package does not download the whole chunk, but a `require` or an `export *`
516
+ * overrules it and the package loses its own file — a page importing `shiki` then gets everything `expressive-code` brought.
517
+ */
518
+ function findPulledIn(chunks, chunkOf, seeds) {
519
+ const byChunk = /* @__PURE__ */ new Map();
520
+ for (const [specifier, chunkNames] of chunkOf) {
521
+ if (!seeds.has(specifier)) continue;
522
+ for (const chunk of chunkNames) {
523
+ const option = chunks[chunk];
524
+ if (!option || Array.isArray(option)) continue;
525
+ if (matchesPackage(specifier, option.include)) continue;
526
+ pushInto(byChunk, chunk, specifier);
527
+ }
528
+ }
529
+ return byChunk.entries().map(([chunk, specifiers]) => ({
530
+ chunk,
531
+ specifiers: specifiers.toSorted((a, b) => a.localeCompare(b))
532
+ })).toArray();
533
+ }
534
+ /**
535
+ * Finds patterns on a chunk that no package answers to.
536
+ *
537
+ * A misspelled name, an uninstalled package and an unused subpath all end in a silently empty chunk, so the pattern is reported
538
+ * rather than the chunk.
539
+ */
540
+ function findUnmatched(chunk, named, excluded, everyPackage) {
541
+ const found = [];
542
+ const hasNoMatch = (pattern) => everyPackage.every((specifier) => !matchesPackage(specifier, [pattern]));
543
+ for (const pattern of named) {
544
+ if (!hasNoMatch(pattern)) continue;
545
+ found.push({
546
+ chunk,
547
+ pattern,
548
+ isExclusion: false
549
+ });
550
+ }
551
+ for (const pattern of excluded) {
552
+ if (!hasNoMatch(pattern)) continue;
553
+ found.push({
554
+ chunk,
555
+ pattern,
556
+ isExclusion: true
557
+ });
558
+ }
559
+ return found;
560
+ }
561
+ /**
562
+ * Adds the rest of a group to whichever chunk took one of its members.
563
+ *
564
+ * A group cannot be split across files at all, so taking one member into a chunk brings the rest with it.
565
+ */
566
+ function pullInWholeGroups(chunkOf, groups, assign) {
567
+ for (const [specifier, chunkNames] of chunkOf) {
568
+ const group = groups.groupOf.get(specifier);
569
+ if (!group) continue;
570
+ const groupMembers = groups.membersOf.get(group) ?? [];
571
+ for (const member of groupMembers) for (const chunkName of chunkNames) assign(member, chunkName);
572
+ }
573
+ }
574
+ /** Whether a package the walk reached should be left out of the chunk. */
575
+ function isLeftOut(specifier, isLazyOnly, named, excluded, seeds) {
576
+ if (matchesPackage(specifier, named)) return false;
577
+ if (matchesPackage(specifier, excluded)) return true;
578
+ if (seeds.has(specifier)) return true;
579
+ return isLazyOnly;
580
+ }
581
+
582
+ //#endregion
583
+ //#region src/plugins/bundle-packages/plan/plan-units.ts
584
+ /** Whether a file holds more than one package, in which case their default exports get renamed. */
585
+ function isSharedUnit(unit) {
586
+ return unit.exports.size > 1;
587
+ }
588
+ /** Follows the imports of the project's packages to find every other package they reach, and what each must export. */
589
+ function discoverPackages(seeds, index) {
590
+ const wanted = /* @__PURE__ */ new Map();
591
+ for (const [specifier, names] of seeds) didWidenEntry(wanted, specifier, names);
592
+ let isSettled = false;
593
+ while (!isSettled) {
594
+ isSettled = true;
595
+ for (const [specifier, names] of wanted) {
596
+ const closure = index.closureOf(specifier, names);
597
+ if (!closure) continue;
598
+ for (const edge of closure.crossPackage) if (didWidenEntry(wanted, edge.specifier, edge.names)) isSettled = false;
599
+ }
600
+ }
601
+ return wanted;
602
+ }
603
+ /**
604
+ * Maps each package to the group it must be bundled with.
605
+ *
606
+ * Two edges cannot cross a file:
607
+ *
608
+ * - `require("x")`, which esbuild replaces with a stub that throws `Dynamic require of "x" is not supported`.
609
+ * - `export * from "x"`, which cannot name what it forwards, so left external the forwarded names disappear.
610
+ *
611
+ * The group takes the name of its lowest member, so the output file name does not change with discovery order.
612
+ */
613
+ function mustShareGroups(wanted, index) {
614
+ const parent = /* @__PURE__ */ new Map();
615
+ const find = (name) => {
616
+ let current = name;
617
+ while (parent.get(current) !== current) current = parent.get(current) ?? current;
618
+ return current;
619
+ };
620
+ const union = (a, b) => {
621
+ const rootA = find(a);
622
+ const rootB = find(b);
623
+ if (rootA === rootB) return;
624
+ if (rootA < rootB) {
625
+ parent.set(rootB, rootA);
626
+ return;
627
+ }
628
+ parent.set(rootA, rootB);
629
+ };
630
+ for (const name of wanted.keys()) parent.set(name, name);
631
+ for (const [name, names] of wanted) {
632
+ const closure = index.closureOf(name, names);
633
+ if (!closure) continue;
634
+ for (const edge of closure.crossPackage) {
635
+ if (!(edge.kind === "require" || edge.isStar) || !parent.has(edge.specifier)) continue;
636
+ union(name, edge.specifier);
637
+ }
638
+ }
639
+ const groupOf = /* @__PURE__ */ new Map();
640
+ const membersOf = /* @__PURE__ */ new Map();
641
+ for (const name of parent.keys()) {
642
+ const group = find(name);
643
+ groupOf.set(name, group);
644
+ pushInto(membersOf, group, name);
645
+ }
646
+ return {
647
+ groupOf,
648
+ membersOf
649
+ };
650
+ }
651
+ /**
652
+ * Decides which output file each package goes in.
653
+ *
654
+ * A `chunks` entry wins. Otherwise packages that require each other share one file, and every other package gets its own.
655
+ *
656
+ * Only what the project itself imports is exported. A package pulled in by a file-mate needs no exports: esbuild bundles it
657
+ * either way, and re-exporting it would collide with its file-mates: `version` from both `react` and `react-dom`.
658
+ */
659
+ function planUnits(discovered, seeds, chunkOf, groups) {
660
+ const units = /* @__PURE__ */ new Map();
661
+ const place = (specifier, unitName) => {
662
+ let unit = units.get(unitName);
663
+ if (!unit) {
664
+ unit = {
665
+ name: unitName,
666
+ exports: /* @__PURE__ */ new Map()
667
+ };
668
+ units.set(unitName, unit);
669
+ }
670
+ const imported = seeds.get(specifier) ?? NameSet.of();
671
+ unit.exports.set(specifier, imported.clone());
672
+ };
673
+ for (const specifier of discovered.keys()) {
674
+ const chunkNames = chunkOf.get(specifier);
675
+ if (!chunkNames || chunkNames.length === 0) {
676
+ place(specifier, groups.groupOf.get(specifier) ?? specifier);
677
+ continue;
678
+ }
679
+ for (const chunkName of chunkNames) place(specifier, chunkName);
680
+ }
681
+ return units;
682
+ }
683
+ /**
684
+ * Finds sets of packages that require each other but were spread over several files.
685
+ *
686
+ * Grouping is automatic, so only a `chunks` entry taking part of a group can cause this. Reported per group, not per edge, since
687
+ * the whole group has to move together anyway.
688
+ */
689
+ function validateUnits(units, groups) {
690
+ const unitOf = unitsBySpecifier(units);
691
+ const problems = [];
692
+ for (const [group, packages] of groups.membersOf) {
693
+ if (packages.length < 2) continue;
694
+ const spread = new Set(packages.flatMap((specifier) => unitOf.get(specifier) ?? []));
695
+ if (spread.size < 2) continue;
696
+ problems.push({
697
+ group,
698
+ members: packages.toSorted((a, b) => a.localeCompare(b)),
699
+ units: [...spread]
700
+ });
701
+ }
702
+ return problems;
703
+ }
704
+ /**
705
+ * Finds packages that resolve to more than one copy.
706
+ *
707
+ * One output file per package name means only one copy ships and importers of the other silently get it instead. Shipping both
708
+ * would need a second output path and per-importer resolution, so this only reports the conflict.
709
+ */
710
+ function findVersionConflicts(units, discovered, index) {
711
+ const entriesBySpecifier = /* @__PURE__ */ new Map();
712
+ const record = (specifier, entry) => {
713
+ const existing = entriesBySpecifier.get(specifier) ?? /* @__PURE__ */ new Set();
714
+ existing.add(entry);
715
+ entriesBySpecifier.set(specifier, existing);
716
+ };
717
+ for (const unit of units.values()) for (const specifier of unit.exports.keys()) {
718
+ const closure = index.closureOf(specifier, discovered.get(specifier) ?? NameSet.of());
719
+ if (!closure) continue;
720
+ record(specifier, closure.entry);
721
+ for (const edge of closure.crossPackage) for (const entry of edge.entries) record(edge.specifier, entry);
722
+ }
723
+ const conflicts = [];
724
+ for (const [specifier, entries] of entriesBySpecifier) {
725
+ if (entries.size < 2) continue;
726
+ conflicts.push({
727
+ specifier,
728
+ entries: [...entries]
729
+ });
730
+ }
731
+ return conflicts;
732
+ }
733
+ /**
734
+ * Adds the export names that other output files import.
735
+ *
736
+ * `react-dom.js` imports `useState` from `react.js`. Nothing in the project imported it, so `react.js` would not export it.
737
+ */
738
+ function addCrossUnitNames(units, discovered, index) {
739
+ const unitOf = unitsBySpecifier(units);
740
+ for (const unit of units.values()) for (const specifier of unit.exports.keys()) {
741
+ const closure = index.closureOf(specifier, discovered.get(specifier) ?? NameSet.of());
742
+ if (!closure) continue;
743
+ for (const edge of closure.crossPackage) {
744
+ if (unit.exports.has(edge.specifier)) continue;
745
+ const dependencyUnits = unitOf.get(edge.specifier) ?? [];
746
+ for (const dependencyUnit of dependencyUnits) {
747
+ const target = units.get(dependencyUnit);
748
+ if (!target) continue;
749
+ didWidenEntry(target.exports, edge.specifier, edge.names);
750
+ }
751
+ }
752
+ }
753
+ }
754
+ /** Package name → every output file holding it. More than one means a chunk asked for its own copy. */
755
+ function unitsBySpecifier(units) {
756
+ const unitOf = /* @__PURE__ */ new Map();
757
+ for (const unit of units.values()) for (const specifier of unit.exports.keys()) pushInto(unitOf, specifier, unit.name);
758
+ return unitOf;
759
+ }
760
+ /** Runs discovery, grouping, chunk resolution and the cross-file export pass, which are always used together. */
761
+ function planLayout(seeds, chunks, index) {
762
+ const discovered = discoverPackages(seeds, index);
763
+ const groups = mustShareGroups(discovered, index);
764
+ const plan = resolveChunks(chunks, discovered, seeds, index, groups);
765
+ const units = planUnits(discovered, seeds, plan.chunkOf, groups);
766
+ addCrossUnitNames(units, discovered, index);
767
+ return {
768
+ units,
769
+ discovered,
770
+ plan,
771
+ groups
772
+ };
773
+ }
774
+
775
+ //#endregion
776
+ //#region src/plugins/bundle-packages/build/package-exports.ts
777
+ /** For a package no page imports, present only because a file-mate needs it. */
778
+ const NOTHING_NEEDED = {
779
+ names: NameSet.of(),
780
+ hasDefault: false,
781
+ hasSideEffect: false
782
+ };
783
+ /**
784
+ * Per file, not per package: packages in one file constrain each other. Two can own the same name, and two defaults cannot both
785
+ * be called `default`.
786
+ */
787
+ function exportsForUnit(unit, context) {
788
+ const isSharedFile = isSharedUnit(unit);
789
+ /** Name → the package in this file that claimed it. */
790
+ const taken = /* @__PURE__ */ new Map();
791
+ const members = [...unit.exports].toSorted(([a], [b]) => a.localeCompare(b));
792
+ const exports = [];
793
+ for (const [specifier, names] of members) {
794
+ const needed = context.neededByPackage.get(specifier) ?? NOTHING_NEEDED;
795
+ exports.push(exportsForPackage({
796
+ specifier,
797
+ names,
798
+ needed,
799
+ isSharedFile,
800
+ taken,
801
+ unit,
802
+ context
803
+ }));
804
+ }
805
+ return exports;
806
+ }
807
+ function exportsForPackage(turn) {
808
+ const { specifier, names, needed, isSharedFile, taken, unit, context } = turn;
809
+ const index = context.index;
810
+ const asked = mergeNeededExports(needed, names);
811
+ const wanted = expandNamesForCjs(specifier, asked.names, index);
812
+ const owned = ownedNames(specifier, index);
813
+ const named = [];
814
+ for (const name of wanted.list()) {
815
+ if (name === "default") continue;
816
+ const keptBy = taken.get(name);
817
+ if (keptBy !== void 0) {
818
+ if (!needed.names.isAll && needed.names.has(name)) context.onNameClash?.({
819
+ unit: unit.name,
820
+ name,
821
+ keptBy,
822
+ lostBy: specifier
823
+ });
824
+ continue;
825
+ }
826
+ if (owned && !owned.has(name)) continue;
827
+ taken.set(name, specifier);
828
+ named.push(name);
829
+ }
830
+ const hasStar = wanted.isAll;
831
+ const hasDefault = asked.hasDefault || hasStar && index.hasDefaultExport(specifier);
832
+ const result = {
833
+ specifier,
834
+ hasSideEffect: asked.hasSideEffect,
835
+ hasStar,
836
+ defaultAs: defaultNameFor(specifier, hasDefault, isSharedFile),
837
+ named
838
+ };
839
+ if (!result.hasStar && !result.defaultAs && result.named.length === 0 && !result.hasSideEffect) return {
840
+ ...result,
841
+ hasStar: true,
842
+ defaultAs: defaultNameFor(specifier, index.hasDefaultExport(specifier), isSharedFile)
843
+ };
844
+ return result;
845
+ }
846
+ /** Collapses every import of a package into the set of names it must export. */
847
+ function neededExports(specifiers) {
848
+ const names = NameSet.of();
849
+ let hasDefault = false;
850
+ let hasSideEffect = false;
851
+ for (const specifier of specifiers) {
852
+ if (specifier.type === "namespace" || specifier.type === "unknown") {
853
+ names.widen(NameSet.all());
854
+ continue;
855
+ }
856
+ if (specifier.type === "default") {
857
+ names.widen(NameSet.of(["default"]));
858
+ hasDefault = true;
859
+ continue;
860
+ }
861
+ if (specifier.type === "side-effect") {
862
+ hasSideEffect = true;
863
+ continue;
864
+ }
865
+ if (specifier.type !== "named") continue;
866
+ const wanted = specifier.kind === "export" ? specifier.publicName : specifier.name;
867
+ names.widen(NameSet.of([wanted]));
868
+ }
869
+ return {
870
+ names,
871
+ hasDefault,
872
+ hasSideEffect
873
+ };
874
+ }
875
+ /** Merges what this project imports with what other output files need. */
876
+ function mergeNeededExports(needed, names) {
877
+ const wanted = names.clone();
878
+ wanted.widen(needed.names);
879
+ const isDefaultNamed = !names.isAll && names.has("default");
880
+ return {
881
+ names: wanted,
882
+ hasDefault: needed.hasDefault || isDefaultNamed,
883
+ hasSideEffect: needed.hasSideEffect
884
+ };
885
+ }
886
+ /**
887
+ * `export *` carries nothing out of CommonJS, whose exports are assignments rather than syntax, so spell the names out.
888
+ *
889
+ * ES modules keep the star; spelling those out would include `export type` names that do not exist at runtime.
890
+ */
891
+ function expandNamesForCjs(specifier, names, index) {
892
+ if (!names.isAll || index.formatOf(specifier) !== "cjs") return names;
893
+ const entry = index.resolveFrom(specifier, join(index.root, "package.json"));
894
+ if (!entry) return names;
895
+ return NameSet.of(index.exportsOf(entry));
896
+ }
897
+ /** @returns The names a package publishes, or nothing when they could not be read, since an empty list means the lexer failed */
898
+ function ownedNames(specifier, index) {
899
+ const entry = index.resolveFrom(specifier, join(index.root, "package.json"));
900
+ if (!entry) return;
901
+ const owned = index.exportsOf(entry);
902
+ if (owned.length === 0) return;
903
+ return new Set(owned);
904
+ }
905
+ /** `pointReferencesAtUnits` renames the import sites to match, deriving the name the same way. */
906
+ function defaultNameFor(specifier, hasDefault, isSharedFile) {
907
+ if (!hasDefault) return;
908
+ if (!isSharedFile) return "default";
909
+ return defaultExportAlias(specifier);
910
+ }
911
+ /** Keeps defaults apart when several packages share a file. */
912
+ function defaultExportAlias(packageName) {
913
+ return "default_" + packageName.replace(/@|-|\/|\./g, "_");
914
+ }
915
+ /** Writes the entry esbuild bundles a file from, deciding nothing itself. */
916
+ function printBarrel(exports) {
917
+ let contents = "";
918
+ for (const one of exports) {
919
+ const from = `from '${one.specifier}';`;
920
+ if (one.hasSideEffect) contents += `import '${one.specifier}';`;
921
+ if (one.hasStar) contents += `\nexport * ${from}`;
922
+ if (one.defaultAs) {
923
+ const as = one.defaultAs === "default" ? "default" : `default as ${one.defaultAs}`;
924
+ contents += `\nexport { ${as} } ${from}`;
925
+ }
926
+ if (one.named.length > 0) contents += `\nexport { ${one.named.join(", ")} } ${from}`;
927
+ }
928
+ return contents;
929
+ }
930
+
931
+ //#endregion
932
+ //#region src/plugins/bundle-packages/build/build-unit.ts
933
+ /**
934
+ * Bundles one output file, skipping it when the entry has not changed.
935
+ *
936
+ * Compared on the entry itself rather than a summary of it, since a summary missing one input — the kind of import, say — leaves
937
+ * a file built from an entry the pages no longer ask for.
938
+ */
939
+ async function buildUnit(unit, context, report) {
940
+ const existingMetadata = this.findMetadata({
941
+ type: METADATA_TYPES.Package,
942
+ id: unit.name
943
+ });
944
+ const contents = printBarrel(exportsForUnit(unit, context));
945
+ if (!contents) return;
946
+ if (existingMetadata && context.builtEntries.get(unit.name) === contents) return;
947
+ const packageOutputPath = calcPackageOutPath(unit.name, context.packagesDirectory);
948
+ const packageMetadata = isPackageMetadata(existingMetadata) ? existingMetadata : newPackageMetadata(unit.name, packageOutputPath);
949
+ packageMetadata.directDependencies.clear();
950
+ const [bundledCode, bundleError] = await bundlePackage({
951
+ contents,
952
+ resolveDir: this.root,
953
+ outfile: packageOutputPath,
954
+ production: this.production,
955
+ onPackageResolve: (name) => resolveImport.call(this, name, unit, context, packageOutputPath, packageMetadata)
956
+ });
957
+ if (bundleError) {
958
+ report(`Failed to bundle package "${unit.name}"`, bundleError);
959
+ if (this.production) process.exitCode = 1;
960
+ return;
961
+ }
962
+ packageMetadata.code = bundledCode;
963
+ context.builtEntries.set(unit.name, contents);
964
+ if (!existingMetadata) this.addMetadata(packageMetadata);
965
+ }
966
+ /**
967
+ * Decides what esbuild does with a bare import it met while bundling.
968
+ *
969
+ * A package belonging to another file is made external and pointed at it. One belonging to this file, or to none, is bundled in,
970
+ * and only then can a `resolvePackage` entry say where it is read from.
971
+ */
972
+ function resolveImport(importedPackageName, unit, context, packageOutputPath, packageMetadata) {
973
+ const onProblem = (message) => this.log.warn(message);
974
+ const owningUnits = context.unitOf.get(importedPackageName) ?? [];
975
+ const customPath = getCustomPackagePath(this.root, importedPackageName, this.production, context.resolvePackage, onProblem);
976
+ const owningUnit = owningUnits.includes(unit.name) ? unit.name : owningUnits[0];
977
+ if (!owningUnit || owningUnit === unit.name) {
978
+ if (!customPath) return;
979
+ return {
980
+ path: customPath,
981
+ external: false
982
+ };
983
+ }
984
+ const dependencyOutputPath = calcPackageOutPath(owningUnit, context.packagesDirectory);
985
+ packageMetadata.directDependencies.add(dependencyOutputPath);
986
+ return {
987
+ path: relative(dirname(packageOutputPath), dependencyOutputPath),
988
+ external: true
989
+ };
990
+ }
991
+ /** A fresh metadata record for an output file that has not been built before. */
992
+ function newPackageMetadata(name, filePath) {
993
+ return {
994
+ type: METADATA_TYPES.Package,
995
+ packageName: name,
996
+ code: "",
997
+ filePath,
998
+ id: name,
999
+ directDependencies: /* @__PURE__ */ new Set()
1000
+ };
1001
+ }
1002
+
1003
+ //#endregion
1004
+ //#region src/plugins/bundle-packages/collect/narrow-namespace.ts
1005
+ /**
1006
+ * Records what a binding holding a whole package is used for: the whole namespace when nothing could be narrowed, only the names
1007
+ * actually read, or a side-effect import when the binding is never read.
1008
+ *
1009
+ * `import * as z from "zod"` and `const z = await import("zod")` both come here, so the static and dynamic forms stay in step.
1010
+ */
1011
+ function recordNamespaceUse(path, localName, specifiers) {
1012
+ const used = namesReadFrom(path, localName);
1013
+ if (used.isAll) {
1014
+ specifiers.push({ type: "namespace" });
1015
+ return;
1016
+ }
1017
+ const names = used.list();
1018
+ if (names.length === 0) {
1019
+ specifiers.push({ type: "side-effect" });
1020
+ return;
1021
+ }
1022
+ for (const name of names) {
1023
+ if (name === "default") {
1024
+ specifiers.push({
1025
+ type: "default",
1026
+ setDefaultName: renameDefaultReads(path, localName)
1027
+ });
1028
+ continue;
1029
+ }
1030
+ specifiers.push({
1031
+ type: "named",
1032
+ kind: "import",
1033
+ name,
1034
+ publicName: name
1035
+ });
1036
+ }
1037
+ }
1038
+ /**
1039
+ * The names read off a binding that holds a whole package.
1040
+ *
1041
+ * Narrowed by:
1042
+ *
1043
+ * - Reading a property: `z.string()`
1044
+ * - Destructuring: `const { string, object } = z;`
1045
+ * - Following an alias: `const zod = z;`
1046
+ *
1047
+ * Anything else gives back `NameSet.all()`: a runtime property name, a `...rest`, or the binding used as a value.
1048
+ */
1049
+ function namesReadFrom(path, importName) {
1050
+ const binding = path.scope.getBinding(importName);
1051
+ if (!binding) return NameSet.all();
1052
+ const names = NameSet.of();
1053
+ for (const referencePath of binding.referencePaths) {
1054
+ const parentPath = referencePath.parentPath;
1055
+ if (!parentPath) continue;
1056
+ const parentNode = parentPath.node;
1057
+ if (t.isMemberExpression(parentNode) && t.isIdentifier(parentNode.property)) {
1058
+ if (parentNode.computed) return NameSet.all();
1059
+ names.widen(NameSet.of([parentNode.property.name]));
1060
+ continue;
1061
+ }
1062
+ if (t.isVariableDeclarator(parentNode) && t.isIdentifier(parentNode.init)) {
1063
+ const id = parentNode.id;
1064
+ if (t.isObjectPattern(id)) {
1065
+ if (id.properties.some((property) => t.isRestElement(property))) return NameSet.all();
1066
+ for (const property of id.properties) {
1067
+ if (!t.isObjectProperty(property) || !t.isIdentifier(property.key)) continue;
1068
+ names.widen(NameSet.of([property.key.name]));
1069
+ }
1070
+ continue;
1071
+ }
1072
+ if (t.isIdentifier(id)) {
1073
+ names.widen(namesReadFrom(parentPath.get("id"), id.name));
1074
+ continue;
1075
+ }
1076
+ }
1077
+ return NameSet.all();
1078
+ }
1079
+ return names;
1080
+ }
1081
+ /**
1082
+ * Renames every `binding.default` read, so a package sharing a file keeps the default alias it was given.
1083
+ *
1084
+ * The import stays as is: the binding is the whole namespace, only the one property moves.
1085
+ */
1086
+ function renameDefaultReads(path, localName) {
1087
+ return (newName) => {
1088
+ const binding = path.scope.getBinding(localName);
1089
+ if (!binding) return;
1090
+ for (const referencePath of binding.referencePaths) {
1091
+ const parentNode = referencePath.parentPath?.node;
1092
+ if (!t.isMemberExpression(parentNode) || !t.isIdentifier(parentNode.property)) continue;
1093
+ if (parentNode.property.name !== "default") continue;
1094
+ parentNode.property = t.identifier(newName);
1095
+ }
1096
+ };
1097
+ }
1098
+
1099
+ //#endregion
1100
+ //#region src/plugins/bundle-packages/collect/collect-specifiers.ts
259
1101
  const traverse$1 = typeof _traverse === "function" ? _traverse : _traverse.default;
1102
+ const NODE_MODULES = "node_modules";
1103
+ /** Pairs the names an import takes with a `source` that reads and writes the AST node holding its path. */
1104
+ function sourceReference(read, write, specifiers) {
1105
+ return {
1106
+ get source() {
1107
+ return read();
1108
+ },
1109
+ set source(newValue) {
1110
+ write(newValue);
1111
+ },
1112
+ specifiers
1113
+ };
1114
+ }
1115
+ /**
1116
+ * Whether any import in a file names a path inside `node_modules`.
1117
+ *
1118
+ * A plain node walk, with no paths or scope built, so it is cheap enough to run over every script before deciding whether the
1119
+ * full traversal is worth it. `resolveSource` has already rewritten package imports into `node_modules` paths, so a file whose
1120
+ * imports never name one cannot reach a package.
1121
+ */
1122
+ function hasNodeModulesImport(node) {
1123
+ if (!node || typeof node.type !== "string") return false;
1124
+ let isFound = false;
1125
+ t.traverseFast(node, (current) => {
1126
+ if (importSourceOf(current)?.includes(NODE_MODULES)) isFound = true;
1127
+ });
1128
+ return isFound;
1129
+ }
1130
+ /** The literal path one node imports from, whatever form the import takes. */
1131
+ function importSourceOf(node) {
1132
+ if (t.isImportDeclaration(node) || t.isExportAllDeclaration(node) || t.isExportNamedDeclaration(node)) return t.isStringLiteral(node.source) ? node.source.value : void 0;
1133
+ return dynamicImportSource(node)?.value;
1134
+ }
1135
+ /** Every import in a file, with the names it takes and a writable `source` that rewrites its path. */
260
1136
  function collectSpecifiersFromAst(ast) {
261
1137
  const result = [];
262
1138
  traverse$1(ast, {
263
1139
  ImportDeclaration(path) {
264
1140
  if (!t.isStringLiteral(path.node.source)) return;
265
- if ((path.node.importKind ?? "value") === "type") return;
1141
+ if (path.node.importKind === "type") return;
266
1142
  const source = path.node.source.value;
267
1143
  if (source.startsWith("node:")) return;
268
1144
  const specifiers = [];
269
1145
  collectImportDeclarationSpecifiers(path, specifiers);
270
- result.push({
271
- get source() {
272
- return source;
273
- },
274
- set source(newValue) {
275
- if (path.node) path.node.source = t.stringLiteral(newValue);
276
- },
277
- specifiers
278
- });
1146
+ const write = (newValue) => {
1147
+ if (!path.node) return;
1148
+ path.node.source = t.stringLiteral(newValue);
1149
+ };
1150
+ result.push(sourceReference(() => source, write, specifiers));
279
1151
  },
280
1152
  ExportAllDeclaration(path) {
281
1153
  if (!path.node.source.value) return;
282
- if ((path.node.exportKind ?? "value") === "type") return;
1154
+ if (path.node.exportKind === "type") return;
283
1155
  const specifiers = [];
284
1156
  collectExportAllDeclarationSpecifiers(path, specifiers);
285
- result.push({
286
- get source() {
287
- return path.node.source.value;
288
- },
289
- set source(newValue) {
290
- path.node.source = t.stringLiteral(newValue);
291
- },
292
- specifiers
293
- });
1157
+ const read = () => path.node.source.value;
1158
+ const write = (newValue) => {
1159
+ path.node.source = t.stringLiteral(newValue);
1160
+ };
1161
+ result.push(sourceReference(read, write, specifiers));
294
1162
  },
295
1163
  ExportNamedDeclaration(path) {
296
1164
  if (!t.isStringLiteral(path.node.source)) return;
297
- if ((path.node.exportKind ?? "value") === "type") return;
1165
+ if (path.node.exportKind === "type") return;
298
1166
  const source = path.node.source.value;
299
1167
  if (source.startsWith("node:")) return;
300
1168
  const specifiers = [];
301
1169
  collectExportNamedDeclarationSpecifiers(path, specifiers);
302
- result.push({
303
- get source() {
304
- return source;
305
- },
306
- set source(newValue) {
307
- path.node.source = t.stringLiteral(newValue);
308
- },
309
- specifiers
310
- });
1170
+ const write = (newValue) => {
1171
+ path.node.source = t.stringLiteral(newValue);
1172
+ };
1173
+ result.push(sourceReference(() => source, write, specifiers));
311
1174
  },
312
1175
  CallExpression(path) {
313
- if (!t.isImport(path.node.callee) || !t.isStringLiteral(path.node.arguments[0])) return;
314
- const setSource = (newValue) => {
315
- path.node.arguments[0] = t.stringLiteral(newValue);
316
- };
317
- const source = path.node.arguments[0].value;
318
- if (source.startsWith("node:")) return;
319
- const specifiers = [];
320
- collectDynamicImportSpecifiers(path, specifiers);
321
- result.push({
322
- get source() {
323
- return source;
324
- },
325
- set source(newValue) {
326
- setSource(newValue);
327
- },
328
- specifiers
329
- });
1176
+ collectDynamicImport(path, result);
1177
+ },
1178
+ ImportExpression(path) {
1179
+ collectDynamicImport(path, result);
330
1180
  }
331
1181
  });
332
1182
  return result;
333
1183
  }
1184
+ /** The literal specifier of a dynamic import, for either node shape. */
1185
+ function dynamicImportSource(node) {
1186
+ if (t.isImportExpression(node)) return t.isStringLiteral(node.source) ? node.source : void 0;
1187
+ if (!t.isCallExpression(node) || !t.isImport(node.callee)) return;
1188
+ const argument = node.arguments[0];
1189
+ return t.isStringLiteral(argument) ? argument : void 0;
1190
+ }
1191
+ /** Records one `import()` along with how to rewrite its path. */
1192
+ function collectDynamicImport(path, result) {
1193
+ const literal = dynamicImportSource(path.node);
1194
+ if (!literal) return;
1195
+ const source = literal.value;
1196
+ if (source.startsWith("node:")) return;
1197
+ const write = (newValue) => {
1198
+ if (t.isImportExpression(path.node)) {
1199
+ path.node.source = t.stringLiteral(newValue);
1200
+ return;
1201
+ }
1202
+ path.node.arguments[0] = t.stringLiteral(newValue);
1203
+ };
1204
+ const specifiers = [];
1205
+ collectDynamicImportSpecifiers(path, specifiers);
1206
+ result.push(sourceReference(() => source, write, specifiers));
1207
+ }
1208
+ /** What one `import ... from "x"` takes. No names at all means a side-effect import. */
334
1209
  function collectImportDeclarationSpecifiers(path, specifiers) {
335
1210
  const node = path.node;
336
1211
  if (!t.isStringLiteral(node.source) || node.importKind && node.importKind !== "value") return;
337
1212
  let hasFoundSpecifier = false;
338
- for (let index = 0; index < node.specifiers.length; index++) {
339
- const specifier = node.specifiers[index];
1213
+ for (const specifier of node.specifiers) {
340
1214
  if (t.isImportDefaultSpecifier(specifier)) {
341
1215
  const setDefaultName = (newName) => {
342
- const index = node.specifiers.indexOf(specifier);
343
- if (index === -1) return;
344
- node.specifiers[index] = t.importSpecifier(t.identifier(specifier.local.name), t.identifier(newName));
1216
+ const at = node.specifiers.indexOf(specifier);
1217
+ if (at === -1) return;
1218
+ node.specifiers[at] = t.importSpecifier(t.identifier(specifier.local.name), t.identifier(newName));
345
1219
  };
346
1220
  specifiers.push({
347
1221
  type: "default",
348
- name: specifier.local.name,
349
1222
  setDefaultName
350
1223
  });
351
1224
  hasFoundSpecifier = true;
@@ -364,60 +1237,30 @@ function collectImportDeclarationSpecifiers(path, specifiers) {
364
1237
  continue;
365
1238
  }
366
1239
  if (t.isImportNamespaceSpecifier(specifier)) {
367
- const usedNames = collectUsedNames(path, specifier.local.name);
368
- for (const name of usedNames) {
369
- if (name === "default") {
370
- const capturedLocalName = specifier.local.name;
371
- const capturedPath = path;
372
- const setDefaultName = (newName) => {
373
- const binding = capturedPath.scope.getBinding(capturedLocalName);
374
- if (!binding) return;
375
- for (const referencePath of binding.referencePaths) {
376
- const parentNode = referencePath.parentPath?.node;
377
- if (t.isMemberExpression(parentNode) && t.isIdentifier(parentNode.property) && parentNode.property.name === "default") parentNode.property = t.identifier(newName);
378
- }
379
- };
380
- specifiers.push({
381
- type: "default",
382
- name: capturedLocalName,
383
- setDefaultName
384
- });
385
- hasFoundSpecifier = true;
386
- continue;
387
- }
388
- specifiers.push({
389
- type: "named",
390
- kind: "import",
391
- name,
392
- publicName: name
393
- });
394
- hasFoundSpecifier = true;
395
- }
1240
+ recordNamespaceUse(path, specifier.local.name, specifiers);
1241
+ hasFoundSpecifier = true;
396
1242
  }
397
1243
  }
398
1244
  if (!hasFoundSpecifier) specifiers.push({ type: "side-effect" });
399
1245
  }
1246
+ /** `export * from "x"` can forward any name, so the whole package is taken. */
400
1247
  function collectExportAllDeclarationSpecifiers(path, specifiers) {
401
1248
  if (!path.node.source.value || path.node.exportKind !== "value") return;
402
- specifiers.push({
403
- type: "namespace",
404
- name: "*"
405
- });
1249
+ specifiers.push({ type: "namespace" });
406
1250
  }
1251
+ /** What one `export { ... } from "x"` takes, using the names before any rename. */
407
1252
  function collectExportNamedDeclarationSpecifiers(path, specifiers) {
408
1253
  const node = path.node;
409
1254
  if (!t.isStringLiteral(node.source) || node.exportKind !== "value") return;
410
- for (let index = 0; index < node.specifiers.length; index++) {
411
- const specifier = node.specifiers[index];
1255
+ for (const specifier of node.specifiers) {
412
1256
  if (t.isExportDefaultSpecifier(specifier) && t.isIdentifier(specifier.exported)) {
413
1257
  const setDefaultName = (newName) => {
414
- const index = node.specifiers.indexOf(specifier);
415
- if (index === -1) return;
416
- node.specifiers[index] = t.exportSpecifier(t.identifier(newName), t.identifier(specifier.exported.name));
1258
+ const at = node.specifiers.indexOf(specifier);
1259
+ if (at === -1) return;
1260
+ node.specifiers[at] = t.exportSpecifier(t.identifier(newName), t.identifier(specifier.exported.name));
417
1261
  };
418
1262
  specifiers.push({
419
1263
  type: "default",
420
- name: specifier.exported.name,
421
1264
  setDefaultName
422
1265
  });
423
1266
  continue;
@@ -432,15 +1275,11 @@ function collectExportNamedDeclarationSpecifiers(path, specifiers) {
432
1275
  });
433
1276
  continue;
434
1277
  }
435
- if (t.isExportNamespaceSpecifier(specifier)) specifiers.push({
436
- type: "namespace",
437
- name: t.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value
438
- });
1278
+ if (t.isExportNamespaceSpecifier(specifier)) specifiers.push({ type: "namespace" });
439
1279
  }
440
1280
  }
441
1281
  function collectDynamicImportSpecifiers(path, specifiers) {
442
- if (!t.isImport(path.node.callee)) return;
443
- if (!t.isStringLiteral(path.node.arguments[0])) {
1282
+ if (!dynamicImportSource(path.node)) {
444
1283
  specifiers.push({ type: "unknown" });
445
1284
  return;
446
1285
  }
@@ -451,17 +1290,17 @@ function collectDynamicImportSpecifiers(path, specifiers) {
451
1290
  }
452
1291
  const parentNode = parentPath.parentPath?.node;
453
1292
  if (t.isMemberExpression(parentNode)) {
454
- const property = parentNode.property;
455
- const importedName = getNodeName(property);
456
- if (!importedName) return;
1293
+ const importedName = staticKey(parentNode.property, parentNode.computed);
1294
+ if (!importedName) {
1295
+ specifiers.push({ type: "unknown" });
1296
+ return;
1297
+ }
457
1298
  if (importedName === "default") {
458
- const capturedMemberExpression = parentNode;
459
1299
  const setDefaultName = (newName) => {
460
- capturedMemberExpression.property = t.identifier(newName);
1300
+ parentNode.property = t.identifier(newName);
461
1301
  };
462
1302
  specifiers.push({
463
1303
  type: "default",
464
- name: importedName,
465
1304
  setDefaultName
466
1305
  });
467
1306
  return;
@@ -475,322 +1314,769 @@ function collectDynamicImportSpecifiers(path, specifiers) {
475
1314
  return;
476
1315
  }
477
1316
  if (t.isVariableDeclarator(parentNode) && t.isObjectPattern(parentNode.id)) {
478
- for (const property of parentNode.id.properties) {
479
- if (!t.isObjectProperty(property)) continue;
480
- const importedName = getNodeName(property.key);
481
- if (!importedName) continue;
482
- const localName = getNodeName(property.value);
483
- if (importedName === "default") {
484
- const setDefaultName = (newName) => {
485
- property.key = t.identifier(newName);
486
- };
487
- specifiers.push({
488
- type: "default",
489
- name: localName ?? importedName,
490
- setDefaultName
491
- });
492
- continue;
493
- }
494
- specifiers.push({
495
- type: "named",
496
- kind: "import",
497
- name: importedName,
498
- publicName: localName ?? importedName
499
- });
500
- }
1317
+ collectDestructuredNames(parentNode.id, specifiers);
501
1318
  return;
502
1319
  }
503
1320
  if (t.isVariableDeclarator(parentNode) && t.isIdentifier(parentNode.id)) {
504
- const usedNames = collectUsedNames(path, parentNode.id.name);
505
- for (const name of usedNames) {
506
- if (name === "default") {
507
- const capturedId = parentNode.id;
508
- const capturedPath = path;
509
- const setDefaultName = (newName) => {
510
- const binding = capturedPath.scope.getBinding(capturedId.name);
511
- if (!binding) return;
512
- for (const referencePath of binding.referencePaths) {
513
- const pNode = referencePath.parentPath?.node;
514
- if (t.isMemberExpression(pNode) && t.isIdentifier(pNode.property) && pNode.property.name === "default") pNode.property = t.identifier(newName);
515
- }
516
- };
517
- specifiers.push({
518
- type: "default",
519
- name: parentNode.id.name,
520
- setDefaultName
521
- });
522
- continue;
523
- }
524
- specifiers.push({
525
- type: "named",
526
- kind: "import",
527
- name,
528
- publicName: name
1321
+ recordNamespaceUse(path, parentNode.id.name, specifiers);
1322
+ return;
1323
+ }
1324
+ if (!parentNode || t.isExpressionStatement(parentNode)) {
1325
+ specifiers.push({ type: "side-effect" });
1326
+ return;
1327
+ }
1328
+ specifiers.push({ type: "unknown" });
1329
+ }
1330
+ /** What `const { a, b: c } = await import('path')` takes, or the whole package when a property cannot be named. */
1331
+ function collectDestructuredNames(pattern, specifiers) {
1332
+ const found = [];
1333
+ for (const property of pattern.properties) {
1334
+ if (!t.isObjectProperty(property)) {
1335
+ specifiers.push({ type: "unknown" });
1336
+ return;
1337
+ }
1338
+ const importedName = staticKey(property.key, property.computed);
1339
+ if (!importedName) {
1340
+ specifiers.push({ type: "unknown" });
1341
+ return;
1342
+ }
1343
+ const localName = t.isIdentifier(property.value) ? property.value.name : void 0;
1344
+ if (importedName === "default") {
1345
+ const setDefaultName = (newName) => {
1346
+ property.key = t.identifier(newName);
1347
+ };
1348
+ found.push({
1349
+ type: "default",
1350
+ setDefaultName
529
1351
  });
1352
+ continue;
530
1353
  }
531
- return;
1354
+ found.push({
1355
+ type: "named",
1356
+ kind: "import",
1357
+ name: importedName,
1358
+ publicName: localName ?? importedName
1359
+ });
532
1360
  }
533
- if (!parentNode || t.isExpressionStatement(parentNode)) specifiers.push({ type: "side-effect" });
1361
+ specifiers.push(...found);
1362
+ }
1363
+ /** The name a property key stands for, or nothing when only the running program knows it. */
1364
+ function staticKey(node, isComputed) {
1365
+ if (t.isStringLiteral(node)) return node.value;
1366
+ if (isComputed) return null;
1367
+ return getNodeName(node);
534
1368
  }
1369
+ /** The name a node stands for, however it was written. */
1370
+ function getNodeName(node) {
1371
+ if (t.isIdentifier(node)) return node.name;
1372
+ if (t.isStringLiteral(node)) return node.value;
1373
+ if (t.isPrivateName(node)) return node.id.name;
1374
+ return null;
1375
+ }
1376
+
1377
+ //#endregion
1378
+ //#region src/plugins/bundle-packages/collect/collect-references.ts
1379
+ /** The name of the package an import points into, or nothing if it stays inside the project. */
1380
+ function packageOfSource(source, fromFile, context) {
1381
+ const sourceRootRelative = join(dirname(fromFile), source);
1382
+ if (isNodeModulesPath(sourceRootRelative)) return context.pathPackageMap.get(join(context.root, sourceRootRelative));
1383
+ }
1384
+ /** Records what an import takes from a package, so the package's file exports it. */
1385
+ function recordSpecifiers(packageName, specifiers, context) {
1386
+ const existing = context.specifiersByPackage.get(packageName) ?? [];
1387
+ existing.push(...specifiers);
1388
+ context.specifiersByPackage.set(packageName, existing);
1389
+ }
1390
+ /** Every package import in a script, standalone or inline in a script tag. */
1391
+ function collectScriptReferences(metadata, context) {
1392
+ const references = [];
1393
+ if (!isNodeModulesPath(metadata.filePath) && !hasNodeModulesImport(metadata.ast)) return references;
1394
+ for (const sourceReference of collectSpecifiersFromAst(metadata.ast)) {
1395
+ const packageName = packageOfSource(sourceReference.source, metadata.filePath, context);
1396
+ if (!packageName) {
1397
+ reportUntracedSource(sourceReference.source, metadata.filePath, context);
1398
+ continue;
1399
+ }
1400
+ recordSpecifiers(packageName, sourceReference.specifiers, context);
1401
+ references.push({
1402
+ setSource: (source) => {
1403
+ sourceReference.source = source;
1404
+ },
1405
+ fromFile: metadata.filePath,
1406
+ packageName,
1407
+ specifiers: sourceReference.specifiers
1408
+ });
1409
+ }
1410
+ return references;
1411
+ }
1412
+ /** Every package import written as a `<script src>` tag, which only runs the package, so it counts as a side-effect import. */
1413
+ function collectScriptTagReferences(metadata, context) {
1414
+ const references = [];
1415
+ for (const scriptTag of metadata.ast.querySelectorAll("script[src]")) {
1416
+ const source = scriptTag.getAttribute("src");
1417
+ if (!source) continue;
1418
+ if (!isScriptType(scriptTag.getAttribute("type"))) continue;
1419
+ const packageName = packageOfSource(source, metadata.filePath, context);
1420
+ if (!packageName) {
1421
+ reportUntracedSource(source, metadata.filePath, context);
1422
+ continue;
1423
+ }
1424
+ const specifiers = [{ type: "side-effect" }];
1425
+ recordSpecifiers(packageName, specifiers, context);
1426
+ references.push({
1427
+ setSource: (newSource) => {
1428
+ scriptTag.setAttribute("src", newSource);
1429
+ },
1430
+ fromFile: metadata.filePath,
1431
+ packageName,
1432
+ specifiers
1433
+ });
1434
+ }
1435
+ return references;
1436
+ }
1437
+ /** The `href` of every stylesheet link, as sources that write back to the tag. */
1438
+ function styleLinkSources(metadata) {
1439
+ return metadata.ast.querySelectorAll("link[rel=\"stylesheet\"], link[rel=\"preload\"][as=\"style\"]").map((styleLink) => ({
1440
+ get source() {
1441
+ return styleLink.getAttribute("href") ?? "";
1442
+ },
1443
+ set source(value) {
1444
+ styleLink.setAttribute("href", value);
1445
+ }
1446
+ }));
1447
+ }
1448
+ /** Moves the files a package ships next to the package output and points the sources reaching into it at the new place. */
1449
+ async function rebasePackageAssets(fromFile, sources, packagesDirectory, assetByPath) {
1450
+ for (const reference of sources) {
1451
+ const [link, suffix] = splitHtmlLink(reference.source);
1452
+ const sourceRootRelative = join(dirname(fromFile), link);
1453
+ if (!isNodeModulesPath(sourceRootRelative)) continue;
1454
+ const asset = assetByPath.get(join(this.root, sourceRootRelative));
1455
+ if (!asset) continue;
1456
+ const assetOutputPath = calcPackageAssetOutPath(sourceRootRelative, packagesDirectory);
1457
+ if (asset.filePath !== assetOutputPath) {
1458
+ await this.rebase(asset, join(this.root, assetOutputPath));
1459
+ asset.filePath = assetOutputPath;
1460
+ }
1461
+ reference.source = relative(dirname(fromFile), assetOutputPath) + suffix;
1462
+ }
1463
+ }
1464
+ /** Walks every file in the build and collects the imports that reach into `node_modules`. */
1465
+ async function collectReferences(context, packagesDirectory) {
1466
+ const references = [];
1467
+ /** Absolute path → the metadata holding it, so a stylesheet is found without scanning the whole list per link. */
1468
+ const assetByPath = /* @__PURE__ */ new Map();
1469
+ for (const item of this.metadataList) assetByPath.set(join(this.root, item.id), item);
1470
+ for (const inputMetadata of this.metadataList) {
1471
+ for (const { metadata } of filterScriptMetadata(inputMetadata)) references.push(...collectScriptReferences(metadata, context));
1472
+ for (const { metadata } of filterStyleMetadata(inputMetadata)) {
1473
+ const sources = await this.requestSources(metadata);
1474
+ await rebasePackageAssets.call(this, metadata.filePath, sources, packagesDirectory, assetByPath);
1475
+ }
1476
+ if (!isHtmlMetadata(inputMetadata)) continue;
1477
+ references.push(...collectScriptTagReferences(inputMetadata, context));
1478
+ await rebasePackageAssets.call(this, inputMetadata.filePath, styleLinkSources(inputMetadata), packagesDirectory, assetByPath);
1479
+ }
1480
+ return references;
1481
+ }
1482
+ /** Points every import at the file its package ended up in, renaming defaults when a file holds several packages. */
1483
+ function pointReferencesAtUnits(references, units, unitOf, packagesDirectory) {
1484
+ for (const reference of references) {
1485
+ const unitName = unitOf.get(reference.packageName)?.[0] ?? reference.packageName;
1486
+ const packageOutputPath = calcPackageOutPath(unitName, packagesDirectory);
1487
+ reference.setSource(relative(dirname(reference.fromFile), packageOutputPath));
1488
+ const unit = units.get(unitName);
1489
+ if (!unit || !isSharedUnit(unit)) continue;
1490
+ const alias = defaultExportAlias(reference.packageName);
1491
+ for (const specifier of reference.specifiers) if (specifier.type === "default") specifier.setDefaultName(alias);
1492
+ }
1493
+ }
1494
+ /** Reports an import into `node_modules` whose package could not be named, leaving it out of the bundles. */
1495
+ function reportUntracedSource(source, fromFile, context) {
1496
+ const sourceRootRelative = join(dirname(fromFile), source);
1497
+ if (isNodeModulesPath(sourceRootRelative)) context.reportUntraced(`Could not resolve a package name for "${source}" imported from "${fromFile}".\nThe import is left pointing into node_modules and will not be bundled.`);
1498
+ }
1499
+
1500
+ //#endregion
1501
+ //#region src/plugins/bundle-packages/index/read-requires.ts
1502
+ const ENVIRONMENT_GUARD = "process.env.NODE_ENV";
535
1503
  /**
536
- * Collects used names from a name space import. E.g., `import * as z from "zod"`\
537
- * Covers most cases when importing a package using a name space.
1504
+ * Collects the specifiers a CommonJS file requires, with dead branches removed.
538
1505
  *
539
- * Covered cases:
1506
+ * Packages routinely ship an entry that switches on the build mode:
540
1507
  *
541
- * - Direct member expression: `z.string()`
542
- * - Destructuring: `const { string, object } = z;`
543
- * - Following aliases: `const zod = z;`
544
- *
545
- * Ignored cases:
1508
+ * ```js
1509
+ * if (process.env.NODE_ENV !== "production") {
1510
+ * module.exports = require("./cjs/react.development.js");
1511
+ * } else {
1512
+ * module.exports = require("./cjs/react.production.js");
1513
+ * }
1514
+ * ```
546
1515
  *
547
- * - Object destructuring with spread syntax: `const { string, ...rest } = z;`
1516
+ * Esbuild gets `process.env.NODE_ENV` as a define, so it keeps one branch and drops the other. Reading both would report requires
1517
+ * that never reach the output, grouping packages that do not need each other in that mode.
548
1518
  */
549
- function collectUsedNames(path, importName) {
550
- const usedNames = /* @__PURE__ */ new Set();
551
- const binding = path.scope.getBinding(importName);
552
- if (!binding) return usedNames;
553
- for (const referencePath of binding.referencePaths) {
554
- const parentPath = referencePath.parentPath;
555
- if (!parentPath) continue;
556
- const parentNode = parentPath.node;
557
- if (t.isMemberExpression(parentNode) && t.isIdentifier(parentNode.property)) {
558
- usedNames.add(parentNode.property.name);
1519
+ function readRequires(code, isProduction) {
1520
+ if (!code.includes(ENVIRONMENT_GUARD)) return matchRequires(code);
1521
+ const ast = tryParse(code);
1522
+ if (!ast) return matchRequires(code);
1523
+ const specifiers = [];
1524
+ for (const node of ast.program.body) collectFromNode(node, isProduction ? "production" : "development", specifiers);
1525
+ return specifiers;
1526
+ }
1527
+ /** Every `require("…")` in the source, without looking at what guards it. */
1528
+ function matchRequires(code) {
1529
+ return Array.from(code.matchAll(/require\(\s*["']([^"']+)["']\s*\)/g), (match) => match[1]);
1530
+ }
1531
+ /** Parses CommonJS, which is a script rather than a module. */
1532
+ function tryParse(code) {
1533
+ try {
1534
+ return parse$3(code, {
1535
+ sourceType: "script",
1536
+ allowReturnOutsideFunction: true,
1537
+ errorRecovery: true
1538
+ });
1539
+ } catch {
1540
+ return null;
1541
+ }
1542
+ }
1543
+ /** Walks one node, following only the branches that survive in this mode. */
1544
+ function collectFromNode(node, mode, specifiers) {
1545
+ if (!node) return;
1546
+ if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" })) {
1547
+ const argument = node.arguments[0];
1548
+ if (t.isStringLiteral(argument)) specifiers.push(argument.value);
1549
+ }
1550
+ const taken = takenBranch(node, mode);
1551
+ if (taken !== void 0) {
1552
+ collectFromNode(taken, mode, specifiers);
1553
+ return;
1554
+ }
1555
+ const keys = t.VISITOR_KEYS[node.type] ?? [];
1556
+ for (const key of keys) {
1557
+ const value = node[key];
1558
+ if (Array.isArray(value)) {
1559
+ for (const element of value) if (t.isNode(element)) collectFromNode(element, mode, specifiers);
559
1560
  continue;
560
1561
  }
561
- if (t.isVariableDeclarator(parentNode) && t.isIdentifier(parentNode.init)) {
562
- const id = parentNode.id;
563
- if (t.isObjectPattern(id)) {
564
- for (const property of id.properties) {
565
- if (t.isRestElement(property) || !t.isIdentifier(property.key)) continue;
566
- usedNames.add(property.key.name);
567
- }
568
- continue;
569
- }
570
- if (t.isIdentifier(id)) {
571
- const aliasUsed = collectUsedNames(parentPath.get("id"), id.name);
572
- for (const n of aliasUsed) usedNames.add(n);
573
- }
574
- }
1562
+ if (t.isNode(value)) collectFromNode(value, mode, specifiers);
575
1563
  }
576
- return usedNames;
577
1564
  }
578
- function getNodeName(node) {
579
- if (t.isIdentifier(node)) return node.name;
580
- if (t.isStringLiteral(node)) return node.value;
581
- if (t.isPrivateName(node)) return node.id.name;
582
- return null;
1565
+ /**
1566
+ * @returns The branch a mode check resolves to, `undefined` when the node is not a mode check or its answer is unknown, and
1567
+ * `null` when it resolves to a branch that is not there, such as an `if` with no `else`
1568
+ */
1569
+ function takenBranch(node, mode) {
1570
+ if (!t.isIfStatement(node) && !t.isConditionalExpression(node)) return;
1571
+ const result = evaluateEnvironmentTest(node.test, mode);
1572
+ if (result === void 0) return;
1573
+ return result ? node.consequent : node.alternate ?? null;
1574
+ }
1575
+ /** Evaluates a comparison between `process.env.NODE_ENV` and a string, or nothing if the test is anything else. */
1576
+ function evaluateEnvironmentTest(test, mode) {
1577
+ if (!t.isBinaryExpression(test)) return;
1578
+ if (![
1579
+ "===",
1580
+ "==",
1581
+ "!==",
1582
+ "!="
1583
+ ].includes(test.operator)) return;
1584
+ const left = readComparisonSide(test.left);
1585
+ const right = readComparisonSide(test.right);
1586
+ if (!left || !right) return;
1587
+ if (left.kind === right.kind) return;
1588
+ const isEqual = (left.kind === "literal" ? left.value : right.value) === mode;
1589
+ return test.operator.startsWith("!") ? !isEqual : isEqual;
1590
+ }
1591
+ /** Reads one side of a comparison as either the environment lookup or a string literal. */
1592
+ function readComparisonSide(node) {
1593
+ if (t.isStringLiteral(node)) return {
1594
+ kind: "literal",
1595
+ value: node.value
1596
+ };
1597
+ if (isEnvironmentLookup(node)) return { kind: "environment" };
1598
+ }
1599
+ /** Whether a node is `process.env.NODE_ENV`. */
1600
+ function isEnvironmentLookup(node) {
1601
+ if (!t.isMemberExpression(node) || !t.isIdentifier(node.property, { name: "NODE_ENV" })) return false;
1602
+ const object = node.object;
1603
+ if (!t.isMemberExpression(object) || !t.isIdentifier(object.property, { name: "env" })) return false;
1604
+ return t.isIdentifier(object.object, { name: "process" });
583
1605
  }
584
1606
 
585
1607
  //#endregion
586
- //#region src/plugins/bundle-packages/helpers.ts
1608
+ //#region src/plugins/bundle-packages/index/lex-module.ts
587
1609
  /**
588
- * Some package names may include `.js` as part of their name (e.g., `highlight.js`).\
589
- * If we have a subpackage within such a package, we face a conflict:\
590
- * We cannot create a folder named `highlight.js` because a file with the same name (`highlight.js`) already exists.\
591
- * To resolve this, we replace `.js` with `_js` in the subpackage path only, avoiding conflicts.\
592
- * Example: `highlight.js/languages/javascript.js` -> `highlight_js/languages/javascript.js`
1610
+ * Reads one file into a record, without resolving anything or opening another file.
1611
+ *
1612
+ * A file with neither imports nor exports lexes as neither ESM nor CJS, and is treated as an ES module exporting nothing.
593
1613
  */
594
- function normalizePackageName$1(originalSource) {
595
- const parts = originalSource.split("/");
596
- if (parts.length <= 1) return originalSource;
597
- const outParts = [];
598
- for (let index = 0; index < parts.length; index++) {
599
- const part = parts[index];
600
- if (index === parts.length - 1) {
601
- outParts.push(part);
1614
+ function lexModule(file, code, mtimeMs, isProduction) {
1615
+ const [imports, exports] = parse$2(code, file);
1616
+ const isEsm = imports.length > 0 || exports.length > 0;
1617
+ const record = {
1618
+ file,
1619
+ mtimeMs,
1620
+ format: isEsm ? "esm" : "cjs",
1621
+ reexportOf: /* @__PURE__ */ new Map(),
1622
+ ownExports: [],
1623
+ starSources: [],
1624
+ edges: []
1625
+ };
1626
+ if (isEsm) {
1627
+ readEsmRecord(record, code, imports, exports);
1628
+ return record;
1629
+ }
1630
+ readCjsRecord(record, code, isProduction);
1631
+ return record;
1632
+ }
1633
+ /** Fills a record from the ES module lexer's output. */
1634
+ function readEsmRecord(record, code, imports, exports) {
1635
+ for (const exported of exports) {
1636
+ const owner = imports.find((index) => index.n && exported.s >= index.ss && exported.e <= index.se);
1637
+ if (!owner?.n) {
1638
+ record.ownExports.push(exported.n);
1639
+ continue;
1640
+ }
1641
+ const renames = reexportedNames(code.slice(owner.ss, owner.se));
1642
+ record.reexportOf.set(exported.n, {
1643
+ source: owner.n,
1644
+ importedName: renames.get(exported.n)
1645
+ });
1646
+ }
1647
+ for (const imported of imports) {
1648
+ if (!imported.n) continue;
1649
+ const statement = code.slice(imported.ss, imported.se);
1650
+ if (/^export\s*\*/.test(statement)) {
1651
+ record.starSources.push(imported.n);
602
1652
  continue;
603
1653
  }
604
- outParts.push(part.replace(".js", "_js"));
1654
+ if (/^export\s*\{/.test(statement)) continue;
1655
+ const isDynamic = imported.d > -1;
1656
+ record.edges.push({
1657
+ specifier: imported.n,
1658
+ kind: "import",
1659
+ names: namesOfImport(statement),
1660
+ isDynamic
1661
+ });
605
1662
  }
606
- return outParts.join("/");
607
1663
  }
608
- function calcPackageOutPath(packageName, packagesDirectory) {
609
- const normalizedPackageName = normalizePackageName$1(packageName);
610
- const withJsExtension = replaceExtension(normalizedPackageName, ".js");
611
- return join(packagesDirectory, withJsExtension);
1664
+ /** Fills a record from the CommonJS lexer's output, plus the `require()` calls that survive in this mode. */
1665
+ function readCjsRecord(record, code, isProduction) {
1666
+ const parsed = tryParseCjs(code);
1667
+ if (!parsed) {
1668
+ record.format = "esm";
1669
+ return;
1670
+ }
1671
+ const live = new Set(readRequires(code, isProduction));
1672
+ record.ownExports.push(...parsed.exports);
1673
+ for (const reexport of liveReexports(parsed.reexports, live)) record.reexportOf.set(reexport, {
1674
+ source: reexport,
1675
+ importedName: reexport
1676
+ });
1677
+ for (const specifier of live) record.edges.push({
1678
+ specifier,
1679
+ kind: "require",
1680
+ names: void 0,
1681
+ isDynamic: false
1682
+ });
612
1683
  }
613
1684
  /**
614
- * Output path for an asset shipped by a package, keeping its own extension.
1685
+ * The re-export targets that survive in this mode.
615
1686
  *
616
- * Example: `node_modules/highlight.js/styles/default.css` -> `packages/highlight_js/styles/default.css`
1687
+ * The lexer always reports the development branch of a mode check, so in production it names a file the build drops. The relative
1688
+ * requires that did survive are the real targets.
617
1689
  */
618
- function calcPackageAssetOutPath(nodeModulesPath, packagesDirectory) {
619
- const markerIndex = nodeModulesPath.lastIndexOf("node_modules/");
620
- const packagePath = markerIndex === -1 ? nodeModulesPath : nodeModulesPath.slice(markerIndex + 13);
621
- return join(packagesDirectory, normalizePackageName$1(packagePath));
622
- }
623
- function generateStdinString(specifiers, packageName) {
624
- let isDefaultImport = false;
625
- let newDefaultName;
626
- let hasSideEffect = false;
627
- let isNamespace = false;
628
- const named = /* @__PURE__ */ new Set();
629
- for (const specifier of specifiers) {
630
- if (specifier.type === "default") {
631
- isDefaultImport = true;
632
- newDefaultName = specifier.newDefaultName;
633
- }
634
- if (specifier.type === "named" && specifier.kind === "import") named.add(specifier.name);
635
- if (specifier.type === "named" && specifier.kind === "export") {
636
- named.delete(specifier.name);
637
- named.add(specifier.publicName);
638
- }
639
- if (specifier.type === "namespace") isNamespace = true;
640
- else if (specifier.type === "side-effect") hasSideEffect = true;
1690
+ function liveReexports(reported, live) {
1691
+ const kept = reported.filter((reexport) => live.has(reexport));
1692
+ if (kept.length > 0) return kept;
1693
+ if (reported.length === 0) return reported;
1694
+ return live.values().filter((specifier) => specifier.startsWith(".")).toArray();
1695
+ }
1696
+ /** Runs the CommonJS lexer, which throws on ES module syntax. */
1697
+ function tryParseCjs(code) {
1698
+ try {
1699
+ return parse$1(code);
1700
+ } catch {
1701
+ return null;
641
1702
  }
642
- let contents = "";
643
- if (hasSideEffect) contents += `import '${packageName}';`;
644
- if (isNamespace) contents += `\nexport * from '${packageName}';`;
645
- if (isDefaultImport) contents += `\nexport { default${newDefaultName ? ` as ${newDefaultName}` : ""} } from '${packageName}';`;
646
- if (named.size > 0 && !isNamespace) contents += `\nexport { ${Array.from(named).join(", ")} } from '${packageName}';`;
647
- return contents;
648
1703
  }
649
- function getChunkName(packageName, chunks) {
650
- for (const [chunkName, packages] of Object.entries(chunks)) if (matchesPackage(packageName, packages)) return chunkName;
1704
+ /**
1705
+ * Exported name the name asked of the source, read off one `export ... from` statement.
1706
+ *
1707
+ * An entry is absent when the whole namespace is taken, which `export * as ns from "x"` does and a brace list never does.
1708
+ */
1709
+ function reexportedNames(statement) {
1710
+ const renames = /* @__PURE__ */ new Map();
1711
+ const braces = /\{([^}]*)\}/.exec(statement);
1712
+ if (!braces) return renames;
1713
+ const parts = braces[1].split(",");
1714
+ for (const part of parts) {
1715
+ const [imported, exported] = part.split(/\s+as\s+/).map((side) => side.trim().replaceAll(/^["']|["']$/g, ""));
1716
+ if (imported) renames.set(exported || imported, imported);
1717
+ }
1718
+ return renames;
1719
+ }
1720
+ /** The names an import statement binds, or `undefined` for `import * as`, which binds no individual names. */
1721
+ function namesOfImport(statement) {
1722
+ if (/import\s+\*\s+as/.test(statement)) return;
1723
+ const names = [];
1724
+ if (/^import\s+(?!\{)[A-Za-z_$][\w$]*/.test(statement)) names.push("default");
1725
+ const braces = /\{([^}]*)\}/.exec(statement);
1726
+ if (!braces) {
1727
+ if (names.length === 0) return;
1728
+ return names;
1729
+ }
1730
+ const parts = braces[1].split(",");
1731
+ for (const part of parts) {
1732
+ const name = part.trim().split(/\s+as\s+/, 1)[0].trim();
1733
+ if (name) names.push(name);
1734
+ }
1735
+ return names;
651
1736
  }
652
- function matchesPackage(packageName, packages) {
653
- for (const pattern of packages) {
654
- if (pattern === packageName) return true;
655
- if (pattern.endsWith("/*")) {
656
- const base = pattern.slice(0, -2);
657
- if (packageName === base || packageName.startsWith(base + "/")) return true;
1737
+
1738
+ //#endregion
1739
+ //#region src/plugins/bundle-packages/index/vendor-index.ts
1740
+ /**
1741
+ * Lexes the part of `node_modules` the project uses. No bundler runs. It answers:
1742
+ *
1743
+ * - Which files a specifier pulls in, narrowed to the names asked for.
1744
+ * - Which names a specifier exports, following `export *` and CommonJS re-export chains.
1745
+ * - Which of its imports go to another package, with their names, and which of those are `require` calls.
1746
+ */
1747
+ var VendorIndex = class VendorIndex {
1748
+ root;
1749
+ #resolver;
1750
+ #resolveCache = /* @__PURE__ */ new Map();
1751
+ #modules = /* @__PURE__ */ new Map();
1752
+ #closures = /* @__PURE__ */ new Map();
1753
+ #isProduction;
1754
+ constructor(root, isProduction) {
1755
+ this.root = root;
1756
+ this.#isProduction = isProduction;
1757
+ this.#resolver = new ResolverFactory({
1758
+ conditionNames: [
1759
+ "browser",
1760
+ "import",
1761
+ "module",
1762
+ "default",
1763
+ isProduction ? "production" : "development"
1764
+ ],
1765
+ mainFields: [
1766
+ "browser",
1767
+ "module",
1768
+ "main"
1769
+ ],
1770
+ extensions: [
1771
+ ".js",
1772
+ ".mjs",
1773
+ ".cjs",
1774
+ ".json"
1775
+ ],
1776
+ symlinks: false
1777
+ });
1778
+ }
1779
+ static async create(root, isProduction) {
1780
+ await init$1;
1781
+ await init();
1782
+ return new VendorIndex(root, isProduction);
1783
+ }
1784
+ /** Resolves a specifier from `fromFile`. Handles relative paths, bare package names and `#subpath` imports. */
1785
+ resolveFrom(specifier, fromFile) {
1786
+ const directory = dirname(fromFile);
1787
+ const key = directory + "\0" + specifier;
1788
+ if (this.#resolveCache.has(key)) return this.#resolveCache.get(key);
1789
+ let resolved;
1790
+ try {
1791
+ resolved = this.#resolver.sync(directory, specifier).path ?? void 0;
1792
+ } catch {
1793
+ resolved = void 0;
1794
+ }
1795
+ this.#resolveCache.set(key, resolved);
1796
+ return resolved;
1797
+ }
1798
+ /** Lexes one file. Re-reads it only when its mtime changed. */
1799
+ lex(file) {
1800
+ const mtimeMs = statSync(file).mtimeMs;
1801
+ const cached = this.#modules.get(file);
1802
+ if (cached && cached.mtimeMs === mtimeMs) return cached;
1803
+ const code = readFileSync(file, "utf8");
1804
+ const record = lexModule(file, code, mtimeMs, this.#isProduction);
1805
+ this.#modules.set(file, record);
1806
+ return record;
1807
+ }
1808
+ /**
1809
+ * Whether the module a specifier resolves to has a default export.
1810
+ *
1811
+ * `export *` does not carry a default, so a package taken whole needs its default re-exported by name or it comes out empty.
1812
+ */
1813
+ hasDefaultExport(specifier, fromFile = join(this.root, "package.json")) {
1814
+ const entry = this.resolveFrom(specifier, fromFile);
1815
+ if (!entry) return false;
1816
+ const record = this.lex(entry);
1817
+ return record.ownExports.includes("default") || record.reexportOf.has("default");
1818
+ }
1819
+ /** The format of the file a specifier resolves to. */
1820
+ formatOf(specifier, fromFile = join(this.root, "package.json")) {
1821
+ const entry = this.resolveFrom(specifier, fromFile);
1822
+ if (!entry) return;
1823
+ return this.lex(entry).format;
1824
+ }
1825
+ /**
1826
+ * Every name a file exports, following `export *` and CommonJS re-export chains.
1827
+ *
1828
+ * Use this for CommonJS only. In an ES module `export type { X }` and `export { X }` look identical to the lexer, so the list
1829
+ * would include type names, and re-exporting one is a build error.
1830
+ */
1831
+ exportsOf(file, seen = /* @__PURE__ */ new Set()) {
1832
+ if (seen.has(file)) return [];
1833
+ seen.add(file);
1834
+ const record = this.lex(file);
1835
+ const names = new Set(record.ownExports.filter((name) => name !== "__esModule"));
1836
+ for (const [name, reexport] of record.reexportOf) {
1837
+ if (!isPathReexport(name, reexport)) {
1838
+ names.add(name);
1839
+ continue;
1840
+ }
1841
+ const target = this.resolveFrom(reexport.source, file);
1842
+ if (!target) continue;
1843
+ for (const inner of this.exportsOf(target, seen)) names.add(inner);
658
1844
  }
659
- if (pattern.endsWith("/**")) {
660
- const base = pattern.slice(0, -2);
661
- if (packageName === base || packageName.startsWith(base + "/")) return true;
1845
+ for (const source of record.starSources) {
1846
+ const target = this.resolveFrom(source, file);
1847
+ if (!target) continue;
1848
+ for (const inner of this.exportsOf(target, seen)) names.add(inner);
662
1849
  }
1850
+ names.delete("default");
1851
+ return [...names];
663
1852
  }
664
- return false;
665
- }
666
- /** Join the package name with its entry point. Example: `join("react-dom", "./client") // => "react-dom/client` */
667
- function packageJoin(name, subPackage) {
668
- return nodePath.join(name, subPackage).replace(/\\/g, "/").replace(/\/$/, "");
669
- }
670
- function getNodeModuleDirectory(packageName, root) {
671
- try {
672
- const entryPoint = createRequire(root.endsWith(nodePath.sep) ? root : root + nodePath.sep).resolve(packageName);
673
- const parts = entryPoint.split(nodePath.sep);
674
- const index = parts.lastIndexOf("node_modules");
675
- if (index === -1) {
676
- console.error(`node_modules not found in resolved path for "${packageName}": ${entryPoint}`);
677
- return null;
1853
+ /**
1854
+ * Walks everything `specifier` needs to provide `names`, and collects the imports that leave its package.
1855
+ *
1856
+ * Asking for every name walks the whole package. Naming them follows only the re-export branches that lead to those names,
1857
+ * which is the difference between reading 15 files of `lodash-es` and reading all 600.
1858
+ */
1859
+ closureOf(specifier, names, fromFile = join(this.root, "package.json")) {
1860
+ const cacheKey = fromFile + "\0" + specifier + "\0" + names.key();
1861
+ if (this.#closures.has(cacheKey)) return this.#closures.get(cacheKey);
1862
+ const closure = this.#walkClosure(specifier, names, fromFile);
1863
+ this.#closures.set(cacheKey, closure);
1864
+ return closure;
1865
+ }
1866
+ #walkClosure(specifier, names, fromFile) {
1867
+ const entry = this.resolveFrom(specifier, fromFile);
1868
+ if (!entry) return;
1869
+ const visited = /* @__PURE__ */ new Set();
1870
+ const crossPackage = /* @__PURE__ */ new Map();
1871
+ const queue = [{
1872
+ file: entry,
1873
+ wanted: names
1874
+ }];
1875
+ while (queue.length > 0) {
1876
+ const { file, wanted } = queue.shift();
1877
+ const key = file + "\0" + wanted.key();
1878
+ if (visited.has(key)) continue;
1879
+ visited.add(key);
1880
+ const record = this.lex(file);
1881
+ for (const [source, wantedNames] of sourcesForNames(record, wanted)) {
1882
+ const target = this.resolveFrom(source, file);
1883
+ if (!target) continue;
1884
+ if (isBareSpecifier(source)) mergeEdge(crossPackage, {
1885
+ specifier: source,
1886
+ kind: "import",
1887
+ names: wantedNames,
1888
+ entry: target,
1889
+ isDynamic: false,
1890
+ isStar: record.starSources.includes(source)
1891
+ });
1892
+ const forward = wantedNames.isAll ? NameSet.all() : NameSet.of([...wantedNames.list(), "default"]);
1893
+ queue.push({
1894
+ file: target,
1895
+ wanted: forward
1896
+ });
1897
+ }
1898
+ for (const edge of record.edges) {
1899
+ const target = this.resolveFrom(edge.specifier, file);
1900
+ if (!target) continue;
1901
+ if (isBareSpecifier(edge.specifier)) {
1902
+ const names = edge.names ? NameSet.of(edge.names) : NameSet.all();
1903
+ mergeEdge(crossPackage, {
1904
+ specifier: edge.specifier,
1905
+ kind: edge.kind,
1906
+ names,
1907
+ entry: target,
1908
+ isDynamic: edge.isDynamic,
1909
+ isStar: false
1910
+ });
1911
+ }
1912
+ queue.push({
1913
+ file: target,
1914
+ wanted: NameSet.all()
1915
+ });
1916
+ }
678
1917
  }
679
- return parts.slice(0, index + 1).join(nodePath.sep);
680
- } catch (error) {
681
- console.error(`Failed to resolve package directory for "${packageName}":`, error);
682
- return null;
1918
+ return {
1919
+ entry,
1920
+ crossPackage: crossPackage.values().toArray()
1921
+ };
1922
+ }
1923
+ };
1924
+ /** The `export ... from "x"` branches that can lead to a wanted name → the names to look for in each. */
1925
+ function sourcesForNames(record, wanted) {
1926
+ const sources = /* @__PURE__ */ new Map();
1927
+ for (const [name, reexport] of record.reexportOf) {
1928
+ if (!wanted.has(name) && !isPathReexport(name, reexport)) continue;
1929
+ const asked = reexport.importedName === void 0 ? NameSet.all() : NameSet.of([reexport.importedName]);
1930
+ didWidenEntry(sources, reexport.source, asked);
683
1931
  }
1932
+ for (const source of record.starSources) sources.set(source, wanted);
1933
+ return sources;
684
1934
  }
685
- function getCustomPackagePath(root, importPath, isProduction, resolvePackage) {
686
- const environment = isProduction ? "production" : "development";
687
- for (const [packageName, exports] of Object.entries(resolvePackage)) for (const [entry, stringOrObject] of Object.entries(exports)) {
688
- if (packageJoin(packageName, entry) !== importPath) continue;
689
- const relativePath = typeof stringOrObject === "string" ? stringOrObject : stringOrObject[environment];
690
- if (!relativePath) continue;
691
- const nodeModulesDirectory = getNodeModuleDirectory(packageName, root);
692
- if (!nodeModulesDirectory) {
693
- console.error(`Skipping "${packageName}" — could not determine node_modules directory`);
694
- continue;
695
- }
696
- return join(nodeModulesDirectory, packageName, relativePath);
1935
+ /**
1936
+ * Whether a re-export names a whole module rather than one of its exports.
1937
+ *
1938
+ * `module.exports = require("./cjs/react.production.js")` is reported by the CommonJS lexer as re-exporting the path itself, so
1939
+ * the names live in the target and this branch is always followed.
1940
+ */
1941
+ function isPathReexport(name, reexport) {
1942
+ return name === reexport.source && reexport.source.startsWith(".");
1943
+ }
1944
+ /** Merges an edge into the ones already found for the same specifier. */
1945
+ function mergeEdge(edges, found) {
1946
+ const existing = edges.get(found.specifier);
1947
+ if (!existing) {
1948
+ edges.set(found.specifier, {
1949
+ specifier: found.specifier,
1950
+ kind: found.kind,
1951
+ names: found.names.clone(),
1952
+ entries: /* @__PURE__ */ new Set([found.entry]),
1953
+ isLazyOnly: found.isDynamic,
1954
+ isStar: found.isStar
1955
+ });
1956
+ return;
697
1957
  }
1958
+ existing.entries.add(found.entry);
1959
+ if (!found.isDynamic) existing.isLazyOnly = false;
1960
+ if (found.isStar) existing.isStar = true;
1961
+ if (found.kind === "require") existing.kind = "require";
1962
+ existing.names.widen(found.names);
698
1963
  }
699
- function isNodeModulesPath(path, root) {
700
- return isPathMatch(path, {
701
- include: ["**/node_modules/**"],
702
- ignore: [],
703
- root
704
- });
1964
+ /**
1965
+ * Whether a specifier can land in a different output file than the one importing it.
1966
+ *
1967
+ * Only bare specifiers can; `./x` and `#internal` resolve inside their own package and travel with it. Same package is not the
1968
+ * same file though: `react/jsx-runtime` is a subpath with a file of its own, and its development build requires `react` back.
1969
+ */
1970
+ function isBareSpecifier(specifier) {
1971
+ return !specifier.startsWith(".") && !specifier.startsWith("#") && !specifier.startsWith("/");
705
1972
  }
706
- /** Directory of the package a `node_modules` path belongs to, `undefined` for paths outside `node_modules`. */
707
- function packageDirectory(path) {
708
- return /^(.*?node_modules\/(?:@[^/]+\/)?[^/]+)(?:\/|$)/.exec(path)?.[1];
1973
+
1974
+ //#endregion
1975
+ //#region src/plugins/bundle-packages/shared/report-problems.ts
1976
+ /** `a, b` becomes `"a", "b"`. */
1977
+ function quoted(names, separator = ", ") {
1978
+ return names.map((name) => `"${name}"`).join(separator);
709
1979
  }
710
1980
  /**
711
- * Returns a stable key for a specifier, ignoring publicName and function refs. Side-effects are keyed by type alone (only one per
712
- * source).
1981
+ * Reports a `chunks` grouping that split packages joined by a `require` or an `export *`.
1982
+ *
1983
+ * Grouping is automatic, so only a hand-written entry can cause this. One message per group, since the whole group has to move
1984
+ * together anyway.
713
1985
  */
714
- function specifierKey(s) {
715
- switch (s.type) {
716
- case "default": return `default::${s.name}`;
717
- case "namespace": return `namespace::${s.name}`;
718
- case "named": return `named::${s.name}`;
719
- case "unknown":
720
- case "side-effect": return `side-effect`;
721
- }
1986
+ function reportLayoutProblems(units, groups, report) {
1987
+ for (const problem of validateUnits(units, groups)) {
1988
+ const chunkName = problem.units.find((name) => !problem.members.includes(name)) ?? problem.group;
1989
+ report(`These packages cannot be in separate files, but they were spread over ${quoted(problem.units)}. A "require" between them throws when the bundle runs, and an "export *" between them loses the names it forwards with no error at all.\nPut all of them in one chunk:\n bundlePackagesPlugin({ chunks: { "${chunkName}": [${quoted(problem.members)}] } })`);
1990
+ }
1991
+ }
1992
+ /** Warns about anything in `chunks` that could not be honoured as written. */
1993
+ function reportChunkPlan(plan, warn) {
1994
+ for (const { chunk, pattern, isExclusion } of plan.unmatched) warn(`The chunk "${chunk}" ${isExclusion ? "excludes" : "asks for"} "${pattern}", which no package in this build answers to. Check the spelling, or that the package is installed and something imports it.`);
1995
+ for (const clash of plan.clashes) warn(`The chunk "${clash.name}" is named after a package this build uses, so that package's own file is now the chunk and carries ${clash.otherMembers} other package(s) with it. Every page importing "${clash.name}" downloads all of them. Give the chunk a name no package has.`);
1996
+ for (const { chunk, specifiers } of plan.pulled) warn(`${quoted(specifiers)} would each have had a file of their own, since this project imports them directly, but "${chunk}" holds packages they cannot be separated from and took them too. A page importing only one of them now downloads the whole chunk. Drop the chunk to get separate files back.`);
1997
+ for (const refused of plan.refused) warn(`"${refused.chunk}" excludes "${refused.specifier}", but "${refused.requiredBy}" requires it at runtime and the two cannot be in separate files. It was kept in the chunk.`);
1998
+ if (plan.duplicated.length === 0) return;
1999
+ const names = quoted(plan.duplicated.map((duplicate) => duplicate.specifier));
2000
+ warn(`The chunks ${quoted([...new Set(plan.duplicated.flatMap((duplicate) => duplicate.chunks))], " and ")} both pull in ${names}, so each is built into both files. Every import of them points at one of the two, which leaves the other carrying packages nothing loads. Name an overlapping package in one chunk only.`);
2001
+ }
2002
+ /**
2003
+ * One output file cannot export a name twice, so a page importing it from the losing package silently reads the other's value.
2004
+ *
2005
+ * Only a `chunks` entry can put two directly-imported packages in one file, so naming the chunk is the way out.
2006
+ */
2007
+ function nameClashMessage(clash) {
2008
+ return `"${clash.keptBy}" and "${clash.lostBy}" both export "${clash.name}" and share the output file "${clash.unit}", so only one of them can publish it. "${clash.keptBy}" keeps it, and every page importing "${clash.name}" from "${clash.lostBy}" silently reads "${clash.keptBy}"'s value instead. Put them in separate chunks.`;
722
2009
  }
723
2010
  /**
724
- * Diffs two specifier arrays and returns only the entries from `collected` that are not already present in `existing`.
2011
+ * Warns about a package `node_modules` holds more than one copy of.
725
2012
  *
726
- * @param existing - The full current set of specifiers.
727
- * @param collected - Newly gathered specifiers to screen for novelty.
728
- * @returns Specifiers in `collected` that are absent from `existing`.
2013
+ * One output file per package name means only one copy ships. Only a deduped install or a version override can fix that.
729
2014
  */
730
- function diffSpecifiers(existing, collected) {
731
- const existingKeys = new Set(existing.map((element) => specifierKey(element)));
732
- return collected.filter((s) => !existingKeys.has(specifierKey(s)));
2015
+ function reportVersionConflicts(root, units, discovered, index, warn) {
2016
+ for (const conflict of findVersionConflicts(units, discovered, index)) {
2017
+ const copies = conflict.entries.map((entry) => ` ${relative(root, entry)}`).join("\n");
2018
+ warn(`"${conflict.specifier}" resolves to more than one copy, and only one of them can be bundled:\n${copies}\nWhichever importer wanted the other copy will get this one instead. Dedupe the install to be sure which.`);
2019
+ }
733
2020
  }
734
2021
 
735
2022
  //#endregion
736
2023
  //#region src/plugins/bundle-packages/bundle-packages-plugin.ts
737
- function normalizePackageName(packageName) {
738
- return "default_" + packageName.replace(/@|-|\/|\./g, "_");
739
- }
2024
+ const JS_EXTENSIONS = /* @__PURE__ */ new Set([
2025
+ ".js",
2026
+ ".mjs",
2027
+ ".cjs",
2028
+ ".jsx",
2029
+ ".ts",
2030
+ ".mts",
2031
+ ".cts",
2032
+ ".tsx"
2033
+ ]);
740
2034
  /**
741
- * Bundles every "node_modules" package imported anywhere in the project into its own file under "packagesDir" and rewrites the
742
- * imports to point at it. Specifiers are collected across the whole codebase first, so esbuild can tree-shake each package down
743
- * to what is actually used.
2035
+ * Bundles every `node_modules` package imported anywhere in the project into a file under `packagesDir`, and rewrites the imports
2036
+ * to point at it.
744
2037
  *
745
2038
  * Notes:
746
2039
  *
747
- * - Runs in both modes; minification and tree-shaking are production-only.
748
- * - Bare imports in scripts and "<script src>" tags resolving into "node_modules" are both redirected; a script tag is counted as a
749
- * side-effect import.
750
- * - A "<link rel=stylesheet>" shipped by a package is moved next to the package output, keeping its path inside the package. Other
751
- * file types stay where they resolve, under "node_modules".
752
- * - Dots in the output path become underscores ("highlight.js" becomes "highlight_js"), so a package cannot collide with the file
753
- * bundled from it.
754
- * - "chunks" merges packages into one file and renames their default imports to "default_<package>" so they share a module scope,
755
- * which is what CommonJS packages need to work in the browser.
756
- * - A package first discovered while bundling another one is bundled right away, then re-bundled once its specifiers are known.
2040
+ * - Minification and tree-shaking are production-only.
2041
+ * - `<script src>` into `node_modules` is redirected too, and counts as a side-effect import.
2042
+ * - A stylesheet shipped by a package, linked from HTML or `@import`ed from CSS, is copied next to the package output, keeping its
2043
+ * path inside the package.
2044
+ * - Dots in output paths become underscores (`highlight.js` → `highlight_js`) so a package cannot collide with its own bundle.
2045
+ * - Packages that `require` each other are bundled together.
757
2046
  */
758
2047
  function bundlePackagesPlugin(options = {}) {
759
- const packagesOutputDirection = options.packagesDir ?? "./packages";
2048
+ const packagesDirectory = options.packagesDir ?? "./packages";
760
2049
  const resolvePackage = options.resolvePackage ?? {};
761
2050
  const chunks = options.chunks ?? {};
762
- /** Maps package names to a list of collected import specifiers (named, default, side-effect, etc.) from various source files. */
2051
+ /** Package specifier specifiers imported from it. */
763
2052
  const specifiersByPackage = /* @__PURE__ */ new Map();
764
- /** Maps the absolute path of a package entry point to its original source name */
2053
+ /** Resolved `node_modules` path package specifier. */
765
2054
  const pathPackageMap = /* @__PURE__ */ new Map();
766
- /** Maps a chunk name to the entry contents it was last bundled from, to tell a stale chunk from an up to date one. */
767
- const chunkContents = /* @__PURE__ */ new Map();
768
- const JS_EXTENSIONS = /* @__PURE__ */ new Set([
769
- ".js",
770
- ".mjs",
771
- ".cjs",
772
- ".jsx",
773
- ".ts",
774
- ".mts",
775
- ".cts",
776
- ".tsx"
777
- ]);
2055
+ /** Output file entry it was last built from. */
2056
+ const builtEntries = /* @__PURE__ */ new Map();
2057
+ /** Every package import found so far. */
2058
+ const allReferences = [];
2059
+ /** Warnings already printed. */
2060
+ const reported = /* @__PURE__ */ new Set();
778
2061
  const printFormattedError = PrintFormattedError.create({ function: bundlePackagesPlugin });
2062
+ let index;
779
2063
  return {
780
2064
  name: "bundle-packages",
2065
+ async setup() {
2066
+ index = await VendorIndex.create(this.root, this.production);
2067
+ },
781
2068
  resolveSource(source, filePath) {
782
2069
  if (!isValidRelativePath(source)) return;
783
2070
  const [link] = splitHtmlLink(source);
784
2071
  const target = join(dirname(filePath), link);
785
- if (isNodeModulesPath(target, this.root) && packageDirectory(target) !== packageDirectory(filePath)) return {
2072
+ if (isNodeModulesPath(target) && packageDirectory(target) !== packageDirectory(filePath)) return {
786
2073
  source,
787
2074
  dependencyID: void 0
788
2075
  };
789
2076
  const resolveResult = this.resolver.resolve(source, filePath);
790
2077
  if (!resolveResult) return;
791
2078
  if (!isAbsolute(resolveResult.path)) return;
792
- if (!resolveResult.isPackage) return;
793
- if (!JS_EXTENSIONS.has(extname(resolveResult.path))) return;
2079
+ if (!resolveResult.isPackage || !JS_EXTENSIONS.has(extname(resolveResult.path))) return;
794
2080
  if (!resolveResult.path.startsWith(this.root)) this.log.warn(`Resolved package "${resolveResult.path}" outside of the root directory.`);
795
2081
  pathPackageMap.set(resolveResult.path, source);
796
2082
  const absFileDirectory = join(this.root, dirname(filePath));
@@ -800,272 +2086,49 @@ function bundlePackagesPlugin(options = {}) {
800
2086
  };
801
2087
  },
802
2088
  async postTransform() {
803
- const root = this.root;
804
- for (const inputMetadata of this.metadataList) {
805
- for (const { metadata } of filterScriptMetadata(inputMetadata)) {
806
- const sources = collectSpecifiersFromAst(metadata.ast);
807
- for (const sourceReference of sources) {
808
- const source = sourceReference.source;
809
- const sourceRootRelative = join(dirname(metadata.filePath), source);
810
- if (!isNodeModulesPath(sourceRootRelative, root)) continue;
811
- const packageName = pathPackageMap.get(join(root, sourceRootRelative));
812
- if (!packageName) {
813
- printFormattedError(`Could not resolve package name for path "${sourceRootRelative}"\nimported from "${metadata.filePath}").\nSkipping specifier collection for this import.`);
814
- continue;
815
- }
816
- const chunkName = getChunkName(packageName, chunks);
817
- const packageOutputPath = calcPackageOutPath(chunkName ?? packageName, packagesOutputDirection);
818
- sourceReference.source = relative(dirname(metadata.filePath), packageOutputPath);
819
- const existingSpecifiers = specifiersByPackage.get(packageName) ?? [];
820
- specifiersByPackage.set(packageName, existingSpecifiers);
821
- const specifiers = sourceReference.specifiers;
822
- if (chunkName) {
823
- const normalizedPackageName = normalizePackageName(packageName);
824
- for (const specifier of specifiers) {
825
- if (specifier.type === "default") {
826
- specifier.setDefaultName(normalizedPackageName);
827
- specifier.newDefaultName = normalizedPackageName;
828
- }
829
- existingSpecifiers.push(specifier);
830
- }
831
- continue;
832
- }
833
- existingSpecifiers.push(...specifiers);
834
- }
835
- }
836
- if (isHtmlMetadata(inputMetadata)) {
837
- const scriptTags = inputMetadata.ast.querySelectorAll("script[src]");
838
- for (const scriptTag of scriptTags) {
839
- const source = scriptTag.getAttribute("src");
840
- if (!source) continue;
841
- const scriptType = scriptTag.getAttribute("type");
842
- if (!isScriptType(scriptType)) continue;
843
- const sourceRootRelative = join(dirname(inputMetadata.filePath), source);
844
- if (!isNodeModulesPath(sourceRootRelative, root)) continue;
845
- const packageName = pathPackageMap.get(join(root, sourceRootRelative));
846
- if (!packageName) {
847
- printFormattedError(`Could not resolve package name for script src "${source}" (in "${inputMetadata.filePath}"). The script tag will not be included in the bundled packages.`);
848
- continue;
849
- }
850
- const packageOutputPath = calcPackageOutPath(packageName, packagesOutputDirection);
851
- const newSource = relative(dirname(inputMetadata.filePath), packageOutputPath);
852
- scriptTag.setAttribute("src", newSource);
853
- const existingSpecifiers = specifiersByPackage.get(packageName) ?? [];
854
- existingSpecifiers.push({ type: "side-effect" });
855
- specifiersByPackage.set(packageName, existingSpecifiers);
856
- }
857
- const styleLinks = inputMetadata.ast.querySelectorAll("link[rel=\"stylesheet\"], link[rel=\"preload\"][as=\"style\"]");
858
- for (const styleLink of styleLinks) {
859
- const source = styleLink.getAttribute("href");
860
- if (!source) continue;
861
- const sourceRootRelative = join(dirname(inputMetadata.filePath), source);
862
- if (!isNodeModulesPath(sourceRootRelative, root)) continue;
863
- const assetOutputPath = calcPackageAssetOutPath(sourceRootRelative, packagesOutputDirection);
864
- const assetPath = join(root, sourceRootRelative);
865
- const asset = this.metadataList.find((metadata) => join(root, metadata.id) === assetPath);
866
- if (asset && asset.filePath !== assetOutputPath) {
867
- await this.rebase(asset, join(root, assetOutputPath));
868
- asset.filePath = assetOutputPath;
869
- }
870
- styleLink.setAttribute("href", relative(dirname(inputMetadata.filePath), assetOutputPath));
871
- }
872
- }
873
- }
874
- /**
875
- * Tracks packages that need to be re-bundled. During bundling, we might detect that a package imports another package whose
876
- * specifiers we have already collected. This means we need to recollect the specifiers from the discovered package and
877
- * re-bundle the current package.
878
- */
879
- const pendingReBundles = [];
880
- for (const [chunkName, packages] of Object.entries(chunks)) {
881
- if (packages.length === 0) continue;
882
- let contents = "";
883
- for (const [packageName, specifiers] of specifiersByPackage) {
884
- if (specifiers.length === 0) continue;
885
- if (!matchesPackage(packageName, packages)) continue;
886
- contents += generateStdinString(specifiers, packageName);
887
- }
888
- if (!contents) continue;
889
- const bundledChunk = this.findMetadata({
890
- type: METADATA_TYPES.Package,
891
- id: chunkName
892
- });
893
- if (bundledChunk && chunkContents.get(chunkName) === contents) continue;
894
- const packageOutputPath = calcPackageOutPath(chunkName, packagesOutputDirection);
895
- const packageMetadata = isPackageMetadata(bundledChunk) ? bundledChunk : {
896
- type: METADATA_TYPES.Package,
897
- packageName: chunkName,
898
- code: "",
899
- filePath: packageOutputPath,
900
- id: chunkName,
901
- directDependencies: /* @__PURE__ */ new Set()
902
- };
903
- packageMetadata.directDependencies.clear();
904
- const [bundledCode, bundleError] = await bundlePackage({
905
- contents,
906
- resolveDir: this.root,
907
- outfile: packageOutputPath,
908
- packageName: chunkName,
909
- minify: this.production,
910
- production: this.production,
911
- treeShaking: this.production,
912
- onPackageResolve: (importedPackageName) => {
913
- const packageChunkName = getChunkName(importedPackageName, chunks);
914
- if (packageChunkName === chunkName) {
915
- const customPath = getCustomPackagePath(this.root, importedPackageName, this.production, resolvePackage);
916
- if (customPath) return {
917
- path: customPath,
918
- external: false
919
- };
920
- return;
921
- }
922
- const dependencyOutputPath = calcPackageOutPath(packageChunkName ?? importedPackageName, packagesOutputDirection);
923
- packageMetadata.directDependencies.add(dependencyOutputPath);
924
- pendingReBundles.push({
925
- dependencyName: importedPackageName,
926
- dependencyOutputPath,
927
- importerName: chunkName,
928
- importerOutputPath: packageOutputPath
929
- });
930
- if (!specifiersByPackage.has(importedPackageName)) specifiersByPackage.set(importedPackageName, [{ type: "unknown" }]);
931
- return {
932
- path: relative(dirname(packageOutputPath), dependencyOutputPath),
933
- external: true
934
- };
935
- }
936
- });
937
- if (bundleError) {
938
- printFormattedError(`Failed to bundle package "${chunkName}"`, bundleError);
939
- continue;
940
- }
941
- packageMetadata.code = bundledCode;
942
- chunkContents.set(chunkName, contents);
943
- if (!bundledChunk) this.addMetadata(packageMetadata);
944
- }
945
- for (const [packageName, specifiers] of specifiersByPackage) {
946
- if (specifiers.length === 0) continue;
947
- if (this.findMetadata({
948
- type: METADATA_TYPES.Package,
949
- id: packageName
950
- })) continue;
951
- if (getChunkName(packageName, chunks)) continue;
952
- const packageOutputPath = calcPackageOutPath(packageName, packagesOutputDirection);
953
- const packageMetadata = {
954
- type: METADATA_TYPES.Package,
955
- packageName,
956
- code: "",
957
- filePath: packageOutputPath,
958
- id: packageName,
959
- directDependencies: /* @__PURE__ */ new Set()
960
- };
961
- let isLoadAsIs = !this.production;
962
- for (const [index, specifier] of Array.from(specifiers).entries()) {
963
- if (specifier.type !== "unknown") continue;
964
- isLoadAsIs = true;
965
- specifiers.splice(index, 1);
966
- }
967
- const contents = isLoadAsIs ? void 0 : generateStdinString(specifiers, packageName);
968
- const [bundledCode, bundleError] = await bundlePackage({
969
- contents,
970
- entryPoint: isLoadAsIs ? packageName : void 0,
971
- resolveDir: this.root,
972
- outfile: packageOutputPath,
973
- packageName,
974
- minify: this.production,
975
- production: this.production,
976
- treeShaking: this.production,
977
- onPackageResolve: (importedPackageName) => {
978
- const packageOrChunkName = getChunkName(importedPackageName, chunks) ?? importedPackageName;
979
- const dependencyOutputPath = calcPackageOutPath(packageOrChunkName, packagesOutputDirection);
980
- packageMetadata.directDependencies.add(dependencyOutputPath);
981
- pendingReBundles.push({
982
- dependencyName: importedPackageName,
983
- dependencyOutputPath,
984
- importerName: packageName,
985
- importerOutputPath: packageOutputPath
986
- });
987
- if (!specifiersByPackage.has(importedPackageName)) specifiersByPackage.set(importedPackageName, [{ type: "unknown" }]);
988
- return {
989
- path: relative(dirname(packageOutputPath), dependencyOutputPath),
990
- external: true
991
- };
992
- }
993
- });
994
- if (bundleError) {
995
- printFormattedError(`Failed to bundle package "${packageName}"`, bundleError);
996
- continue;
997
- }
998
- packageMetadata.code = bundledCode;
999
- this.addMetadata(packageMetadata);
1000
- }
1001
- if (!this.production) return;
1002
- for (const { dependencyOutputPath, dependencyName, importerName, importerOutputPath } of pendingReBundles) {
1003
- const packageToBundle = this.metadataList.find((m) => m.filePath === dependencyOutputPath);
1004
- if (!isPackageMetadata(packageToBundle)) {
1005
- printFormattedError(`Re-bundle failed: could not find bundled asset for "${dependencyOutputPath}".`, `The package "${dependencyName}" is not bundled.`);
1006
- continue;
1007
- }
1008
- const importerBundle = this.metadataList.find((m) => m.filePath === importerOutputPath);
1009
- if (!isPackageMetadata(importerBundle)) {
1010
- printFormattedError(`Re-bundle failed: could not find bundled asset for "${importerOutputPath}".`, `The package "${importerName}" is not bundled.`);
1011
- continue;
1012
- }
1013
- const importerScript = await this.load(importerBundle.filePath, {
1014
- code: importerBundle.code,
1015
- type: "js"
1016
- });
1017
- if (!isScriptMetadata(importerScript)) {
1018
- printFormattedError(`Re-bundle failed: could not parse asset "${importerOutputPath}" as a script.`, `The package "${importerName}" may be missing specifiers.`);
1019
- continue;
1020
- }
1021
- const sources = collectSpecifiersFromAst(importerScript.ast);
1022
- const collectedSpecifiers = [];
1023
- for (const sourceReference of sources) {
1024
- const source = sourceReference.source;
1025
- if (join(dirname(importerScript.filePath), source) === dependencyOutputPath) {
1026
- const specifiers = sourceReference.specifiers;
1027
- collectedSpecifiers.push(...specifiers);
1028
- }
1029
- }
1030
- if (collectedSpecifiers.length === 0) {
1031
- printFormattedError(`Re-bundle failed: could not find an import of "${dependencyOutputPath}" inside "${importerOutputPath}".`, `\nThe dependency "${dependencyName}" may be missing specifiers.`);
1032
- continue;
1033
- }
1034
- if (collectedSpecifiers.some((specifier) => specifier.type === "unknown")) {
1035
- this.log.warn(`Cannot tree-shake the package "${dependencyName}" correctly. The package "${importerName}" has dynamic imports with unresolved specifiers.`);
1036
- continue;
1037
- }
1038
- const allSpecifiers = specifiersByPackage.get(dependencyName) ?? [];
1039
- const newSpecifiers = diffSpecifiers(allSpecifiers, collectedSpecifiers);
1040
- if (newSpecifiers.length === 0) continue;
1041
- allSpecifiers.push(...newSpecifiers);
1042
- specifiersByPackage.set(dependencyName, allSpecifiers);
1043
- packageToBundle.directDependencies.clear();
1044
- const contents = generateStdinString(allSpecifiers, dependencyName);
1045
- const [reBundledCode, reBundleError] = await bundlePackage({
1046
- contents,
1047
- resolveDir: this.root,
1048
- outfile: dependencyOutputPath,
1049
- packageName: dependencyName,
1050
- production: this.production,
1051
- minify: true,
1052
- treeShaking: true,
1053
- onPackageResolve: (importedPackageName) => {
1054
- const packageOrChunkName = getChunkName(importedPackageName, chunks) ?? importedPackageName;
1055
- const dependencyOutputPath = calcPackageOutPath(packageOrChunkName, packagesOutputDirection);
1056
- packageToBundle.directDependencies.add(dependencyOutputPath);
1057
- return {
1058
- path: relative(dirname(packageToBundle.filePath), dependencyOutputPath),
1059
- external: true
1060
- };
1061
- }
1062
- });
1063
- if (reBundleError) {
1064
- printFormattedError(`Failed to re-bundle package "${dependencyName}"`, reBundleError);
1065
- continue;
1066
- }
1067
- packageToBundle.code = reBundledCode;
1068
- }
2089
+ if (!index) return;
2090
+ const collectContext = {
2091
+ root: this.root,
2092
+ pathPackageMap,
2093
+ specifiersByPackage,
2094
+ reportUntraced: printFormattedError
2095
+ };
2096
+ const references = await collectReferences.call(this, collectContext, packagesDirectory);
2097
+ allReferences.push(...references);
2098
+ if (specifiersByPackage.size === 0) return;
2099
+ const neededByPackage = /* @__PURE__ */ new Map();
2100
+ const seeds = /* @__PURE__ */ new Map();
2101
+ for (const [specifier, specifiers] of specifiersByPackage) {
2102
+ const needed = neededExports(specifiers);
2103
+ neededByPackage.set(specifier, needed);
2104
+ seeds.set(specifier, needed.names);
2105
+ }
2106
+ const { units, discovered, plan, groups } = planLayout(seeds, chunks, index);
2107
+ const unitOf = unitsBySpecifier(units);
2108
+ const warn = (message) => {
2109
+ if (reported.has(message)) return;
2110
+ reported.add(message);
2111
+ this.log.warn(message);
2112
+ };
2113
+ const fail = (message) => {
2114
+ if (reported.has(message)) return;
2115
+ reported.add(message);
2116
+ printFormattedError(message);
2117
+ };
2118
+ reportLayoutProblems(units, groups, fail);
2119
+ reportChunkPlan(plan, warn);
2120
+ reportVersionConflicts(this.root, units, discovered, index, warn);
2121
+ pointReferencesAtUnits(allReferences, units, unitOf, packagesDirectory);
2122
+ const buildContext = {
2123
+ packagesDirectory,
2124
+ unitOf,
2125
+ neededByPackage,
2126
+ onNameClash: (clash) => warn(nameClashMessage(clash)),
2127
+ resolvePackage,
2128
+ index,
2129
+ builtEntries
2130
+ };
2131
+ for (const unit of units.values()) await buildUnit.call(this, unit, buildContext, printFormattedError);
1069
2132
  }
1070
2133
  };
1071
2134
  }
@@ -4169,25 +5232,28 @@ var StyleBundler = class {
4169
5232
  * Bundles `metadata.ast` in place.
4170
5233
  *
4171
5234
  * @param bundleAbsolutePath Where the bundle ends up, which imported `url()` references are rebased onto.
5235
+ * @param isExternal Marks a resolved import path as external: the `@import` is kept instead of inlined, at any depth.
4172
5236
  */
4173
- async bundle(metadata, bundleAbsolutePath) {
4174
- return await new BundleRun(this.#app, this.#loaded, bundleAbsolutePath).bundle(metadata);
5237
+ async bundle(metadata, bundleAbsolutePath, isExternal) {
5238
+ return await new BundleRun(this.#app, this.#loaded, bundleAbsolutePath, isExternal).bundle(metadata);
4175
5239
  }
4176
5240
  };
4177
5241
  var BundleRun = class {
4178
5242
  #app;
4179
5243
  #loaded;
4180
5244
  #bundleAbsolutePath;
5245
+ #isExternal;
4181
5246
  #entries = [];
4182
5247
  #occurrences = [];
4183
5248
  #diagnostics = [];
4184
5249
  #reportedNodes = /* @__PURE__ */ new Set();
4185
5250
  #charsetParams;
4186
5251
  #hasKeptImports = false;
4187
- constructor(app, loaded, bundleAbsolutePath) {
5252
+ constructor(app, loaded, bundleAbsolutePath, isExternal) {
4188
5253
  this.#app = app;
4189
5254
  this.#loaded = loaded;
4190
5255
  this.#bundleAbsolutePath = bundleAbsolutePath;
5256
+ this.#isExternal = isExternal;
4191
5257
  }
4192
5258
  async bundle(metadata) {
4193
5259
  const absolutePath = join(this.#app.root, metadata.filePath);
@@ -4308,6 +5374,7 @@ var BundleRun = class {
4308
5374
  this.#error(node, filePath, `Failed to resolve "${prelude.url}"`);
4309
5375
  return keep;
4310
5376
  }
5377
+ if (this.#isExternal?.(absolutePath)) return keep;
4311
5378
  if (ancestors.has(absolutePath)) return { action: "drop" };
4312
5379
  const metadata = await this.#load(absolutePath);
4313
5380
  if (!metadata) {
@@ -4472,95 +5539,183 @@ function keptImportPrelude(text, prelude, chain) {
4472
5539
  //#endregion
4473
5540
  //#region src/plugins/html-bundle-style/html-bundle-style-plugin.ts
4474
5541
  /**
4475
- * Inlines the "@import" tree of a stylesheet, so the tag ends up self-contained. Opt-in per tag through the "bundle" attribute
4476
- * (configurable via HtmlBundleStyleOptions) on a "<link rel=stylesheet>" or a "<style>" tag.
5542
+ * Inlines the "@import" tree of a stylesheet, so the tag ends up self-contained. Opt-in per tag through the "bundle" attribute on
5543
+ * a "<link rel=stylesheet>", a "<link rel=preload as=style>" or a "<style>" tag.
5544
+ *
5545
+ * Attributes (names configurable via HtmlBundleStyleOptions):
5546
+ *
5547
+ * - "bundle": bundle this tag.
5548
+ * - "bundle-out": output path for a link tag, resolved from the output directory, not from the HTML file.
5549
+ * - "bundle-externals" / "bundle-externals-exclude": semicolon-separated globs, override the plugin defaults for this tag.
4477
5550
  *
4478
- * Bundling happens in place: a "<style>" tag gets the resolved CSS as its content, and a linked file is rewritten with its
4479
- * imports inlined while "href" stays as it is.
5551
+ * A "<link>" is bundled to "{filename}.bundle.css" next to the original (or to "bundle-out") and "href" is rewritten to it; the
5552
+ * source file is never touched. A "<style>" tag gets the resolved CSS as its content, in place.
4480
5553
  *
4481
5554
  * Notes:
4482
5555
  *
4483
- * - Production only. In development nothing runs and the "bundle" attribute is left on the tag.
4484
- * - "<link rel=preload as=style>" is always bundled, with or without the attribute, since a preloaded stylesheet has to carry
4485
- * everything it needs.
4486
- * - A linked stylesheet is bundled once even when several pages link to it; they share the same file.
5556
+ * - Bundling only runs in production; in dev the attributes are just stripped. They never reach the output either way.
5557
+ * - If the target output path is already bundled, the work is skipped and "href" is simply pointed at it.
4487
5558
  * - Imports resolve the way every other source does: relative paths, path aliases and packages, through the build's resolver.
4488
5559
  * - Imported files are pulled from the build (so transforms other plugins made are included) and only read from disk when they are
4489
- * not part of it. They stay in the output as separate files, now unreferenced. Their "url()" references are rebased onto the
4490
- * bundle's location.
5560
+ * not part of it. They stay in the output as separate files. Their "url()" references are rebased onto the bundle's location.
5561
+ * - An import matching the externals globs is kept as an "@import" (hoisted, conditions folded in, path rebased) and the file it
5562
+ * points at stays in the output.
4491
5563
  * - Conditions ("layer", "supports()", media) wrap the imported rules, a file imported twice under the same conditions is kept
4492
5564
  * where it was imported last, as in a browser, and imports from other origins are hoisted to the top.
5565
+ * - Combining with html-inline-style: register this plugin first, so a tag carrying both attributes bundles first and the inlined
5566
+ * content is the bundle.
4493
5567
  */
4494
5568
  function htmlBundleStylePlugin(options = {}) {
5569
+ const defaultExternals = options.externals ?? [];
5570
+ const defaultExternalsExclude = options.externalsExclude ?? [];
4495
5571
  const bundleAttribute = options.bundleAttribute ?? "bundle";
4496
- const query = `link[rel="stylesheet"][${bundleAttribute}], link[rel="preload"][as="style"], style[${bundleAttribute}]`;
4497
- const alreadyBundledIds = /* @__PURE__ */ new Set();
5572
+ const bundleOutAttribute = options.bundleOutAttribute ?? "bundle-out";
5573
+ const externalsAttribute = options.externalsAttribute ?? "bundle-externals";
5574
+ const externalsExcludeAttribute = options.externalsExcludeAttribute ?? "bundle-externals-exclude";
5575
+ const query = `link[rel="stylesheet"][${bundleAttribute}], link[rel="preload"][as="style"][${bundleAttribute}], style[${bundleAttribute}]`;
5576
+ /** Bundled `<style bundle>` tags by content, rebase target and externals, so a tag repeated through a layout is built once. */
5577
+ const bundledStyleTags = /* @__PURE__ */ new Map();
5578
+ const printFmtError = PrintFormattedError.create({ function: htmlBundleStylePlugin });
5579
+ const report = (diagnostics) => {
5580
+ for (const { level, message, node, filePath } of diagnostics) printFmtError(message, {
5581
+ node,
5582
+ filePath,
5583
+ level
5584
+ });
5585
+ };
4498
5586
  let bundler;
4499
5587
  return {
4500
5588
  name: "html-bundle-style",
4501
- setup() {
5589
+ preTransform() {
5590
+ bundledStyleTags.clear();
4502
5591
  bundler = new StyleBundler(this);
4503
5592
  },
4504
5593
  async transform(metadata) {
4505
- if (!this.production) return;
4506
5594
  if (!isHtmlMetadata(metadata)) return;
5595
+ const filePath = metadata.filePath;
4507
5596
  const elements = metadata.ast.querySelectorAll(query);
4508
5597
  for (const node of elements) {
5598
+ const bundleOutValue = node.getAttribute(bundleOutAttribute);
5599
+ const bundleOut = bundleOutValue ? normalize(bundleOutValue.replace(/^\/+/, "")) : null;
5600
+ const externalsAttributeValue = node.getAttribute(externalsAttribute);
5601
+ const externalsExcludeAttributeValue = node.getAttribute(externalsExcludeAttribute);
4509
5602
  node.removeAttribute(bundleAttribute);
4510
- const isLinkTag = node.tagName.toLowerCase() === "link";
4511
- const styleMetadata = (() => {
4512
- if (isLinkTag) {
4513
- const href = node.getAttribute("href");
4514
- if (!href) return;
4515
- const [link] = splitHtmlLink(href);
4516
- const source = join(dirname(metadata.filePath), link);
4517
- const getStyleMetadata = this.findMetadata({ filePath: source });
4518
- if (!isStyleMetadata(getStyleMetadata)) return;
4519
- return getStyleMetadata;
5603
+ node.removeAttribute(bundleOutAttribute);
5604
+ node.removeAttribute(externalsAttribute);
5605
+ node.removeAttribute(externalsExcludeAttribute);
5606
+ if (!this.production) continue;
5607
+ const externals = parsePatterns(externalsAttributeValue) ?? defaultExternals;
5608
+ const externalsIgnore = parsePatterns(externalsExcludeAttributeValue) ?? defaultExternalsExclude;
5609
+ const isExternal = externals.length === 0 ? void 0 : (absolutePath) => isPathMatch(absolutePath, {
5610
+ include: externals,
5611
+ ignore: externalsIgnore,
5612
+ root: this.root
5613
+ });
5614
+ if (node.tagName.toLowerCase() === "link") {
5615
+ const href = node.getAttribute("href");
5616
+ if (!href) {
5617
+ printFmtError("The link tag does not have an \"href\" attribute", {
5618
+ node,
5619
+ filePath
5620
+ });
5621
+ continue;
4520
5622
  }
4521
- const metadataID = node.getAttribute(CUSTOM_ATTRIBUTES.MetadataID);
4522
- if (!metadataID) return;
4523
- return metadata.stylesMetadataList.get(metadataID);
4524
- })();
4525
- if (!styleMetadata) {
4526
- printFmtError(`Could not find style metadata.`, {
4527
- function: htmlBundleStylePlugin,
5623
+ const [link, suffix] = splitHtmlLink(href);
5624
+ const sourcePath = join(dirname(filePath), link);
5625
+ const outfile = bundleOut ?? replaceExtension(sourcePath, ".bundle.css");
5626
+ if (this.findMetadata({ filePath: outfile })) {
5627
+ this.log.debug(`The stylesheet "${sourcePath}" is already bundled in "${outfile}", skipping...`);
5628
+ node.setAttribute("href", relative(dirname(filePath), outfile) + suffix);
5629
+ continue;
5630
+ }
5631
+ const sourceMetadata = this.findMetadata({ filePath: sourcePath });
5632
+ if (!isStyleMetadata(sourceMetadata)) {
5633
+ printFmtError(`Could not find style metadata for "${href}".`, {
5634
+ node,
5635
+ filePath
5636
+ });
5637
+ continue;
5638
+ }
5639
+ const bundleAbsolutePath = join(this.root, outfile);
5640
+ const clone = this.clone(sourceMetadata);
5641
+ await this.rebase(clone, bundleAbsolutePath);
5642
+ report(await bundler.bundle(clone, bundleAbsolutePath, isExternal));
5643
+ const bundledMetadata = await this.load(outfile, {
5644
+ code: await this.stringify(clone),
5645
+ type: "css"
5646
+ });
5647
+ if (!isStyleMetadata(bundledMetadata)) {
5648
+ printFmtError("Failed to load the bundled stylesheet", {
5649
+ node,
5650
+ filePath
5651
+ });
5652
+ continue;
5653
+ }
5654
+ this.addMetadata(bundledMetadata);
5655
+ node.setAttribute("href", relative(dirname(filePath), outfile) + suffix);
5656
+ continue;
5657
+ }
5658
+ const metadataID = node.getAttribute(CUSTOM_ATTRIBUTES.MetadataID);
5659
+ const styleMetadata = metadataID ? metadata.stylesMetadataList.get(metadataID) : void 0;
5660
+ if (!metadataID || !styleMetadata) {
5661
+ printFmtError("Could not find style metadata.", {
4528
5662
  node,
4529
- filePath: metadata.id
5663
+ filePath
4530
5664
  });
4531
5665
  continue;
4532
5666
  }
4533
- if (isLinkTag) {
4534
- if (alreadyBundledIds.has(styleMetadata.id)) continue;
4535
- alreadyBundledIds.add(styleMetadata.id);
4536
- }
4537
- const bundleAbsolutePath = join(this.root, isLinkTag ? styleMetadata.filePath : metadata.filePath);
4538
- const diagnostics = await bundler.bundle(styleMetadata, bundleAbsolutePath);
4539
- for (const { level, message, node: importNode, filePath } of diagnostics) printFmtError(message, {
4540
- function: htmlBundleStylePlugin,
4541
- node: importNode,
4542
- filePath,
4543
- level
4544
- });
5667
+ const cacheKey = [
5668
+ metadataID,
5669
+ dirname(filePath),
5670
+ externals.join(";"),
5671
+ externalsIgnore.join(";")
5672
+ ].join("\0");
5673
+ const cached = bundledStyleTags.get(cacheKey);
5674
+ if (cached) {
5675
+ metadata.stylesMetadataList.set(metadataID, this.clone(cached));
5676
+ continue;
5677
+ }
5678
+ report(await bundler.bundle(styleMetadata, join(this.root, filePath), isExternal));
5679
+ bundledStyleTags.set(cacheKey, styleMetadata);
4545
5680
  }
4546
5681
  },
4547
5682
  lspHtmlData() {
4548
- const description = `Adds the \`${bundleAttribute}\` attribute, which inlines the stylesheet's \`@import\` tree at build time.`;
4549
- const attributes = [{
5683
+ const description = `Adds the \`${bundleAttribute}\` attribute and its options, which inline the stylesheet's \`@import\` tree at build time.`;
5684
+ const bundle = {
4550
5685
  name: bundleAttribute,
4551
5686
  description: "Enable style bundling.",
4552
5687
  valueSet: "v"
4553
- }];
5688
+ };
5689
+ const externals = {
5690
+ name: externalsAttribute,
5691
+ description: `Glob patterns for imports to keep as external \`@import\`s instead of inlining them, matched against the resolved import path. (semicolon-separated)\n\nDefaults to "${defaultExternals.join(";")}".`
5692
+ };
5693
+ const externalsExclude = {
5694
+ name: externalsExcludeAttribute,
5695
+ description: `Glob patterns that exempt imports from being treated as external, even if they match \`${externalsAttribute}\`. (semicolon-separated)\n\nDefaults to "${defaultExternalsExclude.join(";")}".`
5696
+ };
4554
5697
  return {
4555
5698
  version: 1.1,
4556
5699
  tags: [{
4557
5700
  name: "link",
4558
5701
  description,
4559
- attributes
5702
+ attributes: [
5703
+ bundle,
5704
+ {
5705
+ name: bundleOutAttribute,
5706
+ description: "Specify the output path for the bundled stylesheet relative to the output directory, regardless of the HTML file's location.\nDefaults to `{filename}.bundle.css` next to the original file."
5707
+ },
5708
+ externals,
5709
+ externalsExclude
5710
+ ]
4560
5711
  }, {
4561
5712
  name: "style",
4562
5713
  description,
4563
- attributes
5714
+ attributes: [
5715
+ bundle,
5716
+ externals,
5717
+ externalsExclude
5718
+ ]
4564
5719
  }]
4565
5720
  };
4566
5721
  }
@@ -4913,6 +6068,8 @@ async function loadScriptMetadata(source, filePath) {
4913
6068
  * - A local file has to be part of the build already, otherwise its metadata is not found and nothing is inlined.
4914
6069
  * - The inlined copy is rebased onto the HTML file, so relative "url()" and "@import" paths keep resolving, and the original file
4915
6070
  * stays in the output unless something removes it.
6071
+ * - Combining with html-bundle-style: register that plugin first, so a link carrying both attributes is bundled before it is
6072
+ * inlined and the inlined content is the bundle.
4916
6073
  */
4917
6074
  function htmlInlineStylePlugin(options = {}) {
4918
6075
  const inlineAttribute = options.inlineAttribute ?? "inline";
@@ -6307,7 +7464,7 @@ function isSelfReference(sourceAbsolute, filePathAbsolute) {
6307
7464
  function calculatePagePath(originalPath, pagesDirectory) {
6308
7465
  if (extname(originalPath) === ".md") originalPath = replaceExtension(originalPath, ".html");
6309
7466
  if (!isSubpath(pagesDirectory, dirname(originalPath))) return normalize(originalPath);
6310
- const fileName = parse$2(originalPath).name;
7467
+ const fileName = parse$5(originalPath).name;
6311
7468
  const extension = extname(originalPath);
6312
7469
  let newPath = cutLeadingSubpath(originalPath, pagesDirectory);
6313
7470
  if (extension && fileName !== "index") newPath = join(dirname(newPath), fileName, "index" + extension);
@@ -7327,7 +8484,7 @@ function coreMarkdownPlugin(options = {}) {
7327
8484
  const frontmatter = {};
7328
8485
  const firstNode = root.children[0];
7329
8486
  if (firstNode?.type === "yaml") {
7330
- const parsed = parse$1(firstNode.value) ?? {};
8487
+ const parsed = parse$4(firstNode.value) ?? {};
7331
8488
  Object.assign(frontmatter, parsed);
7332
8489
  }
7333
8490
  return [{