@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 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`).
@@ -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
+ }