@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +70 -14
- package/akanMcpContract.ts +25 -4
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +340 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +132 -5
- package/executors.ts +89 -14
- package/fileEditor.ts +19 -19
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
- package/index.ts +0 -5
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +109 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +6 -6
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
|
@@ -0,0 +1,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
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface IdProps { taskId: string; } // @ok
|
|
2
|
+
export interface EnumProps { role: cnst.AdminRole["value"]; } // @ok
|
|
3
|
+
export interface InitProps { init: ClientInit<cnst.Task>; } // @ok
|
|
4
|
+
export interface CallbackProps { onPick: (task: cnst.Task) => void; } // @ok
|
|
5
|
+
export interface ModelsWrapperProps { models: ModelsProps<cnst.Task>; } // @ok
|
|
6
|
+
export const localAnnotationStaysInFile = (raw: unknown) => raw as cnst.Task; // @ok
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const TwoInOneString = () => <div className="bg-blue-500 text-gray-700" />; // @flag
|
|
2
|
+
export const VariantPrefixAndOpacity = () => <div className="hover:bg-red-500/50" />; // @flag
|
|
3
|
+
export const NumericNeutral = () => <div className="bg-neutral-500" />; // @flag
|
|
4
|
+
export const AlongsideAUrlString = () => <a href="https://x.io" className="bg-blue-500" />; // @flag
|
|
5
|
+
export const BorderSide = () => <div className="border-t-emerald-300" />; // @flag
|
|
6
|
+
export const InsideCn = () => <div className={cn("flex", "text-slate-400")} />; // @flag
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const BareSemanticNeutral = () => <div className="bg-neutral text-neutral-foreground" />; // @ok
|
|
2
|
+
export const SemanticTokens = () => <div className="bg-primary text-muted-foreground border-border" />; // @ok
|
|
3
|
+
export const BlackAndWhiteStayInVocabulary = () => <div className="bg-black text-white bg-white/30 bg-black/50" />; // @ok
|
|
4
|
+
export const NonColorNumericUtilities = () => <div className="gap-4 mt-2 grid-cols-3 w-500" />; // @ok
|
|
5
|
+
export const OpacityOnASemanticToken = () => <div className="text-foreground/70 bg-primary/10" />; // @ok
|
|
6
|
+
// iconClassName="bg-blue-500" — a class name in a line comment is not code
|
|
7
|
+
/** legacy: bg-red-500 */
|
|
8
|
+
export const CommentsAreNotCode = () => <div className="flex" />; // @ok
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export class TaskEndpoint extends endpoint(srv.task, ({ query, mutation }) => ({
|
|
2
|
+
task: query(cnst.Task).exec(() => null), // @flag
|
|
3
|
+
lightTask: query(cnst.Task).exec(() => null), // @flag
|
|
4
|
+
createTask: mutation(cnst.Task).exec(() => null), // @flag
|
|
5
|
+
updateTask: mutation(cnst.Task).exec(() => null), // @flag
|
|
6
|
+
removeTask: mutation(cnst.Task).exec(() => null), // @flag
|
|
7
|
+
viewTask: query(cnst.Task).exec(() => null), // @flag
|
|
8
|
+
editTask: query(cnst.Task).exec(() => null), // @flag
|
|
9
|
+
mergeTask: mutation(cnst.Task).exec(() => null), // @flag
|
|
10
|
+
})) {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "path": "apps/demo/lib/task/task.signal.ts" }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export class TaskEndpoint extends endpoint(srv.task, ({ query, mutation }) => ({
|
|
2
|
+
startTask: mutation(cnst.Task).exec(() => null), // @ok
|
|
3
|
+
taskSummary: query(cnst.Task).exec(() => null), // @ok
|
|
4
|
+
viewTaskBoard: query(cnst.Task).exec(() => null), // @ok
|
|
5
|
+
createTaskComment: mutation(cnst.Task).exec(() => null), // @ok
|
|
6
|
+
})) {}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export class CStore extends store(sig.c, () => ({})) { load() { this.set({ n: 1 }); } } // @ok
|
|
2
|
+
export class DStore extends store(sig.d, () => ({})) { load() { if (!this.get()) return; this.set({ n: 1 }); } } // @ok
|
|
3
|
+
export class EStore extends store(sig.e, () => ({})) { static helper() { return 1; } } // @ok
|
|
4
|
+
export class FStore extends store(sig.f, () => ({})) { get total() { return 1; } } // @ok
|
|
5
|
+
export class GStore extends store(sig.g, () => ({})) { load() { items.map((x) => { return x; }); } } // @ok
|
|
6
|
+
export class HPlain { load() { return 1; } } // @ok
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const throwsNewError = () => { throw new Error("boom"); }; // @flag
|
|
2
|
+
export const throwsBareError = () => { throw Error("boom"); }; // @flag
|
|
3
|
+
export const throwsWithCause = () => { throw new Error("boom", { cause: reason }); }; // @flag
|
|
4
|
+
export const throwsWithNoArgument = () => { throw new Error(); }; // @flag
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const throwsErr = () => { throw new Err("ticket.error.notFound"); }; // @ok
|
|
2
|
+
export const throwsASubclass = () => { throw new HttpError("boom"); }; // @ok
|
|
3
|
+
export const constructsWithoutThrowing = () => new Error("boom"); // @ok
|
|
4
|
+
export const rethrowsACaught = () => { try { run(); } catch (error) { throw error; } }; // @ok
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const A = () => <Field.Text onChange={(v) => st.do.setNameOnTask(v)} />; // @flag
|
|
2
|
+
export const B = () => <Switch onChange={(value) => void st.do.setDoneOnTask(value)} />; // @flag
|
|
3
|
+
export const C = () => <Select onChange={(v) => { st.do.setTypeOnTask(v); }} />; // @flag
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const D = () => <Field.Text onChange={st.do.setNameOnTask} />; // @ok
|
|
2
|
+
export const E = () => <Field.Text onChange={(v) => st.do.setNameOnTask(formatPhone(v))} />; // @ok
|
|
3
|
+
export const F = () => <Field.Text onChange={(v) => { st.do.setNameOnTask(v); track(v); }} />; // @ok
|
|
4
|
+
export const G = () => <Field.Text onChange={(v) => st.do.writeOnTask("rows.3.name", v)} />; // @ok
|
|
5
|
+
export const H = () => <Field.Text onChange={(a, b) => st.do.setNameOnTask(a)} />; // @ok
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const ArrowProp = () => <Button onClick={() => save()} />; // @flag
|
|
2
|
+
export const ArrowWithTwoParams = () => <Button onSelect={(id, kind) => save(id, kind)} />; // @flag
|
|
3
|
+
export const ArrowWithBareParam = () => <Button onChange={value => save(value)} />; // @flag
|
|
4
|
+
export const FunctionExpressionProp = () => <Button onClick={function () { save(); }} />; // @flag
|
|
5
|
+
export const NamedFunctionProp = () => <Button onClick={function handle() { save(); }} />; // @flag
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const LoaderIsAllowed = () => <Zone loader={() => fetchThing()} />; // @ok
|
|
2
|
+
export const OfIsAllowed = () => <Load of={() => fetchThing()} />; // @ok
|
|
3
|
+
export const ScalarProps = () => <Button label="Save" count={3} disabled />; // @ok
|
|
4
|
+
export const IdentifierProp = () => <Button onClick={handleSave} />; // @ok
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fixture tests for the GritQL plugins in this folder.
|
|
10
|
+
*
|
|
11
|
+
* These rules break the apps/libs build, and a rule that stops matching reports **zero diagnostics** rather
|
|
12
|
+
* than an error — so a biome upgrade can retire one silently. GritQL has known sharp edges here (escaped
|
|
13
|
+
* quotes, argument-position matching), which is what makes the quiet failure plausible rather than
|
|
14
|
+
* theoretical.
|
|
15
|
+
*
|
|
16
|
+
* Each rule runs alone, against a config this test writes, so a diagnostic can only have come from that
|
|
17
|
+
* plugin. The path scoping in `biome.base.json` is deliberately not exercised — what is under test is
|
|
18
|
+
* whether the pattern still matches. A rule that reads `$filename` needs a real path anyway, which is what
|
|
19
|
+
* a case's `fixture.json` supplies.
|
|
20
|
+
*/
|
|
21
|
+
const lintDir = import.meta.dir;
|
|
22
|
+
const fixturesDir = path.join(lintDir, "__fixtures__");
|
|
23
|
+
const workspaceRoot = path.resolve(lintDir, "../../../..");
|
|
24
|
+
const biomeBin = path.join(workspaceRoot, "node_modules/.bin/biome");
|
|
25
|
+
|
|
26
|
+
interface BiomeDiagnostic {
|
|
27
|
+
severity: string;
|
|
28
|
+
message: string;
|
|
29
|
+
category: string;
|
|
30
|
+
location?: { start?: { line?: number } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `path` is where the source is written inside the temp workspace, for a rule that gates on `$filename`;
|
|
35
|
+
* its extension also decides whether the file may hold JSX. `expect: "file"` is for a rule whose span is
|
|
36
|
+
* the whole module (`JsModule()`), where there is no per-line diagnostic to match.
|
|
37
|
+
*/
|
|
38
|
+
interface FixtureMeta {
|
|
39
|
+
path?: string;
|
|
40
|
+
expect?: "lines" | "file";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface FixtureCase {
|
|
44
|
+
rule: string;
|
|
45
|
+
name: string;
|
|
46
|
+
dir: string;
|
|
47
|
+
meta: FixtureMeta;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tempDirs: string[] = [];
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const runBiome = (args: string[], cwd: string) =>
|
|
57
|
+
new Promise<string>((resolve, reject) => {
|
|
58
|
+
const proc = spawn(biomeBin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
59
|
+
let stdout = "";
|
|
60
|
+
let stderr = "";
|
|
61
|
+
proc.stdout.on("data", (chunk: Buffer) => (stdout += chunk.toString()));
|
|
62
|
+
proc.stderr.on("data", (chunk: Buffer) => (stderr += chunk.toString()));
|
|
63
|
+
proc.on("error", reject);
|
|
64
|
+
proc.on("close", () => resolve(stdout || stderr));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** Every directory holding a `bad.*`, named `<rule>` or `<rule>/<case>`. */
|
|
68
|
+
const collectCases = async (): Promise<FixtureCase[]> => {
|
|
69
|
+
const cases: FixtureCase[] = [];
|
|
70
|
+
const walk = async (dir: string, rule: string, name: string) => {
|
|
71
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
72
|
+
if (entries.some((entry) => entry.isFile() && entry.name.startsWith("bad."))) {
|
|
73
|
+
const metaPath = path.join(dir, "fixture.json");
|
|
74
|
+
const meta = existsSync(metaPath) ? (JSON.parse(await readFile(metaPath, "utf-8")) as FixtureMeta) : {};
|
|
75
|
+
cases.push({ rule, name: name || rule, dir, meta });
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries.filter((entry) => entry.isDirectory()))
|
|
78
|
+
await walk(path.join(dir, entry.name), rule, `${rule}/${entry.name}`);
|
|
79
|
+
};
|
|
80
|
+
for (const entry of (await readdir(fixturesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()))
|
|
81
|
+
await walk(path.join(fixturesDir, entry.name), entry.name, "");
|
|
82
|
+
return cases.sort((left, right) => left.name.localeCompare(right.name));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const sourceName = (fixture: FixtureCase, kind: "bad" | "good") => {
|
|
86
|
+
const target = fixture.meta.path;
|
|
87
|
+
if (!target) return { read: `${kind}.tsx`, write: `${kind}.tsx` };
|
|
88
|
+
const ext = path.extname(target);
|
|
89
|
+
return { read: `${kind}${ext}`, write: target };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const readFixture = async (fixture: FixtureCase, kind: "bad" | "good") =>
|
|
93
|
+
await readFile(path.join(fixture.dir, sourceName(fixture, kind).read), "utf-8");
|
|
94
|
+
|
|
95
|
+
/** Diagnostics from one plugin against one fixture, keyed by the 1-based line each was reported on. */
|
|
96
|
+
const lintFixture = async (fixture: FixtureCase, kind: "bad" | "good") => {
|
|
97
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), `grit-${fixture.rule}-`));
|
|
98
|
+
tempDirs.push(dir);
|
|
99
|
+
// Every built-in rule off and the formatter silent, so the only thing left that can report is the plugin.
|
|
100
|
+
await writeFile(
|
|
101
|
+
path.join(dir, "biome.json"),
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
linter: { enabled: true, rules: { recommended: false } },
|
|
104
|
+
formatter: { enabled: false },
|
|
105
|
+
assist: { enabled: false },
|
|
106
|
+
plugins: [path.join(lintDir, `${fixture.rule}.grit`)],
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
const source = path.join(dir, sourceName(fixture, kind).write);
|
|
110
|
+
await mkdir(path.dirname(source), { recursive: true });
|
|
111
|
+
await writeFile(source, await readFixture(fixture, kind));
|
|
112
|
+
const output = await runBiome(
|
|
113
|
+
["check", "--reporter=json", "--max-diagnostics=none", "--no-errors-on-unmatched", `--config-path=${dir}`, source],
|
|
114
|
+
dir,
|
|
115
|
+
);
|
|
116
|
+
const start = output.indexOf("{");
|
|
117
|
+
const end = output.lastIndexOf("}");
|
|
118
|
+
if (start === -1 || end < start)
|
|
119
|
+
throw new Error(`biome produced no JSON for ${fixture.name}/${kind}: ${output.slice(0, 400)}`);
|
|
120
|
+
const diagnostics =
|
|
121
|
+
(JSON.parse(output.slice(start, end + 1)) as { diagnostics?: BiomeDiagnostic[] }).diagnostics ?? [];
|
|
122
|
+
return {
|
|
123
|
+
diagnostics,
|
|
124
|
+
lines: [...new Set(diagnostics.map((diagnostic) => diagnostic.location?.start?.line ?? 0))].sort(
|
|
125
|
+
(left, right) => left - right,
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** The 1-based line of every line carrying `marker`. One case per line is the fixture contract. */
|
|
131
|
+
const markedLines = async (fixture: FixtureCase, kind: "bad" | "good", marker: string) =>
|
|
132
|
+
(await readFixture(fixture, kind))
|
|
133
|
+
.split("\n")
|
|
134
|
+
.map((line, index) => (line.includes(marker) ? index + 1 : 0))
|
|
135
|
+
.filter((line) => line > 0);
|
|
136
|
+
|
|
137
|
+
const fixtures = await collectCases();
|
|
138
|
+
|
|
139
|
+
describe("grit lint rules", () => {
|
|
140
|
+
test("the harness found the biome binary and at least one fixture", () => {
|
|
141
|
+
expect(existsSync(biomeBin)).toBe(true);
|
|
142
|
+
expect(fixtures.length).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("every fixture folder names a rule that exists", () => {
|
|
146
|
+
for (const fixture of fixtures) expect(existsSync(path.join(lintDir, `${fixture.rule}.grit`))).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("every rule in this folder has a fixture", async () => {
|
|
150
|
+
const ruleFiles = (await readdir(lintDir))
|
|
151
|
+
.filter((name) => name.endsWith(".grit"))
|
|
152
|
+
.map((name) => name.slice(0, -5));
|
|
153
|
+
const covered = new Set(fixtures.map((fixture) => fixture.rule));
|
|
154
|
+
expect(ruleFiles.filter((rule) => !covered.has(rule))).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
for (const fixture of fixtures) {
|
|
158
|
+
test(`${fixture.name} flags exactly the marked lines in bad`, async () => {
|
|
159
|
+
const { lines, diagnostics } = await lintFixture(fixture, "bad");
|
|
160
|
+
for (const diagnostic of diagnostics) expect(diagnostic.category).toBe("plugin");
|
|
161
|
+
if (fixture.meta.expect === "file") {
|
|
162
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const expected = await markedLines(fixture, "bad", "// @flag");
|
|
166
|
+
expect(expected.length).toBeGreaterThan(0);
|
|
167
|
+
// Set equality, not containment: a marked line nobody reported is a pattern that stopped matching,
|
|
168
|
+
// and a reported line nobody marked is the rule reaching further than the fixture claims.
|
|
169
|
+
expect(lines).toEqual(expected);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test(`${fixture.name} leaves good clean`, async () => {
|
|
173
|
+
expect((await markedLines(fixture, "good", "// @ok")).length).toBeGreaterThan(0);
|
|
174
|
+
const { diagnostics } = await lintFixture(fixture, "good");
|
|
175
|
+
expect(diagnostics.map((diagnostic) => `${diagnostic.location?.start?.line}: ${diagnostic.message}`)).toEqual([]);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
@@ -3,7 +3,7 @@ language js(typescript, jsx)
|
|
|
3
3
|
|
|
4
4
|
// Arbitrary-value color literals (`bg-[#3b82f6]`, `text-[rgb(0,0,0)]`) bypass the semantic
|
|
5
5
|
// token layer, so they ignore data-theme switching and theme contrast validation. Grit port
|
|
6
|
-
//
|
|
6
|
+
// Cases: lint/__fixtures__/no-arbitrary-color/{bad,good}.tsx
|
|
7
7
|
// Variable references (`bg-[--brand]`, `text-[var(--fg)]`) do not match, by design.
|
|
8
8
|
or {
|
|
9
9
|
JsxString() as $s,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// React has no async client component: an async body is a server-only shape, so a `ui/` component that
|
|
5
|
+
// awaits stops rendering the moment any client parent imports it — and `ui/` is written to be reachable
|
|
6
|
+
// from both sides. It also moves the load below the route, where the framework can no longer start it
|
|
7
|
+
// before the first byte. Awaiting belongs to `page/**`, which passes the resolved data down as props.
|
|
8
|
+
//
|
|
9
|
+
// Component-ness is a PascalCase regex on the binding, the same test `no-model-type-in-util-zone.grit`
|
|
10
|
+
// uses. The declarator regex anchors `async` to the initializer position (`[^=]*` cannot cross an `=`),
|
|
11
|
+
// so an async handler declared inside a synchronous component is not read as the component's own body;
|
|
12
|
+
// `not within JsFunctionBody()` drops every nested declaration for the same reason. Out of scope by
|
|
13
|
+
// construction, since neither is a named binding at statement level: an inline `onClick={async () => …}`
|
|
14
|
+
// and an async member of an object literal.
|
|
15
|
+
JsIdentifierBinding() as $name where {
|
|
16
|
+
$name <: r"[A-Z][A-Za-z0-9_$]*",
|
|
17
|
+
not $name <: within JsFunctionBody(),
|
|
18
|
+
not $name <: within JsFormalParameter(),
|
|
19
|
+
or {
|
|
20
|
+
$name <: within JsVariableDeclarator() as $decl where {
|
|
21
|
+
$decl <: r"[A-Z][A-Za-z0-9_$]*\s*(?::[^=]*)?=\s*async[\s\S]*"
|
|
22
|
+
},
|
|
23
|
+
$name <: within JsFunctionDeclaration() as $fn where {
|
|
24
|
+
$fn <: r"(?:export\s+)?(?:default\s+)?async\s+function[\s\S]*"
|
|
25
|
+
},
|
|
26
|
+
$name <: within JsFunctionExportDefaultDeclaration() as $default where {
|
|
27
|
+
$default <: r"(?:export\s+)?(?:default\s+)?async\s+function[\s\S]*"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
register_diagnostic(
|
|
31
|
+
span = $name,
|
|
32
|
+
message = "An async component belongs in page/. React has no async client component, so a ui/ component that awaits breaks as soon as a client parent renders it, and the route can no longer start the load before the first byte. Await in the page — or hand an unawaited fetch.* to a Zone as an init / view prop — and take the resolved data as a prop.",
|
|
33
|
+
severity = "error"
|
|
34
|
+
)
|
|
35
|
+
}
|