@danieljvdm/dev-kit 0.7.0 → 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 +67 -4
- package/dev-kit.example.jsonc +4 -1
- package/package.json +3 -1
- package/schema/dev-kit.schema.json +31 -0
- package/skills/dev-kit/SKILL.md +19 -4
- package/src/index.ts +4 -0
- package/src/manifest.ts +21 -0
- package/src/path-digest.ts +17 -3
- package/src/project-package.ts +1 -0
- package/src/sync.ts +60 -6
- package/src/tool-metadata.ts +2 -0
- package/src/vite-plus-dependency.ts +70 -0
- package/src/vite-plus-hooks.ts +9 -18
- package/src/vite-plus-quality.ts +162 -6
- package/templates/vite-plus/github-actions-check.yml +9 -4
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.
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
package/dev-kit.example.jsonc
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danieljvdm/dev-kit",
|
|
3
|
-
"version": "0.7.
|
|
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
|
},
|
package/skills/dev-kit/SKILL.md
CHANGED
|
@@ -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`
|
|
122
|
-
|
|
123
|
-
`
|
|
124
|
-
|
|
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
|
},
|
package/src/path-digest.ts
CHANGED
|
@@ -27,6 +27,7 @@ const textEncoder = new TextEncoder();
|
|
|
27
27
|
// Git preserves only the executable distinction for regular files. Canonicalizing
|
|
28
28
|
// the remaining bits keeps digests stable across checkout and copy umasks.
|
|
29
29
|
const canonicalFileMode = (mode: number): number => ((mode & 0o111) === 0 ? 0o644 : 0o755);
|
|
30
|
+
const rawFileMode = (mode: number): number => mode & 0o777;
|
|
30
31
|
|
|
31
32
|
const frame = (value: string | Uint8Array): Uint8Array => {
|
|
32
33
|
const bytes = typeof value === "string" ? textEncoder.encode(value) : value;
|
|
@@ -75,6 +76,7 @@ const digestFrames = Effect.fn("digestPathFrames")(function* (
|
|
|
75
76
|
|
|
76
77
|
const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
|
|
77
78
|
absolutePath: string,
|
|
79
|
+
fileMode: (mode: number) => number,
|
|
78
80
|
): Effect.fn.Return<
|
|
79
81
|
ObservedPath,
|
|
80
82
|
PlatformError.PlatformError | PathInspectionError,
|
|
@@ -107,7 +109,7 @@ const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
|
|
|
107
109
|
kind: "file",
|
|
108
110
|
digest: yield* digestFrames([
|
|
109
111
|
"file-v1",
|
|
110
|
-
String(
|
|
112
|
+
String(fileMode(info.mode)),
|
|
111
113
|
yield* fs.readFile(absolutePath),
|
|
112
114
|
]),
|
|
113
115
|
};
|
|
@@ -119,7 +121,7 @@ const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
|
|
|
119
121
|
|
|
120
122
|
for (const entry of entries) {
|
|
121
123
|
const childPath = path.join(absolutePath, entry);
|
|
122
|
-
const child = yield* digestFileSystemPath(childPath);
|
|
124
|
+
const child = yield* digestFileSystemPath(childPath, fileMode);
|
|
123
125
|
|
|
124
126
|
if (child.kind === "missing") {
|
|
125
127
|
return yield* new PathInspectionError({
|
|
@@ -142,7 +144,19 @@ const digestFileSystemPath = Effect.fn("digestFileSystemPath")(function* (
|
|
|
142
144
|
});
|
|
143
145
|
|
|
144
146
|
export const observePath = Effect.fn("observeManagedPath")(function* (absolutePath: string) {
|
|
145
|
-
return yield* digestFileSystemPath(absolutePath).pipe(
|
|
147
|
+
return yield* digestFileSystemPath(absolutePath, canonicalFileMode).pipe(
|
|
148
|
+
Effect.mapError((cause) =>
|
|
149
|
+
cause instanceof PathInspectionError
|
|
150
|
+
? cause
|
|
151
|
+
: new PathInspectionError({ path: absolutePath, operation: "inspect", cause }),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const observePathWithRawModes = Effect.fn("observeManagedPathWithRawModes")(function* (
|
|
157
|
+
absolutePath: string,
|
|
158
|
+
) {
|
|
159
|
+
return yield* digestFileSystemPath(absolutePath, rawFileMode).pipe(
|
|
146
160
|
Effect.mapError((cause) =>
|
|
147
161
|
cause instanceof PathInspectionError
|
|
148
162
|
? cause
|
package/src/project-package.ts
CHANGED
|
@@ -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
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
digestSymlinkTarget,
|
|
24
24
|
digestText,
|
|
25
25
|
observePath,
|
|
26
|
+
observePathWithRawModes,
|
|
26
27
|
type ObservedPath,
|
|
27
28
|
} from "./path-digest.ts";
|
|
28
29
|
import { readDirectDependencyNames } from "./project-package.ts";
|
|
@@ -47,6 +48,8 @@ import {
|
|
|
47
48
|
type VitePlusHooksPlan,
|
|
48
49
|
} from "./vite-plus-hooks.ts";
|
|
49
50
|
import {
|
|
51
|
+
renderVitePlusConfigTemplate,
|
|
52
|
+
renderVitePlusWorkflowTemplate,
|
|
50
53
|
validateVitePlusQualitySupport,
|
|
51
54
|
VITE_PLUS_CONFIG_PATH,
|
|
52
55
|
VITE_PLUS_CONFIG_TEMPLATE,
|
|
@@ -430,6 +433,27 @@ const outputIdentity = (output: ManagedOutput) =>
|
|
|
430
433
|
: { sourcePath: output.sourcePath }),
|
|
431
434
|
});
|
|
432
435
|
|
|
436
|
+
const outputOwnershipIdentity = (output: ManagedOutput) =>
|
|
437
|
+
JSON.stringify({
|
|
438
|
+
resourceId: output.resourceId,
|
|
439
|
+
path: output.path,
|
|
440
|
+
mode: output.mode,
|
|
441
|
+
kind: output.kind,
|
|
442
|
+
...("skill" in output
|
|
443
|
+
? { skill: output.skill, target: output.target }
|
|
444
|
+
: { sourcePath: output.sourcePath }),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const usesRawFileModeDigests = (toolVersion: string): boolean => {
|
|
448
|
+
const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
|
|
449
|
+
|
|
450
|
+
if (match === null) return false;
|
|
451
|
+
const major = Number(match[1]);
|
|
452
|
+
const minor = Number(match[2]);
|
|
453
|
+
|
|
454
|
+
return major === 0 && minor <= 6;
|
|
455
|
+
};
|
|
456
|
+
|
|
433
457
|
const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
434
458
|
projectDir: string,
|
|
435
459
|
outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
|
|
@@ -624,7 +648,16 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
624
648
|
},
|
|
625
649
|
]) {
|
|
626
650
|
const managed = yield* resolveManagedPath(projectDir, generated.path);
|
|
627
|
-
const
|
|
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
|
+
);
|
|
628
661
|
|
|
629
662
|
outputs.push({
|
|
630
663
|
resourceId: generated.resourceId,
|
|
@@ -743,12 +776,31 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
743
776
|
const receipt = receiptsById.get(output.resourceId);
|
|
744
777
|
const sameReceipt = receipt?.path === output.path ? receipt : undefined;
|
|
745
778
|
const locked = lockById.get(output.resourceId);
|
|
746
|
-
const
|
|
779
|
+
const matchingLockedOutput =
|
|
780
|
+
locked !== undefined &&
|
|
781
|
+
outputOwnershipIdentity(locked) === outputOwnershipIdentity(output) &&
|
|
782
|
+
observed.kind === locked.kind
|
|
783
|
+
? locked
|
|
784
|
+
: undefined;
|
|
785
|
+
const rawModeObservation =
|
|
786
|
+
matchingLockedOutput !== undefined &&
|
|
787
|
+
observed.kind !== "missing" &&
|
|
788
|
+
observed.digest !== matchingLockedOutput.digest &&
|
|
789
|
+
currentLock !== undefined &&
|
|
790
|
+
usesRawFileModeDigests(currentLock.toolVersion)
|
|
791
|
+
? yield* observePathWithRawModes(output.destination)
|
|
792
|
+
: undefined;
|
|
793
|
+
const lockedOwnsObserved =
|
|
794
|
+
matchingLockedOutput !== undefined &&
|
|
795
|
+
observed.kind !== "missing" &&
|
|
796
|
+
(observed.digest === matchingLockedOutput.digest ||
|
|
797
|
+
(rawModeObservation?.kind === matchingLockedOutput.kind &&
|
|
798
|
+
rawModeObservation.digest === matchingLockedOutput.digest));
|
|
747
799
|
|
|
748
800
|
if (observed.kind === "missing") {
|
|
749
801
|
actions.push({ action: "create", desired: output, observed });
|
|
750
802
|
} else if (observed.kind === output.kind && observed.digest === output.digest) {
|
|
751
|
-
if (sameReceipt ||
|
|
803
|
+
if (sameReceipt || lockedOwnsObserved || ("adoptIfExact" in output && output.adoptIfExact)) {
|
|
752
804
|
actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
|
|
753
805
|
} else {
|
|
754
806
|
actions.push({
|
|
@@ -758,9 +810,10 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
758
810
|
});
|
|
759
811
|
}
|
|
760
812
|
} else if (
|
|
761
|
-
sameReceipt &&
|
|
762
|
-
|
|
763
|
-
|
|
813
|
+
(sameReceipt !== undefined &&
|
|
814
|
+
observed.kind === sameReceipt.kind &&
|
|
815
|
+
observed.digest === sameReceipt.digest) ||
|
|
816
|
+
lockedOwnsObserved
|
|
764
817
|
) {
|
|
765
818
|
actions.push({ action: "update", desired: output, observed });
|
|
766
819
|
} else {
|
|
@@ -876,6 +929,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
876
929
|
projectDir,
|
|
877
930
|
packageRoot,
|
|
878
931
|
manifest.setup.effectTsgo.typescriptPackage,
|
|
932
|
+
manifest.setup.vitePlus.quality.typecheck,
|
|
879
933
|
);
|
|
880
934
|
}
|
|
881
935
|
const effectSource = manifest.setup.effectSource.enabled
|
package/src/tool-metadata.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/vite-plus-hooks.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 =
|
package/src/vite-plus-quality.ts
CHANGED
|
@@ -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* (
|
|
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(["
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
"
|
|
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
|
-
-
|
|
26
|
-
|
|
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:
|
|
32
|
-
run: vp
|
|
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
|