@akanjs/devkit 3.0.0-beta.9 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -2,17 +2,15 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import zlib from "node:zlib";
|
|
4
4
|
import type { App } from "../commandDecorators";
|
|
5
|
+
import { FontPruner } from "./fontPruner";
|
|
5
6
|
|
|
6
7
|
const COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
|
|
7
8
|
const MIN_COMPRESS_BYTES = 1024;
|
|
8
|
-
|
|
9
|
-
* Quality 11 is roughly 100x slower than gzip, so it is spent only where it pays: there is one CSS asset
|
|
10
|
-
* per basePath and it is the largest single file the app ships, worth ~20% over gzip for ~0.2s. Raising the
|
|
11
|
-
* several hundred JS chunks from 9 to 11 costs ~12s of build time to save a few hundred KB, so they stay at 9.
|
|
12
|
-
*/
|
|
9
|
+
// Brotli 11 is ~100x slower than gzip: worth it for the one CSS asset per basePath, not for hundreds of JS chunks.
|
|
13
10
|
const BROTLI_QUALITY_BY_EXT = { ".css": 11 } as const;
|
|
14
11
|
const DEFAULT_BROTLI_QUALITY = 9;
|
|
15
12
|
const GZIP_LEVEL = 9;
|
|
13
|
+
const { formatBytes } = FontPruner;
|
|
16
14
|
|
|
17
15
|
export interface PrecompressArtifactsResult {
|
|
18
16
|
files: number;
|
|
@@ -22,8 +20,7 @@ export interface PrecompressArtifactsResult {
|
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
export async function precompressArtifacts(app: App): Promise<PrecompressArtifactsResult> {
|
|
25
|
-
//*
|
|
26
|
-
//* one CSS asset per basePath is the largest uncompressed payload the app ships.
|
|
23
|
+
//* WebRouter serves client/ and styles/ through the same sidecar-aware #fileResponse.
|
|
27
24
|
const roots = [
|
|
28
25
|
path.join(app.dist.cwdPath, ".akan/artifact/client"),
|
|
29
26
|
path.join(app.dist.cwdPath, ".akan/artifact/styles"),
|
|
@@ -47,7 +44,7 @@ async function precompressRoot(root: string, result: PrecompressArtifactsResult)
|
|
|
47
44
|
for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
|
|
48
45
|
if (!(await shouldPrecompress(filePath))) continue;
|
|
49
46
|
const bytes = await Bun.file(filePath).bytes();
|
|
50
|
-
const buffer =
|
|
47
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
51
48
|
const gz = Bun.gzipSync(buffer, { level: GZIP_LEVEL });
|
|
52
49
|
const br = brotliCompress(buffer, path.extname(filePath).toLowerCase());
|
|
53
50
|
await Promise.all([Bun.write(`${filePath}.gz`, gz), Bun.write(`${filePath}.br`, br)]);
|
|
@@ -75,13 +72,3 @@ async function shouldPrecompress(filePath: string): Promise<boolean> {
|
|
|
75
72
|
if (!(await file.exists())) return false;
|
|
76
73
|
return file.size >= MIN_COMPRESS_BYTES;
|
|
77
74
|
}
|
|
78
|
-
|
|
79
|
-
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
80
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function formatBytes(bytes: number): string {
|
|
84
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
85
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
86
|
-
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
87
|
-
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
|
|
7
|
+
import { RegistryMetadataFingerprint } from "./registryMetadataFingerprint";
|
|
8
|
+
|
|
9
|
+
describe("RegistryMetadataFingerprint", () => {
|
|
10
|
+
let root: string;
|
|
11
|
+
let later = Date.now() / 1000 + 60;
|
|
12
|
+
const file = (relative: string) => path.join(root, relative);
|
|
13
|
+
const touch = (relative: string) => {
|
|
14
|
+
later += 1;
|
|
15
|
+
fs.utimesSync(file(relative), later, later);
|
|
16
|
+
};
|
|
17
|
+
const ssrMetadata = (target: string) =>
|
|
18
|
+
isAkanRuntimeMetadataFile(target) && !target.endsWith(".dictionary.ts") && path.basename(target) !== "dict.ts";
|
|
19
|
+
const stamp = async (isMetadataFile = isAkanRuntimeMetadataFile) =>
|
|
20
|
+
await RegistryMetadataFingerprint.of([root], isMetadataFile);
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
root = await mkdtemp(path.join(os.tmpdir(), "akan-registry-metadata-"));
|
|
24
|
+
for (const relative of [
|
|
25
|
+
"lib/sig.ts",
|
|
26
|
+
"lib/dict.ts",
|
|
27
|
+
"lib/useClient.ts",
|
|
28
|
+
"lib/user/user.signal.ts",
|
|
29
|
+
"lib/user/user.dictionary.ts",
|
|
30
|
+
"lib/user/User.Zone.tsx",
|
|
31
|
+
"lib/user/user.signal.test.ts",
|
|
32
|
+
])
|
|
33
|
+
await Bun.write(file(relative), "export {};\n");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await rm(root, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("moves with a signal save and stays with a component save", async () => {
|
|
41
|
+
const before = await stamp();
|
|
42
|
+
touch("lib/user/User.Zone.tsx");
|
|
43
|
+
touch("lib/user/user.signal.test.ts");
|
|
44
|
+
expect(await stamp()).toBe(before);
|
|
45
|
+
touch("lib/user/user.signal.ts");
|
|
46
|
+
expect(await stamp()).not.toBe(before);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a registry that inlines no dictionary is not moved by a dictionary save", async () => {
|
|
50
|
+
const ssrBefore = await stamp(ssrMetadata);
|
|
51
|
+
const csrBefore = await stamp();
|
|
52
|
+
touch("lib/user/user.dictionary.ts");
|
|
53
|
+
touch("lib/dict.ts");
|
|
54
|
+
expect(await stamp(ssrMetadata)).toBe(ssrBefore);
|
|
55
|
+
expect(await stamp()).not.toBe(csrBefore);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a root without a lib folder counts for nothing", async () => {
|
|
59
|
+
expect(await RegistryMetadataFingerprint.of([root, path.join(root, "lib/user")], isAkanRuntimeMetadataFile)).toBe(
|
|
60
|
+
await stamp(),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//* The sources `lib/useClient.ts` reads through its macros, which no registry graph lists as a dependency. A registry
|
|
5
|
+
//* records their stamp, so a builder that loads one built before a signal or dictionary save rebuilds it, whichever
|
|
6
|
+
//* process saw that save (a builder restarted for it never finishes the rebuild it started).
|
|
7
|
+
export class RegistryMetadataFingerprint {
|
|
8
|
+
static readonly #sources = new Bun.Glob("**/*.{signal,dictionary}.ts");
|
|
9
|
+
static readonly #libFiles = ["sig.ts", "dict.ts", "useClient.ts"];
|
|
10
|
+
|
|
11
|
+
/** `roots` are app and lib directories; only the metadata files under each one's `lib/` count. */
|
|
12
|
+
static async of(roots: string[], isMetadataFile: (file: string) => boolean): Promise<string> {
|
|
13
|
+
const stamps: string[] = [];
|
|
14
|
+
for (const root of new Set(roots)) {
|
|
15
|
+
const libDir = path.join(root, "lib");
|
|
16
|
+
if (!fs.existsSync(libDir)) continue;
|
|
17
|
+
const files = RegistryMetadataFingerprint.#libFiles.map((name) => path.join(libDir, name));
|
|
18
|
+
for await (const relative of RegistryMetadataFingerprint.#sources.scan({ cwd: libDir, onlyFiles: true }))
|
|
19
|
+
files.push(path.join(libDir, relative));
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
if (!isMetadataFile(file)) continue;
|
|
22
|
+
const stat = fs.statSync(file, { throwIfNoEntry: false });
|
|
23
|
+
if (stat) stamps.push(`${file}:${stat.mtimeMs}:${stat.size}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Bun.hash(stamps.sort((a, b) => a.localeCompare(b)).join("\n")).toString(36);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { BaseBuildArtifact, ClientManifest, SsrManifest } from "akanjs/server";
|
|
3
|
+
import type { BaseBuildArtifact, ClientManifest, ClientManifestEntry, SsrManifest } from "akanjs/server";
|
|
4
|
+
import { SSR_DEV_CHUNK, SSR_DEV_ID_PREFIX } from "akanjs/server/hmr/csrDevManifest";
|
|
4
5
|
import type { App } from "../commandDecorators";
|
|
5
6
|
import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
6
|
-
import {
|
|
7
|
+
import { loaderFor } from "../transforms/moduleSyntax";
|
|
8
|
+
import { scanUseClientExports, toClientReferencePath } from "../transforms/rscUseClientTransform";
|
|
7
9
|
import type { ClientBundleTarget, ClientEntryDiscovery } from "./clientBuildTypes";
|
|
8
10
|
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
9
11
|
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
12
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
10
13
|
import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
|
|
11
14
|
|
|
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
15
|
const SSR_CLIENT_ALIAS_EXTERNALS = [
|
|
21
16
|
"react",
|
|
22
17
|
"react-dom",
|
|
@@ -24,39 +19,33 @@ const SSR_CLIENT_ALIAS_EXTERNALS = [
|
|
|
24
19
|
"react/jsx-runtime",
|
|
25
20
|
"react/jsx-dev-runtime",
|
|
26
21
|
] as const;
|
|
22
|
+
const SSR_CLIENT_EXTERNALS = [...SSR_CLIENT_ALIAS_EXTERNALS, "akanjs/fetch"] as const;
|
|
27
23
|
|
|
28
24
|
export interface BuildRouteClientOptions {
|
|
29
25
|
app: App;
|
|
30
26
|
seeds: string[];
|
|
27
|
+
/** Every route's seeds: a build that runs at all bundles the whole app, since two builds each copy shared modules. */
|
|
28
|
+
graphSeeds?: string[];
|
|
31
29
|
artifact: BaseBuildArtifact;
|
|
32
30
|
knownEntries?: Set<string>;
|
|
33
31
|
routeId?: string;
|
|
34
32
|
command?: "build" | "start";
|
|
35
33
|
discovery?: ClientEntryDiscovery;
|
|
36
|
-
/**
|
|
37
|
-
* Client entries resolved by the caller, which skips discovery and bundles exactly this list.
|
|
38
|
-
*
|
|
39
|
-
* `AllRoutesBuilder` passes every route's entries at once so they share one `Bun.build`. Chunk
|
|
40
|
-
* splitting is scoped to a single invocation, so a dependency reachable from entries spread across
|
|
41
|
-
* several invocations is emitted once per invocation — mermaid landed in `apps/akan` four times that
|
|
42
|
-
* way. Dev keeps one build per route and leaves this unset.
|
|
43
|
-
*/
|
|
34
|
+
/** Pre-resolved client entries: skips discovery and bundles exactly this list. */
|
|
44
35
|
entries?: string[];
|
|
36
|
+
/** Defaults to `registry` under `start`: the browser loads each entry from the SSR dev registry, so only `client-ssr` builds. */
|
|
37
|
+
browser?: "chunks" | "registry";
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
export interface BuildRouteClientResult {
|
|
48
|
-
/** Newly-emitted manifest rows keyed by `${absEntry}#${exportName}`. */
|
|
49
41
|
manifestDelta: ClientManifest;
|
|
50
|
-
/** Newly-emitted ssrManifest rows keyed by entry URL. */
|
|
51
42
|
ssrManifestDelta: SsrManifest;
|
|
52
|
-
/** Absolute paths of the `"use client"` leaves this build added. */
|
|
53
43
|
newEntries: string[];
|
|
54
|
-
/** Absolute paths of all `"use client"` leaves discovered from this route's seeds. */
|
|
55
44
|
discoveredEntries?: string[];
|
|
56
|
-
/** Absolute source files included in the browser bundles for new entries. */
|
|
57
45
|
clientDeps: string[];
|
|
58
|
-
/** Absolute source files included in the browser bundle, grouped by original client entry. */
|
|
59
46
|
clientDepsByEntry?: Record<string, string[]>;
|
|
47
|
+
/** Registry mode: the entries the manifest rows name, which the SSR dev registry must hold before they are served. */
|
|
48
|
+
registryEntries?: string[];
|
|
60
49
|
}
|
|
61
50
|
|
|
62
51
|
interface BootstrapEntries {
|
|
@@ -67,45 +56,56 @@ interface BootstrapEntries {
|
|
|
67
56
|
export class RouteClientBuilder {
|
|
68
57
|
#app: App;
|
|
69
58
|
#seeds: string[];
|
|
59
|
+
#graphSeeds?: string[];
|
|
70
60
|
#knownEntries: Set<string>;
|
|
71
61
|
#command: "build" | "start";
|
|
72
62
|
#discovery?: ClientEntryDiscovery;
|
|
73
63
|
#entries?: string[];
|
|
64
|
+
#browser: "chunks" | "registry";
|
|
74
65
|
|
|
75
66
|
constructor(options: BuildRouteClientOptions) {
|
|
76
67
|
this.#app = options.app;
|
|
77
68
|
this.#seeds = options.seeds;
|
|
69
|
+
this.#graphSeeds = options.graphSeeds;
|
|
78
70
|
this.#knownEntries = options.knownEntries ?? new Set<string>();
|
|
79
71
|
this.#command = options.command ?? "start";
|
|
80
72
|
this.#discovery = options.discovery;
|
|
81
73
|
this.#entries = options.entries;
|
|
74
|
+
this.#browser = options.browser ?? (this.#command === "start" ? "registry" : "chunks");
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
async build(): Promise<BuildRouteClientResult> {
|
|
85
|
-
const discovered =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
const discovered = this.#entries ?? (await (await this.#getDiscovery()).discover(this.#seeds));
|
|
79
|
+
if (discovered.every((e) => this.#knownEntries.has(e))) return this.#emptyResult(discovered);
|
|
80
|
+
const entries = this.#graphSeeds
|
|
81
|
+
? [...new Set([...discovered, ...(await (await this.#getDiscovery()).discover(this.#graphSeeds))])].sort()
|
|
82
|
+
: discovered.filter((e) => !this.#knownEntries.has(e));
|
|
90
83
|
|
|
91
84
|
const bootstrapEntries = await this.#createBootstrapEntries(entries);
|
|
92
|
-
|
|
85
|
+
//? Registry mode builds no browser bundle: the SSR bundle has the same entries, exports and imports to read.
|
|
86
|
+
const registry = this.#browser === "registry" ? new CsrDevPaths(this.#app.workspace.workspaceRoot) : null;
|
|
87
|
+
const browserBundle = registry ? null : await this.#buildBrowserBundle(bootstrapEntries);
|
|
93
88
|
const ssrBundle = await this.#buildSsrBundle(bootstrapEntries);
|
|
89
|
+
const referenceBundle = browserBundle ?? ssrBundle;
|
|
94
90
|
|
|
95
91
|
const acceptedEntries = new Set(entries);
|
|
92
|
+
const routeEntries = new Set(discovered);
|
|
96
93
|
const manifestDelta: ClientManifest = {};
|
|
97
94
|
const ssrModuleMap: SsrManifest["moduleMap"] = {};
|
|
98
95
|
const clientDeps = new Set<string>();
|
|
99
96
|
const clientDepsByEntry: Record<string, string[]> = {};
|
|
100
|
-
for (const [key,
|
|
97
|
+
for (const [key, bundleRow] of Object.entries(referenceBundle.manifest)) {
|
|
101
98
|
const manifestEntry = RouteClientBuilder.resolveOriginalManifestEntry(
|
|
102
99
|
key,
|
|
103
100
|
bootstrapEntries.originalByBuildEntry,
|
|
104
|
-
|
|
101
|
+
referenceBundle.clientReferenceIdByAbsPath,
|
|
105
102
|
this.#app.workspace.workspaceRoot,
|
|
106
103
|
);
|
|
107
104
|
if (!manifestEntry) continue;
|
|
108
105
|
if (!acceptedEntries.has(manifestEntry.originalEntry)) continue;
|
|
106
|
+
const row = registry
|
|
107
|
+
? RouteClientBuilder.#registryRow(registry, manifestEntry.originalEntry, bundleRow)
|
|
108
|
+
: bundleRow;
|
|
109
109
|
manifestDelta[manifestEntry.key] = row;
|
|
110
110
|
|
|
111
111
|
const ssrOutput = ssrBundle.entryOutputAbsByAbsPath.get(manifestEntry.buildEntry);
|
|
@@ -119,22 +119,37 @@ export class RouteClientBuilder {
|
|
|
119
119
|
const originalEntry = path.resolve(bootstrapEntries.originalByBuildEntry.get(buildEntry) ?? buildEntry);
|
|
120
120
|
if (!acceptedEntries.has(originalEntry)) continue;
|
|
121
121
|
const deps = new Set<string>([originalEntry]);
|
|
122
|
-
for (const dep of
|
|
122
|
+
for (const dep of referenceBundle.entryDepsByAbsPath.get(buildEntry) ?? []) deps.add(path.resolve(dep));
|
|
123
123
|
const sortedDeps = [...deps].sort();
|
|
124
124
|
clientDepsByEntry[originalEntry] = sortedDeps;
|
|
125
|
-
for (const dep of sortedDeps) clientDeps.add(dep);
|
|
125
|
+
if (routeEntries.has(originalEntry)) for (const dep of sortedDeps) clientDeps.add(dep);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
return {
|
|
129
129
|
manifestDelta,
|
|
130
130
|
ssrManifestDelta: { moduleLoading: null, moduleMap: ssrModuleMap },
|
|
131
|
-
newEntries: entries,
|
|
131
|
+
newEntries: entries.filter((e) => !this.#knownEntries.has(e)),
|
|
132
132
|
discoveredEntries: discovered,
|
|
133
133
|
clientDeps: [...clientDeps].sort(),
|
|
134
134
|
clientDepsByEntry,
|
|
135
|
+
...(registry ? { registryEntries: [...acceptedEntries].sort() } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static #registryRow(paths: CsrDevPaths, entry: string, row: ClientManifestEntry): ClientManifestEntry {
|
|
140
|
+
return {
|
|
141
|
+
id: `${SSR_DEV_ID_PREFIX}${paths.idOf(entry)}`,
|
|
142
|
+
chunks: [SSR_DEV_CHUNK, SSR_DEV_CHUNK],
|
|
143
|
+
name: row.name,
|
|
144
|
+
async: true,
|
|
135
145
|
};
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
async #getDiscovery(): Promise<ClientEntryDiscovery> {
|
|
149
|
+
this.#discovery ??= await GraphClientEntryDiscovery.create(this.#app);
|
|
150
|
+
return this.#discovery;
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
#emptyResult(discoveredEntries: string[] = []): BuildRouteClientResult {
|
|
139
154
|
return {
|
|
140
155
|
manifestDelta: {},
|
|
@@ -206,31 +221,58 @@ export class RouteClientBuilder {
|
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
async #scanExportNames(absEntry: string): Promise<string[]> {
|
|
209
|
-
|
|
210
|
-
const transpiler = new Bun.Transpiler({ loader: this.#loaderFor(absEntry) });
|
|
211
|
-
return transpiler.scan(source).exports;
|
|
224
|
+
return scanUseClientExports(await Bun.file(absEntry).text(), absEntry, this.#app.workspace.workspaceRoot);
|
|
212
225
|
}
|
|
213
226
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
227
|
+
static normalizeNamedDefaultFunctionForFastRefresh(
|
|
228
|
+
source: string,
|
|
229
|
+
{ path: filePath }: { path?: string } = {},
|
|
230
|
+
): string | null {
|
|
231
|
+
const declared = RouteClientBuilder.#declaredNamedDefaultFunctions(source, filePath);
|
|
232
|
+
if (declared.length === 0) return null;
|
|
233
|
+
let next = "";
|
|
234
|
+
let cursor = 0;
|
|
235
|
+
for (const { index, text, lineStart, indent, asyncKeyword, name } of declared) {
|
|
236
|
+
next += `${source.slice(cursor, index)}${lineStart}${indent}${asyncKeyword}function ${name}`;
|
|
237
|
+
cursor = index + text.length;
|
|
238
|
+
}
|
|
239
|
+
next += source.slice(cursor);
|
|
240
|
+
return `${next}\n${declared.map(({ name }) => `export default ${name};`).join("\n")}\n`;
|
|
219
241
|
}
|
|
220
242
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
/(^|\n)(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)(?=\s*(?:<|\())/g,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
//? The pattern also matches inside strings and comments (a docs page quoting code), so every candidate is renamed to
|
|
244
|
+
//? a probe export and only the ones the parser reports as exports are declarations.
|
|
245
|
+
static #declaredNamedDefaultFunctions(source: string, filePath?: string) {
|
|
246
|
+
const candidates = [
|
|
247
|
+
...source.matchAll(/(^|\n)(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)(?=\s*(?:<|\())/g),
|
|
248
|
+
].map((match) => ({
|
|
249
|
+
index: match.index,
|
|
250
|
+
text: match[0],
|
|
251
|
+
lineStart: match[1] ?? "",
|
|
252
|
+
indent: match[2] ?? "",
|
|
253
|
+
asyncKeyword: match[3] ?? "",
|
|
254
|
+
name: match[4] ?? "",
|
|
255
|
+
}));
|
|
256
|
+
if (candidates.length === 0) return [];
|
|
257
|
+
const probeName = (idx: number) => `__akanNamedDefault${idx}`;
|
|
258
|
+
let probe = "";
|
|
259
|
+
let cursor = 0;
|
|
260
|
+
candidates.forEach(({ index, text, lineStart, indent, asyncKeyword }, idx) => {
|
|
261
|
+
probe += `${source.slice(cursor, index)}${lineStart}${indent}export ${asyncKeyword}function ${probeName(idx)}`;
|
|
262
|
+
cursor = index + text.length;
|
|
263
|
+
});
|
|
264
|
+
probe += source.slice(cursor);
|
|
265
|
+
const exported = RouteClientBuilder.#scanExports(probe, filePath);
|
|
266
|
+
return candidates.filter((_, idx) => exported.has(probeName(idx)));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static #scanExports(source: string, filePath?: string): Set<string> {
|
|
270
|
+
try {
|
|
271
|
+
return new Set(new Bun.Transpiler({ loader: filePath ? loaderFor(filePath) : "tsx" }).scan(source).exports);
|
|
272
|
+
} catch {
|
|
273
|
+
// Unparseable source is left alone, so the bundler reports its syntax error at the real line.
|
|
274
|
+
return new Set();
|
|
275
|
+
}
|
|
234
276
|
}
|
|
235
277
|
|
|
236
278
|
static resolveSsrClientRuntimeAliases(): Record<string, string> {
|
|
@@ -238,12 +280,8 @@ export class RouteClientBuilder {
|
|
|
238
280
|
return { [Bun.resolveSync("akanjs/fetch", serverEntry)]: "akanjs/fetch" };
|
|
239
281
|
}
|
|
240
282
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
* resolved through its `browser` export condition can touch `document` at module scope and throw mid-render,
|
|
244
|
-
* degrading the whole document to client rendering. Bun's `conditions` only adds to the target's defaults —
|
|
245
|
-
* `browser` still wins — so the target itself has to say server.
|
|
246
|
-
*/
|
|
283
|
+
// `target: "bun"` is load-bearing: a `browser` export condition can touch `document` at module scope mid-SSR, and
|
|
284
|
+
// Bun's `conditions` only add to the target's defaults, so the target itself must say server.
|
|
247
285
|
static resolveSsrClientBundleOptions(command: "build" | "start"): {
|
|
248
286
|
target: ClientBundleTarget;
|
|
249
287
|
external: readonly string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type { RoutesManifest } from "akanjs/server";
|
|
2
|
+
import { RoutesManifestStore } from "akanjs/server/artifact/routesManifestStore";
|
|
3
3
|
|
|
4
4
|
export type SerializedRoutesManifest = Omit<RoutesManifest, "knownEntries"> &
|
|
5
5
|
Partial<Pick<RoutesManifest, "knownEntries">>;
|
|
@@ -11,7 +11,7 @@ export class RoutesManifestArtifactSerializer {
|
|
|
11
11
|
|
|
12
12
|
constructor(manifest: RoutesManifest, artifactDir: string, options: { production?: boolean } = {}) {
|
|
13
13
|
this.#manifest = manifest;
|
|
14
|
-
this.#artifactDir =
|
|
14
|
+
this.#artifactDir = artifactDir;
|
|
15
15
|
this.#production = options.production ?? false;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -24,39 +24,8 @@ export class RoutesManifestArtifactSerializer {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
serialize(): SerializedRoutesManifest {
|
|
27
|
-
const serialized: SerializedRoutesManifest =
|
|
28
|
-
...this.#manifest,
|
|
29
|
-
knownEntries: this.#manifest.knownEntries.map((entry) => this.#serializeArtifactPath(entry)),
|
|
30
|
-
};
|
|
27
|
+
const serialized: SerializedRoutesManifest = RoutesManifestStore.serialize(this.#manifest, this.#artifactDir);
|
|
31
28
|
if (this.#production) delete serialized.knownEntries;
|
|
32
|
-
return
|
|
33
|
-
...serialized,
|
|
34
|
-
clientManifest: Object.fromEntries(
|
|
35
|
-
Object.entries(this.#manifest.clientManifest).map(([key, row]) => [this.#serializeArtifactPath(key), row]),
|
|
36
|
-
),
|
|
37
|
-
ssrManifest: {
|
|
38
|
-
...this.#manifest.ssrManifest,
|
|
39
|
-
moduleMap: Object.fromEntries(
|
|
40
|
-
Object.entries(this.#manifest.ssrManifest.moduleMap).map(([entryUrl, byName]) => [
|
|
41
|
-
entryUrl,
|
|
42
|
-
Object.fromEntries(
|
|
43
|
-
Object.entries(byName).map(([name, entry]) => [
|
|
44
|
-
name,
|
|
45
|
-
{
|
|
46
|
-
...entry,
|
|
47
|
-
id: this.#serializeArtifactPath(entry.id),
|
|
48
|
-
chunks: entry.chunks.map((chunk) => this.#serializeArtifactPath(chunk)),
|
|
49
|
-
} satisfies SsrManifestEntry,
|
|
50
|
-
]),
|
|
51
|
-
),
|
|
52
|
-
]),
|
|
53
|
-
),
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
#serializeArtifactPath(artifactPath: string): string {
|
|
59
|
-
if (!path.isAbsolute(artifactPath)) return artifactPath;
|
|
60
|
-
return path.relative(this.#artifactDir, artifactPath).split(path.sep).join("/");
|
|
29
|
+
return serialized;
|
|
61
30
|
}
|
|
62
31
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { type ServerGraph, ServerGraphFile } from "./serverGraphFile";
|
|
7
|
+
|
|
8
|
+
//? Resolved, as the graph holds real paths: on Windows a missing `/repo/...` resolves onto the current drive.
|
|
9
|
+
const at = (relative: string) => path.resolve("/repo/apps/a", relative);
|
|
10
|
+
|
|
11
|
+
const graph: ServerGraph = {
|
|
12
|
+
inputs: [at("page/_index.tsx"), at("lib/task.constant.ts"), at("ui/Card.tsx")],
|
|
13
|
+
clientExports: { [at("ui/Card.tsx")]: ["Card", "CardBody"] },
|
|
14
|
+
};
|
|
15
|
+
const touches = (files: string[], exportsNow: string[] | null = ["CardBody", "Card"]) =>
|
|
16
|
+
ServerGraphFile.touches(graph, files, () => exportsNow);
|
|
17
|
+
|
|
18
|
+
describe("ServerGraphFile.touches", () => {
|
|
19
|
+
test("a file the server graph never read changes nothing it renders", async () => {
|
|
20
|
+
expect(await touches([at("ui/OnlyClient.tsx")])).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("a module the server renders counts", async () => {
|
|
24
|
+
expect(await touches([at("lib/task.constant.ts")])).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a client module that keeps its export names does not count, in any order", async () => {
|
|
28
|
+
expect(await touches([at("ui/Card.tsx")])).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a client module whose names changed, or that stopped being one, counts", async () => {
|
|
32
|
+
expect(await touches([at("ui/Card.tsx")], ["Card"])).toBe(true);
|
|
33
|
+
expect(await touches([at("ui/Card.tsx")], null)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("without a graph every save counts", async () => {
|
|
37
|
+
expect(await ServerGraphFile.touches(null, [at("ui/OnlyClient.tsx")], () => null)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a changed file the build just read for the first time counts: a failed import was waiting for it", async () => {
|
|
41
|
+
const next: ServerGraph = { ...graph, inputs: [...graph.inputs, at("ui/card.ts")] };
|
|
42
|
+
expect(await ServerGraphFile.touches(graph, [at("ui/card.ts")], () => null, next)).toBe(true);
|
|
43
|
+
expect(await ServerGraphFile.touches(graph, [at("ui/card.ts")], () => null)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a failed build keeps the last good graph and carries its files to the next build's check", async () => {
|
|
47
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "akan-server-graph-"));
|
|
48
|
+
try {
|
|
49
|
+
await ServerGraphFile.write(dir, graph);
|
|
50
|
+
await ServerGraphFile.carry(dir, [at("page/_index.tsx")]);
|
|
51
|
+
const kept = await ServerGraphFile.read(dir);
|
|
52
|
+
expect(kept?.inputs).toEqual(graph.inputs);
|
|
53
|
+
expect(kept?.carried).toEqual([at("page/_index.tsx")]);
|
|
54
|
+
expect(await ServerGraphFile.touches(kept, [at("ui/Card.tsx")], () => ["Card", "CardBody"])).toBe(false);
|
|
55
|
+
const changed = [at("ui/Card.tsx"), ...(kept?.carried ?? [])];
|
|
56
|
+
expect(await ServerGraphFile.touches(kept, changed, () => ["Card", "CardBody"])).toBe(true);
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(dir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a graph written again inside one mtime tick is read afresh", async () => {
|
|
63
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "akan-server-graph-"));
|
|
64
|
+
try {
|
|
65
|
+
const file = path.join(dir, ServerGraphFile.fileName);
|
|
66
|
+
const tick = new Date(Math.floor(Date.now() / 1000) * 1000);
|
|
67
|
+
await ServerGraphFile.write(dir, graph);
|
|
68
|
+
fs.utimesSync(file, tick, tick);
|
|
69
|
+
await ServerGraphFile.read(dir);
|
|
70
|
+
await ServerGraphFile.carry(dir, [at("page/_index.tsx")]);
|
|
71
|
+
fs.utimesSync(file, tick, tick);
|
|
72
|
+
expect((await ServerGraphFile.read(dir))?.carried).toEqual([at("page/_index.tsx")]);
|
|
73
|
+
} finally {
|
|
74
|
+
await rm(dir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FileSys } from "../fileSys";
|
|
4
|
+
import { hasUseClientDirective, scanUseClientExports } from "../transforms/rscUseClientTransform";
|
|
5
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
6
|
+
|
|
7
|
+
export interface ServerGraph {
|
|
8
|
+
/** Real paths of the workspace sources the dev pages bundle read. */
|
|
9
|
+
inputs: string[];
|
|
10
|
+
/** Export names of every `"use client"` module among them: the server holds each only as a reference by name. */
|
|
11
|
+
clientExports: Record<string, string[]>;
|
|
12
|
+
/** Files saved in batches whose pages build failed since this graph was written: the next build's changes too. */
|
|
13
|
+
carried?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//* What the dev server bundle read, written by every dev pages build that succeeds, so a save can be told apart as one
|
|
17
|
+
//* the server renders (it needs an RSC refresh) or one only the client registry holds. A failed build keeps the last
|
|
18
|
+
//* good graph and carries its files over, so the fixing save still refreshes what the failed ones changed.
|
|
19
|
+
export class ServerGraphFile {
|
|
20
|
+
static readonly fileName = "server-graph.json";
|
|
21
|
+
static #cache: { file: string; stamp: string; graph: ServerGraph } | null = null;
|
|
22
|
+
|
|
23
|
+
static async read(artifactDir: string): Promise<ServerGraph | null> {
|
|
24
|
+
const file = path.join(artifactDir, ServerGraphFile.fileName);
|
|
25
|
+
const stat = fs.statSync(file, { throwIfNoEntry: false });
|
|
26
|
+
if (!stat) return null;
|
|
27
|
+
//? Not the mtime alone: two writes inside one mtime tick (seen on Linux) read back the first. A write replaces the
|
|
28
|
+
//? file, so its inode moves too.
|
|
29
|
+
const stamp = `${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
30
|
+
const cached = ServerGraphFile.#cache;
|
|
31
|
+
if (cached?.file === file && cached.stamp === stamp) return cached.graph;
|
|
32
|
+
const graph = (await Bun.file(file)
|
|
33
|
+
.json()
|
|
34
|
+
.catch(() => null)) as ServerGraph | null;
|
|
35
|
+
if (graph) ServerGraphFile.#cache = { file, stamp, graph };
|
|
36
|
+
return graph;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async write(artifactDir: string, graph: ServerGraph): Promise<void> {
|
|
40
|
+
const file = path.join(artifactDir, ServerGraphFile.fileName);
|
|
41
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
42
|
+
await Bun.write(temp, JSON.stringify(graph));
|
|
43
|
+
await FileSys.replace(temp, file);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static async carry(artifactDir: string, files: string[]): Promise<void> {
|
|
47
|
+
const graph = await ServerGraphFile.read(artifactDir);
|
|
48
|
+
if (!graph || files.length === 0) return;
|
|
49
|
+
const carried = [...new Set([...(graph.carried ?? []), ...files.map((file) => CsrDevPaths.realpath(file))])];
|
|
50
|
+
await ServerGraphFile.write(artifactDir, { ...graph, carried });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// `exportsNow` answers a client module's current export names, or null once it is no longer one. `next` is the graph
|
|
54
|
+
// the build just wrote: a changed file it reads for the first time (a module a failed import was waiting for) counts.
|
|
55
|
+
static async touches(
|
|
56
|
+
graph: ServerGraph | null,
|
|
57
|
+
files: string[],
|
|
58
|
+
exportsNow: (file: string) => string[] | null | Promise<string[] | null>,
|
|
59
|
+
next?: ServerGraph | null,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
if (!graph) return true;
|
|
62
|
+
const inputs = new Set(graph.inputs);
|
|
63
|
+
const joined = new Set((next?.inputs ?? []).filter((input) => !inputs.has(input)));
|
|
64
|
+
for (const rawFile of files) {
|
|
65
|
+
const file = CsrDevPaths.realpath(rawFile);
|
|
66
|
+
if (joined.has(file)) return true;
|
|
67
|
+
if (!inputs.has(file)) continue;
|
|
68
|
+
const before = graph.clientExports[file];
|
|
69
|
+
if (!before) return true;
|
|
70
|
+
//? The server holds a "use client" module only as references by name: an edit that keeps them renders the same.
|
|
71
|
+
const now = await exportsNow(file);
|
|
72
|
+
if (!now || ServerGraphFile.#names(now) !== ServerGraphFile.#names(before)) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static async clientExportsOf(file: string): Promise<string[] | null> {
|
|
78
|
+
const source = await Bun.file(file)
|
|
79
|
+
.text()
|
|
80
|
+
.catch(() => null);
|
|
81
|
+
if (source === null || !hasUseClientDirective(source)) return null;
|
|
82
|
+
try {
|
|
83
|
+
return scanUseClientExports(source, file);
|
|
84
|
+
} catch {
|
|
85
|
+
// A module the server can no longer read as client references is a server change; the pages build says why.
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static #names(exports: string[]): string {
|
|
91
|
+
return [...exports].sort().join(",");
|
|
92
|
+
}
|
|
93
|
+
}
|