@akanjs/devkit 3.0.0-beta.24 → 3.0.0-beta.25
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/DEV_RUNTIME_KNOBS.md +37 -22
- package/akanApp/akanApp.host.ts +34 -1
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +25 -0
- package/akanApp/devHostPolicy.ts +27 -2
- package/cloud/globalConfig.ts +3 -17
- package/csrE2e/csrE2eHarness.fixture.ts +66 -11
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/fileSys.ts +16 -1
- package/frontendBuild/buildRouteClient.test.ts +61 -0
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +5 -1
- package/frontendBuild/clientEntryDiscovery.test.ts +37 -0
- package/frontendBuild/clientEntryDiscovery.ts +10 -5
- package/frontendBuild/csrArtifactBuilder.ts +3 -6
- package/frontendBuild/csrDevArtifactWriter.test.ts +143 -0
- package/frontendBuild/csrDevArtifactWriter.ts +158 -24
- package/frontendBuild/csrDevBundler.ts +44 -205
- package/frontendBuild/csrDevModuleCompiler.ts +51 -9
- package/frontendBuild/csrDevPatcher.test.ts +340 -0
- package/frontendBuild/csrDevPatcher.ts +374 -0
- package/frontendBuild/csrDevPaths.ts +60 -0
- package/frontendBuild/csrDevResolver.test.ts +165 -0
- package/frontendBuild/csrDevResolver.ts +70 -18
- package/frontendBuild/csrDevTypes.ts +14 -1
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCompiler.ts +2 -4
- package/frontendBuild/devRegistryBundler.ts +177 -0
- package/frontendBuild/hmrWatcher.test.ts +51 -0
- package/frontendBuild/hmrWatcher.ts +17 -3
- package/frontendBuild/index.ts +3 -0
- package/frontendBuild/pagesBundleBuilder.ts +24 -1
- package/frontendBuild/pagesEntrySourceGenerator.ts +9 -2
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +79 -18
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/ssrDevBundler.ts +91 -0
- package/incrementalBuilder/buildBatch.proc.ts +94 -16
- package/incrementalBuilder/buildBatchProtocol.ts +6 -3
- package/incrementalBuilder/buildBatchRunner.test.ts +31 -0
- package/incrementalBuilder/buildBatchRunner.ts +17 -8
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.host.ts +32 -3
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +129 -2
- package/incrementalBuilder/incrementalBuilder.proc.ts +518 -52
- package/integration/devStability.integration.test.ts +18 -7
- package/package.json +2 -2
- package/transforms/rscUseClientTransform.ts +7 -0
- package/transforms/transforms.test.ts +13 -0
- package/transforms/useClientBundlePlugin.ts +4 -1
|
@@ -1,66 +1,34 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import { mkdir } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { CSR_DEV_DIRNAME,
|
|
5
|
-
import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
|
|
3
|
+
import { CSR_DEV_DIRNAME, CSR_DEV_ROUTE_PREFIX } from "akanjs/server/hmr/csrDevManifest";
|
|
6
4
|
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
7
5
|
import type { App } from "../commandDecorators";
|
|
6
|
+
import { AsyncDefaultExportDetector } from "../transforms/asyncDefaultExportDetector";
|
|
8
7
|
import { bundleDefine } from "./bundleDefine";
|
|
9
|
-
import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
10
|
-
import { CsrDevArtifactWriter } from "./csrDevArtifactWriter";
|
|
11
|
-
import { CsrDevModuleCompiler } from "./csrDevModuleCompiler";
|
|
12
8
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
9
|
+
import type { CsrDevContext } from "./csrDevTypes";
|
|
10
|
+
import { CsrEntryFiles } from "./csrEntryFiles";
|
|
11
|
+
import { DevRegistryBundler } from "./devRegistryBundler";
|
|
15
12
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
reload: boolean;
|
|
20
|
-
reason?: string;
|
|
21
|
-
patchUrl?: string;
|
|
22
|
-
changedIds: string[];
|
|
23
|
-
moduleCount: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
//* Dev-only CSR as a module registry. The build worker is a fresh process per save, so the graph, every module's
|
|
27
|
-
//* factory and the resolutions live on disk under `.akan/artifact/csr-dev`.
|
|
28
|
-
export class CsrDevBundler {
|
|
14
|
+
//* Dev-only CSR as a module registry, under `.akan/artifact/csr-dev`: one entry per basePath boots every page.
|
|
15
|
+
export class CsrDevBundler extends DevRegistryBundler {
|
|
29
16
|
static readonly #formatVersion = 3;
|
|
30
|
-
readonly
|
|
31
|
-
|
|
32
|
-
readonly #
|
|
33
|
-
readonly #entryDir: string;
|
|
34
|
-
readonly #writer: CsrDevArtifactWriter;
|
|
17
|
+
readonly reloadsOnEntryChange = true;
|
|
18
|
+
//? Detecting a page's async default parses it with TypeScript: on apps/akan every page each save cost ~100ms.
|
|
19
|
+
readonly #asyncDefaults = new Map<string, { mtimeMs: number; detected: Promise<boolean> }>();
|
|
35
20
|
|
|
36
21
|
constructor(app: App) {
|
|
37
|
-
|
|
38
|
-
this.#paths = new CsrDevPaths(app.workspace.workspaceRoot);
|
|
39
|
-
this.#outDir = path.join(app.cwdPath, ".akan/artifact", CSR_DEV_DIRNAME);
|
|
40
|
-
this.#entryDir = path.join(app.cwdPath, ".akan/generated", CSR_DEV_DIRNAME);
|
|
41
|
-
this.#writer = new CsrDevArtifactWriter(this.#outDir);
|
|
22
|
+
super(app, { dirName: CSR_DEV_DIRNAME, routePrefix: CSR_DEV_ROUTE_PREFIX, library: false });
|
|
42
23
|
}
|
|
43
24
|
|
|
44
|
-
async
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const graph = await this.#writer.readJson<CsrDevGraph>("graph.json");
|
|
49
|
-
const generation = (manifest?.generation ?? 0) + 1;
|
|
50
|
-
if (!manifest || !graph || graph.version !== 1 || graph.configKey !== context.configKey)
|
|
51
|
-
return await this.#fullBuild(context, generation, manifest ? "the dev bundle config changed" : "first build");
|
|
52
|
-
const metadataFile = changedFiles.find(isAkanRuntimeMetadataFile);
|
|
53
|
-
// `lib/useClient.ts` inlines signal and dictionary metadata through a macro at build time.
|
|
54
|
-
if (metadataFile) return await this.#fullBuild(context, generation, `${path.basename(metadataFile)} changed`);
|
|
55
|
-
return await this.#incrementalBuild(context, graph, manifest, changedFiles, generation);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async #context(): Promise<CsrDevContext> {
|
|
59
|
-
const akanConfig = await this.#app.getConfig();
|
|
60
|
-
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
|
|
25
|
+
async context(): Promise<CsrDevContext | null> {
|
|
26
|
+
const akanConfig = await this.app.getConfig();
|
|
27
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.app, await this.app.getPageKeys());
|
|
28
|
+
if (pageEntries.length === 0) return null;
|
|
61
29
|
const basePaths = [...akanConfig.basePaths];
|
|
62
30
|
const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
|
|
63
|
-
const define = bundleDefine(this
|
|
31
|
+
const define = bundleDefine(this.app, "start", "csr");
|
|
64
32
|
const optimizeImports = [...akanConfig.optimizeImports];
|
|
65
33
|
const configKey = Bun.hash(
|
|
66
34
|
JSON.stringify([CsrDevBundler.#formatVersion, Bun.version, define, optimizeImports, htmlBasePaths]),
|
|
@@ -76,152 +44,21 @@ export class CsrDevBundler {
|
|
|
76
44
|
};
|
|
77
45
|
}
|
|
78
46
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const bases = [
|
|
82
|
-
this.#app.cwdPath,
|
|
83
|
-
this.#paths.root,
|
|
84
|
-
...(akanWebkit ? [path.dirname(path.dirname(akanWebkit))] : []),
|
|
85
|
-
];
|
|
86
|
-
for (const base of bases) {
|
|
87
|
-
const resolved = CsrDevPaths.tryResolve("react-refresh/runtime", base);
|
|
88
|
-
if (resolved) return CsrDevPaths.realpath(resolved);
|
|
89
|
-
}
|
|
90
|
-
throw new Error(`[csr-dev] react-refresh/runtime is not resolvable from ${bases.join(", ")}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async #fullBuild(context: CsrDevContext, generation: number, reason: string): Promise<CsrDevUpdate> {
|
|
94
|
-
await this.#writer.reset();
|
|
95
|
-
const entries = await this.#writeEntries(context);
|
|
96
|
-
const entryFiles = Object.values(entries.files);
|
|
97
|
-
const resolver = new CsrDevResolver({ paths: this.#paths, context, entryFiles });
|
|
98
|
-
await resolver.prepass();
|
|
99
|
-
const compiled = await this.#compiler(context, resolver).compile([...entryFiles, context.refreshFile], new Set());
|
|
100
|
-
const graph: CsrDevGraph = {
|
|
101
|
-
version: 1,
|
|
102
|
-
configKey: context.configKey,
|
|
103
|
-
refresh: this.#paths.idOf(context.refreshFile),
|
|
104
|
-
entries: this.#entryIds(entries.files),
|
|
105
|
-
modules: {},
|
|
106
|
-
resolution: {},
|
|
107
|
-
pending: [],
|
|
108
|
-
};
|
|
109
|
-
this.#merge(graph, resolver, compiled);
|
|
110
|
-
await this.#writer.writeModules(compiled);
|
|
111
|
-
const vendorFile = await this.#writer.writeVendor(graph);
|
|
112
|
-
await this.#writer.writeApp(graph, generation);
|
|
113
|
-
await this.#writer.writeState(graph, { version: 1, generation, vendorFile, entries: graph.entries });
|
|
114
|
-
await this.#writer.prune(vendorFile, generation);
|
|
115
|
-
return { generation, reload: true, reason, changedIds: [], moduleCount: Object.keys(graph.modules).length };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async #incrementalBuild(
|
|
119
|
-
context: CsrDevContext,
|
|
120
|
-
graph: CsrDevGraph,
|
|
121
|
-
manifest: CsrDevManifest,
|
|
122
|
-
changedFiles: string[],
|
|
123
|
-
generation: number,
|
|
124
|
-
): Promise<CsrDevUpdate | null> {
|
|
125
|
-
const entries = await this.#writeEntries(context);
|
|
126
|
-
const entryIds = this.#entryIds(entries.files);
|
|
127
|
-
await this.#forgetDeletedModules(graph);
|
|
128
|
-
const changed = await this.#changedModules(graph, changedFiles);
|
|
129
|
-
for (const file of entries.changed) changed.add(this.#paths.idOf(file));
|
|
130
|
-
const routesMoved = entries.changed.length > 0 || JSON.stringify(entryIds) !== JSON.stringify(graph.entries);
|
|
131
|
-
if (changed.size === 0) return null;
|
|
132
|
-
|
|
133
|
-
const resolver = new CsrDevResolver({
|
|
134
|
-
paths: this.#paths,
|
|
135
|
-
context,
|
|
136
|
-
entryFiles: Object.values(entries.files),
|
|
137
|
-
resolution: graph.resolution,
|
|
138
|
-
});
|
|
139
|
-
const known = new Set(Object.keys(graph.modules).map((id) => this.#paths.fileOf(id)));
|
|
140
|
-
let compiled: CsrDevCompiledModule[];
|
|
141
|
-
try {
|
|
142
|
-
compiled = await this.#compiler(context, resolver).compile(
|
|
143
|
-
[...changed].map((id) => this.#paths.fileOf(id)),
|
|
144
|
-
known,
|
|
145
|
-
);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
graph.pending = [...changed];
|
|
148
|
-
await this.#writer.writeJson("graph.json", graph);
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
const vendorJoined = compiled.some((module) => module.vendor && !graph.modules[module.id]);
|
|
152
|
-
this.#merge(graph, resolver, compiled);
|
|
153
|
-
graph.entries = entryIds;
|
|
154
|
-
await this.#writer.writeModules(compiled);
|
|
155
|
-
const vendorFile = vendorJoined ? await this.#writer.writeVendor(graph) : manifest.vendorFile;
|
|
156
|
-
await this.#writer.writeApp(graph, generation);
|
|
157
|
-
const changedIds = compiled.filter((module) => !module.vendor).map((module) => module.id);
|
|
158
|
-
const constantId = changedIds.find((id) => id.endsWith(".constant.ts"));
|
|
159
|
-
// A model class swapped under live store state would mix old and new instances; reload instead.
|
|
160
|
-
const reason = vendorJoined
|
|
161
|
-
? "an npm module joined the graph"
|
|
162
|
-
: routesMoved
|
|
163
|
-
? "the route table changed"
|
|
164
|
-
: constantId
|
|
165
|
-
? `${path.basename(constantId)} changed`
|
|
166
|
-
: undefined;
|
|
167
|
-
const patchUrl = reason
|
|
168
|
-
? undefined
|
|
169
|
-
: await this.#writer.writePatch(
|
|
170
|
-
generation,
|
|
171
|
-
compiled.filter((module) => !module.vendor),
|
|
172
|
-
);
|
|
173
|
-
await this.#writer.writeState(graph, { ...manifest, generation, vendorFile, entries: entryIds });
|
|
174
|
-
await this.#writer.prune(vendorFile, generation);
|
|
175
|
-
return {
|
|
176
|
-
generation,
|
|
177
|
-
reload: !!reason,
|
|
178
|
-
reason,
|
|
179
|
-
patchUrl,
|
|
180
|
-
changedIds,
|
|
181
|
-
moduleCount: Object.keys(graph.modules).length,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async #changedModules(graph: CsrDevGraph, changedFiles: string[]): Promise<Set<string>> {
|
|
186
|
-
const changed = new Set<string>(graph.pending.filter((id) => graph.modules[id]));
|
|
187
|
-
for (const file of changedFiles) {
|
|
188
|
-
const id = this.#paths.idOf(file);
|
|
189
|
-
if (graph.modules[id]) changed.add(id);
|
|
190
|
-
}
|
|
191
|
-
// Stat, not trust: a save made while CSR was unarmed, or a build that failed, never reached this graph. The hash
|
|
192
|
-
// settles it because route discovery rewrites the generated layout wrappers, touching them on every call.
|
|
193
|
-
for (const [id, module] of Object.entries(graph.modules)) {
|
|
194
|
-
if (module.vendor || changed.has(id)) continue;
|
|
195
|
-
const file = this.#paths.fileOf(id);
|
|
196
|
-
if (CsrDevPaths.mtimeOf(file) === module.mtimeMs) continue;
|
|
197
|
-
if ((await CsrDevPaths.hashOf(file)) !== module.hash) changed.add(id);
|
|
198
|
-
}
|
|
199
|
-
return changed;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// A deleted module leaves the graph instead of being rebuilt: rebuilding a missing file fails every save after it.
|
|
203
|
-
// An importer that still names it fails on its own rebuild, with the resolution error that says why.
|
|
204
|
-
async #forgetDeletedModules(graph: CsrDevGraph): Promise<void> {
|
|
205
|
-
const deleted = Object.keys(graph.modules).filter(
|
|
206
|
-
(id) => !graph.modules[id]?.vendor && !fs.existsSync(this.#paths.fileOf(id)),
|
|
207
|
-
);
|
|
208
|
-
for (const id of deleted) delete graph.modules[id];
|
|
209
|
-
await this.#writer.forgetModules(deleted);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async #writeEntries(context: CsrDevContext): Promise<{ files: Record<string, string>; changed: string[] }> {
|
|
213
|
-
await mkdir(this.#entryDir, { recursive: true });
|
|
47
|
+
async writeEntries(context: CsrDevContext): Promise<{ files: Record<string, string>; changed: string[] }> {
|
|
48
|
+
await mkdir(this.entryDir, { recursive: true });
|
|
214
49
|
const files: Record<string, string> = {};
|
|
215
50
|
const changed: string[] = [];
|
|
216
51
|
for (const basePath of context.htmlBasePaths) {
|
|
217
|
-
const file = path.join(this
|
|
218
|
-
const entryPages =
|
|
219
|
-
const generator = new PagesEntrySourceGenerator(entryPages
|
|
220
|
-
|
|
52
|
+
const file = path.join(this.entryDir, CsrEntryFiles.entryFilename(basePath));
|
|
53
|
+
const entryPages = CsrEntryFiles.pageEntriesForBasePath(context.pageEntries, basePath, context.basePaths);
|
|
54
|
+
const generator = new PagesEntrySourceGenerator(entryPages, {
|
|
55
|
+
isAsyncDefault: (moduleAbsPath) => this.#isAsyncDefault(moduleAbsPath),
|
|
56
|
+
});
|
|
57
|
+
const pages = await generator.generateStatic({ fromDir: this.entryDir });
|
|
221
58
|
const hot = await generator.generateHotReplace({
|
|
222
|
-
fromDir: this
|
|
223
|
-
ownerId: this
|
|
224
|
-
moduleIds: entryPages.map(({ moduleAbsPath }) => this
|
|
59
|
+
fromDir: this.entryDir,
|
|
60
|
+
ownerId: this.paths.idOf(file),
|
|
61
|
+
moduleIds: entryPages.map(({ moduleAbsPath }) => this.paths.idOf(moduleAbsPath)),
|
|
225
62
|
});
|
|
226
63
|
const source = `import * as akanWebkit from "akanjs/webkit";\n${pages}\nvoid akanWebkit.bootCsr(pages);\n${hot}`;
|
|
227
64
|
const unchanged = (await Bun.file(file).exists()) && (await Bun.file(file).text()) === source;
|
|
@@ -233,24 +70,26 @@ export class CsrDevBundler {
|
|
|
233
70
|
return { files, changed };
|
|
234
71
|
}
|
|
235
72
|
|
|
236
|
-
|
|
237
|
-
return
|
|
73
|
+
protected async rootFiles(entryFiles: string[]): Promise<string[]> {
|
|
74
|
+
return entryFiles;
|
|
238
75
|
}
|
|
239
76
|
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
...(module.helpers ? { helpers: module.helpers.hash } : {}),
|
|
248
|
-
};
|
|
249
|
-
graph.pending = [];
|
|
250
|
-
graph.resolution = resolver.serialize();
|
|
77
|
+
#isAsyncDefault(moduleAbsPath: string): Promise<boolean> {
|
|
78
|
+
const mtimeMs = CsrDevPaths.mtimeOf(moduleAbsPath);
|
|
79
|
+
const cached = this.#asyncDefaults.get(moduleAbsPath);
|
|
80
|
+
if (cached?.mtimeMs === mtimeMs) return cached.detected;
|
|
81
|
+
const detected = AsyncDefaultExportDetector.detect(moduleAbsPath);
|
|
82
|
+
this.#asyncDefaults.set(moduleAbsPath, { mtimeMs, detected });
|
|
83
|
+
return detected;
|
|
251
84
|
}
|
|
252
85
|
|
|
253
|
-
#
|
|
254
|
-
|
|
86
|
+
#resolveRefreshRuntime(): string {
|
|
87
|
+
const akanWebkit = CsrDevPaths.tryResolve("akanjs/webkit", this.app.cwdPath);
|
|
88
|
+
const bases = [this.app.cwdPath, this.paths.root, ...(akanWebkit ? [path.dirname(path.dirname(akanWebkit))] : [])];
|
|
89
|
+
for (const base of bases) {
|
|
90
|
+
const resolved = CsrDevPaths.tryResolve("react-refresh/runtime", base);
|
|
91
|
+
if (resolved) return CsrDevPaths.realpath(resolved);
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`[csr-dev] react-refresh/runtime is not resolvable from ${bases.join(", ")}`);
|
|
255
94
|
}
|
|
256
95
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { CSR_DEV_ROUTE_PREFIX } from "akanjs/server/hmr/csrDevManifest";
|
|
3
2
|
import type { BunPlugin } from "bun";
|
|
4
3
|
import type { App } from "../commandDecorators";
|
|
5
4
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
@@ -23,6 +22,21 @@ export interface CsrDevModuleCompilerOptions {
|
|
|
23
22
|
resolver: CsrDevResolver;
|
|
24
23
|
context: CsrDevContext;
|
|
25
24
|
outDir: string;
|
|
25
|
+
/** Where the dev server serves `outDir`: an imported asset's URL starts with it. */
|
|
26
|
+
routePrefix: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CsrDevCompileOptions {
|
|
30
|
+
/** Stop before building an npm module the graph does not know yet, and name it instead. */
|
|
31
|
+
refuseNewVendor?: boolean;
|
|
32
|
+
/** Stop at an import no recorded resolution answers, instead of running the resolution build (a whole-app build). */
|
|
33
|
+
refusePrepass?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CsrDevCompileResult {
|
|
37
|
+
modules: CsrDevCompiledModule[];
|
|
38
|
+
refusedVendors: string[];
|
|
39
|
+
unresolved: string[];
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
//* Every module becomes its own CJS factory: one Bun.build with each import external, so a save re-runs only the
|
|
@@ -40,21 +54,37 @@ export class CsrDevModuleCompiler {
|
|
|
40
54
|
readonly #resolver: CsrDevResolver;
|
|
41
55
|
readonly #context: CsrDevContext;
|
|
42
56
|
readonly #outDir: string;
|
|
57
|
+
readonly #routePrefix: string;
|
|
58
|
+
readonly #assets = new Set<string>();
|
|
43
59
|
|
|
44
|
-
constructor({ app, paths, resolver, context, outDir }: CsrDevModuleCompilerOptions) {
|
|
60
|
+
constructor({ app, paths, resolver, context, outDir, routePrefix }: CsrDevModuleCompilerOptions) {
|
|
45
61
|
this.#app = app;
|
|
46
62
|
this.#paths = paths;
|
|
47
63
|
this.#resolver = resolver;
|
|
48
64
|
this.#context = context;
|
|
49
65
|
this.#outDir = outDir;
|
|
66
|
+
this.#routePrefix = routePrefix;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every asset file this compiler wrote into the registry directory. */
|
|
70
|
+
get assets(): string[] {
|
|
71
|
+
return [...this.#assets];
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
async compile(
|
|
74
|
+
async compile(
|
|
75
|
+
startFiles: string[],
|
|
76
|
+
known: Set<string>,
|
|
77
|
+
{ refuseNewVendor = false, refusePrepass = false }: CsrDevCompileOptions = {},
|
|
78
|
+
): Promise<CsrDevCompileResult> {
|
|
53
79
|
const compiled = new Map<string, CsrDevCompiledModule>();
|
|
54
80
|
const seen = new Set([...known, ...startFiles]);
|
|
55
81
|
let frontier = [...new Set(startFiles)];
|
|
56
82
|
while (frontier.length > 0) {
|
|
83
|
+
const refusedVendors = refuseNewVendor ? frontier.filter((file) => CsrDevPaths.isVendorFile(file)) : [];
|
|
84
|
+
if (refusedVendors.length > 0) return { modules: [...compiled.values()], refusedVendors, unresolved: [] };
|
|
57
85
|
const round = await this.#compileRound(frontier);
|
|
86
|
+
if (round.misses.length > 0 && !this.#resolver.prepassDone && refusePrepass)
|
|
87
|
+
return { modules: [...compiled.values()], refusedVendors: [], unresolved: round.misses };
|
|
58
88
|
if (round.misses.length > 0 && !this.#resolver.prepassDone) {
|
|
59
89
|
this.#app.verbose(`[csr-dev] ${round.misses.length} unresolved import(s); rerunning the resolution build`);
|
|
60
90
|
await this.#resolver.prepass();
|
|
@@ -76,7 +106,7 @@ export class CsrDevModuleCompiler {
|
|
|
76
106
|
this.#app.verbose(
|
|
77
107
|
`[csr-dev] ${fallbacks.size} import(s) outside the browser build's graph resolved with Bun's runtime resolver: ${[...fallbacks].slice(0, 5).join(", ")}${fallbacks.size > 5 ? ", ..." : ""}`,
|
|
78
108
|
);
|
|
79
|
-
return [...compiled.values()];
|
|
109
|
+
return { modules: [...compiled.values()], refusedVendors: [], unresolved: [] };
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
async #compileRound(files: string[]): Promise<Omit<BuildRound, "tangled">> {
|
|
@@ -100,9 +130,12 @@ export class CsrDevModuleCompiler {
|
|
|
100
130
|
}
|
|
101
131
|
|
|
102
132
|
async #bunBuild(files: string[], vendor: boolean): Promise<BuildRound> {
|
|
103
|
-
|
|
133
|
+
//? Keyed as the output is: by the disk's spelling, which a case-only rename leaves apart from the requested path.
|
|
134
|
+
const mtimes = new Map(files.map((file) => [CsrDevPaths.realpath(file), CsrDevPaths.mtimeOf(file)]));
|
|
104
135
|
const hashes = new Map(
|
|
105
|
-
await Promise.all(
|
|
136
|
+
await Promise.all(
|
|
137
|
+
files.map(async (file) => [CsrDevPaths.realpath(file), vendor ? "" : await CsrDevPaths.hashOf(file)] as const),
|
|
138
|
+
),
|
|
106
139
|
);
|
|
107
140
|
const misses: string[] = [];
|
|
108
141
|
const result = await Bun.build({
|
|
@@ -113,7 +146,7 @@ export class CsrDevModuleCompiler {
|
|
|
113
146
|
reactFastRefresh: !vendor,
|
|
114
147
|
sourcemap: vendor ? "none" : "external",
|
|
115
148
|
naming: { entry: "[dir]/[name].[ext]", asset: "assets/[name]-[hash].[ext]" },
|
|
116
|
-
publicPath:
|
|
149
|
+
publicPath: this.#routePrefix,
|
|
117
150
|
metafile: true,
|
|
118
151
|
env: "AKAN_PUBLIC_*",
|
|
119
152
|
define: this.#context.define,
|
|
@@ -128,7 +161,9 @@ export class CsrDevModuleCompiler {
|
|
|
128
161
|
const tangled: string[] = [];
|
|
129
162
|
for (const artifact of result.outputs) {
|
|
130
163
|
if (artifact.kind === "asset") {
|
|
131
|
-
|
|
164
|
+
const assetPath = path.join(this.#outDir, artifact.path);
|
|
165
|
+
await Bun.write(assetPath, artifact);
|
|
166
|
+
this.#assets.add(assetPath);
|
|
132
167
|
continue;
|
|
133
168
|
}
|
|
134
169
|
if (artifact.kind !== "entry-point") continue;
|
|
@@ -167,13 +202,20 @@ export class CsrDevModuleCompiler {
|
|
|
167
202
|
if (CsrDevPaths.isVendorFile(args.path)) return undefined;
|
|
168
203
|
const normalized = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(
|
|
169
204
|
await Bun.file(args.path).text(),
|
|
205
|
+
{ path: args.path },
|
|
170
206
|
);
|
|
171
207
|
return normalized ? { contents: normalized, loader: "tsx" } : undefined;
|
|
172
208
|
});
|
|
173
209
|
build.onResolve({ filter: /.*/ }, (args) => {
|
|
174
210
|
if (args.kind === "entry-point-build" || !args.importer) return undefined;
|
|
211
|
+
if (this.#context.externals?.includes(args.path))
|
|
212
|
+
return { path: `${CsrDevPaths.vendorPrefix}${args.path}`, external: true };
|
|
175
213
|
const importer = CsrDevPaths.realpath(args.importer);
|
|
176
|
-
const target = this.#resolver.resolve(
|
|
214
|
+
const target = this.#resolver.resolve(
|
|
215
|
+
importer,
|
|
216
|
+
args.path,
|
|
217
|
+
args.kind === "require-call" ? "require" : "import",
|
|
218
|
+
);
|
|
177
219
|
if (target === CsrDevResolver.inline) return undefined;
|
|
178
220
|
if (target === null) {
|
|
179
221
|
misses.push(`${args.path} from ${this.#paths.idOf(importer)}`);
|