@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.90

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 (183) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/DEV_RUNTIME_KNOBS.md +27 -0
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/agentsIndex.test.ts +10 -0
  6. package/agentsIndex.ts +21 -1
  7. package/aiEditor.ts +1 -1
  8. package/akanApp/BackendImportGraph.test.ts +120 -0
  9. package/akanApp/BackendImportGraph.ts +167 -0
  10. package/akanApp/akanApp.host.test.ts +16 -678
  11. package/akanApp/akanApp.host.ts +113 -578
  12. package/akanApp/devHostPolicy.test.ts +542 -0
  13. package/akanApp/devHostPolicy.ts +442 -0
  14. package/akanApp/index.ts +2 -0
  15. package/akanConfig/akanConfig.test.ts +182 -14
  16. package/akanConfig/akanConfig.ts +143 -47
  17. package/akanConfig/types.ts +9 -0
  18. package/akanContext.ts +30 -14
  19. package/akanMcpContract.ts +25 -4
  20. package/appSelectionMemory.ts +28 -0
  21. package/applicationBuildRunner.test.ts +1 -1
  22. package/applicationBuildRunner.ts +45 -21
  23. package/artifact/implicitRootLayout.test.ts +67 -0
  24. package/artifact/implicitRootLayout.ts +27 -7
  25. package/artifact/routeSeedIndex.test.ts +1 -0
  26. package/biome.base.json +86 -47
  27. package/biomeBase.ts +6 -5
  28. package/capacitorApp.ts +10 -0
  29. package/cloud/globalConfig.ts +9 -2
  30. package/codegenLock.test.ts +137 -0
  31. package/codegenLock.ts +136 -0
  32. package/commandDecorators/argMeta.ts +9 -1
  33. package/commandDecorators/command.ts +94 -7
  34. package/commandDecorators/commandBuilder.ts +20 -5
  35. package/commandDecorators/commandDecorators.test.ts +115 -3
  36. package/commandDecorators/helpFormatter.ts +4 -0
  37. package/dependencyScanner.test.ts +99 -0
  38. package/dependencyScanner.ts +27 -24
  39. package/devkitUtils.test.ts +0 -104
  40. package/executors.test.ts +129 -4
  41. package/executors.ts +105 -24
  42. package/fileEditor.ts +19 -19
  43. package/fileSys.ts +19 -1
  44. package/formSetterScanner.test.ts +80 -0
  45. package/formSetterScanner.ts +92 -0
  46. package/frontendBuild/autoImportSync.test.ts +58 -0
  47. package/frontendBuild/autoImportSync.ts +7 -2
  48. package/frontendBuild/buildRouteClient.test.ts +47 -24
  49. package/frontendBuild/clientBuildTypes.ts +4 -0
  50. package/frontendBuild/clientEntriesBundler.ts +4 -1
  51. package/frontendBuild/clientEntryDiscovery.ts +2 -2
  52. package/frontendBuild/csrArtifactBuilder.ts +116 -84
  53. package/frontendBuild/cssCompiler.ts +123 -12
  54. package/frontendBuild/cssImportResolver.ts +8 -7
  55. package/frontendBuild/devGeneratedIndexSync.ts +3 -2
  56. package/frontendBuild/fontOptimizer.ts +37 -18
  57. package/frontendBuild/fontPruner.test.ts +220 -0
  58. package/frontendBuild/fontPruner.ts +206 -0
  59. package/frontendBuild/frontendBuild.test.ts +178 -10
  60. package/frontendBuild/hmrWatcher.ts +1 -1
  61. package/frontendBuild/index.ts +1 -1
  62. package/frontendBuild/pagesBundleBuilder.ts +3 -3
  63. package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
  64. package/frontendBuild/routeClientBuilder.ts +12 -5
  65. package/frontendBuild/sourceMtimeIndex.ts +1 -1
  66. package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
  67. package/frontendBuild/styleContract.ts +14 -20
  68. package/frontendBuild/themeValidator.ts +22 -17
  69. package/frontendBuild/vendorSpecifiers.ts +1 -0
  70. package/frontendBuild/watchRootResolver.test.ts +67 -0
  71. package/frontendBuild/watchRootResolver.ts +13 -1
  72. package/incrementalBuilder/devWatchBatch.test.ts +18 -20
  73. package/incrementalBuilder/devWatchBatch.ts +6 -2
  74. package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
  75. package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
  76. package/index.ts +0 -7
  77. package/integration/devStabilityHarness.ts +2 -10
  78. package/libSource.test.ts +112 -0
  79. package/libSource.ts +126 -0
  80. package/lint/__fixtures__/README.md +40 -0
  81. package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
  82. package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
  83. package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
  84. package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
  85. package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
  86. package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
  87. package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
  88. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
  89. package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
  90. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
  91. package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
  92. package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
  93. package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
  94. package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
  95. package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
  96. package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
  97. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
  98. package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
  99. package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
  100. package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
  101. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
  102. package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
  103. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
  104. package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
  105. package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
  106. package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
  107. package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
  108. package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
  109. package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
  110. package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
  111. package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
  112. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
  113. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
  114. package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
  115. package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
  116. package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
  117. package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
  118. package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
  119. package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
  120. package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
  121. package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
  122. package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
  123. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
  124. package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
  125. package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
  126. package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
  127. package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
  128. package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
  129. package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
  130. package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
  131. package/lint/gritRules.test.ts +178 -0
  132. package/lint/no-arbitrary-color.grit +1 -1
  133. package/lint/no-async-component-in-ui.grit +35 -0
  134. package/lint/no-bang-comment-in-client.grit +23 -10
  135. package/lint/no-daisyui-legacy-class.grit +26 -9
  136. package/lint/no-deprecated-log-level.grit +17 -0
  137. package/lint/no-init-fetch-in-client.grit +47 -0
  138. package/lint/no-inline-color.grit +10 -8
  139. package/lint/no-interpolated-arbitrary-class.grit +3 -3
  140. package/lint/no-model-type-in-util-zone.grit +58 -0
  141. package/lint/no-raw-palette-class.grit +3 -3
  142. package/lint/no-unpublished-form-setter.grit +41 -0
  143. package/lint/non-scalar-props-restricted.grit +16 -7
  144. package/linter.test.ts +80 -0
  145. package/linter.ts +99 -25
  146. package/package.json +5 -8
  147. package/prompter.ts +9 -4
  148. package/qualityScanner.test.ts +116 -0
  149. package/qualityScanner.ts +89 -21
  150. package/recipeScanner.ts +4 -1
  151. package/scanInfo.ts +32 -26
  152. package/semver.test.ts +26 -0
  153. package/semver.ts +31 -0
  154. package/slicePlanner.test.ts +182 -0
  155. package/slicePlanner.ts +235 -0
  156. package/ssrScanner.test.ts +301 -0
  157. package/subspace.test.ts +418 -0
  158. package/subspace.ts +723 -0
  159. package/subspaceConfig.ts +76 -0
  160. package/transforms/asyncDefaultExportDetector.ts +103 -0
  161. package/transforms/barrelImportsPlugin.ts +1 -1
  162. package/transforms/externalizeFrameworkPlugin.ts +0 -1
  163. package/transforms/transforms.test.ts +5 -5
  164. package/transforms/tsconfigPackageResolver.test.ts +230 -0
  165. package/tsconfig.json +6 -2
  166. package/typeChecker.ts +1 -1
  167. package/types.ts +1 -0
  168. package/uploadRelease.ts +2 -2
  169. package/workflow/executor.test.ts +146 -0
  170. package/workspaceLayout.test.ts +56 -4
  171. package/workspaceLayout.ts +48 -2
  172. package/builder.ts +0 -164
  173. package/extractDeps.ts +0 -86
  174. package/frontendBuild/styleGuard.test.ts +0 -165
  175. package/frontendBuild/styleGuard.ts +0 -322
  176. package/getCredentials.ts +0 -19
  177. package/getModelFileData.ts +0 -62
  178. package/src/capacitorApp.ts +0 -282
  179. package/streamAi.ts +0 -45
  180. package/ui/MultiScrollList.tsx +0 -242
  181. package/ui/ScrollList.tsx +0 -107
  182. package/ui/index.ts +0 -2
  183. package/useStdoutDimensions.ts +0 -20
