@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
package/spinner.ts
CHANGED
|
@@ -2,23 +2,11 @@ import ora, { type Ora } from "ora";
|
|
|
2
2
|
|
|
3
3
|
export class Spinner {
|
|
4
4
|
static padding = 12;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* spinner runs, and a Bun child spawned in that window snapshots the raw termios and writes it back
|
|
8
|
-
* when it exits — long after the spinner restored the terminal. `akan start` spawns the builder and
|
|
9
|
-
* the backend under the "Preparing backend..." spinner, so the first builder recycle (a config or
|
|
10
|
-
* runtime-metadata change) SIGTERMs that child and silently turns the developer's terminal raw:
|
|
11
|
-
* `isig` goes off, Ctrl+C stops producing SIGINT at all, and the dev server looks unkillable.
|
|
12
|
-
*/
|
|
5
|
+
// XXX: `discardStdin` must stay off: ora raw-modes the terminal while spinning, a Bun child spawned then writes that
|
|
6
|
+
// raw termios back when it exits, and Ctrl+C stops producing SIGINT.
|
|
13
7
|
static oraOptions = { discardStdin: false } as const;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* makes it `Infinity` and `clear()` never returns. An unsized pty does exactly that (`isTTY: true`,
|
|
17
|
-
* `columns: 0`) — CI runners, `expect`/`script` harnesses, some detached panes. Measured: 750MB of
|
|
18
|
-
* cursor moves and 8.7GB RSS inside a minute, in a loop that no longer reaches the point where SIGINT
|
|
19
|
-
* or SIGTERM could be handled, so only SIGKILL ends it. A terminal with no width has nothing to
|
|
20
|
-
* animate anyway; fall back to plain lines.
|
|
21
|
-
*/
|
|
8
|
+
// ora's clear loop is `ceil(lineWidth / columns)`, so an unsized pty (`isTTY` with 0 columns: CI, `script`) never
|
|
9
|
+
// returns from `clear()`; such a terminal gets plain lines.
|
|
22
10
|
static canAnimate(stream: NodeJS.WriteStream = process.stderr): boolean {
|
|
23
11
|
return !stream.isTTY || stream.columns > 0;
|
|
24
12
|
}
|
package/sshTunnel.ts
CHANGED
|
@@ -20,7 +20,6 @@ interface TunnelSocketData {
|
|
|
20
20
|
closed: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
type BunServer = { stop(): void };
|
|
24
23
|
type BunSocket = Bun.Socket<TunnelSocketData>;
|
|
25
24
|
|
|
26
25
|
const closeQuietly = (close: () => void) => {
|
|
@@ -126,7 +125,7 @@ export const createSshTunnel = async (options: SshTunnelOptions): Promise<SshTun
|
|
|
126
125
|
closing = true;
|
|
127
126
|
for (const socket of sockets) closeSocket(socket, error);
|
|
128
127
|
closeQuietly(() => client.end());
|
|
129
|
-
|
|
128
|
+
server.stop();
|
|
130
129
|
};
|
|
131
130
|
|
|
132
131
|
client.once("error", (error) => closeTunnel(error));
|
|
@@ -146,7 +145,3 @@ const closeBunSocket = (socket: BunSocket, error?: Error) => {
|
|
|
146
145
|
else socket.data.stream?.end();
|
|
147
146
|
closeQuietly(() => socket.end());
|
|
148
147
|
};
|
|
149
|
-
|
|
150
|
-
const closeBunServer = (server: BunServer) => {
|
|
151
|
-
server.stop();
|
|
152
|
-
};
|
package/ssrScanner.test.ts
CHANGED
|
@@ -3,12 +3,7 @@ import ts from "typescript";
|
|
|
3
3
|
import type { SourceFileInfo } from "./qualityScanner";
|
|
4
4
|
import { formatSsrBalance, SSR_SERVER_SHARE_TARGET, SsrScanner } from "./ssrScanner";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
* `SsrScanner` is a documented gate — `akan quality ssr` prints the server share and AGENTS.md names 50% as
|
|
8
|
-
* the floor — so both halves matter: a rule that stops firing lets the share rot, and a rule that
|
|
9
|
-
* over-reports is what makes developers stop reading the output. Every rule here gets a positive and the
|
|
10
|
-
* negative that must stay quiet.
|
|
11
|
-
*/
|
|
6
|
+
//? Each rule gets a positive and a negative: one that stops firing lets the share rot, one that over-reports is ignored
|
|
12
7
|
const fileOf = (file: string, content: string): SourceFileInfo => ({
|
|
13
8
|
file,
|
|
14
9
|
absolutePath: `/repo/${file}`,
|
package/ssrScanner.ts
CHANGED
|
@@ -22,34 +22,32 @@ interface ComponentInfo {
|
|
|
22
22
|
vendorTags: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// A component with no client-only touch
|
|
26
|
-
//
|
|
27
|
-
// it only pays off once the static subtree is large enough to matter.
|
|
25
|
+
// A component with no client-only touch is worth moving even when small; a mostly-static one only once its static
|
|
26
|
+
// subtree is large enough to matter.
|
|
28
27
|
const STATIC_COMPONENT_MIN_MASS = 4;
|
|
29
28
|
const MIXED_COMPONENT_MIN_MASS = 10;
|
|
30
29
|
const MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
31
30
|
const MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
|
|
32
31
|
|
|
32
|
+
export const fileWarning = (
|
|
33
|
+
rule: string,
|
|
34
|
+
scope: QualityWarning["scope"],
|
|
35
|
+
file: string,
|
|
36
|
+
message: string,
|
|
37
|
+
line?: number,
|
|
38
|
+
): QualityWarning =>
|
|
39
|
+
line === undefined
|
|
40
|
+
? { rule, scope, severity: "warning", file, message }
|
|
41
|
+
: { rule, scope, severity: "warning", file, line, message };
|
|
42
|
+
|
|
33
43
|
export class SsrScanner {
|
|
34
|
-
// `usePage` and `getSelf` read request-scoped server context
|
|
35
|
-
// must not count as evidence that a file needs "use client".
|
|
44
|
+
// `usePage` and `getSelf` read request-scoped server context, which server components may do.
|
|
36
45
|
static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
|
|
37
|
-
static #clientGlobals = new Set(
|
|
38
|
-
"window"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"sessionStorage",
|
|
43
|
-
"location",
|
|
44
|
-
"history",
|
|
45
|
-
"screen",
|
|
46
|
-
"matchMedia",
|
|
47
|
-
"IntersectionObserver",
|
|
48
|
-
"ResizeObserver",
|
|
49
|
-
"MutationObserver",
|
|
50
|
-
"requestAnimationFrame",
|
|
51
|
-
"WebSocket",
|
|
52
|
-
]);
|
|
46
|
+
static #clientGlobals = new Set(
|
|
47
|
+
"window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver"
|
|
48
|
+
.split(" ")
|
|
49
|
+
.concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")),
|
|
50
|
+
);
|
|
53
51
|
// Runtime singletons that only exist in the client bundle; importing either is what forces the directive.
|
|
54
52
|
static #clientRuntimeImports = new Set(["st", "fetch"]);
|
|
55
53
|
|
|
@@ -78,27 +76,29 @@ export class SsrScanner {
|
|
|
78
76
|
this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 &&
|
|
79
77
|
!this.#isConventionClientFile(sourceFile.file)
|
|
80
78
|
) {
|
|
81
|
-
warnings.push(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
warnings.push(
|
|
80
|
+
fileWarning(
|
|
81
|
+
"akan.ssr.unnecessary-use-client",
|
|
82
|
+
"ssr",
|
|
83
|
+
sourceFile.file,
|
|
84
|
+
`"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`,
|
|
85
|
+
1,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
for (const component of components) {
|
|
92
91
|
if (component.vendorTags) continue;
|
|
93
92
|
if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
|
|
94
|
-
warnings.push(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
warnings.push(
|
|
94
|
+
fileWarning(
|
|
95
|
+
"akan.ssr.client-static-component",
|
|
96
|
+
"ssr",
|
|
97
|
+
sourceFile.file,
|
|
98
|
+
`Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`,
|
|
99
|
+
component.line,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
104
|
if (
|
|
@@ -106,14 +106,15 @@ export class SsrScanner {
|
|
|
106
106
|
component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES &&
|
|
107
107
|
component.mass >= MIXED_COMPONENT_MIN_MASS
|
|
108
108
|
) {
|
|
109
|
-
warnings.push(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
warnings.push(
|
|
110
|
+
fileWarning(
|
|
111
|
+
"akan.ssr.client-static-markup",
|
|
112
|
+
"ssr",
|
|
113
|
+
sourceFile.file,
|
|
114
|
+
`Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`,
|
|
115
|
+
component.line,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -122,8 +123,7 @@ export class SsrScanner {
|
|
|
122
123
|
return warnings;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
// A
|
|
126
|
-
// all, so every consumer pays for hydration even when it only needs to display the model.
|
|
126
|
+
// A module rendered only from Template/Zone/Util makes every consumer pay for hydration just to display it.
|
|
127
127
|
#scanModules(sourceFiles: SourceFileInfo[]): QualityWarning[] {
|
|
128
128
|
const modules = new Map<string, { clientMass: number; serverFiles: number; line: string }>();
|
|
129
129
|
for (const sourceFile of sourceFiles) {
|
|
@@ -136,36 +136,32 @@ export class SsrScanner {
|
|
|
136
136
|
}
|
|
137
137
|
return [...modules]
|
|
138
138
|
.filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS)
|
|
139
|
-
.map(([moduleDir, entry]) =>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
.map(([moduleDir, entry]) =>
|
|
140
|
+
fileWarning(
|
|
141
|
+
"akan.ssr.module-missing-server-view",
|
|
142
|
+
"ssr",
|
|
143
|
+
entry.line,
|
|
144
|
+
`Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
//
|
|
149
|
-
// empty shell, hydrates, then fetches. A reactive effect (non-empty deps) responds to client state instead
|
|
150
|
-
// and has no server-side equivalent, so only the empty-dependency form is a finding.
|
|
149
|
+
// Only an empty-dependency effect is a finding: a reactive effect answers client state and has no server equivalent.
|
|
151
150
|
#getMountLoadWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
152
151
|
const warnings: QualityWarning[] = [];
|
|
153
|
-
|
|
154
|
-
if (this.#isMountEffect(sourceFile.sourceFile, node))
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
ts.forEachChild(node, visit);
|
|
167
|
-
};
|
|
168
|
-
ts.forEachChild(sourceFile.sourceFile, visit);
|
|
152
|
+
SsrScanner.#walk(sourceFile.sourceFile, (node) => {
|
|
153
|
+
if (!this.#isMountEffect(sourceFile.sourceFile, node)) return;
|
|
154
|
+
for (const load of this.#getLoadCalls(sourceFile.sourceFile, node))
|
|
155
|
+
warnings.push(
|
|
156
|
+
fileWarning(
|
|
157
|
+
"akan.ssr.client-mount-load",
|
|
158
|
+
"ssr",
|
|
159
|
+
sourceFile.file,
|
|
160
|
+
`Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`,
|
|
161
|
+
this.#getLine(sourceFile.sourceFile, load.node),
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
169
165
|
return warnings;
|
|
170
166
|
}
|
|
171
167
|
|
|
@@ -179,35 +175,30 @@ export class SsrScanner {
|
|
|
179
175
|
|
|
180
176
|
#getLoadCalls(sourceFile: ts.SourceFile, node: ts.Node) {
|
|
181
177
|
const calls: Array<{ callee: string; node: ts.Node }> = [];
|
|
182
|
-
|
|
183
|
-
if (ts.isCallExpression(child))
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
ts.forEachChild(child, visit);
|
|
189
|
-
};
|
|
190
|
-
ts.forEachChild(node, visit);
|
|
178
|
+
SsrScanner.#walk(node, (child) => {
|
|
179
|
+
if (!ts.isCallExpression(child)) return;
|
|
180
|
+
const callee = child.expression.getText(sourceFile);
|
|
181
|
+
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
182
|
+
calls.push({ callee, node: child });
|
|
183
|
+
});
|
|
191
184
|
return calls;
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
#getTemplateStateWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
195
188
|
if (!sourceFile.file.endsWith(".Template.tsx")) return [];
|
|
196
189
|
const warnings: QualityWarning[] = [];
|
|
197
|
-
|
|
198
|
-
if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
|
|
199
|
-
warnings.push(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
};
|
|
210
|
-
ts.forEachChild(sourceFile.sourceFile, visit);
|
|
190
|
+
SsrScanner.#walk(sourceFile.sourceFile, (node) => {
|
|
191
|
+
if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
|
|
192
|
+
warnings.push(
|
|
193
|
+
fileWarning(
|
|
194
|
+
"akan.ssr.template-client-state",
|
|
195
|
+
"ssr",
|
|
196
|
+
sourceFile.file,
|
|
197
|
+
"Template holds form state in useState. Templates are store-driven and carry no local state.",
|
|
198
|
+
this.#getLine(sourceFile.sourceFile, node),
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
211
202
|
return warnings;
|
|
212
203
|
}
|
|
213
204
|
|
|
@@ -261,7 +252,7 @@ export class SsrScanner {
|
|
|
261
252
|
|
|
262
253
|
#getTouches(sourceFile: ts.SourceFile, node: ts.Node): string[] {
|
|
263
254
|
const touches: string[] = [];
|
|
264
|
-
|
|
255
|
+
SsrScanner.#walk(node, (child) => {
|
|
265
256
|
if (ts.isCallExpression(child)) {
|
|
266
257
|
const callee = child.expression.getText(sourceFile);
|
|
267
258
|
const bareName = callee.split(".").pop() ?? callee;
|
|
@@ -275,41 +266,42 @@ export class SsrScanner {
|
|
|
275
266
|
if (root === "st") touches.push("st");
|
|
276
267
|
else if (SsrScanner.#clientGlobals.has(root)) touches.push(root);
|
|
277
268
|
}
|
|
278
|
-
|
|
279
|
-
};
|
|
280
|
-
ts.forEachChild(node, visit);
|
|
269
|
+
});
|
|
281
270
|
return touches;
|
|
282
271
|
}
|
|
283
272
|
|
|
284
273
|
#getTagNames(sourceFile: ts.SourceFile, node: ts.Node): Set<string> {
|
|
285
274
|
const tags = new Set<string>();
|
|
286
|
-
|
|
275
|
+
SsrScanner.#walk(node, (child) => {
|
|
287
276
|
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
|
|
288
277
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
289
|
-
|
|
290
|
-
};
|
|
291
|
-
ts.forEachChild(node, visit);
|
|
278
|
+
});
|
|
292
279
|
return tags;
|
|
293
280
|
}
|
|
294
281
|
|
|
295
282
|
#getMass(node: ts.Node) {
|
|
296
283
|
let mass = 0;
|
|
297
|
-
|
|
284
|
+
SsrScanner.#walk(node, (child) => {
|
|
298
285
|
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child)) mass += 1;
|
|
299
|
-
|
|
300
|
-
};
|
|
301
|
-
ts.forEachChild(node, visit);
|
|
286
|
+
});
|
|
302
287
|
return mass;
|
|
303
288
|
}
|
|
304
289
|
|
|
290
|
+
static #walk(node: ts.Node, visit: (descendant: ts.Node) => void) {
|
|
291
|
+
const walk = (child: ts.Node) => {
|
|
292
|
+
visit(child);
|
|
293
|
+
ts.forEachChild(child, walk);
|
|
294
|
+
};
|
|
295
|
+
ts.forEachChild(node, walk);
|
|
296
|
+
}
|
|
297
|
+
|
|
305
298
|
#hasUseClient(sourceFile: ts.SourceFile) {
|
|
306
299
|
const first = sourceFile.statements[0];
|
|
307
300
|
if (!first || !ts.isExpressionStatement(first) || !ts.isStringLiteral(first.expression)) return false;
|
|
308
301
|
return first.expression.text === "use client";
|
|
309
302
|
}
|
|
310
303
|
|
|
311
|
-
// A
|
|
312
|
-
// directive that no amount of AST reading can rule out.
|
|
304
|
+
// A third-party package may be client-only, a legitimate reason for the directive no AST reading can rule out.
|
|
313
305
|
#hasVendorImport(sourceFile: ts.SourceFile) {
|
|
314
306
|
return sourceFile.statements.some(
|
|
315
307
|
(statement) => ts.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)),
|
|
@@ -340,9 +332,7 @@ export class SsrScanner {
|
|
|
340
332
|
return false;
|
|
341
333
|
}
|
|
342
334
|
|
|
343
|
-
// Zone/Template/Util carry the directive
|
|
344
|
-
// "use client" + lazy() boundary. In neither case is the directive a stray — for module UI it means markup
|
|
345
|
-
// belongs in a Unit or View instead, which the component rules already cover.
|
|
335
|
+
// Zone/Template/Util carry the directive by file role, and `index_.tsx` is the declared lazy() boundary.
|
|
346
336
|
#isConventionClientFile(file: string) {
|
|
347
337
|
if (file.endsWith("/index_.tsx")) return true;
|
|
348
338
|
return /\.(Zone|Template|Util)\.tsx$/.test(file) && this.#getModuleDir(file) !== null;
|
package/stripAnsi.ts
ADDED
package/subspace.test.ts
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, setDefaultTimeout, test } from "bun:test";
|
|
2
|
+
import { mkdir, readdir, rm } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { Executor, WorkspaceExecutor } from "./executors";
|
|
6
5
|
import { FileSys } from "./fileSys";
|
|
7
6
|
import { formatSubspaceDiff, formatSubspacePushResults, formatSubspaceStatuses, Subspace } from "./subspace";
|
|
8
7
|
import { SubspaceConfig } from "./subspaceConfig";
|
|
8
|
+
import { isolateEnv, tempDirs, writeText as write } from "./testHelpers";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
//? Each case drives several real git clones and pushes; on a 4-core Windows VM that alone lands at 2-5s.
|
|
11
|
+
setDefaultTimeout(20_000);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
|
|
16
|
-
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
17
|
-
process.env.AKAN_PUBLIC_ENV = "local";
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
process.env = { ...originalEnv };
|
|
22
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const write = async (filePath: string, content: string) => {
|
|
26
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
27
|
-
await writeFile(filePath, content);
|
|
28
|
-
};
|
|
13
|
+
isolateEnv({ AKAN_PUBLIC_REPO_NAME: "workspace", AKAN_PUBLIC_SERVE_DOMAIN: "example.com", AKAN_PUBLIC_ENV: "local" });
|
|
14
|
+
const makeTempRoot = tempDirs("akan-subspace-");
|
|
29
15
|
|
|
30
16
|
const git = async (cwd: string, args: string[]) =>
|
|
31
17
|
await new Executor("fixture", cwd).spawn("git", ["-c", "user.email=t@t", "-c", "user.name=t", ...args]);
|
|
@@ -64,10 +50,8 @@ const makeSys = async (root: string, member: "apps" | "libs", name: string, extr
|
|
|
64
50
|
await write(path.join(root, member, name, relative), content);
|
|
65
51
|
};
|
|
66
52
|
|
|
67
|
-
/** A workspace with two apps, one shared library, and a subspace that serves only the first app. */
|
|
68
53
|
const makeMirror = async (servedApp: string, privateApp: string, libName: string) => {
|
|
69
|
-
const workRoot = await
|
|
70
|
-
tempRoots.push(workRoot);
|
|
54
|
+
const workRoot = await makeTempRoot();
|
|
71
55
|
const wsRoot = path.join(workRoot, "workspace");
|
|
72
56
|
const bareRoot = path.join(workRoot, "subspace.git");
|
|
73
57
|
await mkdir(wsRoot, { recursive: true });
|
|
@@ -96,7 +80,7 @@ const makeMirror = async (servedApp: string, privateApp: string, libName: string
|
|
|
96
80
|
await write(path.join(wsRoot, "benchmarks/bench.ts"), "export {};\n");
|
|
97
81
|
await write(
|
|
98
82
|
path.join(wsRoot, "akan.subspace.ts"),
|
|
99
|
-
`export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo:
|
|
83
|
+
`export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo: ${JSON.stringify(bareRoot)}, apps: ["${servedApp}"] }] };\n`,
|
|
100
84
|
);
|
|
101
85
|
|
|
102
86
|
await makeSys(wsRoot, "apps", servedApp, {
|