@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.30
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 +2 -2
- package/README.md +3 -2
- 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 +404 -141
- package/akanConfig/akanConfig.ts +317 -274
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +21 -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 +269 -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 +177 -0
- package/mobile/desktopBin.ts +143 -0
- package/mobile/desktopServerStage.test.ts +204 -0
- package/mobile/desktopServerStage.ts +138 -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 +151 -0
- package/mobile/nativeApp.ts +405 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +414 -0
- package/mobile/nativeConfig.ts +296 -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 +30 -25
- package/subspace.ts +81 -115
- package/subspaceConfig.ts +11 -9
- 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
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { chmod, mkdir,
|
|
3
|
-
import os from "node:os";
|
|
2
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import type { Logger } from "akanjs/common";
|
|
5
|
+
import { tempDirs } from "../testHelpers";
|
|
6
6
|
import { type ChangeBatch, HmrWatcher } from "./hmrWatcher";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
* These run against the real Bun watcher rather than a fake, because the behaviour under test only
|
|
10
|
-
* exists there: Bun 1.3.14's recursive `fs.watch` reports about one path per ~200ms coalescing window and
|
|
11
|
-
* discards the rest (`local/optimize-resource/06-watcher-dropped-event.md`). A fake that delivered every
|
|
12
|
-
* event would pass no matter what the watcher did with them.
|
|
13
|
-
*/
|
|
8
|
+
// The real Bun watcher, not a fake: the dropped-event behaviour under test only exists there.
|
|
14
9
|
const STREAM_WARMUP_MS = 600;
|
|
15
10
|
const SETTLE_MS = 1_500;
|
|
16
11
|
const TEST_TIMEOUT_MS = 15_000;
|
|
17
12
|
|
|
18
13
|
const started: HmrWatcher[] = [];
|
|
19
|
-
const roots: string[] = [];
|
|
20
14
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
-
/** Every level `HmrWatcher` can reach, so adding a log line cannot fail a test for the wrong reason. */
|
|
22
15
|
const silentLogger = {
|
|
23
16
|
trace: () => undefined,
|
|
24
17
|
verbose: () => undefined,
|
|
@@ -29,12 +22,6 @@ const silentLogger = {
|
|
|
29
22
|
error: () => undefined,
|
|
30
23
|
} as unknown as Logger;
|
|
31
24
|
|
|
32
|
-
const makeRoot = async () => {
|
|
33
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-hmr-watcher-"));
|
|
34
|
-
roots.push(root);
|
|
35
|
-
return root;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
25
|
const seed = async (root: string, rel: string, content = "export const x = 1;\n") => {
|
|
39
26
|
const abs = path.join(root, rel);
|
|
40
27
|
await mkdir(path.dirname(abs), { recursive: true });
|
|
@@ -56,12 +43,63 @@ const watch = async (root: string) => {
|
|
|
56
43
|
return { watcher, batches, seen: () => new Set(batches.flatMap((batch) => batch.files)) };
|
|
57
44
|
};
|
|
58
45
|
|
|
59
|
-
afterEach(
|
|
46
|
+
afterEach(() => {
|
|
60
47
|
for (const watcher of started.splice(0)) watcher.stop();
|
|
61
|
-
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
62
48
|
});
|
|
49
|
+
const makeRoot = tempDirs("akan-hmr-watcher-");
|
|
63
50
|
|
|
64
51
|
describe("HmrWatcher", () => {
|
|
52
|
+
test(
|
|
53
|
+
"marks when a batch's window opened and when it was handed over",
|
|
54
|
+
async () => {
|
|
55
|
+
const root = await makeRoot();
|
|
56
|
+
const source = await seed(root, "lib/a.ts");
|
|
57
|
+
const { batches } = await watch(root);
|
|
58
|
+
|
|
59
|
+
const writtenAt = Date.now();
|
|
60
|
+
await writeFile(source, "export const x = 2;\n");
|
|
61
|
+
await sleep(SETTLE_MS);
|
|
62
|
+
|
|
63
|
+
const trace = batches.filter((batch) => batch.files.includes(source)).at(-1)?.trace;
|
|
64
|
+
expect(trace?.eventAt).toBeGreaterThanOrEqual(writtenAt);
|
|
65
|
+
expect(trace?.flushAt).toBeGreaterThanOrEqual(trace?.eventAt ?? Number.POSITIVE_INFINITY);
|
|
66
|
+
},
|
|
67
|
+
TEST_TIMEOUT_MS,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
test(
|
|
71
|
+
"takes its debounce window from AKAN_DEV_WATCH_DEBOUNCE_MS when no option names one",
|
|
72
|
+
async () => {
|
|
73
|
+
const previous = process.env.AKAN_DEV_WATCH_DEBOUNCE_MS;
|
|
74
|
+
process.env.AKAN_DEV_WATCH_DEBOUNCE_MS = "400";
|
|
75
|
+
try {
|
|
76
|
+
const root = await makeRoot();
|
|
77
|
+
const source = await seed(root, "lib/a.ts");
|
|
78
|
+
const batches: ChangeBatch[] = [];
|
|
79
|
+
// Without the verification scan, which would otherwise hand the batch over at 250ms.
|
|
80
|
+
const watcher = new HmrWatcher({
|
|
81
|
+
roots: [root],
|
|
82
|
+
logger: silentLogger,
|
|
83
|
+
verifyDelayMs: 0,
|
|
84
|
+
onBatch: (batch) => void batches.push(batch),
|
|
85
|
+
});
|
|
86
|
+
started.push(watcher);
|
|
87
|
+
await watcher.start();
|
|
88
|
+
await sleep(STREAM_WARMUP_MS);
|
|
89
|
+
|
|
90
|
+
await writeFile(source, "export const x = 3;\n");
|
|
91
|
+
await sleep(SETTLE_MS);
|
|
92
|
+
|
|
93
|
+
const trace = batches.find((batch) => batch.files.includes(source))?.trace;
|
|
94
|
+
expect((trace?.flushAt ?? 0) - (trace?.eventAt ?? 0)).toBeGreaterThanOrEqual(395);
|
|
95
|
+
} finally {
|
|
96
|
+
if (previous === undefined) delete process.env.AKAN_DEV_WATCH_DEBOUNCE_MS;
|
|
97
|
+
else process.env.AKAN_DEV_WATCH_DEBOUNCE_MS = previous;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
TEST_TIMEOUT_MS,
|
|
101
|
+
);
|
|
102
|
+
|
|
65
103
|
test(
|
|
66
104
|
"reports every file of a save-all",
|
|
67
105
|
async () => {
|
|
@@ -69,15 +107,12 @@ describe("HmrWatcher", () => {
|
|
|
69
107
|
const files = await Promise.all([0, 1, 2, 3, 4].map((i) => seed(root, `lib/File${i}.ts`)));
|
|
70
108
|
const { batches, seen, watcher } = await watch(root);
|
|
71
109
|
|
|
72
|
-
// No gaps between writes, so they share one coalescing window and Bun names at most one of them.
|
|
73
110
|
for (const [i, abs] of files.entries()) await writeFile(abs, `export const x = ${i}00;\n`);
|
|
74
111
|
await sleep(SETTLE_MS);
|
|
75
112
|
|
|
76
113
|
expect(batches.length).toBeGreaterThan(0);
|
|
77
114
|
for (const abs of files) expect([...seen()]).toContain(abs);
|
|
78
|
-
// Not
|
|
79
|
-
// events, so this suite keeps passing if the upstream defect is ever fixed. The counter is the
|
|
80
|
-
// signal for when the compensation can be removed.
|
|
115
|
+
// Not `> 0`: the suite must keep passing if Bun ever stops dropping events.
|
|
81
116
|
expect(watcher.unreportedChanges).toBeGreaterThanOrEqual(0);
|
|
82
117
|
},
|
|
83
118
|
TEST_TIMEOUT_MS,
|
|
@@ -90,8 +125,6 @@ describe("HmrWatcher", () => {
|
|
|
90
125
|
const source = await seed(root, "lib/a.ts");
|
|
91
126
|
const { seen, batches } = await watch(root);
|
|
92
127
|
|
|
93
|
-
// What every build ends with: a burst under `.akan/`, which the classifier ignores. Before this
|
|
94
|
-
// was handled, the burst was the one path Bun reported and the save right after it was invisible.
|
|
95
128
|
const artifactDir = path.join(root, ".akan", "artifact", "server");
|
|
96
129
|
await mkdir(artifactDir, { recursive: true });
|
|
97
130
|
for (let i = 0; i < 60; i++) await writeFile(path.join(artifactDir, `chunk-${i}.js`), "x".repeat(8192));
|
|
@@ -100,8 +133,6 @@ describe("HmrWatcher", () => {
|
|
|
100
133
|
await sleep(SETTLE_MS);
|
|
101
134
|
|
|
102
135
|
expect([...seen()]).toContain(source);
|
|
103
|
-
// Exactly one batch for one save. Bun does deliver a real event for some paths the scan has already
|
|
104
|
-
// emitted, and counting both produced a second generation and a second build for no change.
|
|
105
136
|
expect(batches.filter((batch) => batch.files.includes(source))).toHaveLength(1);
|
|
106
137
|
},
|
|
107
138
|
TEST_TIMEOUT_MS,
|
|
@@ -119,7 +150,6 @@ describe("HmrWatcher", () => {
|
|
|
119
150
|
for (let i = 0; i < 20; i++) await writeFile(path.join(artifactDir, `chunk-${i}.js`), "x".repeat(4096));
|
|
120
151
|
await sleep(SETTLE_MS);
|
|
121
152
|
|
|
122
|
-
// The verification scan is triggered by ignored paths, so it must not invent work from them either.
|
|
123
153
|
expect(batches).toEqual([]);
|
|
124
154
|
},
|
|
125
155
|
TEST_TIMEOUT_MS,
|
|
@@ -161,7 +191,7 @@ describe("HmrWatcher", () => {
|
|
|
161
191
|
TEST_TIMEOUT_MS,
|
|
162
192
|
);
|
|
163
193
|
|
|
164
|
-
test.skipIf(process.getuid?.() === 0)(
|
|
194
|
+
test.skipIf(process.getuid?.() === 0 || process.platform === "win32")(
|
|
165
195
|
"warns at startup when part of the tree cannot be read",
|
|
166
196
|
async () => {
|
|
167
197
|
const root = await makeRoot();
|
|
@@ -176,12 +206,9 @@ describe("HmrWatcher", () => {
|
|
|
176
206
|
});
|
|
177
207
|
started.push(watcher);
|
|
178
208
|
await watcher.start();
|
|
179
|
-
// Restored before asserting
|
|
180
|
-
// cannot traverse, and the leak would fail the *next* test instead of this one.
|
|
209
|
+
// Restored before asserting: a failed assertion would leave a directory `rm` cannot traverse.
|
|
181
210
|
await chmod(path.dirname(hidden), 0o755);
|
|
182
211
|
|
|
183
|
-
// At startup rather than at the first save, because an unreadable root means edits under it never
|
|
184
|
-
// rebuild — waiting for a save to reveal that means waiting for the save that silently does nothing.
|
|
185
212
|
expect(warnings).toHaveLength(1);
|
|
186
213
|
expect(warnings[0]).toContain("will not rebuild");
|
|
187
214
|
expect(warnings[0]).toContain("EACCES");
|
|
@@ -12,24 +12,12 @@ export interface WatcherOptions {
|
|
|
12
12
|
debounceMs?: number;
|
|
13
13
|
logger: Logger;
|
|
14
14
|
onBatch: (batch: ChangeBatch) => void | Promise<void>;
|
|
15
|
-
/**
|
|
16
|
-
* Delay before re-checking mtimes for changes `fs.watch` never reported. Must clear Bun's ~200ms
|
|
17
|
-
* coalescing window, or a write late in the same window as a delivered event stays invisible.
|
|
18
|
-
*/
|
|
15
|
+
/** Delay (ms, default 250) before the mtime re-check; must outlast Bun's ~200ms fs.watch coalescing window. */
|
|
19
16
|
verifyDelayMs?: number;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* classification coarse (`code` / `css` / `config`) so the orchestrator can
|
|
25
|
-
* decide whether a full rebuild-and-reload or a narrower action (e.g. CSS
|
|
26
|
-
* hot-swap) is sufficient.
|
|
27
|
-
*
|
|
28
|
-
* Bun's event payloads are treated as a hint, not the answer: its recursive `fs.watch` reports about one
|
|
29
|
-
* path per coalescing window and silently discards the rest, so what changed is resolved against a
|
|
30
|
-
* `SourceMtimeIndex` instead (see that class for the measurements). Events still drive *when* to look,
|
|
31
|
-
* which is what keeps this cheap — Bun does reliably deliver at least one event per window.
|
|
32
|
-
*/
|
|
19
|
+
// Bun delivers at least one fs.watch event per coalescing window but drops the other paths, so events decide
|
|
20
|
+
// when to look and the SourceMtimeIndex decides what changed.
|
|
33
21
|
export class HmrWatcher {
|
|
34
22
|
readonly #roots: string[];
|
|
35
23
|
readonly #debounceMs: number;
|
|
@@ -38,12 +26,12 @@ export class HmrWatcher {
|
|
|
38
26
|
readonly #logger: Logger;
|
|
39
27
|
readonly #watchers: fs.FSWatcher[] = [];
|
|
40
28
|
readonly #pending = new Map<string, Exclude<ChangeKind, "ignore">>();
|
|
41
|
-
/** Paths the watcher's own events named since the last batch — diagnostics only, see `#queue`. */
|
|
42
29
|
readonly #hinted = new Set<string>();
|
|
43
30
|
readonly #classifier = new HmrChangeClassifier();
|
|
44
31
|
readonly #index: SourceMtimeIndex;
|
|
45
32
|
#timer: ReturnType<typeof setTimeout> | null = null;
|
|
46
33
|
#verifyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
+
#windowStartedAt: number | null = null;
|
|
47
35
|
#stopped = false;
|
|
48
36
|
#flushing = false;
|
|
49
37
|
#unreportedChanges = 0;
|
|
@@ -52,26 +40,19 @@ export class HmrWatcher {
|
|
|
52
40
|
|
|
53
41
|
constructor(opts: WatcherOptions) {
|
|
54
42
|
this.#roots = [...new Set(opts.roots.map((r) => path.resolve(r)))];
|
|
55
|
-
this.#debounceMs = opts.debounceMs ??
|
|
43
|
+
this.#debounceMs = opts.debounceMs ?? HmrWatcher.#envDebounceMs() ?? 30;
|
|
56
44
|
this.#verifyDelayMs = opts.verifyDelayMs ?? 250;
|
|
57
45
|
this.#onBatch = opts.onBatch;
|
|
58
46
|
this.#logger = opts.logger;
|
|
59
47
|
this.#index = new SourceMtimeIndex({ roots: this.#roots, classifier: this.#classifier });
|
|
60
48
|
}
|
|
61
49
|
|
|
62
|
-
/**
|
|
63
|
-
* How many changes the mtime scan found that `fs.watch` never reported. Non-zero means this watcher is
|
|
64
|
-
* compensating for the Bun defect rather than the defect being absent, which is the number to watch if
|
|
65
|
-
* it is ever fixed upstream.
|
|
66
|
-
*/
|
|
50
|
+
/** Changes the mtime scan found that fs.watch never reported; non-zero means it is compensating for Bun. */
|
|
67
51
|
get unreportedChanges(): number {
|
|
68
52
|
return this.#unreportedChanges;
|
|
69
53
|
}
|
|
70
54
|
|
|
71
|
-
/**
|
|
72
|
-
* Watchers are installed before the baseline is taken, so an edit made during priming is reported by
|
|
73
|
-
* the event side even though the baseline already reflects it.
|
|
74
|
-
*/
|
|
55
|
+
/** Installs the watchers before priming, so an edit made while priming is still reported by the event side. */
|
|
75
56
|
async start(): Promise<void> {
|
|
76
57
|
for (const root of this.#roots) {
|
|
77
58
|
try {
|
|
@@ -88,8 +69,7 @@ export class HmrWatcher {
|
|
|
88
69
|
}
|
|
89
70
|
try {
|
|
90
71
|
await this.#index.prime();
|
|
91
|
-
//
|
|
92
|
-
// waiting for a save to surface it means waiting for a save that never rebuilds.
|
|
72
|
+
// At boot: edits under an unreadable root never rebuild, so no later save would surface it.
|
|
93
73
|
this.#reportCoverageGaps();
|
|
94
74
|
this.#logger.verbose(`[hmr] tracking ${this.#index.trackedFileCount} source files for change verification`);
|
|
95
75
|
} catch (err) {
|
|
@@ -112,23 +92,14 @@ export class HmrWatcher {
|
|
|
112
92
|
}
|
|
113
93
|
}
|
|
114
94
|
|
|
115
|
-
/**
|
|
116
|
-
* Adopt writes the batch handler made itself (regenerated barrels, inserted imports) so the
|
|
117
|
-
* verification scan does not spend a second generation rebuilding content this one already consumed.
|
|
118
|
-
*/
|
|
95
|
+
/** Baseline the batch handler's own writes so the verification scan does not rebuild them a second time. */
|
|
119
96
|
async absorb(paths: string[]): Promise<void> {
|
|
120
97
|
if (paths.length === 0) return;
|
|
121
98
|
await this.#index.absorb(paths);
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* used purely to decide when to look, and the mtime scan names the files.
|
|
127
|
-
*
|
|
128
|
-
* Taking the payload as well would double-report: Bun does deliver a real event for some of the paths a
|
|
129
|
-
* scan has already emitted, and adding it back here produced a second batch for the same save — one more
|
|
130
|
-
* generation and one more build for no change.
|
|
131
|
-
*/
|
|
101
|
+
// Once primed, a payload only schedules a scan: Bun also delivers events for paths the scan already emitted,
|
|
102
|
+
// so adding payloads to the batch double-reports a save.
|
|
132
103
|
#queue(abs: string): void {
|
|
133
104
|
const kind = this.#classifier.classify(abs);
|
|
134
105
|
if (!this.#index.primed) {
|
|
@@ -138,20 +109,18 @@ export class HmrWatcher {
|
|
|
138
109
|
this.#scheduleFlush();
|
|
139
110
|
return;
|
|
140
111
|
}
|
|
141
|
-
// An ignored path
|
|
142
|
-
// ends in a burst under `.akan/`, and the burst is what Bun reports instead of the save beside it.
|
|
143
|
-
// Rescheduling on each one folds a build's whole burst into a single scan once it goes quiet.
|
|
112
|
+
// An ignored path (a build's .akan/ burst) still marks a coalescing window that may hide a real save.
|
|
144
113
|
if (kind === "ignore") {
|
|
145
114
|
this.#scheduleVerify();
|
|
146
115
|
return;
|
|
147
116
|
}
|
|
148
|
-
//
|
|
149
|
-
// what is in a batch.
|
|
117
|
+
// Diagnostics only, for `unreportedChanges`; it never decides what is in a batch.
|
|
150
118
|
this.#hinted.add(abs);
|
|
151
119
|
this.#scheduleFlush();
|
|
152
120
|
}
|
|
153
121
|
|
|
154
122
|
#scheduleFlush(): void {
|
|
123
|
+
this.#windowStartedAt ??= Date.now();
|
|
155
124
|
if (this.#flushing) return;
|
|
156
125
|
if (this.#timer) clearTimeout(this.#timer);
|
|
157
126
|
this.#timer = setTimeout(() => this.#flush(), this.#debounceMs);
|
|
@@ -168,13 +137,19 @@ export class HmrWatcher {
|
|
|
168
137
|
try {
|
|
169
138
|
while (!this.#stopped) {
|
|
170
139
|
await this.#mergeDetectedChanges();
|
|
171
|
-
if (this.#pending.size === 0)
|
|
140
|
+
if (this.#pending.size === 0) {
|
|
141
|
+
this.#windowStartedAt = null;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
172
144
|
const files = Array.from(this.#pending.keys());
|
|
173
145
|
const kinds = new Set(this.#pending.values());
|
|
146
|
+
const flushAt = Date.now();
|
|
147
|
+
const trace = { eventAt: this.#windowStartedAt ?? flushAt, flushAt };
|
|
148
|
+
this.#windowStartedAt = null;
|
|
174
149
|
this.#pending.clear();
|
|
175
150
|
this.#hinted.clear();
|
|
176
151
|
try {
|
|
177
|
-
await this.#onBatch({ files, kinds });
|
|
152
|
+
await this.#onBatch({ files, kinds, trace });
|
|
178
153
|
} catch (e) {
|
|
179
154
|
this.#logger.error(`[hmr] onBatch error: ${(e as Error).message}`);
|
|
180
155
|
}
|
|
@@ -186,10 +161,6 @@ export class HmrWatcher {
|
|
|
186
161
|
}
|
|
187
162
|
}
|
|
188
163
|
|
|
189
|
-
/**
|
|
190
|
-
* Fold in everything the mtime index has seen change, since a delivered event names at most one of the
|
|
191
|
-
* paths that moved in its window.
|
|
192
|
-
*/
|
|
193
164
|
async #mergeDetectedChanges(): Promise<void> {
|
|
194
165
|
const detected = await this.#index.collectChanges().catch((err) => {
|
|
195
166
|
this.#logger.error(`[hmr] mtime scan failed: ${(err as Error).message}`);
|
|
@@ -205,8 +176,6 @@ export class HmrWatcher {
|
|
|
205
176
|
}
|
|
206
177
|
if (unreported === 0) return;
|
|
207
178
|
this.#unreportedChanges += unreported;
|
|
208
|
-
// Once at info, so it is visible that the watcher is compensating rather than the defect being absent;
|
|
209
|
-
// per-batch detail stays at verbose because a save-all trips this on every save.
|
|
210
179
|
if (!this.#reportedCompensating) {
|
|
211
180
|
this.#reportedCompensating = true;
|
|
212
181
|
this.#logger.verbose(
|
|
@@ -218,13 +187,6 @@ export class HmrWatcher {
|
|
|
218
187
|
);
|
|
219
188
|
}
|
|
220
189
|
|
|
221
|
-
/**
|
|
222
|
-
* Say so when part of the tree cannot be read, and say so again when it recovers.
|
|
223
|
-
*
|
|
224
|
-
* A blind spot here means edits under that path are not rebuilt at all, which is indistinguishable from
|
|
225
|
-
* the dev server being broken. Keyed on the gap list itself so a persistent failure logs once rather than
|
|
226
|
-
* once per save, while a *different* gap appearing still gets its own line.
|
|
227
|
-
*/
|
|
228
190
|
#reportCoverageGaps(): void {
|
|
229
191
|
const gaps = this.#index.coverageGaps;
|
|
230
192
|
const key = gaps
|
|
@@ -247,10 +209,13 @@ export class HmrWatcher {
|
|
|
247
209
|
);
|
|
248
210
|
}
|
|
249
211
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
212
|
+
static #envDebounceMs(): number | null {
|
|
213
|
+
const raw = process.env.AKAN_DEV_WATCH_DEBOUNCE_MS;
|
|
214
|
+
const ms = raw ? Number(raw) : Number.NaN;
|
|
215
|
+
return Number.isInteger(ms) && ms >= 0 ? ms : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A write in the same window as a delivered event raises no event of its own, so one scan follows each window.
|
|
254
219
|
#scheduleVerify(): void {
|
|
255
220
|
if (this.#stopped || this.#verifyDelayMs <= 0) return;
|
|
256
221
|
if (this.#verifyTimer) clearTimeout(this.#verifyTimer);
|
|
@@ -260,16 +225,8 @@ export class HmrWatcher {
|
|
|
260
225
|
}, this.#verifyDelayMs);
|
|
261
226
|
}
|
|
262
227
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
* artifacts under `.akan/` — which the classifier ignores — while codegen writes are content-guarded,
|
|
266
|
-
* so a rebuild does not move a tracked mtime.
|
|
267
|
-
*
|
|
268
|
-
* The one case that does schedule another is a directory the index could not date reliably (Linux stamps
|
|
269
|
-
* directory mtimes on a 1ms clock, see `SourceMtimeIndex`). That still terminates: the next scan is
|
|
270
|
-
* `verifyDelayMs` later, by which point the timestamp has settled unless something is writing to that
|
|
271
|
-
* directory right now — in which case looking again is the right answer anyway.
|
|
272
|
-
*/
|
|
228
|
+
// Terminates: build output lands in ignored .akan/ and codegen writes are content-guarded, so a rebuild moves no
|
|
229
|
+
// tracked mtime; only an unsettled directory schedules another scan.
|
|
273
230
|
async #verify(): Promise<void> {
|
|
274
231
|
if (this.#stopped || this.#flushing) return;
|
|
275
232
|
await this.#mergeDetectedChanges();
|
package/frontendBuild/index.ts
CHANGED
|
@@ -4,10 +4,12 @@ export * from "./clientBuildTypes";
|
|
|
4
4
|
export * from "./clientEntriesBundler";
|
|
5
5
|
export * from "./clientEntryDiscovery";
|
|
6
6
|
export * from "./csrArtifactBuilder";
|
|
7
|
+
export * from "./csrDevBundler";
|
|
7
8
|
export * from "./cssCompiler";
|
|
8
9
|
export * from "./cssImportResolver";
|
|
9
10
|
export * from "./devChangePlanner";
|
|
10
11
|
export * from "./devGeneratedIndexSync";
|
|
12
|
+
export * from "./devRegistryBundler";
|
|
11
13
|
export * from "./fontOptimizer";
|
|
12
14
|
export * from "./fontPruner";
|
|
13
15
|
export * from "./hmrChangeClassifier";
|
|
@@ -17,8 +19,10 @@ export * from "./pagesEntrySourceGenerator";
|
|
|
17
19
|
export * from "./precompressArtifacts";
|
|
18
20
|
export * from "./routeClientBuilder";
|
|
19
21
|
export * from "./routesManifestArtifactSerializer";
|
|
22
|
+
export * from "./serverGraphFile";
|
|
20
23
|
export * from "./sourceMtimeIndex";
|
|
21
24
|
export * from "./ssrBaseArtifactBuilder";
|
|
25
|
+
export * from "./ssrDevBundler";
|
|
22
26
|
export * from "./styleContract";
|
|
23
27
|
export * from "./themeValidator";
|
|
24
28
|
export * from "./vendorSpecifiers";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { BunPlugin } from "bun";
|
|
3
4
|
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
@@ -5,18 +6,17 @@ import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
|
5
6
|
import type { App } from "../commandDecorators";
|
|
6
7
|
import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
7
8
|
import { createExternalizeFrameworkPlugin } from "../transforms/externalizeFrameworkPlugin";
|
|
9
|
+
import { loaderFor } from "../transforms/moduleSyntax";
|
|
8
10
|
import { transformUseClient } from "../transforms/rscUseClientTransform";
|
|
9
11
|
import { createUseClientBundlePlugin } from "../transforms/useClientBundlePlugin";
|
|
12
|
+
import { bundleDefine } from "./bundleDefine";
|
|
13
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
10
14
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
15
|
+
import { ServerGraphFile } from "./serverGraphFile";
|
|
11
16
|
|
|
12
17
|
export interface BuildPagesBundleResult {
|
|
13
|
-
/** Absolute path to the emitted `pages-[hash].js`. */
|
|
14
18
|
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
|
-
*/
|
|
19
|
+
/** Cache-bust value for `import(bundlePath?v=<buildId>)`. */
|
|
20
20
|
buildId: number;
|
|
21
21
|
splitting: boolean;
|
|
22
22
|
entryBytes: number;
|
|
@@ -27,10 +27,6 @@ export interface BuildPagesBundleResult {
|
|
|
27
27
|
|
|
28
28
|
const VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Build the server-side pages bundle. The RSC worker loads the result with
|
|
32
|
-
* `await import(bundlePath?v=buildId)`.
|
|
33
|
-
*/
|
|
34
30
|
export class PagesBundleBuilder {
|
|
35
31
|
#app: App;
|
|
36
32
|
#command: "build" | "start";
|
|
@@ -49,6 +45,13 @@ export class PagesBundleBuilder {
|
|
|
49
45
|
this.#pageEntries ?? (await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys()));
|
|
50
46
|
const entrySource = PagesEntrySourceGenerator.generate(resolvedEntries);
|
|
51
47
|
const workspaceRoot = this.#app.workspace.workspaceRoot;
|
|
48
|
+
const dev = this.#command === "start";
|
|
49
|
+
const clientExports: Record<string, string[]> = {};
|
|
50
|
+
const onClientModule = dev
|
|
51
|
+
? (file: string, exports: string[]) => {
|
|
52
|
+
clientExports[CsrDevPaths.realpath(file)] = exports;
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
52
55
|
const result = await Bun.build({
|
|
53
56
|
entrypoints: [VIRTUAL_PAGES_ENTRY],
|
|
54
57
|
outdir: `${this.#artifactDir}/server`,
|
|
@@ -61,7 +64,8 @@ export class PagesBundleBuilder {
|
|
|
61
64
|
chunk: "chunks/[name]-[hash].[ext]",
|
|
62
65
|
asset: "assets/[name]-[hash].[ext]",
|
|
63
66
|
},
|
|
64
|
-
define: this.#
|
|
67
|
+
define: bundleDefine(this.#app, this.#command, "ssr"),
|
|
68
|
+
metafile: dev,
|
|
65
69
|
plugins: [
|
|
66
70
|
PagesBundleBuilder.createPagesEntryPlugin(entrySource),
|
|
67
71
|
PagesBundleBuilder.createCssStubPlugin(),
|
|
@@ -73,9 +77,10 @@ export class PagesBundleBuilder {
|
|
|
73
77
|
transformUseClient(source, {
|
|
74
78
|
path: args.path,
|
|
75
79
|
workspaceRoot,
|
|
80
|
+
onClientModule,
|
|
76
81
|
}),
|
|
77
82
|
})
|
|
78
|
-
: createUseClientBundlePlugin({ workspaceRoot }),
|
|
83
|
+
: createUseClientBundlePlugin({ workspaceRoot, onClientModule }),
|
|
79
84
|
],
|
|
80
85
|
});
|
|
81
86
|
|
|
@@ -86,6 +91,8 @@ export class PagesBundleBuilder {
|
|
|
86
91
|
|
|
87
92
|
const bundlePath = path.resolve(entryArtifact.path);
|
|
88
93
|
const buildId = Date.now();
|
|
94
|
+
if (dev && result.metafile)
|
|
95
|
+
await ServerGraphFile.write(this.#artifactDir, this.#serverGraph(result.metafile, clientExports));
|
|
89
96
|
const outputBytes = result.outputs.reduce((sum, output) => sum + output.size, 0);
|
|
90
97
|
const chunkCount = result.outputs.filter((output) => output.kind === "chunk").length;
|
|
91
98
|
this.#app.verbose(
|
|
@@ -102,6 +109,15 @@ export class PagesBundleBuilder {
|
|
|
102
109
|
};
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
#serverGraph(metafile: Bun.BuildMetafile, clientExports: Record<string, string[]>) {
|
|
113
|
+
const inputs = Object.keys(metafile.inputs)
|
|
114
|
+
.filter((input) => !input.includes("node_modules"))
|
|
115
|
+
.map((input) => path.resolve(input))
|
|
116
|
+
.filter((input) => fs.existsSync(input))
|
|
117
|
+
.map((input) => CsrDevPaths.realpath(input));
|
|
118
|
+
return { inputs, clientExports };
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
get #artifactDir(): string {
|
|
106
122
|
return `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
|
|
107
123
|
}
|
|
@@ -110,17 +126,6 @@ export class PagesBundleBuilder {
|
|
|
110
126
|
return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
#define(): Record<string, string> {
|
|
114
|
-
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
115
|
-
return {
|
|
116
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
117
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
|
|
118
|
-
...Object.fromEntries(
|
|
119
|
-
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
120
|
-
),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
129
|
static createPagesEntryPlugin(source: string): BunPlugin {
|
|
125
130
|
return {
|
|
126
131
|
name: "akan-pages-entry",
|
|
@@ -176,10 +181,3 @@ export class PagesBundleBuilder {
|
|
|
176
181
|
);
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
|
-
|
|
180
|
-
function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
|
|
181
|
-
if (absPath.endsWith(".tsx")) return "tsx";
|
|
182
|
-
if (absPath.endsWith(".jsx")) return "jsx";
|
|
183
|
-
if (absPath.endsWith(".ts")) return "ts";
|
|
184
|
-
return "js";
|
|
185
|
-
}
|
|
@@ -2,11 +2,19 @@ import path from "node:path";
|
|
|
2
2
|
import type { PageEntry } from "../artifact/implicitRootLayout";
|
|
3
3
|
import { AsyncDefaultExportDetector } from "../transforms/asyncDefaultExportDetector";
|
|
4
4
|
|
|
5
|
+
export interface PagesEntrySourceOptions {
|
|
6
|
+
/** Detects an async default export; a caller that outlives one generation keeps its own cache across saves. */
|
|
7
|
+
isAsyncDefault?: (moduleAbsPath: string) => Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export class PagesEntrySourceGenerator {
|
|
6
11
|
#pageEntries: PageEntry[];
|
|
12
|
+
readonly #asyncDefaults = new Map<string, Promise<boolean>>();
|
|
13
|
+
readonly #detect: (moduleAbsPath: string) => Promise<boolean>;
|
|
7
14
|
|
|
8
|
-
constructor(pageEntries: PageEntry[]) {
|
|
15
|
+
constructor(pageEntries: PageEntry[], { isAsyncDefault }: PagesEntrySourceOptions = {}) {
|
|
9
16
|
this.#pageEntries = pageEntries;
|
|
17
|
+
this.#detect = isAsyncDefault ?? ((moduleAbsPath) => AsyncDefaultExportDetector.detect(moduleAbsPath));
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
static generate(pageEntries: PageEntry[]): string {
|
|
@@ -21,25 +29,67 @@ export class PagesEntrySourceGenerator {
|
|
|
21
29
|
return `export const pages = {\n${lines.join("\n")}\n};\n`;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
static async generateStatic(pageEntries: PageEntry[]): Promise<string> {
|
|
25
|
-
return await new PagesEntrySourceGenerator(pageEntries).generateStatic();
|
|
32
|
+
static async generateStatic(pageEntries: PageEntry[], { fromDir }: { fromDir?: string } = {}): Promise<string> {
|
|
33
|
+
return await new PagesEntrySourceGenerator(pageEntries).generateStatic({ fromDir });
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
// `fromDir` makes the specifiers relative: Bun inlines an absolute import of a file that is also an entrypoint.
|
|
37
|
+
async generateStatic({ fromDir }: { fromDir?: string } = {}): Promise<string> {
|
|
29
38
|
const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
|
|
30
|
-
const specifier =
|
|
39
|
+
const specifier = fromDir
|
|
40
|
+
? PagesEntrySourceGenerator.#toRelativeSpecifier(fromDir, moduleAbsPath)
|
|
41
|
+
: PagesEntrySourceGenerator.#toImportSpecifier(moduleAbsPath);
|
|
31
42
|
return `import * as page${index} from ${JSON.stringify(specifier)};`;
|
|
32
43
|
});
|
|
33
44
|
const entries = await Promise.all(
|
|
34
45
|
this.#pageEntries.map(async ({ key, moduleAbsPath }, index) => {
|
|
35
|
-
const isAsyncDefault = await
|
|
46
|
+
const isAsyncDefault = await this.#isAsyncDefault(moduleAbsPath);
|
|
36
47
|
return ` ${JSON.stringify(key)}: { loader: async () => page${index}, isAsyncDefault: ${isAsyncDefault} },`;
|
|
37
48
|
}),
|
|
38
49
|
);
|
|
39
50
|
return `${imports.join("\n")}\nexport const pages = {\n${entries.join("\n")}\n};\n`;
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
// Route HMR in the dev registry bundle: `akanWebkit` is the entry's namespace import of `akanjs/webkit`.
|
|
54
|
+
async generateHotReplace({
|
|
55
|
+
fromDir,
|
|
56
|
+
ownerId,
|
|
57
|
+
moduleIds,
|
|
58
|
+
}: {
|
|
59
|
+
fromDir: string;
|
|
60
|
+
ownerId: string;
|
|
61
|
+
moduleIds: string[];
|
|
62
|
+
}): Promise<string> {
|
|
63
|
+
const entries = await Promise.all(
|
|
64
|
+
this.#pageEntries.map(async ({ key, moduleAbsPath }) => {
|
|
65
|
+
const specifier = PagesEntrySourceGenerator.#toRelativeSpecifier(fromDir, moduleAbsPath);
|
|
66
|
+
const isAsyncDefault = await this.#isAsyncDefault(moduleAbsPath);
|
|
67
|
+
return ` ${JSON.stringify(key)}: { loader: async () => require(${JSON.stringify(specifier)}), isAsyncDefault: ${isAsyncDefault} },`;
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
return `__akan.accept(${JSON.stringify(ownerId)}, ${JSON.stringify(moduleIds)}, async () => {
|
|
71
|
+
const replaced = await akanWebkit.replacePages({
|
|
72
|
+
${entries.join("\n")}
|
|
73
|
+
});
|
|
74
|
+
if (!replaced) throw new Error("the route table no longer matches the pages it was built from");
|
|
75
|
+
});
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#isAsyncDefault(moduleAbsPath: string): Promise<boolean> {
|
|
80
|
+
const known = this.#asyncDefaults.get(moduleAbsPath);
|
|
81
|
+
if (known) return known;
|
|
82
|
+
const detected = this.#detect(moduleAbsPath);
|
|
83
|
+
this.#asyncDefaults.set(moduleAbsPath, detected);
|
|
84
|
+
return detected;
|
|
85
|
+
}
|
|
86
|
+
|
|
42
87
|
static #toImportSpecifier(moduleAbsPath: string): string {
|
|
43
88
|
return path.resolve(moduleAbsPath).split(path.sep).join("/");
|
|
44
89
|
}
|
|
90
|
+
|
|
91
|
+
static #toRelativeSpecifier(fromDir: string, moduleAbsPath: string): string {
|
|
92
|
+
const relative = path.relative(fromDir, path.resolve(moduleAbsPath)).split(path.sep).join("/");
|
|
93
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
94
|
+
}
|
|
45
95
|
}
|