@danieljvdm/dev-kit 0.15.0 → 0.17.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.
@@ -1,4 +1,4 @@
1
- import { Effect, FileSystem, Path, Schema } from "effect";
1
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
2
2
 
3
3
  export class ProjectPackageError extends Schema.TaggedError<ProjectPackageError>()(
4
4
  "ProjectPackageError",
@@ -91,23 +91,87 @@ export const detectPackageManager = Effect.fn("detectPackageManager")(function*
91
91
  return detected.length === 1 ? detected[0] : undefined;
92
92
  });
93
93
 
94
- export const readDirectDependencyNames = Effect.fn("readDirectDependencyNames")(function* (
95
- projectDir: string,
94
+ const readOptionalProjectPackage = Effect.fn("readOptionalProjectPackage")(function* (
95
+ packageDir: string,
96
96
  ) {
97
- const manifest = yield* readProjectPackage(projectDir).pipe(
97
+ return yield* readProjectPackage(packageDir).pipe(
98
98
  Effect.catchTag("ProjectPackageError", (error) =>
99
99
  error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
100
100
  ),
101
101
  );
102
+ });
103
+
104
+ const manifestDependencyNames = (
105
+ manifest: (typeof ProjectPackageSchema)["Type"] | undefined | void,
106
+ ): ReadonlyArray<string> =>
107
+ manifest === undefined
108
+ ? []
109
+ : [
110
+ ...Object.keys(manifest.dependencies ?? {}),
111
+ ...Object.keys(manifest.devDependencies ?? {}),
112
+ ...Object.keys(manifest.optionalDependencies ?? {}),
113
+ ...Object.keys(manifest.peerDependencies ?? {}),
114
+ ];
115
+
116
+ export const readDirectDependencyNames = Effect.fn("readDirectDependencyNames")(function* (
117
+ projectDir: string,
118
+ ) {
119
+ const manifest = yield* readOptionalProjectPackage(projectDir);
120
+
121
+ return [...new Set(manifestDependencyNames(manifest))].sort();
122
+ });
123
+
124
+ const WorkspacePatternsSchema = Schema.Union([
125
+ Schema.Array(Schema.String),
126
+ Schema.Struct({ packages: Schema.Array(Schema.String) }),
127
+ ]);
128
+ // `workspaces` is declared `Schema.Unknown` in the project manifest schema, so
129
+ // this is a genuinely untyped boundary.
130
+ const decodeWorkspacePatterns = Schema.decodeUnknownOption(WorkspacePatternsSchema);
131
+
132
+ const workspacePatterns = (workspaces: unknown): ReadonlyArray<string> => {
133
+ const decoded = decodeWorkspacePatterns(workspaces);
102
134
 
103
- if (manifest === undefined) return [];
135
+ if (Option.isNone(decoded)) return [];
136
+
137
+ return "packages" in decoded.value ? decoded.value.packages : decoded.value;
138
+ };
139
+
140
+ /**
141
+ * Direct dependency names of the project package plus every workspace member
142
+ * package. Only literal workspace paths and single trailing-star globs
143
+ * (`apps/*`) are expanded; other patterns are skipped.
144
+ */
145
+ export const readWorkspaceDependencyNames = Effect.fn("readWorkspaceDependencyNames")(function* (
146
+ projectDir: string,
147
+ ) {
148
+ const fs = yield* FileSystem.FileSystem;
149
+ const path = yield* Path.Path;
150
+ const manifest = yield* readOptionalProjectPackage(projectDir);
151
+ const names = new Set(manifestDependencyNames(manifest));
152
+
153
+ for (const pattern of workspacePatterns(manifest?.workspaces)) {
154
+ if (pattern.startsWith("!")) continue;
155
+ const star = pattern.indexOf("*");
156
+ let memberDirs: ReadonlyArray<string> = [];
157
+
158
+ if (star === -1) {
159
+ memberDirs = [pattern];
160
+ } else if (pattern.endsWith("/*") && star === pattern.length - 1) {
161
+ const parent = path.join(projectDir, pattern.slice(0, -2));
162
+
163
+ if (yield* fs.exists(parent)) {
164
+ memberDirs = (yield* fs.readDirectory(parent)).map((name) =>
165
+ path.join(pattern.slice(0, -2), name),
166
+ );
167
+ }
168
+ }
169
+ for (const memberDir of memberDirs) {
170
+ const member = yield* readOptionalProjectPackage(path.join(projectDir, memberDir));
171
+
172
+ for (const name of manifestDependencyNames(member)) names.add(name);
173
+ }
174
+ }
104
175
 
105
- return [
106
- ...new Set([
107
- ...Object.keys(manifest.dependencies ?? {}),
108
- ...Object.keys(manifest.devDependencies ?? {}),
109
- ...Object.keys(manifest.optionalDependencies ?? {}),
110
- ...Object.keys(manifest.peerDependencies ?? {}),
111
- ]),
112
- ].sort();
176
+ return [...names].sort();
113
177
  });
package/src/sync.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  planEffectTsgoPatch,
16
16
  type EffectTsgoPatchPlan,
17
17
  } from "./effect-tsgo.ts";
18
+ import { maybePruneGlobalCache } from "./global-cache.ts";
18
19
  import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
19
20
  import { observeSymbolicLink } from "./node-symbolic-link.ts";
20
21
  import { resolvePackageSkillSelector } from "./package-skill-source.ts";
@@ -29,6 +30,7 @@ import {
29
30
  detectPackageManager,
30
31
  PACKAGE_MANAGER_COMMANDS,
31
32
  readDirectDependencyNames,
33
+ readWorkspaceDependencyNames,
32
34
  readProjectPackage,
33
35
  type PackageManagerName,
34
36
  } from "./project-package.ts";
@@ -266,7 +268,13 @@ const encodePlanSnapshotJson = Schema.encodeSync(Schema.fromJsonString(Schema.Un
266
268
  const encodeAppliedStatePrettyJson = Schema.encodeSync(fromJsonString(AppliedStateSchema, 2));
267
269
 
268
270
  const SKILL_FAMILIES: SkillCatalog = {
269
- effect: ["effect-ts", "effect-architecture-audit", "build-effect-apis", "build-effect-clis"],
271
+ effect: [
272
+ "effect-ts",
273
+ "effect-architecture-audit",
274
+ "build-effect-apis",
275
+ "effect-atom-state",
276
+ "build-effect-clis",
277
+ ],
270
278
  };
271
279
 
272
280
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
@@ -805,7 +813,7 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
805
813
  );
806
814
  const directDependencyNames = yield* readDirectDependencyNames(projectDir);
807
815
  const usesVitePlus = directDependencyNames.includes("vite-plus");
808
- const effectInstructions =
816
+ const effectGuideInstructions =
809
817
  directDependencyNames.includes("effect") &&
810
818
  (yield* observePath(path.join(projectDir, "node_modules", "effect", "AGENTS.md"))).kind ===
811
819
  "file"
@@ -821,6 +829,20 @@ guide doesn't cover, search through the source code in \`node_modules/effect/src
821
829
 
822
830
  `
823
831
  : "";
832
+ const atomBoundaryInstructions = (yield* readWorkspaceDependencyNames(projectDir)).includes(
833
+ "@effect/atom-react",
834
+ )
835
+ ? `# Effect Atom client boundary
836
+
837
+ This repository consumes APIs through Effect Atom clients (\`@effect/atom-react\`).
838
+ Keep business logic in Effect: compose multi-step client workflows as atoms,
839
+ declare cross-query invalidation as reactivity keys on mutations, and keep
840
+ promise-mode dispatches at the React boundary logic-free — no \`.then\` chains
841
+ in components or routes.
842
+
843
+ `
844
+ : "";
845
+ const effectInstructions = `${effectGuideInstructions}${atomBoundaryInstructions}`;
824
846
  const projectPackage = yield* readProjectPackage(projectDir).pipe(
825
847
  Effect.catchTag("ProjectPackageError", (error) =>
826
848
  error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
@@ -1877,6 +1899,18 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1877
1899
  yield* applyPlannedSkillChanges(replanned);
1878
1900
  }),
1879
1901
  );
1902
+ const fs = yield* FileSystem.FileSystem;
1903
+ const path = yield* Path.Path;
1904
+
1905
+ // Catalog checkouts moved to the machine-global cache; drop the regenerable
1906
+ // project-local copies left behind by earlier dev-kit versions.
1907
+ yield* fs
1908
+ .remove(path.join(plan.projectDir, ".dev-kit", "cache", "catalog"), {
1909
+ force: true,
1910
+ recursive: true,
1911
+ })
1912
+ .pipe(Effect.ignore);
1913
+ yield* maybePruneGlobalCache().pipe(Effect.ignore);
1880
1914
  yield* printStatus(
1881
1915
  "success",
1882
1916
  changes === 0 && !replanned.metadataChanged ? "Dev kit up to date" : "Dev kit ready",
package/src/vite-plus.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { recommendedOxfmtConfig } from "./oxfmt.js";
2
- import { recommendedOxlintConfig } from "./oxlint.js";
2
+ import { createAbsoluteImportsOxlintOverride, recommendedOxlintConfig } from "./oxlint.js";
3
3
  import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
4
4
 
5
5
  export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
@@ -58,6 +58,12 @@ const createTypecheckTask = (options) => {
58
58
  /** Build composable quality defaults for a project-owned Vite+ config. */
59
59
  export const createRecommendedVitePlusConfig = (options = {}) => {
60
60
  const ignorePatterns = [...devKitToolIgnorePatterns, ...(options.ignorePatterns ?? [])];
61
+ const lintOverrides = options.absoluteImports
62
+ ? [
63
+ ...recommendedOxlintConfig.overrides,
64
+ createAbsoluteImportsOxlintOverride(options.absoluteImports),
65
+ ]
66
+ : recommendedOxlintConfig.overrides;
61
67
 
62
68
  return {
63
69
  staged: {
@@ -70,6 +76,7 @@ export const createRecommendedVitePlusConfig = (options = {}) => {
70
76
  lint: {
71
77
  ...recommendedOxlintConfig,
72
78
  ignorePatterns,
79
+ overrides: lintOverrides,
73
80
  },
74
81
  run: {
75
82
  tasks: {
package/src/vite-plus.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import { recommendedOxfmtConfig } from "./oxfmt.ts";
2
- import { recommendedOxlintConfig } from "./oxlint.ts";
2
+ import {
3
+ type AbsoluteImportsOptions,
4
+ createAbsoluteImportsOxlintOverride,
5
+ recommendedOxlintConfig,
6
+ } from "./oxlint.ts";
3
7
  import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
4
8
 
9
+ export type { AbsoluteImportsOptions } from "./oxlint.ts";
5
10
  export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
6
11
 
7
12
  export type VitePlusTypecheckOptions =
@@ -16,6 +21,8 @@ export type VitePlusTypecheckOptions =
16
21
  };
17
22
 
18
23
  export type RecommendedVitePlusConfigOptions = {
24
+ /** Enforce path-alias imports (no `../`) inside the given globs. */
25
+ readonly absoluteImports?: AbsoluteImportsOptions;
19
26
  /** Additional project-owned generated or vendored paths. */
20
27
  readonly ignorePatterns?: ReadonlyArray<string>;
21
28
  readonly typecheck?: VitePlusTypecheckOptions;
@@ -79,6 +86,12 @@ const createTypecheckTask = (options: VitePlusTypecheckOptions | undefined) => {
79
86
  */
80
87
  export const createRecommendedVitePlusConfig = (options: RecommendedVitePlusConfigOptions = {}) => {
81
88
  const ignorePatterns = [...devKitToolIgnorePatterns, ...(options.ignorePatterns ?? [])];
89
+ const lintOverrides = options.absoluteImports
90
+ ? [
91
+ ...recommendedOxlintConfig.overrides,
92
+ createAbsoluteImportsOxlintOverride(options.absoluteImports),
93
+ ]
94
+ : recommendedOxlintConfig.overrides;
82
95
 
83
96
  return {
84
97
  staged: {
@@ -91,6 +104,7 @@ export const createRecommendedVitePlusConfig = (options: RecommendedVitePlusConf
91
104
  lint: {
92
105
  ...recommendedOxlintConfig,
93
106
  ignorePatterns,
107
+ overrides: lintOverrides,
94
108
  },
95
109
  run: {
96
110
  tasks: {