@danieljvdm/dev-kit 0.3.2 → 0.4.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 CHANGED
@@ -309,16 +309,21 @@ transforms, and installs the result through the ownership-safe sync path. Normal
309
309
  installs never float to a newer upstream commit; only a reviewed catalog refresh
310
310
  changes what is approved.
311
311
 
312
- ## Oxlint preset for Vite+
312
+ ## Oxlint and Oxfmt configurations
313
313
 
314
- Dev Kit exports a typed, composable set of high-signal Oxlint rules for Vite+
315
- projects:
314
+ Dev Kit exports one typed Oxlint ruleset and one typed Oxfmt configuration for
315
+ both standalone Oxc projects and Vite+ projects. A Vite+ project composes them
316
+ in `vite.config.ts`:
316
317
 
317
318
  ```ts
318
319
  import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
320
+ import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
319
321
  import { defineConfig } from "vite-plus";
320
322
 
321
323
  export default defineConfig({
324
+ fmt: {
325
+ ...recommendedOxfmtConfig,
326
+ },
322
327
  lint: {
323
328
  extends: [recommendedOxlintConfig],
324
329
  rules: {
@@ -329,7 +334,38 @@ export default defineConfig({
329
334
  ```
330
335
 
331
336
  Use `lint.extends` rather than a shallow object spread so Vite+ composes the
332
- nested plugin and rule configuration correctly.
337
+ nested plugin and rule configuration correctly. Oxfmt has no `extends`, so
338
+ spread its configuration before project-local formatter options.
339
+
340
+ Standalone projects import the same objects from their native config files:
341
+
342
+ ```ts
343
+ // oxlint.config.ts
344
+ import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
345
+ import { defineConfig } from "oxlint";
346
+
347
+ export default defineConfig({
348
+ extends: [recommendedOxlintConfig],
349
+ });
350
+ ```
351
+
352
+ ```ts
353
+ // oxfmt.config.ts
354
+ import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
355
+ import { defineConfig } from "oxfmt";
356
+
357
+ export default defineConfig({
358
+ ...recommendedOxfmtConfig,
359
+ });
360
+ ```
361
+
362
+ The Oxlint preset also registers the shared `effect` JavaScript plugin. Effect
363
+ projects opt into its rules in path-specific overrides, for example
364
+ `effect/no-effect-run`, `effect/no-unsafe-promise`, and
365
+ `effect/no-untyped-throw`. The package exports the plugin directly from
366
+ `@danieljvdm/dev-kit/oxlint-plugin-effect` for configurations that do not
367
+ extend the recommended preset. Strict workflow, Atom, and boundary rules remain
368
+ consumer-scoped because application and host boundaries differ by repository.
333
369
 
334
370
  ## Development
335
371
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Declarative project development toolkit with portable agent skills.",
@@ -23,6 +23,14 @@
23
23
  "./oxlint": {
24
24
  "types": "./src/oxlint.ts",
25
25
  "default": "./src/oxlint.js"
26
+ },
27
+ "./oxfmt": {
28
+ "types": "./src/oxfmt.ts",
29
+ "default": "./src/oxfmt.js"
30
+ },
31
+ "./oxlint-plugin-effect": {
32
+ "types": "./src/oxlint-plugin-effect.d.ts",
33
+ "default": "./src/oxlint-plugin-effect.js"
26
34
  }
27
35
  },
28
36
  "files": [
@@ -64,14 +72,24 @@
64
72
  "@effect/tsgo": "0.24.3",
65
73
  "@effect/vitest": "4.0.0-beta.102",
66
74
  "@types/node": "25.9.1",
75
+ "oxfmt": "0.60.0",
76
+ "oxlint": "1.75.0",
67
77
  "typescript": "7.0.2",
68
78
  "vite-plus": "0.2.6",
69
79
  "vitest": "4.1.10"
70
80
  },
71
81
  "peerDependencies": {
82
+ "oxfmt": ">=0.60.0 <0.61.0",
83
+ "oxlint": ">=1.75.0 <2.0.0",
72
84
  "vite-plus": ">=0.2.6 <0.3.0"
73
85
  },
