@akanjs/devkit 3.0.0-alpha.71 → 3.0.0-alpha.73

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.
Files changed (117) hide show
  1. package/akanApp/BackendImportGraph.test.ts +120 -0
  2. package/akanApp/BackendImportGraph.ts +167 -0
  3. package/akanApp/akanApp.host.test.ts +16 -678
  4. package/akanApp/akanApp.host.ts +65 -562
  5. package/akanApp/devHostPolicy.test.ts +542 -0
  6. package/akanApp/devHostPolicy.ts +412 -0
  7. package/akanApp/index.ts +2 -0
  8. package/akanContext.ts +17 -3
  9. package/akanMcpContract.ts +25 -4
  10. package/artifact/routeSeedIndex.test.ts +1 -0
  11. package/capacitorApp.ts +10 -0
  12. package/cloud/globalConfig.ts +9 -2
  13. package/commandDecorators/command.ts +30 -6
  14. package/commandDecorators/commandBuilder.ts +20 -5
  15. package/commandDecorators/commandDecorators.test.ts +115 -3
  16. package/dependencyScanner.test.ts +99 -0
  17. package/dependencyScanner.ts +27 -24
  18. package/devkitUtils.test.ts +0 -104
  19. package/executors.test.ts +45 -0
  20. package/executors.ts +29 -5
  21. package/fileEditor.ts +19 -19
  22. package/frontendBuild/autoImportSync.test.ts +58 -0
  23. package/frontendBuild/autoImportSync.ts +4 -0
  24. package/frontendBuild/buildRouteClient.test.ts +19 -22
  25. package/frontendBuild/clientEntryDiscovery.ts +2 -2
  26. package/frontendBuild/fontOptimizer.ts +37 -18
  27. package/frontendBuild/index.ts +0 -1
  28. package/frontendBuild/styleContract.ts +14 -20
  29. package/frontendBuild/themeValidator.ts +22 -17
  30. package/incrementalBuilder/devWatchBatch.ts +6 -2
  31. package/incrementalBuilder/incrementalBuilder.proc.ts +6 -2
  32. package/index.ts +0 -5
  33. package/lint/__fixtures__/README.md +40 -0
  34. package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
  35. package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
  36. package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
  37. package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
  38. package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
  39. package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
  40. package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
  41. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
  42. package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
  43. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
  44. package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
  45. package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
  46. package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
  47. package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
  48. package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
  49. package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
  50. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
  51. package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
  52. package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
  53. package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
  54. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
  55. package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
  56. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
  57. package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
  58. package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
  59. package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
  60. package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
  61. package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
  62. package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
  63. package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
  64. package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
  65. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
  66. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
  67. package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
  68. package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
  69. package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
  70. package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
  71. package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
  72. package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
  73. package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
  74. package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
  75. package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
  76. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
  77. package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
  78. package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
  79. package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
  80. package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
  81. package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
  82. package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
  83. package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
  84. package/lint/gritRules.test.ts +178 -0
  85. package/lint/no-arbitrary-color.grit +1 -1
  86. package/lint/no-bang-comment-in-client.grit +23 -10
  87. package/lint/no-daisyui-legacy-class.grit +2 -2
  88. package/lint/no-inline-color.grit +10 -8
  89. package/lint/no-interpolated-arbitrary-class.grit +3 -3
  90. package/lint/no-raw-palette-class.grit +3 -3
  91. package/lint/non-scalar-props-restricted.grit +16 -7
  92. package/linter.test.ts +80 -0
  93. package/linter.ts +82 -13
  94. package/package.json +3 -3
  95. package/prompter.ts +9 -4
  96. package/qualityScanner.test.ts +64 -0
  97. package/qualityScanner.ts +43 -3
  98. package/recipeScanner.ts +4 -1
  99. package/scanInfo.ts +3 -3
  100. package/ssrScanner.test.ts +301 -0
  101. package/transforms/barrelImportsPlugin.ts +1 -1
  102. package/transforms/transforms.test.ts +5 -5
  103. package/transforms/tsconfigPackageResolver.test.ts +230 -0
  104. package/tsconfig.json +5 -1
  105. package/typeChecker.ts +1 -1
  106. package/types.ts +1 -0
  107. package/ui/ScrollList.tsx +6 -8
  108. package/uploadRelease.ts +2 -2
  109. package/workflow/executor.test.ts +146 -0
  110. package/builder.ts +0 -164
  111. package/extractDeps.ts +0 -86
  112. package/frontendBuild/styleGuard.test.ts +0 -180
  113. package/frontendBuild/styleGuard.ts +0 -339
  114. package/getCredentials.ts +0 -17
  115. package/getModelFileData.ts +0 -62
  116. package/src/capacitorApp.ts +0 -282
  117. package/streamAi.ts +0 -45
