@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,107 @@
|
|
|
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
|
+
findObjectProperty,
|
|
7
|
+
getChainRoot,
|
|
8
|
+
isNewElysiaExpression,
|
|
9
|
+
} from "../utils/elysiaChain";
|
|
10
|
+
|
|
11
|
+
export const RULE_NAME = "require-plugin-name";
|
|
12
|
+
|
|
13
|
+
export interface RequirePluginNameOptions {
|
|
14
|
+
readonly allowAnonymousDefault?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type RuleOptions = [RequirePluginNameOptions];
|
|
18
|
+
type MessageIds = "missingPluginName";
|
|
19
|
+
|
|
20
|
+
const optionSchema: JSONSchema4 = {
|
|
21
|
+
type: "object",
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
properties: {
|
|
24
|
+
allowAnonymousDefault: { type: "boolean" },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const requirePluginNameRule = createRule<RuleOptions, MessageIds>({
|
|
29
|
+
name: RULE_NAME,
|
|
30
|
+
meta: {
|
|
31
|
+
type: "problem",
|
|
32
|
+
docs: {
|
|
33
|
+
description:
|
|
34
|
+
"Exported Elysia plugin instances must declare `new Elysia({ name: '...' })` so the runtime can deduplicate plugin re-imports.",
|
|
35
|
+
},
|
|
36
|
+
schema: [optionSchema],
|
|
37
|
+
messages: {
|
|
38
|
+
missingPluginName:
|
|
39
|
+
'Exported Elysia plugin is anonymous — pass `{ name: "..." }` to `new Elysia(...)` so the runtime can deduplicate this plugin across imports.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [{ allowAnonymousDefault: false }],
|
|
43
|
+
create(context, [options]) {
|
|
44
|
+
const allowAnonymousDefault = options.allowAnonymousDefault === true;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
ExportNamedDeclaration(node) {
|
|
48
|
+
if (node.declaration?.type !== AST_NODE_TYPES.VariableDeclaration) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const declarator of node.declaration.declarations) {
|
|
53
|
+
if (declarator.init) {
|
|
54
|
+
checkExpression(declarator.init);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
ExportDefaultDeclaration(node) {
|
|
59
|
+
if (allowAnonymousDefault) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const expr = node.declaration;
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
expr.type === AST_NODE_TYPES.NewExpression ||
|
|
67
|
+
expr.type === AST_NODE_TYPES.CallExpression ||
|
|
68
|
+
expr.type === AST_NODE_TYPES.MemberExpression
|
|
69
|
+
) {
|
|
70
|
+
checkExpression(expr);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function checkExpression(expression: TSESTree.Expression): void {
|
|
76
|
+
let root: TSESTree.Node = expression;
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
expression.type === AST_NODE_TYPES.CallExpression ||
|
|
80
|
+
expression.type === AST_NODE_TYPES.MemberExpression
|
|
81
|
+
) {
|
|
82
|
+
root = getChainRoot(expression);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!isNewElysiaExpression(root)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newExpr = root;
|
|
90
|
+
const arg = newExpr.arguments[0];
|
|
91
|
+
|
|
92
|
+
if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
93
|
+
const nameProperty = findObjectProperty(arg, "name");
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
nameProperty?.value.type === AST_NODE_TYPES.Literal &&
|
|
97
|
+
typeof nameProperty.value.value === "string" &&
|
|
98
|
+
nameProperty.value.value.length > 0
|
|
99
|
+
) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
context.report({ node: newExpr, messageId: "missingPluginName" });
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
export const ROUTE_METHODS = new Set([
|
|
4
|
+
"get",
|
|
5
|
+
"post",
|
|
6
|
+
"put",
|
|
7
|
+
"patch",
|
|
8
|
+
"delete",
|
|
9
|
+
"options",
|
|
10
|
+
"head",
|
|
11
|
+
"trace",
|
|
12
|
+
"all",
|
|
13
|
+
"ws",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const HOOK_METHODS = new Set([
|
|
17
|
+
"onRequest",
|
|
18
|
+
"onParse",
|
|
19
|
+
"onTransform",
|
|
20
|
+
"onBeforeHandle",
|
|
21
|
+
"resolve",
|
|
22
|
+
"onAfterHandle",
|
|
23
|
+
"mapResponse",
|
|
24
|
+
"onError",
|
|
25
|
+
"onAfterResponse",
|
|
26
|
+
"trace",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
type FunctionLike =
|
|
30
|
+
| TSESTree.ArrowFunctionExpression
|
|
31
|
+
| TSESTree.FunctionExpression
|
|
32
|
+
| TSESTree.FunctionDeclaration;
|
|
33
|
+
|
|
34
|
+
export function isNewElysiaExpression(
|
|
35
|
+
node: TSESTree.Node | null | undefined
|
|
36
|
+
): node is TSESTree.NewExpression {
|
|
37
|
+
return (
|
|
38
|
+
!!node &&
|
|
39
|
+
node.type === AST_NODE_TYPES.NewExpression &&
|
|
40
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
41
|
+
node.callee.name === "Elysia"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getChainRoot(
|
|
46
|
+
node: TSESTree.CallExpression | TSESTree.MemberExpression
|
|
47
|
+
): TSESTree.Expression {
|
|
48
|
+
let current: TSESTree.Expression = node;
|
|
49
|
+
|
|
50
|
+
while (
|
|
51
|
+
current.type === AST_NODE_TYPES.CallExpression ||
|
|
52
|
+
current.type === AST_NODE_TYPES.MemberExpression
|
|
53
|
+
) {
|
|
54
|
+
if (current.type === AST_NODE_TYPES.CallExpression) {
|
|
55
|
+
current = current.callee;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
current = current.object;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function collectElysiaVariables(program: TSESTree.Program): Set<string> {
|
|
66
|
+
const elysiaVars = new Set<string>();
|
|
67
|
+
|
|
68
|
+
for (const stmt of program.body) {
|
|
69
|
+
visitDeclarations(stmt, elysiaVars);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return elysiaVars;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function visitDeclarations(stmt: TSESTree.Node, elysiaVars: Set<string>): void {
|
|
76
|
+
if (stmt.type === AST_NODE_TYPES.VariableDeclaration) {
|
|
77
|
+
for (const declarator of stmt.declarations) {
|
|
78
|
+
if (
|
|
79
|
+
declarator.id.type === AST_NODE_TYPES.Identifier &&
|
|
80
|
+
declarator.init &&
|
|
81
|
+
chainRootIsNewElysia(declarator.init)
|
|
82
|
+
) {
|
|
83
|
+
elysiaVars.add(declarator.id.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.declaration) {
|
|
91
|
+
visitDeclarations(stmt.declaration, elysiaVars);
|
|
92
|
+
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function chainRootIsNewElysia(expression: TSESTree.Expression): boolean {
|
|
102
|
+
if (expression.type === AST_NODE_TYPES.NewExpression) {
|
|
103
|
+
return isNewElysiaExpression(expression);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
expression.type === AST_NODE_TYPES.CallExpression ||
|
|
108
|
+
expression.type === AST_NODE_TYPES.MemberExpression
|
|
109
|
+
) {
|
|
110
|
+
const root = getChainRoot(expression);
|
|
111
|
+
|
|
112
|
+
return isNewElysiaExpression(root);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isElysiaRouted(
|
|
119
|
+
call: TSESTree.CallExpression,
|
|
120
|
+
elysiaVars: Set<string>
|
|
121
|
+
): boolean {
|
|
122
|
+
if (call.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const root = getChainRoot(call);
|
|
127
|
+
|
|
128
|
+
if (isNewElysiaExpression(root)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (root.type === AST_NODE_TYPES.Identifier && elysiaVars.has(root.name)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getMemberMethodName(
|
|
140
|
+
call: TSESTree.CallExpression
|
|
141
|
+
): string | null {
|
|
142
|
+
if (
|
|
143
|
+
call.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
144
|
+
call.callee.property.type === AST_NODE_TYPES.Identifier
|
|
145
|
+
) {
|
|
146
|
+
return call.callee.property.name;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function isElysiaRouteCall(
|
|
153
|
+
call: TSESTree.CallExpression,
|
|
154
|
+
elysiaVars: Set<string>
|
|
155
|
+
): boolean {
|
|
156
|
+
const method = getMemberMethodName(call);
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
method !== null &&
|
|
160
|
+
ROUTE_METHODS.has(method) &&
|
|
161
|
+
isElysiaRouted(call, elysiaVars)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isElysiaHookCall(
|
|
166
|
+
call: TSESTree.CallExpression,
|
|
167
|
+
elysiaVars: Set<string>
|
|
168
|
+
): boolean {
|
|
169
|
+
const method = getMemberMethodName(call);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
method !== null &&
|
|
173
|
+
HOOK_METHODS.has(method) &&
|
|
174
|
+
isElysiaRouted(call, elysiaVars)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getRouteMethod(call: TSESTree.CallExpression): string | null {
|
|
179
|
+
const method = getMemberMethodName(call);
|
|
180
|
+
|
|
181
|
+
return method && ROUTE_METHODS.has(method) ? method : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getRoutePathLiteral(
|
|
185
|
+
call: TSESTree.CallExpression
|
|
186
|
+
): string | null {
|
|
187
|
+
const arg = call.arguments[0];
|
|
188
|
+
|
|
189
|
+
if (arg?.type === AST_NODE_TYPES.Literal && typeof arg.value === "string") {
|
|
190
|
+
return arg.value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getRouteHandlerFunction(
|
|
197
|
+
call: TSESTree.CallExpression
|
|
198
|
+
): FunctionLike | null {
|
|
199
|
+
for (let i = call.arguments.length - 1; i >= 0; i--) {
|
|
200
|
+
const arg = call.arguments[i];
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
arg &&
|
|
204
|
+
(arg.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
205
|
+
arg.type === AST_NODE_TYPES.FunctionExpression)
|
|
206
|
+
) {
|
|
207
|
+
return arg;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function getRouteOptionsObject(
|
|
215
|
+
call: TSESTree.CallExpression
|
|
216
|
+
): TSESTree.ObjectExpression | null {
|
|
217
|
+
for (let i = call.arguments.length - 1; i >= 0; i--) {
|
|
218
|
+
const arg = call.arguments[i];
|
|
219
|
+
|
|
220
|
+
if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
221
|
+
return arg;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function findEnclosingRouteHandler(
|
|
229
|
+
node: TSESTree.Node,
|
|
230
|
+
elysiaVars: Set<string>
|
|
231
|
+
): {
|
|
232
|
+
readonly fn: FunctionLike;
|
|
233
|
+
readonly routeCall: TSESTree.CallExpression;
|
|
234
|
+
} | null {
|
|
235
|
+
let current: TSESTree.Node | undefined = node.parent;
|
|
236
|
+
|
|
237
|
+
while (current) {
|
|
238
|
+
if (
|
|
239
|
+
(current.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
240
|
+
current.type === AST_NODE_TYPES.FunctionExpression) &&
|
|
241
|
+
current.parent?.type === AST_NODE_TYPES.CallExpression &&
|
|
242
|
+
current.parent.arguments.includes(current) &&
|
|
243
|
+
isElysiaRouteCall(current.parent, elysiaVars)
|
|
244
|
+
) {
|
|
245
|
+
return { fn: current, routeCall: current.parent };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
current = current.parent;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function getCalleeName(node: TSESTree.CallExpression): string | null {
|
|
255
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
|
256
|
+
return node.callee.name;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
261
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier
|
|
262
|
+
) {
|
|
263
|
+
if (node.callee.object.type === AST_NODE_TYPES.Identifier) {
|
|
264
|
+
return `${node.callee.object.name}.${node.callee.property.name}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
node.callee.object.type === AST_NODE_TYPES.MemberExpression &&
|
|
269
|
+
node.callee.object.object.type === AST_NODE_TYPES.Identifier &&
|
|
270
|
+
node.callee.object.property.type === AST_NODE_TYPES.Identifier
|
|
271
|
+
) {
|
|
272
|
+
return `${node.callee.object.object.name}.${node.callee.object.property.name}.${node.callee.property.name}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return node.callee.property.name;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function findObjectProperty(
|
|
282
|
+
obj: TSESTree.ObjectExpression,
|
|
283
|
+
name: string
|
|
284
|
+
): TSESTree.Property | null {
|
|
285
|
+
for (const property of obj.properties) {
|
|
286
|
+
if (property.type !== AST_NODE_TYPES.Property) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
292
|
+
property.key.name === name
|
|
293
|
+
) {
|
|
294
|
+
return property;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
property.key.type === AST_NODE_TYPES.Literal &&
|
|
299
|
+
property.key.value === name
|
|
300
|
+
) {
|
|
301
|
+
return property;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { noDirectProcessEnvRule } from "./rules/no-direct-process-env";
|
|
4
|
+
import { noProcessExitRule } from "./rules/no-process-exit";
|
|
5
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
6
|
+
|
|
7
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
8
|
+
"no-direct-process-env": noDirectProcessEnvRule,
|
|
9
|
+
"no-process-exit": noProcessExitRule,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const envAccessPack: IRulePack = {
|
|
13
|
+
id: "env-access",
|
|
14
|
+
description:
|
|
15
|
+
"Safe environment variable access patterns (validation and typing)",
|
|
16
|
+
rules,
|
|
17
|
+
rulesConfig: {
|
|
18
|
+
"no-direct-process-env": "error",
|
|
19
|
+
"no-process-exit": "error",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default envAccessPack;
|
|
@@ -0,0 +1,133 @@
|
|
|
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 { matchesAnyGlobPattern, toPosixRelative } from "../../utils";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "no-direct-process-env";
|
|
8
|
+
|
|
9
|
+
export interface NoDirectProcessEnvOptions {
|
|
10
|
+
readonly allowedFiles?: readonly string[];
|
|
11
|
+
readonly singletonSuggestion?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RuleOptions = [NoDirectProcessEnvOptions];
|
|
15
|
+
type MessageIds = "directProcessEnv";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_ALLOWED_FILES: readonly string[] = [
|
|
18
|
+
"src/config/env/**",
|
|
19
|
+
"**/*.config.{ts,js,mjs}",
|
|
20
|
+
"scripts/**",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const DEFAULT_SUGGESTION = "import { env } from '@/config/env'";
|
|
24
|
+
|
|
25
|
+
const optionSchema: JSONSchema4 = {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
allowedFiles: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
uniqueItems: true,
|
|
33
|
+
},
|
|
34
|
+
singletonSuggestion: { type: "string", minLength: 1 },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function isProcessEnv(node: TSESTree.Node): boolean {
|
|
39
|
+
if (node.type !== AST_NODE_TYPES.MemberExpression) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (node.computed) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
node.object.type !== AST_NODE_TYPES.Identifier ||
|
|
49
|
+
node.object.name !== "process"
|
|
50
|
+
) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
node.property.type !== AST_NODE_TYPES.Identifier ||
|
|
56
|
+
node.property.name !== "env"
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const noDirectProcessEnvRule = createRule<RuleOptions, MessageIds>({
|
|
65
|
+
name: RULE_NAME,
|
|
66
|
+
meta: {
|
|
67
|
+
type: "problem",
|
|
68
|
+
docs: {
|
|
69
|
+
description:
|
|
70
|
+
"Disallow direct `process.env` access — force every consumer through a typed, boot-validated singleton.",
|
|
71
|
+
},
|
|
72
|
+
schema: [optionSchema],
|
|
73
|
+
messages: {
|
|
74
|
+
directProcessEnv:
|
|
75
|
+
"Read environment variables via the validated singleton ({{suggestion}}) — `process.env.X` bypasses the boot-time schema check.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultOptions: [
|
|
79
|
+
{
|
|
80
|
+
allowedFiles: [...DEFAULT_ALLOWED_FILES],
|
|
81
|
+
singletonSuggestion: DEFAULT_SUGGESTION,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
create(context, [options]) {
|
|
85
|
+
const allowedFiles = options.allowedFiles ?? DEFAULT_ALLOWED_FILES;
|
|
86
|
+
const suggestion = options.singletonSuggestion ?? DEFAULT_SUGGESTION;
|
|
87
|
+
|
|
88
|
+
const relative = toPosixRelative(context.filename, context.cwd);
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
allowedFiles.length > 0 &&
|
|
92
|
+
matchesAnyGlobPattern(relative, [...allowedFiles])
|
|
93
|
+
) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function report(node: TSESTree.Node): void {
|
|
98
|
+
context.report({
|
|
99
|
+
node,
|
|
100
|
+
messageId: "directProcessEnv",
|
|
101
|
+
data: { suggestion },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
// `process.env.X` (read or write) and `process.env[X]`
|
|
107
|
+
MemberExpression(node) {
|
|
108
|
+
if (isProcessEnv(node.object)) {
|
|
109
|
+
report(node);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
// `const { X, Y } = process.env`
|
|
113
|
+
VariableDeclarator(node) {
|
|
114
|
+
if (node.init === null) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isProcessEnv(node.init)) {
|
|
119
|
+
report(node.init);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
// `({ X } = process.env)` assignment-pattern destructure
|
|
123
|
+
AssignmentExpression(node) {
|
|
124
|
+
if (
|
|
125
|
+
node.left.type === AST_NODE_TYPES.ObjectPattern &&
|
|
126
|
+
isProcessEnv(node.right)
|
|
127
|
+
) {
|
|
128
|
+
report(node.right);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
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 { matchesAnyGlobPattern, toPosixRelative } from "../../utils";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "no-process-exit";
|
|
8
|
+
|
|
9
|
+
export interface NoProcessExitOptions {
|
|
10
|
+
readonly allowedFiles?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RuleOptions = [NoProcessExitOptions];
|
|
14
|
+
type MessageIds = "processExit";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ALLOWED_FILES: readonly string[] = [
|
|
17
|
+
"src/config/error-handlers/**",
|
|
18
|
+
"scripts/**",
|
|
19
|
+
"**/*.test.ts",
|
|
20
|
+
"tests/**",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const optionSchema: JSONSchema4 = {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
allowedFiles: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: { type: "string" },
|
|
30
|
+
uniqueItems: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function isProcessExit(node: TSESTree.CallExpression): boolean {
|
|
36
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (node.callee.computed) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
node.callee.object.type !== AST_NODE_TYPES.Identifier ||
|
|
46
|
+
node.callee.object.name !== "process"
|
|
47
|
+
) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
node.callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
53
|
+
node.callee.property.name !== "exit"
|
|
54
|
+
) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const noProcessExitRule = createRule<RuleOptions, MessageIds>({
|
|
62
|
+
name: RULE_NAME,
|
|
63
|
+
meta: {
|
|
64
|
+
type: "problem",
|
|
65
|
+
docs: {
|
|
66
|
+
description:
|
|
67
|
+
"Disallow `process.exit()` outside the centralized shutdown and CLI entrypoints — forces graceful teardown through the error-handlers module.",
|
|
68
|
+
},
|
|
69
|
+
schema: [optionSchema],
|
|
70
|
+
messages: {
|
|
71
|
+
processExit:
|
|
72
|
+
"`process.exit()` is reserved for graceful shutdown (`src/config/error-handlers/`) and CLI scripts. Route shutdown through the centralized handlers instead.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultOptions: [{ allowedFiles: [...DEFAULT_ALLOWED_FILES] }],
|
|
76
|
+
create(context, [options]) {
|
|
77
|
+
const allowedFiles = options.allowedFiles ?? DEFAULT_ALLOWED_FILES;
|
|
78
|
+
const relative = toPosixRelative(context.filename, context.cwd);
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
allowedFiles.length > 0 &&
|
|
82
|
+
matchesAnyGlobPattern(relative, [...allowedFiles])
|
|
83
|
+
) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
CallExpression(node) {
|
|
89
|
+
if (isProcessExit(node)) {
|
|
90
|
+
context.report({ node, messageId: "processExit" });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { staticTranslationKeyExistsRule } from "./rules/static-translation-key-exists";
|
|
4
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
5
|
+
|
|
6
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
7
|
+
"static-translation-key-exists": staticTranslationKeyExistsRule,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const i18nKeysPack: IRulePack = {
|
|
11
|
+
id: "i18n-keys",
|
|
12
|
+
description: "Internationalization key management and translation patterns",
|
|
13
|
+
rules,
|
|
14
|
+
rulesConfig: {
|
|
15
|
+
"static-translation-key-exists": "error",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default i18nKeysPack;
|