74
86
  "peerDependenciesMeta": {
87
+ "oxfmt": {
88
+ "optional": true
89
+ },
90
+ "oxlint": {
91
+ "optional": true
92
+ },
75
93
  "vite-plus": {
76
94
  "optional": true
77
95
  }
@@ -156,16 +156,19 @@ dependencies; `dev-kit apply` patches once and then converges.
156
156
  Use `dev-kit tsgo patch --dry-run` for focused diagnosis. Use `--force` only
157
157
  after the user accepts a potentially commit-incompatible TypeScript binary.
158
158
 
159
- ## Oxlint preset for Vite+
159
+ ## Oxlint and Oxfmt configurations
160
160
 
161
- For Vite+ projects, compose the package's typed recommended Oxlint preset with
162
- project-specific lint configuration:
161
+ Use Dev Kit's canonical Oxlint and Oxfmt objects in Vite+ projects:
163
162
 
164
163
  ```ts
165
164
  import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
165
+ import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
166
166
  import { defineConfig } from "vite-plus";
167
167
 
168
168
  export default defineConfig({
169
+ fmt: {
170
+ ...recommendedOxfmtConfig,
171
+ },
169
172
  lint: {
170
173
  extends: [recommendedOxlintConfig],
171
174
  rules: {
@@ -176,14 +179,24 @@ export default defineConfig({
176
179
  ```
177
180
 
178
181
  Use `lint.extends` instead of spreading the object so Vite+ composes nested
179
- rule maps correctly. Keep repository-specific path conventions and
180
- platform-specific accessibility rules in the consuming project.
182
+ rule maps correctly. Oxfmt has no inheritance mechanism, so spread its object
183
+ before project-local options. Standalone `oxlint.config.ts` uses the same
184
+ `extends: [recommendedOxlintConfig]`; standalone `oxfmt.config.ts` spreads the
185
+ same `recommendedOxfmtConfig`.
186
+
187
+ The Oxlint preset registers Dev Kit's shared Effect plugin as `effect`, but
188
+ does not enable its scope-sensitive rules globally. Effect projects should
189
+ enable rules such as `effect/no-effect-run`, `effect/no-unsafe-promise`, and
190
+ `effect/no-untyped-throw` only in Effect-owned code, with explicit exceptions
191
+ for tests and host boundaries. The stricter `effect/no-async-workflow`,
192
+ `effect/no-promise-atom-mode`, and `effect/no-sync-boundary-decode` rules also
193
+ need consumer-owned scopes. Keep repository-specific paths and platform rules
194
+ in the consuming project.
181
195
 
182
196
  ## Current boundary
183
197
 
184
198
  Manage skill outputs, the `setup.effectSource` checkout, and the explicit
185
199
  `setup.effectTsgo` task. Edit shared `package.json` and `tsconfig.json`
186
- contributions deliberately. The Oxlint preset is a composable package export,
187
- not a manifest-managed output. Treat named bundles, Oxfmt presets, and broader
188
- setup tasks as future manifest capabilities until the installed CLI exposes
189
- them.
200
+ contributions deliberately. The Oxlint and Oxfmt configurations are composable
201
+ package exports, not manifest-managed outputs. Treat broader setup tasks as
202
+ future manifest capabilities until the installed CLI exposes them.
@@ -99,7 +99,8 @@ Run the narrowest meaningful checks:
99
99
 
100
100
  - Confirm that every TypeScript entrypoint named by `package.json` belongs to
101
101
  one checked TypeScript project.
102
- - `bun run check:scripts` for root scripts.
102
+ - The repository's root `check` or typecheck script when the entrypoint belongs
103
+ to the root project.
103
104
  - Package-level `bun run check` when the script lives under an app/package and is included in that TS config.
104
105
  - The command's `--help` path.
105
106
  - A dry-run or harmless invalid-input path that loads the entrypoint and
package/src/oxfmt.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Runtime form of the typed preset declared in oxfmt.ts.
3
+ *
4
+ * This file is intentionally plain JavaScript because Node does not strip
5
+ * TypeScript from packages in node_modules when tools load their config.
6
+ */
7
+ export const recommendedOxfmtConfig = {
8
+ arrowParens: "always",
9
+ endOfLine: "lf",
10
+ printWidth: 100,
11
+ semi: true,
12
+ singleQuote: false,
13
+ sortImports: true,
14
+ sortPackageJson: true,
15
+ tabWidth: 2,
16
+ trailingComma: "all",
17
+ useTabs: false,
18
+ };
package/src/oxfmt.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { OxfmtConfig } from "oxfmt";
2
+
3
+ /**
4
+ * Canonical formatting defaults for standalone Oxfmt and Vite+ projects.
5
+ *
6
+ * Oxfmt does not support config inheritance. Spread this object into a
7
+ * standalone Oxfmt config or Vite+'s `fmt` block before local overrides.
8
+ */
9
+ export const recommendedOxfmtConfig = {
10
+ arrowParens: "always",
11
+ endOfLine: "lf",
12
+ printWidth: 100,
13
+ semi: true,
14
+ singleQuote: false,
15
+ sortImports: true,
16
+ sortPackageJson: true,
17
+ tabWidth: 2,
18
+ trailingComma: "all",
19
+ useTabs: false,
20
+ } satisfies OxfmtConfig;
21
+
22
+ export type RecommendedOxfmtConfig = typeof recommendedOxfmtConfig;
@@ -0,0 +1,17 @@
1
+ interface EffectOxlintRule {
2
+ readonly meta: {
3
+ readonly type: "problem";
4
+ readonly docs: { readonly description: string };
5
+ readonly messages: Readonly<Record<string, string>>;
6
+ };
7
+ readonly create: (context: {
8
+ report(descriptor: { node: unknown; messageId: string }): void;
9
+ }) => Readonly<Record<string, (node: unknown) => void>>;
10
+ }
11
+
12
+ declare const effectOxlintPlugin: {
13
+ readonly meta: { readonly name: "dev-kit-effect" };
14
+ readonly rules: Readonly<Record<string, EffectOxlintRule>>;
15
+ };
16
+
17
+ export default effectOxlintPlugin;
@@ -0,0 +1,166 @@
1
+ const noUntypedThrow = {
2
+ meta: {
3
+ type: "problem",
4
+ docs: { description: "Disallow throw statements from Effect-owned application code." },
5
+ messages: {
6
+ noUntypedThrow:
7
+ "Do not throw from Effect-owned code. Fail with a tagged error in the Effect error channel.",
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ ThrowStatement(node) {
13
+ context.report({ node, messageId: "noUntypedThrow" });
14
+ },
15
+ };
16
+ },
17
+ };
18
+
19
+ const noPromiseAtomMode = {
20
+ meta: {
21
+ type: "problem",
22
+ docs: { description: "Require Effect Atom AsyncResult mode for React mutations." },
23
+ messages: {
24
+ noPromiseAtomMode:
25
+ 'Do not use Effect Atom mode: "promise". Compose the workflow with Atom.fn and render its AsyncResult.',
26
+ },
27
+ },
28
+ create(context) {
29
+ return {
30
+ Property(node) {
31
+ const key = node.key;
32
+ const value = node.value;
33
+ const isMode =
34
+ (key.type === "Identifier" && key.name === "mode") ||
35
+ (key.type === "Literal" && key.value === "mode");
36
+ if (isMode && value.type === "Literal" && value.value === "promise") {
37
+ context.report({ node, messageId: "noPromiseAtomMode" });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ };
43
+
44
+ const noEffectRun = {
45
+ meta: {
46
+ type: "problem",
47
+ docs: { description: "Keep Effect runtime execution at explicit host entry points." },
48
+ messages: {
49
+ noEffectRun:
50
+ "Do not execute Effect with Effect.run* inside application code. Return or compose the Effect instead.",
51
+ },
52
+ },
53
+ create(context) {
54
+ return {
55
+ MemberExpression(node) {
56
+ if (
57
+ node.object.type === "Identifier" &&
58
+ node.object.name === "Effect" &&
59
+ !node.computed &&
60
+ node.property.type === "Identifier" &&
61
+ node.property.name.startsWith("run")
62
+ ) {
63
+ context.report({ node, messageId: "noEffectRun" });
64
+ }
65
+ },
66
+ };
67
+ },
68
+ };
69
+
70
+ const noUnsafePromise = {
71
+ meta: {
72
+ type: "problem",
73
+ docs: { description: "Require rejecting Promise boundaries to preserve typed failures." },
74
+ messages: {
75
+ noEffectPromise:
76
+ "Effect.promise turns rejection into a defect. Use Effect.tryPromise and map a tagged boundary error.",
77
+ noPromiseConstructor:
78
+ "Do not construct workflow Promises directly. Use Effect async/scheduling/concurrency operators.",
79
+ },
80
+ },
81
+ create(context) {
82
+ return {
83
+ MemberExpression(node) {
84
+ if (
85
+ node.object.type === "Identifier" &&
86
+ node.object.name === "Effect" &&
87
+ !node.computed &&
88
+ node.property.type === "Identifier" &&
89
+ node.property.name === "promise"
90
+ ) {
91
+ context.report({ node, messageId: "noEffectPromise" });
92
+ }
93
+ },
94
+ NewExpression(node) {
95
+ if (node.callee.type === "Identifier" && node.callee.name === "Promise") {
96
+ context.report({ node, messageId: "noPromiseConstructor" });
97
+ }
98
+ },
99
+ };
100
+ },
101
+ };
102
+
103
+ const noAsyncWorkflow = {
104
+ meta: {
105
+ type: "problem",
106
+ docs: { description: "Keep business workflows in Effect instead of async functions." },
107
+ messages: {
108
+ noAsyncWorkflow:
109
+ "Do not implement application workflows as async functions. Return an Effect and capture foreign Promises with Effect.tryPromise.",
110
+ },
111
+ },
112
+ create(context) {
113
+ const check = (node) => {
114
+ if (!node.async) return;
115
+ const parent = node.parent;
116
+ const isCapturedTryThunk =
117
+ parent?.type === "Property" &&
118
+ ((parent.key.type === "Identifier" && parent.key.name === "try") ||
119
+ (parent.key.type === "Literal" && parent.key.value === "try"));
120
+ if (!isCapturedTryThunk) context.report({ node, messageId: "noAsyncWorkflow" });
121
+ };
122
+ return {
123
+ ArrowFunctionExpression: check,
124
+ FunctionDeclaration: check,
125
+ FunctionExpression: check,
126
+ };
127
+ },
128
+ };
129
+
130
+ const noSyncBoundaryDecode = {
131
+ meta: {
132
+ type: "problem",
133
+ docs: { description: "Prevent external values from becoming synchronous schema defects." },
134
+ messages: {
135
+ noSyncBoundaryDecode:
136
+ "Do not synchronously decode route, persisted, or native input. Use Schema.decodeUnknownEffect/Option and handle the typed failure.",
137
+ },
138
+ },
139
+ create(context) {
140
+ return {
141
+ MemberExpression(node) {
142
+ if (
143
+ node.object.type === "Identifier" &&
144
+ node.object.name === "Schema" &&
145
+ !node.computed &&
146
+ node.property.type === "Identifier" &&
147
+ node.property.name === "decodeUnknownSync"
148
+ ) {
149
+ context.report({ node, messageId: "noSyncBoundaryDecode" });
150
+ }
151
+ },
152
+ };
153
+ },
154
+ };
155
+
156
+ export default {
157
+ meta: { name: "dev-kit-effect" },
158
+ rules: {
159
+ "no-async-workflow": noAsyncWorkflow,
160
+ "no-effect-run": noEffectRun,
161
+ "no-promise-atom-mode": noPromiseAtomMode,
162
+ "no-sync-boundary-decode": noSyncBoundaryDecode,
163
+ "no-untyped-throw": noUntypedThrow,
164
+ "no-unsafe-promise": noUnsafePromise,
165
+ },
166
+ };
package/src/oxlint.js CHANGED
@@ -8,6 +8,12 @@ export const recommendedOxlintConfig = {
8
8
  options: {
9
9
  typeAware: true,
10
10
  },
11
+ jsPlugins: [
12
+ {
13
+ name: "effect",
14
+ specifier: "@danieljvdm/dev-kit/oxlint-plugin-effect",
15
+ },
16
+ ],
11
17
  plugins: ["import", "react", "vitest"],
12
18
  rules: {
13
19
  eqeqeq: "error",
package/src/oxlint.ts CHANGED
@@ -1,15 +1,22 @@
1
- import type { OxlintConfig } from "vite-plus/lint";
1
+ import type { OxlintConfig } from "oxlint";
2
2
 
3
3
  /**
4
- * High-signal Oxlint defaults for TypeScript projects using Vite+.
4
+ * High-signal Oxlint defaults for TypeScript projects.
5
5
  *
6
- * Extend this object from `lint.extends` so project-local plugins, rules, and
7
- * overrides compose without losing nested configuration.
6
+ * Extend this object from standalone Oxlint's `extends`, or from Vite+'s
7
+ * `lint.extends`, so project-local plugins, rules, and overrides compose
8
+ * without losing nested configuration.
8
9
  */
9
10
  export const recommendedOxlintConfig = {
10
11
  options: {
11
12
  typeAware: true,
12
13
  },
14
+ jsPlugins: [
15
+ {
16
+ name: "effect",
17
+ specifier: "@danieljvdm/dev-kit/oxlint-plugin-effect",
18
+ },
19
+ ],
13
20
  plugins: ["import", "react", "vitest"],
14
21
  rules: {
15
22
  eqeqeq: "error",