@danieljvdm/dev-kit 0.7.1 → 0.7.2

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/README.md CHANGED
@@ -277,16 +277,79 @@ quality configuration and GitHub Actions workflow:
277
277
 
278
278
  The quality task requires direct `@danieljvdm/dev-kit`, `vite-plus`, `effect`,
279
279
  `@effect/tsgo`, and native TypeScript dependencies, plus the enabled Effect
280
- TypeScript-Go patch. It manages `vite.config.ts` and
280
+ TypeScript-Go patch. The installed Vite+ version must satisfy Dev Kit's
281
+ `vite-plus` peer range; Dev Kit tests one exact development version inside that
282
+ range, while CI lets `setup-vp` resolve the consumer's version from its own
283
+ manifest and lockfile. It manages `vite.config.ts` and
281
284
  `.github/workflows/check.yml` as digest-owned files. Existing custom files or
282
285
  conflicting `check`/`typecheck` package scripts are rejected instead of merged
283
286
  or overwritten. Exact canonical files can be adopted; disabling the task
284
287
  removes only unchanged owned files.
285
288
 
286
289
  The managed Vite config composes the shared Oxfmt and Oxlint presets, configures
287
- `vp staged`, and defines `vp run check` and `vp run typecheck`. The check task
288
- and CI run `vp fmt --check`, `vp lint`, `vp test`, then the Effect-patched
289
- compiler through `vp run typecheck`.
290
+ `vp staged`, and defines separate `vp run check` and pure `vp run typecheck`
291
+ tasks. The default `single-project` strategy runs one `tsc --noEmit` process and
292
+ requires a root `tsconfig.json` without project references:
293
+
294
+ ```jsonc
295
+ {
296
+ "setup": {
297
+ "effectTsgo": { "enabled": true },
298
+ "vitePlus": {
299
+ "quality": {
300
+ "enabled": true,
301
+ "typecheck": { "strategy": "single-project" },
302
+ },
303
+ },
304
+ },
305
+ }
306
+ ```
307
+
308
+ Workspaces can instead opt into Vite Task orchestration explicitly:
309
+
310
+ ```jsonc
311
+ {
312
+ "setup": {
313
+ "effectTsgo": { "enabled": true },
314
+ "vitePlus": {
315
+ "quality": {
316
+ "enabled": true,
317
+ "typecheck": {
318
+ "strategy": "workspace",
319
+ "concurrency": 4,
320
+ "packages": ["apps/web", "packages/core"],
321
+ },
322
+ },
323
+ },
324
+ },
325
+ }
326
+ ```
327
+
328
+ Workspace mode requires `package.json#workspaces` and an explicit list of
329
+ project-relative package directories. Each listed package must define a pure
330
+ `typecheck` package script. Dev Kit generates Vite+ filters for exactly that
331
+ scope with `--fail-if-no-match`; Vite Task uses ordinary workspace dependency
332
+ order, forces script caching on, and bounds concurrency to the configured value
333
+ (1–32). Dev Kit does not generate a project-reference mode: repositories that
334
+ need `tsc -b` must keep the quality setup disabled and define a custom Vite task
335
+ with deliberate `.tsbuildinfo` inputs and outputs.
336
+
337
+ Generated CI performs exactly one `vp install` through
338
+ `voidzero-dev/setup-vp`, using `--frozen-lockfile --ignore-scripts`, then runs
339
+ `vp exec dev-kit apply --locked` before quality checks. This verifies the
340
+ committed Dev Kit lock and explicitly converges the Effect TypeScript-Go patch
341
+ without relying on a consumer lifecycle script or running an unlocked apply.
342
+ Vite+ maps those install flags to the detected npm, pnpm, Yarn, or Bun project;
343
+ the workflow does not pretend their native immutable/no-script flags are
344
+ interchangeable.
345
+ The template pins the exact commit for a supported `setup-vp` release because
346
+ its `v1` tag is frozen; keep the annotated release commit current with Renovate
347
+ or Dependabot. See the primary
348
+ [`setup-vp` versioning guidance](https://github.com/voidzero-dev/setup-vp#versioning),
349
+ [Vite+ install guide](https://viteplus.dev/guide/install), and
350
+ [Vite Task run guide](https://viteplus.dev/guide/run) when maintaining these
351
+ templates. The check task and CI then run `vp fmt --check`, `vp lint`,
352
+ `vp test`, and `vp run typecheck`.
290
353
 
291
354
  ## Effect source checkout
292
355
 
@@ -8,7 +8,10 @@
8
8
  "effectTsgo": { "enabled": true },
9
9
  "vitePlus": {
10
10
  "hooks": { "enabled": true },
11
- "quality": { "enabled": true },
11
+ "quality": {
12
+ "enabled": true,
13
+ "typecheck": { "strategy": "single-project" },
14
+ },
12
15
  },
13
16
  },
14
17
  "targets": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "private": false,
5
5
  "description": "Declarative project development toolkit with portable agent skills.",
6
6
  "license": "MIT",
@@ -69,6 +69,7 @@
69
69
  "@stylistic/eslint-plugin": "5.10.0",
70
70
  "effect": "4.0.0-beta.102",
71
71
  "jsonc-parser": "3.3.1",
72
+ "semver": "7.8.5",
72
73
  "tsx": "4.22.4"
73
74
  },
74
75
  "devDependencies": {
@@ -76,6 +77,7 @@
76
77
  "@effect/tsgo": "0.24.3",
77
78
  "@effect/vitest": "4.0.0-beta.102",
78
79
  "@types/node": "25.9.1",
80
+ "@types/semver": "7.8.0",
79
81
  "oxfmt": "0.60.0",
80
82
  "oxlint": "1.75.0",
81
83
  "typescript": "7.0.2",
@@ -168,6 +168,37 @@
168
168
  "enabled": {
169
169
  "type": "boolean",
170
170
  "default": false
171
+ },
172
+ "typecheck": {
173
+ "$ref": "#/$defs/vitePlusQualityTypecheckSetup"
174
+ }
175
+ }
176
+ },
177
+ "vitePlusQualityTypecheckSetup": {
178
+ "description": "Select the managed typecheck topology. Single-project is the default; workspace mode runs explicitly scoped package scripts through Vite Task.",
179
+ "type": "object",
180
+ "additionalProperties": false,
181
+ "properties": {
182
+ "strategy": {
183
+ "enum": ["single-project", "workspace"],
184
+ "default": "single-project"
185
+ },
186
+ "concurrency": {
187
+ "description": "Maximum concurrent workspace typecheck tasks.",
188
+ "type": "integer",
189
+ "minimum": 1,
190
+ "maximum": 32,
191
+ "default": 4
192
+ },
193
+ "packages": {
194
+ "description": "Project-relative workspace package directories whose typecheck scripts should run.",
195
+ "type": "array",
196
+ "items": {
197
+ "type": "string",
198
+ "minLength": 1
199
+ },
200
+ "uniqueItems": true,
201
+ "default": []
171
202
  }
172
203
  }
173
204
  },
