@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,17 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { DevResourceProbe, type DevResourceProc } from "./devResourceProbe";
|
|
4
5
|
|
|
5
6
|
export type SsrProcRole = "gateway" | "replica" | "rsc" | "other";
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
-
pid: number;
|
|
9
|
-
ppid: number;
|
|
10
|
-
rssMb: number;
|
|
11
|
-
cpuSec: number;
|
|
12
|
-
role: SsrProcRole;
|
|
13
|
-
command: string;
|
|
14
|
-
}
|
|
8
|
+
export type SsrProc = DevResourceProc<SsrProcRole>;
|
|
15
9
|
|
|
16
10
|
export interface SsrCgroupSample {
|
|
17
11
|
currentMb: number;
|
|
@@ -35,12 +29,7 @@ export interface SsrCacheSample {
|
|
|
35
29
|
heap: SsrHeapSample;
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
* RSS minus JS heap is what a module graph costs beyond its objects — compiled code, module
|
|
40
|
-
* records, native allocator retention — so the split is what separates "the app retains objects"
|
|
41
|
-
* from "the runtime retains code". `jscExtra` is JSC's off-heap attribution, which is where
|
|
42
|
-
* typed-array backing stores (cached Flight chunks) land.
|
|
43
|
-
*/
|
|
32
|
+
// `jscExtra` is JSC's off-heap attribution, where typed-array backing stores (cached Flight chunks) land.
|
|
44
33
|
export interface SsrHeapSample {
|
|
45
34
|
replicaHeapUsedMb: number;
|
|
46
35
|
replicaJscHeapMb: number;
|
|
@@ -52,63 +41,30 @@ export interface SsrHeapSample {
|
|
|
52
41
|
|
|
53
42
|
export interface SsrMemoryProbeOptions {
|
|
54
43
|
appName: string;
|
|
55
|
-
/** Built app directory — `dist/apps/<app>` unless the build was relocated. */
|
|
56
44
|
distDir: string;
|
|
57
45
|
port: number;
|
|
58
46
|
basePaths: string;
|
|
59
47
|
repoName: string;
|
|
60
48
|
serveDomain: string;
|
|
61
49
|
scenarios: number[];
|
|
62
|
-
/** Repeat count for scenario 1 (same route) and per-route passes in scenario 6. */
|
|
63
50
|
repeats: number;
|
|
64
51
|
concurrencies: number[];
|
|
65
52
|
idleSeconds: number;
|
|
66
|
-
/** Report cadence forced on the children; also bounds how long a sample waits for a fresh one. */
|
|
67
53
|
metricsIntervalMs: number;
|
|
68
|
-
/** Disables both result caches at boot — the second half of scenario 3. */
|
|
69
54
|
cacheOff: boolean;
|
|
70
|
-
/**
|
|
71
|
-
* Forces `Bun.gc(true)` before every metrics sample. Required for the s7 ratchet test: without
|
|
72
|
-
* it, growth across passes cannot be told apart from garbage that has not been collected yet.
|
|
73
|
-
*/
|
|
74
55
|
gcOnReport: boolean;
|
|
75
|
-
/** s7: how many times to walk the whole route list. */
|
|
76
56
|
passes: number;
|
|
77
|
-
/** s8: cumulative distinct-route counts to sample the per-route slope at. */
|
|
78
57
|
sweep: number[];
|
|
79
58
|
logPath: string;
|
|
80
59
|
}
|
|
81
60
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* `local/reduce-ssr-ram/01-measurement-harness.md`, and the source of the phase 0 results table.
|
|
85
|
-
*
|
|
86
|
-
* bun pkgs/@akanjs/devkit/integration/ssrMemoryProbe.ts <app> [--scenarios=1,2,5] [--cache=off]
|
|
87
|
-
*
|
|
88
|
-
* The production tree is gateway → replica × N → rsc worker × N, with no builder; `DevResourceProbe`
|
|
89
|
-
* measures the dev tree instead and the two are not interchangeable.
|
|
90
|
-
*
|
|
91
|
-
* Two things this exists to get right:
|
|
92
|
-
*
|
|
93
|
-
* - **Replica and worker RSS are reported separately.** They are separate processes and the worker
|
|
94
|
-
* holds a private copy of the pages bundle, so one summed number hides which of the two grows.
|
|
95
|
-
* - **On Linux it samples the cgroup's `anon` / `file` split, not just RSS.** Whether a cache is
|
|
96
|
-
* anonymous heap (an OOM contributor) or page cache (evictable) is the entire question phase 2
|
|
97
|
-
* asks; RSS cannot answer it, and neither can macOS, where the file half does not exist in the
|
|
98
|
-
* same form. Read a macOS run as directional only.
|
|
99
|
-
*/
|
|
61
|
+
// Usage: bun pkgs/@akanjs/devkit/integration/ssrMemoryProbe.ts <app> [--scenarios=1,2,5] [--cache=off]
|
|
62
|
+
// The cgroup `anon`/`file` split exists only on Linux; read a macOS run as directional.
|
|
100
63
|
export class SsrMemoryProbe {
|
|
101
64
|
static readonly #columns: SsrProcRole[] = ["gateway", "replica", "rsc"];
|
|
102
65
|
|
|
103
66
|
static parseArgs(argv: string[]): SsrMemoryProbeOptions {
|
|
104
|
-
const args =
|
|
105
|
-
argv.slice(1).map((arg) => {
|
|
106
|
-
const [key, value] = arg.replace(/^--/, "").split("=");
|
|
107
|
-
return [key ?? "", value ?? "1"];
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
110
|
-
const appName = argv[0] ?? "akan";
|
|
111
|
-
const num = (key: string, fallback: number) => Number(args.get(key) ?? fallback);
|
|
67
|
+
const { appName, args, num } = DevResourceProbe.parseArgMap(argv);
|
|
112
68
|
const list = (key: string, fallback: number[]) =>
|
|
113
69
|
args.has(key)
|
|
114
70
|
? (args.get(key) ?? "")
|
|
@@ -155,10 +111,7 @@ export class SsrMemoryProbe {
|
|
|
155
111
|
NODE_ENV: "production",
|
|
156
112
|
USE_AKANJS_PKGS: "true",
|
|
157
113
|
AKAN_PUBLIC_APP_NAME: appName,
|
|
158
|
-
//
|
|
159
|
-
// gateway stays up: the process tree still looks like a healthy three, and only the child's
|
|
160
|
-
// `ready` flag and the 503s give it away. Both are why `#waitForReady` checks child
|
|
161
|
-
// readiness rather than the gateway's own 200.
|
|
114
|
+
// A missing one fails the replica while the gateway stays up and keeps answering.
|
|
162
115
|
AKAN_PUBLIC_REPO_NAME: process.env.AKAN_PUBLIC_REPO_NAME ?? repoName,
|
|
163
116
|
AKAN_PUBLIC_SERVE_DOMAIN: process.env.AKAN_PUBLIC_SERVE_DOMAIN ?? serveDomain,
|
|
164
117
|
AKAN_PUBLIC_ENV: "local",
|
|
@@ -181,7 +134,7 @@ export class SsrMemoryProbe {
|
|
|
181
134
|
try {
|
|
182
135
|
await this.#measure();
|
|
183
136
|
} finally {
|
|
184
|
-
await this.#
|
|
137
|
+
await DevResourceProbe.killTree(gateway, () => this.#sampleTree());
|
|
185
138
|
}
|
|
186
139
|
}
|
|
187
140
|
|
|
@@ -221,11 +174,7 @@ export class SsrMemoryProbe {
|
|
|
221
174
|
for (const concurrency of concurrencies) await this.#run(`s6 concurrency=${concurrency}`, routes, concurrency);
|
|
222
175
|
}
|
|
223
176
|
|
|
224
|
-
// s7
|
|
225
|
-
// evaluation and later passes pay for nothing new. Flat later passes mean the memory is a
|
|
226
|
-
// working set and only a smaller bundle or fewer replicas reduces it; continued growth means
|
|
227
|
-
// per-render retention, which is a bug worth more than any ceiling. Run with `--cache=off --gc`
|
|
228
|
-
// or it measures cache hits and uncollected garbage instead.
|
|
177
|
+
// s7: flat passes after the first mean a working set; continued growth means per-render retention.
|
|
229
178
|
if (scenarios.includes(7)) {
|
|
230
179
|
if (!this.#options.cacheOff)
|
|
231
180
|
console.info("[probe] WARNING s7 without --cache=off measures cache hits from pass 2 on");
|
|
@@ -235,8 +184,7 @@ export class SsrMemoryProbe {
|
|
|
235
184
|
await this.#run(`s7 pass ${pass}/${this.#options.passes}`, routes, 1);
|
|
236
185
|
}
|
|
237
186
|
|
|
238
|
-
// s8
|
|
239
|
-
// not seen, so the RSS series against distinct-route count separates the two.
|
|
187
|
+
// s8: cumulative, so RSS against distinct-route count separates the per-route slope from the intercept.
|
|
240
188
|
if (scenarios.includes(8)) {
|
|
241
189
|
for (const count of this.#options.sweep) {
|
|
242
190
|
const subset = routes.slice(0, Math.min(count, routes.length));
|
|
@@ -255,11 +203,7 @@ export class SsrMemoryProbe {
|
|
|
255
203
|
}
|
|
256
204
|
}
|
|
257
205
|
|
|
258
|
-
|
|
259
|
-
* A scenario is only a measurement if its requests succeeded. A gateway whose replica died still
|
|
260
|
-
* answers — with 503s, instantly — and the resulting RSS row looks like a legitimately cheap one.
|
|
261
|
-
* Fail loudly rather than publish that number.
|
|
262
|
-
*/
|
|
206
|
+
// A dead replica still answers instant 503s, which would read as a legitimately cheap row.
|
|
263
207
|
async #run(label: string, urls: string[], concurrency: number): Promise<void> {
|
|
264
208
|
const started = Date.now();
|
|
265
209
|
const { ok, failed, statuses } = await this.#browse(urls, concurrency);
|
|
@@ -307,11 +251,7 @@ export class SsrMemoryProbe {
|
|
|
307
251
|
return `${(bytes / 1024 / 1024).toFixed(1)}MiB`;
|
|
308
252
|
}
|
|
309
253
|
|
|
310
|
-
|
|
311
|
-
* Reads the container's own accounting. `memory.current` counts page cache, so a disk-backed
|
|
312
|
-
* cache still shows up here — but under `file`, which the kernel reclaims under pressure instead
|
|
313
|
-
* of OOM-killing. That split is what phase 2 has to move. Returns null off cgroup v2.
|
|
314
|
-
*/
|
|
254
|
+
// cgroup v2 only. `file` is page cache the kernel reclaims under pressure; `anon` is what gets OOM-killed.
|
|
315
255
|
static readCgroupSample(): SsrCgroupSample | null {
|
|
316
256
|
try {
|
|
317
257
|
const current = Number.parseInt(fs.readFileSync("/sys/fs/cgroup/memory.current", "utf8").trim(), 10);
|
|
@@ -328,22 +268,14 @@ export class SsrMemoryProbe {
|
|
|
328
268
|
}
|
|
329
269
|
}
|
|
330
270
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
*
|
|
334
|
-
* The counters ride a periodic IPC report, so reading immediately after a browse returns the
|
|
335
|
-
* state from before it — and the two-hop path (worker → replica → gateway) means the `rsc*` and
|
|
336
|
-
* `rscWorker*` fields can be a further interval behind. Without this wait every row silently
|
|
337
|
-
* attributes one scenario's cache growth to the previous scenario.
|
|
338
|
-
*/
|
|
271
|
+
// Counters ride a periodic two-hop IPC report (worker → replica → gateway), so wait until both hops have
|
|
272
|
+
// re-reported after `since`, or one scenario's growth lands in the next row.
|
|
339
273
|
async #sampleCaches(since: number): Promise<SsrCacheSample | null> {
|
|
340
274
|
const deadline = Date.now() + this.#options.metricsIntervalMs * 3 + 5_000;
|
|
341
275
|
let children: Array<Record<string, number>> = [];
|
|
342
276
|
let fresh = false;
|
|
343
277
|
while (Date.now() < deadline) {
|
|
344
278
|
children = await this.#fetchChildMetrics();
|
|
345
|
-
// Both hops must have re-reported: `reportedAt` is the replica's own sample time, and
|
|
346
|
-
// `rscWorkerReportedAt` the worker's as of the replica's last read of it.
|
|
347
279
|
fresh =
|
|
348
280
|
children.length > 0 &&
|
|
349
281
|
children.every(
|
|
@@ -354,32 +286,30 @@ export class SsrMemoryProbe {
|
|
|
354
286
|
}
|
|
355
287
|
if (children.length === 0) return null;
|
|
356
288
|
if (!fresh) console.info(`${"".padEnd(24)} WARNING cache columns are stale — no fresh report within the wait`);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
};
|
|
382
|
-
}
|
|
289
|
+
const sum = (key: string) => children.reduce((total, metrics) => total + Number(metrics[key] ?? 0), 0);
|
|
290
|
+
const mb = (key: string) => sum(key) / 1024 / 1024;
|
|
291
|
+
return {
|
|
292
|
+
replicaRssMb: mb("rssBytes"),
|
|
293
|
+
rscWorkerRssMb: mb("rscWorkerRssBytes"),
|
|
294
|
+
htmlEntries: sum("httpHtmlCacheEntries"),
|
|
295
|
+
htmlBytes: sum("httpHtmlCacheBytes"),
|
|
296
|
+
rscEntries: sum("rscResultCacheEntries"),
|
|
297
|
+
rscBytes: sum("rscResultCacheBytes"),
|
|
298
|
+
patchEntries: sum("rscPatchResultCacheEntries"),
|
|
299
|
+
patchBytes: sum("rscPatchResultCacheBytes"),
|
|
300
|
+
ssrChunkKeys: sum("ssrChunkRegistrySize"),
|
|
301
|
+
loadedRouteModules: sum("rscLoadedRouteModuleCount"),
|
|
302
|
+
fullSsr: sum("httpFullSsrCount"),
|
|
303
|
+
rscNavigation: sum("httpRscNavigationCount"),
|
|
304
|
+
heap: {
|
|
305
|
+
replicaHeapUsedMb: mb("heapUsedBytes"),
|
|
306
|
+
replicaJscHeapMb: mb("jscHeapSizeBytes"),
|
|
307
|
+
replicaJscExtraMb: mb("jscExtraMemorySizeBytes"),
|
|
308
|
+
workerHeapUsedMb: mb("rscWorkerHeapUsedBytes"),
|
|
309
|
+
workerJscHeapMb: mb("rscWorkerJscHeapSizeBytes"),
|
|
310
|
+
workerJscExtraMb: mb("rscWorkerJscExtraMemorySizeBytes"),
|
|
311
|
+
},
|
|
312
|
+
};
|
|
383
313
|
}
|
|
384
314
|
|
|
385
315
|
async #fetchChildMetrics(): Promise<Array<Record<string, number>>> {
|
|
@@ -402,49 +332,10 @@ export class SsrMemoryProbe {
|
|
|
402
332
|
return "other";
|
|
403
333
|
}
|
|
404
334
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const proc = Bun.spawn(["ps", "-eo", "pid=,ppid=,rss=,time=,command="], { stdout: "pipe" });
|
|
408
|
-
const text = await new Response(proc.stdout).text();
|
|
409
|
-
await proc.exited;
|
|
410
|
-
const all = text
|
|
411
|
-
.split("\n")
|
|
412
|
-
.map((line) => line.trim())
|
|
413
|
-
.filter(Boolean)
|
|
414
|
-
.map((line) => {
|
|
415
|
-
const match = /^(\d+)\s+(\d+)\s+(\d+)\s+([\d:.]+)\s+(.*)$/.exec(line);
|
|
416
|
-
if (!match) return null;
|
|
417
|
-
const [, pid, ppid, rss, time, command] = match;
|
|
418
|
-
const parts = (time ?? "0:0").split(":");
|
|
419
|
-
const cpuSec =
|
|
420
|
-
parts.length === 3
|
|
421
|
-
? Number(parts[0]) * 3600 + Number(parts[1]) * 60 + Number(parts[2])
|
|
422
|
-
: Number(parts[0] ?? 0) * 60 + Number(parts[1] ?? 0);
|
|
423
|
-
return {
|
|
424
|
-
pid: Number(pid),
|
|
425
|
-
ppid: Number(ppid),
|
|
426
|
-
rssMb: Number(rss) / 1024,
|
|
427
|
-
cpuSec,
|
|
428
|
-
role: this.#roleOf(command ?? ""),
|
|
429
|
-
command: command ?? "",
|
|
430
|
-
} satisfies SsrProc;
|
|
431
|
-
})
|
|
432
|
-
.filter((proc): proc is SsrProc => proc !== null);
|
|
433
|
-
const kept = new Map<number, SsrProc>();
|
|
434
|
-
const root = all.find((proc) => proc.pid === this.#gatewayPid);
|
|
435
|
-
if (root) kept.set(this.#gatewayPid, root);
|
|
436
|
-
for (let pass = 0; pass < 12; pass++) {
|
|
437
|
-
const before = kept.size;
|
|
438
|
-
for (const proc of all) if (kept.has(proc.ppid) && !kept.has(proc.pid)) kept.set(proc.pid, proc);
|
|
439
|
-
if (kept.size === before) break;
|
|
440
|
-
}
|
|
441
|
-
return [...kept.values()].filter((proc) => proc.role !== "other" || proc.pid === this.#gatewayPid);
|
|
335
|
+
#sampleTree(): Promise<SsrProc[]> {
|
|
336
|
+
return DevResourceProbe.sampleTree(this.#gatewayPid, (command) => this.#roleOf(command));
|
|
442
337
|
}
|
|
443
338
|
|
|
444
|
-
/**
|
|
445
|
-
* Concrete urls from the built route seed index. `:lang` takes the default locale; any route with
|
|
446
|
-
* another `:param` is skipped because it needs a real id to render.
|
|
447
|
-
*/
|
|
448
339
|
async #staticRoutes(): Promise<string[]> {
|
|
449
340
|
const artifactDir = path.join(this.#options.distDir, ".akan", "artifact");
|
|
450
341
|
const seed = (await Bun.file(path.join(artifactDir, "route-seed-index.json")).json()) as {
|
|
@@ -495,11 +386,7 @@ export class SsrMemoryProbe {
|
|
|
495
386
|
return { ok, failed, statuses };
|
|
496
387
|
}
|
|
497
388
|
|
|
498
|
-
|
|
499
|
-
* Waits for a **child** to report ready, not for the gateway to answer. The gateway binds and
|
|
500
|
-
* serves `/health` happily while every replica is in a crash-restart loop, so a 200 here proves
|
|
501
|
-
* nothing about whether anything can render.
|
|
502
|
-
*/
|
|
389
|
+
// The gateway answers `/health` while every replica crash-loops, so wait for a ready child instead.
|
|
503
390
|
async #waitForReady(timeoutMs: number): Promise<boolean> {
|
|
504
391
|
const deadline = Date.now() + timeoutMs;
|
|
505
392
|
let lastError = "";
|
|
@@ -524,19 +411,6 @@ export class SsrMemoryProbe {
|
|
|
524
411
|
if (lastError) console.info(`[probe] no child became ready; last child error: ${lastError}`);
|
|
525
412
|
return false;
|
|
526
413
|
}
|
|
527
|
-
|
|
528
|
-
async #cleanup(gateway: { kill: (signal: NodeJS.Signals) => void }): Promise<void> {
|
|
529
|
-
for (const proc of (await this.#sampleTree()).reverse()) {
|
|
530
|
-
try {
|
|
531
|
-
process.kill(proc.pid, "SIGKILL");
|
|
532
|
-
} catch {}
|
|
533
|
-
}
|
|
534
|
-
try {
|
|
535
|
-
gateway.kill("SIGKILL");
|
|
536
|
-
} catch {}
|
|
537
|
-
await Bun.sleep(1_000);
|
|
538
|
-
console.info(`[probe] survivors after kill: ${(await this.#sampleTree()).length}`);
|
|
539
|
-
}
|
|
540
414
|
}
|
|
541
415
|
|
|
542
416
|
if (import.meta.main) await new SsrMemoryProbe(SsrMemoryProbe.parseArgs(process.argv.slice(2))).run();
|
package/libSource.test.ts
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import { Executor, LibExecutor, WorkspaceExecutor } from "./executors";
|
|
6
4
|
import { formatLibStatuses, LibSource } from "./libSource";
|
|
5
|
+
import { tempDirs, writeText as write } from "./testHelpers";
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
afterEach(async () => {
|
|
11
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const write = async (filePath: string, content: string) => {
|
|
15
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
-
await writeFile(filePath, content);
|
|
17
|
-
};
|
|
7
|
+
const makeTempRoot = tempDirs("akan-libsource-");
|
|
18
8
|
|
|
19
9
|
// `LibExecutor.from` memoises by name, so each fixture needs a name no other test has used.
|
|
20
10
|
const makeLib = async (libName: string) => {
|
|
21
|
-
const root = await
|
|
22
|
-
tempRoots.push(root);
|
|
11
|
+
const root = await makeTempRoot();
|
|
23
12
|
await write(
|
|
24
13
|
path.join(root, "package.json"),
|
|
25
14
|
'{ "name": "workspace", "version": "0.0.1", "description": "workspace" }\n',
|
package/libSource.ts
CHANGED
|
@@ -20,20 +20,10 @@ export interface LibStatus {
|
|
|
20
20
|
hash: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
* A library's origin, recorded in its own `package.json` under an `akan.source` key.
|
|
25
|
-
*
|
|
26
|
-
* The key rides `package.json` rather than a file of its own because every akan write to a library's
|
|
27
|
-
* manifest is a spread of the existing object (`LibExecutor.syncPackageJson`), so an unknown top-level
|
|
28
|
-
* key survives — while a new root file would have to be added to `libRootAllowedFiles` before
|
|
29
|
-
* `akan sync` stopped rejecting it.
|
|
30
|
-
*/
|
|
23
|
+
// In package.json, not a file of its own: every akan manifest write spreads the existing object, so the key survives.
|
|
31
24
|
export class LibSource {
|
|
32
25
|
static readonly manifestKey = "akan";
|
|
33
|
-
|
|
34
|
-
* Left out of the hash. `env/` holds per-deployment values that belong to the workspace the library
|
|
35
|
-
* was installed into, not to the origin, so an env edit is not drift.
|
|
36
|
-
*/
|
|
26
|
+
// `env/` holds the installing workspace's per-deployment values, so an env edit is not drift.
|
|
37
27
|
static readonly unhashedDirs = ["env"];
|
|
38
28
|
|
|
39
29
|
#lib: LibExecutor;
|
|
@@ -50,33 +40,35 @@ export class LibSource {
|
|
|
50
40
|
return !LibSource.unhashedDirs.includes(relative.split("/")[0] ?? "");
|
|
51
41
|
}
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const content = await this.#lib.workspace.readFile(file);
|
|
56
|
-
if (file !== `${this.#prefix}package.json`) return content;
|
|
57
|
-
const manifest = JSON.parse(content) as PackageJson;
|
|
43
|
+
static unstamped(manifestContent: string) {
|
|
44
|
+
const manifest = JSON.parse(manifestContent) as PackageJson;
|
|
58
45
|
delete manifest[LibSource.manifestKey];
|
|
59
46
|
return JSON.stringify(manifest);
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* yet, and its hash has to be the same one a later `status` recomputes.
|
|
65
|
-
*/
|
|
66
|
-
async computeHash() {
|
|
67
|
-
const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
|
|
68
|
-
(file) => this.#isHashed(file),
|
|
69
|
-
);
|
|
49
|
+
//* `package.json` cannot hash the stamp it carries, so the key comes off before hashing.
|
|
50
|
+
static async hashFiles(files: string[], manifestFile: string, read: (file: string) => Promise<string>) {
|
|
70
51
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
71
52
|
for (const file of files) {
|
|
53
|
+
const content = await read(file);
|
|
72
54
|
hasher.update(file);
|
|
73
55
|
hasher.update("\0");
|
|
74
|
-
hasher.update(
|
|
56
|
+
hasher.update(file === manifestFile ? LibSource.unstamped(content) : content);
|
|
75
57
|
hasher.update("\0");
|
|
76
58
|
}
|
|
77
59
|
return hasher.digest("hex").slice(0, 32);
|
|
78
60
|
}
|
|
79
61
|
|
|
62
|
+
// Untracked files count: a freshly copied library is not committed yet.
|
|
63
|
+
async computeHash() {
|
|
64
|
+
const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
|
|
65
|
+
(file) => this.#isHashed(file),
|
|
66
|
+
);
|
|
67
|
+
return await LibSource.hashFiles(files, `${this.#prefix}package.json`, (file) =>
|
|
68
|
+
this.#lib.workspace.readFile(file),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
80
72
|
async read(): Promise<LibSourceStamp | null> {
|
|
81
73
|
const manifest = await this.#lib.getPackageJson();
|
|
82
74
|
const akan = manifest[LibSource.manifestKey] as { source?: LibSourceStamp } | undefined;
|
|
@@ -91,11 +83,7 @@ export class LibSource {
|
|
|
91
83
|
return stamp;
|
|
92
84
|
}
|
|
93
85
|
|
|
94
|
-
|
|
95
|
-
* Writes the stamp only when it would change. `syncedAt` moves on every write, so an unconditional
|
|
96
|
-
* write leaves the manifest dirty and defeats an idempotent caller — the hash is computed with the
|
|
97
|
-
* stamp removed, so comparing it first is sound.
|
|
98
|
-
*/
|
|
86
|
+
// `syncedAt` moves on every write, so an unchanged stamp is left alone (the hash excludes the stamp itself).
|
|
99
87
|
async syncStamp({ origin, sha }: Pick<LibSourceStamp, "origin" | "sha">) {
|
|
100
88
|
const [current, hash] = await Promise.all([this.read(), this.computeHash()]);
|
|
101
89
|
if (current?.origin === origin && current.sha === sha && current.hash === hash)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const readTheme = () => document.cookie; // @flag
|
|
2
|
+
export const writeTheme = (value: string) => { document.cookie = `theme=${value}`; }; // @flag
|
|
3
|
+
export const throughWindow = () => window.document.cookie; // @flag
|
|
4
|
+
export const throughGlobalThis = () => globalThis.document.cookie; // @flag
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getCookie, setCookie } from "akanjs/client";
|
|
2
|
+
export const readTheme = () => getCookie("theme"); // @ok
|
|
3
|
+
export const writeTheme = (value: string) => setCookie("theme", value); // @ok
|
|
4
|
+
export const otherDocumentFields = () => document.title; // @ok
|
|
5
|
+
export const aFieldNamedCookie = (req: { cookie: string }) => req.cookie; // @ok
|
|
6
|
+
export const aLiteral = "document.cookie is empty on app://"; // @ok
|
|
7
|
+
export const Docs = () => <p>getCookie reads the request, document.cookie the browser.</p>; // @ok
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const share = (url: string) => navigator.share({ url }); // @flag
|
|
2
|
+
export const canShare = () => typeof navigator.canShare === "function"; // @flag
|
|
3
|
+
export const worker = () => navigator.serviceWorker.register("/sw.js"); // @flag
|
|
4
|
+
export const ask = () => Notification.requestPermission(); // @flag
|
|
5
|
+
export const permission = () => Notification.permission; // @flag
|
|
6
|
+
export const locate = () => navigator.geolocation.getCurrentPosition(() => undefined); // @flag
|
|
7
|
+
export const buzz = () => navigator.vibrate(50); // @flag
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { haptics } from "akanjs/client/native";
|
|
2
|
+
import { useGeoLocation } from "akanjs/webkit";
|
|
3
|
+
export const buzz = () => haptics.impact(); // @ok
|
|
4
|
+
export const locate = () => useGeoLocation(); // @ok
|
|
5
|
+
export const languages = () => navigator.languages; // @ok
|
|
6
|
+
export const clipboard = (text: string) => navigator.clipboard.writeText(text); // @ok
|
|
7
|
+
export const aField = (payload: { share: boolean }) => payload.share; // @ok
|
|
8
|
+
export const aLiteral = "navigator.share is missing on Android"; // @ok
|
|
9
|
+
export const Docs = () => <p>navigator.share is missing on Android.</p>; // @ok
|
|
10
|
+
export const ModelZone = () => <Notification.Zone.Initialize />; // @ok
|
|
11
|
+
export const model = (cnst: { Notification: unknown }) => cnst.Notification; // @ok
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const readDraft = () => localStorage.getItem("draft"); // @flag
|
|
2
|
+
export const writeDraft = (value: string) => localStorage.setItem("draft", value); // @flag
|
|
3
|
+
export const readTab = () => sessionStorage.getItem("tab"); // @flag
|
|
4
|
+
export const throughWindow = () => window.localStorage.getItem("draft"); // @flag
|
|
5
|
+
export const throughGlobalThis = () => globalThis.sessionStorage.clear(); // @flag
|
|
6
|
+
export const guarded = () => (typeof localStorage === "undefined" ? null : 1); // @flag
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { secretStorage, storage } from "akanjs/client";
|
|
2
|
+
export const readDraft = () => storage.getItem("draft"); // @ok
|
|
3
|
+
export const keepToken = (jwt: string) => secretStorage.setItem("jwt", jwt); // @ok
|
|
4
|
+
export const aFieldWithTheName = (cache: { localStorage: string }) => cache.localStorage; // @ok
|
|
5
|
+
export const anObjectKey = { sessionStorage: false }; // @ok
|
|
6
|
+
export const aLiteral = "localStorage works in the shells"; // @ok
|
|
7
|
+
export const Docs = () => <p>window.localStorage and sessionStorage work in the shells.</p>; // @ok
|
package/lint/gritRules.test.ts
CHANGED
|
@@ -4,24 +4,14 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { Linter } from "../linter";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* These rules break the apps/libs build, and a rule that stops matching reports **zero diagnostics** rather
|
|
12
|
-
* than an error — so a biome upgrade can retire one silently. GritQL has known sharp edges here (escaped
|
|
13
|
-
* quotes, argument-position matching), which is what makes the quiet failure plausible rather than
|
|
14
|
-
* theoretical.
|
|
15
|
-
*
|
|
16
|
-
* Each rule runs alone, against a config this test writes, so a diagnostic can only have come from that
|
|
17
|
-
* plugin. The path scoping in `biome.base.json` is deliberately not exercised — what is under test is
|
|
18
|
-
* whether the pattern still matches. A rule that reads `$filename` needs a real path anyway, which is what
|
|
19
|
-
* a case's `fixture.json` supplies.
|
|
20
|
-
*/
|
|
9
|
+
//? A rule that stops matching reports zero diagnostics, not an error, so a biome upgrade can retire one silently.
|
|
10
|
+
//? Each rule runs alone against a config written here; the path scoping in biome.base.json is deliberately not tested.
|
|
21
11
|
const lintDir = import.meta.dir;
|
|
22
12
|
const fixturesDir = path.join(lintDir, "__fixtures__");
|
|
23
13
|
const workspaceRoot = path.resolve(lintDir, "../../../..");
|
|
24
|
-
const biomeBin = path.join(workspaceRoot, "node_modules
|
|
14
|
+
const biomeBin = path.join(workspaceRoot, "node_modules", ".bin", Linter.biomeBinName);
|
|
25
15
|
|
|
26
16
|
interface BiomeDiagnostic {
|
|
27
17
|
severity: string;
|
|
@@ -30,13 +20,10 @@ interface BiomeDiagnostic {
|
|
|
30
20
|
location?: { start?: { line?: number } };
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
/**
|
|
34
|
-
* `path` is where the source is written inside the temp workspace, for a rule that gates on `$filename`;
|
|
35
|
-
* its extension also decides whether the file may hold JSX. `expect: "file"` is for a rule whose span is
|
|
36
|
-
* the whole module (`JsModule()`), where there is no per-line diagnostic to match.
|
|
37
|
-
*/
|
|
38
23
|
interface FixtureMeta {
|
|
24
|
+
/** Where the source is written, for a rule that gates on `$filename`; its extension decides whether JSX parses. */
|
|
39
25
|
path?: string;
|
|
26
|
+
/** `"file"`: the rule's span is the whole module (`JsModule()`), so there is no per-line diagnostic to match. */
|
|
40
27
|
expect?: "lines" | "file";
|
|
41
28
|
}
|
|
42
29
|
|
|
@@ -64,7 +51,6 @@ const runBiome = (args: string[], cwd: string) =>
|
|
|
64
51
|
proc.on("close", () => resolve(stdout || stderr));
|
|
65
52
|
});
|
|
66
53
|
|
|
67
|
-
/** Every directory holding a `bad.*`, named `<rule>` or `<rule>/<case>`. */
|
|
68
54
|
const collectCases = async (): Promise<FixtureCase[]> => {
|
|
69
55
|
const cases: FixtureCase[] = [];
|
|
70
56
|
const walk = async (dir: string, rule: string, name: string) => {
|
|
@@ -92,7 +78,6 @@ const sourceName = (fixture: FixtureCase, kind: "bad" | "good") => {
|
|
|
92
78
|
const readFixture = async (fixture: FixtureCase, kind: "bad" | "good") =>
|
|
93
79
|
await readFile(path.join(fixture.dir, sourceName(fixture, kind).read), "utf-8");
|
|
94
80
|
|
|
95
|
-
/** Diagnostics from one plugin against one fixture, keyed by the 1-based line each was reported on. */
|
|
96
81
|
const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
|
|
97
82
|
const dir = await mkdtemp(path.join(os.tmpdir(), `grit-${fixture.rule}-`));
|
|
98
83
|
tempDirs.push(dir);
|
|
@@ -127,7 +112,7 @@ const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
|
|
|
127
112
|
};
|
|
128
113
|
};
|
|
129
114
|
|
|
130
|
-
|
|
115
|
+
//? One case per line is the fixture contract.
|
|
131
116
|
const markedLines = async (fixture: FixtureCase, kind: "bad" | "good", marker: string) =>
|
|
132
117
|
(await readFixture(fixture, kind))
|
|
133
118
|
.split("\n")
|
|
@@ -164,8 +149,7 @@ describe("grit lint rules", () => {
|
|
|
164
149
|
}
|
|
165
150
|
const expected = await markedLines(fixture, "bad", "// @flag");
|
|
166
151
|
expect(expected.length).toBeGreaterThan(0);
|
|
167
|
-
// Set equality
|
|
168
|
-
// and a reported line nobody marked is the rule reaching further than the fixture claims.
|
|
152
|
+
// Set equality: an unreported mark is a pattern that stopped matching, an unmarked report is overreach.
|
|
169
153
|
expect(lines).toEqual(expected);
|
|
170
154
|
});
|
|
171
155
|
|
|
@@ -15,7 +15,7 @@ or {
|
|
|
15
15
|
JsModuleSource() as $source where {
|
|
16
16
|
$source <: within JsImport(),
|
|
17
17
|
not $filename <: r".*\.(?:test|spec)\.tsx?",
|
|
18
|
-
$filename <: r".*(?:apps|libs)
|
|
18
|
+
$filename <: r".*(?:apps|libs)[/\\][^/\\]+[/\\]lib[/\\][^/\\]+[/\\][^/\\]+\.(?:ts|tsx)",
|
|
19
19
|
$source <: r"\"\.\./\.\./.*\"",
|
|
20
20
|
register_diagnostic(
|
|
21
21
|
span = $source,
|
|
@@ -25,7 +25,7 @@ or {
|
|
|
25
25
|
JsModuleSource() as $source where {
|
|
26
26
|
$source <: within JsImport(),
|
|
27
27
|
not $filename <: r".*\.(?:test|spec)\.tsx?",
|
|
28
|
-
$filename <: r"
|
|
28
|
+
$filename <: r".*[/\\]lib[/\\].+\.tsx",
|
|
29
29
|
$source <: r"\"\.\./.*\"",
|
|
30
30
|
register_diagnostic(
|
|
31
31
|
span = $source,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// A native shell serves the page from app://localhost on iOS, macOS and Linux, a custom scheme WebKit keeps no
|
|
5
|
+
// cookie jar for: `document.cookie` reads "" and a write is dropped without an error, so the value exists in the
|
|
6
|
+
// browser and silently vanishes in the app. akanjs/client's cookie helpers keep an app:// page's cookies in a
|
|
7
|
+
// storage-backed jar, which is the only spelling that works in every shell.
|
|
8
|
+
//
|
|
9
|
+
// Pinned to the member nodes: a bare snippet also matches JSX text that spells the same words (a docs page), and a
|
|
10
|
+
// write is its own node (JsStaticMemberAssignment) that the expression form does not match.
|
|
11
|
+
or {
|
|
12
|
+
JsStaticMemberExpression() as $access where {
|
|
13
|
+
$access <: or { `document.cookie`, `window.document.cookie`, `globalThis.document.cookie`, `self.document.cookie` },
|
|
14
|
+
register_diagnostic(
|
|
15
|
+
span = $access,
|
|
16
|
+
message = "`document.cookie` is always empty on a native app page (app://localhost keeps no cookies), so this value vanishes in the app. Use `getCookie` / `setCookie` / `removeCookie` from `akanjs/client`.",
|
|
17
|
+
severity = "error"
|
|
18
|
+
)
|
|
19
|
+
},
|
|
20
|
+
JsStaticMemberAssignment(object = $object, member = $member) as $access where {
|
|
21
|
+
$member <: `cookie`,
|
|
22
|
+
$object <: or { `document`, `window.document`, `globalThis.document`, `self.document` },
|
|
23
|
+
register_diagnostic(
|
|
24
|
+
span = $access,
|
|
25
|
+
message = "`document.cookie` is always empty on a native app page (app://localhost keeps no cookies), so this value vanishes in the app. Use `getCookie` / `setCookie` / `removeCookie` from `akanjs/client`.",
|
|
26
|
+
severity = "error"
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -14,7 +14,8 @@ language js(typescript, jsx)
|
|
|
14
14
|
$base <: contains `endpoint`,
|
|
15
15
|
$className <: r"([A-Z][a-zA-Z0-9]*)Endpoint"($Cap)
|
|
16
16
|
},
|
|
17
|
-
|
|
17
|
+
// `$filename` carries backslashes on Windows, so a separator here must accept both.
|
|
18
|
+
$filename <: r".*[/\\]([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
|
|
18
19
|
or {
|
|
19
20
|
$key <: $model,
|
|
20
21
|
$key <: r"(?:light|create|update|remove|view|edit|merge)([A-Z][a-zA-Z0-9]*)"($keyCap) where {
|