@trebired/bundler 2.0.0 → 3.0.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +207 -287
  3. package/dist/core/asset-manifest.d.ts.map +1 -1
  4. package/dist/core/asset-manifest.js +70 -72
  5. package/dist/core/asset-manifest.js.map +1 -1
  6. package/dist/core/build.d.ts.map +1 -1
  7. package/dist/core/build.js +5 -10
  8. package/dist/core/build.js.map +1 -1
  9. package/dist/core/derive-manifest.d.ts.map +1 -1
  10. package/dist/core/derive-manifest.js +4 -0
  11. package/dist/core/derive-manifest.js.map +1 -1
  12. package/dist/core/discovery.d.ts +13 -17
  13. package/dist/core/discovery.d.ts.map +1 -1
  14. package/dist/core/discovery.js +276 -277
  15. package/dist/core/discovery.js.map +1 -1
  16. package/dist/core/esbuild-options.d.ts +1 -3
  17. package/dist/core/esbuild-options.d.ts.map +1 -1
  18. package/dist/core/esbuild-options.js +4 -21
  19. package/dist/core/esbuild-options.js.map +1 -1
  20. package/dist/core/manifest.d.ts +2 -2
  21. package/dist/core/manifest.d.ts.map +1 -1
  22. package/dist/core/manifest.js +2 -11
  23. package/dist/core/manifest.js.map +1 -1
  24. package/dist/core/shared.d.ts +3 -9
  25. package/dist/core/shared.d.ts.map +1 -1
  26. package/dist/core/shared.js +5 -22
  27. package/dist/core/shared.js.map +1 -1
  28. package/dist/core/watch.d.ts.map +1 -1
  29. package/dist/core/watch.js +20 -32
  30. package/dist/core/watch.js.map +1 -1
  31. package/dist/index.d.ts +1 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +0 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/plugins/virtual-entries.js +1 -1
  36. package/dist/plugins/virtual-entries.js.map +1 -1
  37. package/dist/types.d.ts +67 -32
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +2 -2
@@ -1,31 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
- const DEFAULT_DISCOVERY_EXTENSIONS = [".css", ".js", ".jsx", ".scss", ".ts", ".tsx"];
5
- const DEFAULT_IGNORE_DIRS = [".git", "coverage", "dist", "node_modules"];
4
+ import { walkImportGraph } from "./import-graph.js";
6
5
  const DEFAULT_DISCOVERY_BUNDLE_MAX_SIZE = 50 * 1024 * 1024;
7
- const DEFAULT_DISCOVERY_BUNDLE_GROUPS = [
8
- {
9
- name: "scripts",
10
- extensions: [".js", ".ts"],
11
- loader: "ts",
12
- },
13
- {
14
- name: "styles",
15
- extensions: [".css", ".scss"],
16
- loader: "css",
17
- },
18
- ];
19
- const NORMALIZED_DISCOVERY_BUNDLE_GROUPS = DEFAULT_DISCOVERY_BUNDLE_GROUPS.map((group) => ({
20
- ...group,
21
- extensions: new Set(group.extensions),
22
- }));
6
+ const DEFAULT_IGNORE_DIRS = [".git", "coverage", "dist", "node_modules"];
23
7
  const VIRTUAL_ENTRY_PREFIX = "trebired-virtual:";
24
8
  function toPosixPath(value) {
25
9
  return value.replace(/\\/g, "/");
26
10
  }
27
11
  function normalizePathValue(value) {
28
- return toPosixPath(String(value || "").trim()).replace(/^\.\/+/, "");
12
+ return toPosixPath(String(value || "").trim()).replace(/^\.\/+/, "").replace(/^\/+|\/+$/g, "");
29
13
  }
30
14
  function globToRegExp(pattern) {
31
15
  const normalized = normalizePathValue(pattern);
@@ -74,9 +58,6 @@ function matchesAnyPattern(value, patterns) {
74
58
  return globToRegExp(normalizedPattern).test(normalized);
75
59
  });
76
60
  }