@@ -116,12 +116,27 @@ Effect TypeScript-Go, and native TypeScript dependencies, as well as
116
116
  `setup.effectTsgo.enabled`. It refuses custom destination files and conflicting
117
117
  `check` or `typecheck` package scripts. Exact canonical files can be adopted;
118
118
  later updates and cleanup occur only while the owned files remain unchanged.
119
+ The installed Vite+ version must satisfy Dev Kit's package peer range; generated
120
+ CI resolves the consumer's locked Vite+ rather than carrying a second version
121
+ literal.
119
122
 
120
123
  The managed Vite config composes the shared formatter and linter presets,
121
- configures `vp staged`, and defines cached `check` and `typecheck` Vite tasks.
122
- The check task and GitHub Actions workflow run `vp fmt --check`, `vp lint`,
123
- `vp test`, and finally `vp run typecheck` so Effect diagnostics come from the
124
- patched native compiler.
124
+ configures `vp staged`, and defines separate cached `check` and pure `typecheck`
125
+ Vite tasks. The default `single-project` typecheck strategy runs
126
+ `tsc --noEmit` and rejects root TypeScript project references. An explicit
127
+ `workspace` strategy requires explicit package directories with consumer-owned
128
+ `typecheck` scripts, then runs exactly that filter scope in workspace dependency
129
+ order with caching enabled, unmatched-filter failure, and configurable bounded
130
+ concurrency. Project-reference builds remain custom because they need
131
+ repository-specific `.tsbuildinfo` inputs and outputs.
132
+
133
+ The GitHub Actions workflow uses one frozen, script-suppressed install through
134
+ an exact `setup-vp` release, then runs `vp exec dev-kit apply --locked`. This
135
+ verifies the committed setup and converges the Effect TypeScript-Go patch before
136
+ formatting, linting, tests, and typechecking. Vite+ maps the frozen and
137
+ ignore-scripts flags to the detected package manager. Keep the exact setup
138
+ action release commit current with Renovate or Dependabot; do not switch back
139
+ to the frozen `v1` tag.
125
140
 
