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.
- package/dist/bundler.d.ts +34 -0
- package/dist/bundler.js +320 -0
- package/dist/dependency-graph.d.ts +22 -0
- package/dist/dependency-graph.js +128 -0
- package/dist/fs.d.ts +20 -0
- package/dist/fs.js +107 -0
- package/dist/hmr-runtime.d.ts +14 -0
- package/dist/hmr-runtime.js +231 -0
- package/dist/incremental-bundler.d.ts +71 -0
- package/dist/incremental-bundler.js +646 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/module-cache.d.ts +22 -0
- package/dist/module-cache.js +31 -0
- package/dist/plugins/data-bx-path.d.ts +2 -0
- package/dist/plugins/data-bx-path.js +197 -0
- package/dist/resolver.d.ts +18 -0
- package/dist/resolver.js +84 -0
- package/dist/source-map.d.ts +36 -0
- package/dist/source-map.js +186 -0
- package/dist/transforms/react-refresh.d.ts +5 -0
- package/dist/transforms/react-refresh.js +92 -0
- package/dist/transforms/typescript.d.ts +2 -0
- package/dist/transforms/typescript.js +20 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.js +208 -0
- package/package.json +22 -0
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|