77
- function normalizeStringList(values) {
78
- return (values || []).map(normalizePathValue).filter(Boolean);
79
- }
80
61
  function parseBundleMaxSize(value) {
81
62
  if (typeof value === "number") {
82
63
  if (!Number.isFinite(value) || value <= 0) {
@@ -106,9 +87,36 @@ function parseBundleMaxSize(value) {
106
87
  }
107
88
  return resolved;
108
89
  }
90
+ function normalizeStringList(values) {
91
+ return (values || []).map(normalizePathValue).filter(Boolean);
92
+ }
93
+ function normalizeDiscoverRule(rule) {
94
+ const key = normalizePathValue(rule.key);
95
+ const include = normalizeStringList(rule.include);
96
+ const exclude = normalizeStringList(rule.exclude);
97
+ if (!key) {
98
+ throw new Error("bundler-discover-rule-missing-key");
99
+ }
100
+ if (include.length === 0) {
101
+ throw new Error(`bundler-discover-rule-missing-include :: ${key}`);
102
+ }
103
+ if (rule.strategy !== "bundle" && rule.maxBundleSize != null) {
104
+ throw new Error(`bundler-discover-rule-invalid-max-size-strategy :: ${key}`);
105
+ }
106
+ return {
107
+ exclude,
108
+ include,
109
+ key,
110
+ maxBundleSize: rule.strategy === "bundle" ? parseBundleMaxSize(rule.maxBundleSize) : undefined,
111
+ strategy: rule.strategy,
112
+ };
113
+ }
109
114
  function normalizeDiscoverOptions(rootDir, discover) {
110
115
  const list = Array.isArray(discover) ? discover : discover ? [discover] : [];
111
- return list
116
+ if (list.length === 0) {
117
+ throw new Error("bundler-missing-discover");
118
+ }
119
+ const normalized = list
112
120
  .map((item) => item && typeof item === "object" ? item : null)
113
121
  .filter(Boolean)
114
122
  .map((item) => {
@@ -116,109 +124,30 @@ function normalizeDiscoverOptions(rootDir, discover) {
116
124
  if (!dir) {
117
125
  throw new Error("bundler-discover-missing-dir");
118
126
  }
119
- const extensions = (item.extensions && item.extensions.length ? item.extensions : DEFAULT_DISCOVERY_EXTENSIONS)
120
- .map((value) => String(value || "").trim().toLowerCase())
121
- .filter(Boolean)
122
- .map((value) => value.startsWith(".") ? value : `.${value}`);
127
+ const rules = (item.rules || []).map(normalizeDiscoverRule);
128
+ if (rules.length === 0) {
129
+ throw new Error(`bundler-discover-missing-rules :: ${dir}`);
130
+ }
123
131
  return {
124
132
  dir,
125
133
  dirAbs: path.resolve(rootDir, dir),
126
- exclude: normalizeStringList(item.exclude),
127
- extensions,
128
134
  ignoreDirs: new Set([
129
135
  ...DEFAULT_IGNORE_DIRS,
130
136
  ...normalizeStringList(item.ignoreDirs),
131
137
  ].map((value) => path.basename(value))),
132
- include: normalizeStringList(item.include),
133
- maxBundleSize: parseBundleMaxSize(item.maxBundleSize),
134
- namePrefix: normalizePathValue(item.namePrefix || ""),
138
+ rules,
135
139
  };
136
140
  });
137
- }
138
- function normalizeManualEntries(entries, rootDir) {
139
- if (!entries)
140
- return [];
141
- if (Array.isArray(entries)) {
142
- return entries
143
- .map((value) => String(value || "").trim())
144
- .filter(Boolean)
145
- .map((value) => {
146
- const rel = normalizePathValue(value);
147
- const ext = path.extname(rel);
148
- const noExt = ext ? rel.slice(0, -ext.length) : rel;
149
- return {
150
- name: noExt,
151
- path: path.resolve(rootDir, rel),
152
- source: "manual",
153
- };
154
- });
155
- }
156
- if (typeof entries === "object") {
157
- return Object.entries(entries)
158
- .map(([key, value]) => ({
159
- name: normalizePathValue(key),
160
- path: path.resolve(rootDir, String(value || "").trim()),
161
- source: "manual",
162
- }))
163
- .filter((entry) => Boolean(entry.name && entry.path));
164
- }
165
- return [];
166
- }
167
- function normalizeVirtualEntries(virtualEntries) {
168
- if (!virtualEntries || typeof virtualEntries !== "object")
169
- return [];
170
- return Object.entries(virtualEntries)
171
- .map(([key, value]) => ({
172
- name: normalizePathValue(key),
173
- path: `${VIRTUAL_ENTRY_PREFIX}${normalizePathValue(key)}`,
174
- source: "virtual",
175
- contents: String(value || ""),
176
- }))
177
- .filter((entry) => Boolean(entry.name));
178
- }
179
- function resolveEntryPriority(record) {
180
- if (record.source === "manual")
181
- return 0;
182
- if (record.source === "virtual")
183
- return 1;
184
- return 2;
185
- }
186
- function compareEntries(a, b) {
187
- return resolveEntryPriority(a) - resolveEntryPriority(b)
188
- || a.name.localeCompare(b.name)
189
- || a.path.localeCompare(b.path);
190
- }
191
- function dedupeEntriesBySourcePath(records) {
192
- const duplicates = [];
193
- const keptByPath = new Map();
194
- for (const record of [...records].sort(compareEntries)) {
195
- if (record.source === "virtual") {
196
- const virtualKey = `virtual:${record.name}`;
197
- if (!keptByPath.has(virtualKey)) {
198
- keptByPath.set(virtualKey, record);
141
+ const seenRuleKeys = new Set();
142
+ for (const config of normalized) {
143
+ for (const rule of config.rules) {
144
+ if (seenRuleKeys.has(rule.key)) {
145
+ throw new Error(`bundler-discover-duplicate-rule-key :: ${rule.key}`);
199
146
  }
200
- continue;
201
- }
202
- const key = toPosixPath(path.resolve(record.path));
203
- const existing = keptByPath.get(key);
204
- if (!existing) {
205
- keptByPath.set(key, record);
206
- continue;
147
+ seenRuleKeys.add(rule.key);
207
148
  }
208
- duplicates.push({
209
- dropped: record,
210
- kept: existing,
211
- });
212
149
  }
213
- return {
214
- duplicates: duplicates.sort((a, b) => compareEntries(a.dropped, b.dropped)),
215
- records: Array.from(keptByPath.values()).sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)),
216
- };
217
- }
218
- function buildDiscoveredEntryName(args) {
219
- const ext = path.extname(args.relativePath);
220
- const withoutExt = ext ? args.relativePath.slice(0, -ext.length) : args.relativePath;
221
- return normalizePathValue([args.config.namePrefix, withoutExt].filter(Boolean).join("/"));
150
+ return normalized;
222
151
  }
223
152
  function createStableBundleId(value) {
224
153
  let hash = 2166136261;
@@ -228,10 +157,6 @@ function createStableBundleId(value) {
228
157
  }
229
158
  return (hash >>> 0).toString(36);
230
159
  }
231
- function resolveBundleGroup(entry) {
232
- const ext = path.extname(entry.path).toLowerCase();
233
- return NORMALIZED_DISCOVERY_BUNDLE_GROUPS.find((group) => group.extensions.has(ext));
234
- }
235
160
  function toRootImportSpecifier(rootDir, absPath) {
236
161
  const rel = normalizePathValue(path.relative(rootDir, absPath));
237
162
  return rel.startsWith(".") ? rel : `./${rel}`;
@@ -239,205 +164,279 @@ function toRootImportSpecifier(rootDir, absPath) {
239
164
  function buildBundleContents(args) {
240
165
  if (args.loader === "css") {
241
166
  return args.files
242
- .map((file) => `@import ${JSON.stringify(toRootImportSpecifier(args.rootDir, file.path))};`)
167
+ .map((file) => `@import ${JSON.stringify(toRootImportSpecifier(args.rootDir, file.absPath))};`)
243
168
  .join("\n");
244
169
  }
245
170
  return args.files
246
- .map((file) => `import ${JSON.stringify(toRootImportSpecifier(args.rootDir, file.path))};`)
171
+ .map((file) => `import ${JSON.stringify(toRootImportSpecifier(args.rootDir, file.absPath))};`)
247
172
  .join("\n");
248
173
  }
249
- function splitEntriesByMaxSize(args) {
250
- const chunks = [];
251
- let current = [];
252
- let currentSize = 0;
253
- for (const entry of args.entries) {
254
- const nextSize = currentSize + entry.bytes;
255
- if (current.length > 0 && nextSize > args.maxSize) {
256
- chunks.push(current);
257
- current = [];
258
- currentSize = 0;
259
- }
260
- current.push(entry);
261
- currentSize += entry.bytes;
262
- }
263
- if (current.length > 0) {
264
- chunks.push(current);
265
- }
266
- return chunks;
174
+ function buildEntryKey(args) {
175
+ const ext = path.extname(args.rootRel);
176
+ const withoutExt = ext ? args.rootRel.slice(0, -ext.length) : args.rootRel;
177
+ return `entry:${args.ruleKey}:${normalizePathValue(withoutExt)}`;
267
178
  }
268
- async function toBundledDiscoverEntries(args) {
269
- const resolved = await Promise.all(args.records.map(async (record) => {
270
- const group = resolveBundleGroup(record);
271
- if (!group) {
272
- return {
273
- group: undefined,
274
- record,
275
- };
276
- }
277
- const stats = await fsp.stat(record.path);
278
- const bytes = Math.max(stats.size, 1);
279
- if (bytes > args.config.maxBundleSize) {
280
- throw new Error(`bundler-discover-bundle-file-too-large :: ${normalizePathValue(path.relative(args.rootDir, record.path))}`);
281
- }
282
- return {
283
- group,
284
- record: {
285
- ...record,
286
- bytes,
287
- },
288
- };
289
- }));
290
- const passthrough = [];
291
- const grouped = new Map();
292
- for (const item of resolved) {
293
- if (!item.group) {
294
- passthrough.push(item.record);
295
- continue;
296
- }
297
- const existing = grouped.get(item.group.name);
298
- if (existing) {
299
- existing.files.push(item.record);
300
- continue;
301
- }
302
- grouped.set(item.group.name, {
303
- files: [item.record],
304
- group: item.group,
305
- });
306
- }
307
- const bundledRecords = [];
308
- for (const [groupName, value] of Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b))) {
309
- const bundleId = createStableBundleId(JSON.stringify({
310
- dir: args.config.dir,
311
- exclude: args.config.exclude,
312
- extensions: args.config.extensions,
313
- groupName,
314
- include: args.config.include,
315
- maxBundleSize: args.config.maxBundleSize,
316
- namePrefix: args.config.namePrefix,
317
- }));
318
- const chunks = splitEntriesByMaxSize({
319
- entries: value.files.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)),
320
- maxSize: args.config.maxBundleSize,
321
- });
322
- chunks.forEach((chunk, index) => {
323
- const suffix = chunks.length > 1 ? `-${index + 1}` : "";
324
- const name = `bundle-${bundleId}${suffix}`;
325
- bundledRecords.push({
326
- contents: buildBundleContents({
327
- files: chunk,
328
- loader: value.group.loader,
329
- rootDir: args.rootDir,
330
- }),
331
- name,
332
- path: `${VIRTUAL_ENTRY_PREFIX}${name}`,
333
- source: "virtual",
334
- virtualLoader: value.group.loader,
335
- });
336
- });
337
- }
338
- return [...passthrough, ...bundledRecords]
339
- .sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
179
+ function buildBundleEntryKey(ruleKey, part) {
180
+ return `bundle:${ruleKey}:${part}`;
340
181
  }
341
- async function walkDiscoveredEntries(config) {
182
+ function resolveBundleLoader(filePath) {
183
+ if (/\.(?:css|scss)$/i.test(filePath))
184
+ return "css";
185
+ if (/\.(?:[mc]?[jt]sx?)$/i.test(filePath))
186
+ return "ts";
187
+ return undefined;
188
+ }
189
+ async function scanDiscoveredFiles(config, rootDir) {
342
190
  if (!fs.existsSync(config.dirAbs))
343
191
  return [];
344
- const records = [];
192
+ const files = [];
345
193
  const visit = async (currentAbs) => {
346
194
  const entries = await fsp.readdir(currentAbs, { withFileTypes: true });
347
195
  for (const entry of entries) {
348
196
  const abs = path.join(currentAbs, entry.name);
349
- const relFromDiscover = normalizePathValue(path.relative(config.dirAbs, abs));
350
- if (!relFromDiscover)
197
+ const discoverRel = normalizePathValue(path.relative(config.dirAbs, abs));
198
+ if (!discoverRel)
351
199
  continue;
352
200
  if (entry.isDirectory()) {
353
201
  if (config.ignoreDirs.has(entry.name))
354
202
  continue;
355
- if (matchesAnyPattern(relFromDiscover, config.exclude))
356
- continue;
357
203
  await visit(abs);
358
204
  continue;
359
205
  }
360
206
  if (!entry.isFile())
361
207
  continue;
362
- const ext = path.extname(entry.name).toLowerCase();
363
- if (!config.extensions.includes(ext))
364
- continue;
365
- if (config.include.length && !matchesAnyPattern(relFromDiscover, config.include))
366
- continue;
367
- if (matchesAnyPattern(relFromDiscover, config.exclude))
368
- continue;
369
- records.push({
370
- name: buildDiscoveredEntryName({
371
- config,
372
- relativePath: relFromDiscover,
373
- }),
374
- path: abs,
375
- source: "discover",
208
+ files.push({
209
+ absPath: abs,
210
+ discoverRel,
211
+ rootRel: normalizePathValue(path.relative(rootDir, abs)),
376
212
  });
377
213
  }
378
214
  };
379
215
  await visit(config.dirAbs);
380
- return records.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
216
+ return files.sort((a, b) => a.rootRel.localeCompare(b.rootRel));
381
217
  }
382
- async function resolveBundlerEntries(options, rootDir, settings = {}) {
383
- const manual = normalizeManualEntries(options.entries, rootDir);
384
- const virtual = normalizeVirtualEntries(options.virtualEntries);
385
- const discoveredGroups = await Promise.all(normalizeDiscoverOptions(rootDir, options.discover).map(async (config) => {
386
- const records = await walkDiscoveredEntries(config);
387
- return toBundledDiscoverEntries({
388
- config,
389
- records,
390
- rootDir,
218
+ function classifyFile(args) {
219
+ return args.config.rules.find((rule) => {
220
+ if (!matchesAnyPattern(args.file.discoverRel, rule.include))
221
+ return false;
222
+ if (matchesAnyPattern(args.file.discoverRel, rule.exclude))
223
+ return false;
224
+ return true;
225
+ });
226
+ }
227
+ function splitByMaxSize(args) {
228
+ const chunks = [];
229
+ let current = [];
230
+ let currentSize = 0;
231
+ for (const file of args.files) {
232
+ const nextSize = currentSize + file.bytes;
233
+ if (current.length > 0 && nextSize > args.maxBundleSize) {
234
+ chunks.push(current);
235
+ current = [];
236
+ currentSize = 0;
237
+ }
238
+ current.push(file);
239
+ currentSize += file.bytes;
240
+ }
241
+ if (current.length > 0) {
242
+ chunks.push(current);
243
+ }
244
+ return chunks;
245
+ }
246
+ async function validateGroupedBootImports(args) {
247
+ const groupedScriptSources = new Set(args.entries
248
+ .filter((entry) => entry.strategy === "bundle" && entry.virtualLoader === "ts")
249
+ .flatMap((entry) => entry.ownedSources));
250
+ if (groupedScriptSources.size === 0)
251
+ return;
252
+ const bootEntries = args.entries.filter((entry) => {
253
+ if (entry.strategy !== "entry" || !entry.entrySource)
254
+ return false;
255
+ return /\.(?:client\.tsx?|defer\.ts)$/i.test(entry.entrySource);
256
+ });
257
+ for (const bootEntry of bootEntries) {
258
+ const graph = await walkImportGraph({
259
+ entries: bootEntry.entrySource,
260
+ rootDir: args.rootDir,
391
261
  });
392
- }));
393
- const discovered = discoveredGroups.flat();
394
- const deduped = dedupeEntriesBySourcePath([...manual, ...virtual, ...discovered]);
395
- const all = deduped.records;
396
- if (!all.length && !settings.allowEmpty) {
397
- throw new Error("bundler-missing-entries");
262
+ const groupedDependency = Object.keys(graph.files)
263
+ .sort()
264
+ .find((sourcePath) => sourcePath !== bootEntry.entrySource && groupedScriptSources.has(sourcePath));
265
+ if (groupedDependency) {
266
+ throw new Error(`bundler-discover-entry-imports-grouped-source :: ${bootEntry.entrySource} -> ${groupedDependency}`);
267
+ }
398
268
  }
399
- const byName = new Map();
400
- for (const record of all) {
401
- const existing = byName.get(record.name);
402
- if (!existing) {
403
- byName.set(record.name, record);
404
- continue;
269
+ }
270
+ async function resolveBundlerEntries(options, rootDir, settings = {}) {
271
+ const configs = normalizeDiscoverOptions(rootDir, options.discover);
272
+ const resolvedEntries = [];
273
+ const rules = new Map();
274
+ const sourceOwners = new Map();
275
+ const emittedNames = new Set();
276
+ const emittedKeys = new Set();
277
+ for (const config of configs) {
278
+ const files = await scanDiscoveredFiles(config, rootDir);
279
+ const matchedByRule = new Map();
280
+ for (const rule of config.rules) {
281
+ rules.set(rule.key, {
282
+ entryKeys: [],
283
+ ignoredSources: [],
284
+ ruleKey: rule.key,
285
+ strategy: rule.strategy,
286
+ });
287
+ matchedByRule.set(rule.key, []);
288
+ }
289
+ for (const file of files) {
290
+ const matchedRule = classifyFile({
291
+ config,
292
+ file,
293
+ });
294
+ if (!matchedRule) {
295
+ throw new Error(`bundler-discover-unmatched-file :: ${file.rootRel}`);
296
+ }
297
+ matchedByRule.get(matchedRule.key).push(file);
298
+ }
299
+ for (const rule of config.rules) {
300
+ const matchedFiles = (matchedByRule.get(rule.key) || []).sort((a, b) => a.rootRel.localeCompare(b.rootRel));
301
+ const ruleRecord = rules.get(rule.key);
302
+ if (rule.strategy === "ignore") {
303
+ ruleRecord.ignoredSources = matchedFiles.map((file) => file.rootRel);
304
+ continue;
305
+ }
306
+ if (rule.strategy === "entry") {
307
+ for (const file of matchedFiles) {
308
+ const ext = path.extname(file.rootRel);
309
+ const withoutExt = ext ? file.rootRel.slice(0, -ext.length) : file.rootRel;
310
+ const record = {
311
+ entrySource: file.rootRel,
312
+ key: buildEntryKey({
313
+ rootRel: file.rootRel,
314
+ ruleKey: rule.key,
315
+ }),
316
+ kind: "entry",
317
+ name: normalizePathValue(withoutExt),
318
+ ownedSources: [file.rootRel],
319
+ path: file.absPath,
320
+ ruleKey: rule.key,
321
+ source: "discover",
322
+ strategy: "entry",
323
+ };
324
+ if (emittedKeys.has(record.key)) {
325
+ throw new Error(`bundler-discover-entry-key-conflict :: ${record.key}`);
326
+ }
327
+ if (emittedNames.has(record.name)) {
328
+ throw new Error(`bundler-discover-output-name-conflict :: ${record.name}`);
329
+ }
330
+ emittedKeys.add(record.key);
331
+ emittedNames.add(record.name);
332
+ resolvedEntries.push(record);
333
+ ruleRecord.entryKeys.push(record.key);
334
+ sourceOwners.set(file.rootRel, record.key);
335
+ }
336
+ continue;
337
+ }
338
+ const filesWithStats = await Promise.all(matchedFiles.map(async (file) => {
339
+ const stats = await fsp.stat(file.absPath);
340
+ const bytes = Math.max(stats.size, 1);
341
+ if (bytes > rule.maxBundleSize) {
342
+ throw new Error(`bundler-discover-bundle-file-too-large :: ${file.rootRel}`);
343
+ }
344
+ return {
345
+ ...file,
346
+ bytes,
347
+ };
348
+ }));
349
+ if (filesWithStats.length === 0) {
350
+ continue;
351
+ }
352
+ const loaders = new Set(filesWithStats.map((file) => resolveBundleLoader(file.rootRel)).filter(Boolean));
353
+ if (loaders.size !== 1) {
354
+ throw new Error(`bundler-discover-rule-mixed-loaders :: ${rule.key}`);
355
+ }
356
+ const loader = Array.from(loaders)[0];
357
+ const stableId = createStableBundleId(JSON.stringify({
358
+ dir: config.dir,
359
+ ruleKey: rule.key,
360
+ sources: filesWithStats.map((file) => file.rootRel),
361
+ }));
362
+ const chunks = splitByMaxSize({
363
+ files: filesWithStats.sort((a, b) => a.rootRel.localeCompare(b.rootRel)),
364
+ maxBundleSize: rule.maxBundleSize,
365
+ });
366
+ chunks.forEach((chunk, index) => {
367
+ const part = index + 1;
368
+ const name = chunks.length === 1 ? `bundle-${stableId}` : `bundle-${stableId}-${part}`;
369
+ const key = buildBundleEntryKey(rule.key, part);
370
+ const ownedSources = chunk.map((file) => file.rootRel);
371
+ if (emittedKeys.has(key)) {
372
+ throw new Error(`bundler-discover-entry-key-conflict :: ${key}`);
373
+ }
374
+ if (emittedNames.has(name)) {
375
+ throw new Error(`bundler-discover-output-name-conflict :: ${name}`);
376
+ }
377
+ emittedKeys.add(key);
378
+ emittedNames.add(name);
379
+ resolvedEntries.push({
380
+ contents: buildBundleContents({
381
+ files: chunk,
382
+ loader,
383
+ rootDir,
384
+ }),
385
+ key,
386
+ kind: "bundle",
387
+ name,
388
+ ownedSources,
389
+ path: `${VIRTUAL_ENTRY_PREFIX}${name}`,
390
+ ruleKey: rule.key,
391
+ source: "internal",
392
+ strategy: "bundle",
393
+ virtualLoader: loader,
394
+ });
395
+ ruleRecord.entryKeys.push(key);
396
+ for (const sourcePath of ownedSources) {
397
+ sourceOwners.set(sourcePath, key);
398
+ }
399
+ });
405
400
  }
406
- if (existing.path === record.path)
407
- continue;
408
- throw new Error(`bundler-entry-name-conflict :: ${record.name}`);
409
401
  }
410
- const records = Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
411
- const signature = JSON.stringify(records.map((record) => ({
412
- contents: record.source === "virtual" ? record.contents || "" : undefined,
413
- name: record.name,
414
- path: record.source === "virtual"
415
- ? `virtual:${record.name}`
416
- : normalizePathValue(path.relative(rootDir, record.path)),
417
- source: record.source,
418
- })));
402
+ await validateGroupedBootImports({
403
+ entries: resolvedEntries,
404
+ rootDir,
405
+ });
406
+ if (resolvedEntries.length === 0 && !settings.allowEmpty) {
407
+ throw new Error("bundler-missing-entries");
408
+ }
409
+ const discovery = {
410
+ entries: resolvedEntries.sort((a, b) => a.key.localeCompare(b.key)),
411
+ rules: Object.fromEntries(Array.from(rules.entries()).sort(([a], [b]) => a.localeCompare(b))),
412
+ sourceOwners: Object.fromEntries(Array.from(sourceOwners.entries()).sort(([a], [b]) => a.localeCompare(b))),
413
+ };
419
414
  return {
420
- duplicates: deduped.duplicates,
421
- records,
422
- signature,
415
+ ...discovery,
416
+ signature: JSON.stringify({
417
+ entries: discovery.entries.map((entry) => ({
418
+ entrySource: entry.entrySource,
419
+ key: entry.key,
420
+ kind: entry.kind,
421
+ name: entry.name,
422
+ ownedSources: entry.ownedSources,
423
+ path: entry.source === "internal" ? `virtual:${entry.name}` : entry.path,
424
+ ruleKey: entry.ruleKey,
425
+ strategy: entry.strategy,
426
+ })),
427
+ rules: discovery.rules,
428
+ sourceOwners: discovery.sourceOwners,
429
+ }),
423
430
  };
424
431
  }
425
432
  function toEntryPointMap(records, rootDir) {
426
433
  return Object.fromEntries(records.map((record) => [
427
434
  record.name,
428
- record.source === "virtual"
435
+ record.source === "internal"
429
436
  ? record.path
430
437
  : normalizePathValue(path.relative(rootDir, record.path)),
431
438
  ]));
432
439
  }
433
- function toPublicEntryMap(records, rootDir) {
434
- return Object.fromEntries(records.map((record) => [
435
- record.name,
436
- record.source === "virtual"
437
- ? `virtual:${record.name}`
438
- : normalizePathValue(path.relative(rootDir, record.path)),
439
- ]));
440
- }
441
440
  function normalizeManifestOptions(manifest) {
442
441
  if (!manifest) {
443
442
  return { enabled: false };
@@ -466,5 +465,5 @@ function normalizeDiscoverRoots(rootDir, discover) {
466
465
  });
467
466
  return Array.from(new Set(roots));
468
467
  }
469
- export { normalizeDiscoverRoots, normalizeManifestOptions, resolveBundlerEntries, toPublicEntryMap, toEntryPointMap, toPosixPath, VIRTUAL_ENTRY_PREFIX, };
468
+ export { normalizeDiscoverRoots, normalizeManifestOptions, resolveBundlerEntries, toEntryPointMap, toPosixPath, VIRTUAL_ENTRY_PREFIX, };
470
469
  //# sourceMappingURL=discovery.js.map