@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.80

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 (174) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/agentsIndex.test.ts +10 -0
  5. package/agentsIndex.ts +47 -1
  6. package/aiEditor.ts +1 -1
  7. package/akanApp/BackendImportGraph.test.ts +120 -0
  8. package/akanApp/BackendImportGraph.ts +167 -0
  9. package/akanApp/akanApp.host.test.ts +16 -678
  10. package/akanApp/akanApp.host.ts +65 -562
  11. package/akanApp/devHostPolicy.test.ts +542 -0
  12. package/akanApp/devHostPolicy.ts +412 -0
  13. package/akanApp/index.ts +2 -0
  14. package/akanConfig/akanConfig.test.ts +182 -14
  15. package/akanConfig/akanConfig.ts +132 -47
  16. package/akanConfig/types.ts +8 -0
  17. package/akanContext.ts +70 -14
  18. package/akanMcpContract.ts +25 -4
  19. package/applicationBuildRunner.test.ts +1 -1
  20. package/applicationBuildRunner.ts +45 -21
  21. package/artifact/implicitRootLayout.test.ts +67 -0
  22. package/artifact/implicitRootLayout.ts +27 -7
  23. package/artifact/routeSeedIndex.test.ts +1 -0
  24. package/biome.base.json +340 -0
  25. package/biomeBase.ts +9 -0
  26. package/capacitorApp.ts +10 -0
  27. package/cloud/globalConfig.ts +9 -2
  28. package/commandDecorators/command.ts +30 -6
  29. package/commandDecorators/commandBuilder.ts +20 -5
  30. package/commandDecorators/commandDecorators.test.ts +115 -3
  31. package/dependencyScanner.test.ts +99 -0
  32. package/dependencyScanner.ts +27 -24
  33. package/devkitUtils.test.ts +0 -104
  34. package/executors.test.ts +132 -5
  35. package/executors.ts +89 -14
  36. package/fileEditor.ts +19 -19
  37. package/fleetConfig.ts +73 -0
  38. package/fleetGuard.test.ts +121 -0
  39. package/fleetGuard.ts +157 -0
  40. package/fleetSpoke.test.ts +390 -0
  41. package/fleetSpoke.ts +719 -0
  42. package/formSetterScanner.test.ts +80 -0
  43. package/formSetterScanner.ts +92 -0
  44. package/frontendBuild/autoImportSync.test.ts +58 -0
  45. package/frontendBuild/autoImportSync.ts +4 -0
  46. package/frontendBuild/buildRouteClient.test.ts +47 -24
  47. package/frontendBuild/clientBuildTypes.ts +4 -0
  48. package/frontendBuild/clientEntriesBundler.ts +4 -1
  49. package/frontendBuild/clientEntryDiscovery.ts +2 -2
  50. package/frontendBuild/csrArtifactBuilder.ts +116 -84
  51. package/frontendBuild/cssCompiler.ts +123 -12
  52. package/frontendBuild/cssImportResolver.ts +8 -7
  53. package/frontendBuild/fontOptimizer.ts +37 -18
  54. package/frontendBuild/fontPruner.test.ts +220 -0
  55. package/frontendBuild/fontPruner.ts +206 -0
  56. package/frontendBuild/frontendBuild.test.ts +178 -10
  57. package/frontendBuild/hmrWatcher.ts +1 -1
  58. package/frontendBuild/index.ts +1 -1
  59. package/frontendBuild/pagesBundleBuilder.ts +3 -3
  60. package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
  61. package/frontendBuild/routeClientBuilder.ts +12 -5
  62. package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
  63. package/frontendBuild/styleContract.ts +14 -20
  64. package/frontendBuild/themeValidator.ts +22 -17
  65. package/frontendBuild/vendorSpecifiers.ts +1 -0
  66. package/incrementalBuilder/devWatchBatch.test.ts +18 -20
  67. package/incrementalBuilder/devWatchBatch.ts +6 -2
  68. package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
  69. package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
  70. package/index.ts +0 -5
  71. package/integration/devStabilityHarness.ts +2 -10
  72. package/libSource.test.ts +109 -0
  73. package/libSource.ts +126 -0
  74. package/lint/__fixtures__/README.md +40 -0
  75. package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
  76. package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
  77. package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
  78. package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
  79. package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
  80. package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
  81. package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
  82. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
  83. package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
  84. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
  85. package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
  86. package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
  87. package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
  88. package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
  89. package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
  90. package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
  91. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
  92. package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
  93. package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
  94. package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
  95. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
  96. package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
  97. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
  98. package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
  99. package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
  100. package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
  101. package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
  102. package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
  103. package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
  104. package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
  105. package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
  106. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
  107. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
  108. package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
  109. package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
  110. package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
  111. package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
  112. package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
  113. package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
  114. package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
  115. package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
  116. package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
  117. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
  118. package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
  119. package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
  120. package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
  121. package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
  122. package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
  123. package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
  124. package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
  125. package/lint/gritRules.test.ts +178 -0
  126. package/lint/no-arbitrary-color.grit +1 -1
  127. package/lint/no-async-component-in-ui.grit +35 -0
  128. package/lint/no-bang-comment-in-client.grit +23 -10
  129. package/lint/no-daisyui-legacy-class.grit +26 -9
  130. package/lint/no-deprecated-log-level.grit +17 -0
  131. package/lint/no-import-client-in-server.grit +48 -0
  132. package/lint/no-import-server-in-client.grit +45 -0
  133. package/lint/no-init-fetch-in-client.grit +47 -0
  134. package/lint/no-inline-color.grit +10 -8
  135. package/lint/no-interpolated-arbitrary-class.grit +3 -3
  136. package/lint/no-model-type-in-util-zone.grit +58 -0
  137. package/lint/no-raw-palette-class.grit +3 -3
  138. package/lint/no-unpublished-form-setter.grit +41 -0
  139. package/lint/non-scalar-props-restricted.grit +16 -7
  140. package/linter.test.ts +80 -0
  141. package/linter.ts +99 -25
  142. package/package.json +6 -6
  143. package/prompter.ts +9 -4
  144. package/qualityScanner.test.ts +116 -0
  145. package/qualityScanner.ts +89 -21
  146. package/recipeScanner.ts +4 -1
  147. package/repoIdentity.ts +42 -0
  148. package/scanInfo.ts +32 -26
  149. package/semver.test.ts +26 -0
  150. package/semver.ts +31 -0
  151. package/slicePlanner.test.ts +151 -0
  152. package/slicePlanner.ts +157 -0
  153. package/ssrScanner.test.ts +301 -0
  154. package/transforms/asyncDefaultExportDetector.ts +103 -0
  155. package/transforms/barrelImportsPlugin.ts +1 -1
  156. package/transforms/externalizeFrameworkPlugin.ts +0 -1
  157. package/transforms/transforms.test.ts +5 -5
  158. package/transforms/tsconfigPackageResolver.test.ts +230 -0
  159. package/tsconfig.json +6 -2
  160. package/typeChecker.ts +1 -1
  161. package/types.ts +1 -0
  162. package/ui/ScrollList.tsx +6 -8
  163. package/uploadRelease.ts +2 -2
  164. package/workflow/executor.test.ts +146 -0
  165. package/workspaceLayout.test.ts +56 -4
  166. package/workspaceLayout.ts +49 -4
  167. package/builder.ts +0 -164
  168. package/extractDeps.ts +0 -86
  169. package/frontendBuild/styleGuard.test.ts +0 -165
  170. package/frontendBuild/styleGuard.ts +0 -322
  171. package/getCredentials.ts +0 -19
  172. package/getModelFileData.ts +0 -62
  173. package/src/capacitorApp.ts +0 -282
  174. package/streamAi.ts +0 -45