126
141
  ## Ownership and conflicts
127
142
 
package/src/index.ts CHANGED
@@ -13,8 +13,12 @@ export {
13
13
  TargetConfigSchema,
14
14
  type VitePlusQualitySetup,
15
15
  VitePlusQualitySetupSchema,
16
+ type VitePlusQualityTypecheckSetup,
17
+ VitePlusQualityTypecheckSetupSchema,
16
18
  type VitePlusSetup,
17
19
  VitePlusSetupSchema,
20
+ type VitePlusTypecheckStrategy,
21
+ VitePlusTypecheckStrategySchema,
18
22
  } from "./manifest.ts";
19
23
  export {
20
24
  applyEffectSourcePlan,
package/src/manifest.ts CHANGED
@@ -53,8 +53,19 @@ export const VitePlusHooksSetupSchema = Schema.Struct({
53
53
  enabled: Schema.optional(Schema.Boolean),
54
54
  });
55
55
 
56
+ export const VitePlusTypecheckStrategySchema = Schema.Literals(["single-project", "workspace"]);
57
+ export type VitePlusTypecheckStrategy = typeof VitePlusTypecheckStrategySchema.Type;
58
+
59
+ export const VitePlusQualityTypecheckSetupSchema = Schema.Struct({
60
+ strategy: Schema.optional(VitePlusTypecheckStrategySchema),
61
+ concurrency: Schema.optional(Schema.Int),
62
+ packages: Schema.optional(Schema.Array(Schema.String)),
63
+ });
64
+ export type VitePlusQualityTypecheckSetup = typeof VitePlusQualityTypecheckSetupSchema.Type;
65
+
56
66
  export const VitePlusQualitySetupSchema = Schema.Struct({
57
67
  enabled: Schema.optional(Schema.Boolean),
68
+ typecheck: Schema.optional(VitePlusQualityTypecheckSetupSchema),
58
69
  });
59
70
  export type VitePlusQualitySetup = typeof VitePlusQualitySetupSchema.Type;
60
71
 
@@ -124,6 +135,11 @@ export type NormalizedManifest = {
124
135
  };
125
136
  readonly quality: {
126
137
  readonly enabled: boolean;
138
+ readonly typecheck: {
139
+ readonly strategy: VitePlusTypecheckStrategy;
140
+ readonly concurrency: number;
141
+ readonly packages: ReadonlyArray<string>;
142
+ };
127
143
  };
128
144
  };
129
145
  };
@@ -187,6 +203,11 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
187
203
  },
188
204
  quality: {
189
205
  enabled: manifest.setup?.vitePlus?.quality?.enabled ?? false,
206
+ typecheck: {
207
+ strategy: manifest.setup?.vitePlus?.quality?.typecheck?.strategy ?? "single-project",
208
+ concurrency: manifest.setup?.vitePlus?.quality?.typecheck?.concurrency ?? 4,
209
+ packages: manifest.setup?.vitePlus?.quality?.typecheck?.packages ?? [],
210
+ },
190
211
  },
191
212
  },
192
213
  },
@@ -13,6 +13,7 @@ const ProjectPackageSchema = Schema.fromJsonString(
13
13
  devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
14
14
  optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
15
15
  peerDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
16
+ workspaces: Schema.optional(Schema.Unknown),
16
17
  }),
17
18
  );
18
19
 
package/src/sync.ts CHANGED
@@ -48,6 +48,8 @@ import {
48
48
  type VitePlusHooksPlan,
49
49
  } from "./vite-plus-hooks.ts";
50
50
  import {
51
+ renderVitePlusConfigTemplate,
52
+ renderVitePlusWorkflowTemplate,
51
53
  validateVitePlusQualitySupport,
52
54
  VITE_PLUS_CONFIG_PATH,
53
55
  VITE_PLUS_CONFIG_TEMPLATE,
@@ -646,7 +648,16 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
646
648
  },