@@ -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,
@@ -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
+ }
@@ -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
  }
@@ -2,19 +2,36 @@ engine biome(1.0)
2
2
  language js(typescript, jsx)
3
3
 
4
4
  // daisyUI was removed from the UI system; its component classes (`btn-primary`, `card-body`,
5
- // `mockup-code`, ...) no longer have any CSS behind them and silently render unstyled. Grit
6
- // port of styleGuard's `daisyui-legacy` rule (frontendBuild/styleGuard.ts) keep in sync.
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.
7
+ // Cases: lint/__fixtures__/no-daisyui-legacy-class/{bad,good}.tsx
7
8
  // Only high-signal compound names are matched; bare ambiguous words that collide with
8
- // Tailwind or app classes (`card`, `input`, `badge`, `btn`) are deliberately not flagged.
9
+ // Tailwind or app classes (`card`, `input`, `badge`, `btn`, `divider`) are deliberately not
10
+ // flagged, and colour slots that survived into the semantic vocabulary (`primary`, `info`,
11
+ // `success`, `warning`, `neutral`) are not either — only the slots the vocabulary dropped.
12
+ // Biome keeps only the first diagnostic a single plugin registers on a node, so a string
13
+ // carrying both halves reports the component one and surfaces the colour one on the next run.
9
14
  or {
10
15
  JsxString() as $s,
11
16
  JsStringLiteralExpression() as $s,
12
17
  JsTemplateChunkElement() as $s
13
18
  } where {
14
- $s <: r"[\s\S]*(?:^|[\s\"'`{(\[:!])(?:btn-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|link|outline|square|circle|wide|block|xs|sm|md|lg)|badge-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|outline)|alert-(?:info|success|warning|error)|input-(?:bordered|primary|secondary|accent|ghost|error)|select-(?:bordered|primary|ghost)|textarea-(?:bordered|primary|ghost)|checkbox-(?:primary|secondary|accent)|toggle-(?:primary|secondary|accent)|loading-(?:spinner|dots|ring|ball|bars|infinity)|card-(?:body|title|actions)|modal-(?:box|action|backdrop)|collapse-(?:title|content|arrow|plus)|dropdown-(?:content|end|start|hover)|stat-(?:title|value|desc)|tabs-(?:boxed|lifted|bordered)|tab-active|menu-(?:title|dropdown)|steps-(?:horizontal|vertical)|join-item|mockup-(?:code|phone|browser|window)|drawer-(?:side|content|toggle))(?:[\s\"'`})\]:/!,]|$)[\s\S]*",
15
- register_diagnostic(
16
- span = $s,
17
- message = "daisyUI legacy class - daisyUI was removed, so this renders unstyled. Use akanjs/ui components and recipes (Button, buttonRecipe, badgeRecipe, ...) with semantic tokens instead.",
18
- severity = "error"
19
- )
19
+ any {
20
+ and {
21
+ $s <: r"[\s\S]*(?:^|[\s\"'`{(\[:!])(?:btn-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|link|outline|square|circle|wide|block|xs|sm|md|lg)|badge-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|outline)|alert-(?:info|success|warning|error)|input-(?:bordered|primary|secondary|accent|ghost|error)|select-(?:bordered|primary|ghost)|textarea-(?:bordered|primary|ghost)|checkbox-(?:primary|secondary|accent)|toggle-(?:primary|secondary|accent)|loading-(?:spinner|dots|ring|ball|bars|infinity)|card-(?:body|title|actions)|modal-(?:box|action|backdrop)|collapse-(?:title|content|arrow|plus)|dropdown-(?:content|end|start|hover)|stat-(?:title|value|desc)|tabs-(?:boxed|lifted|bordered)|tab-active|menu-(?:title|dropdown)|steps-(?:horizontal|vertical)|join-item|mockup-(?:code|phone|browser|window)|drawer-(?:side|content|toggle))(?:[\s\"'`})\]:/!,]|$)[\s\S]*",
22
+ register_diagnostic(
23
+ span = $s,
24
+ message = "daisyUI legacy class - daisyUI was removed, so this renders unstyled. Use akanjs/ui components and recipes (Button, buttonRecipe, badgeRecipe, ...) with semantic tokens instead.",
25
+ severity = "error"
26
+ )
27
+ },
28
+ and {
29
+ $s <: r"[\s\S]*(?:^|[\s\"'`{(\[:!])(?:bg|text|border(?:-[tblrxy])?(?:-[se])?|ring(?:-offset)?|fill|stroke|shadow|from|to|via|divide|outline|decoration|placeholder|caret|accent)-(?:base-(?:100|200|300|content)|(?:primary|secondary|accent|neutral|info|success|warning|error)-content|error)(?:[\s\"'`})\]:/!,]|$)[\s\S]*",
30
+ register_diagnostic(
31
+ span = $s,
32
+ message = "daisyUI colour slot - the closed colour vocabulary has no CSS for it, so this renders unstyled. base-100/200/300 -> background/muted/border, base-content -> foreground, <colour>-content -> <colour>-foreground, error -> destructive.",
33
+ severity = "error"
34
+ )
35
+ }
36
+ }
20
37
  }