package/builder.ts DELETED
@@ -1,164 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
3
- import path from "node:path";
4
- import type { Loader } from "bun";
5
- import type { Executor } from "./executors";
6
- import { FileSys } from "./fileSys";
7
- import type { PackageJson } from "./types";
8
-
9
- /** Relative paths under these directories are not used as Bun.build entrypoints (Bun does not expand `**` in entrypoint strings; we scan with {@link Bun.Glob}). */
10
- const SKIP_ENTRY_DIR_SET = new Set(["node_modules", "dist", "build", ".git", ".next"]);
11
-
12
- const assetExtensions = [".css", ".md", ".js", ".png", ".ico", ".svg", ".json", ".template"];
13
- const assetLoader = Object.fromEntries(assetExtensions.map((ext) => [ext, "file" as const])) satisfies Record<
14
- string,
15
- Loader
16
- >;
17
-
18
- type BunBuildConfig = Parameters<typeof Bun.build>[0] & { bundle?: boolean };
19
- type ExportValue = string | string[] | { [condition: string]: ExportValue };
20
-
21
- interface BuildOptions {
22
- bundle?: boolean;
23
- additionalEntryPoints?: string[];
24
- }
25
-
26
- interface BuilderOptions {
27
- executor: Executor;
28
- distExecutor: Executor;
29
- pkgJson: PackageJson;
30
- rootPackageJson: PackageJson;
31
- }
32
- export class Builder {
33
- #executor: Executor;
34
- #distExecutor: Executor;
35
- #pkgJson: PackageJson;
36
-
37
- constructor({ executor, distExecutor, pkgJson }: BuilderOptions) {
38
- this.#executor = executor;
39
- this.#distExecutor = distExecutor;
40
- this.#pkgJson = pkgJson;
41
- }
42
- #globEntrypoints(cwd: string, pattern: string): string[] {
43
- const glob = new Bun.Glob(pattern);
44
- return Array.from(glob.scanSync({ cwd, onlyFiles: true }))
45
- .filter((relativePath) => {
46
- const segments = relativePath.split(path.sep);
47
- return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
48
- })
49
- .map((rel) => path.join(cwd, rel));
50
- }
51
- #globFiles(cwd: string, pattern = "**/*.*"): string[] {
52
- const glob = new Bun.Glob(pattern);
53
- return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
54
- const segments = relativePath.split(path.sep);
55
- return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
56
- });
57
- }
58
-
59
- #resolveAdditionalEntrypoints(cwd: string, additionalEntryPoints: string[]): string[] {
60
- const out: string[] = [];
61
- for (const p of additionalEntryPoints) {
62
- if (p.includes("*")) {
63
- const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path.sep}`) ? p.slice(cwd.length + 1) : p;
64
- out.push(...this.#globEntrypoints(cwd, rel));
65
- } else out.push(path.isAbsolute(p) ? p : path.join(cwd, p));
66
- }
67
- return out;
68
- }
69
- #getBuildOptions({ bundle = false, additionalEntryPoints = [] }: BuildOptions = {}): BunBuildConfig {
70
- const cwd = this.#executor.cwdPath;
71
- const entrypoints = [
72
- ...(bundle ? [path.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}")),
73
- ...this.#resolveAdditionalEntrypoints(cwd, additionalEntryPoints),
74
- ];
75
- return {
76
- root: cwd,
77
- entrypoints,
78
- bundle,
79
- packages: "external",
80
- splitting: false,
81
- target: this.#pkgJson.bun?.platform,
82
- format: "esm",
83
- outdir: this.#distExecutor.cwdPath,
84
- external: ["react", "react-dom"],
85
- loader: assetLoader,
86
- };
87
- }
88
- async #copySourceFiles() {
89
- const cwd = this.#executor.cwdPath;
90
- for (const relativePath of this.#globFiles(cwd)) {
91
- if (relativePath === "package.json") continue;
92
- const sourcePath = path.join(cwd, relativePath);
93
- const targetPath = path.join(this.#distExecutor.cwdPath, relativePath);
94
- await mkdir(path.dirname(targetPath), { recursive: true });
95
- await Bun.write(targetPath, Bun.file(sourcePath));
96
- }
97
- }
98
- #toPublishedPath(publishedPath: string, { useSource }: { useSource: boolean }) {
99
- const hasDotSlash = publishedPath.startsWith("./");
100
- const withoutFormatDir = publishedPath.replace(/^(?:\.\/)?(?:esm|cjs)\//, hasDotSlash ? "./" : "");
101
- if (!useSource) return withoutFormatDir;
102
- if (!hasDotSlash && withoutFormatDir === publishedPath) return publishedPath;
103
- const parsed = path.posix.parse(withoutFormatDir);
104
- if (![".js", ".mjs", ".cjs"].includes(parsed.ext)) return withoutFormatDir;
105
- const withoutExt = path.posix.join(parsed.dir, parsed.name);
106
- const sourcePath = withoutExt.startsWith("./") ? withoutExt.slice(2) : withoutExt;
107
- const sourceCandidates = [`${sourcePath}.ts`, `${sourcePath}.tsx`];
108
- const matchedSource = sourceCandidates.find((candidate) =>
109
- existsSync(path.join(this.#executor.cwdPath, candidate)),
110
- );
111
- if (!matchedSource) return withoutFormatDir;
112
- return hasDotSlash ? `./${matchedSource}` : matchedSource;
113
- }
114
- #normalizeExports(
115
- exportsValue: ExportValue | undefined,
116
- { useSource }: { useSource: boolean },
117
- ): ExportValue | undefined {
118
- if (!exportsValue) return exportsValue;
119
- if (typeof exportsValue === "string") return this.#toPublishedPath(exportsValue, { useSource });
120
- if (Array.isArray(exportsValue))
121
- return exportsValue.map((value) => this.#normalizeExports(value, { useSource }) as string);
122
- if (typeof exportsValue !== "object") return exportsValue;
123
-
124
- return Object.fromEntries(
125
- Object.entries(exportsValue)
126
- .filter(([condition]) => condition !== "require")
127
- .map(([condition, value]) => [condition, this.#normalizeExports(value, { useSource })])
128
- .filter((entry): entry is [string, ExportValue] => entry[1] !== undefined),
129
- );
130
- }
131
- #getPackageJson({ bundle = false }: BuildOptions = {}): PackageJson {
132
- const rootEntry = bundle ? "./index.js" : "./index.ts";
133
- const normalizedExports = this.#normalizeExports(this.#pkgJson.exports as ExportValue | undefined, {
134
- useSource: !bundle,
135
- });
136
- return {
137
- ...this.#pkgJson,
138
- type: "module",
139
- main: rootEntry,
140
- bin: this.#normalizeExports(this.#pkgJson.bin as ExportValue | undefined, { useSource: !bundle }),
141
- exports: {
142
- ...((typeof normalizedExports === "object" && !Array.isArray(normalizedExports)
143
- ? normalizedExports
144
- : {}) as Record<string, ExportValue>),
145
- ".": {
146
- import: rootEntry,
147
- types: bundle ? "./index.d.ts" : "./index.ts",
148
- },
149
- },
150
- };
151
- }
152
- async build(options: BuildOptions = {}) {
153
- if (await FileSys.dirExists(this.#distExecutor.cwdPath))
154
- await this.#distExecutor.exec(`rm -rf ${this.#distExecutor.cwdPath}`);
155
-
156
- if (options.bundle) {
157
- const buildResult = await Bun.build({ ...this.#getBuildOptions(options) });
158
- if (!buildResult.success) throw new AggregateError(buildResult.logs, "Bundle failed");
159
- } else await this.#copySourceFiles();
160
-
161
- const pkgPackageJson = this.#getPackageJson(options);
162
- await this.#distExecutor.setPackageJson(pkgPackageJson);
163
- }
164
- }
package/extractDeps.ts DELETED
@@ -1,86 +0,0 @@
1
- import type { PackageJson } from "./types";
2
-
3
- const NODE_NATIVE_MODULE_SET = new Set([
4
- "assert",
5
- "async_hooks",
6
- "buffer",
7
- "child_process",
8
- "cluster",
9
- "console",
10
- "constants",
11
- "crypto",
12
- "dgram",
13
- "dns",
14
- "domain",
15
- "events",
16
- "fs",
17
- "http",
18
- "http2",
19
- "https",
20
- "inspector",
21
- "module",
22
- "net",
23
- "os",
24
- "path",
25
- "perf_hooks",
26
- "process",
27
- "punycode",
28
- "querystring",
29
- "readline",
30
- "repl",
31
- "stream",
32
- "string_decoder",
33
- "timers",
34
- "tls",
35
- "trace_events",
36
- "tty",
37
- "url",
38
- "util",
39
- "v8",
40
- "vm",
41
- "wasi",
42
- "worker_threads",
43
- "zlib",
44
- ]);
45
-
46
- export const extractDependencies = (
47
- filepaths: { path: string; text: string }[],
48
- pacakgeJson: PackageJson,
49
- defaultDependencies: string[] = [],
50
- ) => {
51
- if (!pacakgeJson.dependencies) throw new Error("No dependencies found in package.json");
52
- const dependencies = new Set<string>(defaultDependencies);
53
-
54
- const existingDependencies = new Set<string>([
55
- ...Object.keys(pacakgeJson.dependencies ?? {}),
56
- ...Object.keys(pacakgeJson.devDependencies ?? {}),
57
- ]);
58
- const versionObj = {
59
- ...(pacakgeJson.dependencies ?? {}),
60
- ...(pacakgeJson.devDependencies ?? {}),
61
- };
62
-
63
- // Look for require statements: require('package-name') or import 'package-name'
64
- const requireRegex = /(?:require\s*\(|import\s*(?:[\w\s{},*]*\s+from\s*)?|import\s*\()\s*['"`]([^'"`]+)['"`]/g;
65
- for (const { text } of filepaths.filter(({ path }) => path.endsWith(".js") || path.endsWith(".ts"))) {
66
- let requireMatch = requireRegex.exec(text);
67
- while (requireMatch !== null) {
68
- const moduleName = requireMatch[1];
69
- if (!moduleName) throw new Error(`No module name found in ${text}`);
70
- const moduleNameParts = moduleName.split("/");
71
- const subModuleLength = moduleNameParts.length;
72
- for (let i = 0; i < subModuleLength; i++) {
73
- const libName = moduleNameParts.slice(0, i + 1).join("/");
74
- if (!NODE_NATIVE_MODULE_SET.has(libName) && existingDependencies.has(libName)) dependencies.add(libName);
75
- }
76
- requireMatch = requireRegex.exec(text);
77
- }
78
- }
79
- return Object.fromEntries(
80
- [...dependencies].sort().map((dep) => {
81
- const version = versionObj[dep];
82
- if (!version) throw new Error(`No version found for ${dep}`);
83
- return [dep, version];
84
- }),
85
- );
86
- };
@@ -1,165 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { StyleGuard, type StyleGuardRule } from "./styleGuard";
3
-
4
- const guard = new StyleGuard();
5
- const scan = (content: string, path = "Demo.tsx") => guard.run([{ path, content }]);
6
- const rules = (content: string): StyleGuardRule[] => scan(content).map((v) => v.rule);
7
-
8
- describe("StyleGuard raw-palette", () => {
9
- test("flags raw Tailwind palette utilities", () => {
10
- expect(rules('<div className="bg-blue-500 text-gray-700" />')).toEqual(["raw-palette", "raw-palette"]);
11
- });
12
-
13
- test("flags palette with variant prefix and opacity", () => {
14
- expect(rules('<div className="hover:bg-red-500/50" />')).toContain("raw-palette");
15
- });
16
-
17
- test("flags numeric neutral but allows bare semantic neutral", () => {
18
- expect(rules('<div className="bg-neutral-500" />')).toEqual(["raw-palette"]);
19
- expect(scan('<div className="bg-neutral text-neutral-foreground" />')).toHaveLength(0);
20
- });
21
-
22
- test("does not flag semantic tokens or black/white", () => {
23
- expect(scan('<div className="bg-primary text-muted-foreground border-border" />')).toHaveLength(0);
24
- expect(scan('<div className="bg-black text-white bg-white/30 bg-black/50" />')).toHaveLength(0);
25
- });
26
-
27
- test("does not flag non-color numeric utilities", () => {
28
- expect(scan('<div className="gap-4 mt-2 grid-cols-3 w-500" />')).toHaveLength(0);
29
- });
30
- });
31
-
32
- describe("StyleGuard arbitrary-color", () => {
33
- test("flags hex and color-function arbitrary values", () => {
34
- expect(rules('<div className="bg-[#3b82f6]" />')).toEqual(["arbitrary-color"]);
35
- expect(rules('<div className="text-[rgb(0,0,0)]" />')).toEqual(["arbitrary-color"]);
36
- });
37
-
38
- test("allows arbitrary CSS variable references", () => {
39
- expect(scan('<div className="bg-[--brand] text-[var(--fg)]" />')).toHaveLength(0);
40
- });
41
- });
42
-
43
- describe("StyleGuard inline-color", () => {
44
- test("flags hardcoded color in style object", () => {
45
- expect(rules("<div style={{ color: '#fff', background: 'rgb(0,0,0)' }} />")).toEqual([
46
- "inline-color",
47
- "inline-color",
48
- ]);
49
- });
50
-
51
- test("flags color literal inside <style> tag", () => {
52
- expect(rules("<style>{`.x { color: #abcdef; }`}</style>")).toEqual(["inline-color"]);
53
- });
54
-
55
- test("allows var() references in style object", () => {
56
- expect(scan("<div style={{ color: 'var(--primary)', width: '100%' }} />")).toHaveLength(0);
57
- });
58
- });
59
-
60
- describe("StyleGuard daisyui-legacy", () => {
61
- test("flags high-signal daisyUI compound classes", () => {
62
- expect(rules('<button className="btn-primary" />')).toEqual(["daisyui-legacy"]);
63
- expect(rules('<span className="badge-success" />')).toEqual(["daisyui-legacy"]);
64
- expect(rules('<div className="modal-box card-body" />')).toEqual(["daisyui-legacy", "daisyui-legacy"]);
65
- });
66
-
67
- test("does not flag bare ambiguous names that collide with Tailwind", () => {
68
- expect(scan('<div className="card input badge btn" />')).toHaveLength(0);
69
- });
70
- });
71
-
72
- // The fixtures below must contain a literal `${`. Writing it inside a plain string trips biome's
73
- // noTemplateCurlyInString, so the placeholder is assembled from `D` — that keeps the rule on for real code
74
- // instead of scattering suppressions through the fixtures.
75
- const D = "$";
76
- const INTERPOLATED = {
77
- size: `<div className={\`min-h-[${D}{minHeight}px] flex\`} />`,
78
- color: `<div className={\`bg-[${D}{color}] w-full\`} />`,
79
- brokenBracket: `<div className={\`min-h-[ w-full${D}{minHeight}px] flex\`} />`,
80
- outsideBrackets: `<div className={\`flex gap-2 ${D}{isOpen ? "opacity-50" : ""}\`} />`,
81
- styleProp: `<div style={{ minHeight }} className={\`flex ${D}{extra}\`} />`,
82
- };
83
-
84
- describe("StyleGuard interpolated-arbitrary", () => {
85
- test("flags an arbitrary value assembled from a runtime expression", () => {
86
- expect(rules(INTERPOLATED.size)).toEqual(["interpolated-arbitrary"]);
87
- expect(rules(INTERPOLATED.color)).toEqual(["interpolated-arbitrary"]);
88
- });
89
-
90
- test("flags the broken-bracket typo that swallows the next class", () => {
91
- expect(rules(INTERPOLATED.brokenBracket)).toEqual(["interpolated-arbitrary"]);
92
- });
93
-
94
- test("allows a literal arbitrary value, and interpolation outside brackets", () => {
95
- expect(scan('<div className="min-h-[300px] flex" />')).toHaveLength(0);
96
- expect(scan(INTERPOLATED.outsideBrackets)).toHaveLength(0);
97
- expect(scan(INTERPOLATED.styleProp)).toHaveLength(0);
98
- });
99
- });
100
-
101
- describe("StyleGuard violation shape", () => {
102
- test("reports 1-based line and trimmed snippet with a suggestion", () => {
103
- const content = ['<div className="ok" />', ' <div className="bg-blue-500" />'].join("\n");
104
- const [v] = scan(content);
105
- expect(v.line).toBe(2);
106
- expect(v.snippet).toBe('<div className="bg-blue-500" />');
107
- expect(v.severity).toBe("error");
108
- expect(v.suggestion.length).toBeGreaterThan(0);
109
- });
110
- });
111
-
112
- describe("StyleGuard comment handling", () => {
113
- test("does not flag class names inside line or block comments", () => {
114
- expect(scan('// iconClassName="btn-primary bg-blue-500"')).toHaveLength(0);
115
- expect(scan("/** legacy: toggle-accent / bg-red-500 */")).toHaveLength(0);
116
- expect(scan("{/* <div className='bg-blue-500' /> */}")).toHaveLength(0);
117
- });
118
-
119
- test("still flags real code on a line that also contains a string with //", () => {
120
- expect(rules('<a href="https://x.io" className="bg-blue-500" />')).toEqual(["raw-palette"]);
121
- });
122
- });
123
-
124
- describe("StyleGuard escape hatch", () => {
125
- test("styleguard-disable-next-line suppresses the following line only", () => {
126
- const content = [
127
- "// styleguard-disable-next-line raw-palette",
128
- '<div className="bg-blue-500" />',
129
- '<div className="bg-red-500" />',
130
- ].join("\n");
131
- const found = scan(content);
132
- expect(found).toHaveLength(1);
133
- expect(found[0].line).toBe(3);
134
- });
135
-
136
- test("file-level styleguard-disable suppresses the named rule everywhere", () => {
137
- const content = ["// styleguard-disable raw-palette", '<div className="bg-blue-500 bg-[#fff]" />'].join("\n");
138
- // raw-palette suppressed, arbitrary-color still reported.
139
- expect(rules(content)).toEqual(["arbitrary-color"]);
140
- });
141
-
142
- test("bare styleguard-disable suppresses all rules in the file", () => {
143
- const content = ["/* styleguard-disable */", '<div className="bg-blue-500 btn-primary bg-[#fff]" />'].join("\n");
144
- expect(scan(content)).toHaveLength(0);
145
- });
146
- });
147
-
148
- describe("StyleGuard.countNamedComponentClasses", () => {
149
- test("counts named classes declared inside @layer components", () => {
150
- const css = `
151
- @layer components {
152
- .foo { color: var(--primary); }
153
- .bar-baz { padding: 1rem; }
154
- }
155
- .outside { color: red; }
156
- `;
157
- const metric = StyleGuard.countNamedComponentClasses(css);
158
- expect(metric.count).toBe(2);
159
- expect(metric.names).toEqual(["bar-baz", "foo"]);
160
- });
161
-
162
- test("returns zero when no component layer exists", () => {
163
- expect(StyleGuard.countNamedComponentClasses(".a { color: red; }").count).toBe(0);
164
- });
165
- });