@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.90
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/CHANGELOG.md +15 -0
- package/DEV_RUNTIME_KNOBS.md +27 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +86 -47
- package/biomeBase.ts +6 -5
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +129 -4
- package/executors.ts +105 -24
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +67 -0
- package/frontendBuild/watchRootResolver.ts +13 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
package/subspace.test.ts
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Executor, WorkspaceExecutor } from "./executors";
|
|
6
|
+
import { FileSys } from "./fileSys";
|
|
7
|
+
import { formatSubspaceDiff, formatSubspacePushResults, formatSubspaceStatuses, Subspace } from "./subspace";
|
|
8
|
+
import { SubspaceConfig } from "./subspaceConfig";
|
|
9
|
+
|
|
10
|
+
const tempRoots: string[] = [];
|
|
11
|
+
const originalEnv = { ...process.env };
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
process.env = { ...originalEnv };
|
|
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
|
+
};
|
|
29
|
+
|
|
30
|
+
const git = async (cwd: string, args: string[]) =>
|
|
31
|
+
await new Executor("fixture", cwd).spawn("git", ["-c", "user.email=t@t", "-c", "user.name=t", ...args]);
|
|
32
|
+
|
|
33
|
+
const wsGitignore = [
|
|
34
|
+
"node_modules",
|
|
35
|
+
"**/.akan",
|
|
36
|
+
"**/bun.lock",
|
|
37
|
+
"apps/*/lib/cnst.ts",
|
|
38
|
+
"**/env.server.local.ts",
|
|
39
|
+
"**/akan.app.json",
|
|
40
|
+
"",
|
|
41
|
+
"# akan:secrets (managed by akan.config.ts — do not edit)",
|
|
42
|
+
"apps/served/secrets/**/*",
|
|
43
|
+
"apps/private/secrets/**",
|
|
44
|
+
"# akan:secrets:end",
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
const wsAgents = [
|
|
48
|
+
"# Workspace",
|
|
49
|
+
"",
|
|
50
|
+
"## Workspace",
|
|
51
|
+
"",
|
|
52
|
+
"- Repo: workspace",
|
|
53
|
+
"- Apps: private, served",
|
|
54
|
+
"- Libraries: kit",
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
|
|
58
|
+
const makeSys = async (root: string, member: "apps" | "libs", name: string, extra: Record<string, string> = {}) => {
|
|
59
|
+
await write(path.join(root, member, name, "akan.config.ts"), "export default {};\n");
|
|
60
|
+
await write(path.join(root, member, name, "tsconfig.json"), "{}\n");
|
|
61
|
+
await write(path.join(root, member, name, "package.json"), `{ "name": "@${name}", "version": "0.0.1" }\n`);
|
|
62
|
+
await mkdir(path.join(root, member, name, "lib"), { recursive: true });
|
|
63
|
+
for (const [relative, content] of Object.entries(extra))
|
|
64
|
+
await write(path.join(root, member, name, relative), content);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** A workspace with two apps, one shared library, and a subspace that serves only the first app. */
|
|
68
|
+
const makeMirror = async (servedApp: string, privateApp: string, libName: string) => {
|
|
69
|
+
const workRoot = await mkdtemp(path.join(os.tmpdir(), "akan-subspace-"));
|
|
70
|
+
tempRoots.push(workRoot);
|
|
71
|
+
const wsRoot = path.join(workRoot, "workspace");
|
|
72
|
+
const bareRoot = path.join(workRoot, "subspace.git");
|
|
73
|
+
await mkdir(wsRoot, { recursive: true });
|
|
74
|
+
|
|
75
|
+
await write(
|
|
76
|
+
path.join(wsRoot, "package.json"),
|
|
77
|
+
`${JSON.stringify(
|
|
78
|
+
{
|
|
79
|
+
name: "workspace",
|
|
80
|
+
version: "0.0.1",
|
|
81
|
+
description: "workspace",
|
|
82
|
+
dependencies: { lodash: "4.0.0", "another-app-dep": "1.0.0", ioredis: "5.0.0" },
|
|
83
|
+
devDependencies: { typescript: "6.0.0" },
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2,
|
|
87
|
+
)}\n`,
|
|
88
|
+
);
|
|
89
|
+
await write(path.join(wsRoot, ".gitignore"), `${wsGitignore}\n`);
|
|
90
|
+
await write(path.join(wsRoot, "AGENTS.md"), wsAgents);
|
|
91
|
+
await write(path.join(wsRoot, "biome.json"), "{}\n");
|
|
92
|
+
await write(
|
|
93
|
+
path.join(wsRoot, "node_modules/akanjs/package.json"),
|
|
94
|
+
'{ "name": "akanjs", "version": "3.0.0", "peerDependencies": { "ioredis": "5.0.0" } }\n',
|
|
95
|
+
);
|
|
96
|
+
await write(path.join(wsRoot, "benchmarks/bench.ts"), "export {};\n");
|
|
97
|
+
await write(
|
|
98
|
+
path.join(wsRoot, "akan.subspace.ts"),
|
|
99
|
+
`export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo: "${bareRoot}", apps: ["${servedApp}"] }] };\n`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await makeSys(wsRoot, "apps", servedApp, {
|
|
103
|
+
"lib/task/task.constant.ts": [
|
|
104
|
+
`import { helper } from "@libs/${libName}/common";`,
|
|
105
|
+
'import lodash from "lodash";',
|
|
106
|
+
"",
|
|
107
|
+
"export { helper, lodash };",
|
|
108
|
+
"",
|
|
109
|
+
].join("\n"),
|
|
110
|
+
"lib/cnst.ts": "export {};\n",
|
|
111
|
+
"env/env.server.local.ts": "export const env = { from: 'workspace' };\n",
|
|
112
|
+
"env/env.server.ts": "export const env = {};\n",
|
|
113
|
+
});
|
|
114
|
+
await makeSys(wsRoot, "apps", privateApp, { "lib/task/task.constant.ts": "export const task = 1;\n" });
|
|
115
|
+
await makeSys(wsRoot, "libs", libName, { "common/helper.ts": "export const helper = 1;\n" });
|
|
116
|
+
|
|
117
|
+
await git(wsRoot, ["init", "--quiet", "--initial-branch=develop"]);
|
|
118
|
+
await git(wsRoot, ["add", "-A"]);
|
|
119
|
+
await git(wsRoot, ["commit", "--quiet", "-m", "workspace"]);
|
|
120
|
+
|
|
121
|
+
await git(workRoot, ["init", "--bare", "--quiet", "--initial-branch=develop", bareRoot]);
|
|
122
|
+
const seedRoot = path.join(workRoot, "seed");
|
|
123
|
+
await git(workRoot, ["clone", "--quiet", bareRoot, seedRoot]);
|
|
124
|
+
await write(path.join(seedRoot, "README.md"), "subspace\n");
|
|
125
|
+
await git(seedRoot, ["checkout", "--quiet", "-B", "develop"]);
|
|
126
|
+
await git(seedRoot, ["add", "-A"]);
|
|
127
|
+
await git(seedRoot, ["commit", "--quiet", "-m", "seed"]);
|
|
128
|
+
await git(seedRoot, ["push", "--quiet", "origin", "develop"]);
|
|
129
|
+
|
|
130
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: wsRoot, repoName: `workspace-${servedApp}` });
|
|
131
|
+
const config = await SubspaceConfig.from(workspace);
|
|
132
|
+
if (!config) throw new Error("fixture config missing");
|
|
133
|
+
const declaration = config.subspaces[0];
|
|
134
|
+
if (!declaration) throw new Error("fixture subspace missing");
|
|
135
|
+
const subspace = new Subspace(workspace, config, declaration);
|
|
136
|
+
const clonePath = path.join(wsRoot, ".akan/subspace/acme");
|
|
137
|
+
return { workRoot, wsRoot, bareRoot, workspace, subspace, clonePath };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** A working copy of the subspace, as a customer developer would have. */
|
|
141
|
+
const cloneSubspace = async (workRoot: string, bareRoot: string, name: string) => {
|
|
142
|
+
const root = path.join(workRoot, name);
|
|
143
|
+
await git(workRoot, ["clone", "--quiet", bareRoot, root]);
|
|
144
|
+
await git(root, ["checkout", "--quiet", "develop"]);
|
|
145
|
+
return root;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
describe("Subspace push", () => {
|
|
149
|
+
test("ships the slice, holds back everything workspace-only, and preserves the subspace's env", async () => {
|
|
150
|
+
const { wsRoot, bareRoot, workRoot, subspace, clonePath } = await makeMirror("served", "private", "kit");
|
|
151
|
+
|
|
152
|
+
const result = await subspace.push("develop", { verify: false });
|
|
153
|
+
expect(result.outcome).toBe("pushed");
|
|
154
|
+
|
|
155
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "check");
|
|
156
|
+
const entries = await readdir(subspaceRoot);
|
|
157
|
+
|
|
158
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, "apps/served/lib/task/task.constant.ts"))).toBe(true);
|
|
159
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, "libs/kit/common/helper.ts"))).toBe(true);
|
|
160
|
+
expect(await FileSys.dirExists(path.join(subspaceRoot, "apps/private"))).toBe(false);
|
|
161
|
+
expect(entries).not.toContain("benchmarks");
|
|
162
|
+
expect(entries).not.toContain("akan.subspace.ts");
|
|
163
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, "apps/served/lib/cnst.ts"))).toBe(false);
|
|
164
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, "apps/served/env/env.server.local.ts"))).toBe(false);
|
|
165
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, "apps/served/env/env.server.ts"))).toBe(true);
|
|
166
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, "README.md"))).toBe(true);
|
|
167
|
+
void wsRoot;
|
|
168
|
+
void clonePath;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("filters the leaky generated blocks down to this subspace", async () => {
|
|
172
|
+
const { bareRoot, workRoot, subspace } = await makeMirror("leak-served", "leak-private", "leak-kit");
|
|
173
|
+
await subspace.push("develop", { verify: false });
|
|
174
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "check");
|
|
175
|
+
|
|
176
|
+
const gitignore = await FileSys.readText(path.join(subspaceRoot, ".gitignore"));
|
|
177
|
+
expect(gitignore).not.toContain("apps/private/secrets");
|
|
178
|
+
expect(gitignore).not.toContain("**/bun.lock");
|
|
179
|
+
|
|
180
|
+
const agents = await FileSys.readText(path.join(subspaceRoot, "AGENTS.md"));
|
|
181
|
+
expect(agents).toContain("- Apps: leak-served");
|
|
182
|
+
expect(agents).toContain("- Libraries: leak-kit");
|
|
183
|
+
expect(agents).not.toContain("leak-private");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("writes the anchor file and stamps the library it shipped", async () => {
|
|
187
|
+
const { bareRoot, workRoot, subspace } = await makeMirror("stamp-served", "stamp-private", "stamp-kit");
|
|
188
|
+
await subspace.push("develop", { verify: false });
|
|
189
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "check");
|
|
190
|
+
|
|
191
|
+
const anchor = (await FileSys.readJson(path.join(subspaceRoot, Subspace.anchorFile))) as {
|
|
192
|
+
branch: string;
|
|
193
|
+
apps: string[];
|
|
194
|
+
};
|
|
195
|
+
expect(anchor.branch).toBe("develop");
|
|
196
|
+
expect(anchor.apps).toEqual(["stamp-served"]);
|
|
197
|
+
|
|
198
|
+
const manifest = (await FileSys.readJson(path.join(subspaceRoot, "libs/stamp-kit/package.json"))) as {
|
|
199
|
+
akan: { source: { origin: string; hash: string } };
|
|
200
|
+
};
|
|
201
|
+
expect(manifest.akan.source.origin).toContain("#develop");
|
|
202
|
+
expect(manifest.akan.source.hash).toMatch(/^[0-9a-f]{32}$/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("is idempotent — a second push with no workspace change is skipped", async () => {
|
|
206
|
+
const { subspace } = await makeMirror("idem-served", "idem-private", "idem-kit");
|
|
207
|
+
expect((await subspace.push("develop", { verify: false })).outcome).toBe("pushed");
|
|
208
|
+
expect((await subspace.push("develop", { verify: false })).outcome).toBe("skipped");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("refuses a branch the subspace does not have, and one the config does not allow", async () => {
|
|
212
|
+
const { subspace } = await makeMirror("refuse-served", "refuse-private", "refuse-kit");
|
|
213
|
+
const missing = await subspace.push("release", { verify: false }).catch((error: Error) => error);
|
|
214
|
+
expect((missing as Error).message).toContain("not pushable");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
//? The fixture workspace deliberately does not gitignore `.env`, so this also proves the clone-local exclude.
|
|
218
|
+
test("leaves the clone a runnable workspace root without shipping the file that makes it one", async () => {
|
|
219
|
+
const { bareRoot, workRoot, subspace, clonePath } = await makeMirror("env-served", "env-private", "env-kit");
|
|
220
|
+
await subspace.push("develop", { verify: false });
|
|
221
|
+
|
|
222
|
+
const localEnv = await FileSys.readText(path.join(clonePath, ".env"));
|
|
223
|
+
expect(localEnv).toContain("AKAN_PUBLIC_REPO_NAME=acme");
|
|
224
|
+
expect(localEnv).toContain("AKAN_PUBLIC_SERVE_DOMAIN=example.com");
|
|
225
|
+
expect(localEnv).not.toContain("AKAN_PUBLIC_REPO_NAME=workspace");
|
|
226
|
+
expect(await FileSys.readText(path.join(clonePath, ".git/info/exclude"))).toContain("/.env");
|
|
227
|
+
|
|
228
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "env-check");
|
|
229
|
+
expect(await FileSys.fileExists(path.join(subspaceRoot, ".env"))).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("rebuilds the root manifest from this subspace's own slices", async () => {
|
|
233
|
+
const { bareRoot, workRoot, subspace } = await makeMirror("dep-served", "dep-private", "dep-kit");
|
|
234
|
+
const result = await subspace.push("develop", { verify: false });
|
|
235
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "dep-check");
|
|
236
|
+
|
|
237
|
+
const manifest = (await FileSys.readJson(path.join(subspaceRoot, "package.json"))) as {
|
|
238
|
+
name: string;
|
|
239
|
+
dependencies: Record<string, string>;
|
|
240
|
+
devDependencies: Record<string, string>;
|
|
241
|
+
};
|
|
242
|
+
expect(manifest.dependencies).toEqual({ lodash: "4.0.0", ioredis: "5.0.0" });
|
|
243
|
+
expect(manifest.devDependencies).toEqual({ typescript: "6.0.0" });
|
|
244
|
+
expect(manifest.name).toBe("acme");
|
|
245
|
+
expect(result.prunedDependencies).toEqual(["another-app-dep"]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("reports a refusal's whole reason, indented, instead of one truncated line", () => {
|
|
249
|
+
const report = formatSubspacePushResults([
|
|
250
|
+
{ name: "acme", outcome: "refused", reason: "verification failed\nexit code: 1", changedFiles: 12 },
|
|
251
|
+
{ name: "beta", outcome: "pushed", commit: "abc1234", changedFiles: 3 },
|
|
252
|
+
]);
|
|
253
|
+
expect(report).toContain("refused acme verification failed");
|
|
254
|
+
expect(report).toContain(" exit code: 1");
|
|
255
|
+
expect(report).toContain("pushed beta abc1234 (3 files)");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("Subspace status", () => {
|
|
260
|
+
test("reads clean right after a push, despite the stamp only the subspace carries", async () => {
|
|
261
|
+
const { subspace } = await makeMirror("status-served", "status-private", "status-kit");
|
|
262
|
+
await subspace.push("develop", { verify: false });
|
|
263
|
+
|
|
264
|
+
const status = await subspace.status("develop");
|
|
265
|
+
|
|
266
|
+
expect(status.hasBranch).toBe(true);
|
|
267
|
+
expect(status.behindPaths).toEqual([]);
|
|
268
|
+
expect(status.driftedLibs).toEqual([]);
|
|
269
|
+
expect(status.anchor.commit).toMatch(/^[0-9a-f]{40}$/);
|
|
270
|
+
expect(status.anchor.incoming).toEqual([]);
|
|
271
|
+
expect(formatSubspaceStatuses([status])).toContain("up to date, no customer commits");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("names the drifted library and counts the customer commits", async () => {
|
|
275
|
+
const { bareRoot, workRoot, subspace } = await makeMirror("sdrift-served", "sdrift-private", "sdrift-kit");
|
|
276
|
+
await subspace.push("develop", { verify: false });
|
|
277
|
+
|
|
278
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "customer");
|
|
279
|
+
await write(path.join(subspaceRoot, "libs/sdrift-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
280
|
+
await git(subspaceRoot, ["add", "-A"]);
|
|
281
|
+
await git(subspaceRoot, ["commit", "--quiet", "-m", "lib hack"]);
|
|
282
|
+
await git(subspaceRoot, ["push", "--quiet", "origin", "develop"]);
|
|
283
|
+
|
|
284
|
+
const status = await subspace.status("develop");
|
|
285
|
+
|
|
286
|
+
expect(status.driftedLibs).toEqual(["sdrift-kit"]);
|
|
287
|
+
expect(status.anchor.incoming.map((commit) => commit.subject)).toEqual(["lib hack"]);
|
|
288
|
+
expect(formatSubspaceStatuses([status])).toContain("DRIFTED LIBS: sdrift-kit");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("reports a subspace with no such branch instead of creating one", async () => {
|
|
292
|
+
const { subspace } = await makeMirror("nobranch-served", "nobranch-private", "nobranch-kit");
|
|
293
|
+
const status = await subspace.status("main");
|
|
294
|
+
|
|
295
|
+
expect(status.hasBranch).toBe(false);
|
|
296
|
+
expect(formatSubspaceStatuses([status])).toContain("no such branch");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("Subspace diff", () => {
|
|
301
|
+
test("reads as what a push would apply, with library changes split out", async () => {
|
|
302
|
+
const { bareRoot, workRoot, subspace } = await makeMirror("diff-served", "diff-private", "diff-kit");
|
|
303
|
+
await subspace.push("develop", { verify: false });
|
|
304
|
+
|
|
305
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "customer");
|
|
306
|
+
await write(path.join(subspaceRoot, "libs/diff-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
307
|
+
await write(path.join(subspaceRoot, "apps/diff-served/lib/task/task.service.ts"), "export const fixed = true;\n");
|
|
308
|
+
await git(subspaceRoot, ["add", "-A"]);
|
|
309
|
+
await git(subspaceRoot, ["commit", "--quiet", "-m", "customer work"]);
|
|
310
|
+
await git(subspaceRoot, ["push", "--quiet", "origin", "develop"]);
|
|
311
|
+
|
|
312
|
+
const result = await subspace.diff("develop");
|
|
313
|
+
|
|
314
|
+
expect(result.libs.files).toEqual(["libs/diff-kit/common/helper.ts"]);
|
|
315
|
+
expect(result.app.files).toEqual(["apps/diff-served/lib/task/task.service.ts"]);
|
|
316
|
+
//? Direction: the workspace would restore `helper = 1` and delete the file the subspace added.
|
|
317
|
+
expect(result.libs.patch).toContain("-export const helper = 99;");
|
|
318
|
+
expect(result.libs.patch).toContain("+export const helper = 1;");
|
|
319
|
+
expect(formatSubspaceDiff(result)).toContain("LIBRARY changes (1)");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("is empty right after a push", async () => {
|
|
323
|
+
const { subspace } = await makeMirror("quietdiff-served", "quietdiff-private", "quietdiff-kit");
|
|
324
|
+
await subspace.push("develop", { verify: false });
|
|
325
|
+
|
|
326
|
+
const result = await subspace.diff("develop");
|
|
327
|
+
expect(result.app.files).toEqual([]);
|
|
328
|
+
expect(result.libs.files).toEqual([]);
|
|
329
|
+
expect(formatSubspaceDiff(result)).toContain("identical to the workspace");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("Subspace pull", () => {
|
|
334
|
+
test("applies the customer's app commits and holds their library edits back", async () => {
|
|
335
|
+
const { wsRoot, bareRoot, workRoot, subspace } = await makeMirror("pull-served", "pull-private", "pull-kit");
|
|
336
|
+
await subspace.push("develop", { verify: false });
|
|
337
|
+
|
|
338
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "customer");
|
|
339
|
+
await write(path.join(subspaceRoot, "apps/pull-served/lib/task/task.service.ts"), "export const fixed = true;\n");
|
|
340
|
+
await write(path.join(subspaceRoot, "libs/pull-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
341
|
+
await write(path.join(subspaceRoot, "apps/pull-served/env/env.server.local.ts"), "export const env = {};\n");
|
|
342
|
+
await git(subspaceRoot, ["add", "-A"]);
|
|
343
|
+
await git(subspaceRoot, ["commit", "--quiet", "-m", "customer hotfix"]);
|
|
344
|
+
await git(subspaceRoot, ["push", "--quiet", "origin", "develop"]);
|
|
345
|
+
|
|
346
|
+
const result = await subspace.pull("develop");
|
|
347
|
+
|
|
348
|
+
expect(result.incoming.map((commit) => commit.subject)).toEqual(["customer hotfix"]);
|
|
349
|
+
expect(result.applied).toEqual(["apps/pull-served/lib/task/task.service.ts"]);
|
|
350
|
+
expect(await FileSys.fileExists(path.join(wsRoot, "apps/pull-served/lib/task/task.service.ts"))).toBe(true);
|
|
351
|
+
|
|
352
|
+
expect(result.libPatch?.files).toEqual(["libs/pull-kit/common/helper.ts"]);
|
|
353
|
+
expect(await FileSys.readText(path.join(wsRoot, "libs/pull-kit/common/helper.ts"))).toContain("helper = 1;");
|
|
354
|
+
expect(await FileSys.fileExists(path.join(wsRoot, result.libPatch?.path ?? "missing"))).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("adopts library edits only when asked", async () => {
|
|
358
|
+
const { wsRoot, bareRoot, workRoot, subspace } = await makeMirror("adopt-served", "adopt-private", "adopt-kit");
|
|
359
|
+
await subspace.push("develop", { verify: false });
|
|
360
|
+
|
|
361
|
+
const subspaceRoot = await cloneSubspace(workRoot, bareRoot, "customer");
|
|
362
|
+
await write(path.join(subspaceRoot, "libs/adopt-kit/common/helper.ts"), "export const helper = 99;\n");
|
|
363
|
+
await git(subspaceRoot, ["add", "-A"]);
|
|
364
|
+
await git(subspaceRoot, ["commit", "--quiet", "-m", "lib tweak"]);
|
|
365
|
+
await git(subspaceRoot, ["push", "--quiet", "origin", "develop"]);
|
|
366
|
+
|
|
367
|
+
const result = await subspace.pull("develop", { adoptLibs: true });
|
|
368
|
+
|
|
369
|
+
expect(result.applied).toEqual(["libs/adopt-kit/common/helper.ts"]);
|
|
370
|
+
expect(result.libPatch).toBeNull();
|
|
371
|
+
expect(await FileSys.readText(path.join(wsRoot, "libs/adopt-kit/common/helper.ts"))).toContain("helper = 99;");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("is a no-op when the subspace has no commits of its own", async () => {
|
|
375
|
+
const { subspace } = await makeMirror("quiet-served", "quiet-private", "quiet-kit");
|
|
376
|
+
await subspace.push("develop", { verify: false });
|
|
377
|
+
|
|
378
|
+
const result = await subspace.pull("develop");
|
|
379
|
+
expect(result.incoming).toEqual([]);
|
|
380
|
+
expect(result.applied).toEqual([]);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("SubspaceConfig", () => {
|
|
385
|
+
test("refuses one app claimed by two subspaces", () => {
|
|
386
|
+
expect(
|
|
387
|
+
() =>
|
|
388
|
+
new SubspaceConfig({
|
|
389
|
+
subspaces: [
|
|
390
|
+
{ name: "a", repo: "a.git", apps: ["shared"] },
|
|
391
|
+
{ name: "b", repo: "b.git", apps: ["shared"] },
|
|
392
|
+
],
|
|
393
|
+
}),
|
|
394
|
+
).toThrow(/claimed by both/);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("refuses a subspace with no apps and a duplicate name", () => {
|
|
398
|
+
expect(() => new SubspaceConfig({ subspaces: [{ name: "a", repo: "a.git", apps: [] }] })).toThrow(
|
|
399
|
+
/declares no apps/,
|
|
400
|
+
);
|
|
401
|
+
expect(
|
|
402
|
+
() =>
|
|
403
|
+
new SubspaceConfig({
|
|
404
|
+
subspaces: [
|
|
405
|
+
{ name: "a", repo: "a.git", apps: ["one"] },
|
|
406
|
+
{ name: "a", repo: "b.git", apps: ["two"] },
|
|
407
|
+
],
|
|
408
|
+
}),
|
|
409
|
+
).toThrow(/duplicate subspace/);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("defaults the pushable branches and rejects anything else", () => {
|
|
413
|
+
const config = new SubspaceConfig({ subspaces: [{ name: "a", repo: "a.git", apps: ["one"] }] });
|
|
414
|
+
expect(config.pushableBranches).toEqual(SubspaceConfig.defaultPushableBranches);
|
|
415
|
+
expect(() => config.assertPushable("feature/x")).toThrow(/not pushable/);
|
|
416
|
+
expect(() => config.assertPushable("develop")).not.toThrow();
|
|
417
|
+
});
|
|
418
|
+
});
|