browser-metro 1.0.5

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.
@@ -0,0 +1,646 @@
1
+ import { Resolver } from "./resolver.js";
2
+ import { DependencyGraph } from "./dependency-graph.js";
3
+ import { ModuleCache } from "./module-cache.js";
4
+ import { emitHmrBundle, HMR_RUNTIME_TEMPLATE } from "./hmr-runtime.js";
5
+ import { buildCombinedSourceMap, countNewlines, inlineSourceMap, shiftSourceMapOrigLines, } from "./source-map.js";
6
+ import { findRequires, rewriteRequires, hashString, buildBundlePreamble } from "./utils.js";
7
+ export class IncrementalBundler {
8
+ constructor(fs, config) {
9
+ this.graph = new DependencyGraph();
10
+ this.cache = new ModuleCache();
11
+ this.moduleMap = {};
12
+ this.sourceMapMap = {};
13
+ this.entryFile = null;
14
+ this.packageVersions = {};
15
+ this.transitiveDepsVersions = {};
16
+ this.fs = fs;
17
+ this.config = config;
18
+ const paths = IncrementalBundler.readTsconfigPaths(fs);
19
+ this.resolver = new Resolver(fs, { ...config.resolver, ...(paths && { paths }) });
20
+ this.plugins = config.plugins ?? [];
21
+ }
22
+ /** Read tsconfig.json "compilerOptions.paths" from the VirtualFS */
23
+ static readTsconfigPaths(fs) {
24
+ const raw = fs.read("/tsconfig.json");
25
+ if (!raw)
26
+ return null;
27
+ try {
28
+ const tsconfig = JSON.parse(raw);
29
+ const paths = tsconfig?.compilerOptions?.paths;
30
+ return paths && typeof paths === "object" ? paths : null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** Run the full pre-transform -> Sucrase -> post-transform pipeline */
37
+ runTransform(filename, src) {
38
+ const originalLines = countNewlines(src);
39
+ // Pre-transform hooks
40
+ for (const plugin of this.plugins) {
41
+ if (plugin.transformSource) {
42
+ const result = plugin.transformSource({ src, filename });
43
+ if (result)
44
+ src = result.src;
45
+ }
46
+ }
47
+ const preTransformAddedLines = countNewlines(src) - originalLines;
48
+ // Core transform (Sucrase)
49
+ const transformResult = this.config.transformer.transform({ src, filename });
50
+ let code = transformResult.code;
51
+ let sourceMap = transformResult.sourceMap;
52
+ // Post-transform hooks -- track line additions for source map offset
53
+ const linesBeforePost = countNewlines(code);
54
+ for (const plugin of this.plugins) {
55
+ if (plugin.transformOutput) {
56
+ const result = plugin.transformOutput({ code, filename });
57
+ if (result)
58
+ code = result.code;
59
+ }
60
+ }
61
+ // Adjust source map for plugin modifications
62
+ if (sourceMap) {
63
+ // Post-transform: shift generated lines for prepended output lines
64
+ const postAddedLines = countNewlines(code) - linesBeforePost;
65
+ if (postAddedLines > 0) {
66
+ sourceMap = {
67
+ ...sourceMap,
68
+ mappings: ";".repeat(postAddedLines) + sourceMap.mappings,
69
+ };
70
+ }
71
+ // Pre-transform: shift origLine back so mappings point to original source
72
+ if (preTransformAddedLines > 0) {
73
+ sourceMap = shiftSourceMapOrigLines(sourceMap, -preTransformAddedLines);
74
+ }
75
+ }
76
+ return { code, sourceMap };
77
+ }
78
+ /** Build a resolve callback that consults plugins then falls back to default resolution */
79
+ makeResolveTarget(fromFile) {
80
+ return (target) => {
81
+ // Let plugins resolve first
82
+ for (const plugin of this.plugins) {
83
+ if (plugin.resolveRequest) {
84
+ const result = plugin.resolveRequest({ fromFile }, target);
85
+ if (result !== null)
86
+ return result;
87
+ }
88
+ }
89
+ // Default resolution: skip npm packages, resolve local paths
90
+ if (this.resolver.isNpmPackage(target))
91
+ return null;
92
+ const resolved = this.resolver.resolvePath(fromFile, target);
93
+ const actual = this.resolver.resolveFile(resolved);
94
+ return actual ?? null;
95
+ };
96
+ }
97
+ /** Collect module aliases from all plugins */
98
+ getModuleAliases() {
99
+ const aliases = {};
100
+ for (const plugin of this.plugins) {
101
+ if (plugin.moduleAliases) {
102
+ Object.assign(aliases, plugin.moduleAliases());
103
+ }
104
+ }
105
+ return aliases;
106
+ }
107
+ /** Collect module shims from all plugins */
108
+ getShimModules() {
109
+ const shims = {};
110
+ for (const plugin of this.plugins) {
111
+ if (plugin.shimModules) {
112
+ Object.assign(shims, plugin.shimModules());
113
+ }
114
+ }
115
+ return shims;
116
+ }
117
+ /** Scan npm packages in the module map for require calls not yet fetched */
118
+ findTransitiveNpmDeps(skipNames) {
119
+ const newDeps = new Set();
120
+ for (const [name, code] of Object.entries(this.moduleMap)) {
121
+ if (!this.resolver.isNpmPackage(name))
122
+ continue;
123
+ for (const dep of findRequires(code)) {
124
+ if (this.resolver.isNpmPackage(dep) && !(dep in this.moduleMap) && !skipNames.has(dep)) {
125
+ newDeps.add(dep);
126
+ }
127
+ }
128
+ }
129
+ return newDeps;
130
+ }
131
+ /** Transform a single file using the configured transformer */
132
+ transformFile(filename, src) {
133
+ return this.runTransform(filename, src).code;
134
+ }
135
+ /** Read dependency versions from the project's package.json */
136
+ getPackageVersions() {
137
+ const raw = this.fs.read("/package.json");
138
+ if (!raw)
139
+ return {};
140
+ try {
141
+ const pkg = JSON.parse(raw);
142
+ return pkg.dependencies || {};
143
+ }
144
+ catch {
145
+ return {};
146
+ }
147
+ }
148
+ /** Resolve an npm specifier to a versioned form.
149
+ * Priority: user's package.json > transitive dep versions from manifests > bare name */
150
+ resolveNpmSpecifier(specifier, versions) {
151
+ let baseName;
152
+ if (specifier.startsWith("@")) {
153
+ const parts = specifier.split("/");
154
+ baseName = parts[0] + "/" + parts[1];
155
+ }
156
+ else {
157
+ baseName = specifier.split("/")[0];
158
+ }
159
+ const version = versions[baseName] || this.transitiveDepsVersions[baseName];
160
+ if (!version)
161
+ return specifier;
162
+ const subpath = specifier.slice(baseName.length);
163
+ return baseName + "@" + version + subpath;
164
+ }
165
+ /** Fetch a pre-bundled npm package from the package server */
166
+ async fetchPackage(specifier) {
167
+ const url = this.config.server.packageServerUrl + "/pkg/" + specifier;
168
+ const res = await fetch(url);
169
+ if (!res.ok) {
170
+ const body = await res.text().catch(() => "");
171
+ throw new Error("Failed to fetch package '" + specifier + "' (HTTP " + res.status + ")" + (body ? ": " + body.slice(0, 200) : ""));
172
+ }
173
+ const code = await res.text();
174
+ let externals = {};
175
+ const externalsHeader = res.headers.get("X-Externals");
176
+ if (externalsHeader) {
177
+ try {
178
+ externals = JSON.parse(externalsHeader);
179
+ }
180
+ catch { }
181
+ }
182
+ return { code, externals };
183
+ }
184
+ /**
185
+ * Process a single local file: transform, rewrite requires, extract deps,
186
+ * update cache and graph. Returns the list of npm deps found.
187
+ */
188
+ processFile(filePath) {
189
+ // Asset files get a stub module that exports the filename (or a real URL for external assets)
190
+ if (this.resolver.isAssetFile(filePath)) {
191
+ if (this.fs.isExternalAsset(filePath) && this.config.assetPublicPath) {
192
+ const assetUrl = this.config.assetPublicPath + filePath;
193
+ this.moduleMap[filePath] = "module.exports = { uri: " + JSON.stringify(assetUrl) + " };";
194
+ }
195
+ else {
196
+ this.moduleMap[filePath] = "module.exports = " + JSON.stringify(filePath) + ";";
197
+ }
198
+ return { localDeps: [], npmDeps: [] };
199
+ }
200
+ const source = this.fs.read(filePath);
201
+ if (!source) {
202
+ throw new Error("File not found: " + filePath);
203
+ }
204
+ const sourceHash = hashString(source);
205
+ // Check cache validity
206
+ if (this.cache.isValid(filePath, sourceHash)) {
207
+ const cached = this.cache.getModule(filePath);
208
+ this.moduleMap[filePath] = cached.rewrittenCode;
209
+ if (cached.sourceMap)
210
+ this.sourceMapMap[filePath] = cached.sourceMap;
211
+ return {
212
+ localDeps: cached.resolvedLocalDeps,
213
+ npmDeps: cached.npmDeps,
214
+ };
215
+ }
216
+ // Transform
217
+ const { code: transformed, sourceMap } = this.runTransform(filePath, source);
218
+ if (sourceMap)
219
+ this.sourceMapMap[filePath] = sourceMap;
220
+ // Rewrite requires
221
+ const rewritten = rewriteRequires(transformed, filePath, this.makeResolveTarget(filePath));
222
+ // Extract deps from rewritten code (has absolute paths for local deps)
223
+ const rawDeps = findRequires(transformed);
224
+ const allDeps = findRequires(rewritten);
225
+ const localDeps = [];
226
+ const npmDeps = [];
227
+ for (const dep of allDeps) {
228
+ if (this.resolver.isNpmPackage(dep)) {
229
+ npmDeps.push(dep);
230
+ }
231
+ else {
232
+ localDeps.push(dep);
233
+ }
234
+ }
235
+ // Update cache
236
+ this.cache.setModule(filePath, {
237
+ sourceHash,
238
+ transformedCode: transformed,
239
+ rewrittenCode: rewritten,
240
+ rawDeps,
241
+ resolvedLocalDeps: localDeps,
242
+ npmDeps,
243
+ sourceMap,
244
+ });
245
+ // Update graph
246
+ this.graph.setModule(filePath, localDeps, npmDeps);
247
+ // Update module map
248
+ this.moduleMap[filePath] = rewritten;
249
+ return { localDeps, npmDeps };
250
+ }
251
+ /**
252
+ * Walk the dependency tree starting from a file, processing all reachable modules.
253
+ *
254
+ * Returns all files that were visited and processed, so callers can include
255
+ * them in HMR updatedModules. Without this, only the direct dependency would
256
+ * be sent to the iframe — transitive deps (e.g. hooks/index.ts → hooks/useAuth.ts)
257
+ * would be missing from the HMR payload, causing "Module not found" at runtime.
258
+ *
259
+ * Files that don't exist yet on the VFS are silently skipped. During AI streaming,
260
+ * a barrel file (e.g. seeds/index.ts) may reference a sibling (e.g. seeds/users.ts)
261
+ * that hasn't been created yet. Without this guard, processFile() would throw
262
+ * "File not found" and crash the entire rebuild. The missing file will be picked
263
+ * up on a subsequent rebuild once it's created.
264
+ */
265
+ walkDeps(startFile, npmPackagesNeeded) {
266
+ const visited = new Set();
267
+ const queue = [startFile];
268
+ while (queue.length > 0) {
269
+ const filePath = queue.shift();
270
+ if (visited.has(filePath))
271
+ continue;
272
+ if (!this.fs.exists(filePath))
273
+ continue;
274
+ visited.add(filePath);
275
+ const { localDeps, npmDeps } = this.processFile(filePath);
276
+ for (const dep of npmDeps) {
277
+ npmPackagesNeeded.add(dep);
278
+ }
279
+ for (const dep of localDeps) {
280
+ if (!visited.has(dep)) {
281
+ queue.push(dep);
282
+ }
283
+ }
284
+ }
285
+ return [...visited];
286
+ }
287
+ /** Fetch all npm packages that aren't already cached */
288
+ async fetchNpmPackages(npmPackagesNeeded) {
289
+ const versions = this.packageVersions;
290
+ const toFetch = [];
291
+ for (const name of npmPackagesNeeded) {
292
+ const specifier = this.resolveNpmSpecifier(name, versions);
293
+ if (!this.cache.hasNpmPackage(specifier)) {
294
+ toFetch.push({ name, specifier });
295
+ }
296
+ else {
297
+ // Use cached version
298
+ this.moduleMap[name] = this.cache.getNpmPackage(specifier);
299
+ }
300
+ }
301
+ if (toFetch.length > 0) {
302
+ const results = await Promise.all(toFetch.map(({ specifier }) => this.fetchPackage(specifier)));
303
+ for (let i = 0; i < toFetch.length; i++) {
304
+ const { name, specifier } = toFetch[i];
305
+ const { code, externals } = results[i];
306
+ this.cache.setNpmPackage(specifier, code);
307
+ this.moduleMap[name] = code;
308
+ // Merge externals into transitive versions (don't overwrite existing entries)
309
+ for (const [dep, ver] of Object.entries(externals)) {
310
+ if (!this.transitiveDepsVersions[dep]) {
311
+ this.transitiveDepsVersions[dep] = ver;
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+ /** Emit the bundle using HMR runtime or standard IIFE */
318
+ emitBundle() {
319
+ const hmrEnabled = this.config.hmr?.enabled ?? false;
320
+ const reactRefresh = this.config.hmr?.reactRefresh ?? false;
321
+ let bundle;
322
+ let headerStr;
323
+ if (hmrEnabled) {
324
+ bundle = emitHmrBundle(this.moduleMap, this.entryFile, this.graph.getReverseDepsMap(), reactRefresh, this.config.env, this.config.routerShim);
325
+ headerStr =
326
+ buildBundlePreamble(this.config.env, this.config.routerShim) +
327
+ HMR_RUNTIME_TEMPLATE +
328
+ "({\n";
329
+ }
330
+ else {
331
+ // Fallback to standard IIFE (same as Bundler.emitBundle)
332
+ const preamble = buildBundlePreamble(this.config.env, this.config.routerShim);
333
+ const runtimeStr = "(function(modules) {\n" +
334
+ " var cache = {};\n" +
335
+ " function require(id) {\n" +
336
+ " if (cache[id]) return cache[id].exports;\n" +
337
+ " if (!modules[id]) throw new Error('Module not found: ' + id);\n" +
338
+ " var module = cache[id] = { exports: {} };\n" +
339
+ " modules[id].call(module.exports, module, module.exports, require);\n" +
340
+ " return module.exports;\n" +
341
+ " }\n" +
342
+ " require(" +
343
+ JSON.stringify(this.entryFile) +
344
+ ");\n" +
345
+ "})({\n";
346
+ headerStr = preamble + runtimeStr;
347
+ const moduleEntries = Object.keys(this.moduleMap)
348
+ .map((id) => {
349
+ return (JSON.stringify(id) +
350
+ ": function(module, exports, require) {\n" +
351
+ this.moduleMap[id] +
352
+ "\n}");
353
+ })
354
+ .join(",\n\n");
355
+ bundle = headerStr + moduleEntries + "\n});\n";
356
+ }
357
+ // Build and append combined source map
358
+ const inputs = this.buildSourceMapInputs(countNewlines(headerStr));
359
+ if (inputs.length > 0) {
360
+ bundle += inlineSourceMap(buildCombinedSourceMap(inputs)) + "\n";
361
+ }
362
+ return bundle;
363
+ }
364
+ /** Compute source map inputs with correct line offsets for each module */
365
+ buildSourceMapInputs(headerLineCount) {
366
+ let lineOffset = headerLineCount;
367
+ const inputs = [];
368
+ const ids = Object.keys(this.moduleMap);
369
+ for (let i = 0; i < ids.length; i++) {
370
+ const id = ids[i];
371
+ if (i > 0)
372
+ lineOffset += 2; // ",\n\n"
373
+ lineOffset += 1; // wrapper function line
374
+ if (this.sourceMapMap[id]) {
375
+ const sourceContent = this.fs.read(id);
376
+ if (sourceContent) {
377
+ inputs.push({
378
+ sourceFile: id,
379
+ sourceContent,
380
+ map: this.sourceMapMap[id],
381
+ generatedLineOffset: lineOffset,
382
+ });
383
+ }
384
+ }
385
+ lineOffset += countNewlines(this.moduleMap[id]);
386
+ lineOffset += 1; // "\n}"
387
+ }
388
+ return inputs;
389
+ }
390
+ /** Initial full build */
391
+ async build(entryFile) {
392
+ const startTime = performance.now();
393
+ this.entryFile = entryFile;
394
+ this.moduleMap = {};
395
+ this.sourceMapMap = {};
396
+ this.packageVersions = this.getPackageVersions();
397
+ const npmPackagesNeeded = new Set();
398
+ this.walkDeps(entryFile, npmPackagesNeeded);
399
+ // Process module aliases: swap sources for targets in the fetch list
400
+ const aliases = this.getModuleAliases();
401
+ for (const [from, to] of Object.entries(aliases)) {
402
+ npmPackagesNeeded.delete(from);
403
+ npmPackagesNeeded.add(to);
404
+ }
405
+ // Collect shims: these replace npm packages with inline code
406
+ const shims = this.getShimModules();
407
+ for (const name of Object.keys(shims)) {
408
+ npmPackagesNeeded.delete(name);
409
+ }
410
+ // React Refresh runtime must be in the module map for the HMR runtime to require() it
411
+ if (this.config.hmr?.reactRefresh) {
412
+ npmPackagesNeeded.add("react-refresh/runtime");
413
+ }
414
+ await this.fetchNpmPackages(npmPackagesNeeded);
415
+ // Resolve transitive npm deps (subpath requires like react-dom/client)
416
+ const skipNames = new Set([...Object.keys(aliases), ...Object.keys(shims)]);
417
+ let newDeps = this.findTransitiveNpmDeps(skipNames);
418
+ while (newDeps.size > 0) {
419
+ await this.fetchNpmPackages(newDeps);
420
+ newDeps = this.findTransitiveNpmDeps(skipNames);
421
+ }
422
+ // Inject alias shim modules
423
+ for (const [from, to] of Object.entries(aliases)) {
424
+ this.moduleMap[from] = 'module.exports = require("' + to + '");';
425
+ }
426
+ // Inject inline shim modules
427
+ for (const [name, code] of Object.entries(shims)) {
428
+ this.moduleMap[name] = code;
429
+ }
430
+ const bundle = this.emitBundle();
431
+ const buildTime = performance.now() - startTime;
432
+ return {
433
+ bundle,
434
+ hmrUpdate: null,
435
+ type: "full",
436
+ rebuiltModules: Object.keys(this.moduleMap).filter((id) => !this.resolver.isNpmPackage(id)),
437
+ removedModules: [],
438
+ buildTime,
439
+ };
440
+ }
441
+ /** Incremental rebuild based on file changes */
442
+ async rebuild(changes) {
443
+ const startTime = performance.now();
444
+ if (!this.entryFile) {
445
+ throw new Error("Must call build() before rebuild()");
446
+ }
447
+ // Check if we need a full rebuild
448
+ const packageJsonChange = changes.find((c) => c.path === "/package.json");
449
+ if (packageJsonChange && packageJsonChange.type !== "delete") {
450
+ const newVersions = this.getPackageVersions();
451
+ const oldVersions = this.packageVersions;
452
+ const depsChanged = JSON.stringify(newVersions) !== JSON.stringify(oldVersions);
453
+ if (depsChanged) {
454
+ // Full rebuild: invalidate all npm caches and transitive version map
455
+ this.cache.invalidateNpmPackages();
456
+ this.transitiveDepsVersions = {};
457
+ this.packageVersions = newVersions;
458
+ return this.build(this.entryFile);
459
+ }
460
+ }
461
+ // Check if entry file was deleted
462
+ const entryDeleted = changes.some((c) => c.path === this.entryFile && c.type === "delete");
463
+ if (entryDeleted) {
464
+ return this.build(this.entryFile);
465
+ }
466
+ // Incremental rebuild
467
+ const rebuiltModules = [];
468
+ const removedModules = [];
469
+ const filesToReprocess = new Set();
470
+ const npmPackagesNeeded = new Set();
471
+ // Collect all npm packages already in the module map
472
+ for (const id of Object.keys(this.moduleMap)) {
473
+ if (this.resolver.isNpmPackage(id)) {
474
+ npmPackagesNeeded.add(id);
475
+ }
476
+ }
477
+ // Snapshot which npm packages exist before this rebuild so we can detect
478
+ // newly-introduced ones. New packages must be injected into HMR updatedModules
479
+ // so the running iframe's `modules` closure has them before re-execution —
480
+ // otherwise require('new-package') throws "Module not found".
481
+ const preRebuildNpmIds = new Set(npmPackagesNeeded);
482
+ // Phase 1: Classify changes and collect files to reprocess
483
+ for (const change of changes) {
484
+ if (change.path === "/package.json")
485
+ continue;
486
+ if (change.type === "delete") {
487
+ // Remove from graph, cache, and module map
488
+ const dependents = this.graph.getDependents(change.path);
489
+ this.graph.removeModule(change.path);
490
+ this.cache.invalidateModule(change.path);
491
+ delete this.moduleMap[change.path];
492
+ delete this.sourceMapMap[change.path];
493
+ removedModules.push(change.path);
494
+ // Dependents need reprocessing (their require target is gone)
495
+ for (const dep of dependents) {
496
+ filesToReprocess.add(dep);
497
+ }
498
+ }
499
+ else {
500
+ // Create or update: only reprocess the changed file itself.
501
+ // Dependents don't need re-transformation -- the HMR runtime
502
+ // handles re-execution by walking accept boundaries.
503
+ this.cache.invalidateModule(change.path);
504
+ filesToReprocess.add(change.path);
505
+ }
506
+ }
507
+ // Phase 2: Reprocess each affected file
508
+ for (const filePath of filesToReprocess) {
509
+ if (!this.fs.exists(filePath))
510
+ continue;
511
+ const { localDeps, npmDeps } = this.processFile(filePath);
512
+ rebuiltModules.push(filePath);
513
+ for (const dep of npmDeps) {
514
+ npmPackagesNeeded.add(dep);
515
+ }
516
+ // Walk any new local deps that aren't yet in the graph.
517
+ // Include ALL transitive deps in rebuiltModules so they appear in the
518
+ // HMR updatedModules payload. Previously only the direct dep was pushed,
519
+ // so transitive deps (e.g. barrel re-exports) were missing from HMR.
520
+ for (const dep of localDeps) {
521
+ if (!this.graph.hasModule(dep) && this.fs.exists(dep)) {
522
+ const walked = this.walkDeps(dep, npmPackagesNeeded);
523
+ for (const w of walked) {
524
+ rebuiltModules.push(w);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ // Process module aliases for incremental rebuilds
530
+ const aliases = this.getModuleAliases();
531
+ for (const [from, to] of Object.entries(aliases)) {
532
+ npmPackagesNeeded.delete(from);
533
+ npmPackagesNeeded.add(to);
534
+ }
535
+ // Collect shims
536
+ const shims = this.getShimModules();
537
+ for (const name of Object.keys(shims)) {
538
+ npmPackagesNeeded.delete(name);
539
+ }
540
+ // Ensure react-refresh/runtime stays in the module map
541
+ if (this.config.hmr?.reactRefresh) {
542
+ npmPackagesNeeded.add("react-refresh/runtime");
543
+ }
544
+ // Phase 3: Fetch any new npm packages + transitive deps
545
+ await this.fetchNpmPackages(npmPackagesNeeded);
546
+ const skipNames = new Set([...Object.keys(aliases), ...Object.keys(shims)]);
547
+ let newDeps = this.findTransitiveNpmDeps(skipNames);
548
+ while (newDeps.size > 0) {
549
+ await this.fetchNpmPackages(newDeps);
550
+ newDeps = this.findTransitiveNpmDeps(skipNames);
551
+ }
552
+ // Inject alias shim modules
553
+ for (const [from, to] of Object.entries(aliases)) {
554
+ this.moduleMap[from] = 'module.exports = require("' + to + '");';
555
+ }
556
+ // Inject inline shim modules
557
+ for (const [name, code] of Object.entries(shims)) {
558
+ this.moduleMap[name] = code;
559
+ }
560
+ // Phase 4: Orphan cleanup
561
+ const orphans = this.graph.findOrphans(this.entryFile);
562
+ for (const orphan of orphans) {
563
+ this.graph.removeModule(orphan);
564
+ this.cache.invalidateModule(orphan);
565
+ delete this.moduleMap[orphan];
566
+ delete this.sourceMapMap[orphan];
567
+ removedModules.push(orphan);
568
+ }
569
+ // Phase 5: Emit result
570
+ const bundle = this.emitBundle();
571
+ const buildTime = performance.now() - startTime;
572
+ // Build HMR update
573
+ let hmrUpdate = null;
574
+ const hmrEnabled = this.config.hmr?.enabled ?? false;
575
+ if (hmrEnabled && rebuiltModules.length > 0) {
576
+ const entryChanged = rebuiltModules.includes(this.entryFile);
577
+ if (entryChanged) {
578
+ hmrUpdate = {
579
+ updatedModules: {},
580
+ removedModules,
581
+ requiresReload: true,
582
+ reloadReason: "Entry file changed",
583
+ };
584
+ }
585
+ else {
586
+ const updatedModules = {};
587
+ // new Function('module','exports','require', code) wraps with:
588
+ // function anonymous(module,exports,require\n) {\n<code>\n}
589
+ // That's 2 extra lines before code starts
590
+ const NEW_FUNCTION_LINES = 2;
591
+ for (const id of rebuiltModules) {
592
+ if (this.moduleMap[id] !== undefined) {
593
+ let code = this.moduleMap[id];
594
+ // Append per-module inline source map for HMR
595
+ const sm = this.sourceMapMap[id];
596
+ if (sm) {
597
+ const sourceContent = this.fs.read(id);
598
+ if (sourceContent) {
599
+ code +=
600
+ "\n" +
601
+ inlineSourceMap({
602
+ version: 3,
603
+ sources: [id],
604
+ sourcesContent: [sourceContent],
605
+ names: sm.names || [],
606
+ mappings: ";".repeat(NEW_FUNCTION_LINES) + sm.mappings,
607
+ });
608
+ }
609
+ }
610
+ code += "\n//# sourceURL=" + id;
611
+ updatedModules[id] = code;
612
+ }
613
+ }
614
+ // Inject npm packages that are new since the last bundle the iframe loaded.
615
+ // The HMR runtime's Phase 3 (module factory replacement) will add them to
616
+ // `modules` before re-executing changed files, preventing "Module not found"
617
+ // when a newly-generated component imports a package for the first time.
618
+ for (const [id, code] of Object.entries(this.moduleMap)) {
619
+ if (this.resolver.isNpmPackage(id) && !preRebuildNpmIds.has(id)) {
620
+ updatedModules[id] = code;
621
+ }
622
+ }
623
+ hmrUpdate = {
624
+ updatedModules,
625
+ removedModules,
626
+ requiresReload: false,
627
+ reverseDepsMap: this.graph.getReverseDepsMap(),
628
+ };
629
+ }
630
+ }
631
+ return {
632
+ bundle,
633
+ hmrUpdate,
634
+ type: "incremental",
635
+ rebuiltModules,
636
+ removedModules,
637
+ buildTime,
638
+ };
639
+ }
640
+ /** Update the virtual filesystem */
641
+ updateFS(fs) {
642
+ this.fs = fs;
643
+ const paths = IncrementalBundler.readTsconfigPaths(fs);
644
+ this.resolver = new Resolver(fs, { ...this.config.resolver, ...(paths && { paths }) });
645
+ }
646
+ }
@@ -0,0 +1,11 @@
1
+ export { Bundler } from "./bundler.js";
2
+ export { IncrementalBundler } from "./incremental-bundler.js";
3
+ export { VirtualFS } from "./fs.js";
4
+ export { Resolver } from "./resolver.js";
5
+ export { DependencyGraph } from "./dependency-graph.js";
6
+ export { ModuleCache } from "./module-cache.js";
7
+ export { typescriptTransformer } from "./transforms/typescript.js";
8
+ export { reactRefreshTransformer, createReactRefreshTransformer, } from "./transforms/react-refresh.js";
9
+ export type { RawSourceMap } from "./source-map.js";
10
+ export { createDataBxPathPlugin } from "./plugins/data-bx-path.js";
11
+ export type { FileEntry, FileMap, ModuleMap, TransformResult, TransformParams, Transformer, ResolverConfig, BundlerPlugin, BundlerConfig, FileChange, ContentChange, HmrUpdate, IncrementalBuildResult, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { Bundler } from "./bundler.js";
2
+ export { IncrementalBundler } from "./incremental-bundler.js";
3
+ export { VirtualFS } from "./fs.js";
4
+ export { Resolver } from "./resolver.js";
5
+ export { DependencyGraph } from "./dependency-graph.js";
6
+ export { ModuleCache } from "./module-cache.js";
7
+ export { typescriptTransformer } from "./transforms/typescript.js";
8
+ export { reactRefreshTransformer, createReactRefreshTransformer, } from "./transforms/react-refresh.js";
9
+ export { createDataBxPathPlugin } from "./plugins/data-bx-path.js";
@@ -0,0 +1,22 @@
1
+ import type { RawSourceMap } from "./source-map.js";
2
+ export interface CachedModule {
3
+ sourceHash: string;
4
+ transformedCode: string;
5
+ rewrittenCode: string;
6
+ rawDeps: string[];
7
+ resolvedLocalDeps: string[];
8
+ npmDeps: string[];
9
+ sourceMap?: RawSourceMap;
10
+ }
11
+ export declare class ModuleCache {
12
+ private modules;
13
+ private npmPackages;
14
+ getModule(id: string): CachedModule | undefined;
15
+ setModule(id: string, data: CachedModule): void;
16
+ invalidateModule(id: string): void;
17
+ isValid(id: string, currentSourceHash: string): boolean;
18
+ getNpmPackage(specifier: string): string | undefined;
19
+ setNpmPackage(specifier: string, code: string): void;
20
+ hasNpmPackage(specifier: string): boolean;
21
+ invalidateNpmPackages(): void;
22
+ }