@@ -0,0 +1,3 @@
1
+ // FIXME: the client-reachable spelling of the same note // @ok
2
+ export const url = "https://host//!path"; // @ok
3
+ export const a = 1; // @ok
@@ -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,3 @@
1
+ import { userConstant } from "@libs/shared/lib/user/user.constant"; // @flag
2
+ import { helper } from "../../common/helper"; // @flag
3
+ import * as cnst from "../cnst"; // @flag
@@ -0,0 +1 @@
1
+ { "path": "apps/demo/lib/task/Task.Zone.tsx" }
@@ -0,0 +1,3 @@
1
+ import { cnst, st } from "@apps/demo/client"; // @ok
2
+ import { Task } from "./task.constant"; // @ok
3
+ import { env } from "@apps/demo/env/env.client"; // @ok
@@ -0,0 +1,4 @@
1
+ export const viaLocal = () => logger.log("x"); // @flag
2
+ export const viaStatic = () => Logger.log("x"); // @flag
3
+ export class ViaField { run() { this.logger.log("x"); } } // @flag
4
+ export class ViaPrivateField { run() { this.#logger.log("x"); } } // @flag
@@ -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,2 @@
1
+ import { useState } from "react"; // @flag
2
+ import { st } from "../st"; // @flag
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from "react"; // @ok
2
+ import { cnst } from "@libs/shared/client"; // @ok
3
+ import { usePage } from "akanjs/client"; // @ok
4
+ export const Card = ({ children }: { children: ReactNode }) => <div>{children}</div>; // @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,5 @@
1
+ import type { TaskStore } from "./task.store"; // @ok
2
+ import * as cnst from "../cnst"; // @ok
3
+ import * as db from "../db"; // @ok
4
+ import { Err } from "../dict"; // @ok
5
+ import { Admin } from "@libs/shared/srvkit"; // @ok
@@ -0,0 +1,4 @@
1
+ import dayjs from "dayjs"; // @flag
2
+ import { z } from "zod"; // @flag
3
+ import fs from "node:fs"; // @flag
4
+ import { clsx } from "clsx"; // @flag
@@ -0,0 +1,5 @@
1
+ import { Ticket } from "./ticket.constant"; // @ok
2
+ import { cnst } from "@libs/shared/client"; // @ok
3
+ import { field } from "akanjs/constant"; // @ok
4
+ import { useState } from "react"; // @ok
5
+ import { test } from "bun:test"; // @ok
@@ -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,4 @@
1
+ import type { TaskService } from "./task.service"; // @ok
2
+ import * as cnst from "../cnst"; // @ok
3
+ import { st } from "../st"; // @ok
4
+ import { Err, Task } from "@libs/shared/client"; // @ok
@@ -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,4 @@
1
+ "use client";
2
+
3
+ export const A = () => { void fetch.initTaskInOrg(orgId); }; // @flag
4
+ export const B = () => { void fetch.getTaskInitInOrg(orgId); }; // @flag
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ export const C = () => { void fetch.initPayment(sessionId); }; // @ok
4
+ export const D = () => { void st.do.initTaskInOrg(); }; // @ok
5
+ export const E = () => { void fetch.viewTask(taskId); }; // @ok
@@ -0,0 +1,3 @@
1
+ export const HexInStyleObject = () => <div style={{ color: "#fff" }} />; // @flag
2
+ export const RgbInStyleObject = () => <div style={{ background: "rgb(0,0,0)" }} />; // @flag
3
+ export const ColorLiteralInStyleTag = () => <style>{`.x { color: #abcdef; }`}</style>; // @flag
@@ -0,0 +1,3 @@
1
+ export const VarReferenceInStyleObject = () => <div style={{ color: "var(--primary)" }} />; // @ok
2
+ export const NonColorStyleProperty = () => <div style={{ width: "100%" }} />; // @ok
3
+ export const ComputedNonColorStyle = () => <div style={{ minHeight }} />; // @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,3 @@
1
+ export class SyncPrivate { #compute() { return 1; } } // @flag
2
+ export class AsyncPrivate { async #load() { return 1; } } // @flag
3
+ export class CallsPrivate { run() { this.#compute(); } } // @flag
@@ -0,0 +1,3 @@
1
+ export class TypescriptPrivate { private _compute() { return 1; } } // @ok
2
+ export class CallsUnderscore { run() { this._compute(); } } // @ok
3
+ export class PrivateField { #cache = new Map(); } // @ok
@@ -0,0 +1,3 @@
1
+ export interface CardProps { task: cnst.Task; } // @flag
2
+ export type RowProps = { banner: cnst.LightBanner }; // @flag
3
+ export const View = ({ task }: { task: cnst.Task }) => <div className="flex" />; // @flag
@@ -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,2 @@
1
+ export class AStore extends store(sig.a, () => ({})) { load() { return 1; } } // @flag
2
+ export class BStore extends store(sig.b, () => ({})) { async save() { return true; } } // @flag
@@ -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,3 @@
1
+ "use client"; // @flag
2
+
3
+ export const Card = () => <div className="flex" />;
@@ -0,0 +1,2 @@
1
+ export const Card = () => <div className="flex" />; // @ok
2
+ export const note = "server only"; // @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
- // of styleGuard's `arbitrary-color` rule (frontendBuild/styleGuard.ts) — keep in sync.
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,
@@ -2,19 +2,32 @@ engine biome(1.0)
2
2
  language js(typescript, jsx)
3
3
 
4
4
  // Bun's bundler classifies `//!` and `/*!` as legal comments (the `@license` / `@preserve` class) and keeps
5
- // them through `minify: true`, so a `//!` marker in browser-reachable code ships verbatim to every visitor.
5
+ // them through `minify: true`, so a bang marker in browser-reachable code ships verbatim to every visitor.
6
6
  // `legalComments` is not a Bun.build option, so the source is the only place to stop it.
7
- // The two alternatives anchor the marker to line start or to whitespace after code, which keeps a literal
8
- // like `'https://host//!path'` from tripping the rule. A marker on the file's very first line is trivia that
9
- // Biome does not expose to the pattern, so it is the one case this rule cannot see.
10
- JsModule() as $mod where {
11
- or {
12
- $mod <: r"(?m)(?s).*^[ \t]*//!.*",
13
- $mod <: r"(?s).*[^\s/][ \t]+//!.*"
7
+ //
8
+ // Matched through `file($name, $body)` rather than a node: a comment is trivia, and Biome's GritQL exposes
9
+ // it to no node pattern — `JsModule()` does not even match as a top-level pattern, which is why the earlier
10
+ // node-based spelling of this rule reported nothing at all.
11
+ //
12
+ // Two consequences, both measured (`local/review-2/data/probes/grit-node-probe.sh`):
13
+ // 1. `$body` is the module's *token* span, so interior trivia is visible but the leading and trailing
14
+ // trivia are not — a marker above the first statement or below the last one is unreachable from
15
+ // GritQL. Those two positions are the rule's blind spot.
16
+ // 2. `$body` is the whole file, so the diagnostic lands on line 1 whatever line the marker is on, and a
17
+ // regex capture carries no range to report instead. Search the file for the marker to find it.
18
+ //
19
+ // The alternations anchor the marker to line start or to whitespace after code, which keeps a literal like
20
+ // `'https://host//!path'` from tripping the rule.
21
+ file($name, $body) where {
22
+ $body <: or {
23
+ r"(?ms).*^[ \t]*//!.*",
24
+ r"(?s).*[^\s/][ \t]+//!.*",
25
+ r"(?ms).*^[ \t]*/\*!.*",
26
+ r"(?s).*[^\s/][ \t]+/\*!.*"
14
27
  },
15
28
  register_diagnostic(
16
- span = $mod,
17
- message = "The `//!` marker survives minification (Bun keeps it as a legal comment) and ships to the browser. Use `// FIXME:` or `// TODO:` in client-reachable code; keep `//!` for server, srvkit, and CLI files.",
29
+ span = $body,
30
+ message = "A `//!` or `/*!` marker survives minification (Bun keeps it as a legal comment) and ships to the browser. Use `// FIXME:` or `// TODO:` in client-reachable code; keep the bang form for server, srvkit, and CLI files. This diagnostic is file-level — search the file for the marker.",
18
31
  severity = "error"
19
32
  )
20
33
  }
@@ -3,8 +3,8 @@ language js(typescript, jsx)
3
3
 
4
4
  // daisyUI was removed from the UI system; its component classes (`btn-primary`, `card-body`,
5
5
  // `mockup-code`, ...) and its colour slots (`bg-base-100`, `text-base-content`, `bg-error`)
6
- // no longer have any CSS behind them and silently render unstyled. Grit port of styleGuard's
7
- // `daisyui-legacy` rule (frontendBuild/styleGuard.ts) — keep in sync.
6
+ // no longer have any CSS behind them and silently render unstyled.
7
+ // Cases: lint/__fixtures__/no-daisyui-legacy-class/{bad,good}.tsx
8
8
  // Only high-signal compound names are matched; bare ambiguous words that collide with
9
9
  // Tailwind or app classes (`card`, `input`, `badge`, `btn`, `divider`) are deliberately not
10
10
  // flagged, and colour slots that survived into the semantic vocabulary (`primary`, `info`,
@@ -2,18 +2,20 @@ engine biome(1.0)
2
2
  language js(typescript, jsx)
3
3
 
4
4
  // Color literals inside `style={{ ... }}` bypass the class scanner, the semantic token layer,
5
- // and theme switching entirely. Grit port of styleGuard's `inline-color` rule
6
- // (frontendBuild/styleGuard.ts) — keep in sync. `var(--token)` references do not match.
5
+ // and theme switching entirely. `var(--token)` references do not match.
7
6
  //
8
- // Known delta vs the TS guard: color literals inside a `<style>` tag's template body are not
9
- // matched here (template chunks are not scoped by this pattern); the build-gate styleGuard
10
- // still covers that case.
11
- JsStringLiteralExpression() as $s where {
12
- $s <: within `style={$obj}`,
7
+ // The `<style>` arm covers the same literal in a tag body, which is the other way a color reaches
8
+ // the page without passing the vocabulary. Both arms are needed because the two sites are different
9
+ // node kinds a string in an object property, and a chunk of a template literal.
10
+ // Cases: lint/__fixtures__/no-inline-color/{bad,good}.tsx
11
+ or {
12
+ JsStringLiteralExpression() as $s where { $s <: within `style={$obj}` },
13
+ JsTemplateChunkElement() as $s where { $s <: within `<style>$body</style>` }
14
+ } where {
13
15
  $s <: r"[\s\S]*(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb)\()[\s\S]*",
14
16
  register_diagnostic(
15
17
  span = $s,
16
- message = "Inline color literal in a style object bypasses semantic tokens and theme switching. Use a token class, or style={{ color: 'var(--primary)' }} when a runtime value is unavoidable.",
18
+ message = "Inline color literal bypasses semantic tokens and theme switching. Use a token class, or style={{ color: 'var(--primary)' }} when a runtime value is unavoidable.",
17
19
  severity = "error"
18
20
  )
19
21
  }
@@ -7,9 +7,9 @@ language js(typescript, jsx)
7
7
  // literal of the same shape happens to exist elsewhere in the codebase: the default value works by accident
8
8
  // while every override dies quietly (that is exactly how `Empty`'s minHeight went unnoticed).
9
9
  //
10
- // Grit port of styleGuard's `interpolated-arbitrary` rule (frontendBuild/styleGuard.ts) keep the two
11
- // regexes in sync. Matching template *chunks* is what makes this detectable: the chunk before `${` still
12
- // carries the unterminated `-[`.
10
+ // Matching template *chunks* is what makes this detectable: the chunk before `${` still carries the
11
+ // unterminated `-[`.
12
+ // Cases: lint/__fixtures__/no-interpolated-arbitrary-class/{bad,good}.tsx
13
13
  //
14
14
  // The fix is a style prop for the runtime value (`style={{ minHeight }}`) — dimensions are not colors, so
15
15
  // no-inline-color does not apply — or a fixed set of literal classes when the value comes from an enum.
@@ -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. It is the
7
- // grit port of styleGuard's `raw-palette` rule (frontendBuild/styleGuard.ts) keep the two
8
- // regexes in sync. Matching string/template nodes (not raw text) skips comments for free.
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.
@@ -1,10 +1,19 @@
1
- `$name={$value}` where {
2
- or {
3
- $value <: `($$$_) => $_`,
4
- $value <: `$_ => $_`,
5
- $value <: `function($$$_) { $$$_ }`,
6
- $value <: `function $fnName($$$_) { $$$_ }`
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,