@deslop/coding-standards 0.1.871 → 0.1.885
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/dist/install.js +1 -1
- package/dist/oxlint.d.ts +34 -2
- package/dist/oxlint.js +58 -0
- package/package.json +1 -1
- package/skills/engineering/SKILL.md +48 -26
- package/skills/testing/SKILL.md +1 -1
package/dist/install.js
CHANGED
|
@@ -24685,7 +24685,7 @@ const runWith = (command, config) => {
|
|
|
24685
24685
|
};
|
|
24686
24686
|
//#endregion
|
|
24687
24687
|
//#region package.json
|
|
24688
|
-
var version = "0.1.
|
|
24688
|
+
var version = "0.1.885";
|
|
24689
24689
|
//#endregion
|
|
24690
24690
|
//#region src/install.ts
|
|
24691
24691
|
const cli = pipe(make("deslop-coding-standards", {}, fnUntraced(function* () {
|
package/dist/oxlint.d.ts
CHANGED
|
@@ -4020,8 +4020,10 @@ export declare const oxlint: {
|
|
|
4020
4020
|
'shadcn/no-restyle'?: undefined;
|
|
4021
4021
|
'shadcn/require-static-classes'?: undefined;
|
|
4022
4022
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4023
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4023
4024
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4024
4025
|
'react/rules-of-hooks'?: undefined;
|
|
4026
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4025
4027
|
'effecttsgo/async-function'?: undefined;
|
|
4026
4028
|
'typescript/require-await'?: undefined;
|
|
4027
4029
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4034,8 +4036,10 @@ export declare const oxlint: {
|
|
|
4034
4036
|
'shadcn/no-restyle': "off";
|
|
4035
4037
|
'shadcn/require-static-classes': "off";
|
|
4036
4038
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4039
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4037
4040
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4038
4041
|
'react/rules-of-hooks'?: undefined;
|
|
4042
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4039
4043
|
'effecttsgo/async-function'?: undefined;
|
|
4040
4044
|
'typescript/require-await'?: undefined;
|
|
4041
4045
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4048,8 +4052,10 @@ export declare const oxlint: {
|
|
|
4048
4052
|
'shadcn/no-restyle'?: undefined;
|
|
4049
4053
|
'shadcn/require-static-classes'?: undefined;
|
|
4050
4054
|
'@deslop/coding-standards/no-destructured-parameter': "off";
|
|
4055
|
+
'@deslop/coding-standards/no-native-control': "off";
|
|
4051
4056
|
'@deslop/coding-standards/no-renamed-import': "off";
|
|
4052
4057
|
'react/rules-of-hooks'?: undefined;
|
|
4058
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4053
4059
|
'effecttsgo/async-function'?: undefined;
|
|
4054
4060
|
'typescript/require-await'?: undefined;
|
|
4055
4061
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4062,8 +4068,10 @@ export declare const oxlint: {
|
|
|
4062
4068
|
'shadcn/no-restyle'?: undefined;
|
|
4063
4069
|
'shadcn/require-static-classes'?: undefined;
|
|
4064
4070
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4071
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4065
4072
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4066
4073
|
'react/rules-of-hooks': "off";
|
|
4074
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4067
4075
|
'effecttsgo/async-function'?: undefined;
|
|
4068
4076
|
'typescript/require-await'?: undefined;
|
|
4069
4077
|
'typescript/strict-void-return'?: undefined;
|
|
@@ -4076,8 +4084,26 @@ export declare const oxlint: {
|
|
|
4076
4084
|
'shadcn/no-restyle'?: undefined;
|
|
4077
4085
|
'shadcn/require-static-classes'?: undefined;
|
|
4078
4086
|
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4087
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4079
4088
|
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4080
4089
|
'react/rules-of-hooks'?: undefined;
|
|
4090
|
+
'@deslop/coding-standards/no-discarded-failure': "off";
|
|
4091
|
+
'effecttsgo/async-function'?: undefined;
|
|
4092
|
+
'typescript/require-await'?: undefined;
|
|
4093
|
+
'typescript/strict-void-return'?: undefined;
|
|
4094
|
+
'unicorn/no-null'?: undefined;
|
|
4095
|
+
};
|
|
4096
|
+
} | {
|
|
4097
|
+
files: string[];
|
|
4098
|
+
rules: {
|
|
4099
|
+
'import/no-default-export'?: undefined;
|
|
4100
|
+
'shadcn/no-restyle'?: undefined;
|
|
4101
|
+
'shadcn/require-static-classes'?: undefined;
|
|
4102
|
+
'@deslop/coding-standards/no-destructured-parameter'?: undefined;
|
|
4103
|
+
'@deslop/coding-standards/no-native-control'?: undefined;
|
|
4104
|
+
'@deslop/coding-standards/no-renamed-import'?: undefined;
|
|
4105
|
+
'react/rules-of-hooks'?: undefined;
|
|
4106
|
+
'@deslop/coding-standards/no-discarded-failure'?: undefined;
|
|
4081
4107
|
'effecttsgo/async-function': "off";
|
|
4082
4108
|
'typescript/require-await': "error";
|
|
4083
4109
|
'typescript/strict-void-return': "off";
|
|
@@ -4090,12 +4116,14 @@ export declare const oxlint: {
|
|
|
4090
4116
|
'@deslop/coding-standards/no-constant-function': "error";
|
|
4091
4117
|
'@deslop/coding-standards/no-deep-pipe': "error";
|
|
4092
4118
|
'@deslop/coding-standards/no-destructured-parameter': "error";
|
|
4119
|
+
'@deslop/coding-standards/no-discarded-failure': "error";
|
|
4093
4120
|
'@deslop/coding-standards/no-double-nullish-check': "error";
|
|
4094
4121
|
'@deslop/coding-standards/no-effect-property-arrow': "error";
|
|
4095
4122
|
'@deslop/coding-standards/no-error-message-assertion': "error";
|
|
4096
4123
|
'@deslop/coding-standards/no-fake-ref-state': "error";
|
|
4097
4124
|
'@deslop/coding-standards/no-hand-written-guard': "error";
|
|
4098
4125
|
'@deslop/coding-standards/no-let': "error";
|
|
4126
|
+
'@deslop/coding-standards/no-native-control': "error";
|
|
4099
4127
|
'@deslop/coding-standards/no-native-emptiness-check': "error";
|
|
4100
4128
|
'@deslop/coding-standards/no-native-method-call': "error";
|
|
4101
4129
|
'@deslop/coding-standards/no-nullish-ternary': "error";
|
|
@@ -4308,11 +4336,15 @@ export declare const oxlint: {
|
|
|
4308
4336
|
}];
|
|
4309
4337
|
'import/no-relative-parent-imports': "error";
|
|
4310
4338
|
'no-restricted-imports': ["error", {
|
|
4311
|
-
paths: {
|
|
4339
|
+
paths: ({
|
|
4312
4340
|
importNames: string[];
|
|
4313
4341
|
message: string;
|
|
4314
4342
|
name: string;
|
|
4315
|
-
}
|
|
4343
|
+
} | {
|
|
4344
|
+
importNames?: undefined;
|
|
4345
|
+
message: string;
|
|
4346
|
+
name: string;
|
|
4347
|
+
})[];
|
|
4316
4348
|
patterns: {
|
|
4317
4349
|
message: string;
|
|
4318
4350
|
regex: string;
|
package/dist/oxlint.js
CHANGED
|
@@ -1021,6 +1021,27 @@ const noDestructuredParameter = defineRule({
|
|
|
1021
1021
|
meta: { type: "problem" }
|
|
1022
1022
|
});
|
|
1023
1023
|
//#endregion
|
|
1024
|
+
//#region src/rules/no-discarded-failure.ts
|
|
1025
|
+
const noDiscardedFailure = defineRule({
|
|
1026
|
+
create: (context) => ({ MemberExpression: (node) => {
|
|
1027
|
+
if (!importedMember({
|
|
1028
|
+
context,
|
|
1029
|
+
importedName: "Effect",
|
|
1030
|
+
node
|
|
1031
|
+
})) return;
|
|
1032
|
+
if (!exists(memberName(node), (name) => contains$1([
|
|
1033
|
+
"ignore",
|
|
1034
|
+
"orDie",
|
|
1035
|
+
"orDieWith"
|
|
1036
|
+
], name))) return;
|
|
1037
|
+
context.report({
|
|
1038
|
+
message: "Map the failure to the service's error; an expected failure is never ignored or turned into a defect.",
|
|
1039
|
+
node
|
|
1040
|
+
});
|
|
1041
|
+
} }),
|
|
1042
|
+
meta: { type: "problem" }
|
|
1043
|
+
});
|
|
1044
|
+
//#endregion
|
|
1024
1045
|
//#region src/rules/no-double-nullish-check.ts
|
|
1025
1046
|
function nullishOperand(node) {
|
|
1026
1047
|
if (node.type === "Literal" && node.value === null) return some("null");
|
|
@@ -1284,6 +1305,30 @@ const noLet = defineRule({
|
|
|
1284
1305
|
meta: { type: "problem" }
|
|
1285
1306
|
});
|
|
1286
1307
|
//#endregion
|
|
1308
|
+
//#region src/rules/no-native-control.ts
|
|
1309
|
+
const primitives = {
|
|
1310
|
+
button: "Button",
|
|
1311
|
+
input: "Input",
|
|
1312
|
+
select: "Select",
|
|
1313
|
+
textarea: "Textarea"
|
|
1314
|
+
};
|
|
1315
|
+
const controlStyle = /(?:^|:)(?:hover|focus|focus-visible|active):|(?:^|:)(?:rounded|shadow|ring)(?:-|$)|(?:^|:)p[blrtxy]?-(?!0$)|(?:^|:)bg-(?!transparent$)|(?:^|:)border(?:-[blrtxy])?$|(?:^|:)border-(?!0$|[blrtxy]-0$)/u;
|
|
1316
|
+
const noNativeControl = defineRule({
|
|
1317
|
+
create: (context) => ({ JSXOpeningElement: (node) => {
|
|
1318
|
+
if (node.name.type !== "JSXIdentifier") return;
|
|
1319
|
+
const name = node.name.name;
|
|
1320
|
+
map(get$2(primitives, name), (primitive) => {
|
|
1321
|
+
const styled = pipe(node.attributes, findFirst((attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "className"), flatMap((attribute) => attribute.type === "JSXAttribute" && attribute.value?.type === "Literal" ? fromNullishOr(attribute.value.value) : none()), exists((value) => some$1(split(value, /\s+/u), matches(controlStyle))));
|
|
1322
|
+
if (name !== "select" && !styled) return;
|
|
1323
|
+
context.report({
|
|
1324
|
+
message: `Use the ${primitive} UI primitive instead of a styled native control.`,
|
|
1325
|
+
node
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
} }),
|
|
1329
|
+
meta: { type: "problem" }
|
|
1330
|
+
});
|
|
1331
|
+
//#endregion
|
|
1287
1332
|
//#region src/rules/no-native-emptiness-check.ts
|
|
1288
1333
|
function emptinessPredicate(node) {
|
|
1289
1334
|
const comparesLength = node.left.type === "MemberExpression" && contains(memberName(node.left), "length") && node.right.type === "Literal" && node.right.value === 0;
|
|
@@ -2477,6 +2522,7 @@ const oxlint = defineConfig({
|
|
|
2477
2522
|
files: ["**/components/ui/**", "**/packages/ui/**"],
|
|
2478
2523
|
rules: {
|
|
2479
2524
|
"@deslop/coding-standards/no-destructured-parameter": "off",
|
|
2525
|
+
"@deslop/coding-standards/no-native-control": "off",
|
|
2480
2526
|
"@deslop/coding-standards/no-renamed-import": "off"
|
|
2481
2527
|
}
|
|
2482
2528
|
},
|
|
@@ -2484,6 +2530,10 @@ const oxlint = defineConfig({
|
|
|
2484
2530
|
files: ["**/*.ts"],
|
|
2485
2531
|
rules: { "react/rules-of-hooks": "off" }
|
|
2486
2532
|
},
|
|
2533
|
+
{
|
|
2534
|
+
files: ["**/*.test.ts", "**/*.test.tsx"],
|
|
2535
|
+
rules: { "@deslop/coding-standards/no-discarded-failure": "off" }
|
|
2536
|
+
},
|
|
2487
2537
|
{
|
|
2488
2538
|
files: ["**/*.tsx"],
|
|
2489
2539
|
rules: {
|
|
@@ -2508,12 +2558,14 @@ const oxlint = defineConfig({
|
|
|
2508
2558
|
"@deslop/coding-standards/no-constant-function": "error",
|
|
2509
2559
|
"@deslop/coding-standards/no-deep-pipe": "error",
|
|
2510
2560
|
"@deslop/coding-standards/no-destructured-parameter": "error",
|
|
2561
|
+
"@deslop/coding-standards/no-discarded-failure": "error",
|
|
2511
2562
|
"@deslop/coding-standards/no-double-nullish-check": "error",
|
|
2512
2563
|
"@deslop/coding-standards/no-effect-property-arrow": "error",
|
|
2513
2564
|
"@deslop/coding-standards/no-error-message-assertion": "error",
|
|
2514
2565
|
"@deslop/coding-standards/no-fake-ref-state": "error",
|
|
2515
2566
|
"@deslop/coding-standards/no-hand-written-guard": "error",
|
|
2516
2567
|
"@deslop/coding-standards/no-let": "error",
|
|
2568
|
+
"@deslop/coding-standards/no-native-control": "error",
|
|
2517
2569
|
"@deslop/coding-standards/no-native-emptiness-check": "error",
|
|
2518
2570
|
"@deslop/coding-standards/no-native-method-call": "error",
|
|
2519
2571
|
"@deslop/coding-standards/no-nullish-ternary": "error",
|
|
@@ -2730,6 +2782,10 @@ const oxlint = defineConfig({
|
|
|
2730
2782
|
message: reactLegacyMessage,
|
|
2731
2783
|
name: "react"
|
|
2732
2784
|
},
|
|
2785
|
+
{
|
|
2786
|
+
message: "Read and write data through Effect Atom.",
|
|
2787
|
+
name: "@tanstack/react-query"
|
|
2788
|
+
},
|
|
2733
2789
|
{
|
|
2734
2790
|
importNames: ["vi"],
|
|
2735
2791
|
message: "A Layer is the seam; never use vi.",
|
|
@@ -3206,12 +3262,14 @@ var oxlint_default = definePlugin({
|
|
|
3206
3262
|
"no-constant-function": noConstantFunction,
|
|
3207
3263
|
"no-deep-pipe": noDeepPipe,
|
|
3208
3264
|
"no-destructured-parameter": noDestructuredParameter,
|
|
3265
|
+
"no-discarded-failure": noDiscardedFailure,
|
|
3209
3266
|
"no-double-nullish-check": noDoubleNullishCheck,
|
|
3210
3267
|
"no-effect-property-arrow": noEffectPropertyArrow,
|
|
3211
3268
|
"no-error-message-assertion": noErrorMessageAssertion,
|
|
3212
3269
|
"no-fake-ref-state": noFakeRefState,
|
|
3213
3270
|
"no-hand-written-guard": noHandWrittenGuard,
|
|
3214
3271
|
"no-let": noLet,
|
|
3272
|
+
"no-native-control": noNativeControl,
|
|
3215
3273
|
"no-native-emptiness-check": noNativeEmptinessCheck,
|
|
3216
3274
|
"no-native-method-call": noNativeMethodCall,
|
|
3217
3275
|
"no-nullish-ternary": noNullishTernary,
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ name: engineering
|
|
|
3
3
|
description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product/test code, including prototypes.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
**Scope.** Apply these rules and repository-specific CODING_STANDARDS.md.
|
|
6
|
+
**Scope.** Apply these rules and repository-specific CODING_STANDARDS.md. Write each line right the first time: the examples exist so lint and review find nothing, and lint is only the fallback.
|
|
7
7
|
|
|
8
8
|
**Dependency APIs.** Match the manifest and lockfile versions. Before adding an API wrapper or workaround, read the upstream source per workflow's research procedure. Library APIs do not define coding policy.
|
|
9
9
|
|
|
@@ -16,16 +16,16 @@ description: 'TypeScript and Effect code conventions paired with shared lint. Us
|
|
|
16
16
|
**Experiments.** Write with these idioms; reserve the full refactor/cleanup pass for retained delivery. Keep disposable source isolated and shared rules intact.
|
|
17
17
|
|
|
18
18
|
- **Reuse first.** Check existing code, installed modules and platform capabilities before adding code. Implement only the missing behavior, without dropping required behavior, safety or accessibility. Workflow owns the scope. A refactor or cleanup request applies every rule to every line of the owned files.
|
|
19
|
-
- **Inward layers.** Domain and service code never import HTTP, RPC, or other transport types.
|
|
19
|
+
- **Inward layers.** Domain and service code never import HTTP, RPC, or other transport types, and domain schemas carry no transport annotation such as an HTTP status.
|
|
20
20
|
- **Building blocks.** For a consequential new interface, start with a realistic caller operation and its ordering, failure and lifetime obligations; hide useful complexity, not forwarded names. Offer services, Layers and functions the caller composes like any Effect module, never a bundle such as `serve(app)`. No mandatory architecture document or alternatives round.
|
|
21
21
|
- **No machinery.** Add hooks, guards or generators only for a required contract or a recurring defect.
|
|
22
|
-
- **Behavior kept.** A refactor keeps observable behavior at every consumer. Report
|
|
22
|
+
- **Behavior kept.** A refactor keeps observable behavior at every consumer. Report accepted differences only when they exist: a change no consumer observes, or one a rule here causes, such as an error keeping its cause. Printed diagnostics are not a contract. Any other behavior change follows workflow's scope.
|
|
23
23
|
- **Reachable bugs.** Fix a bug that real input reaches when it lies inside workflow's scope, and check every consumer of the changed output. For a hard bug, use one reproduction and one prediction per probe. Remove edits based on disproven hypotheses, then rerun the original scenario. Delete handling only for input the contract excludes; keep and report code that looks intentional.
|
|
24
|
-
- **Performance.** Optimize only when the request or a required contract needs it
|
|
24
|
+
- **Performance.** Optimize only when the request or a required contract needs it. Measure one real action on realistic input before and after the change, at the layer that owns the symptom: spans for backend calls, React commits for rerenders, a browser profile for main-thread work. Report run count and variation; a difference within noise is unproved.
|
|
25
25
|
- **Type safety.** A fix never weakens types to make a symptom go away: no widened or erased type, cast, dropped generic, or loosened exported type.
|
|
26
26
|
- **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
|
|
27
27
|
|
|
28
|
-
**Happy path.** Failures flow through the error channel: no catch, retry, fallback, backup, or defensive check unless the request or an existing contract needs it.
|
|
28
|
+
**Happy path.** Failures flow through the error channel: no catch, retry, fallback, backup, or defensive check unless the request or an existing contract needs it. Map an expected failure to the service's error; never `Effect.ignore` it or turn it into a defect with `Effect.orDie`.
|
|
29
29
|
|
|
30
30
|
**Trusted data.** Validate and transform once at the boundary with Effect Schema. Carry narrowed values forward. Never re-check what the schema, the declared type, an earlier filter or every caller guarantees.
|
|
31
31
|
|
|
@@ -72,7 +72,7 @@ _input => onSubmit(_input.value) // or input2: an outer input is shadowed
|
|
|
72
72
|
|
|
73
73
|
**Nearest sibling.** Extend the nearest implementation; match permissions, errors, refresh and tests. Reuse its helpers and change only affected state.
|
|
74
74
|
|
|
75
|
-
**Once.** Extract repeated logic once; one fact has one owner.
|
|
75
|
+
**Once.** Extract repeated logic once; one fact has one owner. A domain enum, such as a status, has one schema that every other schema reuses, and a cache key or atom family key carries the full identity of what it caches.
|
|
76
76
|
|
|
77
77
|
```ts
|
|
78
78
|
// good
|
|
@@ -96,7 +96,7 @@ pipe(
|
|
|
96
96
|
|
|
97
97
|
**Present needs.** No speculative option, abstraction, layer, export, file or check. Configure only what callers vary.
|
|
98
98
|
|
|
99
|
-
**Dead code.** Every superseded or unused file, export, type, branch, config, validation, doc, test, and dependency goes in the same change.
|
|
99
|
+
**Dead code.** Every superseded or unused file, export, type, branch, config, validation, doc, test, and dependency goes in the same change. Docs, guides and generated bundles that describe changed behavior change in the same commit.
|
|
100
100
|
|
|
101
101
|
## Lint pairs
|
|
102
102
|
|
|
@@ -149,22 +149,7 @@ Array.appendAll(
|
|
|
149
149
|
)
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
**Choices.** `Match` for a union, never `switch`.
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
// good
|
|
156
|
-
pipe(
|
|
157
|
-
Match.value(usage.kind),
|
|
158
|
-
Match.when('input', () => 'in'),
|
|
159
|
-
Match.when('output', () => 'out'),
|
|
160
|
-
Match.exhaustive
|
|
161
|
-
)
|
|
162
|
-
// bad
|
|
163
|
-
switch (usage.kind) {
|
|
164
|
-
case 'input':
|
|
165
|
-
return 'in'
|
|
166
|
-
}
|
|
167
|
-
```
|
|
152
|
+
**Choices.** `Match` with `Match.exhaustive` for a union, never `switch`.
|
|
168
153
|
|
|
169
154
|
**Options.** Use Option operators, never .value. Use Option where it composes; keep plain optional values otherwise. This is our convention, not a claim narrowing is unsafe.
|
|
170
155
|
|
|
@@ -187,7 +172,9 @@ const parsed = Option.getOrUndefined(Number.parse(event.target.value))
|
|
|
187
172
|
// good
|
|
188
173
|
const total = Array.reduce(values, 0, Number.sum)
|
|
189
174
|
pipe(teamId, String.split(','), Array.map(String.trim), Array.findFirst(String.isNonEmpty))
|
|
190
|
-
Array.filterMap(items, item =>
|
|
175
|
+
Array.filterMap(items, item =>
|
|
176
|
+
Boolean.match(supports(item), {onFalse: () => Result.failVoid, onTrue: () => Result.succeed(item.value)})
|
|
177
|
+
)
|
|
191
178
|
// bad
|
|
192
179
|
let total = 0
|
|
193
180
|
for (const value of values) total += value
|
|
@@ -399,7 +386,7 @@ const traced = Effect.withSpan('Notes.create')(notes.create(input))
|
|
|
399
386
|
Effect.addFinalizer(() => closeSocket(socket)) // acquisition and release with different owners
|
|
400
387
|
```
|
|
401
388
|
|
|
402
|
-
**Runtime.** The owned runtime runs the program; a Promise an external contract demands is bridged once, where it is returned.
|
|
389
|
+
**Runtime.** The owned runtime runs the program; a Promise an external contract demands is bridged once, where it is returned. React code is no such contract: atoms run its Effects.
|
|
403
390
|
|
|
404
391
|
```ts
|
|
405
392
|
// good
|
|
@@ -559,7 +546,7 @@ const warned = ['sort-keys', 'typescript/no-restricted-types'] // a package rule
|
|
|
559
546
|
|
|
560
547
|
## React
|
|
561
548
|
|
|
562
|
-
**Components.** React Compiler memoizes. Destructure state at its declaration, receive component refs through props, and keep
|
|
549
|
+
**Components.** React Compiler memoizes. Destructure state at its declaration, receive component refs through props, and keep only a component's own interaction state in React.
|
|
563
550
|
|
|
564
551
|
```tsx
|
|
565
552
|
// good
|
|
@@ -571,6 +558,41 @@ const handle = useCallback(() => setOpen(true), [])
|
|
|
571
558
|
const state = useState(false)
|
|
572
559
|
```
|
|
573
560
|
|
|
561
|
+
**Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves that code to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)`, and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them, so never pass them to `Atom.fn`.
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
// good
|
|
565
|
+
const appsRuntime = Atom.runtime(DualApps.layer)
|
|
566
|
+
const appAtom = Atom.family((id: AppId) => pipe(appsRuntime.atom(loadApp(id)), Atom.withReactivity(['apps'])))
|
|
567
|
+
const saveAppAtom = appsRuntime.fn((draft: AppDraft) => saveApp(draft), {reactivityKeys: ['apps']})
|
|
568
|
+
const app = useAtomValue(appAtom(appId))
|
|
569
|
+
const [saveResult, save] = useAtom(saveAppAtom)
|
|
570
|
+
// bad
|
|
571
|
+
useQuery({queryFn: () => Effect.runPromiseWith(Context.empty())(loadApp(appId))})
|
|
572
|
+
onClick={() => Effect.runPromise(saveApp(draft))}
|
|
573
|
+
useAtomValue(Atom.make(loadApp(appId))) // a new atom on every render
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Outcomes.** Read data with `useAtomSuspense`. The route or section that owns a screen wraps it in one `Suspense` boundary for loading and one error boundary that shows the failure with a retry. Components never branch on loading or failure, and never turn a failure into an empty value. Take a write's pending and failure state from its atom's result, never from copied React state.
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
// good
|
|
580
|
+
function AppFrame(props: {appId: AppId}) {
|
|
581
|
+
return <Frame app={useAtomSuspense(appAtom(props.appId)).value} />
|
|
582
|
+
}
|
|
583
|
+
<ErrorBoundary fallback={<LoadError onRetry={refresh} />}>
|
|
584
|
+
<Suspense fallback={<Spinner />}>
|
|
585
|
+
<AppFrame appId={appId} />
|
|
586
|
+
</Suspense>
|
|
587
|
+
</ErrorBoundary>
|
|
588
|
+
<Button disabled={saveResult.waiting} onClick={() => save(draft)}>Save</Button>
|
|
589
|
+
// bad
|
|
590
|
+
app.data ? <AppFrame app={app.data} /> : <Spinner /> // a failed load spins forever
|
|
591
|
+
const members = query.data ?? [] // a failed load looks like no members
|
|
592
|
+
const [pending, setPending] = useState(false) // copies the write atom's waiting state
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
- **Ownership.** Give a component that owns its own atoms, writes or lifetime its own file; keep small presentational helpers beside their one user. Reset an editable draft through a `key` on its identity, never an Effect that copies props.
|
|
574
596
|
- **Pure render.** Derive display values from props and subscribed state during render; never mutate them or perform an RPC, write or subscription there. Event handlers own user-triggered actions; Effects synchronize external systems, not copied state or calculations. Reuse the existing router/atom integration rather than fetching in a new Effect.
|
|
575
597
|
- **State identity.** Keep one owner for each value, stable domain keys for list items and deliberate reset boundaries. For an observed rerender problem, derive the needed value in the existing atom graph instead of subscribing to frequently changing unrelated state. Compiler memoization is a performance optimization, never a correctness or lifetime guarantee. Do not add manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
|
|
576
598
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -20,7 +20,7 @@ description: 'Select and run black-box behavior proof for Effect apps/tools. Use
|
|
|
20
20
|
```ts
|
|
21
21
|
// good — "test files only for the services/packages public interfaces"
|
|
22
22
|
it.layer(Layer.provideMerge(Ledger.layer, NodeServices.layer))(test => {
|
|
23
|
-
test.effect('keeps the previous entries when the file is malformed', () =>
|
|
23
|
+
test.effect('keeps the previous entries when the file is malformed', () => program)
|
|
24
24
|
})
|
|
25
25
|
assert.deepStrictEqual(customCodes(result.stdout), ['@deslop/coding-standards(no-typeof)'])
|
|
26
26
|
assert.containsSubset(error, {_tag: 'SandboxUnavailable', cause: killed})
|