@danieljvdm/dev-kit 0.13.0 → 0.15.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.
@@ -0,0 +1,82 @@
1
+ import { Effect, Schema } from "effect";
2
+
3
+ import { readDirectDependencyNames } from "./project-package.ts";
4
+ import { planScaffold, readScaffoldTemplate, replaceUniqueTemplateMarker } from "./scaffold.ts";
5
+ import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
6
+
7
+ export const VITE_PLUS_GITHUB_ACTIONS_PATH = ".github/workflows/check.yml";
8
+ export const VITE_PLUS_GITHUB_ACTIONS_TEMPLATE = "templates/vite-plus/github-actions-check.yml";
9
+
10
+ export class VitePlusWorkflowSupportError extends Schema.TaggedError<VitePlusWorkflowSupportError>()(
11
+ "VitePlusWorkflowSupportError",
12
+ { message: Schema.String },
13
+ ) {}
14
+
15
+ const LOCKED_DEV_KIT_COMMAND =
16
+ "bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked";
17
+
18
+ export const renderVitePlusWorkflowTemplate = (
19
+ template: string,
20
+ options: { readonly devKitCommand?: string } = {},
21
+ ): string => {
22
+ const devKitCommand = options.devKitCommand;
23
+
24
+ return devKitCommand === undefined || devKitCommand === LOCKED_DEV_KIT_COMMAND
25
+ ? template
26
+ : replaceUniqueTemplateMarker(template, LOCKED_DEV_KIT_COMMAND, devKitCommand);
27
+ };
28
+
29
+ export const validateVitePlusWorkflowSupport = Effect.fn("validateVitePlusWorkflowSupport")(
30
+ function* (projectDir: string, packageRoot: string, typescriptPackage: string) {
31
+ const dependencies = yield* readDirectDependencyNames(projectDir);
32
+ const required = new Set(["effect", "@effect/tsgo", typescriptPackage]);
33
+
34
+ yield* validateInstalledVitePlus(projectDir).pipe(
35
+ Effect.mapError((error) => VitePlusWorkflowSupportError.make({ message: error.message })),
36
+ );
37
+
38
+ if (projectDir !== packageRoot) required.add("@danieljvdm/dev-kit");
39
+ const missing = [...required].filter((dependency) => !dependencies.includes(dependency));
40
+
41
+ if (missing.length > 0) {
42
+ return yield* VitePlusWorkflowSupportError.make({
43
+ message: `setup.vitePlus.workflow requires direct dependencies: ${missing.join(", ")}`,
44
+ });
45
+ }
46
+ },
47
+ );
48
+
49
+ export const planVitePlusWorkflow = (options: {
50
+ readonly packageRoot: string;
51
+ readonly projectDir: string;
52
+ readonly effectTsgoEnabled: boolean;
53
+ readonly typescriptPackage: string;
54
+ }) =>
55
+ planScaffold({
56
+ projectDir: options.projectDir,
57
+ path: VITE_PLUS_GITHUB_ACTIONS_PATH,
58
+ content: Effect.gen(function* () {
59
+ if (!options.effectTsgoEnabled) {
60
+ return yield* VitePlusWorkflowSupportError.make({
61
+ message:
62
+ "setup.vitePlus.workflow requires setup.effectTsgo.enabled so the scaffolded workflow's typecheck uses the Effect-patched compiler",
63
+ });
64
+ }
65
+ yield* validateVitePlusWorkflowSupport(
66
+ options.projectDir,
67
+ options.packageRoot,
68
+ options.typescriptPackage,
69
+ );
70
+ const template = yield* readScaffoldTemplate(
71
+ options.packageRoot,
72
+ VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
73
+ );
74
+
75
+ return renderVitePlusWorkflowTemplate(
76
+ template,
77
+ options.projectDir === options.packageRoot
78
+ ? { devKitCommand: "./bin/dev-kit.mjs apply --locked" }
79
+ : {},
80
+ );
81
+ }),
82
+ });
@@ -0,0 +1,88 @@
1
+ import { Effect, Schema } from "effect";
2
+
3
+ import {
4
+ detectPackageManager,
5
+ PACKAGE_MANAGER_COMMANDS,
6
+ readDirectDependencyNames,
7
+ readProjectPackage,
8
+ } from "./project-package.ts";
9
+ import { planScaffold, readScaffoldTemplate, replaceUniqueTemplateMarker } from "./scaffold.ts";
10
+
11
+ export const WORKTRUNK_CONFIG_PATH = ".config/wt.toml";
12
+ export const WORKTRUNK_CONFIG_TEMPLATE = "templates/worktrunk/wt.toml";
13
+
14
+ export class WorktrunkConfigSupportError extends Schema.TaggedError<WorktrunkConfigSupportError>()(
15
+ "WorktrunkConfigSupportError",
16
+ { message: Schema.String },
17
+ ) {}
18
+
19
+ export type WorktrunkConfigCommands = {
20
+ readonly preMerge: string;
21
+ readonly install: string;
22
+ readonly dev: string;
23
+ };
24
+
25
+ const TEMPLATE_COMMANDS: WorktrunkConfigCommands = {
26
+ preMerge: "vp run check",
27
+ install: "vp install",
28
+ dev: "vp dev",
29
+ };
30
+
31
+ export const renderWorktrunkConfigTemplate = (
32
+ template: string,
33
+ commands: WorktrunkConfigCommands = TEMPLATE_COMMANDS,
34
+ ): string => {
35
+ const replacements: ReadonlyArray<readonly [string, string]> = [
36
+ [`pre-merge = "${TEMPLATE_COMMANDS.preMerge}"`, `pre-merge = "${commands.preMerge}"`],
37
+ [`install = "${TEMPLATE_COMMANDS.install}"`, `install = "${commands.install}"`],
38
+ [TEMPLATE_COMMANDS.dev, commands.dev],
39
+ ];
40
+ let rendered = template;
41
+
42
+ for (const [marker, replacement] of replacements) {
43
+ if (marker === replacement) continue;
44
+ rendered = replaceUniqueTemplateMarker(rendered, marker, replacement);
45
+ }
46
+
47
+ return rendered;
48
+ };
49
+
50
+ export const resolveWorktrunkConfigCommands = Effect.fn("resolveWorktrunkConfigCommands")(
51
+ function* (projectDir: string) {
52
+ const dependencies = yield* readDirectDependencyNames(projectDir);
53
+
54
+ if (dependencies.includes("vite-plus")) return TEMPLATE_COMMANDS;
55
+ const projectPackage = yield* readProjectPackage(projectDir).pipe(
56
+ Effect.catchTag("ProjectPackageError", (error) =>
57
+ error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
58
+ ),
59
+ );
60
+ const scripts = projectPackage?.scripts ?? {};
61
+
62
+ if (scripts["check"] === undefined) {
63
+ return yield* WorktrunkConfigSupportError.make({
64
+ message:
65
+ 'setup.worktrunk.config requires a direct vite-plus dependency or a root "check" package script for the pre-merge hook',
66
+ });
67
+ }
68
+ const manager = yield* detectPackageManager(projectDir, projectPackage?.packageManager);
69
+
70
+ return {
71
+ preMerge: "bun run check",
72
+ install: PACKAGE_MANAGER_COMMANDS[manager ?? "bun"].install,
73
+ dev: "bun run dev",
74
+ } satisfies WorktrunkConfigCommands;
75
+ },
76
+ );
77
+
78
+ export const planWorktrunkConfig = (packageRoot: string, projectDir: string) =>
79
+ planScaffold({
80
+ projectDir,
81
+ path: WORKTRUNK_CONFIG_PATH,
82
+ content: Effect.gen(function* () {
83
+ const template = yield* readScaffoldTemplate(packageRoot, WORKTRUNK_CONFIG_TEMPLATE);
84
+ const commands = yield* resolveWorktrunkConfigCommands(projectDir);
85
+
86
+ return renderWorktrunkConfigTemplate(template, commands);
87
+ }),
88
+ });
@@ -38,8 +38,6 @@ jobs:
38
38
  - name: Verify locked Dev Kit setup
