@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.80
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -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 +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +70 -14
- package/akanMcpContract.ts +25 -4
- 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 +340 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +132 -5
- package/executors.ts +89 -14
- package/fileEditor.ts +19 -19
- package/fleetConfig.ts +73 -0
- package/fleetGuard.test.ts +121 -0
- package/fleetGuard.ts +157 -0
- package/fleetSpoke.test.ts +390 -0
- package/fleetSpoke.ts +719 -0
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- 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/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/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
- package/index.ts +0 -5
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +109 -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-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -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 +6 -6
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +151 -0
- package/slicePlanner.ts +157 -0
- package/ssrScanner.test.ts +301 -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/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
- 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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createTsconfigPackageResolver } from "./barrelImportsPlugin";
|
|
6
|
+
|
|
7
|
+
const tempRoots: string[] = [];
|
|
8
|
+
|
|
9
|
+
const makeTempRoot = async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-resolver-"));
|
|
11
|
+
tempRoots.push(root);
|
|
12
|
+
return root;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const write = async (root: string, relPath: string, content = "export const value = 1;\n") => {
|
|
16
|
+
const filePath = path.join(root, relPath);
|
|
17
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
18
|
+
await writeFile(filePath, content);
|
|
19
|
+
return filePath;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resolverFor = async (root: string, paths: Record<string, string[]>) =>
|
|
23
|
+
await createTsconfigPackageResolver({
|
|
24
|
+
workspace: { workspaceRoot: root },
|
|
25
|
+
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
26
|
+
} as never);
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("createTsconfigPackageResolver — exact tsconfig mapping", () => {
|
|
33
|
+
test("resolves a package barrel and keeps the specifier", async () => {
|
|
34
|
+
const root = await makeTempRoot();
|
|
35
|
+
const entryFile = await write(root, "libs/util/index.ts");
|
|
36
|
+
const resolve = await resolverFor(root, { "@libs/util": ["./libs/util/index.ts"] });
|
|
37
|
+
|
|
38
|
+
expect(await resolve("@libs/util")).toEqual({
|
|
39
|
+
pkgName: "@libs/util",
|
|
40
|
+
entryFile,
|
|
41
|
+
pkgDir: path.join(root, "libs/util"),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("collapses a facet specifier to its parent package", async () => {
|
|
46
|
+
// `@libs/util/server` maps to the sibling file `libs/util/server.ts`, not to a package directory, so a
|
|
47
|
+
// leaf rewritten under the raw specifier would become `@libs/util/server/lib/sig` — a path that does
|
|
48
|
+
// not exist. The parent specifier is what the `@libs/*` wildcard can actually resolve.
|
|
49
|
+
const root = await makeTempRoot();
|
|
50
|
+
const entryFile = await write(root, "libs/util/server.ts");
|
|
51
|
+
const resolve = await resolverFor(root, { "@libs/util/server": ["./libs/util/server.ts"] });
|
|
52
|
+
|
|
53
|
+
expect(await resolve("@libs/util/server")).toEqual({
|
|
54
|
+
pkgName: "@libs/util",
|
|
55
|
+
entryFile,
|
|
56
|
+
pkgDir: path.join(root, "libs/util"),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("keeps the specifier when the entry file name does not match the facet", async () => {
|
|
61
|
+
const root = await makeTempRoot();
|
|
62
|
+
const entryFile = await write(root, "libs/util/clientEntry.ts");
|
|
63
|
+
const resolve = await resolverFor(root, { "@libs/util/client": ["./libs/util/clientEntry.ts"] });
|
|
64
|
+
|
|
65
|
+
expect(await resolve("@libs/util/client")).toEqual({
|
|
66
|
+
pkgName: "@libs/util/client",
|
|
67
|
+
entryFile,
|
|
68
|
+
pkgDir: path.join(root, "libs/util"),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns null when the mapped file is missing", async () => {
|
|
73
|
+
const root = await makeTempRoot();
|
|
74
|
+
const resolve = await resolverFor(root, { "@libs/util": ["./libs/util/index.ts"] });
|
|
75
|
+
expect(await resolve("@libs/util")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("returns null for an empty mapping", async () => {
|
|
79
|
+
const root = await makeTempRoot();
|
|
80
|
+
await write(root, "libs/util/index.ts");
|
|
81
|
+
const resolve = await resolverFor(root, { "@libs/util": [""] });
|
|
82
|
+
expect(await resolve("@libs/util")).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("createTsconfigPackageResolver — wildcard tsconfig mapping", () => {
|
|
87
|
+
test("prefers the longer prefix", async () => {
|
|
88
|
+
const root = await makeTempRoot();
|
|
89
|
+
const entryFile = await write(root, "custom/ui/index.ts");
|
|
90
|
+
await write(root, "libs/util/ui/index.ts");
|
|
91
|
+
const resolve = await resolverFor(root, {
|
|
92
|
+
"@libs/*": ["./libs/*"],
|
|
93
|
+
"@libs/util/*": ["./custom/*"],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(await resolve("@libs/util/ui")).toEqual({
|
|
97
|
+
pkgName: "@libs/util/ui",
|
|
98
|
+
entryFile,
|
|
99
|
+
pkgDir: path.join(root, "custom/ui"),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("collapses a sibling facet file to its parent package", async () => {
|
|
104
|
+
const root = await makeTempRoot();
|
|
105
|
+
const entryFile = await write(root, "apps/minimal/client.ts");
|
|
106
|
+
const resolve = await resolverFor(root, { "@apps/*": ["./apps/*"] });
|
|
107
|
+
|
|
108
|
+
expect(await resolve("@apps/minimal/client")).toEqual({
|
|
109
|
+
pkgName: "@apps/minimal",
|
|
110
|
+
entryFile,
|
|
111
|
+
pkgDir: path.join(root, "apps/minimal"),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("resolves a directory barrel and keeps the specifier", async () => {
|
|
116
|
+
const root = await makeTempRoot();
|
|
117
|
+
const entryFile = await write(root, "apps/minimal/index.tsx");
|
|
118
|
+
const resolve = await resolverFor(root, { "@apps/*": ["./apps/*"] });
|
|
119
|
+
|
|
120
|
+
expect(await resolve("@apps/minimal")).toEqual({
|
|
121
|
+
pkgName: "@apps/minimal",
|
|
122
|
+
entryFile,
|
|
123
|
+
pkgDir: path.join(root, "apps/minimal"),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("prefers a sibling facet file over a same-named directory barrel", async () => {
|
|
128
|
+
const root = await makeTempRoot();
|
|
129
|
+
const entryFile = await write(root, "apps/minimal/client.ts");
|
|
130
|
+
await write(root, "apps/minimal/client/index.ts");
|
|
131
|
+
const resolve = await resolverFor(root, { "@apps/*": ["./apps/*"] });
|
|
132
|
+
|
|
133
|
+
expect((await resolve("@apps/minimal/client"))?.entryFile).toBe(entryFile);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("stops at the matched prefix instead of falling through to a shorter one", async () => {
|
|
137
|
+
// Falling through would resolve `@libs/util/ui` against `./libs/*`, silently loading an unrelated
|
|
138
|
+
// directory that happens to sit at the same subpath under a different root.
|
|
139
|
+
const root = await makeTempRoot();
|
|
140
|
+
await write(root, "libs/util/ui/index.ts");
|
|
141
|
+
const resolve = await resolverFor(root, {
|
|
142
|
+
"@libs/*": ["./libs/*"],
|
|
143
|
+
"@libs/util/*": ["./custom/*"],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(await resolve("@libs/util/ui")).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("tries every replacement of a matched prefix", async () => {
|
|
150
|
+
const root = await makeTempRoot();
|
|
151
|
+
const entryFile = await write(root, "second/util/index.ts");
|
|
152
|
+
const resolve = await resolverFor(root, { "@libs/*": ["./first/*", "./second/*"] });
|
|
153
|
+
|
|
154
|
+
expect((await resolve("@libs/util"))?.entryFile).toBe(entryFile);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("createTsconfigPackageResolver — node_modules fallback", () => {
|
|
159
|
+
const writePackageJson = async (root: string, pkgName: string, pkgJson: unknown) =>
|
|
160
|
+
await write(root, `node_modules/${pkgName}/package.json`, JSON.stringify(pkgJson));
|
|
161
|
+
|
|
162
|
+
test("resolves an exports subpath and preserves the file path", async () => {
|
|
163
|
+
const root = await makeTempRoot();
|
|
164
|
+
await writePackageJson(root, "akanjs", { exports: { "./ui": { source: "./ui/index.ts" } } });
|
|
165
|
+
const entryFile = await write(root, "node_modules/akanjs/ui/index.ts");
|
|
166
|
+
const resolve = await resolverFor(root, {});
|
|
167
|
+
|
|
168
|
+
expect(await resolve("akanjs/ui")).toEqual({
|
|
169
|
+
pkgName: "akanjs/ui",
|
|
170
|
+
entryFile,
|
|
171
|
+
pkgDir: path.join(root, "node_modules/akanjs/ui"),
|
|
172
|
+
preserveFilePath: true,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("resolves an exports wildcard subpath", async () => {
|
|
177
|
+
const root = await makeTempRoot();
|
|
178
|
+
await writePackageJson(root, "akanjs", { exports: { "./*": "./dist/*.js" } });
|
|
179
|
+
const entryFile = await write(root, "node_modules/akanjs/dist/signal.js");
|
|
180
|
+
const resolve = await resolverFor(root, {});
|
|
181
|
+
|
|
182
|
+
expect((await resolve("akanjs/signal"))?.entryFile).toBe(entryFile);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("resolves a scoped package subpath", async () => {
|
|
186
|
+
// A scoped name is two path segments, so the package boundary cannot be found by splitting on the
|
|
187
|
+
// first slash. Kept vendor-neutral on purpose: `packageExports.test.ts` reads every
|
|
188
|
+
// `"@akanjs/devkit/<x>"` literal under this package as a subpath the monorepo imports.
|
|
189
|
+
const root = await makeTempRoot();
|
|
190
|
+
await writePackageJson(root, "@vendor/kit", { exports: { "./lint": "./lint/index.ts" } });
|
|
191
|
+
const entryFile = await write(root, "node_modules/@vendor/kit/lint/index.ts");
|
|
192
|
+
const resolve = await resolverFor(root, {});
|
|
193
|
+
|
|
194
|
+
expect((await resolve("@vendor/kit/lint"))?.entryFile).toBe(entryFile);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("falls back to module/main when exports resolution declines the specifier", async () => {
|
|
198
|
+
// `main: "index.js"` has no leading `./`, which the exports resolver rejects — the last fallback
|
|
199
|
+
// resolves it against the package directory, and it is the one path that does not preserve the file.
|
|
200
|
+
const root = await makeTempRoot();
|
|
201
|
+
await writePackageJson(root, "plainpkg", { main: "index.js" });
|
|
202
|
+
const entryFile = await write(root, "node_modules/plainpkg/index.js");
|
|
203
|
+
const resolve = await resolverFor(root, {});
|
|
204
|
+
|
|
205
|
+
expect(await resolve("plainpkg")).toEqual({
|
|
206
|
+
pkgName: "plainpkg",
|
|
207
|
+
entryFile,
|
|
208
|
+
pkgDir: path.join(root, "node_modules/plainpkg"),
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("returns null when the package is not installed", async () => {
|
|
213
|
+
const root = await makeTempRoot();
|
|
214
|
+
expect(await resolverFor(root, {}).then((resolve) => resolve("missingpkg"))).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("returns null when the package.json is unparseable", async () => {
|
|
218
|
+
const root = await makeTempRoot();
|
|
219
|
+
await write(root, "node_modules/brokenpkg/package.json", "{ not json");
|
|
220
|
+
const resolve = await resolverFor(root, {});
|
|
221
|
+
expect(await resolve("brokenpkg")).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("returns null when the resolved entry file is missing", async () => {
|
|
225
|
+
const root = await makeTempRoot();
|
|
226
|
+
await writePackageJson(root, "ghostpkg", { module: "./dist/index.js" });
|
|
227
|
+
const resolve = await resolverFor(root, {});
|
|
228
|
+
expect(await resolve("ghostpkg")).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
package/tsconfig.json
CHANGED
package/typeChecker.ts
CHANGED
package/types.ts
CHANGED
package/ui/ScrollList.tsx
CHANGED
|
@@ -83,14 +83,12 @@ export const ScrollList = ({ list, ...props }: ScrollListProps) => {
|
|
|
83
83
|
<Text backgroundColor="green">scrolling... + {scrollPos}</Text>
|
|
84
84
|
</>
|
|
85
85
|
) : (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
))}
|
|
93
|
-
</>
|
|
86
|
+
renderLogs.map((log, index) => (
|
|
87
|
+
<>
|
|
88
|
+
<Text key={index}>{log}</Text>
|
|
89
|
+
<Newline />
|
|
90
|
+
</>
|
|
91
|
+
))
|
|
94
92
|
)}
|
|
95
93
|
</Text>
|
|
96
94
|
</Box>
|
package/uploadRelease.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Logger, RestClient } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
import { Spinner } from "./spinner";
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ export const uploadRelease = async (
|
|
|
26
26
|
) => {
|
|
27
27
|
const logger = new Logger("uploadRelease");
|
|
28
28
|
const basePath = local ? "http://localhost:8282/backend" : "https://cloud.akanjs.com/backend";
|
|
29
|
-
const httpClient = new
|
|
29
|
+
const httpClient = new RestClient(basePath);
|
|
30
30
|
const buildPath = `${workspaceRoot}/releases/builds/${appName}-release.tar.gz`;
|
|
31
31
|
const appBuildPath = `${workspaceRoot}/releases/builds/${appName}-appBuild.zip`;
|
|
32
32
|
const sourcePath = `${workspaceRoot}/releases/sources/${appName}-source.tar.gz`;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { WorkflowExecutor, workflowStepKey } from "./executor";
|
|
3
|
+
import type {
|
|
4
|
+
PrimitiveChangedFile,
|
|
5
|
+
WorkflowDiagnostic,
|
|
6
|
+
WorkflowPlan,
|
|
7
|
+
WorkflowStep,
|
|
8
|
+
WorkflowStepRegistry,
|
|
9
|
+
WorkflowStepResult,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `WorkflowExecutor` takes its registry by constructor injection and its plan as data, so `apply` runs with
|
|
14
|
+
* no filesystem and no workspace — the `workspace` argument is what gates every post-apply check. These
|
|
15
|
+
* cover the ordering rules that `cli/workflow/workflow.test.ts` can only reach through a real scaffold.
|
|
16
|
+
*/
|
|
17
|
+
const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
|
|
18
|
+
id,
|
|
19
|
+
tool,
|
|
20
|
+
title: id,
|
|
21
|
+
description: id,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Every field spelled out rather than cast through `unknown`: `apply` reads `validation` before the first
|
|
25
|
+
// step runs, so a partial literal fails at runtime instead of at the type level.
|
|
26
|
+
const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []): WorkflowPlan => ({
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
workflow: "add-field",
|
|
29
|
+
mode: "plan",
|
|
30
|
+
inputs: {},
|
|
31
|
+
optionalSurfaces: {},
|
|
32
|
+
steps,
|
|
33
|
+
predictedChanges: [],
|
|
34
|
+
validation: [],
|
|
35
|
+
diagnostics,
|
|
36
|
+
recommendations: [],
|
|
37
|
+
requiresApproval: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A registry that records **which key** each call resolved through, not just that a step ran — the
|
|
42
|
+
* resolution order is the thing under test, and every candidate key would otherwise record the same step id.
|
|
43
|
+
*/
|
|
44
|
+
const changed = (path: string): PrimitiveChangedFile => ({ path, action: "modify", reason: "test" });
|
|
45
|
+
|
|
46
|
+
const recordingRegistry = (results: Record<string, WorkflowStepResult | undefined> = {}) => {
|
|
47
|
+
const calls: string[] = [];
|
|
48
|
+
const registry: WorkflowStepRegistry = {};
|
|
49
|
+
const on = (key: string) => {
|
|
50
|
+
registry[key] = async () => {
|
|
51
|
+
calls.push(key);
|
|
52
|
+
return results[key];
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
return { calls, registry, on };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("WorkflowExecutor.apply", () => {
|
|
59
|
+
test("runs no step when the plan already carries an error diagnostic", async () => {
|
|
60
|
+
const { calls, registry, on } = recordingRegistry();
|
|
61
|
+
on(workflowStepKey("add-field", "one"));
|
|
62
|
+
const plan = planOf([step("one")], [{ severity: "error", code: "plan-invalid", message: "bad inputs" }]);
|
|
63
|
+
|
|
64
|
+
const report = await new WorkflowExecutor(registry).apply(plan);
|
|
65
|
+
|
|
66
|
+
expect(calls).toEqual([]);
|
|
67
|
+
expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("plan-invalid");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a warning in the plan does not stop the steps", async () => {
|
|
71
|
+
const { calls, registry, on } = recordingRegistry();
|
|
72
|
+
on(workflowStepKey("add-field", "one"));
|
|
73
|
+
const plan = planOf([step("one")], [{ severity: "warning", code: "plan-note", message: "heads up" }]);
|
|
74
|
+
|
|
75
|
+
await new WorkflowExecutor(registry).apply(plan);
|
|
76
|
+
|
|
77
|
+
expect(calls).toEqual([workflowStepKey("add-field", "one")]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("resolves a runner by workflow-scoped key first, then tool, then bare step id", async () => {
|
|
81
|
+
const { calls, registry, on } = recordingRegistry();
|
|
82
|
+
// All three keys are registered for the same step, so only the winner records a call.
|
|
83
|
+
on(workflowStepKey("add-field", "scoped"));
|
|
84
|
+
on("tool:scoped");
|
|
85
|
+
on("scoped");
|
|
86
|
+
await new WorkflowExecutor(registry).apply(planOf([step("scoped")]));
|
|
87
|
+
expect(calls).toEqual([workflowStepKey("add-field", "scoped")]);
|
|
88
|
+
|
|
89
|
+
const byTool = recordingRegistry();
|
|
90
|
+
byTool.on("tool:viaTool");
|
|
91
|
+
byTool.on("viaTool");
|
|
92
|
+
await new WorkflowExecutor(byTool.registry).apply(planOf([step("viaTool")]));
|
|
93
|
+
expect(byTool.calls).toEqual(["tool:viaTool"]);
|
|
94
|
+
|
|
95
|
+
const byId = recordingRegistry();
|
|
96
|
+
byId.on("viaId");
|
|
97
|
+
await new WorkflowExecutor(byId.registry).apply(planOf([step("viaId")]));
|
|
98
|
+
expect(byId.calls).toEqual(["viaId"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("an unsupported step is reported and abandons the rest of the plan", async () => {
|
|
102
|
+
const { calls, registry, on } = recordingRegistry();
|
|
103
|
+
on(workflowStepKey("add-field", "first"));
|
|
104
|
+
on(workflowStepKey("add-field", "third"));
|
|
105
|
+
|
|
106
|
+
const report = await new WorkflowExecutor(registry).apply(planOf([step("first"), step("second"), step("third")]));
|
|
107
|
+
|
|
108
|
+
expect(calls).toEqual([workflowStepKey("add-field", "first")]);
|
|
109
|
+
expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("workflow-step-unsupported");
|
|
110
|
+
expect(report.nextActions.map((action) => action.command)).toContain("akan workflow explain add-field");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("accumulates changed files and diagnostics across steps, and stops at the first error", async () => {
|
|
114
|
+
const key = (id: string) => workflowStepKey("add-field", id);
|
|
115
|
+
const { calls, registry, on } = recordingRegistry({
|
|
116
|
+
[key("a")]: { changedFiles: [changed("apps/demo/lib/task/task.constant.ts")] },
|
|
117
|
+
[key("b")]: {
|
|
118
|
+
changedFiles: [changed("apps/demo/lib/task/task.dictionary.ts")],
|
|
119
|
+
diagnostics: [{ severity: "error", code: "step-failed", message: "no such module" }],
|
|
120
|
+
},
|
|
121
|
+
[key("c")]: { changedFiles: [changed("never-reached.ts")] },
|
|
122
|
+
});
|
|
123
|
+
on(key("a"));
|
|
124
|
+
on(key("b"));
|
|
125
|
+
on(key("c"));
|
|
126
|
+
|
|
127
|
+
const report = await new WorkflowExecutor(registry).apply(planOf([step("a"), step("b"), step("c")]));
|
|
128
|
+
|
|
129
|
+
expect(calls).toEqual([key("a"), key("b")]);
|
|
130
|
+
expect(report.changedFiles.map((file) => file.path)).toEqual([
|
|
131
|
+
"apps/demo/lib/task/task.constant.ts",
|
|
132
|
+
"apps/demo/lib/task/task.dictionary.ts",
|
|
133
|
+
]);
|
|
134
|
+
expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("step-failed");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a runner that answers nothing is not an error", async () => {
|
|
138
|
+
const { calls, registry, on } = recordingRegistry();
|
|
139
|
+
on(workflowStepKey("add-field", "silent"));
|
|
140
|
+
const report = await new WorkflowExecutor(registry).apply(planOf([step("silent")]));
|
|
141
|
+
|
|
142
|
+
expect(calls).toEqual([workflowStepKey("add-field", "silent")]);
|
|
143
|
+
expect(report.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]);
|
|
144
|
+
expect(report.changedFiles).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/workspaceLayout.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appRootAllowedDirs,
|
|
4
|
+
appRootAllowedFiles,
|
|
5
|
+
isAllowedLibFacetRootFile,
|
|
6
|
+
isScannedRootEntry,
|
|
7
|
+
libRootAllowedDirs,
|
|
8
|
+
libRootAllowedFiles,
|
|
9
|
+
} from "./workspaceLayout";
|
|
3
10
|
|
|
4
11
|
describe("app root layout allowlist", () => {
|
|
5
12
|
test("admits the scoped agent guides sync writes into every app", () => {
|
|
@@ -19,8 +26,53 @@ describe("app root layout allowlist", () => {
|
|
|
19
26
|
});
|
|
20
27
|
|
|
21
28
|
test("skips dotfile artifacts the sync glob never sees, but keeps .akan", () => {
|
|
22
|
-
expect(
|
|
23
|
-
expect(
|
|
24
|
-
expect(
|
|
29
|
+
expect(isScannedRootEntry("app", ".DS_Store")).toBe(false);
|
|
30
|
+
expect(isScannedRootEntry("app", ".akan")).toBe(true);
|
|
31
|
+
expect(isScannedRootEntry("app", "lib")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("lib root layout allowlist", () => {
|
|
36
|
+
test("admits what the libRoot template and scan write", () => {
|
|
37
|
+
for (const filename of ["AGENTS.md", "akan.config.ts", "akan.lib.json", "client.ts", "index.ts", "server.ts"]) {
|
|
38
|
+
expect(libRootAllowedFiles.has(filename)).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
for (const dirname of ["common", "env", "lib", "page", "plugin", "private", "public", "srvkit", "ui", "webkit"]) {
|
|
41
|
+
expect(libRootAllowedDirs.has(dirname)).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("rejects a lib root entry no facet owns", () => {
|
|
46
|
+
expect(libRootAllowedFiles.has("helper.ts")).toBe(false);
|
|
47
|
+
expect(libRootAllowedDirs.has("base")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("rejects the app-only run and mobile entries", () => {
|
|
51
|
+
for (const filename of ["main.ts", "capacitor.config.ts", "akan.app.json"]) {
|
|
52
|
+
expect(libRootAllowedFiles.has(filename)).toBe(false);
|
|
53
|
+
}
|
|
54
|
+
for (const dirname of [".akan", "android", "ios", "mobile", "script", "secrets"]) {
|
|
55
|
+
expect(libRootAllowedDirs.has(dirname)).toBe(false);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("skips dotfile artifacts in a lib root too", () => {
|
|
60
|
+
expect(isScannedRootEntry("lib", ".gitignore")).toBe(false);
|
|
61
|
+
expect(isScannedRootEntry("lib", ".akan")).toBe(false);
|
|
62
|
+
expect(isScannedRootEntry("lib", "ui")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("lib facet root allowlist", () => {
|
|
67
|
+
test("admits the generated support facets and a root signal test", () => {
|
|
68
|
+
expect(isAllowedLibFacetRootFile("cnst.ts")).toBe(true);
|
|
69
|
+
expect(isAllowedLibFacetRootFile("option.ts")).toBe(true);
|
|
70
|
+
expect(isAllowedLibFacetRootFile("user.signal.test.ts")).toBe(true);
|
|
71
|
+
expect(isAllowedLibFacetRootFile("user.signal.spec.ts")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("rejects a hand-written file beside the barrels", () => {
|
|
75
|
+
expect(isAllowedLibFacetRootFile("helper.ts")).toBe(false);
|
|
76
|
+
expect(isAllowedLibFacetRootFile("user.test.ts")).toBe(false);
|
|
25
77
|
});
|
|
26
78
|
});
|
package/workspaceLayout.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* 같은 규칙을 scanInfo(`akan sync`, hard error) · akanContext(`akan doctor`, diagnostic) ·
|
|
5
5
|
* qualityScanner(`akan quality scan`, warning) 세 곳이 각자 복사해 두면서 실제로 어긋났다
|
|
6
6
|
* (스코프 AGENTS.md/CLAUDE.md 는 sync 만 허용, `plugin` 은 문서에만, `secrets` 는 doctor 만 거부).
|
|
7
|
-
* 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md
|
|
8
|
-
* 의 목록도 같이 갱신한다.
|
|
7
|
+
* 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md 의 목록도 같이 갱신한다.
|
|
9
8
|
*/
|
|
10
9
|
|
|
10
|
+
export type SysType = "app" | "lib";
|
|
11
|
+
|
|
11
12
|
export const appRootAllowedFiles = new Set([
|
|
12
13
|
// 스코프 에이전트 가이드 — scan(write) 이 유지하는 색인 + 마커 밖 hand-written 내용 (agentsIndex.ts)
|
|
13
14
|
"AGENTS.md",
|
|
@@ -42,6 +43,43 @@ export const appRootAllowedDirs = new Set([
|
|
|
42
43
|
"webkit",
|
|
43
44
|
]);
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 라이브러리 루트는 앱 루트에서 실행/모바일 전용 항목(`main.ts`, `capacitor.config.ts`, `.akan`,
|
|
48
|
+
* `android`, `ios`, `mobile`, `script`, `secrets`)을 뺀 집합이다 — 라이브러리는 부팅되지도, 패키징되지도
|
|
49
|
+
* 않는다. `index.ts` 는 lib 에만 생성되고, `README.md` / `tsconfig.spec.json` 은 라이브러리가 패키지로
|
|
50
|
+
* 배포되기 때문에 남는다. `public` / `private` 은 syncAssets 가 앱으로 심링크하므로 라이브러리도 가진다.
|
|
51
|
+
*/
|
|
52
|
+
export const libRootAllowedFiles = new Set([
|
|
53
|
+
"AGENTS.md",
|
|
54
|
+
"CLAUDE.md",
|
|
55
|
+
"README.md",
|
|
56
|
+
"akan.config.ts",
|
|
57
|
+
"akan.lib.json",
|
|
58
|
+
"client.ts",
|
|
59
|
+
"index.ts",
|
|
60
|
+
"package.json",
|
|
61
|
+
"server.ts",
|
|
62
|
+
"tsconfig.json",
|
|
63
|
+
"tsconfig.spec.json",
|
|
64
|
+
"tsconfig.tsbuildinfo",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const libRootAllowedDirs = new Set([
|
|
68
|
+
"common",
|
|
69
|
+
"env",
|
|
70
|
+
"lib",
|
|
71
|
+
"page",
|
|
72
|
+
"plugin",
|
|
73
|
+
"private",
|
|
74
|
+
"public",
|
|
75
|
+
"srvkit",
|
|
76
|
+
"ui",
|
|
77
|
+
"webkit",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export const rootAllowedFiles = { app: appRootAllowedFiles, lib: libRootAllowedFiles } as const;
|
|
81
|
+
export const rootAllowedDirs = { app: appRootAllowedDirs, lib: libRootAllowedDirs } as const;
|
|
82
|
+
|
|
45
83
|
export const libFacetRootAllowedFiles = new Set([
|
|
46
84
|
"cnst.ts",
|
|
47
85
|
"db.ts",
|
|
@@ -54,8 +92,15 @@ export const libFacetRootAllowedFiles = new Set([
|
|
|
54
92
|
"useServer.ts",
|
|
55
93
|
]);
|
|
56
94
|
|
|
95
|
+
/** `<model>.signal.test.ts` — lib 파싯 루트에 놓이는 유일한 비생성 파일 (테스트가 배럴 전체를 부팅한다). */
|
|
96
|
+
const libFacetRootTestPattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(ts|tsx)$/;
|
|
97
|
+
|
|
98
|
+
export const isAllowedLibFacetRootFile = (filename: string) =>
|
|
99
|
+
libFacetRootAllowedFiles.has(filename) || libFacetRootTestPattern.test(filename);
|
|
100
|
+
|
|
57
101
|
/**
|
|
58
|
-
* scanSync 는
|
|
102
|
+
* scanSync 는 루트를 `Bun.Glob("*")` 로 읽어 dotfile 을 아예 보지 못한다. 디렉터리를 직접 읽는
|
|
59
103
|
* doctor 가 그 차이만큼 `.DS_Store` 같은 툴 산출물을 에러로 올리므로 같은 기준으로 걸러낸다.
|
|
60
104
|
*/
|
|
61
|
-
export const
|
|
105
|
+
export const isScannedRootEntry = (type: SysType, name: string) =>
|
|
106
|
+
!name.startsWith(".") || rootAllowedDirs[type].has(name);
|