@agjs/tsforge 0.1.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/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { formatTables, highlightCode, styleInline } from "./markdown";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Incremental markdown renderer for the live-streamed answer (token channel
|
|
5
|
+
* `content`). Append-only — no cursor tricks, so scrollback is never corrupted:
|
|
6
|
+
* prose flushes as each line completes; fenced ```code``` and GFM `|` tables are
|
|
7
|
+
* held and emitted fully formatted the moment their block closes, producing the
|
|
8
|
+
* same output the settled renderMarkdown pass would. Pure string→string (no
|
|
9
|
+
* I/O), so it is unit-testable and reusable by any front end.
|
|
10
|
+
*/
|
|
11
|
+
export class StreamingMarkdown {
|
|
12
|
+
/** Trailing text of the current, not-yet-complete line. */
|
|
13
|
+
private partial = "";
|
|
14
|
+
/** Completed lines held while inside a fence or table block. */
|
|
15
|
+
private held: string[] = [];
|
|
16
|
+
private fenceLang = "";
|
|
17
|
+
private mode: "prose" | "fence" | "table" = "prose";
|
|
18
|
+
private streamed = false;
|
|
19
|
+
|
|
20
|
+
/** True once any content streamed since the last reset() — the settled
|
|
21
|
+
* `message` event uses this to skip its (duplicate) full render. */
|
|
22
|
+
get sawContent(): boolean {
|
|
23
|
+
return this.streamed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Feed a streamed chunk; returns whatever is ready to print NOW. */
|
|
27
|
+
push(text: string, color: boolean): string {
|
|
28
|
+
// First content of a turn opens with a blank separator, matching the
|
|
29
|
+
// leading "\n" the settled message render used to add.
|
|
30
|
+
let out = this.streamed ? "" : "\n";
|
|
31
|
+
|
|
32
|
+
this.streamed = true;
|
|
33
|
+
this.partial += text;
|
|
34
|
+
|
|
35
|
+
let nl = this.partial.indexOf("\n");
|
|
36
|
+
|
|
37
|
+
while (nl !== -1) {
|
|
38
|
+
const line = this.partial.slice(0, nl);
|
|
39
|
+
|
|
40
|
+
this.partial = this.partial.slice(nl + 1);
|
|
41
|
+
out += this.takeLine(line, color);
|
|
42
|
+
nl = this.partial.indexOf("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Emit anything still held — the closing table/fence of an answer, or the
|
|
49
|
+
* partial last line on abort — and clear the buffers (not the latch). */
|
|
50
|
+
flush(color: boolean): string {
|
|
51
|
+
let out = "";
|
|
52
|
+
|
|
53
|
+
if (this.mode === "fence" && this.held.length > 0) {
|
|
54
|
+
out += `${highlightCode(this.held.join("\n"), this.fenceLang, color)}\n`;
|
|
55
|
+
} else if (this.mode === "table") {
|
|
56
|
+
out += `${formatTables(this.held.join("\n"), color)}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.partial.length > 0) {
|
|
60
|
+
out += `${styleInline(this.partial, color)}\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.mode = "prose";
|
|
64
|
+
this.held = [];
|
|
65
|
+
this.partial = "";
|
|
66
|
+
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Forget everything, including the streamed latch (turn fully consumed). */
|
|
71
|
+
reset(): void {
|
|
72
|
+
this.flush(false);
|
|
73
|
+
this.streamed = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Consume one completed line according to the current block mode. */
|
|
77
|
+
private takeLine(line: string, color: boolean): string {
|
|
78
|
+
if (this.mode === "fence") {
|
|
79
|
+
if (/^\s*```\s*$/.test(line)) {
|
|
80
|
+
const code = highlightCode(this.held.join("\n"), this.fenceLang, color);
|
|
81
|
+
|
|
82
|
+
this.mode = "prose";
|
|
83
|
+
this.held = [];
|
|
84
|
+
|
|
85
|
+
return `${code}\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.held.push(line);
|
|
89
|
+
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.mode === "table") {
|
|
94
|
+
if (line.includes("|")) {
|
|
95
|
+
this.held.push(line);
|
|
96
|
+
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const block = `${formatTables(this.held.join("\n"), color)}\n`;
|
|
101
|
+
|
|
102
|
+
this.mode = "prose";
|
|
103
|
+
this.held = [];
|
|
104
|
+
|
|
105
|
+
// The line that ended the table is a fresh prose/fence-opener line.
|
|
106
|
+
return block + this.takeLine(line, color);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fence = /^\s*```([\w-]*)\s*$/.exec(line);
|
|
110
|
+
|
|
111
|
+
if (fence !== null) {
|
|
112
|
+
this.mode = "fence";
|
|
113
|
+
this.fenceLang =
|
|
114
|
+
fence[1] !== undefined && fence[1].length > 0 ? fence[1] : "typescript";
|
|
115
|
+
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (line.includes("|")) {
|
|
120
|
+
this.mode = "table";
|
|
121
|
+
this.held = [line];
|
|
122
|
+
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `${styleInline(line, color)}\n`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Terminal ANSI styling — shared by the event renderer and the welcome banner.
|
|
2
|
+
* ESC is built via fromCharCode so the control byte is unambiguous in source.
|
|
3
|
+
* Brand truecolor codes mirror apps/docs/src/styles/custom.css (--tf-brand-*). */
|
|
4
|
+
const ESC = String.fromCharCode(27);
|
|
5
|
+
|
|
6
|
+
export const RESET = `${ESC}[0m`;
|
|
7
|
+
|
|
8
|
+
export const STYLE = {
|
|
9
|
+
dim: `${ESC}[2m`,
|
|
10
|
+
bold: `${ESC}[1m`,
|
|
11
|
+
red: `${ESC}[31m`,
|
|
12
|
+
green: `${ESC}[32m`,
|
|
13
|
+
yellow: `${ESC}[33m`,
|
|
14
|
+
magenta: `${ESC}[35m`,
|
|
15
|
+
/** #3b82f6 — primary brand */
|
|
16
|
+
brand: `${ESC}[38;2;59;130;246m`,
|
|
17
|
+
/** #60a5fa — lighter accent */
|
|
18
|
+
brandLight: `${ESC}[38;2;96;165;250m`,
|
|
19
|
+
/** #2563eb — darker accent */
|
|
20
|
+
brandDark: `${ESC}[38;2;37;99;235m`,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** Wrap `text` in an ANSI code when color is on; otherwise return it untouched. */
|
|
24
|
+
export function paint(text: string, code: string, color: boolean): string {
|
|
25
|
+
return color ? `${code}${text}${RESET}` : text;
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { jobNameMustBeConstantRule } from "./rules/job-name-must-be-constant";
|
|
4
|
+
import { jobOptionsMustSetAttemptsRule } from "./rules/job-options-must-set-attempts";
|
|
5
|
+
import { noBlockingConcurrencyZeroRule } from "./rules/no-blocking-concurrency-zero";
|
|
6
|
+
import { queueOptionsMustSetRemoveOnCompleteRule } from "./rules/queue-options-must-set-removeoncomplete";
|
|
7
|
+
import { queueOptionsMustSetRemoveOnFailRule } from "./rules/queue-options-must-set-removeonfail";
|
|
8
|
+
import { workerMustImplementCloseRule } from "./rules/worker-must-implement-close";
|
|
9
|
+
import { workerMustListenFailedRule } from "./rules/worker-must-listen-failed";
|
|
10
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
11
|
+
|
|
12
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
13
|
+
"job-name-must-be-constant": jobNameMustBeConstantRule,
|
|
14
|
+
"job-options-must-set-attempts": jobOptionsMustSetAttemptsRule,
|
|
15
|
+
"no-blocking-concurrency-zero": noBlockingConcurrencyZeroRule,
|
|
16
|
+
"queue-options-must-set-removeoncomplete":
|
|
17
|
+
queueOptionsMustSetRemoveOnCompleteRule,
|
|
18
|
+
"queue-options-must-set-removeonfail": queueOptionsMustSetRemoveOnFailRule,
|
|
19
|
+
"worker-must-implement-close": workerMustImplementCloseRule,
|
|
20
|
+
"worker-must-listen-failed": workerMustListenFailedRule,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const bullmqPack: IRulePack = {
|
|
24
|
+
id: "bullmq",
|
|
25
|
+
description:
|
|
26
|
+
"Job queue patterns with BullMQ (queue options, worker lifecycle, job naming)",
|
|
27
|
+
rules,
|
|
28
|
+
rulesConfig: {
|
|
29
|
+
"job-name-must-be-constant": "warn",
|
|
30
|
+
"job-options-must-set-attempts": "error",
|
|
31
|
+
"no-blocking-concurrency-zero": "error",
|
|
32
|
+
"queue-options-must-set-removeoncomplete": "error",
|
|
33
|
+
"queue-options-must-set-removeonfail": "error",
|
|
34
|
+
"worker-must-implement-close": "error",
|
|
35
|
+
"worker-must-listen-failed": "error",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default bullmqPack;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { jobNameMustBeConstantRule } from "./job-name-must-be-constant";
|
|
2
|
+
export { jobOptionsMustSetAttemptsRule } from "./job-options-must-set-attempts";
|
|
3
|
+
export { noBlockingConcurrencyZeroRule } from "./no-blocking-concurrency-zero";
|
|
4
|
+
export { queueOptionsMustSetRemoveOnCompleteRule } from "./queue-options-must-set-removeoncomplete";
|
|
5
|
+
export { queueOptionsMustSetRemoveOnFailRule } from "./queue-options-must-set-removeonfail";
|
|
6
|
+
export { workerMustImplementCloseRule } from "./worker-must-implement-close";
|
|
7
|
+
export { workerMustListenFailedRule } from "./worker-must-listen-failed";
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
analyzeBullmqImports,
|
|
7
|
+
collectQueueDefinitions,
|
|
8
|
+
getCallReceiverKey,
|
|
9
|
+
isQueueAddCall,
|
|
10
|
+
isQueueLikeReceiverName,
|
|
11
|
+
type BullmqImports,
|
|
12
|
+
type QueueDefinition,
|
|
13
|
+
} from "../utils";
|
|
14
|
+
|
|
15
|
+
export const RULE_NAME = "job-name-must-be-constant";
|
|
16
|
+
|
|
17
|
+
export interface JobNameMustBeConstantOptions {
|
|
18
|
+
readonly queueNamePattern?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RuleOptions = [JobNameMustBeConstantOptions];
|
|
22
|
+
type MessageIds = "literalJobName";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_QUEUE_NAME_PATTERN = "Queue$";
|
|
25
|
+
|
|
26
|
+
const optionSchema: JSONSchema4 = {
|
|
27
|
+
type: "object",
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
queueNamePattern: { type: "string", minLength: 1 },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const jobNameMustBeConstantRule = createRule<RuleOptions, MessageIds>({
|
|
35
|
+
name: RULE_NAME,
|
|
36
|
+
meta: {
|
|
37
|
+
type: "suggestion",
|
|
38
|
+
docs: {
|
|
39
|
+
description:
|
|
40
|
+
"Disallow string-literal job names in `<queue>.add(name, ...)` calls — use a constant identifier so all consumers share one source of truth.",
|
|
41
|
+
},
|
|
42
|
+
schema: [optionSchema],
|
|
43
|
+
messages: {
|
|
44
|
+
literalJobName:
|
|
45
|
+
"Job name `{{value}}` is an inline string literal — pass a constant identifier (e.g., `JOB_NAMES.foo`) so producers, workers, and dashboards share one source of truth.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultOptions: [{ queueNamePattern: DEFAULT_QUEUE_NAME_PATTERN }],
|
|
49
|
+
create(context, [options]) {
|
|
50
|
+
const queuePattern = compilePattern(
|
|
51
|
+
options.queueNamePattern ?? DEFAULT_QUEUE_NAME_PATTERN
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let imports: BullmqImports = {
|
|
55
|
+
hasBullmqImport: false,
|
|
56
|
+
workerLocalNames: new Set(),
|
|
57
|
+
queueLocalNames: new Set(),
|
|
58
|
+
queueEventsLocalNames: new Set(),
|
|
59
|
+
};
|
|
60
|
+
let knownQueues = new Map<string, QueueDefinition>();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
Program(program) {
|
|
64
|
+
imports = analyzeBullmqImports(program);
|
|
65
|
+
knownQueues = collectQueueDefinitions(program, imports);
|
|
66
|
+
},
|
|
67
|
+
CallExpression(node) {
|
|
68
|
+
if (!isQueueAddCall(node)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!receiverIsQueueLike(node, knownQueues, queuePattern)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const nameArg = node.arguments[0];
|
|
77
|
+
|
|
78
|
+
if (!nameArg) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
nameArg.type === AST_NODE_TYPES.Literal &&
|
|
84
|
+
typeof nameArg.value === "string"
|
|
85
|
+
) {
|
|
86
|
+
context.report({
|
|
87
|
+
node: nameArg,
|
|
88
|
+
messageId: "literalJobName",
|
|
89
|
+
data: { value: nameArg.value },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
nameArg.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
97
|
+
nameArg.expressions.length === 0 &&
|
|
98
|
+
nameArg.quasis.length > 0
|
|
99
|
+
) {
|
|
100
|
+
context.report({
|
|
101
|
+
node: nameArg,
|
|
102
|
+
messageId: "literalJobName",
|
|
103
|
+
data: { value: nameArg.quasis[0]?.value.cooked ?? "" },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function receiverIsQueueLike(
|
|
112
|
+
call: TSESTree.CallExpression,
|
|
113
|
+
knownQueues: ReadonlyMap<string, QueueDefinition>,
|
|
114
|
+
queuePattern: RegExp | null
|
|
115
|
+
): boolean {
|
|
116
|
+
const key = getCallReceiverKey(call);
|
|
117
|
+
|
|
118
|
+
if (!key) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (knownQueues.has(key)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!queuePattern) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const last = key.includes(".") ? (key.split(".").pop() ?? key) : key;
|
|
131
|
+
|
|
132
|
+
return queuePattern.test(last) || isQueueLikeReceiverName(last);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function compilePattern(source: string): RegExp | null {
|
|
136
|
+
try {
|
|
137
|
+
return new RegExp(source);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
analyzeBullmqImports,
|
|
7
|
+
collectQueueDefinitions,
|
|
8
|
+
findObjectProperty,
|
|
9
|
+
getCallReceiverKey,
|
|
10
|
+
getOptionsObjectArg,
|
|
11
|
+
isQueueAddCall,
|
|
12
|
+
isQueueLikeReceiverName,
|
|
13
|
+
type BullmqImports,
|
|
14
|
+
type QueueDefinition,
|
|
15
|
+
} from "../utils";
|
|
16
|
+
|
|
17
|
+
export const RULE_NAME = "job-options-must-set-attempts";
|
|
18
|
+
|
|
19
|
+
export interface JobOptionsMustSetAttemptsOptions {
|
|
20
|
+
readonly requireBackoff?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type RuleOptions = [JobOptionsMustSetAttemptsOptions];
|
|
24
|
+
type MessageIds = "missingAttempts" | "missingBackoff";
|
|
25
|
+
|
|
26
|
+
const optionSchema: JSONSchema4 = {
|
|
27
|
+
type: "object",
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
requireBackoff: { type: "boolean" },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const jobOptionsMustSetAttemptsRule = createRule<
|
|
35
|
+
RuleOptions,
|
|
36
|
+
MessageIds
|
|
37
|
+
>({
|
|
38
|
+
name: RULE_NAME,
|
|
39
|
+
meta: {
|
|
40
|
+
type: "problem",
|
|
41
|
+
docs: {
|
|
42
|
+
description:
|
|
43
|
+
"Every `<queue>.add(...)` must configure `attempts` (per-call or via `defaultJobOptions`); when `attempts > 1`, also require `backoff`.",
|
|
44
|
+
},
|
|
45
|
+
schema: [optionSchema],
|
|
46
|
+
messages: {
|
|
47
|
+
missingAttempts:
|
|
48
|
+
"Job has no `attempts` configuration — failed jobs will not retry. Set `attempts` per-call or via `defaultJobOptions` on the Queue.",
|
|
49
|
+
missingBackoff:
|
|
50
|
+
"Job has `attempts > 1` but no `backoff` configuration — retries will fire back-to-back without delay, likely re-failing for the same reason. Add a `backoff` (e.g., `{ type: 'exponential', delay: 1000 }`).",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultOptions: [{ requireBackoff: true }],
|
|
54
|
+
create(context, [options]) {
|
|
55
|
+
const requireBackoff = options.requireBackoff !== false;
|
|
56
|
+
|
|
57
|
+
let imports: BullmqImports = {
|
|
58
|
+
hasBullmqImport: false,
|
|
59
|
+
workerLocalNames: new Set(),
|
|
60
|
+
queueLocalNames: new Set(),
|
|
61
|
+
queueEventsLocalNames: new Set(),
|
|
62
|
+
};
|
|
63
|
+
let knownQueues = new Map<string, QueueDefinition>();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
Program(program) {
|
|
67
|
+
imports = analyzeBullmqImports(program);
|
|
68
|
+
knownQueues = collectQueueDefinitions(program, imports);
|
|
69
|
+
},
|
|
70
|
+
CallExpression(node) {
|
|
71
|
+
if (!isQueueAddCall(node)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!receiverIsQueueLike(node, knownQueues)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const attemptsValue = getEffectiveOptionValue(
|
|
80
|
+
node,
|
|
81
|
+
knownQueues,
|
|
82
|
+
"attempts"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!attemptsValue) {
|
|
86
|
+
context.report({ node, messageId: "missingAttempts" });
|
|
87
|
+
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!requireBackoff) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isAttemptsLiteralOne(attemptsValue)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const backoffValue = getEffectiveOptionValue(
|
|
100
|
+
node,
|
|
101
|
+
knownQueues,
|
|
102
|
+
"backoff"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!backoffValue) {
|
|
106
|
+
context.report({ node, messageId: "missingBackoff" });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function getEffectiveOptionValue(
|
|
114
|
+
call: TSESTree.CallExpression,
|
|
115
|
+
knownQueues: ReadonlyMap<string, QueueDefinition>,
|
|
116
|
+
name: string
|
|
117
|
+
): TSESTree.Property["value"] | null {
|
|
118
|
+
const opts = getOptionsObjectArg(call, 2);
|
|
119
|
+
|
|
120
|
+
if (opts) {
|
|
121
|
+
const property = findObjectProperty(opts, name);
|
|
122
|
+
|
|
123
|
+
if (property) {
|
|
124
|
+
return property.value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const key = getCallReceiverKey(call);
|
|
129
|
+
|
|
130
|
+
if (!key) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const def = knownQueues.get(key);
|
|
135
|
+
|
|
136
|
+
if (!def?.defaultJobOptions) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const defaultProperty = findObjectProperty(def.defaultJobOptions, name);
|
|
141
|
+
|
|
142
|
+
if (!defaultProperty) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return defaultProperty.value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isAttemptsLiteralOne(value: TSESTree.Property["value"]): boolean {
|
|
150
|
+
return (
|
|
151
|
+
value.type === AST_NODE_TYPES.Literal &&
|
|
152
|
+
typeof value.value === "number" &&
|
|
153
|
+
value.value === 1
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function receiverIsQueueLike(
|
|
158
|
+
call: TSESTree.CallExpression,
|
|
159
|
+
knownQueues: ReadonlyMap<string, QueueDefinition>
|
|
160
|
+
): boolean {
|
|
161
|
+
const key = getCallReceiverKey(call);
|
|
162
|
+
|
|
163
|
+
if (!key) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (knownQueues.has(key)) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const last = key.includes(".") ? (key.split(".").pop() ?? key) : key;
|
|
172
|
+
|
|
173
|
+
return isQueueLikeReceiverName(last);
|
|
174
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
analyzeBullmqImports,
|
|
7
|
+
findObjectProperty,
|
|
8
|
+
getOptionsObjectArg,
|
|
9
|
+
isNewWorker,
|
|
10
|
+
type BullmqImports,
|
|
11
|
+
} from "../utils";
|
|
12
|
+
|
|
13
|
+
export const RULE_NAME = "no-blocking-concurrency-zero";
|
|
14
|
+
|
|
15
|
+
type RuleOptions = [];
|
|
16
|
+
type MessageIds = "invalidConcurrency";
|
|
17
|
+
|
|
18
|
+
const optionSchema: JSONSchema4 = {
|
|
19
|
+
type: "object",
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const noBlockingConcurrencyZeroRule = createRule<
|
|
25
|
+
RuleOptions,
|
|
26
|
+
MessageIds
|
|
27
|
+
>({
|
|
28
|
+
name: RULE_NAME,
|
|
29
|
+
meta: {
|
|
30
|
+
type: "problem",
|
|
31
|
+
docs: {
|
|
32
|
+
description:
|
|
33
|
+
"Disallow `new Worker(name, processor, { concurrency: <numericLiteral ≤ 0> })` — non-positive concurrency blocks job processing.",
|
|
34
|
+
},
|
|
35
|
+
schema: [optionSchema],
|
|
36
|
+
messages: {
|
|
37
|
+
invalidConcurrency:
|
|
38
|
+
"Worker concurrency must be ≥ 1 — `{{value}}` would block job processing entirely. Use a positive integer or read from configuration.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultOptions: [],
|
|
42
|
+
create(context) {
|
|
43
|
+
let imports: BullmqImports = {
|
|
44
|
+
hasBullmqImport: false,
|
|
45
|
+
workerLocalNames: new Set(),
|
|
46
|
+
queueLocalNames: new Set(),
|
|
47
|
+
queueEventsLocalNames: new Set(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
Program(program) {
|
|
52
|
+
imports = analyzeBullmqImports(program);
|
|
53
|
+
},
|
|
54
|
+
NewExpression(node) {
|
|
55
|
+
if (!isNewWorker(node, imports)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const opts = getOptionsObjectArg(node, 2);
|
|
60
|
+
|
|
61
|
+
if (!opts) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const concurrency = findObjectProperty(opts, "concurrency");
|
|
66
|
+
|
|
67
|
+
if (!concurrency) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const value = concurrency.value;
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
value.type === AST_NODE_TYPES.Literal &&
|
|
75
|
+
typeof value.value === "number" &&
|
|
76
|
+
value.value <= 0
|
|
77
|
+
) {
|
|
78
|
+
context.report({
|
|
79
|
+
node: value,
|
|
80
|
+
messageId: "invalidConcurrency",
|
|
81
|
+
data: { value: String(value.value) },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
value.type === AST_NODE_TYPES.UnaryExpression &&
|
|
89
|
+
value.operator === "-" &&
|
|
90
|
+
value.argument.type === AST_NODE_TYPES.Literal &&
|
|
91
|
+
typeof value.argument.value === "number" &&
|
|
92
|
+
value.argument.value > 0
|
|
93
|
+
) {
|
|
94
|
+
context.report({
|
|
95
|
+
node: value,
|
|
96
|
+
messageId: "invalidConcurrency",
|
|
97
|
+
data: { value: `-${value.argument.value}` },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|