@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.
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanContext.ts +17 -3
- package/akanMcpContract.ts +25 -4
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +45 -0
- package/executors.ts +29 -5
- package/fileEditor.ts +19 -19
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- package/frontendBuild/buildRouteClient.test.ts +19 -22
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/index.ts +0 -1
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.proc.ts +6 -2
- package/index.ts +0 -5
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +2 -2
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +82 -13
- package/package.json +3 -3
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +64 -0
- package/qualityScanner.ts +43 -3
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +3 -3
- package/ssrScanner.test.ts +301 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +5 -1
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -180
- package/frontendBuild/styleGuard.ts +0 -339
- package/getCredentials.ts +0 -17
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { WorkflowExecutor, workflowStepKey } from "./executor";
|
|
3
|
+
import type {
|
|
4
|
+
PrimitiveChangedFile,
|
|
5
|
+
WorkflowDiagnostic,
|
|
6
|
+
WorkflowPlan,
|
|
7
|
+
WorkflowStep,
|
|
8
|
+
WorkflowStepRegistry,
|
|
9
|
+
WorkflowStepResult,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `WorkflowExecutor` takes its registry by constructor injection and its plan as data, so `apply` runs with
|
|
14
|
+
* no filesystem and no workspace — the `workspace` argument is what gates every post-apply check. These
|
|
15
|
+
* cover the ordering rules that `cli/workflow/workflow.test.ts` can only reach through a real scaffold.
|
|
16
|
+
*/
|
|
17
|
+
const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
|
|
18
|
+
id,
|
|
19
|
+
tool,
|
|
20
|
+
title: id,
|
|
21
|
+
description: id,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Every field spelled out rather than cast through `unknown`: `apply` reads `validation` before the first
|
|
25
|
+
// step runs, so a partial literal fails at runtime instead of at the type level.
|
|
26
|
+
const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []): WorkflowPlan => ({
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
workflow: "add-field",
|
|
29
|
+
mode: "plan",
|
|
30
|
+
inputs: {},
|
|
31
|
+
optionalSurfaces: {},
|
|
32
|
+
steps,
|
|
33
|
+
predictedChanges: [],
|
|
34
|
+
validation: [],
|
|
35
|
+
diagnostics,
|
|
36
|
+
recommendations: [],
|
|
37
|
+
requiresApproval: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A registry that records **which key** each call resolved through, not just that a step ran — the
|
|
42
|
+
* resolution order is the thing under test, and every candidate key would otherwise record the same step id.
|
|
43
|
+
*/
|
|
44
|
+
const changed = (path: string): PrimitiveChangedFile => ({ path, action: "modify", reason: "test" });
|
|
45
|
+
|
|
46
|
+
const recordingRegistry = (results: Record<string, WorkflowStepResult | undefined> = {}) => {
|
|
47
|
+
const calls: string[] = [];
|
|
48
|
+
const registry: WorkflowStepRegistry = {};
|
|
49
|
+
const on = (key: string) => {
|
|
50
|
+
registry[key] = async () => {
|
|
51
|
+
calls.push(key);
|
|
52
|
+
return results[key];
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
return { calls, registry, on };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("WorkflowExecutor.apply", () => {
|
|
59
|
+
test("runs no step when the plan already carries an error diagnostic", async () => {
|
|
60
|
+
const { calls, registry, on } = recordingRegistry();
|
|
61
|
+
on(workflowStepKey("add-field", "one"));
|
|
62
|
+
const plan = planOf([step("one")], [{ severity: "error", code: "plan-invalid", message: "bad inputs" }]);
|
|
63
|
+
|
|
64
|
+
const report = await new WorkflowExecutor(registry).apply(plan);
|
|
65
|
+
|
|
66
|
+
expect(calls).toEqual([]);
|
|
67
|
+
expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("plan-invalid");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a warning in the plan does not stop the steps", async () => {
|
|
71
|
+
const { calls, registry, on } = recordingRegistry();
|
|
72
|
+
on(workflowStepKey("add-field", "one"));
|
|
73
|
+
const plan = planOf([step("one")], [{ severity: "warning", code: "plan-note", message: "heads up" }]);
|
|
74
|
+
|
|
75
|
+
await new WorkflowExecutor(registry).apply(plan);
|
|
76
|
+
|
|
77
|
+
expect(calls).toEqual([workflowStepKey("add-field", "one")]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("resolves a runner by workflow-scoped key first, then tool, then bare step id", async () => {
|
|
81
|
+
const { calls, registry, on } = recordingRegistry();
|
|
82
|
+
// All three keys are registered for the same step, so only the winner records a call.
|
|
83
|
+
on(workflowStepKey("add-field", "scoped"));
|
|
84
|
+
on("tool:scoped");
|
|
85
|
+
on("scoped");
|
|
86
|
+
await new WorkflowExecutor(registry).apply(planOf([step("scoped")]));
|
|
87
|
+
expect(calls).toEqual([workflowStepKey("add-field", "scoped")]);
|
|
88
|
+
|
|
89
|
+
const byTool = recordingRegistry();
|
|
90
|
+
byTool.on("tool:viaTool");
|
|
91
|
+
byTool.on("viaTool");
|
|
92
|
+
await new WorkflowExecutor(byTool.registry).apply(planOf([step("viaTool")]));
|
|
93
|
+
expect(byTool.calls).toEqual(["tool:viaTool"]);
|
|
94
|
+
|
|
95
|
+
const byId = recordingRegistry();
|
|
96
|
+
byId.on("viaId");
|
|
97
|
+
await new WorkflowExecutor(byId.registry).apply(planOf([step("viaId")]));
|
|
98
|
+
expect(byId.calls).toEqual(["viaId"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("an unsupported step is reported and abandons the rest of the plan", async () => {
|
|
102
|
+
const { calls, registry, on } = recordingRegistry();
|
|
103
|
+
on(workflowStepKey("add-field", "first"));
|
|
104
|
+
on(workflowStepKey("add-field", "third"));
|
|
105
|
+
|
|
106
|
+
const report = await new WorkflowExecutor(registry).apply(planOf([step("first"), step("second"), step("third")]));
|
|
107
|
+
|
|
108
|
+
expect(calls).toEqual([workflowStepKey("add-field", "first")]);
|
|
109
|
+
expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("workflow-step-unsupported");
|
|
110
|
+
expect(report.nextActions.map((action) => action.command)).toContain("akan workflow explain add-field");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("accumulates changed files and diagnostics across steps, and stops at the first error", async () => {
|
|
114
|
+
const key = (id: string) => workflowStepKey("add-field", id);
|
|
115
|
+
const { calls, registry, on } = recordingRegistry({
|
|
116
|
+
[key("a")]: { changedFiles: [changed("apps/demo/lib/task/task.constant.ts")] },
|
|
117
|
+
[key("b")]: {
|
|
118
|
+
changedFiles: [changed("apps/demo/lib/task/task.dictionary.ts")],
|
|
119
|
+
diagnostics: [{ severity: "error", code: "step-failed", message: "no such module" }],
|
|
120
|
+
},
|
|
121
|
+
[key("c")]: { changedFiles: [changed("never-reached.ts")] },
|
|
122
|
+
});
|
|
123
|
+
on(key("a"));
|
|
124
|
+
on(key("b"));
|
|
125
|
+
on(key("c"));
|
|
126
|
+
|
|
127
|
+
const report = await new WorkflowExecutor(registry).apply(planOf([step("a"), step("b"), step("c")]));
|
|
128
|
+
|
|
129
|
+
expect(calls).toEqual([key("a"), key("b")]);
|
|
130
|
+
expect(report.changedFiles.map((file) => file.path)).toEqual([
|
|
131
|
+
"apps/demo/lib/task/task.constant.ts",
|
|
132
|
+
"apps/demo/lib/task/task.dictionary.ts",
|
|
133
|
+
]);
|
|
134
|
+
expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("step-failed");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a runner that answers nothing is not an error", async () => {
|
|
138
|
+
const { calls, registry, on } = recordingRegistry();
|
|
139
|
+
on(workflowStepKey("add-field", "silent"));
|
|
140
|
+
const report = await new WorkflowExecutor(registry).apply(planOf([step("silent")]));
|
|
141
|
+
|
|
142
|
+
expect(calls).toEqual([workflowStepKey("add-field", "silent")]);
|
|
143
|
+
expect(report.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]);
|
|
144
|
+
expect(report.changedFiles).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
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,180 +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
|
-
test("flags daisyUI colour slots the vocabulary dropped", () => {
|
|
72
|
-
expect(rules('<div className="bg-base-100" />')).toEqual(["daisyui-legacy"]);
|
|
73
|
-
expect(rules('<div className="text-base-content/70" />')).toEqual(["daisyui-legacy"]);
|
|
74
|
-
expect(rules('<div className="border-t-base-300" />')).toEqual(["daisyui-legacy"]);
|
|
75
|
-
expect(rules('<div className="text-primary-content" />')).toEqual(["daisyui-legacy"]);
|
|
76
|
-
expect(rules('<div className="border-error/30 bg-error/5" />')).toEqual(["daisyui-legacy", "daisyui-legacy"]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("does not flag colour slots that survived into the semantic vocabulary", () => {
|
|
80
|
-
expect(scan('<div className="bg-primary text-primary-foreground" />')).toHaveLength(0);
|
|
81
|
-
expect(scan('<div className="bg-neutral text-info border-warning" />')).toHaveLength(0);
|
|
82
|
-
expect(scan('<div className="bg-destructive/10 text-destructive" />')).toHaveLength(0);
|
|
83
|
-
expect(scan('<div className="content-center justify-content" />')).toHaveLength(0);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// The fixtures below must contain a literal `${`. Writing it inside a plain string trips biome's
|
|
88
|
-
// noTemplateCurlyInString, so the placeholder is assembled from `D` — that keeps the rule on for real code
|
|
89
|
-
// instead of scattering suppressions through the fixtures.
|
|
90
|
-
const D = "$";
|
|
91
|
-
const INTERPOLATED = {
|
|
92
|
-
size: `<div className={\`min-h-[${D}{minHeight}px] flex\`} />`,
|
|
93
|
-
color: `<div className={\`bg-[${D}{color}] w-full\`} />`,
|
|
94
|
-
brokenBracket: `<div className={\`min-h-[ w-full${D}{minHeight}px] flex\`} />`,
|
|
95
|
-
outsideBrackets: `<div className={\`flex gap-2 ${D}{isOpen ? "opacity-50" : ""}\`} />`,
|
|
96
|
-
styleProp: `<div style={{ minHeight }} className={\`flex ${D}{extra}\`} />`,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
describe("StyleGuard interpolated-arbitrary", () => {
|
|
100
|
-
test("flags an arbitrary value assembled from a runtime expression", () => {
|
|
101
|
-
expect(rules(INTERPOLATED.size)).toEqual(["interpolated-arbitrary"]);
|
|
102
|
-
expect(rules(INTERPOLATED.color)).toEqual(["interpolated-arbitrary"]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("flags the broken-bracket typo that swallows the next class", () => {
|
|
106
|
-
expect(rules(INTERPOLATED.brokenBracket)).toEqual(["interpolated-arbitrary"]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("allows a literal arbitrary value, and interpolation outside brackets", () => {
|
|
110
|
-
expect(scan('<div className="min-h-[300px] flex" />')).toHaveLength(0);
|
|
111
|
-
expect(scan(INTERPOLATED.outsideBrackets)).toHaveLength(0);
|
|
112
|
-
expect(scan(INTERPOLATED.styleProp)).toHaveLength(0);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("StyleGuard violation shape", () => {
|
|
117
|
-
test("reports 1-based line and trimmed snippet with a suggestion", () => {
|
|
118
|
-
const content = ['<div className="ok" />', ' <div className="bg-blue-500" />'].join("\n");
|
|
119
|
-
const [v] = scan(content);
|
|
120
|
-
expect(v.line).toBe(2);
|
|
121
|
-
expect(v.snippet).toBe('<div className="bg-blue-500" />');
|
|
122
|
-
expect(v.severity).toBe("error");
|
|
123
|
-
expect(v.suggestion.length).toBeGreaterThan(0);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("StyleGuard comment handling", () => {
|
|
128
|
-
test("does not flag class names inside line or block comments", () => {
|
|
129
|
-
expect(scan('// iconClassName="btn-primary bg-blue-500"')).toHaveLength(0);
|
|
130
|
-
expect(scan("/** legacy: toggle-accent / bg-red-500 */")).toHaveLength(0);
|
|
131
|
-
expect(scan("{/* <div className='bg-blue-500' /> */}")).toHaveLength(0);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("still flags real code on a line that also contains a string with //", () => {
|
|
135
|
-
expect(rules('<a href="https://x.io" className="bg-blue-500" />')).toEqual(["raw-palette"]);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("StyleGuard escape hatch", () => {
|
|
140
|
-
test("styleguard-disable-next-line suppresses the following line only", () => {
|
|
141
|
-
const content = [
|
|
142
|
-
"// styleguard-disable-next-line raw-palette",
|
|
143
|
-
'<div className="bg-blue-500" />',
|
|
144
|
-
'<div className="bg-red-500" />',
|
|
145
|
-
].join("\n");
|
|
146
|
-
const found = scan(content);
|
|
147
|
-
expect(found).toHaveLength(1);
|
|
148
|
-
expect(found[0].line).toBe(3);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("file-level styleguard-disable suppresses the named rule everywhere", () => {
|
|
152
|
-
const content = ["// styleguard-disable raw-palette", '<div className="bg-blue-500 bg-[#fff]" />'].join("\n");
|
|
153
|
-
// raw-palette suppressed, arbitrary-color still reported.
|
|
154
|
-
expect(rules(content)).toEqual(["arbitrary-color"]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("bare styleguard-disable suppresses all rules in the file", () => {
|
|
158
|
-
const content = ["/* styleguard-disable */", '<div className="bg-blue-500 btn-primary bg-[#fff]" />'].join("\n");
|
|
159
|
-
expect(scan(content)).toHaveLength(0);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe("StyleGuard.countNamedComponentClasses", () => {
|
|
164
|
-
test("counts named classes declared inside @layer components", () => {
|
|
165
|
-
const css = `
|
|
166
|
-
@layer components {
|
|
167
|
-
.foo { color: var(--primary); }
|
|
168
|
-
.bar-baz { padding: 1rem; }
|
|
169
|
-
}
|
|
170
|
-
.outside { color: red; }
|
|
171
|
-
`;
|
|
172
|
-
const metric = StyleGuard.countNamedComponentClasses(css);
|
|
173
|
-
expect(metric.count).toBe(2);
|
|
174
|
-
expect(metric.names).toEqual(["bar-baz", "foo"]);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("returns zero when no component layer exists", () => {
|
|
178
|
-
expect(StyleGuard.countNamedComponentClasses(".a { color: red; }").count).toBe(0);
|
|
179
|
-
});
|
|
180
|
-
});
|