@raypx-kit/devkit 0.0.0-alpha.1 → 0.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @raypx-kit/devkit
2
2
 
3
+ ## 0.0.0-alpha.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 0b904b6: Republish through the CI release pipeline with npm provenance. No functional changes.
8
+
9
+ ## 0.0.0-alpha.2
10
+
11
+ ### Minor Changes
12
+
13
+ - 5515e5f: Support Bun-only repository toolchain checks and active runtime validation without
14
+ requiring Node pins. Preserve the existing Node/Bun defaults. Export independent
15
+ Vite+ lint presets and plain Bun test defaults for consumer-owned configuration.
16
+
3
17
  ## 0.0.0-alpha.1
4
18
 
5
19
  ### Major Changes
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  Maintenance checks and task helpers for Bun monorepos: workspace boundaries,
4
4
  toolchain pins, static message catalogs, documentation links and relocated build
5
5
  artifacts.
6
+ The package also exports plain Vite+ lint presets and Bun test defaults.
6
7
 
7
8
  ```sh
8
9
  bun add -d @raypx-kit/devkit
@@ -21,6 +22,8 @@ contains no product rules, so repositories compose their own policy from it.
21
22
  | `./docs` | Report broken relative Markdown links |
22
23
  | `./process` | Run commands, load dotenv files and run Turbo with a guarded cache |
23
24
  | `./artifact` | Stage a relocated output and probe a running Node server |
25
+ | `./lint` | Base and opt-in strict Vite+ lint presets without repository paths |
26
+ | `./testing` | Plain Vitest defaults for Bun workers |
24
27
 
25
28
  ## Workspace architecture
26
29
 
@@ -147,6 +150,71 @@ documented `Node x.y.z` and `Bun x.y.z` versions and Turbo global dependencies.
147
150
  `pinnedNode` returns the Node executable after checking its version; the binary
148
151
  variable selects an executable but never relaxes the version.
149
152
 
153
+ ### Bun-only repositories
154
+
155
+ The default policy still requires Node and Bun. Repositories that run JavaScript
156
+ only with Bun can select that policy explicitly:
157
+
158
+ ```ts
159
+ import { assertBunRuntime, checkToolchain, readBunPin } from "@raypx-kit/devkit/toolchain";
160
+
161
+ const findings = checkToolchain(root, { runtime: "bun", documents: ["README.md"] });
162
+ const version = readBunPin(root);
163
+ const { bun } = assertBunRuntime(root);
164
+ ```
165
+
166
+ This mode reads only `package.json#packageManager`. It checks `devEngines`, Bun
167
+ types, mise, Dockerfiles, GitHub workflows/composite actions and selected docs.
168
+ It rejects Node engine declarations, Node version files, Node setup actions and
169
+ Node runtime images. Node API types remain allowed without a Node runtime pin.
170
+ `readBunPin` and `assertBunRuntime` never read `.nvmrc` or execute Node.
171
+
172
+ ### Lint and test presets
173
+
174
+ With Vite+ 1.3 or later, import the independent configuration entries. They contain
175
+ plain data and do not load Vite+, application plugins, test setup or repository
176
+ configuration. Their declarations do not require Vite+ in unrelated consumers.
177
+
178
+ ```ts
179
+ // vite.config.ts
180
+ import { defineConfig } from "vite-plus";
181
+ import { strictLint } from "@raypx-kit/devkit/lint";
182
+
183
+ export default defineConfig({
184
+ lint: {
185
+ ...strictLint,
186
+ ignorePatterns: ["**/dist/**", "**/*.gen.ts"],
187
+ rules: { ...strictLint.rules, "react/no-danger": "off" },
188
+ },
189
+ });
190
+ ```
191
+
192
+ `baseLint` provides type-aware checking and the Vite+ import rule. `strictLint`
193
+ adds TypeScript, React, accessibility, promise and import correctness checks;
194
+ adopt it explicitly because stricter rules can introduce findings. Neither
195
+ preset chooses generated files, workspace paths or project exceptions.
196
+ Consumers own formatting and staged Git commands as well.
197
+
198
+ ```ts
199
+ // vitest.config.ts
200
+ import { defineConfig, mergeConfig } from "vite-plus/test/config";
201
+ import { bunTestDefaults } from "@raypx-kit/devkit/testing";
202
+
203
+ export default mergeConfig(
204
+ bunTestDefaults,
205
+ defineConfig({
206
+ test: { include: ["tests/**/*.test.ts"], setupFiles: ["./tests/setup.ts"] },
207
+ }),
208
+ );
209
+ ```
210
+
211
+ `bunTestDefaults` selects the Node environment, one worker, Bun's `--no-env-file`
212
+ and `--no-install` flags, and V8 coverage with text/JSON/LCOV reporters. Execute
213
+ tests with `bun --bun vp test --run`; install the matching coverage provider when
214
+ requesting coverage. Suites, DOM environments, setup files, React deduplication
215
+ and application fixtures remain consumer-owned. Use native `mergeConfig` for
216
+ overrides; no configuration factory or custom option schema is required.
217
+
150
218
  ## Catalogs and documentation
151
219
 
