@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
|
@@ -1,61 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
|
|
2
4
|
import type { App } from "../commandDecorators";
|
|
3
5
|
import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
|
|
4
6
|
import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
|
|
5
7
|
import type { AkanConfig, ClientEntryDiscovery, ScannedImport } from "./clientBuildTypes";
|
|
6
8
|
|
|
7
9
|
const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
|
|
8
|
-
const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
9
10
|
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
10
11
|
const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
|
|
11
|
-
//
|
|
12
|
-
// the bundler handles via dedicated loaders. Skipping them avoids resolver
|
|
13
|
-
// noise (Bun's scanImports surfaces CSS / asset imports too).
|
|
12
|
+
// Bun's scanImports also surfaces CSS and asset imports, none of which can be a client component.
|
|
14
13
|
const NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
|
|
15
14
|
type PackageResolver = Awaited<ReturnType<typeof createTsconfigPackageResolver>>;
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
* Everything the traversal ever asks of a file, which is why its text is not kept.
|
|
19
|
-
*
|
|
20
|
-
* `imports` is empty for a client entry: the walk stops there, so they are never scanned.
|
|
21
|
-
*/
|
|
22
16
|
interface FileFacts {
|
|
23
17
|
isClientEntry: boolean;
|
|
24
18
|
imports: ScannedImport[];
|
|
25
19
|
}
|
|
26
20
|
|
|
21
|
+
interface Lookup<T> {
|
|
22
|
+
epoch: number;
|
|
23
|
+
promise: Promise<T>;
|
|
24
|
+
found?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DiscoveryWalk {
|
|
28
|
+
visiting: Set<string>;
|
|
29
|
+
//? Walked below a file the walk was already inside (an import cycle): their result lacks what that file reaches.
|
|
30
|
+
partial: Set<string>;
|
|
31
|
+
//? Every file's result within this walk, partial ones included: the file the cycle went back to is an ancestor here
|
|
32
|
+
//? and brings the rest itself, and re-walking each path through a dense cycle grows exponentially.
|
|
33
|
+
memo: Map<string, Set<string>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
const shouldSkipNodeModule = (absPath: string) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
|
|
28
37
|
|
|
29
|
-
/**
|
|
30
|
-
* Graph-based `"use client"` discovery, seeded from an explicit file list.
|
|
31
|
-
*
|
|
32
|
-
* Walks imports — including dynamic `import()` — while flattening barrel
|
|
33
|
-
* specifiers through the same `BarrelAnalyzer` the runtime plugin uses, so
|
|
34
|
-
* the traversal matches the module graph the bundler will actually see.
|
|
35
|
-
*/
|
|
38
|
+
/** `"use client"` discovery over the import graph (dynamic imports included), flattening barrels as the bundler does. */
|
|
36
39
|
export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
37
40
|
#akanConfig: Pick<AkanConfig, "barrelImports">;
|
|
38
41
|
#resolvePackage: PackageResolver;
|
|
39
42
|
#analyzer: BarrelAnalyzer;
|
|
40
43
|
#tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
41
|
-
#fileExistsCache = new Map<string,
|
|
42
|
-
|
|
43
|
-
* Derived facts per file, never the source text.
|
|
44
|
-
*
|
|
45
|
-
* This instance lives as long as the builder process does — it is rebuilt only when the akan config
|
|
46
|
-
* changes — so caching whole source texts (and a second, barrel-rewritten copy of each) meant the
|
|
47
|
-
* watcher held every source file it had ever walked for the whole dev session. Nothing downstream
|
|
48
|
-
* wanted the text: the walk needs one boolean and one import list per file, and both were already
|
|
49
|
-
* cached separately under the same key.
|
|
50
|
-
*/
|
|
44
|
+
#fileExistsCache = new Map<string, Lookup<boolean>>();
|
|
45
|
+
// Facts, never source text: this instance lives as long as the builder process, so texts would pin every file walked.
|
|
51
46
|
#factsCache = new Map<string, Promise<FileFacts | null>>();
|
|
52
|
-
#resolvedFileCache = new Map<string,
|
|
53
|
-
#resolvedSpecifierCache = new Map<string,
|
|
47
|
+
#resolvedFileCache = new Map<string, Lookup<string | null>>();
|
|
48
|
+
#resolvedSpecifierCache = new Map<string, Lookup<string | null>>();
|
|
54
49
|
#reachableEntriesCache = new Map<string, Set<string>>();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
#
|
|
50
|
+
#readFiles = new Set<string>();
|
|
51
|
+
//? Bumped by every invalidation: a walk that began before one computed from facts it may have read stale, so it
|
|
52
|
+
//? answers its caller but caches nothing (a registry check walks this instance beside the slow lane's route builds).
|
|
53
|
+
#epoch = 0;
|
|
59
54
|
|
|
60
55
|
constructor(akanConfig: Pick<AkanConfig, "barrelImports">, resolvePackage: PackageResolver) {
|
|
61
56
|
this.#akanConfig = akanConfig;
|
|
@@ -70,61 +65,63 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
70
65
|
async discover(seeds: string[]): Promise<string[]> {
|
|
71
66
|
const entries = new Set<string>();
|
|
72
67
|
for (const seed of seeds) {
|
|
73
|
-
|
|
68
|
+
const walk: DiscoveryWalk = { visiting: new Set(), partial: new Set(), memo: new Map() };
|
|
69
|
+
for (const entry of await this.#discoverFromFile(seed, walk)) entries.add(entry);
|
|
74
70
|
}
|
|
75
71
|
return Array.from(entries).sort();
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
invalidate(files: string[]): void {
|
|
75
|
+
if (files.length === 0) return;
|
|
76
|
+
let moved = false;
|
|
79
77
|
for (const file of files) {
|
|
80
78
|
const absPath = path.resolve(file);
|
|
79
|
+
const exists = fs.existsSync(absPath);
|
|
80
|
+
if (exists !== this.#readFiles.has(absPath)) moved = true;
|
|
81
|
+
if (!exists) this.#readFiles.delete(absPath);
|
|
81
82
|
this.#factsCache.delete(absPath);
|
|
82
83
|
this.#fileExistsCache.delete(absPath);
|
|
83
|
-
this.#reachableEntriesCache.delete(absPath);
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
// child can affect any reachable-entry cache above it.
|
|
85
|
+
this.#epoch += 1;
|
|
86
|
+
// Reachable-entry results are transitive, so a changed child invalidates every ancestor.
|
|
88
87
|
this.#reachableEntriesCache.clear();
|
|
89
|
-
|
|
88
|
+
//? A file gone, or one never read, can leave a found resolution on its old target (`Chart.tsx` to
|
|
89
|
+
//? `Chart/index.tsx`), so a move drops them all; a save of a file already read keeps them (a walk re-resolving
|
|
90
|
+
//? every package cost 3x). Misses lapse with the epoch on their own (#lookup).
|
|
91
|
+
if (!moved) return;
|
|
92
|
+
this.#fileExistsCache.clear();
|
|
93
|
+
this.#resolvedFileCache.clear();
|
|
94
|
+
this.#resolvedSpecifierCache.clear();
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
//? A found answer outlives later epochs; a miss, or a lookup still in flight, answers only walks of its own epoch, so
|
|
98
|
+
//? a file created meanwhile is looked up again.
|
|
99
|
+
#lookup<T>(cache: Map<string, Lookup<T>>, key: string, run: () => Promise<T>, isFound: (value: T) => boolean) {
|
|
100
|
+
const cached = cache.get(key);
|
|
101
|
+
if (cached && (cached.found || cached.epoch === this.#epoch)) return cached.promise;
|
|
102
|
+
const entry: Lookup<T> = { epoch: this.#epoch, promise: run() };
|
|
103
|
+
entry.promise.then(
|
|
104
|
+
(value) => {
|
|
105
|
+
entry.found = isFound(value);
|
|
106
|
+
},
|
|
107
|
+
() => {
|
|
108
|
+
entry.found = false;
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
cache.set(key, entry);
|
|
112
|
+
return entry.promise;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
async #fileExists(p: string): Promise<boolean> {
|
|
110
116
|
const absPath = path.resolve(p);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return exists;
|
|
118
|
-
});
|
|
119
|
-
this.#fileExistsCache.set(absPath, cached);
|
|
120
|
-
}
|
|
121
|
-
return cached;
|
|
117
|
+
return await this.#lookup(
|
|
118
|
+
this.#fileExistsCache,
|
|
119
|
+
absPath,
|
|
120
|
+
() => Bun.file(absPath).exists(),
|
|
121
|
+
(exists) => exists,
|
|
122
|
+
);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
/**
|
|
125
|
-
* Read a file once and keep only what the traversal asks of it. The text itself is dropped as soon
|
|
126
|
-
* as the boolean and the import list are out of it — see `#factsCache`.
|
|
127
|
-
*/
|
|
128
125
|
#facts(file: string): Promise<FileFacts | null> {
|
|
129
126
|
const absPath = path.resolve(file);
|
|
130
127
|
let cached = this.#factsCache.get(absPath);
|
|
@@ -134,7 +131,7 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
134
131
|
.text()
|
|
135
132
|
.catch(() => null);
|
|
136
133
|
if (content === null) return null;
|
|
137
|
-
|
|
134
|
+
this.#readFiles.add(absPath);
|
|
138
135
|
if (USE_CLIENT_RE.test(content)) return { isClientEntry: true, imports: [] };
|
|
139
136
|
return { isClientEntry: false, imports: this.#scanImports(await this.#rewrite(content)) };
|
|
140
137
|
})();
|
|
@@ -162,57 +159,71 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
162
159
|
|
|
163
160
|
async #resolveFileCandidate(absPathNoExt: string): Promise<string | null> {
|
|
164
161
|
const cacheKey = path.resolve(absPathNoExt);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
162
|
+
return await this.#lookup(
|
|
163
|
+
this.#resolvedFileCache,
|
|
164
|
+
cacheKey,
|
|
165
|
+
async () => {
|
|
166
|
+
if (await this.#fileExists(cacheKey)) return cacheKey;
|
|
167
|
+
for (const ext of SOURCE_EXTS) {
|
|
168
|
+
const f = `${cacheKey}${ext}`;
|
|
169
|
+
if (await this.#fileExists(f)) return f;
|
|
170
|
+
}
|
|
171
|
+
for (const ext of SOURCE_EXTS) {
|
|
172
|
+
const f = path.join(cacheKey, `index${ext}`);
|
|
173
|
+
if (await this.#fileExists(f)) return f;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
},
|
|
177
|
+
(file) => file !== null,
|
|
178
|
+
);
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
async #resolveSpecifier(spec: string, importerDir: string): Promise<string | null> {
|
|
185
182
|
const cacheKey = `${importerDir}\0${spec}`;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return cached;
|
|
183
|
+
return await this.#lookup(
|
|
184
|
+
this.#resolvedSpecifierCache,
|
|
185
|
+
cacheKey,
|
|
186
|
+
async () => {
|
|
187
|
+
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
188
|
+
const abs = spec.startsWith("/") ? spec : path.resolve(importerDir, spec);
|
|
189
|
+
return await this.#resolveFileCandidate(abs);
|
|
190
|
+
}
|
|
191
|
+
const pkg = await this.#resolvePackage(spec);
|
|
192
|
+
return pkg ? pkg.entryFile : null;
|
|
193
|
+
},
|
|
194
|
+
(file) => file !== null,
|
|
195
|
+
);
|
|
200
196
|
}
|
|
201
197
|
|
|
202
|
-
async #discoverFromFile(file: string,
|
|
198
|
+
async #discoverFromFile(file: string, walk: DiscoveryWalk): Promise<Set<string>> {
|
|
199
|
+
const epoch = this.#epoch;
|
|
203
200
|
const absPath = path.resolve(file);
|
|
204
201
|
const cached = this.#reachableEntriesCache.get(absPath);
|
|
205
202
|
if (cached) return new Set(cached);
|
|
206
|
-
|
|
203
|
+
const memo = walk.memo.get(absPath);
|
|
204
|
+
if (memo) {
|
|
205
|
+
//? Reused from another path: whatever this path is walking now inherits its gap and stays out of the cache.
|
|
206
|
+
if (walk.partial.has(absPath)) for (const onPath of walk.visiting) walk.partial.add(onPath);
|
|
207
|
+
return new Set(memo);
|
|
208
|
+
}
|
|
209
|
+
if (walk.visiting.has(absPath)) {
|
|
210
|
+
let below = false;
|
|
211
|
+
for (const onPath of walk.visiting) {
|
|
212
|
+
if (below) walk.partial.add(onPath);
|
|
213
|
+
else below = onPath === absPath;
|
|
214
|
+
}
|
|
215
|
+
return new Set();
|
|
216
|
+
}
|
|
217
|
+
if (shouldSkipNodeModule(absPath)) return new Set();
|
|
207
218
|
|
|
208
|
-
visiting.add(absPath);
|
|
219
|
+
walk.visiting.add(absPath);
|
|
209
220
|
const entries = new Set<string>();
|
|
210
221
|
const facts = await this.#facts(absPath);
|
|
211
|
-
if (!facts) return this.#finishDiscovery(absPath,
|
|
222
|
+
if (!facts) return this.#finishDiscovery(absPath, walk, entries, epoch);
|
|
212
223
|
|
|
213
224
|
if (facts.isClientEntry) {
|
|
214
225
|
entries.add(absPath);
|
|
215
|
-
return this.#finishDiscovery(absPath,
|
|
226
|
+
return this.#finishDiscovery(absPath, walk, entries, epoch);
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
const importerDir = path.dirname(absPath);
|
|
@@ -223,15 +234,16 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
|
|
|
223
234
|
const resolved = await this.#resolveSpecifier(spec, importerDir);
|
|
224
235
|
if (!resolved) continue;
|
|
225
236
|
if (shouldSkipNodeModule(resolved)) continue;
|
|
226
|
-
for (const entry of await this.#discoverFromFile(resolved,
|
|
237
|
+
for (const entry of await this.#discoverFromFile(resolved, walk)) entries.add(entry);
|
|
227
238
|
}
|
|
228
239
|
|
|
229
|
-
return this.#finishDiscovery(absPath,
|
|
240
|
+
return this.#finishDiscovery(absPath, walk, entries, epoch);
|
|
230
241
|
}
|
|
231
242
|
|
|
232
|
-
#finishDiscovery(absPath: string,
|
|
233
|
-
visiting.delete(absPath);
|
|
234
|
-
|
|
243
|
+
#finishDiscovery(absPath: string, walk: DiscoveryWalk, entries: Set<string>, epoch: number): Set<string> {
|
|
244
|
+
walk.visiting.delete(absPath);
|
|
245
|
+
walk.memo.set(absPath, entries);
|
|
246
|
+
if (epoch === this.#epoch && !walk.partial.has(absPath)) this.#reachableEntriesCache.set(absPath, entries);
|
|
235
247
|
return new Set(entries);
|
|
236
248
|
}
|
|
237
249
|
}
|
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
4
4
|
import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
|
-
import {
|
|
6
|
+
import { bundleDefine } from "./bundleDefine";
|
|
7
|
+
import { CsrEntryFiles } from "./csrEntryFiles";
|
|
7
8
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
8
9
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
9
10
|
|
|
@@ -61,10 +62,9 @@ export class CsrArtifactBuilder {
|
|
|
61
62
|
splitting: false,
|
|
62
63
|
minify: true,
|
|
63
64
|
env: "AKAN_PUBLIC_*",
|
|
64
|
-
define: this.#
|
|
65
|
+
define: bundleDefine(this.#app, this.#command, "csr"),
|
|
65
66
|
optimizeImports: akanConfig.optimizeImports,
|
|
66
|
-
//
|
|
67
|
-
// the route graph is Tailwind source, and every root layout stylesheet in the graph would land in every HTML.
|
|
67
|
+
// A raw `.css` in the route graph is Tailwind source; the base artifact's compiled sheet is the only stylesheet.
|
|
68
68
|
plugins: [PagesBundleBuilder.createCssStubPlugin()],
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -80,10 +80,7 @@ export class CsrArtifactBuilder {
|
|
|
80
80
|
|
|
81
81
|
/** The routes one basePath's HTML boots: its own plus every route outside any basePath, matching `bootCsr`. */
|
|
82
82
|
static pageEntriesForBasePath(pageEntries: PageEntry[], basePath: string, basePaths: string[]): PageEntry[] {
|
|
83
|
-
return
|
|
84
|
-
const entryBasePath = getPageKeyBasePath(entry.key, basePaths);
|
|
85
|
-
return entryBasePath === null || entryBasePath === basePath;
|
|
86
|
-
});
|
|
83
|
+
return CsrEntryFiles.pageEntriesForBasePath(pageEntries, basePath, basePaths);
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
static htmlFilename(basePath: string): string {
|
|
@@ -91,7 +88,7 @@ export class CsrArtifactBuilder {
|
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
static entryFilename(basePath: string): string {
|
|
94
|
-
return
|
|
91
|
+
return CsrEntryFiles.entryFilename(basePath);
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
static basePathOfHtml(htmlPath: string): string {
|
|
@@ -118,17 +115,6 @@ export class CsrArtifactBuilder {
|
|
|
118
115
|
return path.join(this.#generatedDir, filename);
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
#define(): Record<string, string> {
|
|
122
|
-
const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
|
|
123
|
-
return {
|
|
124
|
-
"process.env.NODE_ENV": JSON.stringify(nodeEnv),
|
|
125
|
-
"process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
|
|
126
|
-
...Object.fromEntries(
|
|
127
|
-
Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
|
|
128
|
-
),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
118
|
async #createEntryFile(basePath: string, pageEntries: PageEntry[]): Promise<readonly [string, string]> {
|
|
133
119
|
return [
|
|
134
120
|
this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
|
|
@@ -168,7 +154,7 @@ void bootCsr(pages);
|
|
|
168
154
|
|
|
169
155
|
async #inlineCsrArtifacts(cssAssets: Record<string, CssAsset>): Promise<void> {
|
|
170
156
|
const jsFiles = new Set<string>();
|
|
171
|
-
for (const htmlPath of await this.#
|
|
157
|
+
for (const htmlPath of await this.#listOutputFiles((filePath) => filePath.endsWith(".html"))) {
|
|
172
158
|
const htmlFile = Bun.file(htmlPath);
|
|
173
159
|
if (!(await htmlFile.exists())) continue;
|
|
174
160
|
const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
|
|
@@ -210,10 +196,6 @@ void bootCsr(pages);
|
|
|
210
196
|
return { html: next, jsFiles };
|
|
211
197
|
}
|
|
212
198
|
|
|
213
|
-
async #htmlOutputPaths(): Promise<string[]> {
|
|
214
|
-
return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
199
|
async #listOutputFiles(predicate: (filePath: string) => boolean): Promise<string[]> {
|
|
218
200
|
const glob = new Bun.Glob("**/*");
|
|
219
201
|
const files: string[] = [];
|
|
@@ -223,12 +205,8 @@ void bootCsr(pages);
|
|
|
223
205
|
return files.sort();
|
|
224
206
|
}
|
|
225
207
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* of the text being searched — and a React bundle contains `<body` and `</head>` as strings. Positions are
|
|
229
|
-
* taken on a copy with script, style and comment bodies blanked, and the snippet always lands after whatever
|
|
230
|
-
* was injected before it: prepending would reverse the cascade order the caller chose.
|
|
231
|
-
*/
|
|
208
|
+
// Bun hoists the module script into <head> and a React bundle contains "</head>" as text, so positions come from a
|
|
209
|
+
// copy with script/style/comment bodies blanked. Never prepends: that would reverse the caller's cascade order.
|
|
232
210
|
static injectBeforeHeadEnd(html: string, snippet: string): string {
|
|
233
211
|
const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
|
|
234
212
|
const headEnd = scannable.search(/<\/head\s*>/i);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, readFileSync } 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 {
|
|
7
|
+
CSR_DEV_MANIFEST_FILE,
|
|
8
|
+
CSR_DEV_ROUTE_PREFIX,
|
|
9
|
+
type CsrDevManifest,
|
|
10
|
+
csrDevModuleFile,
|
|
11
|
+
} from "akanjs/server/hmr/csrDevManifest";
|
|
12
|
+
import { CsrDevArtifactWriter, type CsrDevArtifactWriterOptions } from "./csrDevArtifactWriter";
|
|
13
|
+
import type { CsrDevCompiledModule, CsrDevGraph } from "./csrDevTypes";
|
|
14
|
+
|
|
15
|
+
const dirs: string[] = [];
|
|
16
|
+
const module: CsrDevCompiledModule = {
|
|
17
|
+
id: "a.ts",
|
|
18
|
+
file: "/a.ts",
|
|
19
|
+
vendor: false,
|
|
20
|
+
factory: "function () {}",
|
|
21
|
+
helpers: null,
|
|
22
|
+
deps: [],
|
|
23
|
+
mtimeMs: 0,
|
|
24
|
+
hash: "",
|
|
25
|
+
};
|
|
26
|
+
const readModule = (dir: string) => readFileSync(path.join(dir, csrDevModuleFile("a.ts", ".js")), "utf8");
|
|
27
|
+
const makeWriter = async (
|
|
28
|
+
options: CsrDevArtifactWriterOptions = { routePrefix: CSR_DEV_ROUTE_PREFIX },
|
|
29
|
+
entries: Record<string, string> = {},
|
|
30
|
+
) => {
|
|
31
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-writer-"));
|
|
32
|
+
dirs.push(dir);
|
|
33
|
+
const writer = new CsrDevArtifactWriter(dir, options);
|
|
34
|
+
const graph: CsrDevGraph = {
|
|
35
|
+
version: 1,
|
|
36
|
+
configKey: "key",
|
|
37
|
+
refresh: "refresh.js",
|
|
38
|
+
entries,
|
|
39
|
+
modules: { "a.ts": { vendor: false, mtimeMs: 0, hash: "", deps: [] } },
|
|
40
|
+
resolution: {},
|
|
41
|
+
pending: [],
|
|
42
|
+
};
|
|
43
|
+
await writer.writeModules([module]);
|
|
44
|
+
const read = () => ({
|
|
45
|
+
manifest: JSON.parse(readFileSync(path.join(dir, CSR_DEV_MANIFEST_FILE), "utf8")) as CsrDevManifest,
|
|
46
|
+
app: readFileSync(path.join(dir, "app.js"), "utf8"),
|
|
47
|
+
});
|
|
48
|
+
return { writer, graph, read, dir };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("CsrDevArtifactWriter", () => {
|
|
56
|
+
test("announces a patch before the module files, graph.json and app.js catch up", async () => {
|
|
57
|
+
const { writer, graph, read, dir } = await makeWriter();
|
|
58
|
+
await writer.writeApp(graph, { generation: 4, vendorFile: "v.js" });
|
|
59
|
+
const edited = { ...module, factory: "function () { edited(); }" };
|
|
60
|
+
let atAnnounce: (ReturnType<typeof read> & { graph: boolean; module: string }) | null = null;
|
|
61
|
+
await writer.commitPatch(
|
|
62
|
+
graph,
|
|
63
|
+
{ version: 1, generation: 5, appGeneration: 4, vendorFile: "v.js", entries: {} },
|
|
64
|
+
() => {
|
|
65
|
+
atAnnounce = { ...read(), graph: existsSync(path.join(dir, "graph.json")), module: readModule(dir) };
|
|
66
|
+
},
|
|
67
|
+
{ modules: [edited] },
|
|
68
|
+
);
|
|
69
|
+
expect(atAnnounce).toMatchObject({ manifest: { generation: 5, appGeneration: 4 }, graph: false });
|
|
70
|
+
expect((atAnnounce as { app: string } | null)?.app).toContain('"generation":4');
|
|
71
|
+
expect((atAnnounce as { module: string } | null)?.module).not.toContain("edited");
|
|
72
|
+
const after = read();
|
|
73
|
+
expect(after.manifest).toMatchObject({ generation: 5, appGeneration: 5 });
|
|
74
|
+
expect(after.app).toContain('"generation":5');
|
|
75
|
+
expect(after.app).toContain("edited()");
|
|
76
|
+
expect(existsSync(path.join(dir, "graph.json"))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a module a package's browser field turns off is written empty, and a Node built-in still throws", async () => {
|
|
80
|
+
const { writer, graph, dir } = await makeWriter();
|
|
81
|
+
graph.modules["a.ts"] = { vendor: false, mtimeMs: 0, hash: "", deps: ["stub:empty:fs", "stub:path"] };
|
|
82
|
+
const vendor = readFileSync(path.join(dir, await writer.writeVendor(graph)), "utf8");
|
|
83
|
+
expect(vendor).toContain('__akan.define("stub:empty:fs", function () {});');
|
|
84
|
+
expect(vendor).toContain("[akan-csr] path is a Node built-in the browser does not have");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("rewrites an app.js a crash left behind the manifest's generation", async () => {
|
|
88
|
+
const { writer, graph, read } = await makeWriter();
|
|
89
|
+
await writer.writeApp(graph, { generation: 5, vendorFile: "v.js" });
|
|
90
|
+
const behind: CsrDevManifest = { version: 1, generation: 6, appGeneration: 5, vendorFile: "v.js", entries: {} };
|
|
91
|
+
await writer.writeJson(CSR_DEV_MANIFEST_FILE, behind);
|
|
92
|
+
const healed = await writer.healApp(graph, behind);
|
|
93
|
+
expect(healed.appGeneration).toBe(6);
|
|
94
|
+
expect(read().manifest.appGeneration).toBe(6);
|
|
95
|
+
expect(read().app).toContain('"generation":6');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("leaves an app.js that already holds the manifest's generation alone", async () => {
|
|
99
|
+
const { writer, graph } = await makeWriter();
|
|
100
|
+
const current: CsrDevManifest = { version: 1, generation: 3, vendorFile: "v.js", entries: {} };
|
|
101
|
+
expect(await writer.healApp(graph, current)).toBe(current);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a library app.js starts its bootstrap module, and a patch is announced under the writer's route", async () => {
|
|
105
|
+
const { writer, graph, dir } = await makeWriter(
|
|
106
|
+
{ routePrefix: "/_akan/ssr-dev/", library: true },
|
|
107
|
+
{ "": "boot.ts" },
|
|
108
|
+
);
|
|
109
|
+
await writer.writeApp(graph, { generation: 2, vendorFile: "v.js", epoch: 7 });
|
|
110
|
+
expect(readFileSync(path.join(dir, "app.js"), "utf8")).toContain(
|
|
111
|
+
'__akan.startLibrary({"generation":2,"refresh":"refresh.js","bootstrap":"boot.ts","vendorFile":"v.js","epoch":7});',
|
|
112
|
+
);
|
|
113
|
+
expect(await writer.writePatch(3, [module])).toBe("/_akan/ssr-dev/patch-3.js");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("after a whole build, clears what the registry before it left and keeps what the build wrote", async () => {
|
|
117
|
+
const { writer, graph, dir } = await makeWriter();
|
|
118
|
+
const gone = { ...module, id: "gone.ts" };
|
|
119
|
+
await writer.writeModules([gone]);
|
|
120
|
+
await writer.writePatch(7, [module]);
|
|
121
|
+
await Bun.write(path.join(dir, "assets/old-abc.png"), "old");
|
|
122
|
+
await Bun.write(path.join(dir, ".patching"), "123");
|
|
123
|
+
await Bun.write(path.join(dir, "manifest.json.4242.tmp"), "{");
|
|
124
|
+
await Bun.write(path.join(dir, "assets/new-def.png"), "new");
|
|
125
|
+
await writer.pruneAfterFullBuild(graph, "vendor-a.js", {
|
|
126
|
+
generation: 8,
|
|
127
|
+
assets: [path.join(dir, "assets/new-def.png")],
|
|
128
|
+
});
|
|
129
|
+
expect(existsSync(path.join(dir, csrDevModuleFile("a.ts", ".js")))).toBe(true);
|
|
130
|
+
expect(existsSync(path.join(dir, csrDevModuleFile("gone.ts", ".js")))).toBe(false);
|
|
131
|
+
expect(existsSync(path.join(dir, "patch-7.js"))).toBe(false);
|
|
132
|
+
expect(existsSync(path.join(dir, "patch-7.js.layout.json"))).toBe(false);
|
|
133
|
+
expect(existsSync(path.join(dir, "assets/old-abc.png"))).toBe(false);
|
|
134
|
+
expect(existsSync(path.join(dir, "assets/new-def.png"))).toBe(true);
|
|
135
|
+
expect(existsSync(path.join(dir, ".patching"))).toBe(false);
|
|
136
|
+
expect(existsSync(path.join(dir, "manifest.json.4242.tmp"))).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a reload drops a patch file left under its generation, and a whole build is marked while it writes", async () => {
|
|
140
|
+
const { writer, dir } = await makeWriter();
|
|
141
|
+
await writer.writePatch(6, [module]);
|
|
142
|
+
await writer.forgetPatch(6);
|
|
143
|
+
expect(existsSync(path.join(dir, "patch-6.js"))).toBe(false);
|
|
144
|
+
expect(existsSync(path.join(dir, "patch-6.js.layout.json"))).toBe(false);
|
|
145
|
+
expect(await writer.isBuilding()).toBe(false);
|
|
146
|
+
await writer.markBuilding();
|
|
147
|
+
expect(await writer.isBuilding()).toBe(true);
|
|
148
|
+
await writer.clearBuilding();
|
|
149
|
+
expect(await writer.isBuilding()).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
});
|