@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,209 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
4
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
5
|
+
|
|
6
|
+
import { createRule } from "../../create-rule";
|
|
7
|
+
import {
|
|
8
|
+
findCallExpressionsDeep,
|
|
9
|
+
isForeignKeyCall,
|
|
10
|
+
isPgTableCall,
|
|
11
|
+
isRelationsCall,
|
|
12
|
+
} from "../utils";
|
|
13
|
+
|
|
14
|
+
export const RULE_NAME = "relations-must-cover-fks";
|
|
15
|
+
|
|
16
|
+
export interface RelationsMustCoverFksOptions {
|
|
17
|
+
readonly allowExternalFile?: boolean;
|
|
18
|
+
readonly relationsFilePatterns?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RuleOptions = [RelationsMustCoverFksOptions];
|
|
22
|
+
type MessageIds = "missingRelations";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_RELATIONS_FILES: readonly string[] = [
|
|
25
|
+
"relations.ts",
|
|
26
|
+
"relations.tsx",
|
|
27
|
+
"relations.js",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const optionSchema: JSONSchema4 = {
|
|
31
|
+
type: "object",
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
properties: {
|
|
34
|
+
allowExternalFile: {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
},
|
|
37
|
+
relationsFilePatterns: {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: { type: "string" },
|
|
40
|
+
uniqueItems: true,
|
|
41
|
+
minItems: 1,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface IRelationsFileCache {
|
|
47
|
+
readonly mtimeMs: number;
|
|
48
|
+
readonly tables: ReadonlySet<string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const relationsFileCache = new Map<string, IRelationsFileCache>();
|
|
52
|
+
const RELATIONS_CALL_PATTERN = /\brelations\s*\(\s*([A-Za-z_$][\w$]*)/g;
|
|
53
|
+
|
|
54
|
+
function readTablesFromRelationsFile(filePath: string): ReadonlySet<string> {
|
|
55
|
+
let stat: fs.Stats;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
stat = fs.statSync(filePath);
|
|
59
|
+
} catch {
|
|
60
|
+
return new Set();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cached = relationsFileCache.get(filePath);
|
|
64
|
+
|
|
65
|
+
if (cached?.mtimeMs === stat.mtimeMs) {
|
|
66
|
+
return cached.tables;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let source: string;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
source = fs.readFileSync(filePath, "utf8");
|
|
73
|
+
} catch {
|
|
74
|
+
return new Set();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tables = new Set<string>();
|
|
78
|
+
let match: RegExpExecArray | null;
|
|
79
|
+
|
|
80
|
+
while ((match = RELATIONS_CALL_PATTERN.exec(source)) !== null) {
|
|
81
|
+
if (match[1]) {
|
|
82
|
+
tables.add(match[1]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
RELATIONS_CALL_PATTERN.lastIndex = 0;
|
|
87
|
+
|
|
88
|
+
relationsFileCache.set(filePath, { mtimeMs: stat.mtimeMs, tables });
|
|
89
|
+
|
|
90
|
+
return tables;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectExternalRelations(
|
|
94
|
+
startDir: string,
|
|
95
|
+
patterns: readonly string[]
|
|
96
|
+
): Set<string> {
|
|
97
|
+
const out = new Set<string>();
|
|
98
|
+
const cwd = process.cwd();
|
|
99
|
+
let cursor = path.resolve(startDir);
|
|
100
|
+
|
|
101
|
+
while (true) {
|
|
102
|
+
for (const basename of patterns) {
|
|
103
|
+
const candidate = path.join(cursor, basename);
|
|
104
|
+
|
|
105
|
+
if (fs.existsSync(candidate)) {
|
|
106
|
+
for (const t of readTablesFromRelationsFile(candidate)) {
|
|
107
|
+
out.add(t);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (cursor === cwd || path.dirname(cursor) === cursor) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
cursor = path.dirname(cursor);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const relationsMustCoverFksRule = createRule<RuleOptions, MessageIds>({
|
|
123
|
+
name: RULE_NAME,
|
|
124
|
+
meta: {
|
|
125
|
+
type: "problem",
|
|
126
|
+
docs: {
|
|
127
|
+
description:
|
|
128
|
+
"Every Drizzle table that declares a foreignKey(...) must be covered by a relations(...) call. Searches sibling `relations.ts` files by default.",
|
|
129
|
+
},
|
|
130
|
+
schema: [optionSchema],
|
|
131
|
+
messages: {
|
|
132
|
+
missingRelations:
|
|
133
|
+
"Table '{{name}}' declares foreignKey(...) but no relations(...) call covers it. Drizzle's `db.query.{{name}}.findMany({ with: ... })` will not work.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
defaultOptions: [{ allowExternalFile: true }],
|
|
137
|
+
create(context, [options]) {
|
|
138
|
+
const allowExternalFile = options.allowExternalFile ?? true;
|
|
139
|
+
const relationsFiles =
|
|
140
|
+
options.relationsFilePatterns ?? DEFAULT_RELATIONS_FILES;
|
|
141
|
+
|
|
142
|
+
const tablesWithFks = new Map<string, TSESTree.VariableDeclarator>();
|
|
143
|
+
const tablesWithRelations = new Set<string>();
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
VariableDeclarator(node) {
|
|
147
|
+
if (node.init?.type !== AST_NODE_TYPES.CallExpression) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!isPgTableCall(node.init)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fkCalls = findCallExpressionsDeep(node.init, isForeignKeyCall);
|
|
160
|
+
|
|
161
|
+
if (fkCalls.length === 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
tablesWithFks.set(node.id.name, node);
|
|
166
|
+
},
|
|
167
|
+
CallExpression(node) {
|
|
168
|
+
if (!isRelationsCall(node)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const firstArg = node.arguments[0];
|
|
173
|
+
|
|
174
|
+
if (firstArg?.type === AST_NODE_TYPES.Identifier) {
|
|
175
|
+
tablesWithRelations.add(firstArg.name);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"Program:exit"() {
|
|
179
|
+
if (allowExternalFile) {
|
|
180
|
+
const filename = context.filename;
|
|
181
|
+
|
|
182
|
+
if (filename && filename !== "<input>") {
|
|
183
|
+
const dir = path.dirname(filename);
|
|
184
|
+
|
|
185
|
+
for (const t of collectExternalRelations(dir, relationsFiles)) {
|
|
186
|
+
tablesWithRelations.add(t);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const [name, declarator] of tablesWithFks) {
|
|
192
|
+
if (tablesWithRelations.has(name)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
context.report({
|
|
197
|
+
node: declarator,
|
|
198
|
+
messageId: "missingRelations",
|
|
199
|
+
data: { name },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export function __clearRelationsFileCacheForTests(): void {
|
|
208
|
+
relationsFileCache.clear();
|
|
209
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import { matchesAnyGlobPattern } from "../../utils";
|
|
5
|
+
|
|
6
|
+
export const RULE_NAME = "schema-files-must-not-import-driver";
|
|
7
|
+
|
|
8
|
+
export interface SchemaFilesMustNotImportDriverOptions {
|
|
9
|
+
readonly filePattern?: string;
|
|
10
|
+
readonly forbiddenSources?: readonly string[];
|
|
11
|
+
readonly forbiddenSourcePatterns?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RuleOptions = [SchemaFilesMustNotImportDriverOptions];
|
|
15
|
+
type MessageIds = "forbiddenDriverImport";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_FILE_PATTERN = "**/schema/**/*.schema.ts";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_FORBIDDEN_SOURCES: readonly string[] = [
|
|
20
|
+
"pg",
|
|
21
|
+
"postgres",
|
|
22
|
+
"node-postgres",
|
|
23
|
+
"mysql2",
|
|
24
|
+
"better-sqlite3",
|
|
25
|
+
"@libsql/client",
|
|
26
|
+
"@neondatabase/serverless",
|
|
27
|
+
"@vercel/postgres",
|
|
28
|
+
"@planetscale/database",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const DEFAULT_FORBIDDEN_SOURCE_PATTERNS: readonly string[] = [
|
|
32
|
+
"drizzle-orm/node-postgres",
|
|
33
|
+
"drizzle-orm/postgres-js",
|
|
34
|
+
"drizzle-orm/neon-http",
|
|
35
|
+
"drizzle-orm/neon-serverless",
|
|
36
|
+
"drizzle-orm/vercel-postgres",
|
|
37
|
+
"drizzle-orm/aws-data-api/**",
|
|
38
|
+
"drizzle-orm/mysql2",
|
|
39
|
+
"drizzle-orm/planetscale-serverless",
|
|
40
|
+
"drizzle-orm/tidb-serverless",
|
|
41
|
+
"drizzle-orm/better-sqlite3",
|
|
42
|
+
"drizzle-orm/bun-sqlite",
|
|
43
|
+
"drizzle-orm/d1",
|
|
44
|
+
"drizzle-orm/expo-sqlite",
|
|
45
|
+
"drizzle-orm/libsql",
|
|
46
|
+
"drizzle-orm/op-sqlite",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const optionSchema: JSONSchema4 = {
|
|
50
|
+
type: "object",
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
properties: {
|
|
53
|
+
filePattern: {
|
|
54
|
+
type: "string",
|
|
55
|
+
},
|
|
56
|
+
forbiddenSources: {
|
|
57
|
+
type: "array",
|
|
58
|
+
uniqueItems: true,
|
|
59
|
+
items: { type: "string" },
|
|
60
|
+
},
|
|
61
|
+
forbiddenSourcePatterns: {
|
|
62
|
+
type: "array",
|
|
63
|
+
uniqueItems: true,
|
|
64
|
+
items: { type: "string" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const schemaFilesMustNotImportDriverRule = createRule<
|
|
70
|
+
RuleOptions,
|
|
71
|
+
MessageIds
|
|
72
|
+
>({
|
|
73
|
+
name: RULE_NAME,
|
|
74
|
+
meta: {
|
|
75
|
+
type: "problem",
|
|
76
|
+
docs: {
|
|
77
|
+
description:
|
|
78
|
+
"Disallow imports from database driver packages inside schema files. Schema files must remain driver-agnostic.",
|
|
79
|
+
},
|
|
80
|
+
schema: [optionSchema],
|
|
81
|
+
messages: {
|
|
82
|
+
forbiddenDriverImport:
|
|
83
|
+
"Schema files must not import from driver package '{{source}}' — schema definitions must remain driver-agnostic. Move the connection setup to the consuming application.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
defaultOptions: [
|
|
87
|
+
{
|
|
88
|
+
filePattern: DEFAULT_FILE_PATTERN,
|
|
89
|
+
forbiddenSources: [...DEFAULT_FORBIDDEN_SOURCES],
|
|
90
|
+
forbiddenSourcePatterns: [...DEFAULT_FORBIDDEN_SOURCE_PATTERNS],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
create(context, [options]) {
|
|
94
|
+
const filePattern = options.filePattern ?? DEFAULT_FILE_PATTERN;
|
|
95
|
+
|
|
96
|
+
if (!matchesAnyGlobPattern(context.filename, [filePattern])) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const forbiddenSources = new Set(
|
|
101
|
+
options.forbiddenSources ?? DEFAULT_FORBIDDEN_SOURCES
|
|
102
|
+
);
|
|
103
|
+
const forbiddenPatterns =
|
|
104
|
+
options.forbiddenSourcePatterns ?? DEFAULT_FORBIDDEN_SOURCE_PATTERNS;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ImportDeclaration(node) {
|
|
108
|
+
const source = node.source.value;
|
|
109
|
+
|
|
110
|
+
if (typeof source !== "string") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
forbiddenSources.has(source) ||
|
|
116
|
+
matchesAnyGlobPattern(source, forbiddenPatterns)
|
|
117
|
+
) {
|
|
118
|
+
context.report({
|
|
119
|
+
node: node.source,
|
|
120
|
+
messageId: "forbiddenDriverImport",
|
|
121
|
+
data: { source },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
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 { isSchemaBuilderCall } from "../utils";
|
|
6
|
+
import { matchesAnyGlobPattern } from "../../utils";
|
|
7
|
+
|
|
8
|
+
export const RULE_NAME = "schema-files-must-only-export-schema";
|
|
9
|
+
|
|
10
|
+
export interface SchemaFilesMustOnlyExportSchemaOptions {
|
|
11
|
+
readonly filePattern?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RuleOptions = [SchemaFilesMustOnlyExportSchemaOptions];
|
|
15
|
+
type MessageIds = "nonSchemaExport";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_FILE_PATTERN = "**/schema/**/*.schema.ts";
|
|
18
|
+
|
|
19
|
+
const optionSchema: JSONSchema4 = {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
filePattern: {
|
|
24
|
+
type: "string",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const schemaFilesMustOnlyExportSchemaRule = createRule<
|
|
30
|
+
RuleOptions,
|
|
31
|
+
MessageIds
|
|
32
|
+
>({
|
|
33
|
+
name: RULE_NAME,
|
|
34
|
+
meta: {
|
|
35
|
+
type: "suggestion",
|
|
36
|
+
docs: {
|
|
37
|
+
description:
|
|
38
|
+
"Restrict schema files to exporting only Drizzle schema artifacts (tables, schemas, relations, indices) and types.",
|
|
39
|
+
},
|
|
40
|
+
schema: [optionSchema],
|
|
41
|
+
messages: {
|
|
42
|
+
nonSchemaExport:
|
|
43
|
+
"Schema files may only export tables, schemas, relations, and types — move '{{name}}' elsewhere.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultOptions: [{ filePattern: DEFAULT_FILE_PATTERN }],
|
|
47
|
+
create(context, [options]) {
|
|
48
|
+
const filePattern = options.filePattern ?? DEFAULT_FILE_PATTERN;
|
|
49
|
+
|
|
50
|
+
if (!matchesAnyGlobPattern(context.filename, [filePattern])) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
ExportNamedDeclaration(node) {
|
|
56
|
+
if (node.source) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const declaration = node.declaration;
|
|
61
|
+
|
|
62
|
+
if (!declaration) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
declaration.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
68
|
+
declaration.type === AST_NODE_TYPES.TSInterfaceDeclaration
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (declaration.type === AST_NODE_TYPES.VariableDeclaration) {
|
|
74
|
+
for (const declarator of declaration.declarations) {
|
|
75
|
+
if (
|
|
76
|
+
declarator.init?.type === AST_NODE_TYPES.CallExpression &&
|
|
77
|
+
isSchemaBuilderCall(declarator.init)
|
|
78
|
+
) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
context.report({
|
|
83
|
+
node: declarator,
|
|
84
|
+
messageId: "nonSchemaExport",
|
|
85
|
+
data: { name: getDeclaratorName(declarator) ?? "<anonymous>" },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
context.report({
|
|
93
|
+
node: declaration,
|
|
94
|
+
messageId: "nonSchemaExport",
|
|
95
|
+
data: { name: getNamedDeclarationName(declaration) ?? "<anonymous>" },
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
ExportDefaultDeclaration(node) {
|
|
99
|
+
context.report({
|
|
100
|
+
node,
|
|
101
|
+
messageId: "nonSchemaExport",
|
|
102
|
+
data: { name: getDefaultExportName(node) ?? "default" },
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function getDeclaratorName(
|
|
110
|
+
declarator: TSESTree.VariableDeclarator
|
|
111
|
+
): string | null {
|
|
112
|
+
if (declarator.id.type === AST_NODE_TYPES.Identifier) {
|
|
113
|
+
return declarator.id.name;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getNamedDeclarationName(
|
|
120
|
+
declaration: NonNullable<TSESTree.ExportNamedDeclaration["declaration"]>
|
|
121
|
+
): string | null {
|
|
122
|
+
if (
|
|
123
|
+
"id" in declaration &&
|
|
124
|
+
declaration.id?.type === AST_NODE_TYPES.Identifier
|
|
125
|
+
) {
|
|
126
|
+
return declaration.id.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getDefaultExportName(
|
|
133
|
+
node: TSESTree.ExportDefaultDeclaration
|
|
134
|
+
): string | null {
|
|
135
|
+
const declaration = node.declaration;
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
"id" in declaration &&
|
|
139
|
+
declaration.id?.type === AST_NODE_TYPES.Identifier
|
|
140
|
+
) {
|
|
141
|
+
return declaration.id.name;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (declaration.type === AST_NODE_TYPES.Identifier) {
|
|
145
|
+
return declaration.name;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|