152
220
  ```ts
@@ -0,0 +1,68 @@
1
+ /** Vite+ checks shared by libraries and applications; paths belong to the consumer. */
2
+ export declare const baseLint: {
3
+ jsPlugins: {
4
+ name: string;
5
+ specifier: string;
6
+ }[];
7
+ options: {
8
+ typeAware: true;
9
+ typeCheck: true;
10
+ };
11
+ rules: {
12
+ "vite-plus/prefer-vite-plus-imports": "error";
13
+ };
14
+ };
15
+ /** Opt-in correctness rules for TypeScript and React repositories. */
16
+ export declare const strictLint: {
17
+ jsPlugins: {
18
+ name: string;
19
+ specifier: string;
20
+ }[];
21
+ plugins: ("import" | "jsx-a11y" | "node" | "oxc" | "promise" | "react" | "typescript" | "unicorn" | "vitest")[];
22
+ categories: {
23
+ correctness: "error";
24
+ suspicious: "error";
25
+ };
26
+ options: {
27
+ typeAware: true;
28
+ typeCheck: true;
29
+ reportUnusedDisableDirectives: "error";
30
+ };
31
+ rules: {
32
+ "vite-plus/prefer-vite-plus-imports": "error";
33
+ "array-callback-return": "error";
34
+ eqeqeq: "error";
35
+ "prefer-const": "error";
36
+ "no-duplicate-imports": ["error", {
37
+ allowSeparateTypeImports: true;
38
+ }];
39
+ "import/no-cycle": "error";
40
+ "oxc/no-accumulating-spread": "error";
41
+ "react/rules-of-hooks": "error";
42
+ "react/no-danger": "error";
43
+ "typescript/consistent-type-imports": ["error", {
44
+ disallowTypeAnnotations: false;
45
+ }];
46
+ "typescript/switch-exhaustiveness-check": "error";
47
+ "import/no-unassigned-import": "off";
48
+ "import/no-named-as-default": "off";
49
+ "import/no-named-as-default-member": "off";
50
+ "promise/always-return": "off";
51
+ "promise/no-callback-in-promise": "off";
52
+ "promise/no-promise-in-callback": "off";
53
+ "react/exhaustive-effect-dependencies": "off";
54
+ "react/react-in-jsx-scope": "off";
55
+ "vitest/require-mock-type-parameters": "off";
56
+ "vitest/warn-todo": "off";
57
+ "vitest/prefer-snapshot-hint": "off";
58
+ "typescript/consistent-return": "off";
59
+ "typescript/no-unsafe-type-assertion": "off";
60
+ "unicorn/no-array-sort": "off";
61
+ "unicorn/no-array-reverse": "off";
62
+ "no-underscore-dangle": "off";
63
+ "unicorn/consistent-function-scoping": "off";
64
+ "jsx-a11y/prefer-tag-over-role": "off";
65
+ "jsx-a11y/no-autofocus": "off";
66
+ };
67
+ };
68
+ //# sourceMappingURL=lint.d.mts.map
package/dist/lint.mjs ADDED
@@ -0,0 +1,71 @@
1
+ // src/lint.ts
2
+ /** Vite+ checks shared by libraries and applications; paths belong to the consumer. */
3
+ const baseLint = {
4
+ jsPlugins: [{
5
+ name: "vite-plus",
6
+ specifier: "vite-plus/oxlint-plugin"
7
+ }],
8
+ options: {
9
+ typeAware: true,
10
+ typeCheck: true
11
+ },
12
+ rules: { "vite-plus/prefer-vite-plus-imports": "error" }
13
+ };
14
+ /** Opt-in correctness rules for TypeScript and React repositories. */
15
+ const strictLint = {
16
+ ...baseLint,
17
+ plugins: [
18
+ "typescript",
19
+ "unicorn",
20
+ "oxc",
21
+ "import",
22
+ "react",
23
+ "vitest",
24
+ "jsx-a11y",
25
+ "promise",
26
+ "node"
27
+ ],
28
+ categories: {
29
+ correctness: "error",
30
+ suspicious: "error"
31
+ },
32
+ options: {
33
+ ...baseLint.options,
34
+ reportUnusedDisableDirectives: "error"
35
+ },
36
+ rules: {
37
+ ...baseLint.rules,
38
+ "array-callback-return": "error",
39
+ eqeqeq: "error",
40
+ "prefer-const": "error",
41
+ "no-duplicate-imports": ["error", { allowSeparateTypeImports: true }],
42
+ "import/no-cycle": "error",
43
+ "oxc/no-accumulating-spread": "error",
44
+ "react/rules-of-hooks": "error",
45
+ "react/no-danger": "error",
46
+ "typescript/consistent-type-imports": ["error", { disallowTypeAnnotations: false }],
47
+ "typescript/switch-exhaustiveness-check": "error",
48
+ "import/no-unassigned-import": "off",
49
+ "import/no-named-as-default": "off",
50
+ "import/no-named-as-default-member": "off",
51
+ "promise/always-return": "off",
52
+ "promise/no-callback-in-promise": "off",
53
+ "promise/no-promise-in-callback": "off",
54
+ "react/exhaustive-effect-dependencies": "off",
55
+ "react/react-in-jsx-scope": "off",
56
+ "vitest/require-mock-type-parameters": "off",
57
+ "vitest/warn-todo": "off",
58
+ "vitest/prefer-snapshot-hint": "off",
59
+ "typescript/consistent-return": "off",
60
+ "typescript/no-unsafe-type-assertion": "off",
61
+ "unicorn/no-array-sort": "off",
62
+ "unicorn/no-array-reverse": "off",
63
+ "no-underscore-dangle": "off",
64
+ "unicorn/consistent-function-scoping": "off",
65
+ "jsx-a11y/prefer-tag-over-role": "off",
66
+ "jsx-a11y/no-autofocus": "off"
67
+ }
68
+ };
69
+ export { baseLint, strictLint };
70
+
71
+ //# sourceMappingURL=lint.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.mjs","names":[],"sources":["../src/lint.ts"],"sourcesContent":["import type { ViteUserConfig } from \"vite-plus\";\n\n/** Vite+ checks shared by libraries and applications; paths belong to the consumer. */\nexport const baseLint = {\n jsPlugins: [{ name: \"vite-plus\", specifier: \"vite-plus/oxlint-plugin\" }],\n options: { typeAware: true, typeCheck: true },\n rules: { \"vite-plus/prefer-vite-plus-imports\": \"error\" },\n} satisfies NonNullable<ViteUserConfig[\"lint\"]>;\n\n/** Opt-in correctness rules for TypeScript and React repositories. */\nexport const strictLint = {\n ...baseLint,\n plugins: [\n \"typescript\",\n \"unicorn\",\n \"oxc\",\n \"import\",\n \"react\",\n \"vitest\",\n \"jsx-a11y\",\n \"promise\",\n \"node\",\n ],\n categories: { correctness: \"error\", suspicious: \"error\" },\n options: { ...baseLint.options, reportUnusedDisableDirectives: \"error\" },\n rules: {\n ...baseLint.rules,\n \"array-callback-return\": \"error\",\n eqeqeq: \"error\",\n \"prefer-const\": \"error\",\n \"no-duplicate-imports\": [\"error\", { allowSeparateTypeImports: true }],\n \"import/no-cycle\": \"error\",\n \"oxc/no-accumulating-spread\": \"error\",\n \"react/rules-of-hooks\": \"error\",\n \"react/no-danger\": \"error\",\n \"typescript/consistent-type-imports\": [\"error\", { disallowTypeAnnotations: false }],\n \"typescript/switch-exhaustiveness-check\": \"error\",\n // Bootstrap imports, third-party defaults and callback adapters are valid.\n \"import/no-unassigned-import\": \"off\",\n \"import/no-named-as-default\": \"off\",\n \"import/no-named-as-default-member\": \"off\",\n \"promise/always-return\": \"off\",\n \"promise/no-callback-in-promise\": \"off\",\n \"promise/no-promise-in-callback\": \"off\",\n \"react/exhaustive-effect-dependencies\": \"off\",\n \"react/react-in-jsx-scope\": \"off\",\n \"vitest/require-mock-type-parameters\": \"off\",\n \"vitest/warn-todo\": \"off\",\n \"vitest/prefer-snapshot-hint\": \"off\",\n \"typescript/consistent-return\": \"off\",\n \"typescript/no-unsafe-type-assertion\": \"off\",\n // ES2022 consumers do not have to polyfill the copying array methods.\n \"unicorn/no-array-sort\": \"off\",\n \"unicorn/no-array-reverse\": \"off\",\n // Keep naming, helper placement and deliberate focus under local control.\n \"no-underscore-dangle\": \"off\",\n \"unicorn/consistent-function-scoping\": \"off\",\n \"jsx-a11y/prefer-tag-over-role\": \"off\",\n \"jsx-a11y/no-autofocus\": \"off\",\n },\n} satisfies NonNullable<ViteUserConfig[\"lint\"]>;\n"],"mappings":";;AAGA,MAAa,WAAW;CACtB,WAAW,CAAC;EAAE,MAAM;EAAa,WAAW;CAA0B,CAAC;CACvE,SAAS;EAAE,WAAW;EAAM,WAAW;CAAK;CAC5C,OAAO,EAAE,sCAAsC,QAAQ;AACzD;;AAGA,MAAa,aAAa;CACxB,GAAG;CACH,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,YAAY;EAAE,aAAa;EAAS,YAAY;CAAQ;CACxD,SAAS;EAAE,GAAG,SAAS;EAAS,+BAA+B;CAAQ;CACvE,OAAO;EACL,GAAG,SAAS;EACZ,yBAAyB;EACzB,QAAQ;EACR,gBAAgB;EAChB,wBAAwB,CAAC,SAAS,EAAE,0BAA0B,KAAK,CAAC;EACpE,mBAAmB;EACnB,8BAA8B;EAC9B,wBAAwB;EACxB,mBAAmB;EACnB,sCAAsC,CAAC,SAAS,EAAE,yBAAyB,MAAM,CAAC;EAClF,0CAA0C;EAE1C,+BAA+B;EAC/B,8BAA8B;EAC9B,qCAAqC;EACrC,yBAAyB;EACzB,kCAAkC;EAClC,kCAAkC;EAClC,wCAAwC;EACxC,4BAA4B;EAC5B,uCAAuC;EACvC,oBAAoB;EACpB,+BAA+B;EAC/B,gCAAgC;EAChC,uCAAuC;EAEvC,yBAAyB;EACzB,4BAA4B;EAE5B,wBAAwB;EACxB,uCAAuC;EACvC,iCAAiC;EACjC,yBAAyB;CAC3B;AACF"}
@@ -0,0 +1,13 @@
1
+ /** Plain Vitest defaults for Bun workers; consumers own suites, setup and environments. */
2
+ export declare const bunTestDefaults: {
3
+ test: {
4
+ environment: "node";
5
+ maxWorkers: number;
6
+ execArgv: string[];
7
+ coverage: {
8
+ provider: "v8";
9
+ reporter: ("json" | "lcov" | "text")[];
10
+ };
11
+ };
12
+ };
13
+ //# sourceMappingURL=testing.d.mts.map
@@ -0,0 +1,18 @@
1
+ // src/testing.ts
2
+ /** Plain Vitest defaults for Bun workers; consumers own suites, setup and environments. */
3
+ const bunTestDefaults = { test: {
4
+ environment: "node",
5
+ maxWorkers: 1,
6
+ execArgv: ["--no-env-file", "--no-install"],
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: [
10
+ "text",
11
+ "json",
12
+ "lcov"
13
+ ]
14
+ }
15
+ } };
16
+ export { bunTestDefaults };
17
+
18
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.mjs","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import type { ViteUserConfig } from \"vite-plus\";\n\n/** Plain Vitest defaults for Bun workers; consumers own suites, setup and environments. */\nexport const bunTestDefaults = {\n test: {\n environment: \"node\",\n maxWorkers: 1,\n execArgv: [\"--no-env-file\", \"--no-install\"],\n coverage: { provider: \"v8\", reporter: [\"text\", \"json\", \"lcov\"] },\n },\n} satisfies ViteUserConfig;\n"],"mappings":";;AAGA,MAAa,kBAAkB,EAC7B,MAAM;CACJ,aAAa;CACb,YAAY;CACZ,UAAU,CAAC,iBAAiB,cAAc;CAC1C,UAAU;EAAE,UAAU;EAAM,UAAU;GAAC;GAAQ;GAAQ;EAAM;CAAE;AACjE,EACF"}
@@ -3,6 +3,8 @@ export interface Toolchain {
3
3
  bun: string;
4
4
  }
5
5
  export interface ToolchainOptions {
6
+ /** Bun-only repositories forbid Node runtime pins; the default checks both runtimes. */
7
+ runtime?: "node-bun" | "bun";
6
8
  /** Repository-relative documents whose `Node x.y.z` and `Bun x.y.z` mentions must match. */
7
9
  documents?: readonly string[];
8
10
  /** Files every Turbo cache key must include when `turbo.json` exists. */
@@ -12,6 +14,12 @@ export interface RuntimeOptions {
12
14
  /** Environment variable that may select a Node executable outside PATH. */
13
15
  nodeBinaryEnv?: string;
14
16
  }
17
+ /** Read the exact Bun pin without requiring a Node development runtime. */
18
+ export declare function readBunPin(root: string): string;
19
+ /** Check the active Bun runtime without executing Node or inspected repository code. */
20
+ export declare function assertBunRuntime(root: string): {
21
+ bun: string;
22
+ };
15
23
  /** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */
16
24
  export declare function readToolchain(root: string): Toolchain;
17
25
  /** Returns the Node executable after confirming it provides the pinned release. */
@@ -21,5 +29,5 @@ export declare function assertToolchainRuntime(root: string, options?: RuntimeOp
21
29
  nodeBinary: string;
22
30
  };
23
31
  /** Check duplicated declarations without installing tools or running application code. */
24
- export declare function checkToolchain(root: string, { documents, turboGlobalDependencies }?: ToolchainOptions): string[];
32
+ export declare function checkToolchain(root: string, { runtime, documents, turboGlobalDependencies }?: ToolchainOptions): string[];
25
33
  //# sourceMappingURL=toolchain.d.mts.map
@@ -13,10 +13,26 @@ const catalogsOf = (manifest) => {
13
13
  const { catalog, catalogs } = workspaces;
14
14
  return [...catalog ? [catalog] : [], ...Object.values(catalogs ?? {})];
15
15
  };
16
+ const bunVersion = (root) => /^bun@(\d+\.\d+\.\d+)$/.exec(manifestAt(root).packageManager ?? "")?.[1];
17
+ /** Read the exact Bun pin without requiring a Node development runtime. */
18
+ function readBunPin(root) {
19
+ const bun = bunVersion(root);
20
+ if (!bun) throw new Error("Pin an exact Bun version in package.json packageManager.");
21
+ return bun;
22
+ }
23
+ function assertBunVersion(bun) {
24
+ if (process.versions.bun !== bun) throw new Error(`Expected Bun ${bun}, got ${process.versions.bun ?? "none"}.`);
25
+ }
26
+ /** Check the active Bun runtime without executing Node or inspected repository code. */
27
+ function assertBunRuntime(root) {
28
+ const bun = readBunPin(root);
29
+ assertBunVersion(bun);
30
+ return { bun };
31
+ }
16
32
  /** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */
17
33
  function readToolchain(root) {
18
34
  const node = readFileSync(join(root, ".nvmrc"), "utf8").trim().replace(/^v/, "");
19
- const bun = /^bun@(.+)$/.exec(manifestAt(root).packageManager ?? "")?.[1] ?? "";
35
+ const bun = bunVersion(root) ?? "";
20
36
  if (!exactVersion.test(node) || !exactVersion.test(bun)) throw new Error("Pin exact Node and Bun versions in .nvmrc and package.json packageManager.");
21
37
  return {
22
38
  node,
@@ -42,7 +58,7 @@ function pinnedNode(root, { nodeBinaryEnv = "DEVKIT_NODE_BINARY" } = {}) {
42
58
  /** The binary override changes the consumer executable, never the required version. */
43
59
  function assertToolchainRuntime(root, options = {}) {
44
60
  const pins = readToolchain(root);
45
- if (process.versions.bun !== pins.bun) throw new Error(`Expected Bun ${pins.bun}, got ${process.versions.bun ?? "none"}.`);
61
+ assertBunVersion(pins.bun);
46
62
  return {
47
63
  ...pins,
48
64
  nodeBinary: pinnedNode(root, options)
@@ -51,7 +67,7 @@ function assertToolchainRuntime(root, options = {}) {
51
67
  function isRecord(value) {
52
68
  return typeof value === "object" && value !== null && !Array.isArray(value);
53
69
  }
54
- function checkMise(root) {
70
+ function checkMise(root, bunOnly) {
55
71
  const file = "mise.toml";
56
72
  const path = join(root, file);
57
73
  if (!existsSync(path)) return [`${file}: missing runtime manager configuration`];
@@ -65,10 +81,11 @@ function checkMise(root) {
65
81
  const findings = [];
66
82
  if (Object.hasOwn(mise, "tools")) findings.push(`${file}: read existing pins instead of declaring [tools] versions`);
67
83
  const tools = isRecord(mise.settings) ? mise.settings.idiomatic_version_file_enable_tools : void 0;
68
- if (!Array.isArray(tools) || tools.length !== 2 || !["node", "bun"].every((tool) => tools.includes(tool))) findings.push(`${file}: enable idiomatic version files for node and bun`);
84
+ const expected = bunOnly ? ["bun"] : ["node", "bun"];
85
+ if (!Array.isArray(tools) || tools.length !== expected.length || !expected.every((tool) => tools.includes(tool))) findings.push(`${file}: enable idiomatic version files for ${bunOnly ? "bun only" : "node and bun"}`);
69
86
  return findings;
70
87
  }
71
- function checkSetupStep(file, location, step) {
88
+ function checkSetupStep(file, location, step, bunOnly) {
72
89
  if (typeof step.uses !== "string") return [];
73
90
  const refSeparator = step.uses.indexOf("@");
74
91
  if (refSeparator < 0) return [];
@@ -84,6 +101,7 @@ function checkSetupStep(file, location, step) {
84
101
  ]].find(([, action]) => repository === action);
85
102
  if (!setup) return [];
86
103
  const [tool, , source] = setup;
104
+ if (bunOnly && tool === "node") return [`${file}: remove actions/setup-node; Bun is the only runtime`];
87
105
  const findings = [];
88
106
  if (step.with !== void 0 && !isRecord(step.with)) findings.push(`${file}: ${location}.with must be a mapping`);
89
107
  const inputs = isRecord(step.with) ? step.with : {};
@@ -93,7 +111,7 @@ function checkSetupStep(file, location, step) {
93
111
  else if (versionFile !== source) findings.push(`${file}: ${tool}-version-file must be ${source}`);
94
112
  return findings;
95
113
  }
96
- function checkSetupSteps(file, location, steps) {
114
+ function checkSetupSteps(file, location, steps, bunOnly) {
97
115
  if (!Array.isArray(steps)) return [`${file}: ${location} must be an array`];
98
116
  const findings = [];
99
117
  for (const [index, step] of steps.entries()) {
@@ -102,11 +120,11 @@ function checkSetupSteps(file, location, steps) {
102
120
  findings.push(`${file}: ${stepLocation} must be a mapping`);
103
121
  continue;
104
122
  }
105
- findings.push(...checkSetupStep(file, stepLocation, step));
123
+ findings.push(...checkSetupStep(file, stepLocation, step, bunOnly));
106
124
  }
107
125
  return findings;
108
126
  }
109
- function checkWorkflows(root) {
127
+ function checkWorkflows(root, bunOnly) {
110
128
  const findings = [];
111
129
  for (const file of globSync(".github/workflows/*.{yml,yaml}", { cwd: root })) {
112
130
  let workflow;
@@ -127,12 +145,12 @@ function checkWorkflows(root) {
127
145
  continue;
128
146
  }
129
147
  if (job.steps === void 0) continue;
130
- findings.push(...checkSetupSteps(file, `${location}.steps`, job.steps));
148
+ findings.push(...checkSetupSteps(file, `${location}.steps`, job.steps, bunOnly));
131
149
  }
132
150
  }
133
151
  return findings;
134
152
  }
135
- function checkCompositeActions(root) {
153
+ function checkCompositeActions(root, bunOnly) {
136
154
  const findings = [];
137
155
  for (const file of globSync(".github/actions/**/action.{yml,yaml}", { cwd: root })) {
138
156
  let action;
@@ -147,15 +165,16 @@ function checkCompositeActions(root) {
147
165
  continue;
148
166
  }
149
167
  if (action.runs.using !== "composite") continue;
150
- findings.push(...checkSetupSteps(file, "runs.steps", action.runs.steps));
168
+ findings.push(...checkSetupSteps(file, "runs.steps", action.runs.steps, bunOnly));
151
169
  }
152
170
  return findings;
153
171
  }
154
172
  /** Check duplicated declarations without installing tools or running application code. */
155
- function checkToolchain(root, { documents = ["README.md", "AGENTS.md"], turboGlobalDependencies = [".nvmrc", "mise.toml"] } = {}) {
173
+ function checkToolchain(root, { runtime = "node-bun", documents = ["README.md", "AGENTS.md"], turboGlobalDependencies = runtime === "bun" ? ["mise.toml"] : [".nvmrc", "mise.toml"] } = {}) {
174
+ const bunOnly = runtime === "bun";
156
175
  let pins;
157
176
  try {
158
- pins = readToolchain(root);
177
+ pins = bunOnly ? { bun: readBunPin(root) } : readToolchain(root);
159
178
  } catch (error) {
160
179
  return [error instanceof Error ? error.message : String(error)];
161
180
  }
@@ -164,23 +183,26 @@ function checkToolchain(root, { documents = ["README.md", "AGENTS.md"], turboGlo
164
183
  const expect = (file, label, actual, expected) => {
165
184
  if (actual !== expected) findings.push(`${file}: ${label} is ${actual ?? "missing"}, expected ${expected}`);
166
185
  };
167
- expect("package.json", "engines.node", manifest.engines?.node, pins.node);
186
+ if (bunOnly) {
187
+ if (manifest.engines?.node !== void 0) findings.push("package.json: remove engines.node; Bun is the only runtime");
188
+ } else expect("package.json", "engines.node", manifest.engines?.node, pins.node);
168
189
  for (const key of ["packageManager", "runtime"]) {
169
190
  const engine = manifest.devEngines?.[key];
170
191
  expect("package.json", `devEngines.${key}`, `${engine?.name}@${engine?.version}`, `bun@${pins.bun}`);
171
192
  expect("package.json", `devEngines.${key}.onFail`, engine?.onFail, "error");
172
193
  }
173
194
  for (const file of [
195
+ ...bunOnly ? [".nvmrc"] : [],
174
196
  ".node-version",
175
197
  ".bun-version",
176
198
  ".tool-versions"
177
- ]) if (existsSync(join(root, file))) findings.push(`${file}: use .nvmrc and package.json instead of a second version source`);
178
- findings.push(...checkMise(root));
179
- const nodeMajor = pins.node.split(".")[0];
199
+ ]) if (existsSync(join(root, file))) findings.push(bunOnly ? `${file}: package.json packageManager is the only runtime pin` : `${file}: use .nvmrc and package.json instead of a second version source`);
200
+ findings.push(...checkMise(root, bunOnly));
201
+ const nodeMajor = pins.node?.split(".")[0];
180
202
  const version = (range) => range && !/^[a-z]+:/.test(range) ? range.replace(/^[\^~]/, "") : void 0;
181
203
  const checkTypes = (file, dependencies) => {
182
204
  const node = version(dependencies?.["@types/node"]);
183
- if (node && node.split(".")[0] !== nodeMajor) findings.push(`${file}: @types/node must match the Node ${nodeMajor} major`);
205
+ if (nodeMajor && node && node.split(".")[0] !== nodeMajor) findings.push(`${file}: @types/node must match the Node ${nodeMajor} major`);
184
206
  const bun = version(dependencies?.["@types/bun"]);
185
207
  if (bun) expect(file, "@types/bun", bun, pins.bun);
186
208
  };
@@ -191,14 +213,17 @@ function checkToolchain(root, { documents = ["README.md", "AGENTS.md"], turboGlo
191
213
  exclude: ["**/node_modules/**"]
192
214
  })) {
193
215
  const source = readFileSync(join(root, file), "utf8");
194
- for (const [, version] of source.matchAll(/^FROM\s+node:(\d+\.\d+\.\d+)/gm)) expect(file, "node image", version, pins.node);
216
+ if (bunOnly) {
217
+ if (/^\s*FROM\s+(?:--platform=\S+\s+)?node:/im.test(source)) findings.push(`${file}: build and run on oven/bun images`);
218
+ } else for (const [, version] of source.matchAll(/^FROM\s+node:(\d+\.\d+\.\d+)/gm)) expect(file, "node image", version, pins.node);
195
219
  for (const [, version] of source.matchAll(/^FROM\s+oven\/bun:(\d+\.\d+\.\d+)/gm)) expect(file, "bun image", version, pins.bun);
196
220
  }
197
- findings.push(...checkWorkflows(root), ...checkCompositeActions(root));
221
+ findings.push(...checkWorkflows(root, bunOnly), ...checkCompositeActions(root, bunOnly));
198
222
  for (const file of documents) {
199
223
  if (!existsSync(join(root, file))) continue;
200
224
  const document = readFileSync(join(root, file), "utf8").replaceAll("**", "");
201
- for (const [, tool, version] of document.matchAll(/\b(Node|Bun) v?(\d+\.\d+\.\d+)\b/g)) expect(file, `documented ${tool}`, version, tool === "Node" ? pins.node : pins.bun);
225
+ for (const [, tool, version] of document.matchAll(/\b(Node|Bun) v?(\d+\.\d+\.\d+)\b/g)) if (bunOnly && tool === "Node") findings.push(`${file}: documents a Node release; Bun is the only runtime`);
226
+ else expect(file, `documented ${tool}`, version, tool === "Node" ? pins.node : pins.bun);
202
227
  }
203
228
  const turboFile = join(root, "turbo.json");
204
229
  if (existsSync(turboFile)) {
@@ -207,6 +232,6 @@ function checkToolchain(root, { documents = ["README.md", "AGENTS.md"], turboGlo
207
232
  }
208
233
  return [...new Set(findings)];
209
234
  }
210
- export { assertToolchainRuntime, checkToolchain, pinnedNode, readToolchain };
235
+ export { assertBunRuntime, assertToolchainRuntime, checkToolchain, pinnedNode, readBunPin, readToolchain };
211
236
 
212
237
  //# sourceMappingURL=toolchain.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"toolchain.mjs","names":[],"sources":["../src/toolchain.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { existsSync, globSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parseTOML } from \"confbox/toml\";\nimport { parseYAML } from \"confbox/yaml\";\nimport { workspacePatterns } from \"./workspaces.js\";\n\nexport interface Toolchain {\n node: string;\n bun: string;\n}\n\nexport interface ToolchainOptions {\n /** Repository-relative documents whose `Node x.y.z` and `Bun x.y.z` mentions must match. */\n documents?: readonly string[];\n /** Files every Turbo cache key must include when `turbo.json` exists. */\n turboGlobalDependencies?: readonly string[];\n}\n\nexport interface RuntimeOptions {\n /** Environment variable that may select a Node executable outside PATH. */\n nodeBinaryEnv?: string;\n}\n\ninterface Manifest {\n packageManager?: string;\n engines?: { node?: string };\n devEngines?: Record<string, { name?: string; version?: string; onFail?: string }>;\n devDependencies?: Record<string, string>;\n workspaces?: unknown;\n}\n\nconst exactVersion = /^\\d+\\.\\d+\\.\\d+$/;\nconst manifestAt = (root: string, file = \"package.json\"): Manifest =>\n JSON.parse(readFileSync(join(root, file), \"utf8\")) as Manifest;\nconst catalogsOf = (manifest: Manifest): Record<string, string>[] => {\n const workspaces = manifest.workspaces;\n if (!workspaces || typeof workspaces !== \"object\" || Array.isArray(workspaces)) return [];\n const { catalog, catalogs } = workspaces as {\n catalog?: Record<string, string>;\n catalogs?: Record<string, Record<string, string>>;\n };\n return [...(catalog ? [catalog] : []), ...Object.values(catalogs ?? {})];\n};\n\n/** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */\nexport function readToolchain(root: string): Toolchain {\n const node = readFileSync(join(root, \".nvmrc\"), \"utf8\").trim().replace(/^v/, \"\");\n const bun = /^bun@(.+)$/.exec(manifestAt(root).packageManager ?? \"\")?.[1] ?? \"\";\n if (!exactVersion.test(node) || !exactVersion.test(bun))\n throw new Error(\"Pin exact Node and Bun versions in .nvmrc and package.json packageManager.\");\n return { node, bun };\n}\n\n/** Returns the Node executable after confirming it provides the pinned release. */\nexport function pinnedNode(\n root: string,\n { nodeBinaryEnv = \"DEVKIT_NODE_BINARY\" }: RuntimeOptions = {},\n) {\n const pins = readToolchain(root);\n const nodeBinary = process.env[nodeBinaryEnv] || \"node\";\n let node: string;\n try {\n node = execFileSync(nodeBinary, [\"--version\"], { encoding: \"utf8\", stdio: \"pipe\" }).trim();\n } catch {\n throw new Error(\n `Node is unavailable; select Node ${pins.node} from .nvmrc or set ${nodeBinaryEnv}.`,\n );\n }\n if (node !== `v${pins.node}`)\n throw new Error(\n `Expected Node ${pins.node}, got ${node}; select the pinned Node from .nvmrc or set ${nodeBinaryEnv}.`,\n );\n return nodeBinary;\n}\n\n/** The binary override changes the consumer executable, never the required version. */\nexport function assertToolchainRuntime(\n root: string,\n options: RuntimeOptions = {},\n): Toolchain & { nodeBinary: string } {\n const pins = readToolchain(root);\n if (process.versions.bun !== pins.bun)\n throw new Error(`Expected Bun ${pins.bun}, got ${process.versions.bun ?? \"none\"}.`);\n return { ...pins, nodeBinary: pinnedNode(root, options) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction checkMise(root: string): string[] {\n const file = \"mise.toml\";\n const path = join(root, file);\n if (!existsSync(path)) return [`${file}: missing runtime manager configuration`];\n let mise: unknown;\n try {\n mise = parseTOML(readFileSync(path, \"utf8\"));\n } catch {\n return [`${file}: invalid TOML configuration`];\n }\n if (!isRecord(mise)) return [`${file}: configuration must be a table`];\n const findings: string[] = [];\n if (Object.hasOwn(mise, \"tools\"))\n findings.push(`${file}: read existing pins instead of declaring [tools] versions`);\n const tools = isRecord(mise.settings)\n ? mise.settings.idiomatic_version_file_enable_tools\n : undefined;\n if (\n !Array.isArray(tools) ||\n tools.length !== 2 ||\n ![\"node\", \"bun\"].every((tool) => tools.includes(tool))\n )\n findings.push(`${file}: enable idiomatic version files for node and bun`);\n return findings;\n}\n\nfunction checkSetupStep(file: string, location: string, step: Record<string, unknown>): string[] {\n if (typeof step.uses !== \"string\") return [];\n const refSeparator = step.uses.indexOf(\"@\");\n if (refSeparator < 0) return [];\n const repository = step.uses.slice(0, refSeparator).toLowerCase();\n const setup = (\n [\n [\"node\", \"actions/setup-node\", \".nvmrc\"],\n [\"bun\", \"oven-sh/setup-bun\", \"package.json\"],\n ] as const\n ).find(([, action]) => repository === action);\n if (!setup) return [];\n const [tool, , source] = setup;\n const findings: string[] = [];\n if (step.with !== undefined && !isRecord(step.with))\n findings.push(`${file}: ${location}.with must be a mapping`);\n const inputs = isRecord(step.with) ? step.with : {};\n if (Object.hasOwn(inputs, `${tool}-version`))\n findings.push(`${file}: read ${tool}-version-file: ${source} instead of a literal version`);\n const versionFile = inputs[`${tool}-version-file`];\n if (versionFile === undefined) findings.push(`${file}: every ${tool} setup must read ${source}`);\n else if (versionFile !== source) findings.push(`${file}: ${tool}-version-file must be ${source}`);\n return findings;\n}\n\nfunction checkSetupSteps(file: string, location: string, steps: unknown): string[] {\n if (!Array.isArray(steps)) return [`${file}: ${location} must be an array`];\n const findings: string[] = [];\n for (const [index, step] of steps.entries()) {\n const stepLocation = `${location}[${index}]`;\n if (!isRecord(step)) {\n findings.push(`${file}: ${stepLocation} must be a mapping`);\n continue;\n }\n findings.push(...checkSetupStep(file, stepLocation, step));\n }\n return findings;\n}\n\nfunction checkWorkflows(root: string): string[] {\n const findings: string[] = [];\n for (const file of globSync(\".github/workflows/*.{yml,yaml}\", { cwd: root })) {\n let workflow: unknown;\n try {\n workflow = parseYAML(readFileSync(join(root, file), \"utf8\"), { filename: file });\n } catch {\n findings.push(`${file}: invalid YAML configuration`);\n continue;\n }\n if (!isRecord(workflow) || !isRecord(workflow.jobs)) {\n findings.push(`${file}: workflow jobs must be a mapping`);\n continue;\n }\n for (const [name, job] of Object.entries(workflow.jobs)) {\n const location = `jobs.${name}`;\n if (!isRecord(job)) {\n findings.push(`${file}: ${location} must be a mapping`);\n continue;\n }\n if (job.steps === undefined) continue;\n findings.push(...checkSetupSteps(file, `${location}.steps`, job.steps));\n }\n }\n return findings;\n}\n\nfunction checkCompositeActions(root: string): string[] {\n const findings: string[] = [];\n for (const file of globSync(\".github/actions/**/action.{yml,yaml}\", { cwd: root })) {\n let action: unknown;\n try {\n action = parseYAML(readFileSync(join(root, file), \"utf8\"), { filename: file });\n } catch {\n findings.push(`${file}: invalid YAML configuration`);\n continue;\n }\n if (!isRecord(action) || !isRecord(action.runs)) {\n findings.push(`${file}: action runs must be a mapping`);\n continue;\n }\n if (action.runs.using !== \"composite\") continue;\n findings.push(...checkSetupSteps(file, \"runs.steps\", action.runs.steps));\n }\n return findings;\n}\n\n/** Check duplicated declarations without installing tools or running application code. */\nexport function checkToolchain(\n root: string,\n {\n documents = [\"README.md\", \"AGENTS.md\"],\n turboGlobalDependencies = [\".nvmrc\", \"mise.toml\"],\n }: ToolchainOptions = {},\n): string[] {\n let pins: Toolchain;\n try {\n pins = readToolchain(root);\n } catch (error) {\n return [error instanceof Error ? error.message : String(error)];\n }\n const findings: string[] = [];\n const manifest = manifestAt(root);\n const expect = (file: string, label: string, actual: string | undefined, expected: string) => {\n if (actual !== expected)\n findings.push(`${file}: ${label} is ${actual ?? \"missing\"}, expected ${expected}`);\n };\n expect(\"package.json\", \"engines.node\", manifest.engines?.node, pins.node);\n for (const key of [\"packageManager\", \"runtime\"]) {\n const engine = manifest.devEngines?.[key];\n expect(\n \"package.json\",\n `devEngines.${key}`,\n `${engine?.name}@${engine?.version}`,\n `bun@${pins.bun}`,\n );\n expect(\"package.json\", `devEngines.${key}.onFail`, engine?.onFail, \"error\");\n }\n for (const file of [\".node-version\", \".bun-version\", \".tool-versions\"])\n if (existsSync(join(root, file)))\n findings.push(`${file}: use .nvmrc and package.json instead of a second version source`);\n findings.push(...checkMise(root));\n\n // Runtime type packages follow the pins: @types/node by major, @types/bun exactly.\n const nodeMajor = pins.node.split(\".\")[0]!;\n // Protocol ranges such as catalog: and workspace: are checked where they resolve.\n const version = (range: string | undefined) =>\n range && !/^[a-z]+:/.test(range) ? range.replace(/^[\\^~]/, \"\") : undefined;\n const checkTypes = (file: string, dependencies: Record<string, string> | undefined) => {\n const node = version(dependencies?.[\"@types/node\"]);\n if (node && node.split(\".\")[0] !== nodeMajor)\n findings.push(`${file}: @types/node must match the Node ${nodeMajor} major`);\n const bun = version(dependencies?.[\"@types/bun\"]);\n if (bun) expect(file, \"@types/bun\", bun, pins.bun);\n };\n for (const catalog of catalogsOf(manifest)) checkTypes(\"package.json catalog\", catalog);\n for (const file of [\n \"package.json\",\n ...globSync(\n workspacePatterns(manifest).map((path) => `${path}/package.json`),\n { cwd: root },\n ),\n ])\n checkTypes(file, manifestAt(root, file).devDependencies);\n\n for (const file of globSync(\"**/Dockerfile\", { cwd: root, exclude: [\"**/node_modules/**\"] })) {\n const source = readFileSync(join(root, file), \"utf8\");\n for (const [, version] of source.matchAll(/^FROM\\s+node:(\\d+\\.\\d+\\.\\d+)/gm))\n expect(file, \"node image\", version, pins.node);\n for (const [, version] of source.matchAll(/^FROM\\s+oven\\/bun:(\\d+\\.\\d+\\.\\d+)/gm))\n expect(file, \"bun image\", version, pins.bun);\n }\n findings.push(...checkWorkflows(root), ...checkCompositeActions(root));\n for (const file of documents) {\n if (!existsSync(join(root, file))) continue;\n const document = readFileSync(join(root, file), \"utf8\").replaceAll(\"**\", \"\");\n for (const [, tool, version] of document.matchAll(/\\b(Node|Bun) v?(\\d+\\.\\d+\\.\\d+)\\b/g))\n expect(file, `documented ${tool}`, version, tool === \"Node\" ? pins.node : pins.bun);\n }\n const turboFile = join(root, \"turbo.json\");\n if (existsSync(turboFile)) {\n const turbo = JSON.parse(readFileSync(turboFile, \"utf8\")) as { globalDependencies?: string[] };\n for (const file of turboGlobalDependencies)\n if (!turbo.globalDependencies?.includes(file))\n findings.push(`turbo.json: include ${file} in globalDependencies`);\n }\n return [...new Set(findings)];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"toolchain.mjs","names":[],"sources":["../src/toolchain.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { existsSync, globSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parseTOML } from \"confbox/toml\";\nimport { parseYAML } from \"confbox/yaml\";\nimport { workspacePatterns } from \"./workspaces.js\";\n\nexport interface Toolchain {\n node: string;\n bun: string;\n}\n\nexport interface ToolchainOptions {\n /** Bun-only repositories forbid Node runtime pins; the default checks both runtimes. */\n runtime?: \"node-bun\" | \"bun\";\n /** Repository-relative documents whose `Node x.y.z` and `Bun x.y.z` mentions must match. */\n documents?: readonly string[];\n /** Files every Turbo cache key must include when `turbo.json` exists. */\n turboGlobalDependencies?: readonly string[];\n}\n\nexport interface RuntimeOptions {\n /** Environment variable that may select a Node executable outside PATH. */\n nodeBinaryEnv?: string;\n}\n\ninterface Manifest {\n packageManager?: string;\n engines?: { node?: string };\n devEngines?: Record<string, { name?: string; version?: string; onFail?: string }>;\n devDependencies?: Record<string, string>;\n workspaces?: unknown;\n}\n\nconst exactVersion = /^\\d+\\.\\d+\\.\\d+$/;\nconst manifestAt = (root: string, file = \"package.json\"): Manifest =>\n JSON.parse(readFileSync(join(root, file), \"utf8\")) as Manifest;\nconst catalogsOf = (manifest: Manifest): Record<string, string>[] => {\n const workspaces = manifest.workspaces;\n if (!workspaces || typeof workspaces !== \"object\" || Array.isArray(workspaces)) return [];\n const { catalog, catalogs } = workspaces as {\n catalog?: Record<string, string>;\n catalogs?: Record<string, Record<string, string>>;\n };\n return [...(catalog ? [catalog] : []), ...Object.values(catalogs ?? {})];\n};\n\nconst bunVersion = (root: string) =>\n /^bun@(\\d+\\.\\d+\\.\\d+)$/.exec(manifestAt(root).packageManager ?? \"\")?.[1];\n\n/** Read the exact Bun pin without requiring a Node development runtime. */\nexport function readBunPin(root: string): string {\n const bun = bunVersion(root);\n if (!bun) throw new Error(\"Pin an exact Bun version in package.json packageManager.\");\n return bun;\n}\n\nfunction assertBunVersion(bun: string) {\n if (process.versions.bun !== bun)\n throw new Error(`Expected Bun ${bun}, got ${process.versions.bun ?? \"none\"}.`);\n}\n\n/** Check the active Bun runtime without executing Node or inspected repository code. */\nexport function assertBunRuntime(root: string): { bun: string } {\n const bun = readBunPin(root);\n assertBunVersion(bun);\n return { bun };\n}\n\n/** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */\nexport function readToolchain(root: string): Toolchain {\n const node = readFileSync(join(root, \".nvmrc\"), \"utf8\").trim().replace(/^v/, \"\");\n const bun = bunVersion(root) ?? \"\";\n if (!exactVersion.test(node) || !exactVersion.test(bun))\n throw new Error(\"Pin exact Node and Bun versions in .nvmrc and package.json packageManager.\");\n return { node, bun };\n}\n\n/** Returns the Node executable after confirming it provides the pinned release. */\nexport function pinnedNode(\n root: string,\n { nodeBinaryEnv = \"DEVKIT_NODE_BINARY\" }: RuntimeOptions = {},\n) {\n const pins = readToolchain(root);\n const nodeBinary = process.env[nodeBinaryEnv] || \"node\";\n let node: string;\n try {\n node = execFileSync(nodeBinary, [\"--version\"], { encoding: \"utf8\", stdio: \"pipe\" }).trim();\n } catch {\n throw new Error(\n `Node is unavailable; select Node ${pins.node} from .nvmrc or set ${nodeBinaryEnv}.`,\n );\n }\n if (node !== `v${pins.node}`)\n throw new Error(\n `Expected Node ${pins.node}, got ${node}; select the pinned Node from .nvmrc or set ${nodeBinaryEnv}.`,\n );\n return nodeBinary;\n}\n\n/** The binary override changes the consumer executable, never the required version. */\nexport function assertToolchainRuntime(\n root: string,\n options: RuntimeOptions = {},\n): Toolchain & { nodeBinary: string } {\n const pins = readToolchain(root);\n assertBunVersion(pins.bun);\n return { ...pins, nodeBinary: pinnedNode(root, options) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction checkMise(root: string, bunOnly: boolean): string[] {\n const file = \"mise.toml\";\n const path = join(root, file);\n if (!existsSync(path)) return [`${file}: missing runtime manager configuration`];\n let mise: unknown;\n try {\n mise = parseTOML(readFileSync(path, \"utf8\"));\n } catch {\n return [`${file}: invalid TOML configuration`];\n }\n if (!isRecord(mise)) return [`${file}: configuration must be a table`];\n const findings: string[] = [];\n if (Object.hasOwn(mise, \"tools\"))\n findings.push(`${file}: read existing pins instead of declaring [tools] versions`);\n const tools = isRecord(mise.settings)\n ? mise.settings.idiomatic_version_file_enable_tools\n : undefined;\n const expected = bunOnly ? [\"bun\"] : [\"node\", \"bun\"];\n if (\n !Array.isArray(tools) ||\n tools.length !== expected.length ||\n !expected.every((tool) => tools.includes(tool))\n )\n findings.push(\n `${file}: enable idiomatic version files for ${bunOnly ? \"bun only\" : \"node and bun\"}`,\n );\n return findings;\n}\n\nfunction checkSetupStep(\n file: string,\n location: string,\n step: Record<string, unknown>,\n bunOnly: boolean,\n): string[] {\n if (typeof step.uses !== \"string\") return [];\n const refSeparator = step.uses.indexOf(\"@\");\n if (refSeparator < 0) return [];\n const repository = step.uses.slice(0, refSeparator).toLowerCase();\n const setup = (\n [\n [\"node\", \"actions/setup-node\", \".nvmrc\"],\n [\"bun\", \"oven-sh/setup-bun\", \"package.json\"],\n ] as const\n ).find(([, action]) => repository === action);\n if (!setup) return [];\n const [tool, , source] = setup;\n if (bunOnly && tool === \"node\")\n return [`${file}: remove actions/setup-node; Bun is the only runtime`];\n const findings: string[] = [];\n if (step.with !== undefined && !isRecord(step.with))\n findings.push(`${file}: ${location}.with must be a mapping`);\n const inputs = isRecord(step.with) ? step.with : {};\n if (Object.hasOwn(inputs, `${tool}-version`))\n findings.push(`${file}: read ${tool}-version-file: ${source} instead of a literal version`);\n const versionFile = inputs[`${tool}-version-file`];\n if (versionFile === undefined) findings.push(`${file}: every ${tool} setup must read ${source}`);\n else if (versionFile !== source) findings.push(`${file}: ${tool}-version-file must be ${source}`);\n return findings;\n}\n\nfunction checkSetupSteps(\n file: string,\n location: string,\n steps: unknown,\n bunOnly: boolean,\n): string[] {\n if (!Array.isArray(steps)) return [`${file}: ${location} must be an array`];\n const findings: string[] = [];\n for (const [index, step] of steps.entries()) {\n const stepLocation = `${location}[${index}]`;\n if (!isRecord(step)) {\n findings.push(`${file}: ${stepLocation} must be a mapping`);\n continue;\n }\n findings.push(...checkSetupStep(file, stepLocation, step, bunOnly));\n }\n return findings;\n}\n\nfunction checkWorkflows(root: string, bunOnly: boolean): string[] {\n const findings: string[] = [];\n for (const file of globSync(\".github/workflows/*.{yml,yaml}\", { cwd: root })) {\n let workflow: unknown;\n try {\n workflow = parseYAML(readFileSync(join(root, file), \"utf8\"), { filename: file });\n } catch {\n findings.push(`${file}: invalid YAML configuration`);\n continue;\n }\n if (!isRecord(workflow) || !isRecord(workflow.jobs)) {\n findings.push(`${file}: workflow jobs must be a mapping`);\n continue;\n }\n for (const [name, job] of Object.entries(workflow.jobs)) {\n const location = `jobs.${name}`;\n if (!isRecord(job)) {\n findings.push(`${file}: ${location} must be a mapping`);\n continue;\n }\n if (job.steps === undefined) continue;\n findings.push(...checkSetupSteps(file, `${location}.steps`, job.steps, bunOnly));\n }\n }\n return findings;\n}\n\nfunction checkCompositeActions(root: string, bunOnly: boolean): string[] {\n const findings: string[] = [];\n for (const file of globSync(\".github/actions/**/action.{yml,yaml}\", { cwd: root })) {\n let action: unknown;\n try {\n action = parseYAML(readFileSync(join(root, file), \"utf8\"), { filename: file });\n } catch {\n findings.push(`${file}: invalid YAML configuration`);\n continue;\n }\n if (!isRecord(action) || !isRecord(action.runs)) {\n findings.push(`${file}: action runs must be a mapping`);\n continue;\n }\n if (action.runs.using !== \"composite\") continue;\n findings.push(...checkSetupSteps(file, \"runs.steps\", action.runs.steps, bunOnly));\n }\n return findings;\n}\n\n/** Check duplicated declarations without installing tools or running application code. */\nexport function checkToolchain(\n root: string,\n {\n runtime = \"node-bun\",\n documents = [\"README.md\", \"AGENTS.md\"],\n turboGlobalDependencies = runtime === \"bun\" ? [\"mise.toml\"] : [\".nvmrc\", \"mise.toml\"],\n }: ToolchainOptions = {},\n): string[] {\n const bunOnly = runtime === \"bun\";\n let pins: { bun: string; node?: string };\n try {\n pins = bunOnly ? { bun: readBunPin(root) } : readToolchain(root);\n } catch (error) {\n return [error instanceof Error ? error.message : String(error)];\n }\n const findings: string[] = [];\n const manifest = manifestAt(root);\n const expect = (file: string, label: string, actual: string | undefined, expected: string) => {\n if (actual !== expected)\n findings.push(`${file}: ${label} is ${actual ?? \"missing\"}, expected ${expected}`);\n };\n if (bunOnly) {\n if (manifest.engines?.node !== undefined)\n findings.push(\"package.json: remove engines.node; Bun is the only runtime\");\n } else expect(\"package.json\", \"engines.node\", manifest.engines?.node, pins.node!);\n for (const key of [\"packageManager\", \"runtime\"]) {\n const engine = manifest.devEngines?.[key];\n expect(\n \"package.json\",\n `devEngines.${key}`,\n `${engine?.name}@${engine?.version}`,\n `bun@${pins.bun}`,\n );\n expect(\"package.json\", `devEngines.${key}.onFail`, engine?.onFail, \"error\");\n }\n for (const file of [\n ...(bunOnly ? [\".nvmrc\"] : []),\n \".node-version\",\n \".bun-version\",\n \".tool-versions\",\n ])\n if (existsSync(join(root, file)))\n findings.push(\n bunOnly\n ? `${file}: package.json packageManager is the only runtime pin`\n : `${file}: use .nvmrc and package.json instead of a second version source`,\n );\n findings.push(...checkMise(root, bunOnly));\n\n // Runtime type packages follow the pins: @types/node by major, @types/bun exactly.\n const nodeMajor = pins.node?.split(\".\")[0];\n // Protocol ranges such as catalog: and workspace: are checked where they resolve.\n const version = (range: string | undefined) =>\n range && !/^[a-z]+:/.test(range) ? range.replace(/^[\\^~]/, \"\") : undefined;\n const checkTypes = (file: string, dependencies: Record<string, string> | undefined) => {\n const node = version(dependencies?.[\"@types/node\"]);\n if (nodeMajor && node && node.split(\".\")[0] !== nodeMajor)\n findings.push(`${file}: @types/node must match the Node ${nodeMajor} major`);\n const bun = version(dependencies?.[\"@types/bun\"]);\n if (bun) expect(file, \"@types/bun\", bun, pins.bun);\n };\n for (const catalog of catalogsOf(manifest)) checkTypes(\"package.json catalog\", catalog);\n for (const file of [\n \"package.json\",\n ...globSync(\n workspacePatterns(manifest).map((path) => `${path}/package.json`),\n { cwd: root },\n ),\n ])\n checkTypes(file, manifestAt(root, file).devDependencies);\n\n for (const file of globSync(\"**/Dockerfile\", { cwd: root, exclude: [\"**/node_modules/**\"] })) {\n const source = readFileSync(join(root, file), \"utf8\");\n if (bunOnly) {\n if (/^\\s*FROM\\s+(?:--platform=\\S+\\s+)?node:/im.test(source))\n findings.push(`${file}: build and run on oven/bun images`);\n } else {\n for (const [, version] of source.matchAll(/^FROM\\s+node:(\\d+\\.\\d+\\.\\d+)/gm))\n expect(file, \"node image\", version, pins.node!);\n }\n for (const [, version] of source.matchAll(/^FROM\\s+oven\\/bun:(\\d+\\.\\d+\\.\\d+)/gm))\n expect(file, \"bun image\", version, pins.bun);\n }\n findings.push(...checkWorkflows(root, bunOnly), ...checkCompositeActions(root, bunOnly));\n for (const file of documents) {\n if (!existsSync(join(root, file))) continue;\n const document = readFileSync(join(root, file), \"utf8\").replaceAll(\"**\", \"\");\n for (const [, tool, version] of document.matchAll(/\\b(Node|Bun) v?(\\d+\\.\\d+\\.\\d+)\\b/g)) {\n if (bunOnly && tool === \"Node\")\n findings.push(`${file}: documents a Node release; Bun is the only runtime`);\n else expect(file, `documented ${tool}`, version, tool === \"Node\" ? pins.node! : pins.bun);\n }\n }\n const turboFile = join(root, \"turbo.json\");\n if (existsSync(turboFile)) {\n const turbo = JSON.parse(readFileSync(turboFile, \"utf8\")) as { globalDependencies?: string[] };\n for (const file of turboGlobalDependencies)\n if (!turbo.globalDependencies?.includes(file))\n findings.push(`turbo.json: include ${file} in globalDependencies`);\n }\n return [...new Set(findings)];\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raypx-kit/devkit",
3
- "version": "0.0.0-alpha.1",
3
+ "version": "0.0.0-alpha.3",
4
4
  "description": "Monorepo maintenance checks: workspace boundaries, toolchain pins, static catalogs, artifacts and task helpers.",
5
5
  "homepage": "https://github.com/raypx/raypx-kit/tree/canary/packages/devkit",
6
6
  "license": "Apache-2.0",
@@ -49,6 +49,14 @@
49
49
  "./docs": {
50
50
  "types": "./dist/docs.d.mts",
51
51
  "import": "./dist/docs.mjs"
52
+ },
53
+ "./lint": {
54
+ "types": "./dist/lint.d.mts",
55
+ "import": "./dist/lint.mjs"
56
+ },
57
+ "./testing": {
58
+ "types": "./dist/testing.d.mts",
59
+ "import": "./dist/testing.mjs"
52
60
  }
53
61
  },
54
62
  "publishConfig": {
@@ -71,7 +79,8 @@
71
79
  "oxc-parser": "^0.155.0"
72
80
  },
73
81
  "devDependencies": {
74
- "@vitest/coverage-v8": "^5.0.5"
82
+ "@vitest/coverage-v8": "^5.0.5",
83
+ "vite-plus": "^1.3.0"
75
84
  },
76
85
  "engines": {
77
86
  "node": ">=22.21"