@akanjs/devkit 1.0.20 → 2.1.0-rc.1
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/README.ko.md +65 -0
- package/README.md +62 -6
- package/aiEditor.ts +304 -0
- package/akanApp/akanApp.host.ts +393 -0
- package/akanApp/index.ts +1 -0
- package/akanConfig/akanConfig.test.ts +236 -0
- package/akanConfig/akanConfig.ts +384 -0
- package/akanConfig/index.ts +2 -0
- package/akanConfig/types.ts +23 -0
- package/applicationBuildReporter.ts +69 -0
- package/applicationBuildRunner.ts +302 -0
- package/applicationReleasePackager.ts +206 -0
- package/artifact/implicitRootLayout.ts +155 -0
- package/artifact/index.ts +1 -0
- package/artifact/routeSeedIndex.test.ts +98 -0
- package/artifact/routeSeedIndex.ts +130 -0
- package/auth.ts +41 -0
- package/builder.ts +164 -0
- package/capacitor.base.config.ts +88 -0
- package/capacitorApp.ts +440 -0
- package/commandDecorators/argMeta.ts +102 -0
- package/commandDecorators/command.ts +351 -0
- package/commandDecorators/commandBuilder.ts +224 -0
- package/commandDecorators/commandDecorators.test.ts +212 -0
- package/commandDecorators/commandMeta.ts +7 -0
- package/commandDecorators/dependencyBuilder.ts +100 -0
- package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
- package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
- package/commandDecorators/targetMeta.ts +31 -0
- package/commandDecorators/types.ts +10 -0
- package/constants.ts +25 -0
- package/createTunnel.ts +36 -0
- package/dependencyScanner.ts +357 -0
- package/devkitUtils.test.ts +259 -0
- package/executors.test.ts +315 -0
- package/executors.ts +1390 -0
- package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
- package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
- package/fileSys.ts +39 -0
- package/frontendBuild/allRoutesBuilder.ts +103 -0
- package/frontendBuild/buildRouteClient.test.ts +190 -0
- package/frontendBuild/clientBuildTypes.ts +114 -0
- package/frontendBuild/clientEntriesBundler.ts +303 -0
- package/frontendBuild/clientEntryDiscovery.ts +199 -0
- package/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/frontendBuild/cssCompiler.ts +286 -0
- package/frontendBuild/cssImportResolver.ts +116 -0
- package/frontendBuild/fontOptimizer.ts +427 -0
- package/frontendBuild/frontendBuild.test.ts +204 -0
- package/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/frontendBuild/hmrWatcher.ts +102 -0
- package/frontendBuild/index.ts +18 -0
- package/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/frontendBuild/precompressArtifacts.ts +59 -0
- package/frontendBuild/routeClientBuilder.ts +290 -0
- package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/frontendBuild/vendorSpecifiers.ts +16 -0
- package/frontendBuild/watchRootResolver.ts +28 -0
- package/getCredentials.ts +19 -0
- package/getDirname.ts +3 -0
- package/getModelFileData.ts +59 -0
- package/getRelatedCnsts.ts +313 -0
- package/guideline.ts +19 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
- package/incrementalBuilder/index.ts +1 -0
- package/{esm/src/index.js → index.ts} +28 -15
- package/lint/no-deep-internal-import.grit +25 -0
- package/lint/no-import-client-functions.grit +32 -0
- package/lint/no-import-external-library.grit +21 -0
- package/lint/no-js-private-class-method.grit +42 -0
- package/lint/no-use-client-in-server.grit +7 -0
- package/lint/non-scalar-props-restricted.grit +13 -0
- package/linter.ts +271 -0
- package/mobile/index.ts +1 -0
- package/mobile/mobileTarget.test.ts +53 -0
- package/mobile/mobileTarget.ts +88 -0
- package/package.json +48 -31
- package/prompter.ts +72 -0
- package/scanInfo.ts +606 -0
- package/selectModel.ts +11 -0
- package/{esm/src/spinner.js → spinner.ts} +22 -28
- package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
- package/sshTunnel.ts +152 -0
- package/{esm/src/streamAi.js → streamAi.ts} +18 -12
- package/transforms/barrelAnalyzer.ts +278 -0
- package/transforms/barrelImportsPlugin.ts +504 -0
- package/transforms/externalizeFrameworkPlugin.ts +185 -0
- package/transforms/index.ts +5 -0
- package/transforms/rscUseClientTransform.ts +59 -0
- package/transforms/transforms.test.ts +208 -0
- package/transforms/useClientBundlePlugin.ts +47 -0
- package/tsconfig.json +37 -0
- package/typeChecker.ts +264 -0
- package/types.ts +44 -0
- package/ui/MultiScrollList.tsx +242 -0
- package/ui/ScrollList.tsx +107 -0
- package/ui/index.ts +2 -0
- package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
- package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
- package/cjs/index.js +0 -21
- package/cjs/src/aiEditor.js +0 -311
- package/cjs/src/auth.js +0 -72
- package/cjs/src/builder.js +0 -114
- package/cjs/src/capacitorApp.js +0 -313
- package/cjs/src/commandDecorators/argMeta.js +0 -88
- package/cjs/src/commandDecorators/command.js +0 -324
- package/cjs/src/commandDecorators/commandMeta.js +0 -30
- package/cjs/src/commandDecorators/helpFormatter.js +0 -211
- package/cjs/src/commandDecorators/index.js +0 -31
- package/cjs/src/commandDecorators/targetMeta.js +0 -57
- package/cjs/src/commandDecorators/types.js +0 -15
- package/cjs/src/constants.js +0 -46
- package/cjs/src/createTunnel.js +0 -49
- package/cjs/src/dependencyScanner.js +0 -220
- package/cjs/src/executors.js +0 -964
- package/cjs/src/extractDeps.js +0 -103
- package/cjs/src/fileEditor.js +0 -120
- package/cjs/src/getCredentials.js +0 -44
- package/cjs/src/getDirname.js +0 -38
- package/cjs/src/getModelFileData.js +0 -66
- package/cjs/src/getRelatedCnsts.js +0 -260
- package/cjs/src/guideline.js +0 -15
- package/cjs/src/index.js +0 -65
- package/cjs/src/linter.js +0 -238
- package/cjs/src/prompter.js +0 -85
- package/cjs/src/scanInfo.js +0 -491
- package/cjs/src/selectModel.js +0 -46
- package/cjs/src/spinner.js +0 -93
- package/cjs/src/streamAi.js +0 -62
- package/cjs/src/typeChecker.js +0 -207
- package/cjs/src/types.js +0 -15
- package/cjs/src/uploadRelease.js +0 -112
- package/cjs/src/useStdoutDimensions.js +0 -43
- package/esm/index.js +0 -1
- package/esm/src/aiEditor.js +0 -282
- package/esm/src/auth.js +0 -42
- package/esm/src/builder.js +0 -81
- package/esm/src/commandDecorators/argMeta.js +0 -54
- package/esm/src/commandDecorators/command.js +0 -290
- package/esm/src/commandDecorators/commandMeta.js +0 -7
- package/esm/src/commandDecorators/targetMeta.js +0 -33
- package/esm/src/commandDecorators/types.js +0 -0
- package/esm/src/constants.js +0 -17
- package/esm/src/createTunnel.js +0 -26
- package/esm/src/dependencyScanner.js +0 -187
- package/esm/src/executors.js +0 -928
- package/esm/src/getCredentials.js +0 -11
- package/esm/src/getDirname.js +0 -5
- package/esm/src/getModelFileData.js +0 -33
- package/esm/src/getRelatedCnsts.js +0 -221
- package/esm/src/guideline.js +0 -0
- package/esm/src/linter.js +0 -205
- package/esm/src/prompter.js +0 -51
- package/esm/src/scanInfo.js +0 -455
- package/esm/src/selectModel.js +0 -13
- package/esm/src/typeChecker.js +0 -174
- package/esm/src/types.js +0 -0
- package/index.d.ts +0 -1
- package/src/aiEditor.d.ts +0 -50
- package/src/auth.d.ts +0 -9
- package/src/builder.d.ts +0 -18
- package/src/capacitorApp.d.ts +0 -39
- package/src/commandDecorators/argMeta.d.ts +0 -67
- package/src/commandDecorators/command.d.ts +0 -2
- package/src/commandDecorators/commandMeta.d.ts +0 -2
- package/src/commandDecorators/helpFormatter.d.ts +0 -3
- package/src/commandDecorators/index.d.ts +0 -6
- package/src/commandDecorators/targetMeta.d.ts +0 -19
- package/src/commandDecorators/types.d.ts +0 -1
- package/src/constants.d.ts +0 -26
- package/src/createTunnel.d.ts +0 -8
- package/src/dependencyScanner.d.ts +0 -23
- package/src/executors.d.ts +0 -296
- package/src/extractDeps.d.ts +0 -7
- package/src/fileEditor.d.ts +0 -16
- package/src/getCredentials.d.ts +0 -12
- package/src/getDirname.d.ts +0 -1
- package/src/getModelFileData.d.ts +0 -16
- package/src/getRelatedCnsts.d.ts +0 -53
- package/src/guideline.d.ts +0 -19
- package/src/index.d.ts +0 -23
- package/src/linter.d.ts +0 -109
- package/src/prompter.d.ts +0 -14
- package/src/scanInfo.d.ts +0 -82
- package/src/selectModel.d.ts +0 -1
- package/src/spinner.d.ts +0 -20
- package/src/streamAi.d.ts +0 -6
- package/src/typeChecker.d.ts +0 -52
- package/src/types.d.ts +0 -31
- package/src/uploadRelease.d.ts +0 -10
- package/src/useStdoutDimensions.d.ts +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Logger } from "akanjs/common";
|
|
4
|
+
import type { ChangeBatch, ChangeKind } from "akanjs/server";
|
|
5
|
+
import { HmrChangeClassifier } from "./hmrChangeClassifier";
|
|
6
|
+
|
|
7
|
+
export type { ChangeBatch, ChangeKind };
|
|
8
|
+
|
|
9
|
+
export interface WatcherOptions {
|
|
10
|
+
roots: string[];
|
|
11
|
+
debounceMs?: number;
|
|
12
|
+
logger: Logger;
|
|
13
|
+
onBatch: (batch: ChangeBatch) => void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Recursive filesystem watcher with debounced batching. We deliberately keep
|
|
18
|
+
* classification coarse (`code` / `css` / `config`) so the orchestrator can
|
|
19
|
+
* decide whether a full rebuild-and-reload or a narrower action (e.g. CSS
|
|
20
|
+
* hot-swap) is sufficient.
|
|
21
|
+
*/
|
|
22
|
+
export class HmrWatcher {
|
|
23
|
+
readonly #roots: string[];
|
|
24
|
+
readonly #debounceMs: number;
|
|
25
|
+
readonly #onBatch: WatcherOptions["onBatch"];
|
|
26
|
+
readonly #logger: Logger;
|
|
27
|
+
readonly #watchers: fs.FSWatcher[] = [];
|
|
28
|
+
readonly #pending = new Map<string, Exclude<ChangeKind, "ignore">>();
|
|
29
|
+
readonly #classifier = new HmrChangeClassifier();
|
|
30
|
+
#timer: ReturnType<typeof setTimeout> | null = null;
|
|
31
|
+
#stopped = false;
|
|
32
|
+
#flushing = false;
|
|
33
|
+
|
|
34
|
+
constructor(opts: WatcherOptions) {
|
|
35
|
+
this.#roots = [...new Set(opts.roots.map((r) => path.resolve(r)))];
|
|
36
|
+
this.#debounceMs = opts.debounceMs ?? 80;
|
|
37
|
+
this.#onBatch = opts.onBatch;
|
|
38
|
+
this.#logger = opts.logger;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
start(): void {
|
|
42
|
+
for (const root of this.#roots) {
|
|
43
|
+
try {
|
|
44
|
+
const w = fs.watch(root, { recursive: true, persistent: false }, (_event, filename) => {
|
|
45
|
+
if (!filename) return;
|
|
46
|
+
const abs = path.resolve(root, filename.toString());
|
|
47
|
+
this.#queue(abs);
|
|
48
|
+
});
|
|
49
|
+
this.#watchers.push(w);
|
|
50
|
+
this.#logger.verbose(`[hmr] watching ${root}`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
this.#logger.error(`[hmr] failed to watch ${root}: ${(err as Error).message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
stop(): void {
|
|
58
|
+
this.#stopped = true;
|
|
59
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
60
|
+
for (const w of this.#watchers) {
|
|
61
|
+
try {
|
|
62
|
+
w.close();
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#queue(abs: string): void {
|
|
70
|
+
const kind = this.#classifier.classify(abs);
|
|
71
|
+
if (kind === "ignore") return;
|
|
72
|
+
this.#pending.set(abs, kind);
|
|
73
|
+
if (this.#flushing) return;
|
|
74
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
75
|
+
this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#flush(): void {
|
|
79
|
+
this.#timer = null;
|
|
80
|
+
if (this.#stopped || this.#pending.size === 0 || this.#flushing) return;
|
|
81
|
+
void this.#drain();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async #drain(): Promise<void> {
|
|
85
|
+
this.#flushing = true;
|
|
86
|
+
try {
|
|
87
|
+
while (!this.#stopped && this.#pending.size > 0) {
|
|
88
|
+
const files = Array.from(this.#pending.keys());
|
|
89
|
+
const kinds = new Set(this.#pending.values());
|
|
90
|
+
this.#pending.clear();
|
|
91
|
+
try {
|
|
92
|
+
await this.#onBatch({ files, kinds });
|
|
93
|
+
} catch (e) {
|
|
94
|
+
this.#logger.error(`[hmr] onBatch error: ${(e as Error).message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
this.#flushing = false;
|
|
99
|
+
if (!this.#stopped && this.#pending.size > 0) this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./allRoutesBuilder";
|
|
2
|
+
export * from "./clientBuildTypes";
|
|
3
|
+
export * from "./clientEntriesBundler";
|
|
4
|
+
export * from "./clientEntryDiscovery";
|
|
5
|
+
export * from "./csrArtifactBuilder";
|
|
6
|
+
export * from "./cssCompiler";
|
|
7
|
+
export * from "./cssImportResolver";
|
|
8
|
+
export * from "./fontOptimizer";
|
|
9
|
+
export * from "./hmrChangeClassifier";
|
|
10
|
+
export * from "./hmrWatcher";
|
|
11
|
+
export * from "./pagesBundleBuilder";
|
|
12
|
+
export * from "./pagesEntrySourceGenerator";
|
|
13
|
+
export * from "./precompressArtifacts";
|
|
14
|
+
export * from "./routeClientBuilder";
|
|
15
|
+
export * from "./routesManifestArtifactSerializer";
|
|
16
|
+
export * from "./ssrBaseArtifactBuilder";
|
|
17
|
+
export * from "./vendorSpecifiers";
|
|
18
|
+
export * from "./watchRootResolver";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { BunPlugin } from "bun";
|
|
3
|
+
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
4
|
+
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
|
+
import type { App } from "../commandDecorators";
|
|
6
|
+
import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
7
|
+
import { createExternalizeFrameworkPlugin } from "../transforms/externalizeFrameworkPlugin";
|
|
8
|
+
import { transformUseClient } from "../transforms/rscUseClientTransform";
|
|
9
|
+
import { createUseClientBundlePlugin } from "../transforms/useClientBundlePlugin";
|
|
10
|
+
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
11
|
+
|
|
12
|
+
export interface BuildPagesBundleResult {
|
|
13
|
+
/** Absolute path to the emitted `pages-[hash].js`. */
|
|
14
|
+
bundlePath: string;
|
|
15
|
+
/**
|
|
16
|
+
* Monotonic build identifier. Bun.build emits a fresh filename whenever
|
|
17
|
+
* any input changes, but importers still benefit from a `?v=<buildId>`
|
|
18
|
+
* query-string cache bust — `buildId` is that value.
|
|
19
|
+
*/
|
|
20
|
+
buildId: number;
|
|
21
|
+
splitting: boolean;
|
|
22
|
+
entryBytes: number;
|
|
23
|
+
outputBytes: number;
|
|
24
|
+
outputCount: number;
|
|
25
|
+
chunkCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the server-side pages bundle. The RSC worker loads the result with
|
|
32
|
+
* `await import(bundlePath?v=buildId)`.
|
|
33
|
+
*/
|
|
34
|
+
export class PagesBundleBuilder {
|
|
35
|
+
#app: App;
|
|
36
|
+
#command: "build" | "start";
|
|
37
|
+
#pageEntries?: PageEntry[];
|
|
38
|
+
#started = Date.now();
|
|
39
|
+
|
|
40
|
+
constructor(app: App, command: "build" | "start" = "start", pageEntries?: PageEntry[]) {
|
|
41
|
+
this.#app = app;
|
|
42
|
+
this.#command = command;
|
|
43
|
+
this.#pageEntries = pageEntries;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async build(): Promise<BuildPagesBundleResult> {
|
|
47
|
+
const akanConfig = await this.#app.getConfig();
|
|
48
|
+
const resolvedEntries =
|
|
49
|
+
this.#pageEntries ?? (await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys()));
|
|
50
|
+
const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
|
|
51
|
+
const workspaceRoot = this.#app.workspace.workspaceRoot;
|
|
52
|
+
const result = await Bun.build({
|
|
53
|
+
entrypoints: [VIRTUAL_PAGES_ENTRY],
|
|
54
|
+
outdir: `${this.#artifactDir}/server`,
|
|
55
|
+
target: "bun",
|
|
56
|
+
format: "esm",
|
|
57
|
+
splitting: this.#splitting,
|
|
58
|
+
minify: this.#command === "build",
|
|
59
|
+
naming: {
|
|
60
|
+
entry: "pages-[hash].[ext]",
|
|
61
|
+
chunk: "chunks/[name]-[hash].[ext]",
|
|
62
|
+
asset: "assets/[name]-[hash].[ext]",
|
|
63
|
+
},
|
|
64
|
+
define: this.#define(),
|
|
65
|
+
plugins: [
|
|
66
|
+
PagesBundleBuilder.createPagesEntryPlugin(entrySource),
|
|
67
|
+
await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
|
|
68
|
+
akanConfig.barrelImports.length > 0
|
|
69
|
+
? await createBarrelImportsPlugin(this.#app, {
|
|
70
|
+
pipeAfter: (source, args) =>
|
|
71
|
+
transformUseClient(source, {
|
|
72
|
+
path: args.path,
|
|
73
|
+
workspaceRoot,
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
: createUseClientBundlePlugin({ workspaceRoot }),
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!result.success) throw new AggregateError(result.logs, "[PagesBundleBuilder] Bun.build failed");
|
|
81
|
+
|
|
82
|
+
const entryArtifact = result.outputs.find((a) => a.kind === "entry-point");
|
|
83
|
+
if (!entryArtifact) throw new Error("[PagesBundleBuilder] Bun.build emitted no entry-point artifact");
|
|
84
|
+
|
|
85
|
+
const bundlePath = path.resolve(entryArtifact.path);
|
|
86
|
+
const buildId = Date.now();
|
|
87
|
+
const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
|
|
88
|
+
const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
|
|
89
|
+
this.#app.verbose(
|
|
90
|
+
`[PagesBundleBuilder] ${path.basename(bundlePath)} emitted in ${Date.now() - this.#started}ms splitting=${this.#splitting} entry=${entryArtifact.size} bytes outputs=${result.outputs.length} chunks=${chunkCount} total=${outputBytes} bytes`,
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
bundlePath,
|
|
94
|
+
buildId,
|
|
95
|
+
splitting: this.#splitting,
|
|
96
|
+
entryBytes: entryArtifact.size,
|
|
97
|
+
outputBytes,
|
|
98
|
+
outputCount: result.outputs.length,
|
|
99
|
+
chunkCount,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get #artifactDir(): string {
|
|
104
|
+
return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get #splitting(): boolean {
|
|
108
|
+
return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#define(): Record<string, string> {
|
|
112
|
+
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
113
|
+
return {
|
|
114
|
+
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
115
|
+
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
116
|
+
...Object.fromEntries(
|
|
117
|
+
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static createPagesEntryPlugin(source: string): BunPlugin {
|
|
123
|
+
return {
|
|
124
|
+
name: "akan-pages-entry",
|
|
125
|
+
setup(build) {
|
|
126
|
+
build.onResolve({ filter: /^akan-pages-entry$/ }, () => ({
|
|
127
|
+
path: VIRTUAL_PAGES_ENTRY,
|
|
128
|
+
namespace: "akan-virtual",
|
|
129
|
+
}));
|
|
130
|
+
build.onLoad({ filter: /^akan-pages-entry$/, namespace: "akan-virtual" }, () => ({
|
|
131
|
+
contents: source,
|
|
132
|
+
loader: "tsx",
|
|
133
|
+
}));
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
3
|
+
|
|
4
|
+
export class PagesEntrySourceGenerator {
|
|
5
|
+
#pageEntries: PageEntry[];
|
|
6
|
+
|
|
7
|
+
constructor(pageEntries: PageEntry[]) {
|
|
8
|
+
this.#pageEntries = pageEntries;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static generate(pageEntries: PageEntry[]): string {
|
|
12
|
+
return new PagesEntrySourceGenerator(pageEntries).generate();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
generate(): string {
|
|
16
|
+
const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
|
|
17
|
+
const absPath = path.resolve(moduleAbsPath);
|
|
18
|
+
return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(absPath)}),`;
|
|
19
|
+
});
|
|
20
|
+
return `export const pages = {\n${lines.join("\n")}\n};\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static generateStatic(pageEntries: PageEntry[]): string {
|
|
24
|
+
return new PagesEntrySourceGenerator(pageEntries).generateStatic();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
generateStatic(): string {
|
|
28
|
+
const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
|
|
29
|
+
const absPath = path.resolve(moduleAbsPath);
|
|
30
|
+
return `import * as page${index} from ${JSON.stringify(absPath)};`;
|
|
31
|
+
});
|
|
32
|
+
const entries = this.#pageEntries.map(({ key }, index) => {
|
|
33
|
+
return ` ${JSON.stringify(key)}: async () => page${index},`;
|
|
34
|
+
});
|
|
35
|
+
return `${imports.join("\n")}\nexport const pages = {\n${entries.join("\n")}\n};\n`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { App } from "../commandDecorators";
|
|
4
|
+
|
|
5
|
+
const COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
|
|
6
|
+
const MIN_COMPRESS_BYTES = 1024;
|
|
7
|
+
|
|
8
|
+
export interface PrecompressArtifactsResult {
|
|
9
|
+
files: number;
|
|
10
|
+
inputBytes: number;
|
|
11
|
+
outputBytes: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function precompressArtifacts(app: App): Promise<PrecompressArtifactsResult> {
|
|
15
|
+
const roots = [path.join(app.dist.cwdPath, ".akan/artifact/client")];
|
|
16
|
+
const result: PrecompressArtifactsResult = { files: 0, inputBytes: 0, outputBytes: 0 };
|
|
17
|
+
|
|
18
|
+
await Promise.all(roots.map((root) => precompressRoot(root, result)));
|
|
19
|
+
if (result.files > 0) {
|
|
20
|
+
app.verbose(
|
|
21
|
+
`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(
|
|
22
|
+
result.outputBytes,
|
|
23
|
+
)})`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function precompressRoot(root: string, result: PrecompressArtifactsResult): Promise<void> {
|
|
30
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return;
|
|
31
|
+
const glob = new Bun.Glob("**/*");
|
|
32
|
+
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
33
|
+
if (!(await shouldPrecompress(filePath))) continue;
|
|
34
|
+
const bytes = await Bun.file(filePath).bytes();
|
|
35
|
+
const gz = Bun.gzipSync(toArrayBuffer(bytes));
|
|
36
|
+
await Bun.write(`${filePath}.gz`, gz);
|
|
37
|
+
result.files += 1;
|
|
38
|
+
result.inputBytes += bytes.byteLength;
|
|
39
|
+
result.outputBytes += gz.byteLength;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function shouldPrecompress(filePath: string): Promise<boolean> {
|
|
44
|
+
if (filePath.endsWith(".gz")) return false;
|
|
45
|
+
if (!COMPRESSIBLE_EXTS.has(path.extname(filePath).toLowerCase())) return false;
|
|
46
|
+
const file = Bun.file(filePath);
|
|
47
|
+
if (!(await file.exists())) return false;
|
|
48
|
+
return file.size >= MIN_COMPRESS_BYTES;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
52
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatBytes(bytes: number): string {
|
|
56
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
57
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
58
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BaseBuildArtifact, ClientManifest, SsrManifest } from "akanjs/server";
|
|
4
|
+
import type { App } from "../commandDecorators";
|
|
5
|
+
import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
6
|
+
import { toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
7
|
+
import type { ClientEntryDiscovery } from "./clientBuildTypes";
|
|
8
|
+
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
9
|
+
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
10
|
+
import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
|
|
11
|
+
|
|
12
|
+
const SSR_CLIENT_EXTERNALS = [
|
|
13
|
+
"react",
|
|
14
|
+
"react-dom",
|
|
15
|
+
"react-dom/client",
|
|
16
|
+
"react/jsx-runtime",
|
|
17
|
+
"react/jsx-dev-runtime",
|
|
18
|
+
"akanjs/fetch",
|
|
19
|
+
] as const;
|
|
20
|
+
const SSR_CLIENT_ALIAS_EXTERNALS = [
|
|
21
|
+
"react",
|
|
22
|
+
"react-dom",
|
|
23
|
+
"react-dom/client",
|
|
24
|
+
"react/jsx-runtime",
|
|
25
|
+
"react/jsx-dev-runtime",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export interface BuildRouteClientOptions {
|
|
29
|
+
app: App;
|
|
30
|
+
seeds: string[];
|
|
31
|
+
artifact: BaseBuildArtifact;
|
|
32
|
+
knownEntries?: Set<string>;
|
|
33
|
+
routeId?: string;
|
|
34
|
+
command?: "build" | "start";
|
|
35
|
+
discovery?: ClientEntryDiscovery;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BuildRouteClientResult {
|
|
39
|
+
/** Newly-emitted manifest rows keyed by `${absEntry}#${exportName}`. */
|
|
40
|
+
manifestDelta: ClientManifest;
|
|
41
|
+
/** Newly-emitted ssrManifest rows keyed by entry URL. */
|
|
42
|
+
ssrManifestDelta: SsrManifest;
|
|
43
|
+
/** Absolute paths of the `"use client"` leaves this build added. */
|
|
44
|
+
newEntries: string[];
|
|
45
|
+
/** Absolute paths of all `"use client"` leaves discovered from this route's seeds. */
|
|
46
|
+
discoveredEntries?: string[];
|
|
47
|
+
/** Absolute source files included in the browser bundles for new entries. */
|
|
48
|
+
clientDeps: string[];
|
|
49
|
+
/** Absolute source files included in the browser bundle, grouped by original client entry. */
|
|
50
|
+
clientDepsByEntry?: Record<string, string[]>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BootstrapEntries {
|
|
54
|
+
buildEntries: string[];
|
|
55
|
+
originalByBuildEntry: Map<string, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class RouteClientBuilder {
|
|
59
|
+
#app: App;
|
|
60
|
+
#seeds: string[];
|
|
61
|
+
#knownEntries: Set<string>;
|
|
62
|
+
#command: "build" | "start";
|
|
63
|
+
#discovery?: ClientEntryDiscovery;
|
|
64
|
+
|
|
65
|
+
constructor(options: BuildRouteClientOptions) {
|
|
66
|
+
this.#app = options.app;
|
|
67
|
+
this.#seeds = options.seeds;
|
|
68
|
+
this.#knownEntries = options.knownEntries ?? new Set<string>();
|
|
69
|
+
this.#command = options.command ?? "start";
|
|
70
|
+
this.#discovery = options.discovery;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async build(): Promise<BuildRouteClientResult> {
|
|
74
|
+
const discovery = this.#discovery ?? (await GraphClientEntryDiscovery.create(this.#app));
|
|
75
|
+
const discovered = await discovery.discover(this.#seeds);
|
|
76
|
+
const entries = discovered.filter((e) => !this.#knownEntries.has(e));
|
|
77
|
+
if (entries.length === 0) return this.#emptyResult(discovered);
|
|
78
|
+
|
|
79
|
+
const bootstrapEntries = await this.#createBootstrapEntries(entries);
|
|
80
|
+
const browserBundle = await this.#buildBrowserBundle(bootstrapEntries);
|
|
81
|
+
const ssrBundle = await this.#buildSsrBundle(bootstrapEntries);
|
|
82
|
+
|
|
83
|
+
const acceptedEntries = new Set(entries);
|
|
84
|
+
const manifestDelta: ClientManifest = {};
|
|
85
|
+
const ssrModuleMap: SsrManifest["moduleMap"] = {};
|
|
86
|
+
const clientDeps = new Set<string>();
|
|
87
|
+
const clientDepsByEntry: Record<string, string[]> = {};
|
|
88
|
+
for (const [key, row] of Object.entries(browserBundle.manifest)) {
|
|
89
|
+
const manifestEntry = RouteClientBuilder.resolveOriginalManifestEntry(
|
|
90
|
+
key,
|
|
91
|
+
bootstrapEntries.originalByBuildEntry,
|
|
92
|
+
browserBundle.clientReferenceIdByAbsPath,
|
|
93
|
+
this.#app.workspace.workspaceRoot,
|
|
94
|
+
);
|
|
95
|
+
if (!manifestEntry) continue;
|
|
96
|
+
if (!acceptedEntries.has(manifestEntry.originalEntry)) continue;
|
|
97
|
+
manifestDelta[manifestEntry.key] = row;
|
|
98
|
+
|
|
99
|
+
const ssrOutput = ssrBundle.entryOutputAbsByAbsPath.get(manifestEntry.buildEntry);
|
|
100
|
+
if (!ssrOutput) continue;
|
|
101
|
+
|
|
102
|
+
ssrModuleMap[row.id] ??= {};
|
|
103
|
+
ssrModuleMap[row.id][row.name] = { id: ssrOutput, chunks: [ssrOutput, ssrOutput], name: row.name, async: true };
|
|
104
|
+
}
|
|
105
|
+
for (const entry of bootstrapEntries.buildEntries) {
|
|
106
|
+
const buildEntry = path.resolve(entry);
|
|
107
|
+
const originalEntry = path.resolve(bootstrapEntries.originalByBuildEntry.get(buildEntry) ?? buildEntry);
|
|
108
|
+
if (!acceptedEntries.has(originalEntry)) continue;
|
|
109
|
+
const deps = new Set<string>([originalEntry]);
|
|
110
|
+
for (const dep of browserBundle.entryDepsByAbsPath.get(buildEntry) ?? []) deps.add(path.resolve(dep));
|
|
111
|
+
const sortedDeps = [...deps].sort();
|
|
112
|
+
clientDepsByEntry[originalEntry] = sortedDeps;
|
|
113
|
+
for (const dep of sortedDeps) clientDeps.add(dep);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
manifestDelta,
|
|
118
|
+
ssrManifestDelta: { moduleLoading: null, moduleMap: ssrModuleMap },
|
|
119
|
+
newEntries: entries,
|
|
120
|
+
discoveredEntries: discovered,
|
|
121
|
+
clientDeps: [...clientDeps].sort(),
|
|
122
|
+
clientDepsByEntry,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#emptyResult(discoveredEntries: string[] = []): BuildRouteClientResult {
|
|
127
|
+
return {
|
|
128
|
+
manifestDelta: {},
|
|
129
|
+
ssrManifestDelta: { moduleLoading: null, moduleMap: {} },
|
|
130
|
+
newEntries: [],
|
|
131
|
+
discoveredEntries,
|
|
132
|
+
clientDeps: [],
|
|
133
|
+
clientDepsByEntry: {},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async #buildBrowserBundle(bootstrapEntries: BootstrapEntries) {
|
|
138
|
+
const reactFastRefresh = process.env.AKAN_REACT_FAST_REFRESH !== "0";
|
|
139
|
+
return new ClientEntriesBundler({
|
|
140
|
+
app: this.#app,
|
|
141
|
+
entries: bootstrapEntries.buildEntries,
|
|
142
|
+
plugins: [
|
|
143
|
+
await createBarrelImportsPlugin(this.#app, {
|
|
144
|
+
pipeAfter: reactFastRefresh ? RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh : undefined,
|
|
145
|
+
}),
|
|
146
|
+
],
|
|
147
|
+
external: VENDOR_SPECIFIERS,
|
|
148
|
+
command: this.#command,
|
|
149
|
+
reactFastRefresh,
|
|
150
|
+
}).bundle();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async #buildSsrBundle(bootstrapEntries: BootstrapEntries) {
|
|
154
|
+
const externalOptions = RouteClientBuilder.resolveSsrClientExternalOptions(this.#command);
|
|
155
|
+
return new ClientEntriesBundler({
|
|
156
|
+
app: this.#app,
|
|
157
|
+
entries: bootstrapEntries.buildEntries,
|
|
158
|
+
plugins: [await createBarrelImportsPlugin(this.#app)],
|
|
159
|
+
...externalOptions,
|
|
160
|
+
outputSubdir: "client-ssr",
|
|
161
|
+
command: this.#command,
|
|
162
|
+
}).bundle();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async #createBootstrapEntries(entries: string[]): Promise<BootstrapEntries> {
|
|
166
|
+
if (!(await Bun.file(path.join(this.#app.cwdPath, "lib", "st.ts")).exists())) {
|
|
167
|
+
return { buildEntries: entries, originalByBuildEntry: new Map() };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const outdir = path.join(this.#app.cwdPath, ".akan", "generated", "client-entry-bootstrap");
|
|
171
|
+
await mkdir(outdir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
const originalByBuildEntry = new Map<string, string>();
|
|
174
|
+
const buildEntries = await Promise.all(
|
|
175
|
+
entries.map(async (entry) => {
|
|
176
|
+
const absEntry = path.resolve(entry);
|
|
177
|
+
const hash = Bun.hash(`${this.#app.name}\n${absEntry}`).toString(36);
|
|
178
|
+
const base = path.basename(absEntry).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
179
|
+
const wrapperEntry = path.join(outdir, `${base}-${hash}.tsx`);
|
|
180
|
+
const exportNames = await this.#scanExportNames(absEntry);
|
|
181
|
+
await Bun.write(
|
|
182
|
+
wrapperEntry,
|
|
183
|
+
RouteClientBuilder.createStoreBootstrapEntrySource({
|
|
184
|
+
appName: this.#app.name,
|
|
185
|
+
originalEntry: absEntry,
|
|
186
|
+
exportNames,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
originalByBuildEntry.set(path.resolve(wrapperEntry), absEntry);
|
|
190
|
+
return wrapperEntry;
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return { buildEntries, originalByBuildEntry };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async #scanExportNames(absEntry: string): Promise<string[]> {
|
|
198
|
+
const source = await Bun.file(absEntry).text();
|
|
199
|
+
const transpiler = new Bun.Transpiler({ loader: this.#loaderFor(absEntry) });
|
|
200
|
+
return transpiler.scan(source).exports;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
|
|
204
|
+
if (absPath.endsWith(".tsx")) return "tsx";
|
|
205
|
+
if (absPath.endsWith(".jsx")) return "jsx";
|
|
206
|
+
if (absPath.endsWith(".ts")) return "ts";
|
|
207
|
+
return "js";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static normalizeNamedDefaultFunctionForFastRefresh(source: string): string | null {
|
|
211
|
+
let changed = false;
|
|
212
|
+
const defaultNames: string[] = [];
|
|
213
|
+
const next = source.replace(
|
|
214
|
+
/(^|\n)(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)(?=\s*(?:<|\())/g,
|
|
215
|
+
(match, lineStart: string, indent: string, asyncKeyword: string | undefined, name: string) => {
|
|
216
|
+
changed = true;
|
|
217
|
+
defaultNames.push(name);
|
|
218
|
+
return `${lineStart}${indent}${asyncKeyword ?? ""}function ${name}`;
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
if (!changed) return null;
|
|
222
|
+
return `${next}\n${defaultNames.map((name) => `export default ${name};`).join("\n")}\n`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static resolveSsrClientRuntimeAliases(): Record<string, string> {
|
|
226
|
+
const serverEntry = RouteClientBuilder.resolveAkanServerEntry();
|
|
227
|
+
const aliases = Object.fromEntries(
|
|
228
|
+
SSR_CLIENT_ALIAS_EXTERNALS.map((specifier) => [specifier, Bun.resolveSync(specifier, serverEntry)]),
|
|
229
|
+
) as Record<string, string>;
|
|
230
|
+
aliases[Bun.resolveSync("akanjs/fetch", serverEntry)] = "akanjs/fetch";
|
|
231
|
+
return aliases;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static resolveSsrClientExternalOptions(command: "build" | "start"): {
|
|
235
|
+
external: readonly string[];
|
|
236
|
+
externalSubpaths?: readonly string[];
|
|
237
|
+
externalAliases?: Record<string, string>;
|
|
238
|
+
} {
|
|
239
|
+
if (command === "start") {
|
|
240
|
+
return {
|
|
241
|
+
external: SSR_CLIENT_EXTERNALS,
|
|
242
|
+
externalSubpaths: ["akanjs/fetch"],
|
|
243
|
+
externalAliases: RouteClientBuilder.resolveSsrClientRuntimeAliases(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { external: SSR_CLIENT_ALIAS_EXTERNALS };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static resolveAkanServerEntry(): string {
|
|
251
|
+
try {
|
|
252
|
+
return Bun.resolveSync("akanjs/server", import.meta.dir);
|
|
253
|
+
} catch {
|
|
254
|
+
return path.resolve(import.meta.dir, "../../../server/index.ts");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static createStoreBootstrapEntrySource(args: {
|
|
259
|
+
appName: string;
|
|
260
|
+
originalEntry: string;
|
|
261
|
+
exportNames: string[];
|
|
262
|
+
}): string {
|
|
263
|
+
const originalEntry = JSON.stringify(path.resolve(args.originalEntry));
|
|
264
|
+
const namedExports = args.exportNames.filter((name) => name !== "default");
|
|
265
|
+
const lines = [
|
|
266
|
+
`import ${JSON.stringify(`@apps/${args.appName}/client`)};`,
|
|
267
|
+
...(namedExports.length > 0 ? [`export { ${namedExports.join(", ")} } from ${originalEntry};`] : []),
|
|
268
|
+
];
|
|
269
|
+
if (args.exportNames.includes("default")) lines.push(`export { default } from ${originalEntry};`);
|
|
270
|
+
return `${lines.join("\n")}\n`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
static resolveOriginalManifestEntry(
|
|
274
|
+
manifestKey: string,
|
|
275
|
+
originalByBuildEntry: Map<string, string>,
|
|
276
|
+
clientReferenceIdByBuildEntry: Map<string, string> = new Map(),
|
|
277
|
+
workspaceRoot = process.cwd(),
|
|
278
|
+
): { buildEntry: string; originalEntry: string; name: string; key: string } | null {
|
|
279
|
+
const hashIdx = manifestKey.lastIndexOf("#");
|
|
280
|
+
if (hashIdx < 0) return null;
|
|
281
|
+
const buildReferenceId = manifestKey.slice(0, hashIdx);
|
|
282
|
+
const name = manifestKey.slice(hashIdx + 1);
|
|
283
|
+
const buildEntry =
|
|
284
|
+
[...clientReferenceIdByBuildEntry.entries()].find(([, referenceId]) => referenceId === buildReferenceId)?.[0] ??
|
|
285
|
+
buildReferenceId;
|
|
286
|
+
const originalEntry = originalByBuildEntry.get(buildEntry) ?? buildEntry;
|
|
287
|
+
const originalReferenceId = toClientReferencePath(originalEntry, workspaceRoot);
|
|
288
|
+
return { buildEntry, originalEntry, name, key: `${originalReferenceId}#${name}` };
|
|
289
|
+
}
|
|
290
|
+
}
|