@akanjs/devkit 3.0.0-alpha.11 → 3.0.0-alpha.111
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
|
@@ -3,9 +3,9 @@ language js(typescript, jsx)
|
|
|
3
3
|
|
|
4
4
|
// Vocabulary closure (`@theme { --color-*: initial }` in akanjs/ui/styles.css) strips the raw
|
|
5
5
|
// Tailwind palette, so classes like `bg-blue-500` generate NO css and silently render as
|
|
6
|
-
// currentColor/transparent. This rule surfaces that silent breakage at lint time.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// currentColor/transparent. This rule surfaces that silent breakage at lint time. Matching
|
|
7
|
+
// string/template nodes (not raw text) skips comments for free.
|
|
8
|
+
// Cases: lint/__fixtures__/no-raw-palette-class/{bad,good}.tsx
|
|
9
9
|
//
|
|
10
10
|
// Bare `bg-neutral` is a semantic token and allowed; the numeric suffix (`bg-neutral-500`)
|
|
11
11
|
// is what marks a raw palette class. black/white stay in the vocabulary for overlays.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// A form control publishes its own setter by reading the action off the function it was handed
|
|
5
|
+
// (`actionTagOf` in pkgs/akanjs/store/actionTag.ts). A setter passed by reference names the field it
|
|
6
|
+
// writes, so the control emits `data-akan-action` and `useFieldTool` publishes the field to the in-page
|
|
7
|
+
// agent. An arrow that only forwards its argument is a fresh anonymous closure carrying neither, so the
|
|
8
|
+
// field silently becomes unreachable to the agent, to an E2E selector, and to the accessibility tree —
|
|
9
|
+
// while looking identical to a reader. Generated field setters take exactly one value
|
|
10
|
+
// (`makeFormSetter` in pkgs/akanjs/store/action.ts), so the wrapper never changes what runs.
|
|
11
|
+
//
|
|
12
|
+
// Only a pure forwarding body is reported: the argument reaches the setter unchanged, as the sole
|
|
13
|
+
// statement. Each other shape has its own home, so none of them is reported here:
|
|
14
|
+
//
|
|
15
|
+
// normalize `(v) => set(formatPhone(v))` -> the control's own `transform` prop, which every text and
|
|
16
|
+
// number `Field.*` already takes. Keep `onChange` a reference.
|
|
17
|
+
// composite `(v) => { set(v); other(v); }` -> a `_postSet<Field>` method on the store, with the generated
|
|
18
|
+
// setter left on the control. A generated action cannot be
|
|
19
|
+
// overridden: mapped types make them properties (TS2425).
|
|
20
|
+
// nested `(v) => writeOnX('a.3.b', v)` -> unannotatable by design. An agent reaches an embedded row
|
|
21
|
+
// through `fill<Model>Form`, which waves composites through.
|
|
22
|
+
//
|
|
23
|
+
// A typed parameter (`(v: string) => st.do.setVOnX(v)`) is not matched: the parameter and the argument
|
|
24
|
+
// bind to different text, so the metavariable cannot unify them. Under-reporting, not a false positive.
|
|
25
|
+
//
|
|
26
|
+
JsxAttribute() as $attr where {
|
|
27
|
+
$attr <: contains JsArrowFunctionExpression() as $arrow,
|
|
28
|
+
$arrow <: `($p) => $body`,
|
|
29
|
+
// One bare parameter. A metavariable matches a whole parameter list leniently, so `(a, b)` binds the
|
|
30
|
+
// text "a, b" and is excluded here rather than by the snippet.
|
|
31
|
+
$p <: r"[A-Za-z_$][A-Za-z0-9_$]*",
|
|
32
|
+
$body <: contains `st.do.$action($p)` as $call,
|
|
33
|
+
$action <: r"set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*",
|
|
34
|
+
// The body is that call and nothing else — `r"..."` is a full match, so an extra statement fails it.
|
|
35
|
+
$body <: r"\{?\s*(?:void\s+|await\s+)?st\.do\.[A-Za-z0-9_$]+\([A-Za-z_$][A-Za-z0-9_$]*\)\s*;?\s*\}?",
|
|
36
|
+
register_diagnostic(
|
|
37
|
+
span = $arrow,
|
|
38
|
+
message = "This arrow only forwards its argument, and an anonymous closure names no action — the control publishes no agent tool and emits no data-akan-action for the field. Pass the setter by reference: onChange={st.do.setTypeOnTicket}. A wrapper that does more is not reported: normalization belongs in the control's transform prop, a multi-write belongs in a _postSet<Field> hook on the store, and a nested path through writeOnX is reached by the agent through fill<Model>Form.",
|
|
39
|
+
severity = "error"
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// A function prop cannot cross the RSC boundary: a closure has no serialized form, so a server component
|
|
5
|
+
// handing one down fails at the boundary rather than at the call. `loader`, `render` and `of` are the
|
|
6
|
+
// framework's own slots, which take the function on the client side of the boundary.
|
|
7
|
+
//
|
|
8
|
+
// The attribute is matched by node fields rather than by a `$name={$value}` snippet: Biome's GritQL does
|
|
9
|
+
// not parse a bare snippet in that shape as a JsxAttribute, so the snippet form matches nothing at all.
|
|
10
|
+
// Reading `expression` off the initializer is also what keeps the value anchored to this attribute — a
|
|
11
|
+
// `contains` would reach into nested JSX and report `item={item}` inside `render={(item) => <Row item={item} />}`.
|
|
12
|
+
JsxAttribute(
|
|
13
|
+
name = $name,
|
|
14
|
+
initializer = JsxAttributeInitializerClause(value = JsxExpressionAttributeValue(expression = $value))
|
|
15
|
+
) where {
|
|
16
|
+
$value <: or { JsArrowFunctionExpression(), JsFunctionExpression() },
|
|
8
17
|
not $name <: or { `loader`, `render`, `of` },
|
|
9
18
|
register_diagnostic(
|
|
10
19
|
span = $name,
|
package/linter.test.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { type BiomeReport, parseBiomeReport } from "./linter";
|
|
3
|
+
|
|
4
|
+
const reportOf = (diagnostics: BiomeReport["diagnostics"]): string =>
|
|
5
|
+
JSON.stringify({ summary: { errors: diagnostics?.length ?? 0, warnings: 0 }, diagnostics });
|
|
6
|
+
|
|
7
|
+
const oneDiagnostic: BiomeReport["diagnostics"] = [
|
|
8
|
+
{
|
|
9
|
+
severity: "error",
|
|
10
|
+
message: "Unexpected any",
|
|
11
|
+
category: "lint/suspicious/noExplicitAny",
|
|
12
|
+
location: { path: "apps/demo/ui/Card.tsx", start: { line: 3, column: 10 } },
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("parseBiomeReport", () => {
|
|
17
|
+
test("reads a report that is the whole output", () => {
|
|
18
|
+
const report = parseBiomeReport(reportOf(oneDiagnostic));
|
|
19
|
+
expect(report.diagnostics).toHaveLength(1);
|
|
20
|
+
expect(report.diagnostics?.[0]?.category).toBe("lint/suspicious/noExplicitAny");
|
|
21
|
+
expect(report.summary?.errors).toBe(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("reads a report behind leading text", () => {
|
|
25
|
+
const output = `Skipped 3 files.\n${reportOf(oneDiagnostic)}`;
|
|
26
|
+
expect(parseBiomeReport(output).diagnostics).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("reads a report behind leading text that carries a brace", () => {
|
|
30
|
+
// The whole reason the first-brace-to-last-brace slice was wrong: Biome prints configuration and IO
|
|
31
|
+
// diagnostics onto the same stream as the report, and those messages quote source and config.
|
|
32
|
+
const output = [
|
|
33
|
+
"configuration/deserialize: unknown key `overrides[0].includes { }`",
|
|
34
|
+
" the file `biome.jsonc` cannot be read",
|
|
35
|
+
reportOf(oneDiagnostic),
|
|
36
|
+
].join("\n");
|
|
37
|
+
expect(parseBiomeReport(output).diagnostics).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("skips a complete JSON object in leading text that is not a report", () => {
|
|
41
|
+
const output = `hint: try {"linter":{"enabled":true}} in your config\n${reportOf(oneDiagnostic)}`;
|
|
42
|
+
const report = parseBiomeReport(output);
|
|
43
|
+
expect(report.diagnostics).toHaveLength(1);
|
|
44
|
+
expect(report.summary?.errors).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("reads a report followed by trailing text", () => {
|
|
48
|
+
const output = `${reportOf(oneDiagnostic)}\nChecked 12 files in 40ms. Found 1 error.`;
|
|
49
|
+
expect(parseBiomeReport(output).diagnostics).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("keeps a diagnostic whose message contains braces and quotes", () => {
|
|
53
|
+
const message = 'Replace {" a "} with { " a " } — the `{}` object syntax is not allowed';
|
|
54
|
+
const report = parseBiomeReport(reportOf([{ severity: "error", message }]));
|
|
55
|
+
expect(report.diagnostics?.[0]?.message).toBe(message);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("keeps a diagnostic whose message ends in an escaped backslash before the closing quote", () => {
|
|
59
|
+
const message = "path separator is \\\\";
|
|
60
|
+
const report = parseBiomeReport(reportOf([{ severity: "warning", message }]));
|
|
61
|
+
expect(report.diagnostics?.[0]?.message).toBe(message);
|
|
62
|
+
expect(report.diagnostics).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("accepts a report carrying only a summary", () => {
|
|
66
|
+
expect(parseBiomeReport('{"summary":{"errors":0,"warnings":0}}').summary?.errors).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("throws the output when it holds no JSON at all", () => {
|
|
70
|
+
expect(() => parseBiomeReport("biome: command not found\n")).toThrow("biome: command not found");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("throws the output when the only brace never closes", () => {
|
|
74
|
+
expect(() => parseBiomeReport('internal error: {"diagnostics":[')).toThrow("internal error");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("throws a named error for empty output", () => {
|
|
78
|
+
expect(() => parseBiomeReport(" \n")).toThrow("No Biome JSON output");
|
|
79
|
+
});
|
|
80
|
+
});
|
package/linter.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
|
|
6
|
-
type BiomeSeverity = "error" | "warning" | "information" | "hint";
|
|
6
|
+
export type BiomeSeverity = "error" | "warning" | "information" | "hint";
|
|
7
7
|
|
|
8
|
-
interface BiomePosition {
|
|
8
|
+
export interface BiomePosition {
|
|
9
9
|
line: number;
|
|
10
10
|
column: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
interface BiomeDiagnostic {
|
|
13
|
+
export interface BiomeDiagnostic {
|
|
14
14
|
severity: BiomeSeverity;
|
|
15
15
|
message: string;
|
|
16
16
|
category?: string;
|
|
@@ -21,7 +21,7 @@ interface BiomeDiagnostic {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
interface BiomeReport {
|
|
24
|
+
export interface BiomeReport {
|
|
25
25
|
summary?: {
|
|
26
26
|
changed?: number;
|
|
27
27
|
errors?: number;
|
|
@@ -58,6 +58,56 @@ interface LintResponse {
|
|
|
58
58
|
warnings: LintMessage[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Extracts the `--reporter=json` report from Biome's output.
|
|
63
|
+
*
|
|
64
|
+
* Biome mixes configuration and IO diagnostics into the same stream as the report, so the output is not
|
|
65
|
+
* always JSON alone. Slicing from the first `{` to the last `}` mis-parses as soon as that leading text
|
|
66
|
+
* carries a brace of its own, which is not hypothetical: a config error printed ahead of the report made
|
|
67
|
+
* one unchanged command alternate between 2 and 10 diagnostics. So every `{` is tried as a start, its
|
|
68
|
+
* match is found by depth while string literals are skipped, and the first candidate that both parses and
|
|
69
|
+
* carries a report field wins — a brace in prose parses as nothing, and `{"a":1}` in prose is not a report.
|
|
70
|
+
*/
|
|
71
|
+
export const parseBiomeReport = (output: string): BiomeReport => {
|
|
72
|
+
for (let start = output.indexOf("{"); start !== -1; start = output.indexOf("{", start + 1)) {
|
|
73
|
+
const end = objectEndAt(output, start);
|
|
74
|
+
if (end === -1) break;
|
|
75
|
+
const report = asBiomeReport(output.slice(start, end + 1));
|
|
76
|
+
if (report) return report;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(output.trim() || "No Biome JSON output");
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const objectEndAt = (output: string, start: number): number => {
|
|
82
|
+
let depth = 0;
|
|
83
|
+
let inString = false;
|
|
84
|
+
let escaped = false;
|
|
85
|
+
for (let idx = start; idx < output.length; idx += 1) {
|
|
86
|
+
const char = output[idx];
|
|
87
|
+
if (inString) {
|
|
88
|
+
if (escaped) escaped = false;
|
|
89
|
+
else if (char === "\\") escaped = true;
|
|
90
|
+
else if (char === '"') inString = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"') inString = true;
|
|
94
|
+
else if (char === "{") depth += 1;
|
|
95
|
+
else if (char === "}" && --depth === 0) return idx;
|
|
96
|
+
}
|
|
97
|
+
return -1;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const asBiomeReport = (candidate: string): BiomeReport | null => {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(candidate) as unknown;
|
|
103
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
104
|
+
if (!("diagnostics" in parsed) && !("summary" in parsed)) return null;
|
|
105
|
+
return parsed as BiomeReport;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
61
111
|
/** Biome reads `biome.json` first and `biome.jsonc` second; only the latter may carry comments. */
|
|
62
112
|
const BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"] as const;
|
|
63
113
|
|
|
@@ -123,14 +173,6 @@ export class Linter {
|
|
|
123
173
|
});
|
|
124
174
|
}
|
|
125
175
|
|
|
126
|
-
#parseBiomeReport(output: string): BiomeReport {
|
|
127
|
-
const jsonStart = output.indexOf("{");
|
|
128
|
-
const jsonEnd = output.lastIndexOf("}");
|
|
129
|
-
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart)
|
|
130
|
-
throw new Error(output.trim() || "No Biome JSON output");
|
|
131
|
-
return JSON.parse(output.slice(jsonStart, jsonEnd + 1)) as BiomeReport;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
176
|
#diagnosticFilePath(diagnostic: BiomeDiagnostic, fallbackFilePath: string) {
|
|
135
177
|
const diagnosticPath = diagnostic.location?.path;
|
|
136
178
|
if (!diagnosticPath) return fallbackFilePath;
|
|
@@ -208,7 +250,7 @@ export class Linter {
|
|
|
208
250
|
this.configPath,
|
|
209
251
|
this.#toBiomePath(filePath),
|
|
210
252
|
]);
|
|
211
|
-
const report =
|
|
253
|
+
const report = parseBiomeReport(stdout || stderr);
|
|
212
254
|
const results = this.#toLintResults(report, filePath);
|
|
213
255
|
const { errors, warnings } = this.#splitMessages(results);
|
|
214
256
|
const output = write && existsSync(filePath) ? readFileSync(filePath, "utf8") : undefined;
|
|
@@ -414,6 +456,33 @@ export class Linter {
|
|
|
414
456
|
* @param filePath - Path to the file
|
|
415
457
|
* @returns Biome configuration object
|
|
416
458
|
*/
|
|
459
|
+
/**
|
|
460
|
+
* Applies Biome's safe fixes to many files in one process.
|
|
461
|
+
*
|
|
462
|
+
* One spawn, not one per file: Biome's startup dominates a small file, so the per-file path turns a
|
|
463
|
+
* fifteen-file scaffold into three seconds of process launches. Paths that do not exist are dropped
|
|
464
|
+
* rather than thrown on, because the caller is usually reporting what a template just wrote and a
|
|
465
|
+
* template is allowed to decline a file.
|
|
466
|
+
*/
|
|
467
|
+
async fixFiles(filePaths: string[]): Promise<{ fixed: string[] }> {
|
|
468
|
+
const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync);
|
|
469
|
+
if (resolved.length === 0) return { fixed: [] };
|
|
470
|
+
const before = new Map(resolved.map((filePath) => [filePath, readFileSync(filePath, "utf8")]));
|
|
471
|
+
await this.#runBiome([
|
|
472
|
+
"check",
|
|
473
|
+
"--write",
|
|
474
|
+
"--reporter=json",
|
|
475
|
+
"--max-diagnostics=none",
|
|
476
|
+
"--no-errors-on-unmatched",
|
|
477
|
+
"--config-path",
|
|
478
|
+
this.configPath,
|
|
479
|
+
...resolved.map((filePath) => this.#toBiomePath(filePath)),
|
|
480
|
+
]);
|
|
481
|
+
return {
|
|
482
|
+
fixed: resolved.filter((filePath) => readFileSync(filePath, "utf8") !== before.get(filePath)),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
417
486
|
async getConfigForFile(filePath: string): Promise<unknown> {
|
|
418
487
|
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
419
488
|
if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
|
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.111",
|
|
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
|
".": {
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"./incrementalBuilder": "./incrementalBuilder/index.ts",
|
|
34
34
|
"./mobile": "./mobile/index.ts",
|
|
35
35
|
"./transforms": "./transforms/index.ts",
|
|
36
|
-
"./ui": "./ui/index.ts",
|
|
37
36
|
"./workflow": "./workflow/index.ts",
|
|
38
37
|
"./*.ts": "./*.ts",
|
|
39
38
|
"./*": "./*.ts"
|
|
@@ -45,15 +44,13 @@
|
|
|
45
44
|
"@langchain/openai": "^1.4.6",
|
|
46
45
|
"@tailwindcss/node": "^4.3.0",
|
|
47
46
|
"@trapezedev/project": "^7.1.4",
|
|
48
|
-
"akanjs": "3.0.0-alpha.
|
|
47
|
+
"akanjs": "3.0.0-alpha.111",
|
|
49
48
|
"chalk": "^5.6.2",
|
|
50
49
|
"commander": "^14.0.3",
|
|
51
50
|
"dayjs": "^1.11.20",
|
|
52
51
|
"fontaine": "^0.8.0",
|
|
53
52
|
"fonteditor-core": "^2.6.3",
|
|
54
53
|
"ignore": "^7.0.5",
|
|
55
|
-
"ink": "^6.8.0",
|
|
56
|
-
"js-yaml": "^4.1.1",
|
|
57
54
|
"ora": "^9.4.0",
|
|
58
55
|
"ssh2": "^1.17.0",
|
|
59
56
|
"subset-font": "^2.5.0",
|
|
@@ -63,7 +60,7 @@
|
|
|
63
60
|
},
|
|
64
61
|
"peerDependencies": {
|
|
65
62
|
"@capacitor/cli": "^8.3.4",
|
|
66
|
-
"react": "19.2.
|
|
63
|
+
"react": "19.2.7"
|
|
67
64
|
},
|
|
68
65
|
"peerDependenciesMeta": {
|
|
69
66
|
"@capacitor/cli": {
|
|
@@ -77,4 +74,4 @@
|
|
|
77
74
|
"bun": {
|
|
78
75
|
"platform": "bun"
|
|
79
76
|
}
|
|
80
|
-
}
|
|
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({
|