@danieljvdm/dev-kit 0.11.3 → 0.13.0
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 +111 -78
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/build-effect-clis/SKILL.md +61 -0
- package/skills/build-effect-clis/agents/openai.yaml +4 -0
- package/skills/build-effect-clis/references/command-design.md +93 -0
- package/skills/build-effect-clis/references/entrypoints-and-testing.md +59 -0
- package/skills/build-effect-clis/references/processes-and-platform.md +73 -0
- package/skills/dev-kit/SKILL.md +77 -48
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +86 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +3 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +18 -16
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +181 -131
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
package/src/vite-plus-quality.ts
CHANGED
|
@@ -1,37 +1,26 @@
|
|
|
1
|
-
import { Effect,
|
|
2
|
-
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
3
2
|
|
|
4
|
-
import type { VitePlusQualityWorkflowStep
|
|
5
|
-
import { readDirectDependencyNames
|
|
3
|
+
import type { VitePlusQualityWorkflowStep } from "./manifest.ts";
|
|
4
|
+
import { readDirectDependencyNames } from "./project-package.ts";
|
|
6
5
|
import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
|
|
7
6
|
|
|
8
|
-
export const VITE_PLUS_CONFIG_PATH = "vite.config.ts";
|
|
9
|
-
export const VITE_PLUS_CONFIG_TEMPLATE = "templates/vite-plus/vite.config.ts";
|
|
10
7
|
export const VITE_PLUS_GITHUB_ACTIONS_PATH = ".github/workflows/check.yml";
|
|
11
8
|
export const VITE_PLUS_GITHUB_ACTIONS_TEMPLATE = "templates/vite-plus/github-actions-check.yml";
|
|
12
9
|
|
|
13
|
-
export class VitePlusQualitySupportError extends Schema.
|
|
10
|
+
export class VitePlusQualitySupportError extends Schema.TaggedError<VitePlusQualitySupportError>()(
|
|
14
11
|
"VitePlusQualitySupportError",
|
|
15
12
|
{ message: Schema.String },
|
|
16
13
|
) {}
|
|
17
14
|
|
|
18
|
-
export type VitePlusQualityTypecheck = {
|
|
19
|
-
readonly strategy: VitePlusTypecheckStrategy;
|
|
20
|
-
readonly concurrency: number;
|
|
21
|
-
readonly packages: ReadonlyArray<string>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
15
|
export type VitePlusQualityWorkflow = {
|
|
25
16
|
readonly beforeChecks: ReadonlyArray<VitePlusQualityWorkflowStep>;
|
|
26
17
|
readonly typecheck: ReadonlyArray<string>;
|
|
27
18
|
};
|
|
28
19
|
|
|
29
20
|
export type VitePlusQualitySelection = {
|
|
30
|
-
readonly
|
|
31
|
-
readonly workflow?: VitePlusQualityWorkflow;
|
|
21
|
+
readonly workflow: VitePlusQualityWorkflow;
|
|
32
22
|
};
|
|
33
23
|
|
|
34
|
-
const SINGLE_PROJECT_TYPECHECK_TASK = ' typecheck: "tsc --noEmit",';
|
|
35
24
|
const LOCKED_DEV_KIT_COMMAND =
|
|
36
25
|
"bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked";
|
|
37
26
|
const BEFORE_CHECKS_MARKER =
|
|
@@ -53,28 +42,6 @@ const replaceUniqueTemplateMarker = (
|
|
|
53
42
|
return `${parts[0]}${replacement}${parts[1]}`;
|
|
54
43
|
};
|
|
55
44
|
|
|
56
|
-
const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
57
|
-
|
|
58
|
-
export const renderVitePlusConfigTemplate = (
|
|
59
|
-
template: string,
|
|
60
|
-
typecheck: VitePlusQualityTypecheck,
|
|
61
|
-
): string => {
|
|
62
|
-
if (typecheck.strategy === "single-project") return template;
|
|
63
|
-
const filters = typecheck.packages
|
|
64
|
-
.map((packageDir) => `--filter ${shellQuote(`./${packageDir}`)}`)
|
|
65
|
-
.join(" ");
|
|
66
|
-
const command = `vp run --cache --concurrency-limit ${typecheck.concurrency} ${filters} --fail-if-no-match typecheck`;
|
|
67
|
-
|
|
68
|
-
return replaceUniqueTemplateMarker(
|
|
69
|
-
template,
|
|
70
|
-
SINGLE_PROJECT_TYPECHECK_TASK,
|
|
71
|
-
` typecheck: {
|
|
72
|
-
command: ${JSON.stringify(command)},
|
|
73
|
-
cache: false,
|
|
74
|
-
},`,
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
45
|
export const renderVitePlusWorkflowTemplate = (
|
|
79
46
|
template: string,
|
|
80
47
|
options: {
|
|
@@ -137,161 +104,43 @@ export const validateVitePlusQualitySupport = Effect.fn("validateVitePlusQuality
|
|
|
137
104
|
typescriptPackage: string,
|
|
138
105
|
selection: VitePlusQualitySelection,
|
|
139
106
|
) {
|
|
140
|
-
const fs = yield* FileSystem.FileSystem;
|
|
141
|
-
const path = yield* Path.Path;
|
|
142
|
-
const packageJson = yield* readProjectPackage(projectDir);
|
|
143
107
|
const dependencies = yield* readDirectDependencyNames(projectDir);
|
|
144
108
|
const required = new Set(["effect", "@effect/tsgo", typescriptPackage]);
|
|
145
109
|
|
|
146
110
|
yield* validateInstalledVitePlus(projectDir).pipe(
|
|
147
|
-
Effect.mapError((error) =>
|
|
111
|
+
Effect.mapError((error) => VitePlusQualitySupportError.make({ message: error.message })),
|
|
148
112
|
);
|
|
149
113
|
|
|
150
114
|
if (projectDir !== packageRoot) required.add("@danieljvdm/dev-kit");
|
|
151
115
|
const missing = [...required].filter((dependency) => !dependencies.includes(dependency));
|
|
152
116
|
|
|
153
117
|
if (missing.length > 0) {
|
|
154
|
-
return yield*
|
|
118
|
+
return yield* VitePlusQualitySupportError.make({
|
|
155
119
|
message: `setup.vitePlus.quality requires direct dependencies: ${missing.join(", ")}`,
|
|
156
120
|
});
|
|
157
121
|
}
|
|
158
|
-
if (selection.workflow
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
message: "setup.vitePlus.quality.workflow.typecheck requires at least one command",
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
for (const command of selection.workflow.typecheck) {
|
|
165
|
-
if (command.trim().length === 0) {
|
|
166
|
-
return yield* new VitePlusQualitySupportError({
|
|
167
|
-
message: "setup.vitePlus.quality.workflow.typecheck commands must not be empty",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
for (const step of selection.workflow.beforeChecks) {
|
|
172
|
-
if (step.name.trim().length === 0 || step.run.length === 0) {
|
|
173
|
-
return yield* new VitePlusQualitySupportError({
|
|
174
|
-
message:
|
|
175
|
-
"setup.vitePlus.quality.workflow.beforeChecks steps require a name and at least one command",
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (step.run.some((command) => command.trim().length === 0)) {
|
|
179
|
-
return yield* new VitePlusQualitySupportError({
|
|
180
|
-
message: "setup.vitePlus.quality.workflow.beforeChecks commands must not be empty",
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const typecheck = selection.config;
|
|
186
|
-
|
|
187
|
-
if (typecheck === undefined) return;
|
|
188
|
-
const conflictingScripts = ["check", "typecheck"].filter(
|
|
189
|
-
(script) => packageJson.scripts?.[script] !== undefined,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
if (conflictingScripts.length > 0) {
|
|
193
|
-
return yield* new VitePlusQualitySupportError({
|
|
194
|
-
message: `setup.vitePlus.quality.config defines Vite tasks that conflict with package scripts: ${conflictingScripts.join(", ")}`,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
if (typecheck.concurrency < 1 || typecheck.concurrency > 32) {
|
|
198
|
-
return yield* new VitePlusQualitySupportError({
|
|
199
|
-
message: "setup.vitePlus.quality.config typecheck concurrency must be between 1 and 32",
|
|
122
|
+
if (selection.workflow.typecheck.length === 0) {
|
|
123
|
+
return yield* VitePlusQualitySupportError.make({
|
|
124
|
+
message: "setup.vitePlus.quality.workflow.typecheck requires at least one command",
|
|
200
125
|
});
|
|
201
126
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"setup.vitePlus.quality.
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
const workspacePatterns = Array.isArray(packageJson.workspaces)
|
|
209
|
-
? packageJson.workspaces
|
|
210
|
-
: typeof packageJson.workspaces === "object" && packageJson.workspaces !== null
|
|
211
|
-
? (packageJson.workspaces as { readonly packages?: unknown }).packages
|
|
212
|
-
: undefined;
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
typecheck.strategy === "workspace" &&
|
|
216
|
-
(!Array.isArray(workspacePatterns) ||
|
|
217
|
-
workspacePatterns.length === 0 ||
|
|
218
|
-
!workspacePatterns.every((pattern) => typeof pattern === "string"))
|
|
219
|
-
) {
|
|
220
|
-
return yield* new VitePlusQualitySupportError({
|
|
221
|
-
message:
|
|
222
|
-
"setup.vitePlus.quality.config workspace typechecking requires package.json workspaces",
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
if (typecheck.strategy === "workspace") {
|
|
226
|
-
if (typecheck.packages.length === 0) {
|
|
227
|
-
return yield* new VitePlusQualitySupportError({
|
|
228
|
-
message:
|
|
229
|
-
"setup.vitePlus.quality.config workspace typechecking requires explicit package directories",
|
|
127
|
+
for (const command of selection.workflow.typecheck) {
|
|
128
|
+
if (command.trim().length === 0) {
|
|
129
|
+
return yield* VitePlusQualitySupportError.make({
|
|
130
|
+
message: "setup.vitePlus.quality.workflow.typecheck commands must not be empty",
|
|
230
131
|
});
|
|
231
132
|
}
|
|
232
|
-
const uniquePackages = new Set(typecheck.packages);
|
|
233
|
-
|
|
234
|
-
if (uniquePackages.size !== typecheck.packages.length) {
|
|
235
|
-
return yield* new VitePlusQualitySupportError({
|
|
236
|
-
message: "setup.vitePlus.quality.config workspace typecheck packages must be unique",
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
for (const packageDir of typecheck.packages) {
|
|
240
|
-
const absolute = path.resolve(projectDir, packageDir);
|
|
241
|
-
const relative = path.relative(projectDir, absolute);
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
packageDir.length === 0 ||
|
|
245
|
-
path.isAbsolute(packageDir) ||
|
|
246
|
-
relative.length === 0 ||
|
|
247
|
-
relative === ".." ||
|
|
248
|
-
relative.startsWith(`..${path.sep}`)
|
|
249
|
-
) {
|
|
250
|
-
return yield* new VitePlusQualitySupportError({
|
|
251
|
-
message: `setup.vitePlus.quality.config workspace package must be a project-relative subdirectory: ${packageDir}`,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
const workspacePackage = yield* readProjectPackage(absolute).pipe(
|
|
255
|
-
Effect.mapError(
|
|
256
|
-
() =>
|
|
257
|
-
new VitePlusQualitySupportError({
|
|
258
|
-
message: `setup.vitePlus.quality.config workspace package is missing a valid package.json: ${packageDir}`,
|
|
259
|
-
}),
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (workspacePackage.scripts?.typecheck === undefined) {
|
|
264
|
-
return yield* new VitePlusQualitySupportError({
|
|
265
|
-
message: `setup.vitePlus.quality.config workspace package requires a typecheck script: ${packageDir}`,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
133
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (!(yield* fs.exists(tsconfigPath))) {
|
|
274
|
-
return yield* new VitePlusQualitySupportError({
|
|
134
|
+
for (const step of selection.workflow.beforeChecks) {
|
|
135
|
+
if (step.name.trim().length === 0 || step.run.length === 0) {
|
|
136
|
+
return yield* VitePlusQualitySupportError.make({
|
|
275
137
|
message:
|
|
276
|
-
"setup.vitePlus.quality.
|
|
138
|
+
"setup.vitePlus.quality.workflow.beforeChecks steps require a name and at least one command",
|
|
277
139
|
});
|
|
278
140
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
if (errors.length > 0 || typeof tsconfig !== "object" || tsconfig === null) {
|
|
285
|
-
return yield* new VitePlusQualitySupportError({
|
|
286
|
-
message: "setup.vitePlus.quality.config requires a valid root tsconfig.json",
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
const references = (tsconfig as { readonly references?: unknown }).references;
|
|
290
|
-
|
|
291
|
-
if (Array.isArray(references) && references.length > 0) {
|
|
292
|
-
return yield* new VitePlusQualitySupportError({
|
|
293
|
-
message:
|
|
294
|
-
"setup.vitePlus.quality.config single-project typechecking does not support tsconfig project references; select the workspace strategy or use a custom Vite config",
|
|
141
|
+
if (step.run.some((command) => command.trim().length === 0)) {
|
|
142
|
+
return yield* VitePlusQualitySupportError.make({
|
|
143
|
+
message: "setup.vitePlus.quality.workflow.beforeChecks commands must not be empty",
|
|
295
144
|
});
|
|
296
145
|
}
|
|
297
146
|
}
|
package/src/vite-plus.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { recommendedOxfmtConfig } from "./oxfmt.js";
|
|
2
|
+
import { recommendedOxlintConfig } from "./oxlint.js";
|
|
3
|
+
import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
|
|
4
|
+
|
|
5
|
+
export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
|
|
6
|
+
|
|
7
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
8
|
+
|
|
9
|
+
const validateWorkspacePackage = (packageDir) => {
|
|
10
|
+
const segments = packageDir.split(/[\\/]/);
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
packageDir.trim().length === 0 ||
|
|
14
|
+
packageDir === "." ||
|
|
15
|
+
packageDir === "./" ||
|
|
16
|
+
packageDir === ".\\" ||
|
|
17
|
+
packageDir.startsWith("/") ||
|
|
18
|
+
packageDir.startsWith("\\") ||
|
|
19
|
+
/^[A-Za-z]:[\\/]/.test(packageDir) ||
|
|
20
|
+
segments.includes("..")
|
|
21
|
+
) {
|
|
22
|
+
throw new Error(`workspace package must be a project-relative subdirectory: ${packageDir}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const createTypecheckTask = (options) => {
|
|
27
|
+
if (options?.strategy !== "workspace") {
|
|
28
|
+
const command = options?.command ?? "tsc --noEmit";
|
|
29
|
+
|
|
30
|
+
if (command.trim().length === 0) throw new Error("typecheck command must not be empty");
|
|
31
|
+
|
|
32
|
+
return command;
|
|
33
|
+
}
|
|
34
|
+
const concurrency = options.concurrency ?? 4;
|
|
35
|
+
|
|
36
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32) {
|
|
37
|
+
throw new Error("workspace typecheck concurrency must be between 1 and 32");
|
|
38
|
+
}
|
|
39
|
+
if (options.packages.length === 0) {
|
|
40
|
+
throw new Error("workspace typechecking requires at least one package directory");
|
|
41
|
+
}
|
|
42
|
+
const packages = [...new Set(options.packages)];
|
|
43
|
+
|
|
44
|
+
if (packages.length !== options.packages.length) {
|
|
45
|
+
throw new Error("workspace typecheck packages must be unique");
|
|
46
|
+
}
|
|
47
|
+
for (const packageDir of packages) validateWorkspacePackage(packageDir);
|
|
48
|
+
const filters = packages
|
|
49
|
+
.map((packageDir) => `--filter ${shellQuote(`./${packageDir}`)}`)
|
|
50
|
+
.join(" ");
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
command: `vp run --cache --concurrency-limit ${concurrency} ${filters} --fail-if-no-match typecheck`,
|
|
54
|
+
cache: false,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Build composable quality defaults for a project-owned Vite+ config. */
|
|
59
|
+
export const createRecommendedVitePlusConfig = (options = {}) => {
|
|
60
|
+
const ignorePatterns = [...devKitToolIgnorePatterns, ...(options.ignorePatterns ?? [])];
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
staged: {
|
|
64
|
+
"*": "vp check --fix",
|
|
65
|
+
},
|
|
66
|
+
fmt: {
|
|
67
|
+
...recommendedOxfmtConfig,
|
|
68
|
+
ignorePatterns,
|
|
69
|
+
},
|
|
70
|
+
lint: {
|
|
71
|
+
...recommendedOxlintConfig,
|
|
72
|
+
ignorePatterns,
|
|
73
|
+
},
|
|
74
|
+
run: {
|
|
75
|
+
tasks: {
|
|
76
|
+
check: ["vp fmt --check", "vp lint", "vp test", "vp run typecheck"],
|
|
77
|
+
typecheck: createTypecheckTask(options.typecheck),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
package/src/vite-plus.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { recommendedOxfmtConfig } from "./oxfmt.ts";
|
|
2
|
+
import { recommendedOxlintConfig } from "./oxlint.ts";
|
|
3
|
+
import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
|
|
4
|
+
|
|
5
|
+
export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
|
|
6
|
+
|
|
7
|
+
export type VitePlusTypecheckOptions =
|
|
8
|
+
| {
|
|
9
|
+
readonly strategy?: "single-project";
|
|
10
|
+
readonly command?: string;
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
readonly strategy: "workspace";
|
|
14
|
+
readonly packages: ReadonlyArray<string>;
|
|
15
|
+
readonly concurrency?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RecommendedVitePlusConfigOptions = {
|
|
19
|
+
/** Additional project-owned generated or vendored paths. */
|
|
20
|
+
readonly ignorePatterns?: ReadonlyArray<string>;
|
|
21
|
+
readonly typecheck?: VitePlusTypecheckOptions;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
25
|
+
|
|
26
|
+
const validateWorkspacePackage = (packageDir: string): void => {
|
|
27
|
+
const segments = packageDir.split(/[\\/]/);
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
packageDir.trim().length === 0 ||
|
|
31
|
+
packageDir === "." ||
|
|
32
|
+
packageDir === "./" ||
|
|
33
|
+
packageDir === ".\\" ||
|
|
34
|
+
packageDir.startsWith("/") ||
|
|
35
|
+
packageDir.startsWith("\\") ||
|
|
36
|
+
/^[A-Za-z]:[\\/]/.test(packageDir) ||
|
|
37
|
+
segments.includes("..")
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`workspace package must be a project-relative subdirectory: ${packageDir}`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const createTypecheckTask = (options: VitePlusTypecheckOptions | undefined) => {
|
|
44
|
+
if (options?.strategy !== "workspace") {
|
|
45
|
+
const command = options?.command ?? "tsc --noEmit";
|
|
46
|
+
|
|
47
|
+
if (command.trim().length === 0) throw new Error("typecheck command must not be empty");
|
|
48
|
+
|
|
49
|
+
return command;
|
|
50
|
+
}
|
|
51
|
+
const concurrency = options.concurrency ?? 4;
|
|
52
|
+
|
|
53
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32) {
|
|
54
|
+
throw new Error("workspace typecheck concurrency must be between 1 and 32");
|
|
55
|
+
}
|
|
56
|
+
if (options.packages.length === 0) {
|
|
57
|
+
throw new Error("workspace typechecking requires at least one package directory");
|
|
58
|
+
}
|
|
59
|
+
const packages = [...new Set(options.packages)];
|
|
60
|
+
|
|
61
|
+
if (packages.length !== options.packages.length) {
|
|
62
|
+
throw new Error("workspace typecheck packages must be unique");
|
|
63
|
+
}
|
|
64
|
+
for (const packageDir of packages) validateWorkspacePackage(packageDir);
|
|
65
|
+
const filters = packages
|
|
66
|
+
.map((packageDir) => `--filter ${shellQuote(`./${packageDir}`)}`)
|
|
67
|
+
.join(" ");
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
command: `vp run --cache --concurrency-limit ${concurrency} ${filters} --fail-if-no-match typecheck`,
|
|
71
|
+
cache: false,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build Dev Kit's recommended Vite+ quality defaults without owning the
|
|
77
|
+
* consuming repository's `vite.config.ts`. Spread the result before local
|
|
78
|
+
* project configuration.
|
|
79
|
+
*/
|
|
80
|
+
export const createRecommendedVitePlusConfig = (options: RecommendedVitePlusConfigOptions = {}) => {
|
|
81
|
+
const ignorePatterns = [...devKitToolIgnorePatterns, ...(options.ignorePatterns ?? [])];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
staged: {
|
|
85
|
+
"*": "vp check --fix",
|
|
86
|
+
},
|
|
87
|
+
fmt: {
|
|
88
|
+
...recommendedOxfmtConfig,
|
|
89
|
+
ignorePatterns,
|
|
90
|
+
},
|
|
91
|
+
lint: {
|
|
92
|
+
...recommendedOxlintConfig,
|
|
93
|
+
ignorePatterns,
|
|
94
|
+
},
|
|
95
|
+
run: {
|
|
96
|
+
tasks: {
|
|
97
|
+
check: ["vp fmt --check", "vp lint", "vp test", "vp run typecheck"],
|
|
98
|
+
typecheck: createTypecheckTask(options.typecheck),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
package/templates/AGENTS.md
CHANGED
|
@@ -6,6 +6,6 @@ This project uses `@danieljvdm/dev-kit` to manage portable agent skills and repr
|
|
|
6
6
|
|
|
7
7
|
For dev-kit operations, use the `dev-kit` skill and read `{{DEV_KIT_SKILL_PATH}}` before changing managed outputs.
|
|
8
8
|
|
|
9
|
-
{{PROJECT_COMMAND_POLICY}}
|
|
9
|
+
{{EFFECT_INSTRUCTIONS}}{{PROJECT_COMMAND_POLICY}}
|
|
10
10
|
|
|
11
11
|
<!-- DEV KIT END -->
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Upstream Baseline
|
|
2
|
-
|
|
3
|
-
The general Effect guides in this skill were adapted from
|
|
4
|
-
[`Effect-TS/skills`](https://github.com/Effect-TS/skills) at commit
|
|
5
|
-
`a8b6bb40d1d4d550b49c0ff7a624b5e6da500a24`.
|
|
6
|
-
|
|
7
|
-
They have been modified to:
|
|
8
|
-
|
|
9
|
-
- remove the mandatory `.repos/effect` checkout
|
|
10
|
-
- target the current canonical `Effect-TS/effect` v4 source
|
|
11
|
-
- update version-sensitive guidance from the upstream beta.66 snapshot to
|
|
12
|
-
beta.102
|
|
13
|
-
- preserve this repository's service ownership, layer construction,
|
|
14
|
-
schema-first modeling, type-boundary, HTTP, testing, logging, audit, and CLI
|
|
15
|
-
conventions
|
|
16
|
-
|
|
17
|
-
## Local Authoring Checkout
|
|
18
|
-
|
|
19
|
-
This repository keeps an ignored, version-matched Effect checkout for
|
|
20
|
-
validating source paths and beta-sensitive APIs. `vp i` runs the equivalent
|
|
21
|
-
dev-kit setup automatically; it can also be invoked directly:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
./bin/dev-kit.mjs effect sync
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Update the checkout, package dependencies, review baseline, feature index, and
|
|
28
|
-
stale-API tests together when moving to a newer Effect beta.
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Effect Atom cache lifecycle
|
|
2
|
-
|
|
3
|
-
## Registry and runtime scope
|
|
4
|
-
|
|
5
|
-
`RegistryProvider` creates one `AtomRegistry` on its first render. Its options do not rebuild that registry later. Provider unmount schedules disposal after a short grace period, so a quick React remount can reuse the same registry; moving or keying the provider still changes the cache boundary.
|
|
6
|
-
|
|
7
|
-
Place one provider around the client application subtree that should share data. Nested or route-local providers create separate caches.
|
|
8
|
-
|
|
9
|
-
### React mounts and registry disposal
|
|
10
|
-
|
|
11
|
-
`useAtomValue` keeps an atom active through its value subscription. `useAtomSet` and `useAtomRefresh` mount an atom through a React effect, while `useAtom` combines one value subscription with a setter. Use the combined hook when one component reads and writes the same atom; composing `useAtomValue` and `useAtomSet` adds a second mount and obscures which lifetime owns the work.
|
|
12
|
-
|
|
13
|
-
Unmount releases only that hook's subscription or mount. The registry removes a node only after it has no remaining consumers and its idle TTL permits removal. Node disposal runs registered finalizers, including cancellation of interruptible Effect work. Therefore component unmount, registry eviction, and `Atom.Interrupt` are distinct events; do not use an unconditional interrupt write as a substitute for releasing a React mount.
|
|
14
|
-
|
|
15
|
-
An atom runtime and a registry solve different problems:
|
|
16
|
-
|
|
17
|
-
- the registry stores atom nodes, values, subscriptions, idle timers, and finalizers;
|
|
18
|
-
- `Atom.context({ memoMap })` creates runtimes that share `Layer` construction through one `Layer.MemoMap`;
|
|
19
|
-
- the module-level `Atom.runtime` uses Effect's module-level default memo map.
|
|
20
|
-
|
|
21
|
-
Create one client runtime factory when several API/services must share layers:
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { Layer } from "effect";
|
|
25
|
-
import { Atom } from "effect/unstable/reactivity";
|
|
26
|
-
|
|
27
|
-
export const appAtomRuntime = Atom.context({
|
|
28
|
-
memoMap: Layer.makeMemoMapUnsafe(),
|
|
29
|
-
});
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Pass `appAtomRuntime` to each `AtomHttpApi.Service`. Do not create a memo map per query or component. On an SSR server, do not put request-specific authentication or services into a process-global memo map; use a request-scoped atom environment or keep the atom data path client-only.
|
|
33
|
-
|
|
34
|
-
## Stable identity and families
|
|
35
|
-
|
|
36
|
-
Export fixed queries directly. Use `Atom.family` when a parameter selects the resource:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
export const projectAtom = Atom.family((projectId: string) =>
|
|
40
|
-
ApiClient.query("projects", "get", {
|
|
41
|
-
params: { projectId },
|
|
42
|
-
timeToLive: "5 minutes",
|
|
43
|
-
reactivityKeys: { projects: [projectId] },
|
|
44
|
-
}).pipe(Atom.swr({ staleTime: "30 seconds", revalidateOnMount: true })),
|
|
45
|
-
);
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The family must receive a stable key. Prefer a primitive ID. If the key is an object, give it deliberate Effect `Equal`/`Hash` semantics or reuse the same object; repeated object literals can produce distinct family entries.
|
|
49
|
-
|
|
50
|
-
## Three independent clocks
|
|
51
|
-
|
|
52
|
-
| Control | Clock starts | What happens | What it does not mean |
|
|
53
|
-
| ----------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
54
|
-
| Registry/default idle TTL or query `timeToLive` | When an atom becomes unused | The registry keeps the cached node until idle eviction | The value is fresh during that period |
|
|
55
|
-
| `Atom.swr({ staleTime })` | From the latest success timestamp | A stale value can revalidate automatically on mount or focus while the prior success remains visible | The node survives an unmount long enough to be reused |
|
|
56
|
-
| `Atom.withRefresh(interval)` | While the wrapper is mounted | A timer force-refreshes the source and is canceled on disposal | Fresh requests are skipped |
|
|
57
|
-
|
|
58
|
-
Set retention long enough for the navigation/remount reuse window. `staleTime` cannot rescue a source that idle eviction already removed. A common policy is a longer `timeToLive`, a shorter `staleTime`, and polling only on screens that truly need it.
|
|
59
|
-
|
|
60
|
-
Manual `registry.refresh`, `useAtomRefresh`, invalidation, and `Atom.withRefresh` are forceful. They do not consult SWR freshness. Polling stops when the polling wrapper's lifetime is disposed because its finalizer clears the timer; applying `keepAlive` to that wrapper intentionally keeps polling alive.
|
|
61
|
-
|
|
62
|
-
## The `AsyncResult.all` route reset
|
|
63
|
-
|
|
64
|
-
`AsyncResult.all` returns the first non-success input. Therefore a route aggregate is only as reusable as its least-stable input:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
const routeDataAtom = Atom.make((get) =>
|
|
68
|
-
AsyncResult.all({
|
|
69
|
-
project: get(projectAtom("p-1")),
|
|
70
|
-
// Bad if created during render or rebuilt for every route visit:
|
|
71
|
-
preferences: get(makePreferencesAtom()),
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
If `preferences` is a fresh or evicted atom, it starts at `Initial`; the aggregate also looks initial even though `project` is cached. Fix the input's ownership and retention. Define a singleton/family atom outside render and give it a deliberate idle TTL. Memoizing only the `AsyncResult.all` call does not repair an unstable input atom.
|
|
77
|
-
|
|
78
|
-
`AsyncResult.all` also constructs a new success container. Keep aggregation inside a derived atom so the registry controls recomputation instead of rebuilding the container ad hoc in render.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Effect Atom HTTP queries and invalidation
|
|
2
|
-
|
|
3
|
-
## Build one API service
|
|
4
|
-
|
|
5
|
-
`AtomHttpApi.Service` generates a typed client, a runtime, query atoms, and mutation functions. Pass the shared runtime factory so API services share the intended `Layer.MemoMap`:
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
9
|
-
import { AtomHttpApi } from "effect/unstable/reactivity";
|
|
10
|
-
import { appAtomRuntime } from "./atom-runtime";
|
|
11
|
-
import { Api } from "./api";
|
|
12
|
-
|
|
13
|
-
export const ApiClient = AtomHttpApi.Service()("ApiClient", {
|
|
14
|
-
api: Api,
|
|
15
|
-
httpClient: FetchHttpClient.layer,
|
|
16
|
-
baseUrl: "/api",
|
|
17
|
-
runtime: appAtomRuntime,
|
|
18
|
-
});
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Compile examples against the installed Effect version; the reactivity and HTTP APIs are unstable.
|
|
22
|
-
|
|
23
|
-
## Queries
|
|
24
|
-
|
|
25
|
-
`query(group, endpoint, request)` returns an `Atom<AsyncResult<...>>`. The service internally memoizes request keys with a family. A public `Atom.family` remains useful for expressing domain ownership with a simple, stable parameter and applying one cache policy.
|
|
26
|
-
|
|
27
|
-
Query options have separate roles:
|
|
28
|
-
|
|
29
|
-
- `timeToLive`: finite values apply idle TTL; infinity keeps the query alive;
|
|
30
|
-
- `reactivityKeys`: register the query for refresh after matching invalidation;
|
|
31
|
-
- `serializationKey`: make decoded-only results serializable for hydration; it is not the runtime cache key;
|
|
32
|
-
- `responseMode`: changes the response and error shape.
|
|
33
|
-
|
|
34
|
-
Never place secrets in `serializationKey`, URL state, hydration payloads, or client-visible layers.
|
|
35
|
-
|
|
36
|
-
## Mutations and action ownership
|
|
37
|
-
|
|
38
|
-
Create mutation atoms once, then invoke them from the component or workflow that owns the action:
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
export const updateProject = ApiClient.mutation("projects", "update");
|
|
42
|
-
|
|
43
|
-
// In the initiating component:
|
|
44
|
-
const mutate = useAtomSet(updateProject, { mode: "promise" });
|
|
45
|
-
await mutate({
|
|
46
|
-
params: { projectId },
|
|
47
|
-
payload: patch,
|
|
48
|
-
reactivityKeys: { projects: [projectId] },
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The mutation's `reactivityKeys` are invalidated only after the request succeeds. Failed mutations do not invalidate. Keep navigation, toasts, dialog closure, and optimistic UI at the action owner; keep shared server-state refresh in reactivity keys.
|
|
53
|
-
|
|
54
|
-
If the component also renders the mutation result, use `useAtom(updateProject)` instead of pairing `useAtomValue` with `useAtomSet`. A module-scoped mutation atom is shared registry state, so an unconditional cleanup write of `Atom.Interrupt` can cancel work owned by another consumer and publishes an interrupted failure.
|
|
55
|
-
|
|
56
|
-
Keep a multi-step mutation sequence in an owner that lives for the whole sequence. When navigation can unmount the initiating route after one request succeeds, a component-owned sequential fan-out can leave a partially completed operation. Use one stable workflow atom or service when client ownership is sufficient; use one server-side command or durable workflow when completion must survive browser navigation or disconnects.
|
|
57
|
-
|
|
58
|
-
## Use one key vocabulary
|
|
59
|
-
|
|
60
|
-
Array keys represent independent keys. Record keys support hierarchical broad-plus-entity invalidation:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
const listKeys = { projects: [] };
|
|
64
|
-
const detailKeys = { projects: [projectId] };
|
|
65
|
-
|
|
66
|
-
ApiClient.query("projects", "list", { reactivityKeys: listKeys });
|
|
67
|
-
ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
|
|
68
|
-
|
|
69
|
-
// This mutation invalidates the broad namespace and this entity key.
|
|
70
|
-
await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Record semantics register the property name and each `property:id` combination. Consequently, `{ projects: [projectId] }` is hierarchical: it invalidates both the broad `projects` namespace and the specific `projects:projectId` key. Every record-form project query also subscribed to that broad namespace can refresh. Use this when an entity write may affect lists or aggregates.
|
|
74
|
-
|
|
75
|
-
For exact entity-only invalidation, use namespaced primitive array keys consistently instead:
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
const projectKey = (id: string) => `project:${id}`;
|
|
79
|
-
|
|
80
|
-
const detailKeys = [projectKey(projectId)];
|
|
81
|
-
const collectionKeys = ["projects"];
|
|
82
|
-
|
|
83
|
-
ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
|
|
84
|
-
await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Use `collectionKeys` for the collection query and for mutations that can change membership or ordering.
|
|
88
|
-
|
|
89
|
-
Standardize key constructors in one module when several endpoints share them; mismatched strings fail silently.
|
|
90
|
-
|
|
91
|
-
Choose invalidation breadth from the server write:
|
|
92
|
-
|
|
93
|
-
- invalidate an exact array-form entity key when only one detail changed;
|
|
94
|
-
- invalidate the collection key, or use hierarchical record keys, when list membership, ordering, totals, or filters can change;
|
|
95
|
-
- invalidate multiple record properties when a write affects related aggregates.
|
|
96
|
-
|
|
97
|
-
Do not both invalidate and manually refresh the same query unless two requests are intentional.
|