@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
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/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- 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 +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- 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 +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- 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 +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- 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/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- 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/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- 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/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- 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 +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- 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/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- 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/selectModel.ts
CHANGED
|
@@ -3,9 +3,8 @@ import { select } from "@inquirer/prompts";
|
|
|
3
3
|
|
|
4
4
|
export const selectModel = async (modulePath: string) => {
|
|
5
5
|
const modelNames = readdirSync(`${modulePath}/lib`).filter((dir) => !dir.includes(".") && !dir.startsWith("_"));
|
|
6
|
-
|
|
6
|
+
return await select({
|
|
7
7
|
message: "Select the model to create the unit for",
|
|
8
8
|
choices: modelNames.map((name) => ({ name, value: name })),
|
|
9
9
|
});
|
|
10
|
-
return modelName;
|
|
11
10
|
};
|
package/semver.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// `Bun.semver.order` compares
|
|
2
|
-
// operator prefix has to come off before it sees the version.
|
|
1
|
+
// `Bun.semver.order` compares ranges loosely (`^3.1049.0` vs `^3.721.0` answers 0), so the operator comes off first.
|
|
3
2
|
const stripRangeOperator = (version: string) => version.replace(/^[\s^~>=<v]+/, "").trim();
|
|
4
3
|
|
|
5
|
-
const parseVersion = (version: string): number[] =>
|
|
6
|
-
|
|
4
|
+
const parseVersion = (version: string): number[] =>
|
|
5
|
+
version
|
|
7
6
|
.replace(/^[^\d]*/, "")
|
|
8
7
|
.split(/[.-]/)
|
|
9
8
|
.map((part) => Number.parseInt(part, 10))
|
|
10
9
|
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
11
|
-
};
|
|
12
10
|
|
|
13
11
|
const compareNumeric = (a: string, b: string): number => {
|
|
14
12
|
const left = parseVersion(a);
|
package/slicePlanner.test.ts
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { AppExecutor, Executor, WorkspaceExecutor } from "./executors";
|
|
6
5
|
import { formatSlicePlan, SlicePlanner } from "./slicePlanner";
|
|
6
|
+
import { isolateEnv, tempDirs, writeText as write } from "./testHelpers";
|
|
7
7
|
import type { PackageJson } from "./types";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
process.env = { ...originalEnv };
|
|
14
|
-
process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
|
|
15
|
-
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
16
|
-
process.env.AKAN_PUBLIC_ENV = "local";
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
process.env = { ...originalEnv };
|
|
21
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const write = async (filePath: string, content: string) => {
|
|
25
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
26
|
-
await writeFile(filePath, content);
|
|
27
|
-
};
|
|
9
|
+
isolateEnv({ AKAN_PUBLIC_REPO_NAME: "workspace", AKAN_PUBLIC_SERVE_DOMAIN: "example.com", AKAN_PUBLIC_ENV: "local" });
|
|
10
|
+
const makeTempRoot = tempDirs("akan-slice-");
|
|
28
11
|
|
|
29
12
|
const gitignore = [
|
|
30
13
|
"node_modules",
|
|
@@ -36,8 +19,7 @@ const gitignore = [
|
|
|
36
19
|
|
|
37
20
|
// `AppExecutor.from` and `AppInfo.fromExecutor` both memoise by name, so each fixture needs a fresh one.
|
|
38
21
|
const makeWorkspace = async (appName: string, libName: string) => {
|
|
39
|
-
const root = await
|
|
40
|
-
tempRoots.push(root);
|
|
22
|
+
const root = await makeTempRoot();
|
|
41
23
|
const rootPackageJson: PackageJson = {
|
|
42
24
|
name: "workspace",
|
|
43
25
|
version: "0.0.1",
|
package/slicePlanner.ts
CHANGED
|
@@ -18,12 +18,6 @@ export interface SlicePlan {
|
|
|
18
18
|
warnings: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* The exact file set a single app needs to live in a workspace of its own: the app, its transitive lib
|
|
23
|
-
* closure, the workspace shell around them, and a root manifest for the result.
|
|
24
|
-
*
|
|
25
|
-
* Consumed by `akan plan-slice`, and by anything that moves an app or a lib between workspaces.
|
|
26
|
-
*/
|
|
27
21
|
export class SlicePlanner {
|
|
28
22
|
/** Root entries owned by an app, a lib or an in-tree package rather than by the workspace shell. */
|
|
29
23
|
static readonly memberDirs = ["apps", "libs", "pkgs"];
|
|
@@ -39,13 +33,8 @@ export class SlicePlanner {
|
|
|
39
33
|
];
|
|
40
34
|
static readonly akanPackages = ["akanjs", "@akanjs/devkit", "@akanjs/cli"];
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* `scheduler` and the RSC runtime, but also `ioredis`, `postgres` and `bullmq` the moment an app uses
|
|
45
|
-
* a database or a queue. No source file in the workspace names any of them, so no scan can find them
|
|
46
|
-
* — they are the akan packages' own `peerDependencies`, read wherever those are installed. Anything
|
|
47
|
-
* they declare as a plain dependency is installed with them and needs no root entry.
|
|
48
|
-
*/
|
|
36
|
+
// The akan packages' peerDependencies (react, the RSC runtime, ioredis, postgres, bullmq): the framework imports
|
|
37
|
+
// them but no workspace source names them, so no scan can find them.
|
|
49
38
|
static async #frameworkPeers(workspace: WorkspaceExecutor) {
|
|
50
39
|
const peers = new Set<string>();
|
|
51
40
|
let found = false;
|
|
@@ -67,14 +56,8 @@ export class SlicePlanner {
|
|
|
67
56
|
return dependency.startsWith("@") ? `@types/${dependency.slice(1).replace("/", "__")}` : `@types/${dependency}`;
|
|
68
57
|
}
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* version specs kept — that is what makes one branch resolve to one dependency tree everywhere.
|
|
73
|
-
*
|
|
74
|
-
* A `@types/x` package is kept alongside `x`: nothing imports it by name, so no scan can see it, and
|
|
75
|
-
* dropping it breaks the typecheck of every file that imports `x`. `workspaces` is dropped because it
|
|
76
|
-
* names in-tree packages, which a slice never carries — it consumes akanjs from the registry.
|
|
77
|
-
*/
|
|
59
|
+
// `@types/x` is kept alongside `x` since nothing imports it by name; `workspaces` is dropped because a slice
|
|
60
|
+
// consumes akanjs from the registry.
|
|
78
61
|
static async pruneDependencies(
|
|
79
62
|
workspace: WorkspaceExecutor,
|
|
80
63
|
rootPackageJson: PackageJson,
|
|
@@ -82,9 +65,8 @@ export class SlicePlanner {
|
|
|
82
65
|
) {
|
|
83
66
|
const peers = await SlicePlanner.#frameworkPeers(workspace);
|
|
84
67
|
const { workspaces: _workspaces, ...rest } = rootPackageJson;
|
|
85
|
-
//* Without
|
|
86
|
-
//*
|
|
87
|
-
//* whole instead and say why.
|
|
68
|
+
//* Without the peer list every peer looks unused, and pruning it breaks the server at its first redis or
|
|
69
|
+
//* postgres call.
|
|
88
70
|
if (!peers)
|
|
89
71
|
return {
|
|
90
72
|
packageJson: rest as PackageJson,
|
|
@@ -121,10 +103,6 @@ export class SlicePlanner {
|
|
|
121
103
|
this.#app = app;
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
async #trackedFiles(paths: string[]) {
|
|
125
|
-
return await this.#app.workspace.listGitFiles(paths);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
106
|
async #untrackedFiles(paths: string[]) {
|
|
129
107
|
const [tracked, all] = await Promise.all([
|
|
130
108
|
this.#app.workspace.listGitFiles(paths),
|
|
@@ -170,14 +148,16 @@ export class SlicePlanner {
|
|
|
170
148
|
const slicePaths = [`apps/${this.#app.name}`, ...libs.map((lib) => `libs/${lib}`)];
|
|
171
149
|
|
|
172
150
|
const [appFiles, allTracked, untracked, rootPackageJson] = await Promise.all([
|
|
173
|
-
this.#
|
|
174
|
-
this.#
|
|
151
|
+
this.#app.workspace.listGitFiles([`apps/${this.#app.name}`]),
|
|
152
|
+
this.#app.workspace.listGitFiles(["."]),
|
|
175
153
|
this.#untrackedFiles(slicePaths),
|
|
176
154
|
this.#app.workspace.getPackageJson(),
|
|
177
155
|
]);
|
|
178
156
|
|
|
179
157
|
const libFiles = Object.fromEntries(
|
|
180
|
-
await Promise.all(
|
|
158
|
+
await Promise.all(
|
|
159
|
+
libs.map(async (lib) => [lib, await this.#app.workspace.listGitFiles([`libs/${lib}`])] as const),
|
|
160
|
+
),
|
|
181
161
|
);
|
|
182
162
|
const rootFiles = allTracked.filter((file) => !this.#isMemberFile(file));
|
|
183
163
|
|
|
@@ -211,7 +191,6 @@ export class SlicePlanner {
|
|
|
211
191
|
}
|
|
212
192
|
}
|
|
213
193
|
|
|
214
|
-
/** Top-level entry each path sits under, so a long tree (`infra/**`) reads as one line. */
|
|
215
194
|
const rootEntriesOf = (rootFiles: string[]) => [...new Set(rootFiles.map((file) => file.split("/")[0] ?? file))].sort();
|
|
216
195
|
|
|
217
196
|
export function formatSlicePlan(plan: SlicePlan) {
|
package/spinner.test.ts
CHANGED
|
@@ -6,10 +6,7 @@ interface TtyStub {
|
|
|
6
6
|
rawModeCalls: boolean[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* ora only reaches for stdin when both streams look interactive, so the stub has to fake a tty on
|
|
11
|
-
* stderr (which decides `isEnabled`) as well as on stdin.
|
|
12
|
-
*/
|
|
9
|
+
// ora only reaches for stdin when both streams look interactive, so stderr (which decides `isEnabled`) is faked too.
|
|
13
10
|
const stubTty = (): TtyStub => {
|
|
14
11
|
const rawModeCalls: boolean[] = [];
|
|
15
12
|
const stdin = process.stdin as NodeJS.ReadStream & { setRawMode?: (mode: boolean) => unknown };
|
|
@@ -41,8 +38,7 @@ const stubTty = (): TtyStub => {
|
|
|
41
38
|
};
|
|
42
39
|
|
|
43
40
|
describe("Spinner", () => {
|
|
44
|
-
// A raw terminal at spawn
|
|
45
|
-
// how `akan start` used to leave a terminal that no longer turns Ctrl+C into SIGINT.
|
|
41
|
+
// A Bun child snapshots a raw terminal at spawn and writes it back on exit, leaving Ctrl+C no longer a SIGINT.
|
|
46
42
|
test("never puts the terminal into raw mode while it spins", () => {
|
|
47
43
|
const tty = stubTty();
|
|
48
44
|
try {
|
|
@@ -59,8 +55,7 @@ describe("Spinner", () => {
|
|
|
59
55
|
expect(Spinner.oraOptions.discardStdin).toBe(false);
|
|
60
56
|
});
|
|
61
57
|
|
|
62
|
-
// An unsized pty reports `isTTY: true` with `columns: 0`, which turns ora's clear loop into an
|
|
63
|
-
// infinite one — the process then writes cursor moves until it is SIGKILLed.
|
|
58
|
+
// An unsized pty reports `isTTY: true` with `columns: 0`, which turns ora's clear loop into an infinite one.
|
|
64
59
|
test("refuses to animate against a tty that reports no width", () => {
|
|
65
60
|
expect(Spinner.canAnimate({ isTTY: true, columns: 0 } as NodeJS.WriteStream)).toBe(false);
|
|
66
61
|
expect(Spinner.canAnimate({ isTTY: true, columns: 120 } as NodeJS.WriteStream)).toBe(true);
|
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/subspace.test.ts
CHANGED
|
@@ -1,34 +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
10
|
//? Each case drives several real git clones and pushes; on a 4-core Windows VM that alone lands at 2-5s.
|
|
11
11
|
setDefaultTimeout(20_000);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
process.env = { ...originalEnv };
|
|
18
|
-
process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
|
|
19
|
-
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
20
|
-
process.env.AKAN_PUBLIC_ENV = "local";
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
process.env = { ...originalEnv };
|
|
25
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const write = async (filePath: string, content: string) => {
|
|
29
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
30
|
-
await writeFile(filePath, content);
|
|
31
|
-
};
|
|
13
|
+
isolateEnv({ AKAN_PUBLIC_REPO_NAME: "workspace", AKAN_PUBLIC_SERVE_DOMAIN: "example.com", AKAN_PUBLIC_ENV: "local" });
|
|
14
|
+
const makeTempRoot = tempDirs("akan-subspace-");
|
|
32
15
|
|
|
33
16
|
const git = async (cwd: string, args: string[]) =>
|
|
34
17
|
await new Executor("fixture", cwd).spawn("git", ["-c", "user.email=t@t", "-c", "user.name=t", ...args]);
|
|
@@ -67,10 +50,8 @@ const makeSys = async (root: string, member: "apps" | "libs", name: string, extr
|
|
|
67
50
|
await write(path.join(root, member, name, relative), content);
|
|
68
51
|
};
|
|
69
52
|
|
|
70
|
-
/** A workspace with two apps, one shared library, and a subspace that serves only the first app. */
|
|
71
53
|
const makeMirror = async (servedApp: string, privateApp: string, libName: string) => {
|
|
72
|
-
const workRoot = await
|
|
73
|
-
tempRoots.push(workRoot);
|
|
54
|
+
const workRoot = await makeTempRoot();
|
|
74
55
|
const wsRoot = path.join(workRoot, "workspace");
|
|
75
56
|
const bareRoot = path.join(workRoot, "subspace.git");
|
|
76
57
|
await mkdir(wsRoot, { recursive: true });
|