@akanjs/devkit 3.0.0-alpha.1 → 3.0.0-alpha.100
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 +34 -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 +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 +70 -42
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +37 -2
- package/applicationBuildRunner.ts +53 -24
- 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.test.ts +8 -0
- package/capacitorApp.ts +122 -18
- 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 +137 -5
- package/executors.ts +116 -26
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/allRoutesBuilder.ts +27 -8
- 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/cssCandidateCache.test.ts +38 -0
- package/frontendBuild/cssCandidateCache.ts +15 -3
- 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/precompressArtifacts.ts +35 -7
- package/frontendBuild/routeClientBuilder.ts +26 -7
- 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 +129 -0
- package/frontendBuild/watchRootResolver.ts +66 -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 +33 -0
- 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-return-in-store-action.grit +35 -0
- 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 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +130 -0
- package/qualityScanner.ts +91 -62
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- package/routeSourceValidator.test.ts +41 -0
- package/routeSourceValidator.ts +2 -44
- package/scanInfo.ts +32 -67
- 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 +78 -0
- package/workspaceLayout.ts +106 -0
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.100",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"directory": "pkgs/@akanjs/devkit"
|
|
15
15
|
},
|
|
16
16
|
"engines": {
|
|
17
|
-
"bun": ">=1.
|
|
17
|
+
"bun": ">=1.4.0"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"default": "./index.ts"
|
|
24
24
|
},
|
|
25
25
|
"./package.json": "./package.json",
|
|
26
|
+
"./biome.base.json": "./biome.base.json",
|
|
26
27
|
"./akanApp": "./akanApp/index.ts",
|
|
27
28
|
"./akanConfig": "./akanConfig/index.ts",
|
|
28
29
|
"./artifact": "./artifact/index.ts",
|
|
@@ -32,7 +33,6 @@
|
|
|
32
33
|
"./incrementalBuilder": "./incrementalBuilder/index.ts",
|
|
33
34
|
"./mobile": "./mobile/index.ts",
|
|
34
35
|
"./transforms": "./transforms/index.ts",
|
|
35
|
-
"./ui": "./ui/index.ts",
|
|
36
36
|
"./workflow": "./workflow/index.ts",
|
|
37
37
|
"./*.ts": "./*.ts",
|
|
38
38
|
"./*": "./*.ts"
|
|
@@ -44,15 +44,13 @@
|
|
|
44
44
|
"@langchain/openai": "^1.4.6",
|
|
45
45
|
"@tailwindcss/node": "^4.3.0",
|
|
46
46
|
"@trapezedev/project": "^7.1.4",
|
|
47
|
-
"akanjs": "3.0.0-alpha.
|
|
47
|
+
"akanjs": "3.0.0-alpha.100",
|
|
48
48
|
"chalk": "^5.6.2",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"dayjs": "^1.11.20",
|
|
51
51
|
"fontaine": "^0.8.0",
|
|
52
52
|
"fonteditor-core": "^2.6.3",
|
|
53
53
|
"ignore": "^7.0.5",
|
|
54
|
-
"ink": "^6.8.0",
|
|
55
|
-
"js-yaml": "^4.1.1",
|
|
56
54
|
"ora": "^9.4.0",
|
|
57
55
|
"ssh2": "^1.17.0",
|
|
58
56
|
"subset-font": "^2.5.0",
|
|
@@ -62,7 +60,7 @@
|
|
|
62
60
|
},
|
|
63
61
|
"peerDependencies": {
|
|
64
62
|
"@capacitor/cli": "^8.3.4",
|
|
65
|
-
"react": "19.2.
|
|
63
|
+
"react": "19.2.7"
|
|
66
64
|
},
|
|
67
65
|
"peerDependenciesMeta": {
|
|
68
66
|
"@capacitor/cli": {
|
|
@@ -76,4 +74,4 @@
|
|
|
76
74
|
"bun": {
|
|
77
75
|
"platform": "bun"
|
|
78
76
|
}
|
|
79
|
-
}
|
|
77
|
+
}
|
package/prompter.ts
CHANGED
|
@@ -24,7 +24,6 @@ export class Prompter {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
static async selectGuideline() {
|
|
27
|
-
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
28
27
|
const guideNames = await Prompter.listGuidelines();
|
|
29
28
|
return await select({
|
|
30
29
|
message: "Select a guideline",
|
|
@@ -41,11 +40,17 @@ export class Prompter {
|
|
|
41
40
|
const guideJson = await fsPromise.readFile(filePath, "utf-8");
|
|
42
41
|
return JSON.parse(guideJson) as GuideGenerateJson;
|
|
43
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The name is checked against the directory listing rather than interpolated straight into a path:
|
|
45
|
+
* it arrives from an MCP tool argument, and an unknown one has to name the valid set instead of
|
|
46
|
+
* surfacing an ENOENT that carries the host's own paths.
|
|
47
|
+
*/
|
|
44
48
|
static async getInstruction(guideName: string): Promise<string> {
|
|
49
|
+
const guideNames = await Prompter.listGuidelines();
|
|
50
|
+
if (!guideNames.includes(guideName))
|
|
51
|
+
throw new Error(`Unknown guideline "${guideName}". Available: ${guideNames.join(", ")}`);
|
|
45
52
|
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
46
|
-
|
|
47
|
-
const content = await fsPromise.readFile(filePath, "utf-8");
|
|
48
|
-
return content;
|
|
53
|
+
return await fsPromise.readFile(`${guidelineRoot}/${guideName}/${guideName}.instruction.md`, "utf-8");
|
|
49
54
|
}
|
|
50
55
|
static async getUpdateRequest(guideName: string) {
|
|
51
56
|
return await input({
|
package/qualityScanner.test.ts
CHANGED
|
@@ -183,3 +183,133 @@ describe("AkanQualityScanner ssr rules", () => {
|
|
|
183
183
|
expect(ssrBalance[2]).toMatchObject({ scope: "workspace", serverMass: 3, clientMass: 1 });
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
const signalOf = (entries: string) =>
|
|
188
|
+
[
|
|
189
|
+
`import { endpoint, slice } from "akanjs/signal";`,
|
|
190
|
+
`export class PostSlice extends slice(srv.post, { guards: {}, mcp: { get: true } }, (init) => ({`,
|
|
191
|
+
entries,
|
|
192
|
+
`})) {}`,
|
|
193
|
+
"",
|
|
194
|
+
].join("\n");
|
|
195
|
+
|
|
196
|
+
describe("AkanQualityScanner layout rules", () => {
|
|
197
|
+
test("flags an unknown app root file but not a facet entrypoint", async () => {
|
|
198
|
+
const root = await makeWorkspace({
|
|
199
|
+
"apps/demo/client.ts": "export const client = 1;\n",
|
|
200
|
+
"apps/demo/helper.ts": "export const helper = 1;\n",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.app-root-file");
|
|
204
|
+
|
|
205
|
+
expect(warnings).toHaveLength(1);
|
|
206
|
+
expect(warnings[0]?.file).toBe("apps/demo/helper.ts");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("flags an unknown lib root file but not a facet entrypoint", async () => {
|
|
210
|
+
const root = await makeWorkspace({
|
|
211
|
+
"libs/demo/client.ts": "export const client = 1;\n",
|
|
212
|
+
"libs/demo/helper.ts": "export const helper = 1;\n",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.lib-root-file");
|
|
216
|
+
|
|
217
|
+
expect(warnings).toHaveLength(1);
|
|
218
|
+
expect(warnings[0]?.file).toBe("libs/demo/helper.ts");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("flags a root folder no facet owns, on both sides", async () => {
|
|
222
|
+
const root = await makeWorkspace({
|
|
223
|
+
"apps/demo/base/helper.ts": "export const helper = 1;\n",
|
|
224
|
+
"apps/demo/common/helper.ts": "export const helper = 1;\n",
|
|
225
|
+
"libs/demo/base/helper.ts": "export const helper = 1;\n",
|
|
226
|
+
"libs/demo/common/helper.ts": "export const helper = 1;\n",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const result = await new AkanQualityScanner().scan(root);
|
|
230
|
+
|
|
231
|
+
expect(rulesOf(result, "akan.layout.app-root-folder").map((warning) => warning.file)).toEqual([
|
|
232
|
+
"apps/demo/base/helper.ts",
|
|
233
|
+
]);
|
|
234
|
+
expect(rulesOf(result, "akan.layout.lib-root-folder").map((warning) => warning.file)).toEqual([
|
|
235
|
+
"libs/demo/base/helper.ts",
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("keeps a root signal test out of the lib facet rule", async () => {
|
|
240
|
+
const root = await makeWorkspace({
|
|
241
|
+
"libs/demo/lib/cnst.ts": "export const cnst = 1;\n",
|
|
242
|
+
"libs/demo/lib/user.signal.test.ts": "export const test = 1;\n",
|
|
243
|
+
"libs/demo/lib/helper.ts": "export const helper = 1;\n",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.lib-facet-file");
|
|
247
|
+
|
|
248
|
+
expect(warnings).toHaveLength(1);
|
|
249
|
+
expect(warnings[0]?.file).toBe("libs/demo/lib/helper.ts");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("bang comments in browser-reachable files", () => {
|
|
254
|
+
const bangWarningsIn = async (files: Record<string, string>) => {
|
|
255
|
+
const result = await new AkanQualityScanner().scan(await makeWorkspace(files));
|
|
256
|
+
return rulesOf(result, "akan.file.bang-comment-in-client");
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
test("flags the first and last line, which the grit rule cannot reach", async () => {
|
|
260
|
+
const warnings = await bangWarningsIn({
|
|
261
|
+
"apps/demo/ui/First.tsx": "//! FIXME: broken\nexport const First = () => null;\n",
|
|
262
|
+
"apps/demo/ui/Last.tsx": "export const Last = () => null;\n//! left behind\n",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(warnings.map((warning) => `${warning.file}:${warning.line}`).sort()).toEqual([
|
|
266
|
+
"apps/demo/ui/First.tsx:1",
|
|
267
|
+
"apps/demo/ui/Last.tsx:2",
|
|
268
|
+
]);
|
|
269
|
+
expect(warnings[0]?.fix).toContain("// FIXME:");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("flags the block form and a marker trailing code", async () => {
|
|
273
|
+
const warnings = await bangWarningsIn({
|
|
274
|
+
"apps/demo/lib/task/Task.Zone.tsx": ["/*! keep me */", "export const Zone = () => null; //! and me", ""].join(
|
|
275
|
+
"\n",
|
|
276
|
+
),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(warnings.map((warning) => warning.line)).toEqual([1, 2]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("ignores a double slash inside a string literal", async () => {
|
|
283
|
+
expect(
|
|
284
|
+
await bangWarningsIn({ "apps/demo/ui/Url.tsx": 'export const url = "https://host//!path";\n' }),
|
|
285
|
+
).toHaveLength(0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("stays out of server files and tests", async () => {
|
|
289
|
+
const body = "//! server-only marker\nexport const value = 1;\n";
|
|
290
|
+
expect(
|
|
291
|
+
await bangWarningsIn({
|
|
292
|
+
"apps/demo/srvkit/storage.ts": body,
|
|
293
|
+
"apps/demo/lib/task/task.service.ts": body,
|
|
294
|
+
"apps/demo/ui/Card.test.tsx": body,
|
|
295
|
+
"apps/demo/lib/task/task.signal.spec.ts": body,
|
|
296
|
+
}),
|
|
297
|
+
).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("covers every path role the grit rule is scoped to", async () => {
|
|
301
|
+
const body = "//! marker\nexport const value = 1;\n";
|
|
302
|
+
const files = [
|
|
303
|
+
"apps/demo/ui/Card.tsx",
|
|
304
|
+
"apps/demo/webkit/useThing.tsx",
|
|
305
|
+
"apps/demo/common/format.ts",
|
|
306
|
+
"apps/demo/page/task/_index.tsx",
|
|
307
|
+
"apps/demo/lib/task/task.constant.ts",
|
|
308
|
+
"apps/demo/lib/task/task.store.ts",
|
|
309
|
+
"apps/demo/lib/task/Task.Unit.tsx",
|
|
310
|
+
];
|
|
311
|
+
const warnings = await bangWarningsIn(Object.fromEntries(files.map((file) => [file, body])));
|
|
312
|
+
|
|
313
|
+
expect(warnings.map((warning) => warning.file).sort()).toEqual([...files].sort());
|
|
314
|
+
});
|
|
315
|
+
});
|
package/qualityScanner.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
4
5
|
import ignore from "ignore";
|
|
5
6
|
import ts from "typescript";
|
|
6
7
|
import { AbstractDoc } from "./abstractDoc";
|
|
8
|
+
import { FormSetterScanner } from "./formSetterScanner";
|
|
7
9
|
import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
10
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
8
11
|
|
|
9
12
|
type QualitySeverity = "warning";
|
|
10
|
-
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr";
|
|
13
|
+
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr" | "agent";
|
|
11
14
|
|
|
12
15
|
export interface QualityWarning {
|
|
13
16
|
rule: string;
|
|
@@ -86,33 +89,11 @@ const SUGGESTED_RULES = [
|
|
|
86
89
|
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
87
90
|
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
88
91
|
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
89
|
-
"
|
|
92
|
+
"Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a _postSet<Field> hook on the store.",
|
|
93
|
+
"Move shared app and lib utilities to common/ instead of creating apps/*/base or libs/*/base.",
|
|
90
94
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline.",
|
|
91
95
|
];
|
|
92
96
|
|
|
93
|
-
const APP_ROOT_FILES = new Set([
|
|
94
|
-
"akan.app.json",
|
|
95
|
-
"akan.config.ts",
|
|
96
|
-
"capacitor.config.ts",
|
|
97
|
-
"client.ts",
|
|
98
|
-
"main.ts",
|
|
99
|
-
"package.json",
|
|
100
|
-
"server.ts",
|
|
101
|
-
"tsconfig.json",
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
const LIB_ROOT_FILES = new Set([
|
|
105
|
-
"cnst.ts",
|
|
106
|
-
"db.ts",
|
|
107
|
-
"dict.ts",
|
|
108
|
-
"option.ts",
|
|
109
|
-
"sig.ts",
|
|
110
|
-
"srv.ts",
|
|
111
|
-
"st.ts",
|
|
112
|
-
"useClient.ts",
|
|
113
|
-
"useServer.ts",
|
|
114
|
-
]);
|
|
115
|
-
|
|
116
97
|
const CONVENTION_SUFFIXES = [
|
|
117
98
|
".constant.ts",
|
|
118
99
|
".dictionary.ts",
|
|
@@ -122,24 +103,6 @@ const CONVENTION_SUFFIXES = [
|
|
|
122
103
|
".store.ts",
|
|
123
104
|
] as const;
|
|
124
105
|
|
|
125
|
-
// Non-PascalCase exports the framework recognizes on page/layout route modules (see PageModule/LayoutModule
|
|
126
|
-
// in pkgs/akanjs/client/csrTypes.ts). PascalCase route exports (Loading, NotFound, Error) pass the component
|
|
127
|
-
// check, and the `default` export is handled separately.
|
|
128
|
-
const PAGE_RESERVED_EXPORTS = new Set([
|
|
129
|
-
"pageConfig",
|
|
130
|
-
"head",
|
|
131
|
-
"metadata",
|
|
132
|
-
"generateHead",
|
|
133
|
-
"generateMetadata",
|
|
134
|
-
"fonts",
|
|
135
|
-
"manifest",
|
|
136
|
-
"theme",
|
|
137
|
-
"reconnect",
|
|
138
|
-
"wsConnect",
|
|
139
|
-
"layoutStyle",
|
|
140
|
-
"gaTrackingId",
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
106
|
// How to remediate each rule, keyed by rule id. Surfaced as a `fix:` line per warning (text + JSON output)
|
|
144
107
|
// so the scan result tells the reader what to do, not just what is wrong.
|
|
145
108
|
const RULE_FIXES: Record<string, string> = {
|
|
@@ -154,6 +117,8 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
154
117
|
"Run `akan compact <app-or-lib>` to rewrite the abstract with the AI editor, keeping only the invariants and workflows the source files cannot show.",
|
|
155
118
|
"akan.file.placeholder-export":
|
|
156
119
|
"Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
120
|
+
"akan.file.bang-comment-in-client":
|
|
121
|
+
"Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
|
|
157
122
|
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
158
123
|
"akan.file.global-declaration":
|
|
159
124
|
"Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
|
|
@@ -161,14 +126,22 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
161
126
|
"Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
162
127
|
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
163
128
|
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
129
|
+
"akan.agent.unpublished-form-setter":
|
|
130
|
+
"Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a _postSet<Field> hook on the store, keeping the generated setter on the control.",
|
|
164
131
|
"akan.file.component-internal-declaration":
|
|
165
132
|
"Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
166
133
|
"akan.file.component-export":
|
|
167
134
|
"Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix — only PascalCase components and their `<Component>Props` interface belong here.",
|
|
168
135
|
"akan.layout.app-root-file":
|
|
169
136
|
"Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
137
|
+
"akan.layout.app-root-folder":
|
|
138
|
+
"Move the folder's contents into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit) and delete it.",
|
|
170
139
|
"akan.layout.lib-root-file":
|
|
171
|
-
"Move the file into a
|
|
140
|
+
"Move the file into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit).",
|
|
141
|
+
"akan.layout.lib-root-folder":
|
|
142
|
+
"Move the folder's contents into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit) and delete it.",
|
|
143
|
+
"akan.layout.lib-facet-file":
|
|
144
|
+
"Move the file into a domain module folder under lib/; keep the lib facet root limited to generated support facets.",
|
|
172
145
|
"akan.layout.module-ui-file":
|
|
173
146
|
"Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
|
|
174
147
|
"akan.ssr.unnecessary-use-client":
|
|
@@ -212,6 +185,7 @@ export class AkanQualityScanner {
|
|
|
212
185
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
213
186
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
214
187
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
188
|
+
...new FormSetterScanner().scan(sourceFiles),
|
|
215
189
|
...ssr.warnings,
|
|
216
190
|
];
|
|
217
191
|
|
|
@@ -342,6 +316,7 @@ export class AkanQualityScanner {
|
|
|
342
316
|
});
|
|
343
317
|
}
|
|
344
318
|
|
|
319
|
+
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
345
320
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
346
321
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
347
322
|
warnings.push(...getGlobalMutationWarnings(sourceFile));
|
|
@@ -447,26 +422,31 @@ export class AkanQualityScanner {
|
|
|
447
422
|
}
|
|
448
423
|
|
|
449
424
|
#scanLayoutQuality(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
450
|
-
const segments = sourceFile.file.split("/");
|
|
451
425
|
const warnings: QualityWarning[] = [];
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
426
|
+
const rootEntry = getSysRootEntry(sourceFile.file);
|
|
427
|
+
if (rootEntry) {
|
|
428
|
+
const { type, name, isDir } = rootEntry;
|
|
429
|
+
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
430
|
+
const kind = isDir ? "folder" : "file";
|
|
431
|
+
if (!allowed) {
|
|
432
|
+
warnings.push({
|
|
433
|
+
rule: `akan.layout.${type}-root-${kind}`,
|
|
434
|
+
scope: "layout",
|
|
435
|
+
severity: "warning",
|
|
436
|
+
file: sourceFile.file,
|
|
437
|
+
message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
460
440
|
}
|
|
461
441
|
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
442
|
+
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
443
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
|
|
464
444
|
warnings.push({
|
|
465
|
-
rule: "akan.layout.lib-
|
|
445
|
+
rule: "akan.layout.lib-facet-file",
|
|
466
446
|
scope: "layout",
|
|
467
447
|
severity: "warning",
|
|
468
448
|
file: sourceFile.file,
|
|
469
|
-
message: `Unexpected lib root file "${
|
|
449
|
+
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
|
|
470
450
|
});
|
|
471
451
|
}
|
|
472
452
|
|
|
@@ -619,9 +599,10 @@ function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement
|
|
|
619
599
|
|
|
620
600
|
function getExportedClassNames(sourceFile: ts.SourceFile) {
|
|
621
601
|
return sourceFile.statements
|
|
622
|
-
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement)
|
|
602
|
+
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement))
|
|
623
603
|
.filter((statement) => isExported(statement))
|
|
624
|
-
.map((statement) => statement.name
|
|
604
|
+
.map((statement) => statement.name?.text)
|
|
605
|
+
.filter((name): name is string => !!name);
|
|
625
606
|
}
|
|
626
607
|
|
|
627
608
|
function isComponentDeclarationFile(file: string) {
|
|
@@ -691,7 +672,7 @@ function isRestrictedInternalKind(kind: ComponentFileDeclaration["kind"]) {
|
|
|
691
672
|
|
|
692
673
|
function isAllowedComponentExport(declaration: ComponentFileDeclaration, isPage: boolean) {
|
|
693
674
|
if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name)) return true;
|
|
694
|
-
return isPage &&
|
|
675
|
+
return isPage && RESERVED_ROUTE_CONFIG_EXPORTS.has(declaration.name);
|
|
695
676
|
}
|
|
696
677
|
|
|
697
678
|
function isPascalCaseName(name: string) {
|
|
@@ -701,7 +682,43 @@ function isPascalCaseName(name: string) {
|
|
|
701
682
|
|
|
702
683
|
function isDefaultExportStatement(statement: ts.Statement) {
|
|
703
684
|
if (ts.isExportAssignment(statement)) return !statement.isExportEquals;
|
|
704
|
-
|
|
685
|
+
if (!ts.canHaveModifiers(statement)) return false;
|
|
686
|
+
return !!ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Bun's bundler keeps `//!` and `/*!` through minification (it reads them as the `@license` class), so a
|
|
691
|
+
* bang marker in browser-reachable code ships verbatim to every visitor. `no-bang-comment-in-client.grit`
|
|
692
|
+
* is the build gate; this exists because that rule cannot see the whole file.
|
|
693
|
+
*
|
|
694
|
+
* A comment is trivia, which Biome's GritQL exposes to no node pattern, so the rule matches through
|
|
695
|
+
* `file($name, $body)` — and `$body` is the module's *token* span. A marker above the first statement or
|
|
696
|
+
* below the last one sits in leading or trailing trivia and is unreachable from GritQL at all. Reading the
|
|
697
|
+
* raw text here covers both, and re-reporting the markers grit already catches costs nothing: the two run
|
|
698
|
+
* from different commands, and a file with one is failing lint either way.
|
|
699
|
+
*/
|
|
700
|
+
function getBangCommentWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
701
|
+
if (!isClientReachableFile(sourceFile.file)) return [];
|
|
702
|
+
// Anchored to line start or to whitespace after code, so a literal like `"https://host//!path"` is not a hit.
|
|
703
|
+
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => ({
|
|
704
|
+
rule: "akan.file.bang-comment-in-client",
|
|
705
|
+
scope: "file" as const,
|
|
706
|
+
severity: "warning" as const,
|
|
707
|
+
file: sourceFile.file,
|
|
708
|
+
line,
|
|
709
|
+
message: "A `//!` or `/*!` marker survives minification and ships to the browser.",
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`. */
|
|
714
|
+
function isClientReachableFile(file: string) {
|
|
715
|
+
if (/\.(test|spec)\.tsx?$/.test(file)) return false;
|
|
716
|
+
if (/\.(constant|store)\.ts$/.test(file)) return true;
|
|
717
|
+
if (/\.(Template|Unit|Util|View|Zone)\.tsx$/.test(file)) return true;
|
|
718
|
+
const segments = file.split("/");
|
|
719
|
+
if (segments.includes("ui") || segments.includes("webkit")) return /\.tsx?$/.test(file);
|
|
720
|
+
if (segments.includes("common")) return file.endsWith(".ts");
|
|
721
|
+
return segments.includes("page") && file.endsWith(".tsx");
|
|
705
722
|
}
|
|
706
723
|
|
|
707
724
|
function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
@@ -860,7 +877,19 @@ function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number],
|
|
|
860
877
|
return `${modelName}Store`;
|
|
861
878
|
}
|
|
862
879
|
|
|
863
|
-
|
|
880
|
+
const sysRootTypes = { apps: "app", libs: "lib" } as const;
|
|
881
|
+
|
|
882
|
+
function getSysRootEntry(file: string) {
|
|
883
|
+
const segments = file.split("/");
|
|
884
|
+
const root = segments[0];
|
|
885
|
+
const name = segments[2];
|
|
886
|
+
if (!root || !name || segments.length < 3) return null;
|
|
887
|
+
const type = sysRootTypes[root as keyof typeof sysRootTypes];
|
|
888
|
+
if (!type) return null;
|
|
889
|
+
return { type, name, isDir: segments.length > 3 };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function getLibFacetRootFile(file: string) {
|
|
864
893
|
const segments = file.split("/");
|
|
865
894
|
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
866
895
|
return segments[3];
|
package/recipeScanner.ts
CHANGED
|
@@ -118,7 +118,10 @@ const extractVariants = (config: ts.ObjectLiteralExpression) => {
|
|
|
118
118
|
if (!ts.isPropertyAssignment(variant) || !isNamed(variant.name)) continue;
|
|
119
119
|
if (!ts.isObjectLiteralExpression(variant.initializer)) continue;
|
|
120
120
|
variants[propName(variant.name)] = variant.initializer.properties
|
|
121
|
-
.filter(
|
|
121
|
+
.filter(
|
|
122
|
+
(option): option is ts.PropertyAssignment & { name: ts.Identifier | ts.StringLiteral } =>
|
|
123
|
+
ts.isPropertyAssignment(option) && isNamed(option.name),
|
|
124
|
+
)
|
|
122
125
|
.map((option) => propName(option.name));
|
|
123
126
|
}
|
|
124
127
|
} else if (key === "defaultVariants" && ts.isObjectLiteralExpression(property.initializer)) {
|
package/repoIdentity.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Resolved once per root: every CLI command builds a WorkspaceExecutor, and this would otherwise fork git on each.
|
|
5
|
+
const resolved = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
const readRemoteName = (workspaceRoot: string): string | null => {
|
|
8
|
+
try {
|
|
9
|
+
const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
10
|
+
cwd: workspaceRoot,
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
}).trim();
|
|
14
|
+
// Both remote spellings end in the repository: git@host:owner/name.git and https://host/owner/name.git.
|
|
15
|
+
return (
|
|
16
|
+
url
|
|
17
|
+
.replace(/\.git$/, "")
|
|
18
|
+
.split(/[/:]/)
|
|
19
|
+
.pop() || null
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
// No git, no origin, or no git binary — a fresh `akan workspace` before its first commit lands here.
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The repository's own name.
|
|
29
|
+
*
|
|
30
|
+
* Deriving it from the working directory made every generated file that names the repo — the AGENTS.md title, its
|
|
31
|
+
* `- Repo:` line — depend on what each person happened to call the folder they cloned into, so one commit rendered
|
|
32
|
+
* a different guide per developer and the diff never settled. The origin remote is the one identity every clone
|
|
33
|
+
* shares. `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes,
|
|
34
|
+
* cache keys, secret paths) which a monorepo hosting several products legitimately points somewhere else.
|
|
35
|
+
*/
|
|
36
|
+
export const resolveRepoName = (workspaceRoot: string): string => {
|
|
37
|
+
const cached = resolved.get(workspaceRoot);
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
const repoName = readRemoteName(workspaceRoot) ?? path.basename(workspaceRoot);
|
|
40
|
+
resolved.set(workspaceRoot, repoName);
|
|
41
|
+
return repoName;
|
|
42
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { RouteSourceValidator } from "./routeSourceValidator";
|
|
4
|
+
|
|
5
|
+
const validate = (source: string, kind: "page" | "layout", rootLayout = false) =>
|
|
6
|
+
RouteSourceValidator.validateRouteSourceExports(source, `page/_${kind}.tsx`, kind, { rootLayout });
|
|
7
|
+
|
|
8
|
+
describe("RouteSourceValidator", () => {
|
|
9
|
+
test("accepts every root-layout config export the route tree honors", () => {
|
|
10
|
+
const source = [
|
|
11
|
+
"export default function Layout() { return null; }",
|
|
12
|
+
"export const fonts = [];",
|
|
13
|
+
"export const manifest = {};",
|
|
14
|
+
'export const theme = "dark";',
|
|
15
|
+
"export const reconnect = true;",
|
|
16
|
+
"export const wsConnect = true;",
|
|
17
|
+
"export const layoutStyle = {};",
|
|
18
|
+
'export const gaTrackingId = "G-1";',
|
|
19
|
+
].join("\n");
|
|
20
|
+
|
|
21
|
+
expect(() => validate(source, "layout", true)).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("rejects root-layout-only exports on a nested layout and on a page", () => {
|
|
25
|
+
const source = ["export default function Layout() { return null; }", "export const wsConnect = true;"].join("\n");
|
|
26
|
+
|
|
27
|
+
expect(() => validate(source, "layout")).toThrow('unsupported export "wsConnect"');
|
|
28
|
+
expect(() => validate(source, "page")).toThrow('unsupported export "wsConnect"');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("reads devOnly off pageConfig without evaluating the module", () => {
|
|
32
|
+
const source = [
|
|
33
|
+
"export default function Page() { return null; }",
|
|
34
|
+
"export const pageConfig = { devOnly: true };",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
expect(RouteSourceValidator.validateRouteSourceExports(source, "page/_index.tsx", "page")).toEqual({
|
|
38
|
+
devOnly: true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
package/routeSourceValidator.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRouteExports } from "akanjs/common";
|
|
1
2
|
import ts from "typescript";
|
|
2
3
|
|
|
3
4
|
/** What the build needs out of a route module without evaluating it. */
|
|
@@ -16,44 +17,6 @@ export interface RouteSourceInfo {
|
|
|
16
17
|
* validate a route source stay lean.
|
|
17
18
|
*/
|
|
18
19
|
export class RouteSourceValidator {
|
|
19
|
-
static readonly #pageExports = new Set([
|
|
20
|
-
"default",
|
|
21
|
-
"pageConfig",
|
|
22
|
-
"head",
|
|
23
|
-
"metadata",
|
|
24
|
-
"generateHead",
|
|
25
|
-
"generateMetadata",
|
|
26
|
-
"Loading",
|
|
27
|
-
]);
|
|
28
|
-
static readonly #rootLayoutExports = new Set([
|
|
29
|
-
"default",
|
|
30
|
-
"pageConfig",
|
|
31
|
-
"head",
|
|
32
|
-
"metadata",
|
|
33
|
-
"generateHead",
|
|
34
|
-
"generateMetadata",
|
|
35
|
-
"fonts",
|
|
36
|
-
"manifest",
|
|
37
|
-
"theme",
|
|
38
|
-
"reconnect",
|
|
39
|
-
"layoutStyle",
|
|
40
|
-
"gaTrackingId",
|
|
41
|
-
"Loading",
|
|
42
|
-
"NotFound",
|
|
43
|
-
"Error",
|
|
44
|
-
]);
|
|
45
|
-
static readonly #layoutExports = new Set([
|
|
46
|
-
"default",
|
|
47
|
-
"pageConfig",
|
|
48
|
-
"head",
|
|
49
|
-
"metadata",
|
|
50
|
-
"generateHead",
|
|
51
|
-
"generateMetadata",
|
|
52
|
-
"Loading",
|
|
53
|
-
"NotFound",
|
|
54
|
-
"Error",
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
20
|
static validateRouteSourceExports(
|
|
58
21
|
source: string,
|
|
59
22
|
filePath: string,
|
|
@@ -61,12 +24,7 @@ export class RouteSourceValidator {
|
|
|
61
24
|
options: { rootLayout?: boolean } = {},
|
|
62
25
|
): RouteSourceInfo {
|
|
63
26
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
64
|
-
const allowed =
|
|
65
|
-
kind === "page"
|
|
66
|
-
? RouteSourceValidator.#pageExports
|
|
67
|
-
: options.rootLayout
|
|
68
|
-
? RouteSourceValidator.#rootLayoutExports
|
|
69
|
-
: RouteSourceValidator.#layoutExports;
|
|
27
|
+
const allowed = getRouteExports(kind, { rootLayout: options.rootLayout });
|
|
70
28
|
const exported = new Set<string>();
|
|
71
29
|
const assertExport = (name: string) => {
|
|
72
30
|
if (!allowed.has(name)) {
|