@akanjs/devkit 3.0.0-alpha.11 → 3.0.0-alpha.110
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 +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +37 -2
- package/applicationBuildRunner.ts +52 -23
- package/artifact/implicitRootLayout.test.ts +70 -1
- package/artifact/implicitRootLayout.ts +41 -14
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +28 -1
- package/biomeBase.ts +1 -1
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +135 -7
- package/executors.ts +115 -25
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +63 -10
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.test.ts +75 -2
- package/frontendBuild/fontOptimizer.ts +98 -27
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +124 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +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 +13 -19
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +82 -13
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +80 -142
- package/qualityScanner.ts +89 -31
- package/recipeScanner.ts +4 -1
- package/routeSourceValidator.test.ts +63 -0
- package/routeSourceValidator.ts +123 -16
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +105 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/mcpScanner.ts +0 -217
- package/src/capacitorApp.ts +0 -282
- package/storeScanner.ts +0 -173
- 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
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
// barrel reaches `cssCompiler`/`ssrBaseArtifactBuilder`, which pull tailwindcss + @tailwindcss/node
|
|
5
5
|
// (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
|
|
6
6
|
// into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
|
|
7
|
+
import { CodegenLock } from "@akanjs/devkit/codegenLock";
|
|
7
8
|
import type { App } from "@akanjs/devkit/commandDecorators";
|
|
8
9
|
import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
9
10
|
import { AutoImportSync } from "@akanjs/devkit/frontendBuild/autoImportSync";
|
|
@@ -185,7 +186,12 @@ class IncrementalBuilder {
|
|
|
185
186
|
if (this.#shuttingDown) return;
|
|
186
187
|
this.#shuttingDown = true;
|
|
187
188
|
const started = Date.now();
|
|
188
|
-
this.#logger.
|
|
189
|
+
this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
|
|
190
|
+
// Stopped before the drain, or a save landing mid-drain would enqueue a batch behind the queue tail
|
|
191
|
+
// this method already awaited — and `process.exit(0)` would cut that batch off partway through
|
|
192
|
+
// writing its artifacts. The replacement rebuilds every artifact from its boot build anyway, so the
|
|
193
|
+
// batch is not lost; a half-written one would be.
|
|
194
|
+
this.#watcher?.stop();
|
|
189
195
|
if (this.#cssRebuildTimer) {
|
|
190
196
|
// Only reachable if a css batch landed between the idle report and this request: the fresh
|
|
191
197
|
// boot build recompiles css from scratch anyway, so dropping the debounce loses nothing.
|
|
@@ -200,7 +206,7 @@ class IncrementalBuilder {
|
|
|
200
206
|
// flushed — a `css-updated` relayed milliseconds before this line would be dropped with no error
|
|
201
207
|
// anywhere, leaving the backend serving the previous bundle. See `BuilderChannel`.
|
|
202
208
|
const flushed = await BuilderChannel.drain();
|
|
203
|
-
this.#logger.
|
|
209
|
+
this.#logger.debug(
|
|
204
210
|
`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`,
|
|
205
211
|
);
|
|
206
212
|
process.exit(0);
|
|
@@ -294,22 +300,27 @@ class IncrementalBuilder {
|
|
|
294
300
|
//* Insert framework imports that are used but omitted (e.g. `Int` in *.constant.ts, `fetch` in
|
|
295
301
|
//* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
|
|
296
302
|
//* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
|
|
297
|
-
const autoImport = await
|
|
303
|
+
const [autoImport, indexSync] = await CodegenLock.run(
|
|
304
|
+
this.#app.workspace.workspaceRoot,
|
|
305
|
+
`hmr-batch:${this.#app.name}`,
|
|
306
|
+
async () => {
|
|
307
|
+
const auto = await this.#autoImportSync.syncForBatch(batch.files);
|
|
308
|
+
return [auto, await this.#generatedIndexSync.syncForBatch(batch.files)] as const;
|
|
309
|
+
},
|
|
310
|
+
);
|
|
298
311
|
for (const error of autoImport.errors) this.#logger.error(error);
|
|
299
312
|
if (autoImport.changedFiles.length > 0)
|
|
300
313
|
this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
|
|
301
|
-
const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
|
|
302
314
|
//* Both passes above write source files, and this generation's build consumes what they wrote. Hand
|
|
303
315
|
//* them to the watcher so its verification scan does not read them back as a user edit and spend a
|
|
304
316
|
//* second generation rebuilding identical content.
|
|
305
317
|
await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
|
|
306
|
-
const { files, kinds, expandedBatch, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
318
|
+
const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
307
319
|
generation,
|
|
308
320
|
batch,
|
|
309
321
|
indexSync,
|
|
310
322
|
changePlanner: this.#changePlanner,
|
|
311
323
|
});
|
|
312
|
-
const devPlan = event.devPlan;
|
|
313
324
|
this.#logger.verbose(
|
|
314
325
|
`[hmr] batch generation=${generation} kinds=${kinds.join(",")} files=${files.length} generated=${indexSync.changedFiles.length} roles=${devPlan.roles.join(",") || "(none)"} actions=${devPlan.actions.join(",") || "(none)"}`,
|
|
315
326
|
);
|
package/index.ts
CHANGED
|
@@ -22,19 +22,15 @@ export type * from "./applicationBuildRunner";
|
|
|
22
22
|
export type * from "./applicationReleasePackager";
|
|
23
23
|
export type * from "./applicationTestPreload";
|
|
24
24
|
export type * from "./artifact";
|
|
25
|
-
export type * from "./builder";
|
|
26
25
|
export type * from "./capacitorApp";
|
|
27
26
|
export type * from "./cloud";
|
|
28
27
|
export type * from "./commandDecorators";
|
|
29
28
|
export type * from "./createTunnel";
|
|
30
29
|
export type * from "./dependencyScanner";
|
|
31
30
|
export type * from "./executors";
|
|
32
|
-
export type * from "./extractDeps";
|
|
33
31
|
export type * from "./fileSys";
|
|
34
32
|
export type * from "./frontendBuild";
|
|
35
|
-
export type * from "./getCredentials";
|
|
36
33
|
export type * from "./getDirname";
|
|
37
|
-
export type * from "./getModelFileData";
|
|
38
34
|
export type * from "./getRelatedCnsts";
|
|
39
35
|
export type * from "./guideline";
|
|
40
36
|
export type * from "./incrementalBuilder";
|
|
@@ -46,11 +42,8 @@ export type * from "./recipeScanner";
|
|
|
46
42
|
export type * from "./scanInfo";
|
|
47
43
|
export type * from "./selectModel";
|
|
48
44
|
export type * from "./spinner";
|
|
49
|
-
export type * from "./streamAi";
|
|
50
45
|
export type * from "./transforms";
|
|
51
46
|
export type * from "./typeChecker";
|
|
52
47
|
export type * from "./types";
|
|
53
|
-
export type * from "./ui";
|
|
54
48
|
export type * from "./uploadRelease";
|
|
55
|
-
export type * from "./useStdoutDimensions";
|
|
56
49
|
export type * from "./workflow";
|
|
@@ -118,20 +118,12 @@ export default config;
|
|
|
118
118
|
),
|
|
119
119
|
this.writeFile(
|
|
120
120
|
"env/env.client.ts",
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
export const env = {
|
|
124
|
-
...getEnv(),
|
|
125
|
-
} as const;
|
|
121
|
+
`export const env = {} as const;
|
|
126
122
|
`,
|
|
127
123
|
),
|
|
128
124
|
this.writeFile(
|
|
129
125
|
"env/env.server.ts",
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
export const env = {
|
|
133
|
-
...getEnv(),
|
|
134
|
-
} as const;
|
|
126
|
+
`export const env = {} as const;
|
|
135
127
|
`,
|
|
136
128
|
),
|
|
137
129
|
this.writeFile(
|
|
@@ -167,19 +159,20 @@ export const server = new AkanServer("${this.appName}", {
|
|
|
167
159
|
this.writeFile(
|
|
168
160
|
"page/_layout.tsx",
|
|
169
161
|
`import "./styles.css";
|
|
170
|
-
import
|
|
162
|
+
import { layout } from "akanjs/client";
|
|
171
163
|
|
|
172
|
-
export default
|
|
164
|
+
export default layout().render(({ children }) => {
|
|
173
165
|
return <>{children}</>;
|
|
174
|
-
}
|
|
166
|
+
});
|
|
175
167
|
`,
|
|
176
168
|
),
|
|
177
169
|
this.writeFile(
|
|
178
170
|
"page/_index.tsx",
|
|
179
|
-
`import {
|
|
171
|
+
`import { page } from "akanjs/client";
|
|
172
|
+
import { marker } from "../common/marker";
|
|
180
173
|
import { ClientMarker } from "../ui/ClientMarker";
|
|
181
174
|
|
|
182
|
-
export default
|
|
175
|
+
export default page().render(() => {
|
|
183
176
|
return (
|
|
184
177
|
<main>
|
|
185
178
|
<h1>Dev stability fixture</h1>
|
|
@@ -187,7 +180,7 @@ export default function Page() {
|
|
|
187
180
|
<ClientMarker />
|
|
188
181
|
</main>
|
|
189
182
|
);
|
|
190
|
-
}
|
|
183
|
+
});
|
|
191
184
|
`,
|
|
192
185
|
),
|
|
193
186
|
// A second route exists so a test can ask for a page the backend has *never* built, at a moment of
|
|
@@ -195,16 +188,17 @@ export default function Page() {
|
|
|
195
188
|
// request of a test is the only one that reaches the builder at all.
|
|
196
189
|
this.writeFile(
|
|
197
190
|
"page/second/_index.tsx",
|
|
198
|
-
`import {
|
|
191
|
+
`import { page } from "akanjs/client";
|
|
192
|
+
import { ClientMarker } from "../../ui/ClientMarker";
|
|
199
193
|
|
|
200
|
-
export default
|
|
194
|
+
export default page().render(() => {
|
|
201
195
|
return (
|
|
202
196
|
<main>
|
|
203
197
|
<h1>Second route</h1>
|
|
204
198
|
<ClientMarker />
|
|
205
199
|
</main>
|
|
206
200
|
);
|
|
207
|
-
}
|
|
201
|
+
});
|
|
208
202
|
`,
|
|
209
203
|
),
|
|
210
204
|
this.writeFile(
|
|
@@ -0,0 +1,112 @@
|
|
|
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 { Executor, LibExecutor, WorkspaceExecutor } from "./executors";
|
|
6
|
+
import { formatLibStatuses, LibSource } from "./libSource";
|
|
7
|
+
|
|
8
|
+
const tempRoots: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const write = async (filePath: string, content: string) => {
|
|
15
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
16
|
+
await writeFile(filePath, content);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// `LibExecutor.from` memoises by name, so each fixture needs a name no other test has used.
|
|
20
|
+
const makeLib = async (libName: string) => {
|
|
21
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-libsource-"));
|
|
22
|
+
tempRoots.push(root);
|
|
23
|
+
await write(
|
|
24
|
+
path.join(root, "package.json"),
|
|
25
|
+
'{ "name": "workspace", "version": "0.0.1", "description": "workspace" }\n',
|
|
26
|
+
);
|
|
27
|
+
await write(path.join(root, ".gitignore"), "node_modules\n");
|
|
28
|
+
await write(path.join(root, `libs/${libName}/package.json`), `{ "name": "@${libName}", "version": "0.0.1" }\n`);
|
|
29
|
+
await write(path.join(root, `libs/${libName}/common/helper.ts`), "export const helper = 1;\n");
|
|
30
|
+
await write(path.join(root, `libs/${libName}/env/env.server.testing.ts`), "export const env = { key: 1 };\n");
|
|
31
|
+
|
|
32
|
+
const git = new Executor("fixture", root);
|
|
33
|
+
await git.spawn("git", ["init", "--quiet"]);
|
|
34
|
+
|
|
35
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: `workspace-${libName}` });
|
|
36
|
+
const lib = LibExecutor.from(workspace, libName);
|
|
37
|
+
return { root, lib, source: new LibSource(lib) };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe("LibSource", () => {
|
|
41
|
+
test("reports an unstamped library", async () => {
|
|
42
|
+
const { source } = await makeLib("plain-lib");
|
|
43
|
+
const status = await source.status();
|
|
44
|
+
|
|
45
|
+
expect(status.drift).toBe("unstamped");
|
|
46
|
+
expect(status.stamp).toBeNull();
|
|
47
|
+
expect(status.hash).toMatch(/^[0-9a-f]{32}$/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("stamps the origin into package.json and reads back clean", async () => {
|
|
51
|
+
const { lib, source } = await makeLib("stamped-lib");
|
|
52
|
+
const stamp = await source.write({ origin: "akanjs", sha: "3.0.0" });
|
|
53
|
+
|
|
54
|
+
expect(stamp.origin).toBe("akanjs");
|
|
55
|
+
expect(await source.read()).toEqual(stamp);
|
|
56
|
+
expect((await source.status()).drift).toBe("clean");
|
|
57
|
+
|
|
58
|
+
const manifest = await lib.getPackageJson();
|
|
59
|
+
expect(manifest.name).toBe("@stamped-lib");
|
|
60
|
+
expect((manifest.akan as { source: { sha: string } }).source.sha).toBe("3.0.0");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("detects an edit to library source as drift", async () => {
|
|
64
|
+
const { root, source } = await makeLib("drift-lib");
|
|
65
|
+
await source.write({ origin: "akanjs", sha: "3.0.0" });
|
|
66
|
+
await write(path.join(root, "libs/drift-lib/common/helper.ts"), "export const helper = 2;\n");
|
|
67
|
+
|
|
68
|
+
expect((await source.status()).drift).toBe("drifted");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("survives every other akan write to the manifest", async () => {
|
|
72
|
+
const { lib, source } = await makeLib("merge-lib");
|
|
73
|
+
const stamp = await source.write({ origin: "akanjs", sha: "3.0.0" });
|
|
74
|
+
const manifest = await lib.getPackageJson();
|
|
75
|
+
await lib.setPackageJson({ ...manifest, dependencies: { lodash: "4.0.0" } });
|
|
76
|
+
|
|
77
|
+
expect(await source.read()).toEqual(stamp);
|
|
78
|
+
expect((await source.status()).drift).toBe("drifted");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("leaves env values out of the hash — they belong to the installing workspace", async () => {
|
|
82
|
+
const { root, source } = await makeLib("env-lib");
|
|
83
|
+
await source.write({ origin: "akanjs", sha: "3.0.0" });
|
|
84
|
+
await write(path.join(root, "libs/env-lib/env/env.server.testing.ts"), "export const env = { key: 999 };\n");
|
|
85
|
+
|
|
86
|
+
expect((await source.status()).drift).toBe("clean");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("formatLibStatuses", () => {
|
|
91
|
+
test("marks drifted libraries and counts them", () => {
|
|
92
|
+
const text = formatLibStatuses([
|
|
93
|
+
{
|
|
94
|
+
lib: "util",
|
|
95
|
+
drift: "clean",
|
|
96
|
+
hash: "a".repeat(32),
|
|
97
|
+
stamp: { origin: "akanjs", sha: "3.0.0", hash: "a".repeat(32), syncedAt: "now" },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
lib: "shared",
|
|
101
|
+
drift: "drifted",
|
|
102
|
+
hash: "b".repeat(32),
|
|
103
|
+
stamp: { origin: "akanjs", sha: "3.0.0", hash: "a".repeat(32), syncedAt: "now" },
|
|
104
|
+
},
|
|
105
|
+
{ lib: "local", drift: "unstamped", hash: "c".repeat(32), stamp: null },
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
expect(text).toContain("DRIFTED libs/shared akanjs@3.0.0");
|
|
109
|
+
expect(text).toContain("unstamped libs/local no akan.source in package.json");
|
|
110
|
+
expect(text).toContain("drifted: 1 / 3");
|
|
111
|
+
});
|
|
112
|
+
});
|
package/libSource.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { LibExecutor } from "./executors";
|
|
2
|
+
import type { PackageJson } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface LibSourceStamp {
|
|
5
|
+
/** Where the copy came from: a git remote URL, or `akanjs` for the published package. */
|
|
6
|
+
origin: string;
|
|
7
|
+
/** The origin's commit sha, or the package version when the origin is a registry. */
|
|
8
|
+
sha: string;
|
|
9
|
+
/** Content hash of the library as installed, so a later edit is detectable without the origin. */
|
|
10
|
+
hash: string;
|
|
11
|
+
syncedAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LibDrift = "clean" | "drifted" | "unstamped";
|
|
15
|
+
|
|
16
|
+
export interface LibStatus {
|
|
17
|
+
lib: string;
|
|
18
|
+
drift: LibDrift;
|
|
19
|
+
stamp: LibSourceStamp | null;
|
|
20
|
+
hash: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A library's origin, recorded in its own `package.json` under an `akan.source` key.
|
|
25
|
+
*
|
|
26
|
+
* The key rides `package.json` rather than a file of its own because every akan write to a library's
|
|
27
|
+
* manifest is a spread of the existing object (`LibExecutor.syncPackageJson`), so an unknown top-level
|
|
28
|
+
* key survives — while a new root file would have to be added to `libRootAllowedFiles` before
|
|
29
|
+
* `akan sync` stopped rejecting it.
|
|
30
|
+
*/
|
|
31
|
+
export class LibSource {
|
|
32
|
+
static readonly manifestKey = "akan";
|
|
33
|
+
/**
|
|
34
|
+
* Left out of the hash. `env/` holds per-deployment values that belong to the workspace the library
|
|
35
|
+
* was installed into, not to the origin, so an env edit is not drift.
|
|
36
|
+
*/
|
|
37
|
+
static readonly unhashedDirs = ["env"];
|
|
38
|
+
|
|
39
|
+
#lib: LibExecutor;
|
|
40
|
+
constructor(lib: LibExecutor) {
|
|
41
|
+
this.#lib = lib;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get #prefix() {
|
|
45
|
+
return `libs/${this.#lib.name}/`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#isHashed(file: string) {
|
|
49
|
+
const relative = file.slice(this.#prefix.length);
|
|
50
|
+
return !LibSource.unhashedDirs.includes(relative.split("/")[0] ?? "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `package.json` cannot hash the stamp it carries, so the key comes off before hashing. */
|
|
54
|
+
async #hashableContent(file: string) {
|
|
55
|
+
const content = await this.#lib.workspace.readFile(file);
|
|
56
|
+
if (file !== `${this.#prefix}package.json`) return content;
|
|
57
|
+
const manifest = JSON.parse(content) as PackageJson;
|
|
58
|
+
delete manifest[LibSource.manifestKey];
|
|
59
|
+
return JSON.stringify(manifest);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Hash over the library's own files. Untracked files count: a freshly copied library is not committed
|
|
64
|
+
* yet, and its hash has to be the same one a later `status` recomputes.
|
|
65
|
+
*/
|
|
66
|
+
async computeHash() {
|
|
67
|
+
const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
|
|
68
|
+
(file) => this.#isHashed(file),
|
|
69
|
+
);
|
|
70
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
hasher.update(file);
|
|
73
|
+
hasher.update("\0");
|
|
74
|
+
hasher.update(await this.#hashableContent(file));
|
|
75
|
+
hasher.update("\0");
|
|
76
|
+
}
|
|
77
|
+
return hasher.digest("hex").slice(0, 32);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async read(): Promise<LibSourceStamp | null> {
|
|
81
|
+
const manifest = await this.#lib.getPackageJson();
|
|
82
|
+
const akan = manifest[LibSource.manifestKey] as { source?: LibSourceStamp } | undefined;
|
|
83
|
+
return akan?.source ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async write({ origin, sha }: Pick<LibSourceStamp, "origin" | "sha">) {
|
|
87
|
+
const [manifest, hash] = await Promise.all([this.#lib.getPackageJson(), this.computeHash()]);
|
|
88
|
+
const akan = (manifest[LibSource.manifestKey] ?? {}) as Record<string, unknown>;
|
|
89
|
+
const stamp: LibSourceStamp = { origin, sha, hash, syncedAt: new Date().toISOString() };
|
|
90
|
+
await this.#lib.setPackageJson({ ...manifest, [LibSource.manifestKey]: { ...akan, source: stamp } });
|
|
91
|
+
return stamp;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Writes the stamp only when it would change. `syncedAt` moves on every write, so an unconditional
|
|
96
|
+
* write leaves the manifest dirty and defeats an idempotent caller — the hash is computed with the
|
|
97
|
+
* stamp removed, so comparing it first is sound.
|
|
98
|
+
*/
|
|
99
|
+
async syncStamp({ origin, sha }: Pick<LibSourceStamp, "origin" | "sha">) {
|
|
100
|
+
const [current, hash] = await Promise.all([this.read(), this.computeHash()]);
|
|
101
|
+
if (current?.origin === origin && current.sha === sha && current.hash === hash)
|
|
102
|
+
return { stamp: current, changed: false };
|
|
103
|
+
return { stamp: await this.write({ origin, sha }), changed: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async status(): Promise<LibStatus> {
|
|
107
|
+
const [stamp, hash] = await Promise.all([this.read(), this.computeHash()]);
|
|
108
|
+
const drift = !stamp ? "unstamped" : stamp.hash === hash ? "clean" : "drifted";
|
|
109
|
+
return { lib: this.#lib.name, drift, stamp, hash };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatLibStatuses(statuses: LibStatus[]) {
|
|
114
|
+
const marks = { clean: "clean ", drifted: "DRIFTED ", unstamped: "unstamped" } as const;
|
|
115
|
+
const sections = [
|
|
116
|
+
"Akan Library Source Status",
|
|
117
|
+
"",
|
|
118
|
+
...statuses.map((status) => {
|
|
119
|
+
const origin = status.stamp ? `${status.stamp.origin}@${status.stamp.sha}` : "no akan.source in package.json";
|
|
120
|
+
return ` ${marks[status.drift]} libs/${status.lib} ${origin}`;
|
|
121
|
+
}),
|
|
122
|
+
"",
|
|
123
|
+
`drifted: ${statuses.filter((status) => status.drift === "drifted").length} / ${statuses.length}`,
|
|
124
|
+
];
|
|
125
|
+
return sections.join("\n");
|
|
126
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# grit rule fixtures
|
|
2
|
+
|
|
3
|
+
One folder per `lint/*.grit` rule. `gritRules.test.ts` runs biome with that one plugin enabled and asserts
|
|
4
|
+
`bad` reports **exactly** the marked lines and `good` reports nothing — so a rule that silently stops
|
|
5
|
+
matching after a biome upgrade fails the suite instead of going quiet, and `every rule in this folder has a
|
|
6
|
+
fixture` fails when a rule is added without one.
|
|
7
|
+
|
|
8
|
+
## Contract
|
|
9
|
+
|
|
10
|
+
- One case per line. `// @flag` marks a line the rule must report in `bad`; `// @ok` marks a line in `good`.
|
|
11
|
+
- The `bad` assertion is set equality, not containment: a marked line nobody reported is a pattern that
|
|
12
|
+
stopped matching, and a reported line nobody marked is the rule reaching further than the fixture claims.
|
|
13
|
+
- `good` is the half that matters most: an over-matching rule is what makes developers distrust the gate.
|
|
14
|
+
- `fixture.json` is optional:
|
|
15
|
+
- `path` — where the source is written inside the temp workspace, for a rule that reads `$filename`
|
|
16
|
+
(`no-deep-internal-import`, `no-redeclare-predefined-endpoint`). Its extension also picks which of
|
|
17
|
+
`bad.ts` / `bad.tsx` is read.
|
|
18
|
+
- `expect: "file"` — for a rule whose span is the whole file (`no-bang-comment-in-client`), where there is
|
|
19
|
+
no per-line diagnostic to match. `bad` must report at least once.
|
|
20
|
+
- A rule with two independent gates gets one subfolder per gate (`no-init-fetch-in-client/{use-client,store-file}`).
|
|
21
|
+
- These files are excluded from the repo's own biome run (root `biome.json` `files.includes`) and from
|
|
22
|
+
`tsc` (`tsconfig.json` `exclude`), because they reference undeclared identifiers on purpose and the
|
|
23
|
+
formatter would reorder the class strings the vocabulary fixtures test.
|
|
24
|
+
|
|
25
|
+
## Biome GritQL constraints these fixtures pinned down
|
|
26
|
+
|
|
27
|
+
Each of these made a rule silently report nothing, and each was found by adding the fixture:
|
|
28
|
+
|
|
29
|
+
- A bare snippet in JSX-attribute shape (`` `$name={$value}` ``) matches **no** JsxAttribute. Match the node
|
|
30
|
+
and read its fields: `JsxAttribute(name = $name, initializer = ...)`.
|
|
31
|
+
- `JsModule()` never matches as a top-level pattern, and no node pattern is given comment trivia. A rule
|
|
32
|
+
about comments has to go through `file($name, $body)`, whose span is the whole file.
|
|
33
|
+
- `file()`'s `$body` is the module's **token** span: interior trivia is visible, the leading and trailing
|
|
34
|
+
trivia are not. So a comment above the first statement or below the last one is unreachable from GritQL —
|
|
35
|
+
that is `no-bang-comment-in-client`'s blind spot, and why its fixture puts the marker between statements.
|
|
36
|
+
- A regex capture (`r"..."($var)`) carries no source range: a diagnostic spanned on one gets no location,
|
|
37
|
+
and `$var` does not interpolate into a `message`.
|
|
38
|
+
|
|
39
|
+
The five vocabulary fixtures were ported from `frontendBuild/styleGuard.test.ts`, the pre-grit
|
|
40
|
+
implementation of the same rules, when that scanner was retired.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const CssVariableShorthand = () => <div className="bg-[--brand]" />; // @ok
|
|
2
|
+
export const CssVariableFunction = () => <div className="text-[var(--fg)]" />; // @ok
|
|
3
|
+
export const ArbitrarySizeNotColor = () => <div className="min-h-[300px] w-[42ch]" />; // @ok
|
|
4
|
+
export const SemanticToken = () => <div className="bg-primary" />; // @ok
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const Card = async () => <div className="flex" />; // @flag
|
|
2
|
+
export const Typed: FC = async () => <div className="flex" />; // @flag
|
|
3
|
+
export async function Panel() { return <div className="flex" />; } // @flag
|
|
4
|
+
export default async function Shell() { return <div className="flex" />; } // @flag
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const Card = () => <div className="flex" />; // @ok
|
|
2
|
+
export const Handler = () => { const onSave = async () => save(); return <button onClick={onSave} />; }; // @ok
|
|
3
|
+
export const loadThing = async () => await fetchThing(); // @ok
|
|
4
|
+
export function Panel() { return <div className="flex" />; } // @ok
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "expect": "file" }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const CompoundButton = () => <button className="btn-primary" />; // @flag
|
|
2
|
+
export const CompoundBadge = () => <span className="badge-success" />; // @flag
|
|
3
|
+
export const DroppedBaseSurface = () => <div className="bg-base-100" />; // @flag
|
|
4
|
+
export const DroppedBaseContentWithOpacity = () => <div className="text-base-content/70" />; // @flag
|
|
5
|
+
export const DroppedBaseOnABorderSide = () => <div className="border-t-base-300" />; // @flag
|
|
6
|
+
export const DroppedPrimaryContent = () => <div className="text-primary-content" />; // @flag
|
|
7
|
+
export const DroppedErrorSlot = () => <div className="bg-error/5" />; // @flag
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const BareAmbiguousNamesCollideWithTailwind = () => <div className="card input badge btn" />; // @ok
|
|
2
|
+
export const SurvivingSemanticPair = () => <div className="bg-primary text-primary-foreground" />; // @ok
|
|
3
|
+
export const SurvivingStatusTokens = () => <div className="bg-neutral text-info border-warning" />; // @ok
|
|
4
|
+
export const DestructiveReplacesError = () => <div className="bg-destructive/10 text-destructive" />; // @ok
|
|
5
|
+
export const UnrelatedContentUtilities = () => <div className="content-center justify-content" />; // @ok
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "path": "apps/demo/lib/task/Task.Zone.tsx" }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const infoIsTheReplacement = () => logger.info("x"); // @ok
|
|
2
|
+
export const otherLevelsAreFine = () => logger.warn("x"); // @ok
|
|
3
|
+
export const anUnrelatedReceiverIsNotTheLadder = () => auditTrail.log("x"); // @ok
|
|
4
|
+
export const consoleIsBiomesOwnRule = () => console.log("x"); // @ok
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TaskStore } from "./task.store"; // @flag
|
|
2
|
+
import { Card } from "./Task.Unit"; // @flag
|
|
3
|
+
import { Button } from "@libs/shared/ui"; // @flag
|
|
4
|
+
import { useSpeech } from "@libs/util/webkit"; // @flag
|
|
5
|
+
import { cn } from "akanjs/client"; // @flag
|
|
6
|
+
import { st } from "../st"; // @flag
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TaskService } from "./task.service"; // @flag
|
|
2
|
+
import { TaskModel } from "./task.document"; // @flag
|
|
3
|
+
import { Admin } from "@libs/shared/srvkit"; // @flag
|
|
4
|
+
import { AkanServer } from "akanjs/server"; // @flag
|
|
5
|
+
import * as db from "../db"; // @flag
|
|
6
|
+
import { Err } from "../dict"; // @flag
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TaskStore { async reload(orgId: string) { const snapshot = await fetch.initTaskInOrg(orgId); } } // @flag
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "path": "apps/demo/lib/task/task.store.ts" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TaskStore { async reload() { await st.do.initTaskInOrg(); } } // @ok
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const SizeFromRuntimeExpression = () => <div className={`min-h-[${minHeight}px] flex`} />; // @flag
|
|
2
|
+
export const ColorFromRuntimeExpression = () => <div className={`bg-[${color}] w-full`} />; // @flag
|
|
3
|
+
export const BrokenBracketSwallowsNextClass = () => <div className={`min-h-[ w-full${minHeight}px] flex`} />; // @flag
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const LiteralArbitraryValue = () => <div className="min-h-[300px] flex" />; // @ok
|
|
2
|
+
export const InterpolationOutsideBrackets = () => <div className={`flex gap-2 ${isOpen ? "opacity-50" : ""}`} />; // @ok
|
|
3
|
+
export const InterpolationInStylePropNotClass = () => <div style={{ minHeight }} className={`flex ${extra}`} />; // @ok
|