@dudousxd/nestjs-durable-eslint-plugin 0.2.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 +46 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/no-nondeterminism.d.ts +7 -0
- package/dist/no-nondeterminism.d.ts.map +1 -0
- package/dist/no-nondeterminism.js +92 -0
- package/dist/no-nondeterminism.js.map +1 -0
- package/grit/no-nondeterminism.grit +18 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @dudousxd/nestjs-durable-eslint-plugin
|
|
2
|
+
|
|
3
|
+
Lint for non-deterministic sources inside a durable `@Workflow` — `Date.now()`, `Math.random()`,
|
|
4
|
+
`new Date()`, `crypto.randomUUID()`, `performance.now()`. These differ on every replay and silently
|
|
5
|
+
corrupt a durable run; use the checkpointed `ctx.now()` / `ctx.random()` / `ctx.uuid()` instead.
|
|
6
|
+
|
|
7
|
+
Ships the rule for **both** ecosystems.
|
|
8
|
+
|
|
9
|
+
## ESLint (flat config) — recommended
|
|
10
|
+
|
|
11
|
+
AST-aware: only flags the banned calls inside the `run` method of a `@Workflow`-decorated class.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// eslint.config.js
|
|
15
|
+
import durable from '@dudousxd/nestjs-durable-eslint-plugin';
|
|
16
|
+
|
|
17
|
+
export default [
|
|
18
|
+
{
|
|
19
|
+
files: ['**/*.ts'],
|
|
20
|
+
plugins: { '@dudousxd/nestjs-durable': durable },
|
|
21
|
+
rules: { '@dudousxd/nestjs-durable/no-nondeterminism': 'error' },
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
// or: export default [durable.configs.recommended]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Biome (>= 2.0) — GritQL plugin
|
|
28
|
+
|
|
29
|
+
Biome plugins can't yet scope by decorator/method, so target the plugin at your workflow files via
|
|
30
|
+
`overrides`:
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
// biome.json
|
|
34
|
+
{
|
|
35
|
+
"overrides": [
|
|
36
|
+
{
|
|
37
|
+
"include": ["**/*.workflow.ts"],
|
|
38
|
+
"plugins": ["./node_modules/@dudousxd/nestjs-durable-eslint-plugin/grit/no-nondeterminism.grit"]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> The deterministic escape hatches `ctx.now()` / `ctx.random()` / `ctx.uuid()` already exist in
|
|
45
|
+
> `@dudousxd/nestjs-durable-core`; this rule pushes you to them at author time instead of finding the
|
|
46
|
+
> drift at replay (`NonDeterminismError`).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const rules: {
|
|
2
|
+
'no-nondeterminism': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"useNow" | "useRandom" | "useUuid" | "useNowDate", [], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener> & {
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
};
|
|
6
|
+
declare const plugin: {
|
|
7
|
+
meta: {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
rules: {
|
|
12
|
+
'no-nondeterminism': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"useNow" | "useRandom" | "useUuid" | "useNowDate", [], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
configs: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
export declare const configs: Record<string, unknown>;
|
|
19
|
+
export default plugin;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,KAAK;;;;CAEjB,CAAC;AAEF,QAAA,MAAM,MAAM;;;;;;;;;;aAGK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CACvC,CAAC;AASF,eAAO,MAAM,OAAO,yBAAiB,CAAC;AACtC,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configs = exports.rules = void 0;
|
|
4
|
+
const no_nondeterminism_1 = require("./no-nondeterminism");
|
|
5
|
+
exports.rules = {
|
|
6
|
+
'no-nondeterminism': no_nondeterminism_1.noNondeterminism,
|
|
7
|
+
};
|
|
8
|
+
const plugin = {
|
|
9
|
+
meta: { name: '@dudousxd/nestjs-durable-eslint-plugin', version: '0.1.0' },
|
|
10
|
+
rules: exports.rules,
|
|
11
|
+
configs: {},
|
|
12
|
+
};
|
|
13
|
+
// Flat-config preset: `extends` it (or spread) to turn the rule on. Defined after `plugin` so it can
|
|
14
|
+
// reference the plugin object itself (the flat-config way to register a plugin + its rules).
|
|
15
|
+
plugin.configs.recommended = {
|
|
16
|
+
plugins: { '@dudousxd/nestjs-durable': plugin },
|
|
17
|
+
rules: { '@dudousxd/nestjs-durable/no-nondeterminism': 'error' },
|
|
18
|
+
};
|
|
19
|
+
exports.configs = plugin.configs;
|
|
20
|
+
exports.default = plugin;
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2DAAuD;AAE1C,QAAA,KAAK,GAAG;IACnB,mBAAmB,EAAE,oCAAgB;CACtC,CAAC;AAEF,MAAM,MAAM,GAAG;IACb,IAAI,EAAE,EAAE,IAAI,EAAE,wCAAwC,EAAE,OAAO,EAAE,OAAO,EAAE;IAC1E,KAAK,EAAL,aAAK;IACL,OAAO,EAAE,EAA6B;CACvC,CAAC;AAEF,qGAAqG;AACrG,6FAA6F;AAC7F,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG;IAC3B,OAAO,EAAE,EAAE,0BAA0B,EAAE,MAAM,EAAE;IAC/C,KAAK,EAAE,EAAE,4CAA4C,EAAE,OAAO,EAAE;CACjE,CAAC;AAEW,QAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;AACtC,kBAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
type MessageId = 'useNow' | 'useRandom' | 'useUuid' | 'useNowDate';
|
|
3
|
+
export declare const noNondeterminism: ESLintUtils.RuleModule<MessageId, [], unknown, ESLintUtils.RuleListener> & {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=no-nondeterminism.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-nondeterminism.d.ts","sourceRoot":"","sources":["../src/no-nondeterminism.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAMtE,KAAK,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,YAAY,CAAC;AA4CnE,eAAO,MAAM,gBAAgB;;CA+C3B,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noNondeterminism = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/DavideCarvalho/nestjs-durable#${name}`);
|
|
6
|
+
/** True when `node` sits lexically inside the `run` method of a class decorated with `@Workflow`. */
|
|
7
|
+
function isInWorkflowRun(node) {
|
|
8
|
+
let cur = node;
|
|
9
|
+
let runMethod;
|
|
10
|
+
while (cur) {
|
|
11
|
+
if (cur.type === 'MethodDefinition' &&
|
|
12
|
+
cur.key.type === 'Identifier' &&
|
|
13
|
+
cur.key.name === 'run') {
|
|
14
|
+
runMethod = cur;
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
cur = cur.parent;
|
|
18
|
+
}
|
|
19
|
+
if (!runMethod)
|
|
20
|
+
return false;
|
|
21
|
+
const classNode = runMethod.parent?.parent; // MethodDefinition → ClassBody → Class
|
|
22
|
+
if (!classNode ||
|
|
23
|
+
(classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return (classNode.decorators ?? []).some((d) => {
|
|
27
|
+
const e = d.expression;
|
|
28
|
+
if (e.type === 'Identifier')
|
|
29
|
+
return e.name === 'Workflow';
|
|
30
|
+
if (e.type === 'CallExpression' && e.callee.type === 'Identifier') {
|
|
31
|
+
return e.callee.name === 'Workflow';
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** The receiver name of a member call: `crypto` for `crypto.x()` and `globalThis.crypto.x()`. */
|
|
37
|
+
function receiverName(object) {
|
|
38
|
+
if (object.type === 'Identifier')
|
|
39
|
+
return object.name;
|
|
40
|
+
if (object.type === 'MemberExpression' && object.property.type === 'Identifier') {
|
|
41
|
+
return object.property.name;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
exports.noNondeterminism = createRule({
|
|
46
|
+
name: 'no-nondeterminism',
|
|
47
|
+
meta: {
|
|
48
|
+
type: 'problem',
|
|
49
|
+
docs: {
|
|
50
|
+
description: 'Disallow non-deterministic sources (Date.now, Math.random, new Date, crypto.randomUUID) inside a @Workflow run — they differ across replays and silently corrupt a durable run. Use ctx.now()/ctx.random()/ctx.uuid().',
|
|
51
|
+
},
|
|
52
|
+
messages: {
|
|
53
|
+
useNow: 'Non-deterministic `{{call}}` inside a @Workflow run — use `ctx.now()` (recorded once, then replayed).',
|
|
54
|
+
useRandom: 'Non-deterministic `Math.random()` inside a @Workflow run — use `ctx.random()`.',
|
|
55
|
+
useUuid: 'Non-deterministic `crypto.randomUUID()` inside a @Workflow run — use `ctx.uuid()`.',
|
|
56
|
+
useNowDate: 'Non-deterministic `new Date()` inside a @Workflow run — use `new Date(await ctx.now())`.',
|
|
57
|
+
},
|
|
58
|
+
schema: [],
|
|
59
|
+
},
|
|
60
|
+
defaultOptions: [],
|
|
61
|
+
create(context) {
|
|
62
|
+
return {
|
|
63
|
+
CallExpression(node) {
|
|
64
|
+
const callee = node.callee;
|
|
65
|
+
if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier')
|
|
66
|
+
return;
|
|
67
|
+
const prop = callee.property.name;
|
|
68
|
+
const obj = receiverName(callee.object);
|
|
69
|
+
const isBanned = ((obj === 'Date' || obj === 'performance') && prop === 'now') ||
|
|
70
|
+
(obj === 'Math' && prop === 'random') ||
|
|
71
|
+
(obj === 'crypto' && prop === 'randomUUID');
|
|
72
|
+
if (!isBanned || !isInWorkflowRun(node))
|
|
73
|
+
return;
|
|
74
|
+
if (prop === 'random')
|
|
75
|
+
context.report({ node, messageId: 'useRandom' });
|
|
76
|
+
else if (prop === 'randomUUID')
|
|
77
|
+
context.report({ node, messageId: 'useUuid' });
|
|
78
|
+
else
|
|
79
|
+
context.report({ node, messageId: 'useNow', data: { call: `${obj}.now()` } });
|
|
80
|
+
},
|
|
81
|
+
NewExpression(node) {
|
|
82
|
+
if (node.callee.type === 'Identifier' &&
|
|
83
|
+
node.callee.name === 'Date' &&
|
|
84
|
+
node.arguments.length === 0 &&
|
|
85
|
+
isInWorkflowRun(node)) {
|
|
86
|
+
context.report({ node, messageId: 'useNowDate' });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=no-nondeterminism.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-nondeterminism.js","sourceRoot":"","sources":["../src/no-nondeterminism.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Biome (>= 2.0) GritQL plugin: flag non-deterministic sources that corrupt a durable replay.
|
|
2
|
+
// Biome plugins can't yet scope by decorator/method, so target it at your workflow files via
|
|
3
|
+
// biome.json `overrides` (e.g. include "**/*.workflow.ts") and use ctx.now()/ctx.random()/ctx.uuid().
|
|
4
|
+
language js
|
|
5
|
+
|
|
6
|
+
or {
|
|
7
|
+
`Date.now()`,
|
|
8
|
+
`performance.now()`,
|
|
9
|
+
`Math.random()`,
|
|
10
|
+
`crypto.randomUUID()`,
|
|
11
|
+
`globalThis.crypto.randomUUID()`,
|
|
12
|
+
`new Date()`
|
|
13
|
+
} as $bad where {
|
|
14
|
+
register_diagnostic(
|
|
15
|
+
span = $bad,
|
|
16
|
+
message = "Non-deterministic source inside a durable workflow — it differs across replays and corrupts the run. Use ctx.now() / ctx.random() / ctx.uuid()."
|
|
17
|
+
)
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dudousxd/nestjs-durable-eslint-plugin",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ESLint rule: flag non-deterministic calls (Date.now, Math.random, …) inside a @Workflow run",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Davide Carvalho",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/DavideCarvalho/nestjs-durable.git",
|
|
10
|
+
"directory": "packages/eslint-plugin"
|
|
11
|
+
},
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"grit"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"eslint": ">=8.0.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@typescript-eslint/utils": "^8.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
27
|
+
"@typescript-eslint/utils": "^8.0.0",
|
|
28
|
+
"eslint": "^9.0.0",
|
|
29
|
+
"typescript": "5.9.3"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
34
|
+
}
|
|
35
|
+
}
|