@@ -0,0 +1,17 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // The level ladder no longer has a `log` tier: `logger.log()` and `Logger.log()` are kept for compatibility
5
+ // and emit at `info`, so a call reads like a distinct level and is not one. The receiver regex admits the
6
+ // instance field (`this.logger`, `this.#logger`), a local (`logger`) and the static class (`Logger`), and
7
+ // nothing else — `console.log` is biome's own `noConsole` rule.
8
+ //
9
+ // No autofix: Biome does not apply plugin rewrites, and the rename is a one-word edit at each site.
10
+ `$recv.log($arg)` as $call where {
11
+ $recv <: r"(?:this\.)?#?[lL]ogger",
12
+ register_diagnostic(
13
+ span = $call,
14
+ message = "`.log()` is deprecated and emits at `info`. Call `.info()` instead.",
15
+ severity = "error"
16
+ )
17
+ }
@@ -0,0 +1,47 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // `fetch.init<Model><Suffix>` is not a request, it is a hydration snapshot. `#registerSlice`
5
+ // (pkgs/akanjs/fetch/client/fetchClient.ts) composes it out of the slice's list and insight queries and
6
+ // returns a `ServerInit` whose only consumer is the `init` prop of `Load.Units` / `Load.View`, which
7
+ // writes it into the store before React ever renders. `fetch.get<Model>Init<Suffix>` is the same call
8
+ // returning the payload alone. Run from a route they resolve before the first byte and the markup ships
9
+ // populated; run after hydration they are two extra round-trips for a shell the browser already painted
10
+ // empty, landing in a local variable no store reads — `Load.*` seeds state, so a value held in a client
11
+ // closure reaches nothing.
12
+ //
13
+ // The client is not missing the load, only this spelling of it: every slice also generates
14
+ // `st.do.init<Model><Suffix>` (pkgs/akanjs/store/action.ts), which runs the same two queries and commits
15
+ // them to state.
16
+ //
17
+ // Both names are matched by shape, since a lint rule cannot know which slices exist. The generated one is
18
+ // `init` + `Capitalize<refName>` + `Capitalize<suffix>`, so it always carries two capital-led segments —
19
+ // requiring the second one is what keeps a hand-written `initPayment` / `initSession` endpoint out, while
20
+ // `initializeSomething` was never at risk (`init` is followed by a lowercase letter). What is left over is a
21
+ // custom endpoint that happens to spell the generated shape exactly, `initPaymentSession` or
22
+ // `get<X>Init<Y>`; suppress that one with `// biome-ignore lint/plugin: <reason>`.
23
+ //
24
+ // `view`/`edit` hydrate the same way but are not matched here: `edit<X>` is a plausible custom endpoint
25
+ // name, whereas the two shapes above are not.
26
+ //
27
+ // The gate is the file. `JsDirective` anchors `"use client"` to the real directive — docs pages carry the
28
+ // same text as an ordinary string literal and inside sample code, and neither is one. `*.store.ts` is
29
+ // added by name because a store is client-only by role and carries no directive.
30
+ JsCallExpression() as $call where {
31
+ $call <: `fetch.$endpoint($...)`,
32
+ $endpoint <: or {
33
+ r"init[A-Z][A-Za-z0-9_$]*[A-Z][A-Za-z0-9_$]*",
34
+ r"get[A-Z][A-Za-z0-9_$]*Init[A-Z][A-Za-z0-9_$]*"
35
+ },
36
+ or {
37
+ $filename <: r".*\.store\.ts",
38
+ $call <: within JsModule() as $module where {
39
+ $module <: contains JsDirective() as $directive where { $directive <: r"[\s\S]*use client[\s\S]*" }
40
+ }
41
+ },
42
+ register_diagnostic(
43
+ span = $call,
44
+ message = "This is a server-side hydration call. It composes the slice's list and insight queries into a snapshot whose only consumer is the init prop of Load.Units / Load.View, so on the client it costs two extra round-trips for a shell the browser already painted, and the result lands in a value nothing reads. Load it in the route — await fetch.initXInY(...) in page/** and pass the result down as an init prop, or hand the unawaited promise to a Zone and let Load.* resolve it behind a skeleton. To reload from the client, call the generated store action st.do.initXInY() instead, which writes the same data into state.",
45
+ severity = "error"
46
+ )
47
+ }
@@ -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.
@@ -0,0 +1,58 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // `Util` and `Zone` are always client components, so a *prop* typed as a `cnst` model is a hydrated
5
+ // class instance the server has to hand across the boundary — the methods are stripped on the way and
6
+ // what arrives is a plain object wearing the model's type.
7
+ //
8
+ // Only prop positions are reported: the component's `*Props` interface / type alias, or the inline
9
+ // object type on its parameter. A `cnst` type anywhere else in the file never crosses the boundary and
10
+ // stays legal — a local annotation, a callback parameter the framework already types with the model
11
+ // (`renderItem`, `renderList`), a module-scope helper, a non-`Props` local shape, and the props of a
12
+ // component nested inside another one. A function-typed prop is client-internal too: a closure cannot
13
+ // cross the RSC boundary at all, so whoever passes it already holds the value it takes.
14
+ //
15
+ // The exempt generics are the framework props types whose model parameter never lands on a prop as
16
+ // data: `ClientInit` / `ClientView` / `ClientEdit` (mapped to plain `GetStateObject`) and `ModelsProps`,
17
+ // whose only use of the model is `onClickItem?: (model: M) => unknown` — a callback, so the caller
18
+ // already holds the value. `ModelProps<"x", cnst.LightX>` is *not* exempt: it spreads the model onto
19
+ // the props themselves.
20
+ TsReferenceType() as $ref where {
21
+ $ref <: r"cnst\.[A-Za-z_$][A-Za-z0-9_$]*",
22
+ not $ref <: within TsIndexedAccessType() as $indexed where {
23
+ $indexed <: r"cnst\.[A-Za-z_$][A-Za-z0-9_$]*\[.value.\][\s\S]*"
24
+ },
25
+ not $ref <: within TsReferenceType() as $wrapper where {
26
+ $wrapper <: r"(?:(?:Client|Server)(?:Init|View|Edit)|ModelsProps)<[\s\S]*"
27
+ },
28
+ not $ref <: within TsFunctionType(),
29
+ or {
30
+ $ref <: within TsInterfaceDeclaration() as $propsInterface where {
31
+ $propsInterface <: r"interface\s+[A-Za-z0-9_$]*Props[\s\S]*"
32
+ },
33
+ $ref <: within TsTypeAliasDeclaration() as $propsAlias where {
34
+ $propsAlias <: r"type\s+[A-Za-z0-9_$]*Props[\s\S]*"
35
+ },
36
+ and {
37
+ $ref <: within JsFormalParameter(),
38
+ // A parameter is a sibling of the body, so this only drops the ones belonging to a function
39
+ // declared *inside* a component — a nested component or a local callback.
40
+ not $ref <: within JsFunctionBody(),
41
+ // Same for a callback handed straight to a call, which an expression-bodied component leaves
42
+ // outside every function body: `rows.map((row: cnst.LightTicket) => …)`.
43
+ not $ref <: within JsCallArguments(),
44
+ or {
45
+ $ref <: within JsVariableDeclarator() as $component where {
46
+ $component <: r"[A-Z][A-Za-z0-9_$]*[\s\S]*"
47
+ },
48
+ $ref <: within JsFunctionDeclaration() as $componentFn where {
49
+ $componentFn <: r"(?:async\s+)?function\s+[A-Z][A-Za-z0-9_$]*[\s\S]*"
50
+ }
51
+ }
52
+ }
53
+ },
54
+ register_diagnostic(
55
+ span = $ref,
56
+ message = "Util and Zone are client components, so a cnst model type on a prop is a class instance crossing the server boundary. Take an id instead (bannerId: string), or hand server data across as ClientInit / ClientView. An enum union such as cnst.AdminRole['value'] is allowed, and so is a cnst type that stays inside the file — a local annotation or a callback parameter."
57
+ )
58
+ }
@@ -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.
@@ -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
- `$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,