@danieljvdm/dev-kit 0.3.3 → 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 +40 -4
- package/package.json +19 -1
- package/skills/dev-kit/SKILL.md +22 -9
- package/src/oxfmt.js +18 -0
- package/src/oxfmt.ts +22 -0
- package/src/oxlint-plugin-effect.d.ts +17 -0
- package/src/oxlint-plugin-effect.js +166 -0
- package/src/oxlint.js +6 -0
- package/src/oxlint.ts +11 -4
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
|
|
312
|
+
## Oxlint and Oxfmt configurations
|
|
313
313
|
|
|
314
|
-
Dev Kit exports
|
|
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
|
+
"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
|
}
|
package/skills/dev-kit/SKILL.md
CHANGED
|
@@ -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
|
|
159
|
+
## Oxlint and Oxfmt configurations
|
|
160
160
|
|
|
161
|
-
|
|
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.
|
|
180
|
-
|
|
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
|
|
187
|
-
not
|
|
188
|
-
|
|
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.
|
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 "
|
|
1
|
+
import type { OxlintConfig } from "oxlint";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* High-signal Oxlint defaults for TypeScript projects
|
|
4
|
+
* High-signal Oxlint defaults for TypeScript projects.
|
|
5
5
|
*
|
|
6
|
-
* Extend this object from `
|
|
7
|
-
*
|
|
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",
|