@akanjs/devkit 3.1.1-rc.9 → 3.1.2-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +105 -0
- package/akanConfig/akanConfig.ts +21 -1
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/akanMcpContract.test.ts +28 -1
- package/akanMcpContract.ts +51 -3
- package/applicationBuildRunner.test.ts +53 -0
- package/applicationBuildRunner.ts +7 -4
- package/artifact/implicitRootLayout.ts +4 -2
- package/biome.base.json +4 -1
- package/cloud/globalConfig.test.ts +34 -0
- package/cloud/globalConfig.ts +2 -2
- package/codeAgent/agent/AkanCodeEnv.ts +26 -0
- package/codeAgent/agent/AkanCodeServices.ts +5 -2
- package/codeAgent/agent/CodeAgent.ts +11 -0
- package/codeAgent/agent/CodeAgentGate.ts +156 -10
- package/codeAgent/agent/CodeAgentProxy.ts +5 -4
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +102 -16
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/WorkspaceTrust.ts +200 -0
- package/codeAgent/agent/akanCodePaths.ts +8 -1
- package/codeAgent/agent/akanSystemPrompt.ts +5 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/agent/codeAgentContract.test.ts +119 -0
- package/codeAgent/agent/codeAgentHosts.ts +30 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +53 -1
- package/codeAgent/agent/codeAgentRpcListener.test.ts +115 -13
- package/codeAgent/agent/stringArg.ts +1 -1
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +43 -0
- package/codeAgent/skills/akan-code/SKILL.md +38 -11
- package/codeAgent/skills/akan-debug/SKILL.md +41 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-err/SKILL.md +2 -1
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +39 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +43 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/McpServerConfig.ts +6 -4
- package/codeAgent/tools/McpSignIn.ts +7 -7
- package/codeAgent/tools/McpTokenStore.ts +63 -16
- package/codeAgent/tools/McpToolPack.ts +8 -1
- package/codeAgent/tools/WebToolPack.ts +50 -6
- package/codeAgent/tools/akanCodePlugins.test.ts +130 -60
- package/codeAgent/tools/mcpOAuth.test.ts +50 -14
- package/codeAgent/tools/mcpServerConfig.test.ts +130 -1
- package/commandDecorators/command.ts +4 -1
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/createTunnel.test.ts +200 -2
- package/createTunnel.ts +80 -6
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/envSeed.test.ts +90 -0
- package/envSeed.ts +68 -0
- package/executors.test.ts +19 -5
- package/executors.ts +59 -4
- package/fileSys.test.ts +30 -2
- package/fileSys.ts +14 -0
- package/formSetterScanner.test.ts +44 -1
- package/formSetterScanner.ts +53 -8
- package/frontendBuild/csrArtifactBuilder.ts +7 -0
- package/frontendBuild/mcpAppViewBuilder.test.ts +63 -0
- package/frontendBuild/mcpAppViewBuilder.ts +7 -1
- package/frontendBuild/optionalPeers.test.ts +132 -0
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/integration/ssrMemoryProbe.ts +6 -0
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/lint/no-unguarded-endpoint.grit +5 -5
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +1 -1
- package/mobile/desktopRelease.ts +3 -1
- package/mobile/desktopServerStage.test.ts +1 -0
- package/mobile/desktopServerStage.ts +3 -1
- package/mobile/nativeApp.test.ts +20 -5
- package/mobile/nativeApp.ts +4 -1
- package/mobile/nativeConfig.test.ts +209 -5
- package/mobile/nativeConfig.ts +127 -6
- package/mobile/nativePublicEnv.test.ts +2 -0
- package/mobile/nativePublicEnv.ts +13 -0
- package/package.json +4 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/shellOnlyEnv.test.ts +65 -0
- package/shellOnlyEnv.ts +54 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +23 -82
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/sshHostKeys.ts +59 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +160 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/subspace.test.ts +10 -0
- package/subspaceClone.ts +3 -1
- package/subspaceConfig.ts +6 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/transforms/barrelImportsPlugin.ts +10 -4
- package/transforms/mcpAppPageTrimmer.test.ts +8 -2
- package/transforms/mcpAppPageTrimmer.ts +5 -20
- package/transforms/pureLazyMarker.ts +1 -1
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +533 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +82 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/sourceAst.ts +10 -48
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
4
4
|
import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
|
+
import { type BarrelImportsPluginOptions, createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
|
|
6
7
|
import { createBrowserDirnamePlugin } from "../transforms/browserDirnamePlugin";
|
|
7
8
|
import { SideEffectImportOrder } from "../transforms/sideEffectImportOrder";
|
|
8
9
|
import { bundleDefine } from "./bundleDefine";
|
|
@@ -67,6 +68,7 @@ export class CsrArtifactBuilder {
|
|
|
67
68
|
const options = CsrArtifactBuilder.bundleOptions(this.#app, this.#command, akanConfig.optimizeImports);
|
|
68
69
|
const result = await Bun.build({
|
|
69
70
|
...options,
|
|
71
|
+
plugins: [...options.plugins, await CsrArtifactBuilder.barrelImportsPlugin(this.#app)],
|
|
70
72
|
entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
|
|
71
73
|
files: generatedFiles,
|
|
72
74
|
root: this.#generatedDir,
|
|
@@ -118,6 +120,11 @@ export class CsrArtifactBuilder {
|
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
//? Whole, the akanjs/ui barrel bundles every component and the optional peers (react-icons) they import.
|
|
124
|
+
static async barrelImportsPlugin(app: App, pipeAfter?: BarrelImportsPluginOptions["pipeAfter"]) {
|
|
125
|
+
return await createBarrelImportsPlugin(app, { barrels: ["akanjs/ui"], pipeAfter });
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
/** Where a build records the `AKAN_PUBLIC_*` it inlined, which nothing in the minified bundle names. */
|
|
122
129
|
static publicEnvPath(app: App) {
|
|
123
130
|
return path.join(app.dist.cwdPath, ".akan", "artifact", "csr-public-env.json");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { tempDirs, writeText as write } from "../testHelpers";
|
|
4
|
+
import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
4
5
|
import { McpAppViewBuilder } from "./mcpAppViewBuilder";
|
|
5
6
|
|
|
6
7
|
const makeTempRoot = tempDirs("akan-mcp-app-views-");
|
|
@@ -132,6 +133,68 @@ describe("McpAppViewBuilder.discover", () => {
|
|
|
132
133
|
});
|
|
133
134
|
});
|
|
134
135
|
|
|
136
|
+
describe("McpAppViewBuilder.build", () => {
|
|
137
|
+
test("rewrites the akanjs/ui barrel, trims the page to its apps and drops a lazy component nothing renders", async () => {
|
|
138
|
+
const root = await makeTempRoot();
|
|
139
|
+
const file = (rel: string) => path.join(root, rel);
|
|
140
|
+
await write(
|
|
141
|
+
file("node_modules/akanjs/package.json"),
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
name: "akanjs",
|
|
144
|
+
exports: { "./ui": "./ui/index.ts", "./ui/*": "./ui/*", "./webkit": "./webkit.ts" },
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
await write(
|
|
148
|
+
file("node_modules/akanjs/webkit.ts"),
|
|
149
|
+
"export const McpAppBoot = { run: (view: unknown) => console.info(view) };\n",
|
|
150
|
+
);
|
|
151
|
+
await write(
|
|
152
|
+
file("node_modules/akanjs/ui/index.ts"),
|
|
153
|
+
'export { Link } from "./Link";\nexport { Toolbox } from "./Toolbox";\n',
|
|
154
|
+
);
|
|
155
|
+
await write(file("node_modules/akanjs/ui/Link.ts"), 'export const Link = "a link";\n');
|
|
156
|
+
await write(
|
|
157
|
+
file("node_modules/akanjs/ui/Toolbox.ts"),
|
|
158
|
+
'console.info("a toolbox loaded");\nexport const Toolbox = "a toolbox";\n',
|
|
159
|
+
);
|
|
160
|
+
await write(file("apps/demo/ui/lazy.ts"), "export const lazy = (load: () => Promise<unknown>) => ({ load });\n");
|
|
161
|
+
await write(file("apps/demo/ui/Heavy.ts"), 'export default "a heavy chunk";\n');
|
|
162
|
+
await write(
|
|
163
|
+
file("apps/demo/ui/cards.ts"),
|
|
164
|
+
'import { lazy } from "./lazy";\nexport const Heavy = lazy(() => import("./Heavy"));\nexport const Card = "a card";\n',
|
|
165
|
+
);
|
|
166
|
+
await write(
|
|
167
|
+
file("apps/demo/page/home.tsx"),
|
|
168
|
+
[
|
|
169
|
+
'import { Link } from "akanjs/ui";',
|
|
170
|
+
'import { Card } from "../ui/cards";',
|
|
171
|
+
"const page = () => ({ app: (name: string, description: string, draw: () => unknown) => ({ render: (render: () => unknown) => [name, description, draw, render] }) });",
|
|
172
|
+
'export default page().app("showHome", "Show home.", () => [Link, Card]).render(() => "drawn by the render");',
|
|
173
|
+
].join("\n"),
|
|
174
|
+
);
|
|
175
|
+
const app = {
|
|
176
|
+
cwdPath: file("apps/demo"),
|
|
177
|
+
dist: { cwdPath: file("dist/apps/demo") },
|
|
178
|
+
workspace: { workspaceRoot: root },
|
|
179
|
+
getTsConfig: async () => ({ compilerOptions: {} }),
|
|
180
|
+
getPublicEnv: () => ({}),
|
|
181
|
+
logger: { warn: () => undefined },
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
await new McpAppViewBuilder(app as never, "build").build(
|
|
185
|
+
[{ key: "./home.tsx", moduleAbsPath: file("apps/demo/page/home.tsx") }],
|
|
186
|
+
{ basePaths: [], cssAssets: {}, options: CsrArtifactBuilder.bundleOptions(app as never, "build") },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const view = await Bun.file(file("dist/apps/demo/mcp-apps/showHome.html")).text();
|
|
190
|
+
expect(view).toContain("a link");
|
|
191
|
+
expect(view).toContain("a card");
|
|
192
|
+
expect(view).not.toContain("a toolbox");
|
|
193
|
+
expect(view).not.toContain("drawn by the render");
|
|
194
|
+
expect(view).not.toContain("a heavy chunk");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
135
198
|
describe("McpAppViewBuilder.entryOf", () => {
|
|
136
199
|
test("boots the one app with its page, the root layout and overrides it sits under, by relative path", () => {
|
|
137
200
|
const view = {
|
|
@@ -78,7 +78,13 @@ export class McpAppViewBuilder {
|
|
|
78
78
|
const pagePaths = [...new Set(views.map((view) => view.page.moduleAbsPath))];
|
|
79
79
|
const result = await Bun.build({
|
|
80
80
|
...options,
|
|
81
|
-
plugins: [
|
|
81
|
+
plugins: [
|
|
82
|
+
...options.plugins,
|
|
83
|
+
//? Bun runs one onLoad per module, so the one rewriting the barrel also trims the pages and marks `lazy()`; the
|
|
84
|
+
//? marker plugin takes the TypeScript that one leaves to Bun, a package's own outside node_modules/akanjs.
|
|
85
|
+
await CsrArtifactBuilder.barrelImportsPlugin(this.#app, McpAppPageTrimmer.pipe(pagePaths)),
|
|
86
|
+
PureLazyMarker.plugin(),
|
|
87
|
+
],
|
|
82
88
|
entrypoints: views.map((view) => path.join(generatedDir, `${view.name}.html`)),
|
|
83
89
|
files,
|
|
84
90
|
root: generatedDir,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdir, symlink } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { BunPlugin } from "bun";
|
|
5
|
+
import { resolveSsrPageEntries } from "../artifact/implicitRootLayout";
|
|
6
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
7
|
+
import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
8
|
+
import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
|
|
9
|
+
|
|
10
|
+
const makeTempRoot = tempDirs("akan-optional-peers-");
|
|
11
|
+
const akanjsRoot = path.resolve(import.meta.dir, "../../../akanjs");
|
|
12
|
+
const { peerDependenciesMeta } = (await Bun.file(path.join(akanjsRoot, "package.json")).json()) as {
|
|
13
|
+
peerDependenciesMeta: Record<string, { optional?: boolean }>;
|
|
14
|
+
};
|
|
15
|
+
//? The app installs these itself: they are peers so that it renders with one React.
|
|
16
|
+
const appRenderer = new Set(["react", "react-dom", "react-server-dom-webpack"]);
|
|
17
|
+
const optionalPeers = Object.entries(peerDependenciesMeta)
|
|
18
|
+
.filter(([name, meta]) => meta.optional && !appRenderer.has(name))
|
|
19
|
+
.map(([name]) => name);
|
|
20
|
+
const optionalPeerImport = new RegExp(
|
|
21
|
+
`^(?:${optionalPeers.map((name) => name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")).join("|")})(?:/|$)`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const recordOptionalPeers = (reached: string[]): BunPlugin => ({
|
|
25
|
+
name: "record-optional-peers",
|
|
26
|
+
setup(build) {
|
|
27
|
+
build.onResolve({ filter: optionalPeerImport }, ({ path: specifier, importer }) => {
|
|
28
|
+
reached.push(`${path.relative(akanjsRoot, importer)} -> ${specifier}`);
|
|
29
|
+
return { path: specifier, external: true };
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const linkPackage = async (root: string, name: string, dir: string) => {
|
|
35
|
+
await mkdir(path.join(root, "node_modules"), { recursive: true });
|
|
36
|
+
await symlink(dir, path.join(root, "node_modules", name));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const writeApp = async (indexPage: string) => {
|
|
40
|
+
const root = await makeTempRoot();
|
|
41
|
+
const appDir = path.join(root, "apps/demo");
|
|
42
|
+
await linkPackage(root, "akanjs", akanjsRoot);
|
|
43
|
+
await linkPackage(root, "react", path.dirname(Bun.resolveSync("react/package.json", akanjsRoot)));
|
|
44
|
+
await write(
|
|
45
|
+
path.join(root, "tsconfig.json"),
|
|
46
|
+
JSON.stringify({ compilerOptions: { paths: { "@apps/*": ["./apps/*"] } } }),
|
|
47
|
+
);
|
|
48
|
+
await write(path.join(appDir, "client.ts"), "export {};\n");
|
|
49
|
+
await write(path.join(appDir, "env/env.client.ts"), "export const env = {};\n");
|
|
50
|
+
await write(path.join(appDir, "lib/dict.ts"), "export const getAllDictionary = () => ({});\n");
|
|
51
|
+
await write(path.join(appDir, "page/_index.tsx"), indexPage);
|
|
52
|
+
await write(path.join(appDir, "page/_overrides.tsx"), "export default {};\n");
|
|
53
|
+
return { root, appDir, pageKeys: ["./_index.tsx", "./_overrides.tsx"] };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const buildCsr = async (indexPage: string) => {
|
|
57
|
+
const { root, appDir, pageKeys } = await writeApp(indexPage);
|
|
58
|
+
const app = {
|
|
59
|
+
name: "demo",
|
|
60
|
+
cwdPath: appDir,
|
|
61
|
+
dist: { cwdPath: path.join(root, "dist/apps/demo") },
|
|
62
|
+
workspace: { workspaceRoot: root },
|
|
63
|
+
getPageKeys: async () => pageKeys,
|
|
64
|
+
getConfig: async () => ({ basePaths: [], optimizeImports: [], barrelImports: [] }),
|
|
65
|
+
getTsConfig: async () => ({ compilerOptions: { paths: { "@apps/*": ["./apps/*"] } } }),
|
|
66
|
+
getPublicEnv: () => ({}),
|
|
67
|
+
log: () => undefined,
|
|
68
|
+
verbose: () => undefined,
|
|
69
|
+
logger: { warn: () => undefined },
|
|
70
|
+
};
|
|
71
|
+
const reached: string[] = [];
|
|
72
|
+
const build = Bun.build;
|
|
73
|
+
const recorded = spyOn(Bun, "build").mockImplementation((config) =>
|
|
74
|
+
build({ ...config, plugins: [recordOptionalPeers(reached), ...(config.plugins ?? [])] }),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await new CsrArtifactBuilder(app as never, "build").build();
|
|
79
|
+
} finally {
|
|
80
|
+
recorded.mockRestore();
|
|
81
|
+
}
|
|
82
|
+
return { root, reached };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
describe("optional peers of akanjs", () => {
|
|
86
|
+
test("the generated root layout, the _overrides.tsx wrapper and the runtime entries import none", async () => {
|
|
87
|
+
const { appDir, pageKeys } = await writeApp("export default function Page() {\n return null;\n}\n");
|
|
88
|
+
const generated = (await resolveSsrPageEntries({ appCwdPath: appDir, appName: "demo", pageKeys }))
|
|
89
|
+
.map(({ moduleAbsPath }) => moduleAbsPath)
|
|
90
|
+
.filter((file) => file.includes(`${path.sep}.akan${path.sep}`));
|
|
91
|
+
const runtime = [
|
|
92
|
+
path.join(akanjsRoot, "server/rscClient.tsx"),
|
|
93
|
+
path.join(akanjsRoot, "server/rscSegmentOutlet.tsx"),
|
|
94
|
+
path.join(akanjsRoot, "webkit/index.ts"),
|
|
95
|
+
...VENDOR_SPECIFIERS.map((specifier) =>
|
|
96
|
+
path.join(akanjsRoot, "server/vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`),
|
|
97
|
+
),
|
|
98
|
+
];
|
|
99
|
+
expect(generated).toHaveLength(2);
|
|
100
|
+
|
|
101
|
+
//? No barrel rewrite, as a dev registry compiles every module an import names.
|
|
102
|
+
for (const target of ["browser", "bun"] as const) {
|
|
103
|
+
const reached: string[] = [];
|
|
104
|
+
const result = await Bun.build({
|
|
105
|
+
entrypoints: [...generated, ...runtime],
|
|
106
|
+
target,
|
|
107
|
+
throw: false,
|
|
108
|
+
plugins: [recordOptionalPeers(reached)],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result.logs.map((log) => log.message)).toEqual([]);
|
|
112
|
+
expect(reached).toEqual([]);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("the CSR shell build of a page drawing akanjs/ui components imports none", async () => {
|
|
117
|
+
const { reached } = await buildCsr(
|
|
118
|
+
'import { Link, System } from "akanjs/ui";\n\nexport default function Page() {\n return (\n <Link href="/">\n <System.ThemeToggle />\n </Link>\n );\n}\n',
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(reached).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("the MCP App view build of a page drawing akanjs/ui components imports none", async () => {
|
|
125
|
+
const { root, reached } = await buildCsr(
|
|
126
|
+
'import { page } from "akanjs/client";\nimport { Link } from "akanjs/ui";\n\nexport default page()\n .app("showHome", "Show the home link.", () => <Link href="/">home</Link>)\n .render(() => null);\n',
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(await Bun.file(path.join(root, "dist/apps/demo/mcp-apps/showHome.html")).exists()).toBe(true);
|
|
130
|
+
expect(reached).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
});
|
package/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type * from "./fileSys";
|
|
|
19
19
|
export type * from "./frontendBuild";
|
|
20
20
|
export type * from "./getRelatedCnsts";
|
|
21
21
|
export type * from "./incrementalBuilder";
|
|
22
|
+
export type * from "./maintainabilityContract";
|
|
22
23
|
export type * from "./mobile";
|
|
23
24
|
export type * from "./packageExportsMap";
|
|
24
25
|
export type * from "./prompter";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { AkanAppHost } from "../akanApp";
|
|
2
3
|
import { DevGeneratedIndexSync } from "../frontendBuild";
|
|
3
4
|
import { DevStabilityHarness, type DevStabilityHmrProbe } from "./devStabilityHarness";
|
|
4
5
|
|
|
@@ -467,7 +468,8 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
|
|
|
467
468
|
const harness = await createHarness();
|
|
468
469
|
const port = await harness.resolvePort();
|
|
469
470
|
// Child 0's deterministic ws port, held the way an orphaned replica from a killed run would hold it.
|
|
470
|
-
|
|
471
|
+
//? On the child's own 127.0.0.1: macOS lets a loopback bind share a port a wildcard listener holds.
|
|
472
|
+
const blocker = Bun.serve({ port: port + 10_000, hostname: "127.0.0.1", fetch: () => new Response("occupied") });
|
|
471
473
|
try {
|
|
472
474
|
const host = await harness.startHost();
|
|
473
475
|
await host.waitForLog(/falling back to an ephemeral port/);
|
|
@@ -583,9 +585,9 @@ describe("dev resource budgets", () => {
|
|
|
583
585
|
expect(await DevStabilityHarness.buildWorkerProcess(host.proc.pid)).toBeNull();
|
|
584
586
|
});
|
|
585
587
|
|
|
586
|
-
//? Between a fresh builder and one that has built a route, or the host stops enforcing it or never recycles.
|
|
587
|
-
//?
|
|
588
|
-
const CEILING_MB = process.platform === "
|
|
588
|
+
//? Between a fresh builder and one that has built a route, or the host stops enforcing it or never recycles. Linux
|
|
589
|
+
//? measured 101-115MiB fresh, 157-176MiB built and settled; macOS's footprint (Bun 1.4.4) 65MiB fresh, 109MiB built.
|
|
590
|
+
const CEILING_MB = process.platform === "darwin" ? 87 : 135;
|
|
589
591
|
|
|
590
592
|
budgetTest("recycles the builder at an unmeetable ceiling and keeps developing through it", async () => {
|
|
591
593
|
const harness = await createHarness();
|
|
@@ -635,9 +637,11 @@ describe("dev resource budgets", () => {
|
|
|
635
637
|
await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
|
|
636
638
|
}
|
|
637
639
|
const settled = await DevStabilityHarness.builderProcess(host.proc.pid);
|
|
640
|
+
//? Read as the host reads it: on macOS `ps` also counts the pages the builder freed.
|
|
641
|
+
const settledRss = settled ? await AkanAppHost.readProcessRssBytes(settled.pid) : null;
|
|
638
642
|
const warned = /ceiling costs about one boot build per interval/.test(host.logs.join("").slice(start));
|
|
639
643
|
//? Past the interval (a slow machine) the next report recycles instead of warning: either answer settles it.
|
|
640
|
-
if ((
|
|
644
|
+
if ((settledRss ?? 0) >= CEILING_MB * MB && !warned)
|
|
641
645
|
await host.waitForLogSince(
|
|
642
646
|
settledFrom,
|
|
643
647
|
/ceiling costs about one boot build per interval|recycling builder pid=\d+|skipped: the builder fell to/,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -93,6 +94,7 @@ export class SsrMemoryProbe {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
readonly #options: SsrMemoryProbeOptions;
|
|
97
|
+
readonly #statusToken = randomBytes(32).toString("hex");
|
|
96
98
|
#gatewayPid = 0;
|
|
97
99
|
|
|
98
100
|
constructor(options: SsrMemoryProbeOptions) {
|
|
@@ -119,6 +121,8 @@ export class SsrMemoryProbe {
|
|
|
119
121
|
SERVER_MODE: "federation",
|
|
120
122
|
...(basePaths ? { AKAN_PUBLIC_BASE_PATHS: basePaths } : {}),
|
|
121
123
|
PORT: String(port),
|
|
124
|
+
// A built app run outside `akan start` keeps the health detail and metrics behind this token.
|
|
125
|
+
AKAN_STATUS_TOKEN: this.#statusToken,
|
|
122
126
|
AKAN_MEMORY_LOG: "1",
|
|
123
127
|
AKAN_MEMORY_LOG_INTERVAL_MS: String(this.#options.metricsIntervalMs),
|
|
124
128
|
...(cacheOff ? { AKAN_HTML_RESULT_CACHE: "0", AKAN_RSC_RESULT_CACHE: "0" } : {}),
|
|
@@ -315,6 +319,7 @@ export class SsrMemoryProbe {
|
|
|
315
319
|
async #fetchChildMetrics(): Promise<Array<Record<string, number>>> {
|
|
316
320
|
try {
|
|
317
321
|
const res = await fetch(`http://localhost:${this.#options.port}/_akan/app/metrics`, {
|
|
322
|
+
headers: { authorization: `Bearer ${this.#statusToken}` },
|
|
318
323
|
signal: AbortSignal.timeout(5_000),
|
|
319
324
|
});
|
|
320
325
|
if (!res.ok) return [];
|
|
@@ -393,6 +398,7 @@ export class SsrMemoryProbe {
|
|
|
393
398
|
while (Date.now() < deadline) {
|
|
394
399
|
try {
|
|
395
400
|
const res = await fetch(`http://localhost:${this.#options.port}/_akan/app/health`, {
|
|
401
|
+
headers: { authorization: `Bearer ${this.#statusToken}` },
|
|
396
402
|
signal: AbortSignal.timeout(2_000),
|
|
397
403
|
});
|
|
398
404
|
if (res.ok) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { KindBranchScanner } from "./kindBranchScanner";
|
|
4
|
+
import type { SourceFileInfo } from "./qualityScanner";
|
|
5
|
+
|
|
6
|
+
const filesOf = (files: Record<string, string>): SourceFileInfo[] =>
|
|
7
|
+
Object.entries(files).map(([file, content]) => ({
|
|
8
|
+
file,
|
|
9
|
+
absolutePath: `/repo/${file}`,
|
|
10
|
+
content,
|
|
11
|
+
sourceFile: ts.createSourceFile(
|
|
12
|
+
file,
|
|
13
|
+
content,
|
|
14
|
+
ts.ScriptTarget.Latest,
|
|
15
|
+
true,
|
|
16
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const pinConstant = `import { enumOf } from "akanjs/base";
|
|
21
|
+
export class PinKind extends enumOf("pinKind", ["videoScene", "videoTake", "memo"] as const) {}
|
|
22
|
+
export class PinStatus extends enumOf("pinStatus", ["active", "videoTake"] as const) {}
|
|
23
|
+
`;
|
|
24
|
+
const comparing = (expression: string) => `export const isIt = (pin: { kind: string }) => ${expression};\n`;
|
|
25
|
+
|
|
26
|
+
describe("KindBranchScanner", () => {
|
|
27
|
+
test("warns on a kind value compared in three files outside its module, naming them and the table to declare", () => {
|
|
28
|
+
const warnings = new KindBranchScanner().scan(
|
|
29
|
+
filesOf({
|
|
30
|
+
"apps/demo/lib/pin/pin.constant.ts": pinConstant,
|
|
31
|
+
"apps/demo/lib/pin/Pin.Zone.tsx": comparing('pin.kind === "videoScene"'),
|
|
32
|
+
"apps/demo/lib/board/Board.Zone.tsx": comparing('pin.kind === "videoScene"'),
|
|
33
|
+
"apps/demo/lib/take/take.store.ts": comparing('"videoScene" !== pin.kind'),
|
|
34
|
+
"apps/demo/ui/PinCard.tsx": `export const label = (kind: string) => {\n switch (kind) {\n case "videoScene":\n return 1;\n }\n};\n`,
|
|
35
|
+
"apps/demo/ui/PinCard.test.tsx": comparing('pin.kind === "videoScene"'),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(warnings).toHaveLength(1);
|
|
40
|
+
expect(warnings[0]).toMatchObject({
|
|
41
|
+
rule: "akan.global.kind-branch",
|
|
42
|
+
severity: "warning",
|
|
43
|
+
file: "apps/demo/lib/pin/pin.constant.ts",
|
|
44
|
+
line: 2,
|
|
45
|
+
message: '"videoScene" of cnst.PinKind is branched on in 3 files outside the enum\'s module.',
|
|
46
|
+
locations: [
|
|
47
|
+
{ file: "apps/demo/lib/board/Board.Zone.tsx", line: 1 },
|
|
48
|
+
{ file: "apps/demo/lib/take/take.store.ts", line: 1 },
|
|
49
|
+
{ file: "apps/demo/ui/PinCard.tsx", line: 3 },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
expect(warnings[0]?.fix).toContain('{ [kind in cnst.PinKind["value"]]: … }');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("reads only a subject named for a kind, and only enums named for one", () => {
|
|
56
|
+
const warnings = new KindBranchScanner().scan(
|
|
57
|
+
filesOf({
|
|
58
|
+
"apps/demo/lib/pin/pin.constant.ts": pinConstant,
|
|
59
|
+
"apps/demo/lib/a/A.Zone.tsx": comparing('pin.status === "active" || label === "videoScene"'),
|
|
60
|
+
"apps/demo/lib/b/B.Zone.tsx": comparing('pin.status === "active" || label === "videoScene"'),
|
|
61
|
+
"apps/demo/lib/c/C.Zone.tsx": comparing('pin.status === "active" || label === "videoScene"'),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(warnings).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("groups the enums sharing a value, reached from the app's own lib or a lib, never another app or lib", () => {
|
|
69
|
+
const warnings = new KindBranchScanner().scan(
|
|
70
|
+
filesOf({
|
|
71
|
+
"apps/demo/lib/pin/pin.constant.ts": pinConstant,
|
|
72
|
+
"libs/media/lib/clip/clip.constant.ts": `export class ClipType extends enumOf("clipType", ["videoTake"] as const) {}\n`,
|
|
73
|
+
"apps/demo/lib/a/A.Zone.tsx": comparing('pin.kind === "videoTake"'),
|
|
74
|
+
"apps/demo/lib/b/B.Zone.tsx": comparing('pin.kind === "videoTake"'),
|
|
75
|
+
"apps/demo/lib/c/C.Zone.tsx": comparing('pin.kind === "videoTake"'),
|
|
76
|
+
"apps/other/lib/a/A.Zone.tsx": comparing('pin.kind === "videoTake"'),
|
|
77
|
+
"apps/other/lib/b/B.Zone.tsx": comparing('pin.kind === "videoTake"'),
|
|
78
|
+
"libs/shared/lib/a/A.Zone.tsx": comparing('pin.kind === "videoTake"'),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(warnings.map(({ file, message }) => ({ file, message }))).toEqual([
|
|
83
|
+
{
|
|
84
|
+
file: "apps/demo/lib/pin/pin.constant.ts",
|
|
85
|
+
message: '"videoTake" of cnst.PinKind / cnst.ClipType is branched on in 3 files outside the enum\'s module.',
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { QualityWarning, SourceFileInfo } from "./qualityScanner";
|
|
4
|
+
|
|
5
|
+
interface EnumDeclaration {
|
|
6
|
+
name: string;
|
|
7
|
+
sys: string;
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
module: string;
|
|
11
|
+
values: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BranchSite {
|
|
15
|
+
value: string;
|
|
16
|
+
file: string;
|
|
17
|
+
line: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//? Enums sharing a value form one group: short of the type checker, nothing tells which one a literal means.
|
|
21
|
+
export class KindBranchScanner {
|
|
22
|
+
static readonly rule = "akan.global.kind-branch";
|
|
23
|
+
static readonly minFileNum = 3;
|
|
24
|
+
static readonly #kindName = /(Kind|Type|Mode)$/;
|
|
25
|
+
static readonly #kindSubject = /^(kind|type|mode)$|[a-z](Kind|Type|Mode)$/;
|
|
26
|
+
static readonly #equalities = new Set([
|
|
27
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
28
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
29
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
30
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
scan(sourceFiles: SourceFileInfo[]): QualityWarning[] {
|
|
34
|
+
const enums = sourceFiles.flatMap((sourceFile) => KindBranchScanner.#enumsOf(sourceFile));
|
|
35
|
+
const ownersByValue = new Map<string, EnumDeclaration[]>();
|
|
36
|
+
for (const declaration of enums)
|
|
37
|
+
for (const value of declaration.values)
|
|
38
|
+
ownersByValue.set(value, [...(ownersByValue.get(value) ?? []), declaration]);
|
|
39
|
+
const sitesByGroup = new Map<string, { owners: EnumDeclaration[]; sites: BranchSite[] }>();
|
|
40
|
+
for (const sourceFile of sourceFiles) {
|
|
41
|
+
if (!/^(apps|libs)\//.test(sourceFile.file) || /\.(test|spec)\.tsx?$/.test(sourceFile.file)) continue;
|
|
42
|
+
const sys = KindBranchScanner.#sysOf(sourceFile.file);
|
|
43
|
+
//? An app reaches every lib's enums; a lib only its own, as nothing here knows which libs it depends on.
|
|
44
|
+
for (const site of KindBranchScanner.#sitesIn(sourceFile, ownersByValue)) {
|
|
45
|
+
const owners = (ownersByValue.get(site.value) ?? []).filter(
|
|
46
|
+
(owner) => owner.sys === sys || (owner.sys.startsWith("libs/") && sys.startsWith("apps/")),
|
|
47
|
+
);
|
|
48
|
+
if (!owners.length || owners.some(({ module }) => site.file.startsWith(module))) continue;
|
|
49
|
+
const group = owners.map(({ name, file }) => `${file}#${name}`).join(",");
|
|
50
|
+
const entry = sitesByGroup.get(group) ?? { owners, sites: [] };
|
|
51
|
+
entry.sites.push(site);
|
|
52
|
+
sitesByGroup.set(group, entry);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...sitesByGroup.values()].flatMap(({ owners, sites }) => KindBranchScanner.#warningOf(owners, sites));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static #warningOf(owners: EnumDeclaration[], sites: BranchSite[]): QualityWarning[] {
|
|
59
|
+
const filesByValue = new Map<string, Set<string>>();
|
|
60
|
+
for (const { value, file } of sites) filesByValue.set(value, (filesByValue.get(value) ?? new Set()).add(file));
|
|
61
|
+
const values = [...filesByValue]
|
|
62
|
+
.filter(([, files]) => files.size >= KindBranchScanner.minFileNum)
|
|
63
|
+
.map(([value]) => value)
|
|
64
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
65
|
+
const [owner] = owners;
|
|
66
|
+
if (!owner || !values.length) return [];
|
|
67
|
+
const branched = sites.filter(({ value }) => values.includes(value));
|
|
68
|
+
const fileNum = new Set(branched.map(({ file }) => file)).size;
|
|
69
|
+
const enumNames = owners.map(({ name }) => `cnst.${name}`).join(" / ");
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
rule: KindBranchScanner.rule,
|
|
73
|
+
scope: "global",
|
|
74
|
+
severity: "warning",
|
|
75
|
+
file: owner.file,
|
|
76
|
+
line: owner.line,
|
|
77
|
+
message: `${values.map((value) => `"${value}"`).join(", ")} of ${enumNames} ${values.length > 1 ? "are" : "is"} branched on in ${fileNum} files outside the enum's module.`,
|
|
78
|
+
locations: branched.map(({ file, line }) => ({ file, line })),
|
|
79
|
+
fix: `Declare one table keyed by the kind, \`{ [kind in cnst.${owner.name}["value"]]: … }\` (a component, label or handler per kind), and look the kind up in it instead of comparing it in each file.`,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static #sitesIn({ file, sourceFile }: SourceFileInfo, ownersByValue: Map<string, EnumDeclaration[]>) {
|
|
85
|
+
const sites: BranchSite[] = [];
|
|
86
|
+
const record = (literal: ts.Expression, subject: ts.Expression) => {
|
|
87
|
+
if (!ts.isStringLiteralLike(literal) || !ownersByValue.has(literal.text)) return;
|
|
88
|
+
if (!KindBranchScanner.#isKindSubject(subject)) return;
|
|
89
|
+
sites.push({
|
|
90
|
+
value: literal.text,
|
|
91
|
+
file,
|
|
92
|
+
line: sourceFile.getLineAndCharacterOfPosition(literal.getStart(sourceFile)).line + 1,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const visit = (node: ts.Node) => {
|
|
96
|
+
if (ts.isBinaryExpression(node) && KindBranchScanner.#equalities.has(node.operatorToken.kind)) {
|
|
97
|
+
record(node.left, node.right);
|
|
98
|
+
record(node.right, node.left);
|
|
99
|
+
} else if (ts.isCaseClause(node) && ts.isSwitchStatement(node.parent.parent))
|
|
100
|
+
record(node.expression, node.parent.parent.expression);
|
|
101
|
+
ts.forEachChild(node, visit);
|
|
102
|
+
};
|
|
103
|
+
visit(sourceFile);
|
|
104
|
+
return sites;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//? Only a subject named for a kind (`kind`, `pin.kind`, `viewMode`) counts: a bare `"active"` matches too much.
|
|
108
|
+
static #isKindSubject(subject: ts.Expression) {
|
|
109
|
+
let current = subject;
|
|
110
|
+
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current))
|
|
111
|
+
current = current.expression;
|
|
112
|
+
const name = ts.isIdentifier(current)
|
|
113
|
+
? current.text
|
|
114
|
+
: ts.isPropertyAccessExpression(current)
|
|
115
|
+
? current.name.text
|
|
116
|
+
: ts.isElementAccessExpression(current) && ts.isStringLiteralLike(current.argumentExpression)
|
|
117
|
+
? current.argumentExpression.text
|
|
118
|
+
: "";
|
|
119
|
+
return KindBranchScanner.#kindSubject.test(name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static #enumsOf({ file, sourceFile }: SourceFileInfo): EnumDeclaration[] {
|
|
123
|
+
if (!/^(apps|libs)\/[^/]+\/lib\/.+\.constant\.ts$/.test(file)) return [];
|
|
124
|
+
return sourceFile.statements.flatMap((statement) => {
|
|
125
|
+
if (!ts.isClassDeclaration(statement) || !statement.name) return [];
|
|
126
|
+
if (!KindBranchScanner.#kindName.test(statement.name.text)) return [];
|
|
127
|
+
const values = KindBranchScanner.#enumValuesOf(statement);
|
|
128
|
+
if (!values) return [];
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
name: statement.name.text,
|
|
132
|
+
sys: KindBranchScanner.#sysOf(file),
|
|
133
|
+
file,
|
|
134
|
+
line: sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile)).line + 1,
|
|
135
|
+
module: `${path.posix.dirname(file)}/`,
|
|
136
|
+
values,
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static #sysOf(file: string) {
|
|
143
|
+
return file.split("/").slice(0, 2).join("/");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static #enumValuesOf(statement: ts.ClassDeclaration) {
|
|
147
|
+
for (const clause of statement.heritageClauses ?? [])
|
|
148
|
+
for (const { expression } of clause.types) {
|
|
149
|
+
if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression)) continue;
|
|
150
|
+
if (expression.expression.text !== "enumOf") continue;
|
|
151
|
+
let list = expression.arguments[1];
|
|
152
|
+
while (list && (ts.isAsExpression(list) || ts.isParenthesizedExpression(list))) list = list.expression;
|
|
153
|
+
if (!list || !ts.isArrayLiteralExpression(list)) return null;
|
|
154
|
+
return list.elements.filter(ts.isStringLiteralLike).map(({ text }) => text);
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const everyCourt = () => fetch.courtListInPublic(0, 10000); // @flag
|
|
2
|
+
export const devicesOfOrg = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 1000); // @flag
|
|
3
|
+
export const sortedDevices = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 1000, "latest"); // @flag
|
|
4
|
+
export const withPolicy = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 1000, null, { crystalize: false }); // @flag
|
|
5
|
+
export const rootList = () => fetch.deviceList(null, 501); // @flag
|
|
6
|
+
export const separated = () => fetch.deviceList(undefined, 10_000); // @flag
|
|
7
|
+
export const exponent = () => fetch.deviceList(0, 1e4); // @flag
|
|
8
|
+
export const namedSkip = (orgId: string, skip: number) => fetch.alertListInOrg(orgId, skip, 2000); // @flag
|
|
9
|
+
export const pagedSkip = (orgId: string, page: number) => fetch.alertListInOrg(orgId, (page - 1) * 20, 2000); // @flag
|
|
10
|
+
export const afterSliceNumbers = () => fetch.statListInYear(2026, 0, 600); // @flag
|
|
11
|
+
export const initInRoute = (orgId: string) => fetch.initDeviceInOrg(orgId, { limit: 1000 }); // @flag
|
|
12
|
+
export const initRoot = () => fetch.initDevice({ sort: "latest", limit: 9999 }); // @flag
|
|
13
|
+
export const getInit = (orgId: string) => fetch.getDeviceInitInOrg(orgId, { limit: 1000 }); // @flag
|
|
14
|
+
export const storeInit = (orgId: string) => st.do.initDeviceInOrg(orgId, { limit: 1000 }); // @flag
|
|
15
|
+
export const storeRefresh = () => st.do.refreshDeviceInOrg({ limit: 1000 }); // @flag
|
|
16
|
+
export const quotedKey = (orgId: string) => st.do.initDeviceInOrg(orgId, { "limit": 1000 }); // @flag
|
|
17
|
+
export const storeSetLimit = () => st.do.setLimitOfDeviceInOrg(1000); // @flag
|
|
18
|
+
export const Monitor = () => <button type="button" onClick={() => st.do.setLimitOfAlertInOrg(5000)}>All</button>; // @flag
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const atTheCap = () => fetch.courtListInPublic(0, 500); // @ok
|
|
2
|
+
export const onePage = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 20, "latest"); // @ok
|
|
3
|
+
export const variableLimit = (orgId: string, limit: number) => fetch.deviceListInOrg(orgId, 0, limit); // @ok
|
|
4
|
+
export const everyRowMode = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 0); // @ok
|
|
5
|
+
export const yearSlice = () => fetch.statListInYear(2026); // @ok
|
|
6
|
+
export const yearAfterId = (orgId: string) => fetch.statListInOrgYear(orgId, 2026); // @ok
|
|
7
|
+
export const monthAndYear = () => fetch.statListInMonthOfYear(11, 2026); // @ok
|
|
8
|
+
export const twoYears = () => fetch.statListInRange(2025, 2026); // @ok
|
|
9
|
+
export const numbersBeforePaging = () => fetch.statListInRange(0, 2026, 0, 20); // @ok
|
|
10
|
+
export const numbersWithoutPaging = () => fetch.statListInRange(0, 2026, 0); // @ok
|
|
11
|
+
export const notAList = () => fetch.device(0, 1000); // @ok
|
|
12
|
+
export const notFetch = () => api.deviceList(0, 1000); // @ok
|
|
13
|
+
export const lowercaseList = () => fetch.checklist(0, 1000); // @ok
|
|
14
|
+
export const initAtTheCap = (orgId: string) => fetch.initDeviceInOrg(orgId, { limit: 500 }); // @ok
|
|
15
|
+
export const rateLimit = (orgId: string) => fetch.initDeviceInOrg(orgId, { rateLimit: 1000 }); // @ok
|
|
16
|
+
export const nestedLimit = (orgId: string) => fetch.initDeviceInOrg(orgId, { default: { limit: 1000 } }); // @ok
|
|
17
|
+
export const mutationLimit = (id: string) => fetch.updateQuota(id, { limit: 1000 }); // @ok
|
|
18
|
+
export const storeWrite = () => st.do.setLimitOnQuota(1000); // @ok
|
|
19
|
+
export const storeSetSmallLimit = () => st.do.setLimitOfDeviceInOrg(100); // @ok
|
|
20
|
+
export const storeSetPage = () => st.do.setPageOfDeviceInOrg(1000); // @ok
|
|
21
|
+
export const policyOnList = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 20, "latest", { limit: 1000 }); // @ok
|
|
22
|
+
export const Docs = () => <code>fetch.courtListInPublic(0, 10000)</code>; // @ok
|
|
23
|
+
export const sample = "fetch.courtListInPublic(0, 10000)"; // @ok
|