647
649
  ]) {
648
650
  const managed = yield* resolveManagedPath(projectDir, generated.path);
649
- const content = yield* readGeneratedFileTemplate(packageRoot, generated.sourcePath);
651
+ const template = yield* readGeneratedFileTemplate(packageRoot, generated.sourcePath);
652
+ const content =
653
+ generated.resourceId === "setup:vite-plus-config"
654
+ ? renderVitePlusConfigTemplate(template, setup.vitePlus.quality.typecheck)
655
+ : renderVitePlusWorkflowTemplate(
656
+ template,
657
+ projectDir === packageRoot
658
+ ? "./bin/dev-kit.mjs apply --locked"
659
+ : "vp exec dev-kit apply --locked",
660
+ );
650
661
 
651
662
  outputs.push({
652
663
  resourceId: generated.resourceId,
@@ -918,6 +929,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
918
929
  projectDir,
919
930
  packageRoot,
920
931
  manifest.setup.effectTsgo.typescriptPackage,
932
+ manifest.setup.vitePlus.quality.typecheck,
921
933
  );
922
934
  }
923
935
  const effectSource = manifest.setup.effectSource.enabled
@@ -1,3 +1,5 @@
1
1
  import packageMetadata from "../package.json" with { type: "json" };
2
2
 
3
3
  export const DEV_KIT_VERSION = packageMetadata.version;
4
+ export const VITE_PLUS_TESTED_VERSION = packageMetadata.devDependencies["vite-plus"];
5
+ export const VITE_PLUS_SUPPORTED_RANGE = packageMetadata.peerDependencies["vite-plus"];
@@ -0,0 +1,70 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+ import semver from "semver";
3
+
4
+ import { readDirectDependencyNames } from "./project-package.ts";
5
+ import { VITE_PLUS_SUPPORTED_RANGE } from "./tool-metadata.ts";
6
+
7
+ const InstalledVitePlusPackageSchema = Schema.fromJsonString(
8
+ Schema.Struct({ version: Schema.String }),
9
+ );
10
+
11
+ export class VitePlusDependencyError extends Schema.TaggedErrorClass<VitePlusDependencyError>()(
12
+ "VitePlusDependencyError",
13
+ { message: Schema.String },
14
+ ) {}
15
+
16
+ export type InstalledVitePlus = {
17
+ readonly version: string;
18
+ readonly vpBin: string;
19
+ };
20
+
21
+ export const validateInstalledVitePlus = Effect.fn("validateInstalledVitePlus")(function* (
22
+ projectDir: string,
23
+ ) {
24
+ const fs = yield* FileSystem.FileSystem;
25
+ const path = yield* Path.Path;
26
+ const dependencies = yield* readDirectDependencyNames(projectDir);
27
+
28
+ if (!dependencies.includes("vite-plus")) {
29
+ return yield* new VitePlusDependencyError({
30
+ message: "vite-plus must be a direct project dependency",
31
+ });
32
+ }
33
+ const packagePath = path.join(projectDir, "node_modules", "vite-plus", "package.json");
34
+
35
+ if (!(yield* fs.exists(packagePath))) {
36
+ return yield* new VitePlusDependencyError({
37
+ message:
38
+ "vite-plus must be installed before enabling this setup: node_modules/vite-plus/package.json is missing",
39
+ });
40
+ }
41
+ const installed = yield* fs.readFileString(packagePath).pipe(
42
+ Effect.flatMap(Schema.decodeUnknownEffect(InstalledVitePlusPackageSchema)),
43
+ Effect.mapError(
44
+ () =>
45
+ new VitePlusDependencyError({
46
+ message: "installed vite-plus package metadata has no valid version",
47
+ }),
48
+ ),
49
+ );
50
+
51
+ if (!semver.valid(installed.version)) {
52
+ return yield* new VitePlusDependencyError({
53
+ message: `installed vite-plus package metadata has invalid version: ${installed.version}`,
54
+ });
55
+ }
56
+ if (!semver.satisfies(installed.version, VITE_PLUS_SUPPORTED_RANGE)) {
57
+ return yield* new VitePlusDependencyError({
58
+ message: `installed vite-plus ${installed.version} is incompatible with @danieljvdm/dev-kit; supported range: ${VITE_PLUS_SUPPORTED_RANGE}`,
59
+ });
60
+ }
61
+ const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
62
+
63
+ if (!(yield* fs.exists(vpBin))) {
64
+ return yield* new VitePlusDependencyError({
65
+ message: "vite-plus is installed but node_modules/.bin/vp is missing",
66
+ });
67
+ }
68
+
69
+ return { version: installed.version, vpBin } satisfies InstalledVitePlus;
70
+ });
@@ -1,7 +1,7 @@
1
1
  import { Config, Effect, FileSystem, Path, Schema, Stream } from "effect";
2
2
  import { ChildProcess } from "effect/unstable/process";
3
3
 
4
- import { readDirectDependencyNames } from "./project-package.ts";
4
+ import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
5
5
 
6
6
  export const VITE_PLUS_HOOKS_DIR = ".vite-hooks";
7
7
  export const VITE_PLUS_HOOKS_PATH = `${VITE_PLUS_HOOKS_DIR}/_`;
@@ -121,23 +121,14 @@ const inspectVitePlusHooks = Effect.fn("inspectVitePlusHooks")(function* (projec
121
121
  });
122
122
 
123
123
  export const planVitePlusHooks = Effect.fn("planVitePlusHooks")(function* (projectDir: string) {
124
- const fs = yield* FileSystem.FileSystem;
125
- const path = yield* Path.Path;
126
- const dependencies = yield* readDirectDependencyNames(projectDir);
127
-
128
- if (!dependencies.includes("vite-plus")) {
129
- return yield* new VitePlusHooksDependencyError({
130
- message: "vite-plus must be a direct project dependency before enabling setup.vitePlus.hooks",
131
- });
132
- }
133
- const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
134
-
135
- if (!(yield* fs.exists(vpBin))) {
136
- return yield* new VitePlusHooksDependencyError({
137
- message:
138
- "vite-plus must be installed before enabling setup.vitePlus.hooks: node_modules/.bin/vp is missing",
139
- });
140
- }
124
+ const { vpBin } = yield* validateInstalledVitePlus(projectDir).pipe(
125
+ Effect.mapError(
126
+ (error) =>
127
+ new VitePlusHooksDependencyError({
128
+ message: `${error.message} before enabling setup.vitePlus.hooks`,
129
+ }),
130
+ ),
131
+ );
141
132
  const viteGitHooks = yield* Config.string("VITE_GIT_HOOKS").pipe(Config.withDefault(""));
142
133
  const husky = yield* Config.string("HUSKY").pipe(Config.withDefault(""));
143
134
  const action =
@@ -1,6 +1,9 @@
1
1
  import { Effect, FileSystem, Path, Schema } from "effect";
2
+ import { parse as parseJsonc, type ParseError } from "jsonc-parser";
2
3
 
4
+ import type { VitePlusTypecheckStrategy } from "./manifest.ts";
3
5
  import { readDirectDependencyNames, readProjectPackage } from "./project-package.ts";
6
+ import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
4
7
 
5
8
  export const VITE_PLUS_CONFIG_PATH = "vite.config.ts";
6
9
  export const VITE_PLUS_CONFIG_TEMPLATE = "templates/vite-plus/vite.config.ts";
@@ -12,13 +15,75 @@ export class VitePlusQualitySupportError extends Schema.TaggedErrorClass<VitePlu
12
15
  { message: Schema.String },
13
16
  ) {}
14
17
 
18
+ export type VitePlusQualityTypecheck = {
19
+ readonly strategy: VitePlusTypecheckStrategy;
20
+ readonly concurrency: number;
21
+ readonly packages: ReadonlyArray<string>;
22
+ };
23
+
24
+ const SINGLE_PROJECT_TYPECHECK_TASK = ' typecheck: "tsc --noEmit",';
25
+ const LOCKED_DEV_KIT_COMMAND = "vp exec dev-kit apply --locked";
26
+
27
+ const replaceUniqueTemplateMarker = (
28
+ template: string,
29
+ marker: string,
30
+ replacement: string,
31
+ ): string => {
32
+ const parts = template.split(marker);
33
+
34
+ if (parts.length !== 2) {
35
+ throw new Error(`expected exactly one generated template marker: ${marker}`);
36
+ }
37
+
38
+ return `${parts[0]}${replacement}${parts[1]}`;
39
+ };
40
+
41
+ const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
42
+
43
+ export const renderVitePlusConfigTemplate = (
44
+ template: string,
45
+ typecheck: VitePlusQualityTypecheck,
46
+ ): string => {
47
+ if (typecheck.strategy === "single-project") return template;
48
+ const filters = typecheck.packages
49
+ .map((packageDir) => `--filter ${shellQuote(`./${packageDir}`)}`)
50
+ .join(" ");
51
+ const command = `vp run --cache --concurrency-limit ${typecheck.concurrency} ${filters} --fail-if-no-match typecheck`;
52
+
53
+ return replaceUniqueTemplateMarker(
54
+ template,
55
+ SINGLE_PROJECT_TYPECHECK_TASK,
56
+ ` typecheck: {
57
+ command: ${JSON.stringify(command)},
58
+ cache: false,
59
+ },`,
60
+ );
61
+ };
62
+
63
+ export const renderVitePlusWorkflowTemplate = (
64
+ template: string,
65
+ devKitCommand = LOCKED_DEV_KIT_COMMAND,
66
+ ): string =>
67
+ devKitCommand === LOCKED_DEV_KIT_COMMAND
68
+ ? template
69
+ : replaceUniqueTemplateMarker(template, LOCKED_DEV_KIT_COMMAND, devKitCommand);
70
+
15
71
  export const validateVitePlusQualitySupport = Effect.fn("validateVitePlusQualitySupport")(
16
- function* (projectDir: string, packageRoot: string, typescriptPackage: string) {
72
+ function* (
73
+ projectDir: string,
74
+ packageRoot: string,
75
+ typescriptPackage: string,
76
+ typecheck: VitePlusQualityTypecheck,
77
+ ) {
17
78
  const fs = yield* FileSystem.FileSystem;
18
79
  const path = yield* Path.Path;
19
80
  const packageJson = yield* readProjectPackage(projectDir);
20
81
  const dependencies = yield* readDirectDependencyNames(projectDir);
21
- const required = new Set(["vite-plus", "effect", "@effect/tsgo", typescriptPackage]);
82
+ const required = new Set(["effect", "@effect/tsgo", typescriptPackage]);
83
+
84
+ yield* validateInstalledVitePlus(projectDir).pipe(
85
+ Effect.mapError((error) => new VitePlusQualitySupportError({ message: error.message })),
86
+ );
22
87
 
23
88
  if (projectDir !== packageRoot) required.add("@danieljvdm/dev-kit");
24
89
  const missing = [...required].filter((dependency) => !dependencies.includes(dependency));
@@ -37,13 +102,104 @@ export const validateVitePlusQualitySupport = Effect.fn("validateVitePlusQuality
37
102
  message: `setup.vitePlus.quality defines Vite tasks that conflict with package scripts: ${conflictingScripts.join(", ")}`,
38
103
  });
39
104
  }
40
- const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
41
-
42
- if (!(yield* fs.exists(vpBin))) {
105
+ if (typecheck.concurrency < 1 || typecheck.concurrency > 32) {
106
+ return yield* new VitePlusQualitySupportError({
107
+ message: "setup.vitePlus.quality typecheck concurrency must be between 1 and 32",
108
+ });
109
+ }
110
+ if (typecheck.strategy === "single-project" && typecheck.packages.length > 0) {
43
111
  return yield* new VitePlusQualitySupportError({
44
112
  message:
45
- "vite-plus must be installed before enabling setup.vitePlus.quality: node_modules/.bin/vp is missing",
113
+ "setup.vitePlus.quality single-project typechecking does not accept workspace packages",
114
+ });
115
+ }
116
+ const workspacePatterns = Array.isArray(packageJson.workspaces)
117
+ ? packageJson.workspaces
118
+ : typeof packageJson.workspaces === "object" && packageJson.workspaces !== null
119
+ ? (packageJson.workspaces as { readonly packages?: unknown }).packages
120
+ : undefined;
121
+
122
+ if (
123
+ typecheck.strategy === "workspace" &&
124
+ (!Array.isArray(workspacePatterns) ||
125
+ workspacePatterns.length === 0 ||
126
+ !workspacePatterns.every((pattern) => typeof pattern === "string"))
127
+ ) {
128
+ return yield* new VitePlusQualitySupportError({
129
+ message: "setup.vitePlus.quality workspace typechecking requires package.json workspaces",
130
+ });
131
+ }
132
+ if (typecheck.strategy === "workspace") {
133
+ if (typecheck.packages.length === 0) {
134
+ return yield* new VitePlusQualitySupportError({
135
+ message:
136
+ "setup.vitePlus.quality workspace typechecking requires explicit package directories",
137
+ });
138
+ }
139
+ const uniquePackages = new Set(typecheck.packages);
140
+
141
+ if (uniquePackages.size !== typecheck.packages.length) {
142
+ return yield* new VitePlusQualitySupportError({
143
+ message: "setup.vitePlus.quality workspace typecheck packages must be unique",
144
+ });
145
+ }
146
+ for (const packageDir of typecheck.packages) {
147
+ const absolute = path.resolve(projectDir, packageDir);
148
+ const relative = path.relative(projectDir, absolute);
149
+
150
+ if (
151
+ packageDir.length === 0 ||
152
+ path.isAbsolute(packageDir) ||
153
+ relative.length === 0 ||
154
+ relative === ".." ||
155
+ relative.startsWith(`..${path.sep}`)
156
+ ) {
157
+ return yield* new VitePlusQualitySupportError({
158
+ message: `setup.vitePlus.quality workspace package must be a project-relative subdirectory: ${packageDir}`,
159
+ });
160
+ }
161
+ const workspacePackage = yield* readProjectPackage(absolute).pipe(
162
+ Effect.mapError(
163
+ () =>
164
+ new VitePlusQualitySupportError({
165
+ message: `setup.vitePlus.quality workspace package is missing a valid package.json: ${packageDir}`,
166
+ }),
167
+ ),
168
+ );
169
+
170
+ if (workspacePackage.scripts?.typecheck === undefined) {
171
+ return yield* new VitePlusQualitySupportError({
172
+ message: `setup.vitePlus.quality workspace package requires a typecheck script: ${packageDir}`,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ if (typecheck.strategy === "single-project") {
178
+ const tsconfigPath = path.join(projectDir, "tsconfig.json");
179
+
180
+ if (!(yield* fs.exists(tsconfigPath))) {
181
+ return yield* new VitePlusQualitySupportError({
182
+ message: "setup.vitePlus.quality single-project typechecking requires tsconfig.json",
183
+ });
184
+ }
185
+ const errors: Array<ParseError> = [];
186
+ const tsconfig = parseJsonc(yield* fs.readFileString(tsconfigPath), errors, {
187
+ allowTrailingComma: true,
46
188
  });
189
+
190
+ if (errors.length > 0 || typeof tsconfig !== "object" || tsconfig === null) {
191
+ return yield* new VitePlusQualitySupportError({
192
+ message: "setup.vitePlus.quality requires a valid root tsconfig.json",
193
+ });
194
+ }
195
+ const references = (tsconfig as { readonly references?: unknown }).references;
196
+
197
+ if (Array.isArray(references) && references.length > 0) {
198
+ return yield* new VitePlusQualitySupportError({
199
+ message:
200
+ "setup.vitePlus.quality single-project typechecking does not support tsconfig project references; select the workspace strategy or use a custom Vite config",
201
+ });
202
+ }
47
203
  }
48
204
  },
49
205
  );
@@ -22,14 +22,19 @@ jobs:
22
22
  with:
23
23
  persist-credentials: false
24
24
 
25
- - name: Set up Vite+
26
- uses: voidzero-dev/setup-vp@v1
25
+ # setup-vp's v1 tag is frozen. Keep this v1.16.1 commit current with
26
+ # Renovate or Dependabot so fixes arrive through reviewed pull requests.
27
+ - name: Set up Vite+ and install dependencies
28
+ uses: voidzero-dev/setup-vp@143f5f385f39b1b753ffed1a01ad443811855c8b # v1.16.1
27
29
  with:
30
+ # Vite+ version intentionally resolves from the consumer manifest/lock.
28
31
  node-version: "24"
29
32
  cache: true
33
+ run-install: |
34
+ args: ["--frozen-lockfile", "--ignore-scripts"]
30
35
 
31
- - name: Install dependencies
32
- run: vp install
36
+ - name: Verify locked Dev Kit setup
37
+ run: vp exec dev-kit apply --locked
33
38
 
34
39
  - name: Check formatting
35
40
  run: vp fmt --check