39
39
  run: bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked
40
40
 
41
- # Dev Kit inserts configured quality.workflow.beforeChecks steps here.
42
-
43
41
  - name: Check formatting
44
42
  run: vp fmt --check
45
43
 
@@ -0,0 +1,27 @@
1
+ # Worktrunk project hooks, scaffolded by @danieljvdm/dev-kit (setup.worktrunk.config).
2
+ # This repository owns the file — edit hooks freely; dev-kit never rewrites it.
3
+ # Hooks never run until each user reviews and approves them: wt config approvals add
4
+ # Reference: https://worktrunk.dev/hooks/
5
+
6
+ # Full validation before a worktree merges back.
7
+ pre-merge = "vp run check"
8
+
9
+ # Copy gitignored files matched by .worktreeinclude (.env, local caches) from
10
+ # the source worktree; without a .worktreeinclude file this is a no-op.
11
+ [[pre-start]]
12
+ copy-ignored = "wt step copy-ignored --require-include"
13
+
14
+ # Install dependencies after ignored files land so the new worktree is runnable.
15
+ [[pre-start]]
16
+ install = "vp install"
17
+
18
+ # Optional per-worktree dev server on a stable branch-derived port; tether stops
19
+ # the server when its worktree is removed. Point the command at this
20
+ # repository's dev entrypoint (for example add `apps/web` in a monorepo) and
21
+ # uncomment:
22
+ # [post-start]
23
+ # dev = "wt step tether -- vp dev --host 127.0.0.1 --port {{ branch | hash_port }} --strictPort"
24
+
25
+ # Optional: show each worktree's dev-server URL in `wt list`:
26
+ # [list]
27
+ # url = "http://localhost:{{ branch | hash_port }}"
@@ -1,148 +0,0 @@
1
- import { Effect, Schema } from "effect";
2
-
3
- import type { VitePlusQualityWorkflowStep } from "./manifest.ts";
4
- import { readDirectDependencyNames } from "./project-package.ts";
5
- import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
6
-
7
- export const VITE_PLUS_GITHUB_ACTIONS_PATH = ".github/workflows/check.yml";
8
- export const VITE_PLUS_GITHUB_ACTIONS_TEMPLATE = "templates/vite-plus/github-actions-check.yml";
9
-
10
- export class VitePlusQualitySupportError extends Schema.TaggedError<VitePlusQualitySupportError>()(
11
- "VitePlusQualitySupportError",
12
- { message: Schema.String },
13
- ) {}
14
-
15
- export type VitePlusQualityWorkflow = {
16
- readonly beforeChecks: ReadonlyArray<VitePlusQualityWorkflowStep>;
17
- readonly typecheck: ReadonlyArray<string>;
18
- };
19
-
20
- export type VitePlusQualitySelection = {
21
- readonly workflow: VitePlusQualityWorkflow;
22
- };
23
-
24
- const LOCKED_DEV_KIT_COMMAND =
25
- "bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked";
26
- const BEFORE_CHECKS_MARKER =
27
- " # Dev Kit inserts configured quality.workflow.beforeChecks steps here.\n\n";
28
- const DEFAULT_WORKFLOW_TYPECHECK = ` - name: Type check with Effect TypeScript-Go
29
- run: vp run typecheck`;
30
-
31
- const replaceUniqueTemplateMarker = (
32
- template: string,
33
- marker: string,
34
- replacement: string,
35
- ): string => {
36
- const parts = template.split(marker);
37
-
38
- if (parts.length !== 2) {
39
- throw new Error(`expected exactly one generated template marker: ${marker}`);
40
- }
41
-
42
- return `${parts[0]}${replacement}${parts[1]}`;
43
- };
44
-
45
- export const renderVitePlusWorkflowTemplate = (
46
- template: string,
47
- options: {
48
- readonly devKitCommand?: string;
49
- readonly workflow?: VitePlusQualityWorkflow;
50
- } = {},
51
- ): string => {
52
- const devKitCommand = options.devKitCommand;
53
- const workflow = options.workflow;
54
- let rendered =
55
- devKitCommand === undefined || devKitCommand === LOCKED_DEV_KIT_COMMAND
56
- ? template
57
- : replaceUniqueTemplateMarker(template, LOCKED_DEV_KIT_COMMAND, devKitCommand);
58
-
59
- if (workflow !== undefined) {
60
- const steps = workflow.beforeChecks
61
- .map((step) => {
62
- const commands = step.run
63
- .flatMap((command) => command.split("\n"))
64
- .map((line) => ` ${line}`)
65
- .join("\n");
66
-
67
- return ` - name: ${JSON.stringify(step.name)}
68
- run: |
69
- ${commands}`;
70
- })
71
- .join("\n\n");
72
-
73
- rendered = replaceUniqueTemplateMarker(
74
- rendered,
75
- BEFORE_CHECKS_MARKER,
76
- steps.length === 0 ? "" : `${steps}\n\n`,
77
- );
78
- }
79
- if (
80
- workflow !== undefined &&
81
- (workflow.typecheck.length !== 1 || workflow.typecheck[0] !== "vp run typecheck")
82
- ) {
83
- const commands = workflow.typecheck
84
- .flatMap((command) => command.split("\n"))
85
- .map((line) => ` ${line}`)
86
- .join("\n");
87
-
88
- rendered = replaceUniqueTemplateMarker(
89
- rendered,
90
- DEFAULT_WORKFLOW_TYPECHECK,
91
- ` - name: Type check with Effect TypeScript-Go
92
- run: |
93
- ${commands}`,
94
- );
95
- }
96
-
97
- return rendered;
98
- };
99
-
100
- export const validateVitePlusQualitySupport = Effect.fn("validateVitePlusQualitySupport")(
101
- function* (
102
- projectDir: string,
103
- packageRoot: string,
104
- typescriptPackage: string,
105
- selection: VitePlusQualitySelection,
106
- ) {
107
- const dependencies = yield* readDirectDependencyNames(projectDir);
108
- const required = new Set(["effect", "@effect/tsgo", typescriptPackage]);
109
-
110
- yield* validateInstalledVitePlus(projectDir).pipe(
111
- Effect.mapError((error) => VitePlusQualitySupportError.make({ message: error.message })),
112
- );
113
-
114
- if (projectDir !== packageRoot) required.add("@danieljvdm/dev-kit");
115
- const missing = [...required].filter((dependency) => !dependencies.includes(dependency));
116
-
117
- if (missing.length > 0) {
118
- return yield* VitePlusQualitySupportError.make({
119
- message: `setup.vitePlus.quality requires direct dependencies: ${missing.join(", ")}`,
120
- });
121
- }
122
- if (selection.workflow.typecheck.length === 0) {
123
- return yield* VitePlusQualitySupportError.make({
124
- message: "setup.vitePlus.quality.workflow.typecheck requires at least one command",
125
- });
126
- }
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",
131
- });
132
- }
133
- }
134
- for (const step of selection.workflow.beforeChecks) {
135
- if (step.name.trim().length === 0 || step.run.length === 0) {
136
- return yield* VitePlusQualitySupportError.make({
137
- message:
138
- "setup.vitePlus.quality.workflow.beforeChecks steps require a name and at least one command",
139
- });
140
- }
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",
144
- });
145
- }
146
- }
147
- },
148
- );