cvx-kit 0.1.4 → 0.1.6
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/CHANGELOG.md +31 -0
- package/README.md +32 -11
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/oxlint/architecture.d.mts +20 -0
- package/dist/oxlint/architecture.mjs +2 -0
- package/dist/oxlint/architecture.mjs.map +1 -0
- package/dist/oxlint/ast.d.mts +33 -0
- package/dist/oxlint/ast.mjs +2 -0
- package/dist/oxlint/ast.mjs.map +1 -0
- package/dist/oxlint/ownership.d.mts +62 -0
- package/dist/oxlint/ownership.mjs +2 -0
- package/dist/oxlint/ownership.mjs.map +1 -0
- package/dist/oxlint.d.mts +5 -3
- package/dist/oxlint.mjs +1 -1
- package/dist/oxlint.mjs.map +1 -1
- package/docs/conventions.md +49 -21
- package/docs/llms.md +3 -0
- package/docs/oxlint.md +146 -0
- package/docs/upgrading.md +32 -4
- package/package.json +5 -3
- package/scripts/test-packed-oxlint.mjs +61 -19
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,37 @@ are this file's matching section.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.6] - 2026-09-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Add five opt-in consumer Oxlint rules for project structure, root wiring,
|
|
20
|
+
application orchestration, internal function ownership, and domain file
|
|
21
|
+
responsibilities. The packaged plugin now includes 20 rules.
|
|
22
|
+
- Check colocated test presence, runtime import cycles, and shared-code ownership.
|
|
23
|
+
Export `checkArchitecture` for standalone checks without copying architecture
|
|
24
|
+
test suites into applications. These static checks do not replace behavior tests.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Resolve tsconfig aliases when checking consumer import boundaries and reject
|
|
29
|
+
migration dependencies from application code.
|
|
30
|
+
- Enforce documented ownership of domain implementations, table aggregation,
|
|
31
|
+
query side effects, and component boundaries.
|
|
32
|
+
|
|
33
|
+
## [0.1.5] - 2026-09-18
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Correct `cvx-kit/oxlint` to enforce the documented consumer `convex/` layout
|
|
38
|
+
rather than this repository's `src/` layout. Keep the six library-authoring
|
|
39
|
+
rules in repository tooling and publish 15 consumer rules, with coverage for
|
|
40
|
+
domain boundaries, auth builders, thin adapters, facade ownership, table
|
|
41
|
+
placement, vocabularies, bounded read syntax, and trigger-owned timestamps.
|
|
42
|
+
- Replace consumer `entryPoints`/`packageName` options with `convexDir`; document
|
|
43
|
+
migration, allowed exceptions, and the checks that still need runtime tests.
|
|
44
|
+
Validate the packaged plugin against consumer fixtures installed by npm and Bun.
|
|
45
|
+
|
|
15
46
|
## [0.1.4] - 2026-09-18
|
|
16
47
|
|
|
17
48
|
### Added
|
package/README.md
CHANGED
|
@@ -185,38 +185,59 @@ import approvals from 'cvx-kit/components/approvals/convex.config'
|
|
|
185
185
|
app.use(approvals)
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
##
|
|
188
|
+
## Application lint rules
|
|
189
189
|
|
|
190
|
-
The TypeScript
|
|
191
|
-
|
|
190
|
+
The TypeScript plugin `cvx-kit/oxlint` checks applications using the kit against
|
|
191
|
+
[`conventions.md`](./src/docs/conventions.md). It targets `convex/`, including
|
|
192
|
+
public adapters, domain modules, root facades, and locally authored components.
|
|
193
|
+
Load it in your application's `.oxlintrc.json` and select rules:
|
|
192
194
|
|
|
193
195
|
```json
|
|
194
196
|
{
|
|
195
197
|
"jsPlugins": [{ "name": "cvx", "specifier": "cvx-kit/oxlint" }],
|
|
196
198
|
"rules": {
|
|
199
|
+
"cvx/project-structure": "error",
|
|
200
|
+
"cvx/root-wiring-only": "error",
|
|
201
|
+
"cvx/application-orchestration": "error",
|
|
202
|
+
"cvx/internal-function-ownership": "error",
|
|
203
|
+
"cvx/domain-file-responsibilities": "error",
|
|
204
|
+
"cvx/no-raw-builders": "error",
|
|
205
|
+
"cvx/no-handwritten-references": "error",
|
|
206
|
+
"cvx/domain-import-boundaries": "error",
|
|
207
|
+
"cvx/public-functions-in-api": "error",
|
|
208
|
+
"cvx/thin-api-adapters": "error",
|
|
209
|
+
"cvx/root-facade-ownership": "error",
|
|
210
|
+
"cvx/no-inline-enums": "error",
|
|
211
|
+
"cvx/no-unbounded-reads": "error",
|
|
212
|
+
"cvx/no-manual-timestamps": "error",
|
|
197
213
|
"cvx/component-boundaries": "error",
|
|
198
214
|
"cvx/no-component-env": "error",
|
|
199
215
|
"cvx/schema-file-boundaries": "error",
|
|
200
|
-
"cvx/no-internal-reexports":
|
|
216
|
+
"cvx/no-internal-reexports": "error",
|
|
201
217
|
"cvx/public-api-first": "error",
|
|
202
218
|
"cvx/named-private-helpers": "error"
|
|
203
219
|
}
|
|
204
220
|
}
|
|
205
221
|
```
|
|
206
222
|
|
|
207
|
-
With Vite+, put those fields under `lint` in `vite.config.ts`.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
223
|
+
With Vite+, put those fields under `lint` in `vite.config.ts`. Each rule accepts
|
|
224
|
+
`{ "convexDir": "path/to/convex" }` when your backend directory differs from
|
|
225
|
+
`convex/`. Generated files and tests are excluded from source-code rules. `project-structure`
|
|
226
|
+
inspects the whole backend for directory ownership, colocated runnable tests, and
|
|
227
|
+
module cycles. Run Oxlint on the whole backend, including `convex.config.ts`;
|
|
228
|
+
changed-file-only lint cannot guarantee a project scan. No application-owned
|
|
229
|
+
architecture test framework is required. The plugin also exports `checkArchitecture()`
|
|
230
|
+
for tools that need project diagnostics directly, named `rules`, and `RuleName`. Verified with
|
|
231
|
+
Oxlint 1.79.0 and Vite+ 0.3.0. See [rule coverage and limitations](./src/docs/oxlint.md)
|
|
232
|
+
and the [migration from the initial 0.1.4 plugin](./src/docs/upgrading.md).
|
|
214
233
|
|
|
215
234
|
Repository checks and the enforced library rules are documented in
|
|
216
235
|
[`tools/oxlint/README.md`](./tools/oxlint/README.md). Run `bun run lint` for
|
|
217
236
|
anti-slop and architecture checks, `bun run test:lint` to test the custom
|
|
218
237
|
rules, and `bun run check` for the full Vite+ lint and formatting check.
|
|
219
238
|
|
|
239
|
+
## Component authoring conventions
|
|
240
|
+
|
|
220
241
|
This package follows the official Convex component template
|
|
221
242
|
(`get-convex/templates/template-component`; see
|
|
222
243
|
https://docs.convex.dev/components/authoring):
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{components as e}from"./_generated/api.mjs";import{mutation as t,query as n}from"./_generated/server.mjs";import{APPROVAL_AUDIT_RETENTION_CATEGORY as r,MAX_APPROVAL_AUDIT_RETENTION_DAYS as i}from"./constants.mjs";import{z as a}from"zod";import{
|
|
1
|
+
import{components as e}from"./_generated/api.mjs";import{mutation as t,query as n}from"./_generated/server.mjs";import{APPROVAL_AUDIT_RETENTION_CATEGORY as r,MAX_APPROVAL_AUDIT_RETENTION_DAYS as i}from"./constants.mjs";import{z as a}from"zod";import{AuditLog as o}from"convex-audit-log";import{v as s}from"convex/values";const c=new o(e.auditLog,{samplingEnabled:!1}),l=n({args:{resourceType:s.string(),resourceRef:s.string(),limit:s.optional(s.number())},returns:s.array(s.object({action:s.string(),actorRef:s.optional(s.string()),correlationKey:s.string(),transition:s.string()})),handler:async(e,t)=>{let n=Math.max(1,Math.min(Math.trunc(t.limit??100),100)),r=await c.queryByResource(e,{resourceType:t.resourceType,resourceId:t.resourceRef,limit:n}),i=new Map;for(let e of r){let t=h(e);t&&i.set(t.correlationKey,t)}return[...i.values()]}}),u=t({args:{olderThanDays:s.number(),batchSize:s.number()},returns:s.number(),handler:(e,t)=>{let n=p(t.olderThanDays,1,i,`olderThanDays`),a=p(t.batchSize,1,100,`batchSize`);return c.cleanup(e,{olderThanDays:n,batchSize:a,retentionCategory:r})}});function d(e){return{action:e.action,actorId:e.actorRef,resourceType:e.resourceType,resourceId:e.resourceRef,severity:e.severity??`info`,tags:[`approvals`],retentionCategory:r,metadata:{correlationKey:`${e.runId}:${e.transition}`,runId:e.runId,transition:e.transition}}}async function f(e,t){await c.log(e,d(t))}function p(e,t,n,r){if(!Number.isInteger(e)||e<t||e>n)throw Error(`${r} must be an integer between ${t} and ${n}`);return e}const m=a.object({action:a.string(),actorId:a.string().optional().catch(void 0),metadata:a.object({correlationKey:a.string(),transition:a.string()})});function h(e){let t=m.safeParse(e);if(!t.success)return null;let{action:n,actorId:r,metadata:i}=t.data;return{action:n,actorRef:r,...i}}export{c as approvalAudit,d as approvalAuditEvent,u as cleanup,l as history,f as logApprovalTransition};
|
|
2
2
|
//# sourceMappingURL=audit.mjs.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ResolverFactory } from "oxc-resolver";
|
|
2
|
+
//#region src/oxlint/architecture.d.ts
|
|
3
|
+
interface ArchitectureOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
convexDir?: string;
|
|
6
|
+
}
|
|
7
|
+
interface ArchitectureDiagnostic {
|
|
8
|
+
code: 'missing-root' | 'root-directory' | 'tests' | 'test-location' | 'parse' | 'cycle' | 'symlink' | 'domain-entry' | 'shared-owner';
|
|
9
|
+
file: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
/** Full-project checks, independent of the consumer's test runner. No persistent cache. */
|
|
13
|
+
declare function inspectArchitecture(options?: ArchitectureOptions): ArchitectureDiagnostic[];
|
|
14
|
+
declare function createResolver(): ResolverFactory;
|
|
15
|
+
declare function resolveImport(resolver: ResolverFactory, filename: string, source: string): string | null;
|
|
16
|
+
/** The lint rule runs the project scan on one deterministic source file. */
|
|
17
|
+
declare function architectureAnchor(root: string): string | null;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { ArchitectureDiagnostic, ArchitectureOptions, architectureAnchor, createResolver, inspectArchitecture, resolveImport };
|
|
20
|
+
//# sourceMappingURL=architecture.d.mts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{dirname as e,relative as t,resolve as n,sep as r}from"node:path";import{existsSync as i,readFileSync as a,readdirSync as o,realpathSync as s}from"node:fs";import{Visitor as c,parseSync as l}from"oxc-parser";import{ResolverFactory as u}from"oxc-resolver";function d(o={}){let c=n(o.cwd??process.cwd(),o.convexDir??`convex`),u=i(c)?s(c):c,d=[];if(!i(u))return[{code:`missing-root`,file:`.`,message:`Convex directory does not exist: ${u}`}];let m=h(u,d),y=new Set,w=new Set,T=new Map,E=new Map,D=f();for(let i of m){let o=n(u,i),s=l(o,a(o,`utf8`));if(s.errors.length){d.push({code:`parse`,file:i,message:`Cannot validate ${i}: ${s.errors[0].message}`});continue}if(g(i)){i.includes(`__tests__/`)?e(i).endsWith(`__tests__`)&&v(s.program)&&w.add(e(e(i))):d.push({code:`test-location`,file:i,message:`Place tests in a sibling __tests__/ directory.`});continue}if(i.split(`/`).includes(`__tests__`))continue;/^domain\/[^/]+$/.test(i)&&i!==`domain/table.ts`&&d.push({code:`domain-entry`,file:i,message:`Domain source belongs in domain/<module>/ or domain/shared/. Only domain/table.ts assembles module tables.`}),_(s.program)&&i!==`domain/table.ts`&&y.add(e(i));let c=x(i)??``,f=new Set;E.set(i,f);for(let e of b(s.program)){let n=p(D,o,e);if(!n)continue;let i=t(u,n).split(r).join(`/`);f.add(i);let a=x(i);if(c&&a&&c!==a){let e=T.get(c)??new Set;T.set(c,e),e.add(a)}}}for(let e of y)w.has(e)||d.push({code:`tests`,file:e,message:`${e}/__tests__/ must contain a .test or .spec file with a nonempty, non-skipped test. Parent or child suites do not count.`});d.push(...C(E));for(let e of S(T))d.push({code:`cycle`,file:e[0],message:`Module dependency cycle: ${e.join(` → `)}.`});return d.sort((e,t)=>e.file.localeCompare(t.file)||e.code.localeCompare(t.code))}function f(){return new u({tsconfig:`auto`,extensions:[`.ts`,`.tsx`,`.mts`,`.cts`,`.js`,`.jsx`,`.mjs`,`.cjs`,`.json`],extensionAlias:{".js":[`.ts`,`.tsx`,`.js`],".mjs":[`.mts`,`.mjs`],".cjs":[`.cts`,`.cjs`]},conditionNames:[`types`,`import`,`node`,`default`],symlinks:!0})}function p(t,r,i){return t.resolveFileSync(r,i).path||(i.startsWith(`.`)?n(e(r),i):null)}function m(e){if(!i(e))return null;if(i(n(e,`convex.config.ts`)))return s(n(e,`convex.config.ts`));e=s(e);for(let t of h(e,[]))if(!t.includes(`__tests__/`)&&!g(t))return n(e,t);return null}function*h(e,i){let a=new Set([`_generated`,`__tests__`,`api`,`domain`,`application`,`migrations`,`components`]);function*s(c){for(let l of o(c,{withFileTypes:!0}).sort((e,t)=>e.name.localeCompare(t.name))){let o=n(c,l.name),u=t(e,o).split(r).join(`/`);if(l.isSymbolicLink()){i.push({code:`symlink`,file:u,message:`Keep application source inside its owning directory; symlinked files and folders are not allowed.`});continue}l.isDirectory()?(c===e&&!a.has(l.name)&&i.push({code:`root-directory`,file:u,message:`Root folder ${l.name}/ is not allowed. Use api/, domain/, application/, migrations/, components/, __tests__/, or _generated/.`}),l.name!==`_generated`&&l.name!==`node_modules`&&!l.name.startsWith(`.`)&&(yield*s(o))):/\.[cm]?[jt]sx?$/.test(l.name)&&!/\.d\.[cm]?ts$/.test(l.name)&&(yield u)}}yield*s(e)}function g(e){return/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(e)}function _(e){return e.body.some(e=>{let t=e.type===`ExportNamedDeclaration`?e.declaration:e;return t&&`declare`in t&&t.declare||e.type===`ImportDeclaration`||e.type===`EmptyStatement`?!1:e.type===`ExportNamedDeclaration`?e.exportKind===`type`||!e.declaration?!1:![`TSInterfaceDeclaration`,`TSTypeAliasDeclaration`,`TSDeclareFunction`].includes(e.declaration.type):![`TSInterfaceDeclaration`,`TSTypeAliasDeclaration`,`TSDeclareFunction`,`ExportAllDeclaration`].includes(e.type)})}function v(e){let t=new Map([[`test`,`test`],[`it`,`it`],[`describe`,`describe`],[`suite`,`suite`]]);for(let n of e.body){let e=n.type===`ExportNamedDeclaration`?n.declaration:n;if(e?.type===`FunctionDeclaration`&&e.id&&t.delete(e.id.name),e?.type===`VariableDeclaration`)for(let n of e.declarations)n.id.type===`Identifier`&&t.delete(n.id.name);if(n.type===`ImportDeclaration`)for(let e of n.specifiers)t.delete(e.local.name),[`vitest`,`vite-plus/test`,`bun:test`,`node:test`,`@jest/globals`].includes(n.source.value)&&(e.type===`ImportNamespaceSpecifier`?t.set(e.local.name,`*`):e.type===`ImportDefaultSpecifier`?t.set(e.local.name,`test`):e.imported.type===`Identifier`&&t.set(e.local.name,e.imported.name))}let n=[];new c({CallExpression(e){n.push(e)}}).visit(e);let r=n.filter(e=>{let n=y(e.callee,t);return[`test`,`it`,`describe`,`suite`].includes(n[0])&&(n.some(e=>[`skip`,`todo`,`skipIf`,`runIf`].includes(e))||e.arguments.some(e=>e.type===`ObjectExpression`&&e.properties.some(e=>e.type===`Property`&&[`skip`,`todo`].includes(e.key.type===`Identifier`?e.key.name:e.key.type===`Literal`?String(e.key.value):``)&&(e.value.type!==`Literal`||e.value.value!==!1))))});return n.some(e=>{let n=y(e.callee,t);return![`test`,`it`].includes(n[0])||r.some(t=>e.start>=t.start&&e.end<=t.end)?!1:e.arguments.some(e=>(e.type===`ArrowFunctionExpression`||e.type===`FunctionExpression`)&&e.body&&(e.body.type!==`BlockStatement`||e.body.body.some(e=>e.type!==`EmptyStatement`)))})}function y(e,t){if(e.type===`Identifier`){let n=t.get(e.name);return n?[n]:[]}if(e.type===`CallExpression`)return y(e.callee,t);if(e.type===`MemberExpression`){let n=y(e.object,t),r=!e.computed&&e.property.type===`Identifier`?e.property.name:e.property.type===`Literal`?String(e.property.value):``;return!n.length||!r?[]:n[0]===`*`?[r]:[...n,r]}return[]}function b(e){let t=[];return new c({ImportDeclaration(e){e.importKind!==`type`&&(e.specifiers.length===0||e.specifiers.some(e=>e.type!==`ImportSpecifier`||e.importKind!==`type`))&&t.push(e.source.value)},ExportNamedDeclaration(e){e.source&&e.exportKind!==`type`&&e.specifiers.some(e=>e.exportKind!==`type`)&&t.push(e.source.value)},ExportAllDeclaration(e){e.exportKind!==`type`&&t.push(e.source.value)},ImportExpression(e){e.source.type===`Literal`&&String(e.source.value)===e.source.value&&t.push(String(e.source.value))}}).visit(e),t}function x(e){let t=e.split(`/`);return t[0]===`domain`&&t.length>=3&&t[1]!==`__tests__`?`domain/${t[1]}`:null}function S(e){let t=new Set,n=[],r=[];function i(a){let o=n.indexOf(a);if(o>=0){r.push([...n.slice(o),a]);return}if(!t.has(a)){n.push(a);for(let t of e.get(a)??[])i(t);n.pop(),t.add(a)}}for(let t of e.keys())i(t);return r}function C(e){let t=new Map;for(let[n,r]of e){let i=x(n)??``;if(!i||i===`domain/shared`)continue;let a=new Set;function o(n){if(!n.startsWith(`domain/shared/`)||a.has(n))return;a.add(n);let r=t.get(n)??new Set;r.add(i),t.set(n,r);for(let t of e.get(n)??[])o(t)}for(let e of r)o(e)}let n=[];for(let[e,r]of t)r.size===1&&n.push({code:`shared-owner`,file:e,message:`This shared implementation is used only by ${[...r][0]}. Move it into that module until another module needs it.`});return n}export{m as architectureAnchor,f as createResolver,d as inspectArchitecture,p as resolveImport};
|
|
2
|
+
//# sourceMappingURL=architecture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"architecture.mjs","names":[],"sources":["../../src/oxlint/architecture.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { dirname, relative, resolve, sep } from 'node:path'\nimport { parseSync, Visitor, type Program, type Node, type CallExpression } from 'oxc-parser'\nimport { ResolverFactory } from 'oxc-resolver'\n\nexport interface ArchitectureOptions {\n\tcwd?: string\n\tconvexDir?: string\n}\nexport interface ArchitectureDiagnostic {\n\tcode:\n\t\t| 'missing-root'\n\t\t| 'root-directory'\n\t\t| 'tests'\n\t\t| 'test-location'\n\t\t| 'parse'\n\t\t| 'cycle'\n\t\t| 'symlink'\n\t\t| 'domain-entry'\n\t\t| 'shared-owner'\n\tfile: string\n\tmessage: string\n}\n\n/** Full-project checks, independent of the consumer's test runner. No persistent cache. */\nexport function inspectArchitecture(options: ArchitectureOptions = {}): ArchitectureDiagnostic[] {\n\tconst requestedRoot = resolve(options.cwd ?? process.cwd(), options.convexDir ?? 'convex')\n\tconst root = existsSync(requestedRoot) ? realpathSync(requestedRoot) : requestedRoot\n\tconst diagnostics: ArchitectureDiagnostic[] = []\n\tif (!existsSync(root))\n\t\treturn [\n\t\t\t{ code: 'missing-root', file: '.', message: `Convex directory does not exist: ${root}` },\n\t\t]\n\tconst files = sourceFiles(root, diagnostics)\n\tconst required = new Set<string>()\n\tconst tested = new Set<string>()\n\tconst graph = new Map<string, Set<string>>()\n\tconst dependencies = new Map<string, Set<string>>()\n\tconst resolver = createResolver()\n\tfor (const file of files) {\n\t\tconst filename = resolve(root, file)\n\t\tconst parsed = parseSync(filename, readFileSync(filename, 'utf8'))\n\t\tif (parsed.errors.length) {\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: 'parse',\n\t\t\t\tfile,\n\t\t\t\tmessage: `Cannot validate ${file}: ${parsed.errors[0].message}`,\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\t\tif (testFile(file)) {\n\t\t\tif (!file.includes('__tests__/'))\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tcode: 'test-location',\n\t\t\t\t\tfile,\n\t\t\t\t\tmessage: 'Place tests in a sibling __tests__/ directory.',\n\t\t\t\t})\n\t\t\telse if (dirname(file).endsWith('__tests__') && hasRunnableTest(parsed.program))\n\t\t\t\ttested.add(dirname(dirname(file)))\n\t\t\tcontinue\n\t\t}\n\t\tif (file.split('/').includes('__tests__')) continue\n\t\tif (/^domain\\/[^/]+$/.test(file) && file !== 'domain/table.ts')\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: 'domain-entry',\n\t\t\t\tfile,\n\t\t\t\tmessage:\n\t\t\t\t\t'Domain source belongs in domain/<module>/ or domain/shared/. Only domain/table.ts assembles module tables.',\n\t\t\t})\n\t\tif (executable(parsed.program) && file !== 'domain/table.ts') required.add(dirname(file))\n\t\tconst owner = moduleOwner(file) ?? ''\n\t\tconst targets = new Set<string>()\n\t\tdependencies.set(file, targets)\n\t\tfor (const source of runtimeImports(parsed.program)) {\n\t\t\tconst target = resolveImport(resolver, filename, source)\n\t\t\tif (!target) continue\n\t\t\tconst targetFile = relative(root, target).split(sep).join('/')\n\t\t\ttargets.add(targetFile)\n\t\t\tconst targetOwner = moduleOwner(targetFile)\n\t\t\tif (owner && targetOwner && owner !== targetOwner) {\n\t\t\t\tconst edges = graph.get(owner) ?? new Set<string>()\n\t\t\t\tgraph.set(owner, edges)\n\t\t\t\tedges.add(targetOwner)\n\t\t\t}\n\t\t}\n\t}\n\tfor (const directory of required) {\n\t\tif (!tested.has(directory))\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: 'tests',\n\t\t\t\tfile: directory,\n\t\t\t\tmessage: `${directory}/__tests__/ must contain a .test or .spec file with a nonempty, non-skipped test. Parent or child suites do not count.`,\n\t\t\t})\n\t}\n\tdiagnostics.push(...sharedOwnership(dependencies))\n\tfor (const cycle of cycles(graph))\n\t\tdiagnostics.push({\n\t\t\tcode: 'cycle',\n\t\t\tfile: cycle[0],\n\t\t\tmessage: `Module dependency cycle: ${cycle.join(' → ')}.`,\n\t\t})\n\treturn diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code))\n}\n\nexport function createResolver() {\n\treturn new ResolverFactory({\n\t\ttsconfig: 'auto',\n\t\textensions: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json'],\n\t\textensionAlias: {\n\t\t\t'.js': ['.ts', '.tsx', '.js'],\n\t\t\t'.mjs': ['.mts', '.mjs'],\n\t\t\t'.cjs': ['.cts', '.cjs'],\n\t\t},\n\t\tconditionNames: ['types', 'import', 'node', 'default'],\n\t\tsymlinks: true,\n\t})\n}\n\nexport function resolveImport(resolver: ResolverFactory, filename: string, source: string) {\n\tconst found = resolver.resolveFileSync(filename, source).path\n\tif (found) return found\n\t// Keep path checks useful in unsaved/editor fixtures and before Convex codegen.\n\treturn source.startsWith('.') ? resolve(dirname(filename), source) : null\n}\n\n/** The lint rule runs the project scan on one deterministic source file. */\nexport function architectureAnchor(root: string) {\n\tif (!existsSync(root)) return null\n\tif (existsSync(resolve(root, 'convex.config.ts')))\n\t\treturn realpathSync(resolve(root, 'convex.config.ts'))\n\troot = realpathSync(root)\n\tfor (const file of sourceFiles(root, []))\n\t\tif (!file.includes('__tests__/') && !testFile(file)) return resolve(root, file)\n\treturn null\n}\n\nfunction* sourceFiles(root: string, diagnostics: ArchitectureDiagnostic[]): Generator<string> {\n\tconst allowed = new Set([\n\t\t'_generated',\n\t\t'__tests__',\n\t\t'api',\n\t\t'domain',\n\t\t'application',\n\t\t'migrations',\n\t\t'components',\n\t])\n\tfunction* scan(directory: string): Generator<string> {\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t)) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst file = relative(root, path).split(sep).join('/')\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tcode: 'symlink',\n\t\t\t\t\tfile,\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Keep application source inside its owning directory; symlinked files and folders are not allowed.',\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tif (directory === root && !allowed.has(entry.name))\n\t\t\t\t\tdiagnostics.push({\n\t\t\t\t\t\tcode: 'root-directory',\n\t\t\t\t\t\tfile,\n\t\t\t\t\t\tmessage: `Root folder ${entry.name}/ is not allowed. Use api/, domain/, application/, migrations/, components/, __tests__/, or _generated/.`,\n\t\t\t\t\t})\n\t\t\t\tif (\n\t\t\t\t\tentry.name !== '_generated' &&\n\t\t\t\t\tentry.name !== 'node_modules' &&\n\t\t\t\t\t!entry.name.startsWith('.')\n\t\t\t\t)\n\t\t\t\t\tyield* scan(path)\n\t\t\t} else if (/\\.[cm]?[jt]sx?$/.test(entry.name) && !/\\.d\\.[cm]?ts$/.test(entry.name)) yield file\n\t\t}\n\t}\n\tyield* scan(root)\n}\n\nfunction testFile(file: string) {\n\treturn /\\.(?:test|spec)\\.[cm]?[jt]sx?$/.test(file)\n}\n\nfunction executable(program: Program) {\n\treturn program.body.some((statement) => {\n\t\tconst declaration =\n\t\t\tstatement.type === 'ExportNamedDeclaration' ? statement.declaration : statement\n\t\tif (declaration && 'declare' in declaration && declaration.declare) return false\n\t\tif (statement.type === 'ImportDeclaration' || statement.type === 'EmptyStatement') return false\n\t\tif (statement.type === 'ExportNamedDeclaration') {\n\t\t\tif (statement.exportKind === 'type' || !statement.declaration) return false\n\t\t\treturn !['TSInterfaceDeclaration', 'TSTypeAliasDeclaration', 'TSDeclareFunction'].includes(\n\t\t\t\tstatement.declaration.type,\n\t\t\t)\n\t\t}\n\t\treturn ![\n\t\t\t'TSInterfaceDeclaration',\n\t\t\t'TSTypeAliasDeclaration',\n\t\t\t'TSDeclareFunction',\n\t\t\t'ExportAllDeclaration',\n\t\t].includes(statement.type)\n\t})\n}\n\nfunction hasRunnableTest(program: Program) {\n\tconst names = new Map([\n\t\t['test', 'test'],\n\t\t['it', 'it'],\n\t\t['describe', 'describe'],\n\t\t['suite', 'suite'],\n\t])\n\tfor (const statement of program.body) {\n\t\tconst declaration =\n\t\t\tstatement.type === 'ExportNamedDeclaration' ? statement.declaration : statement\n\t\tif (declaration?.type === 'FunctionDeclaration' && declaration.id)\n\t\t\tnames.delete(declaration.id.name)\n\t\tif (declaration?.type === 'VariableDeclaration')\n\t\t\tfor (const item of declaration.declarations) {\n\t\t\t\tif (item.id.type === 'Identifier') names.delete(item.id.name)\n\t\t\t}\n\t\tif (statement.type !== 'ImportDeclaration') continue\n\t\tfor (const specifier of statement.specifiers) {\n\t\t\tnames.delete(specifier.local.name)\n\t\t\tif (\n\t\t\t\t!['vitest', 'vite-plus/test', 'bun:test', 'node:test', '@jest/globals'].includes(\n\t\t\t\t\tstatement.source.value,\n\t\t\t\t)\n\t\t\t)\n\t\t\t\tcontinue\n\t\t\tif (specifier.type === 'ImportNamespaceSpecifier') names.set(specifier.local.name, '*')\n\t\t\telse if (specifier.type === 'ImportDefaultSpecifier') names.set(specifier.local.name, 'test')\n\t\t\telse if (specifier.imported.type === 'Identifier')\n\t\t\t\tnames.set(specifier.local.name, specifier.imported.name)\n\t\t}\n\t}\n\tconst calls: CallExpression[] = []\n\tnew Visitor({\n\t\tCallExpression(node) {\n\t\t\tcalls.push(node)\n\t\t},\n\t}).visit(program)\n\tconst skipped = calls.filter((node) => {\n\t\tconst parts = testCallee(node.callee, names)\n\t\treturn (\n\t\t\t['test', 'it', 'describe', 'suite'].includes(parts[0]) &&\n\t\t\t(parts.some((part) => ['skip', 'todo', 'skipIf', 'runIf'].includes(part)) ||\n\t\t\t\tnode.arguments.some(\n\t\t\t\t\t(argument) =>\n\t\t\t\t\t\targument.type === 'ObjectExpression' &&\n\t\t\t\t\t\targument.properties.some(\n\t\t\t\t\t\t\t(property) =>\n\t\t\t\t\t\t\t\tproperty.type === 'Property' &&\n\t\t\t\t\t\t\t\t['skip', 'todo'].includes(\n\t\t\t\t\t\t\t\t\tproperty.key.type === 'Identifier'\n\t\t\t\t\t\t\t\t\t\t? property.key.name\n\t\t\t\t\t\t\t\t\t\t: property.key.type === 'Literal'\n\t\t\t\t\t\t\t\t\t\t\t? String(property.key.value)\n\t\t\t\t\t\t\t\t\t\t\t: '',\n\t\t\t\t\t\t\t\t) &&\n\t\t\t\t\t\t\t\t!(property.value.type === 'Literal' && property.value.value === false),\n\t\t\t\t\t\t),\n\t\t\t\t))\n\t\t)\n\t})\n\treturn calls.some((node) => {\n\t\tconst parts = testCallee(node.callee, names)\n\t\tif (\n\t\t\t!['test', 'it'].includes(parts[0]) ||\n\t\t\tskipped.some((skip) => node.start >= skip.start && node.end <= skip.end)\n\t\t)\n\t\t\treturn false\n\t\t// each(...) is a factory; only the outer call has the test callback.\n\t\treturn node.arguments.some(\n\t\t\t(argument) =>\n\t\t\t\t(argument.type === 'ArrowFunctionExpression' || argument.type === 'FunctionExpression') &&\n\t\t\t\targument.body &&\n\t\t\t\t(argument.body.type !== 'BlockStatement' ||\n\t\t\t\t\targument.body.body.some((statement) => statement.type !== 'EmptyStatement')),\n\t\t)\n\t})\n}\n\nfunction testCallee(node: Node, names: Map<string, string>): string[] {\n\tif (node.type === 'Identifier') {\n\t\tconst name = names.get(node.name)\n\t\treturn name ? [name] : []\n\t}\n\tif (node.type === 'CallExpression') return testCallee(node.callee, names)\n\tif (node.type === 'MemberExpression') {\n\t\tconst base = testCallee(node.object, names)\n\t\tconst property =\n\t\t\t!node.computed && node.property.type === 'Identifier'\n\t\t\t\t? node.property.name\n\t\t\t\t: node.property.type === 'Literal'\n\t\t\t\t\t? String(node.property.value)\n\t\t\t\t\t: ''\n\t\tif (!base.length || !property) return []\n\t\treturn base[0] === '*' ? [property] : [...base, property]\n\t}\n\treturn []\n}\n\nfunction runtimeImports(program: Program) {\n\tconst imports: string[] = []\n\tnew Visitor({\n\t\tImportDeclaration(node) {\n\t\t\tif (\n\t\t\t\tnode.importKind !== 'type' &&\n\t\t\t\t(node.specifiers.length === 0 ||\n\t\t\t\t\tnode.specifiers.some((s) => s.type !== 'ImportSpecifier' || s.importKind !== 'type'))\n\t\t\t)\n\t\t\t\timports.push(node.source.value)\n\t\t},\n\t\tExportNamedDeclaration(node) {\n\t\t\tif (\n\t\t\t\tnode.source &&\n\t\t\t\tnode.exportKind !== 'type' &&\n\t\t\t\tnode.specifiers.some((specifier) => specifier.exportKind !== 'type')\n\t\t\t)\n\t\t\t\timports.push(node.source.value)\n\t\t},\n\t\tExportAllDeclaration(node) {\n\t\t\tif (node.exportKind !== 'type') imports.push(node.source.value)\n\t\t},\n\t\tImportExpression(node) {\n\t\t\tif (node.source.type === 'Literal' && String(node.source.value) === node.source.value)\n\t\t\t\timports.push(String(node.source.value))\n\t\t},\n\t}).visit(program)\n\treturn imports\n}\n\nfunction moduleOwner(file: string) {\n\tconst parts = file.split('/')\n\treturn parts[0] === 'domain' && parts.length >= 3 && parts[1] !== '__tests__'\n\t\t? `domain/${parts[1]}`\n\t\t: null\n}\n\nfunction cycles(graph: Map<string, Set<string>>) {\n\tconst completed = new Set<string>()\n\tconst stack: string[] = []\n\tconst found: string[][] = []\n\tfunction visit(owner: string) {\n\t\tconst index = stack.indexOf(owner)\n\t\tif (index >= 0) {\n\t\t\tfound.push([...stack.slice(index), owner])\n\t\t\treturn\n\t\t}\n\t\tif (completed.has(owner)) return\n\t\tstack.push(owner)\n\t\tfor (const dependency of graph.get(owner) ?? []) visit(dependency)\n\t\tstack.pop()\n\t\tcompleted.add(owner)\n\t}\n\tfor (const owner of graph.keys()) visit(owner)\n\treturn found\n}\n\nfunction sharedOwnership(dependencies: Map<string, Set<string>>): ArchitectureDiagnostic[] {\n\tconst consumers = new Map<string, Set<string>>()\n\tfor (const [file, targets] of dependencies) {\n\t\tconst owner = moduleOwner(file) ?? ''\n\t\tif (!owner || owner === 'domain/shared') continue\n\t\tconst visited = new Set<string>()\n\t\tfunction visit(target: string) {\n\t\t\tif (!target.startsWith('domain/shared/') || visited.has(target)) return\n\t\t\tvisited.add(target)\n\t\t\tconst owners = consumers.get(target) ?? new Set<string>()\n\t\t\towners.add(owner)\n\t\t\tconsumers.set(target, owners)\n\t\t\tfor (const next of dependencies.get(target) ?? []) visit(next)\n\t\t}\n\t\tfor (const target of targets) visit(target)\n\t}\n\tconst diagnostics: ArchitectureDiagnostic[] = []\n\tfor (const [file, owners] of consumers) {\n\t\tif (owners.size === 1)\n\t\t\tdiagnostics.push({\n\t\t\t\tcode: 'shared-owner',\n\t\t\t\tfile,\n\t\t\t\tmessage: `This shared implementation is used only by ${[...owners][0]}. Move it into that module until another module needs it.`,\n\t\t\t})\n\t}\n\treturn diagnostics\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Context, ESTree, RuleMeta, Visitor } from "@oxlint/plugins";
|
|
3
|
+
//#region src/oxlint/ast.d.ts
|
|
4
|
+
declare const consumerOptions: z.ZodObject<{
|
|
5
|
+
convexDir: z.ZodDefault<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
declare function appFile(context: Context, filename?: string): string | null;
|
|
8
|
+
declare function imported(context: Context, node: ESTree.Node): {
|
|
9
|
+
source: string;
|
|
10
|
+
name: string;
|
|
11
|
+
} | null;
|
|
12
|
+
declare function sourceVisitors(context: Context, check: (node: ESTree.Node, source: string) => void): Visitor;
|
|
13
|
+
declare function isFunctionsImport(context: Context, source: string): boolean;
|
|
14
|
+
declare function typeOnlySource(node: ESTree.Node): boolean;
|
|
15
|
+
declare function isDbAccess(node: ESTree.CallExpression): boolean;
|
|
16
|
+
declare function isExternalCall(context: Context, node: ESTree.CallExpression): boolean;
|
|
17
|
+
declare function containsComponentReference(context: Context, node: ESTree.Node): boolean;
|
|
18
|
+
declare function rootMember(node: ESTree.Node): ESTree.Node;
|
|
19
|
+
declare function memberPath(node: ESTree.Node): string[];
|
|
20
|
+
declare function isTableChain(node: ESTree.Node): boolean;
|
|
21
|
+
declare function isDbWrite(node: ESTree.CallExpression): boolean;
|
|
22
|
+
declare function hasCall(node: ESTree.Node, name: string): boolean;
|
|
23
|
+
declare function metadata(description: string, messages: Record<string, string>): RuleMeta;
|
|
24
|
+
declare function componentRoot(context: Context, filename: string): string | null;
|
|
25
|
+
declare function staticString(node: ESTree.Node | null | undefined): string | null;
|
|
26
|
+
declare function propertyName(node: ESTree.Node): string | null;
|
|
27
|
+
declare function binding(context: Context, node: ESTree.Node): import("@oxlint/plugins").Variable | null;
|
|
28
|
+
declare function exportedNames(program: ESTree.Program): Set<string>;
|
|
29
|
+
declare function exportName(node: ESTree.Node): string | null;
|
|
30
|
+
declare function delegatingBody(node: ESTree.Node): boolean;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { appFile, binding, componentRoot, consumerOptions, containsComponentReference, delegatingBody, exportName, exportedNames, hasCall, imported, isDbAccess, isDbWrite, isExternalCall, isFunctionsImport, isTableChain, memberPath, metadata, propertyName, rootMember, sourceVisitors, staticString, typeOnlySource };
|
|
33
|
+
//# sourceMappingURL=ast.d.mts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createResolver as e,resolveImport as t}from"./architecture.mjs";import{z as n}from"zod";import{dirname as r,relative as i,resolve as a,sep as o}from"node:path";import{existsSync as s,realpathSync as c}from"node:fs";const l=n.object({convexDir:n.string().min(1).default(`convex`)});function u(e,t=e.filename){let n=a(e.cwd,l.parse(e.options[0]??{}).convexDir),r=s(n)?c(n):n,u=i(r,s(t)?c(t):t).split(o).join(`/`);return u===`..`||u.startsWith(`../`)||u.startsWith(`/`)?null:u}function d(e,t){if(t.type===`CallExpression`)return d(e,t.callee);if(t.type===`MemberExpression`){let n=d(e,t.object),r=E(t);return n&&r&&[`*`,`z`,`default`].includes(n.name)?{source:n.source,name:r}:null}if(t.type!==`Identifier`)return null;for(let n of D(e,t)?.defs??[]){if(n.type!==`ImportBinding`||n.parent?.type!==`ImportDeclaration`||n.parent.importKind===`type`)continue;let e=n.node;if(e.type!==`ImportSpecifier`||e.importKind!==`type`)return{source:n.parent.source.value,name:e.type===`ImportSpecifier`?k(e.imported)??``:e.type===`ImportDefaultSpecifier`?`default`:`*`}}return null}function f(e,t){function n(e,n){let r=T(n);r&&t(e,r)}return{ImportDeclaration(e){n(e,e.source)},ExportNamedDeclaration(e){n(e,e.source)},ExportAllDeclaration(e){n(e,e.source)},ImportExpression(e){n(e,e.source)},TSImportType(e){n(e,e.source)},CallExpression(t){t.callee.type===`Identifier`&&t.callee.name===`require`&&!D(e,t.callee)?.defs.length&&n(t,t.arguments[0])}}}function p(n,i){let o=i.startsWith(`.`)?a(r(n.filename),i):t(e(),n.filename,i);return o!==null&&u(n,o)?.replace(/\.[cm]?[jt]s$/,``)===`functions`}function m(e){return e.type===`TSImportType`||e.type===`ImportDeclaration`&&(e.importKind===`type`||e.specifiers.length>0&&e.specifiers.every(e=>e.type===`ImportSpecifier`&&e.importKind===`type`))||(e.type===`ExportNamedDeclaration`||e.type===`ExportAllDeclaration`)&&e.exportKind===`type`}function h(e){return e.callee.type===`MemberExpression`&&e.callee.object.type===`MemberExpression`&&E(e.callee.object)===`db`}function g(e,t){let n=d(e,t.callee);return n&&/^(?:node:)?(?:https?|net|tls|fs)(?:\/|$)|^(?:undici|axios)(?:\/|$)/.test(n.source)?!0:t.callee.type===`Identifier`&&t.callee.name===`fetch`&&!D(e,t.callee)?.defs.length||t.callee.type===`MemberExpression`&&E(t.callee)===`fetch`&&t.callee.object.type===`Identifier`&&t.callee.object.name===`globalThis`&&!D(e,t.callee.object)?.defs.length}function _(e,t){if(t.type===`MemberExpression`){let n=d(e,t.object);return n?.name===`components`&&/_generated\/api/.test(n.source)?!0:_(e,t.object)}return t.type===`ObjectExpression`&&t.properties.some(t=>t.type===`Property`&&_(e,t.value))}function v(e){return e.type===`MemberExpression`?v(e.object):e}function y(e){return e.type===`Identifier`?[e.name]:e.type===`MemberExpression`?[...y(e.object),E(e)??``]:[]}function b(e){return e.type===`MemberExpression`?E(e)===`table`||b(e.object):e.type===`CallExpression`&&b(e.callee)}function x(e){return e.callee.type===`MemberExpression`&&[`insert`,`patch`,`replace`,`delete`].includes(E(e.callee)??``)&&e.callee.object.type===`MemberExpression`&&E(e.callee.object)===`db`}function S(e,t){return e.type===`CallExpression`?e.callee.type===`MemberExpression`&&E(e.callee)===t&&(t!==`query`||E(e.callee.object)===`db`)||S(e.callee,t):e.type===`MemberExpression`&&S(e.object,t)}function C(e,t){return{type:`problem`,docs:{description:e},schema:[],messages:t}}function w(e,t){let n=a(e.cwd,l.parse(e.options[0]??{}).convexDir),r=s(n)?c(n):n,u=i(r,s(t)?c(t):t).split(o).join(`/`).match(/^components\/([^/]+)(?:\/|$)/);return u?a(r,`components`,u[1]):null}function T(e){return e?.type===`Literal`&&`value`in e&&String(e.value)===e.value?String(e.value):e?.type===`TemplateLiteral`&&e.expressions.length===0?e.quasis[0].value.cooked:null}function E(e){return e.type===`MemberExpression`?e.computed?T(e.property):e.property.name:null}function D(e,t){if(t.type!==`Identifier`)return null;let n=e.sourceCode.getScope(t);for(;n;){let e=n.set.get(t.name);if(e)return e;n=n.upper}return null}function O(e){let t=new Set;for(let n of e.body)if(n.type===`ExportNamedDeclaration`&&!n.source)for(let e of n.specifiers)t.add(k(e.local)??``);else n.type===`ExportDefaultDeclaration`&&n.declaration.type===`Identifier`&&t.add(n.declaration.name);return t}function k(e){return e.type===`Identifier`?e.name:T(e)}function A(e){return e.type===`CallExpression`?!0:e.type===`AwaitExpression`?A(e.argument):e.type===`BlockStatement`&&e.body.length>0&&e.body.every(e=>e.type===`ReturnStatement`&&e.argument!==null&&A(e.argument)||e.type===`ExpressionStatement`&&A(e.expression))}export{u as appFile,D as binding,w as componentRoot,l as consumerOptions,_ as containsComponentReference,A as delegatingBody,k as exportName,O as exportedNames,S as hasCall,d as imported,h as isDbAccess,x as isDbWrite,g as isExternalCall,p as isFunctionsImport,b as isTableChain,y as memberPath,C as metadata,E as propertyName,v as rootMember,f as sourceVisitors,T as staticString,m as typeOnlySource};
|
|
2
|
+
//# sourceMappingURL=ast.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.mjs","names":[],"sources":["../../src/oxlint/ast.ts"],"sourcesContent":["import { existsSync, realpathSync } from 'node:fs'\nimport { dirname, relative, resolve, sep } from 'node:path'\nimport type { Context, ESTree, RuleMeta, Scope, Visitor } from '@oxlint/plugins'\nimport { z } from 'zod'\nimport { createResolver, resolveImport } from './architecture'\n\nexport const consumerOptions = z.object({ convexDir: z.string().min(1).default('convex') })\n\nexport function appFile(context: Context, filename = context.filename) {\n\tconst requestedRoot = resolve(\n\t\tcontext.cwd,\n\t\tconsumerOptions.parse(context.options[0] ?? {}).convexDir,\n\t)\n\tconst root = existsSync(requestedRoot) ? realpathSync(requestedRoot) : requestedRoot\n\tconst file = relative(root, existsSync(filename) ? realpathSync(filename) : filename)\n\t\t.split(sep)\n\t\t.join('/')\n\treturn file === '..' || file.startsWith('../') || file.startsWith('/') ? null : file\n}\n\nexport function imported(\n\tcontext: Context,\n\tnode: ESTree.Node,\n): { source: string; name: string } | null {\n\tif (node.type === 'CallExpression') return imported(context, node.callee)\n\tif (node.type === 'MemberExpression') {\n\t\tconst owner = imported(context, node.object)\n\t\tconst name = propertyName(node)\n\t\treturn owner && name && ['*', 'z', 'default'].includes(owner.name)\n\t\t\t? { source: owner.source, name }\n\t\t\t: null\n\t}\n\tif (node.type !== 'Identifier') return null\n\tfor (const def of binding(context, node)?.defs ?? []) {\n\t\tif (\n\t\t\tdef.type !== 'ImportBinding' ||\n\t\t\tdef.parent?.type !== 'ImportDeclaration' ||\n\t\t\tdef.parent.importKind === 'type'\n\t\t)\n\t\t\tcontinue\n\t\tconst specifier = def.node\n\t\tif (specifier.type === 'ImportSpecifier' && specifier.importKind === 'type') continue\n\t\treturn {\n\t\t\tsource: def.parent.source.value,\n\t\t\tname:\n\t\t\t\tspecifier.type === 'ImportSpecifier'\n\t\t\t\t\t? (exportName(specifier.imported) ?? '')\n\t\t\t\t\t: specifier.type === 'ImportDefaultSpecifier'\n\t\t\t\t\t\t? 'default'\n\t\t\t\t\t\t: '*',\n\t\t}\n\t}\n\treturn null\n}\n\nexport function sourceVisitors(\n\tcontext: Context,\n\tcheck: (node: ESTree.Node, source: string) => void,\n): Visitor {\n\tfunction visit(node: ESTree.Node, source: ESTree.Node | null | undefined) {\n\t\tconst text = staticString(source)\n\t\tif (text) check(node, text)\n\t}\n\treturn {\n\t\tImportDeclaration(node) {\n\t\t\tvisit(node, node.source)\n\t\t},\n\t\tExportNamedDeclaration(node) {\n\t\t\tvisit(node, node.source)\n\t\t},\n\t\tExportAllDeclaration(node) {\n\t\t\tvisit(node, node.source)\n\t\t},\n\t\tImportExpression(node) {\n\t\t\tvisit(node, node.source)\n\t\t},\n\t\tTSImportType(node) {\n\t\t\tvisit(node, node.source)\n\t\t},\n\t\tCallExpression(node) {\n\t\t\tif (\n\t\t\t\tnode.callee.type === 'Identifier' &&\n\t\t\t\tnode.callee.name === 'require' &&\n\t\t\t\t!binding(context, node.callee)?.defs.length\n\t\t\t)\n\t\t\t\tvisit(node, node.arguments[0])\n\t\t},\n\t}\n}\n\nexport function isFunctionsImport(context: Context, source: string) {\n\tconst target = source.startsWith('.')\n\t\t? resolve(dirname(context.filename), source)\n\t\t: resolveImport(createResolver(), context.filename, source)\n\treturn target !== null && appFile(context, target)?.replace(/\\.[cm]?[jt]s$/, '') === 'functions'\n}\n\nexport function typeOnlySource(node: ESTree.Node) {\n\treturn (\n\t\tnode.type === 'TSImportType' ||\n\t\t(node.type === 'ImportDeclaration' &&\n\t\t\t(node.importKind === 'type' ||\n\t\t\t\t(node.specifiers.length > 0 &&\n\t\t\t\t\tnode.specifiers.every(\n\t\t\t\t\t\t(specifier) => specifier.type === 'ImportSpecifier' && specifier.importKind === 'type',\n\t\t\t\t\t)))) ||\n\t\t((node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') &&\n\t\t\tnode.exportKind === 'type')\n\t)\n}\n\nexport function isDbAccess(node: ESTree.CallExpression) {\n\treturn (\n\t\tnode.callee.type === 'MemberExpression' &&\n\t\tnode.callee.object.type === 'MemberExpression' &&\n\t\tpropertyName(node.callee.object) === 'db'\n\t)\n}\n\nexport function isExternalCall(context: Context, node: ESTree.CallExpression) {\n\tconst ref = imported(context, node.callee)\n\tif (ref && /^(?:node:)?(?:https?|net|tls|fs)(?:\\/|$)|^(?:undici|axios)(?:\\/|$)/.test(ref.source))\n\t\treturn true\n\treturn (\n\t\t(node.callee.type === 'Identifier' &&\n\t\t\tnode.callee.name === 'fetch' &&\n\t\t\t!binding(context, node.callee)?.defs.length) ||\n\t\t(node.callee.type === 'MemberExpression' &&\n\t\t\tpropertyName(node.callee) === 'fetch' &&\n\t\t\tnode.callee.object.type === 'Identifier' &&\n\t\t\tnode.callee.object.name === 'globalThis' &&\n\t\t\t!binding(context, node.callee.object)?.defs.length)\n\t)\n}\n\nexport function containsComponentReference(context: Context, node: ESTree.Node): boolean {\n\tif (node.type === 'MemberExpression') {\n\t\tconst owner = imported(context, node.object)\n\t\tif (owner?.name === 'components' && /_generated\\/api/.test(owner.source)) return true\n\t\treturn containsComponentReference(context, node.object)\n\t}\n\tif (node.type === 'ObjectExpression')\n\t\treturn node.properties.some(\n\t\t\t(property) =>\n\t\t\t\tproperty.type === 'Property' && containsComponentReference(context, property.value),\n\t\t)\n\treturn false\n}\n\nexport function rootMember(node: ESTree.Node): ESTree.Node {\n\treturn node.type === 'MemberExpression' ? rootMember(node.object) : node\n}\n\nexport function memberPath(node: ESTree.Node): string[] {\n\tif (node.type === 'Identifier') return [node.name]\n\tif (node.type === 'MemberExpression')\n\t\treturn [...memberPath(node.object), propertyName(node) ?? '']\n\treturn []\n}\n\nexport function isTableChain(node: ESTree.Node): boolean {\n\tif (node.type === 'MemberExpression')\n\t\treturn propertyName(node) === 'table' || isTableChain(node.object)\n\treturn node.type === 'CallExpression' && isTableChain(node.callee)\n}\n\nexport function isDbWrite(node: ESTree.CallExpression) {\n\treturn (\n\t\tnode.callee.type === 'MemberExpression' &&\n\t\t['insert', 'patch', 'replace', 'delete'].includes(propertyName(node.callee) ?? '') &&\n\t\tnode.callee.object.type === 'MemberExpression' &&\n\t\tpropertyName(node.callee.object) === 'db'\n\t)\n}\n\nexport function hasCall(node: ESTree.Node, name: string): boolean {\n\tif (node.type === 'CallExpression')\n\t\treturn (\n\t\t\t(node.callee.type === 'MemberExpression' &&\n\t\t\t\tpropertyName(node.callee) === name &&\n\t\t\t\t(name !== 'query' || propertyName(node.callee.object) === 'db')) ||\n\t\t\thasCall(node.callee, name)\n\t\t)\n\tif (node.type === 'MemberExpression') return hasCall(node.object, name)\n\treturn false\n}\n\nexport function metadata(description: string, messages: Record<string, string>): RuleMeta {\n\treturn { type: 'problem', docs: { description }, schema: [], messages }\n}\n\nexport function componentRoot(context: Context, filename: string) {\n\tconst requestedRoot = resolve(\n\t\tcontext.cwd,\n\t\tconsumerOptions.parse(context.options[0] ?? {}).convexDir,\n\t)\n\tconst root = existsSync(requestedRoot) ? realpathSync(requestedRoot) : requestedRoot\n\tconst file = relative(root, existsSync(filename) ? realpathSync(filename) : filename)\n\t\t.split(sep)\n\t\t.join('/')\n\tconst match = file.match(/^components\\/([^/]+)(?:\\/|$)/)\n\treturn match ? resolve(root, 'components', match[1]) : null\n}\n\nexport function staticString(node: ESTree.Node | null | undefined) {\n\tif (node?.type === 'Literal' && 'value' in node && String(node.value) === node.value)\n\t\treturn String(node.value)\n\tif (node?.type === 'TemplateLiteral' && node.expressions.length === 0)\n\t\treturn node.quasis[0].value.cooked\n\treturn null\n}\n\nexport function propertyName(node: ESTree.Node): string | null {\n\tif (node.type !== 'MemberExpression') return null\n\treturn node.computed ? staticString(node.property) : node.property.name\n}\n\nexport function binding(context: Context, node: ESTree.Node) {\n\tif (node.type !== 'Identifier') return null\n\tlet scope: Scope | null = context.sourceCode.getScope(node)\n\twhile (scope) {\n\t\tconst variable = scope.set.get(node.name)\n\t\tif (variable) return variable\n\t\tscope = scope.upper\n\t}\n\treturn null\n}\n\nexport function exportedNames(program: ESTree.Program) {\n\tconst names = new Set<string>()\n\tfor (const statement of program.body) {\n\t\tif (statement.type === 'ExportNamedDeclaration' && !statement.source) {\n\t\t\tfor (const specifier of statement.specifiers) names.add(exportName(specifier.local) ?? '')\n\t\t} else if (\n\t\t\tstatement.type === 'ExportDefaultDeclaration' &&\n\t\t\tstatement.declaration.type === 'Identifier'\n\t\t) {\n\t\t\tnames.add(statement.declaration.name)\n\t\t}\n\t}\n\treturn names\n}\n\nexport function exportName(node: ESTree.Node): string | null {\n\treturn node.type === 'Identifier' ? node.name : staticString(node)\n}\n\nexport function delegatingBody(node: ESTree.Node): boolean {\n\tif (node.type === 'CallExpression') return true\n\tif (node.type === 'AwaitExpression') return delegatingBody(node.argument)\n\tif (node.type === 'BlockStatement')\n\t\treturn (\n\t\t\tnode.body.length > 0 &&\n\t\t\tnode.body.every(\n\t\t\t\t(statement) =>\n\t\t\t\t\t(statement.type === 'ReturnStatement' &&\n\t\t\t\t\t\tstatement.argument !== null &&\n\t\t\t\t\t\tdelegatingBody(statement.argument)) ||\n\t\t\t\t\t(statement.type === 'ExpressionStatement' && delegatingBody(statement.expression)),\n\t\t\t)\n\t\t)\n\treturn false\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ESTree } from "@oxlint/plugins";
|
|
2
|
+
//#region src/oxlint/ownership.d.ts
|
|
3
|
+
declare const ownershipRules: {
|
|
4
|
+
'project-structure': {
|
|
5
|
+
meta: import("@oxlint/plugins").RuleMeta;
|
|
6
|
+
create(context: import("@oxlint/plugins").Context): {
|
|
7
|
+
Program?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
Program(node: ESTree.Program): void;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
'root-wiring-only': {
|
|
13
|
+
meta: import("@oxlint/plugins").RuleMeta;
|
|
14
|
+
create(context: import("@oxlint/plugins").Context): {
|
|
15
|
+
Program?: undefined;
|
|
16
|
+
NewExpression?: undefined;
|
|
17
|
+
CallExpression?: undefined;
|
|
18
|
+
ArrowFunctionExpression?: undefined;
|
|
19
|
+
FunctionExpression?: undefined;
|
|
20
|
+
IfStatement?: undefined;
|
|
21
|
+
SwitchStatement?: undefined;
|
|
22
|
+
ConditionalExpression?: undefined;
|
|
23
|
+
ForStatement?: undefined;
|
|
24
|
+
ForOfStatement?: undefined;
|
|
25
|
+
WhileStatement?: undefined;
|
|
26
|
+
'Program:exit'?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
Program(node: ESTree.Program): void;
|
|
29
|
+
NewExpression(node: ESTree.NewExpression): void;
|
|
30
|
+
CallExpression(node: ESTree.CallExpression): void;
|
|
31
|
+
ArrowFunctionExpression(node: ESTree.ArrowFunctionExpression): void;
|
|
32
|
+
FunctionExpression(node: ESTree.Function): void;
|
|
33
|
+
IfStatement: (node: ESTree.Node) => void;
|
|
34
|
+
SwitchStatement: (node: ESTree.Node) => void;
|
|
35
|
+
ConditionalExpression: (node: ESTree.Node) => void;
|
|
36
|
+
ForStatement: (node: ESTree.Node) => void;
|
|
37
|
+
ForOfStatement: (node: ESTree.Node) => void;
|
|
38
|
+
WhileStatement: (node: ESTree.Node) => void;
|
|
39
|
+
'Program:exit'(node: ESTree.Program): void;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
'application-orchestration': {
|
|
43
|
+
meta: import("@oxlint/plugins").RuleMeta;
|
|
44
|
+
create(context: import("@oxlint/plugins").Context): {};
|
|
45
|
+
};
|
|
46
|
+
'internal-function-ownership': {
|
|
47
|
+
meta: import("@oxlint/plugins").RuleMeta;
|
|
48
|
+
create(context: import("@oxlint/plugins").Context): {
|
|
49
|
+
CallExpression?: undefined;
|
|
50
|
+
} | {
|
|
51
|
+
CallExpression(node: ESTree.CallExpression): void;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
'domain-file-responsibilities': {
|
|
55
|
+
meta: import("@oxlint/plugins").RuleMeta;
|
|
56
|
+
create(context: import("@oxlint/plugins").Context): {};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
type OwnershipRuleName = keyof typeof ownershipRules;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { OwnershipRuleName, ownershipRules };
|
|
62
|
+
//# sourceMappingURL=ownership.d.mts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{architectureAnchor as e,createResolver as t,inspectArchitecture as n,resolveImport as r}from"./architecture.mjs";import{appFile as i,binding as a,componentRoot as o,consumerOptions as s,containsComponentReference as c,delegatingBody as l,exportedNames as u,imported as d,isDbAccess as f,isDbWrite as p,isExternalCall as m,isFunctionsImport as h,memberPath as g,metadata as _,propertyName as v,rootMember as y,sourceVisitors as b,typeOnlySource as x}from"./ast.mjs";import{basename as S,resolve as C}from"node:path";import{existsSync as w,realpathSync as T}from"node:fs";const E={"project-structure":{meta:_(`Validate the application tree, colocated tests, and module cycles.`,{structure:`{{file}}: {{message}}`}),create(t){let r=s.parse(t.options[0]??{}),i=C(t.cwd,r.convexDir);return(w(t.filename)?T(t.filename):C(t.filename))===e(i)?{Program(e){for(let i of n({cwd:t.cwd,convexDir:r.convexDir}))t.report({node:e,messageId:`structure`,data:{file:i.file,message:i.message}})}}:{}}},"root-wiring-only":{meta:_(`Keep root files limited to configuration, assembly, and component facades.`,{ownership:`Root files only wire configuration and components. Move behavior into domain/<module>/: {{reason}}.`}),create(e){let t=i(e);if(!t||t.includes(`/`))return{};let n=null,r=`business implementation`,a=!1,o=/^(?:convex\.config|auth\.config|vite\.config|vitest\.config|auth|schema|functions|triggers|foundation|http|crons)\.[cm]?[jt]s$/.test(t);function s(e){n??=e}return{Program(e){e.body.some(e=>e.type===`FunctionDeclaration`||e.type===`ExportNamedDeclaration`&&e.declaration?.type===`FunctionDeclaration`)&&s(e)},NewExpression(t){d(e,t.callee)&&t.arguments.some(t=>c(e,t))&&(a=!0)},CallExpression(n){d(e,n.callee)&&n.arguments.some(t=>c(e,t))&&(a=!0),(p(n)||m(e,n)||t===`schema.ts`&&d(e,n.callee)?.name!==`defineSchema`)&&s(n)},ArrowFunctionExpression(e){(!e.body||!l(e.body))&&s(e)},FunctionExpression(e){(!e.body||!l(e.body))&&s(e)},IfStatement:s,SwitchStatement:s,ConditionalExpression:s,ForStatement:s,ForOfStatement:s,WhileStatement:s,"Program:exit"(t){!o&&!a&&(n=t,r=`unknown root file; only declared component facades may add root source files`),n&&e.report({node:n,messageId:`ownership`,data:{reason:r}})}}}},"application-orchestration":{meta:_(`Reserve application/ for thin cross-domain coordination.`,{ownership:`application/ only coordinates two or more domains. Move schemas, providers, storage access, helpers, and business decisions into their owning domain.`}),create(e){if(!i(e)?.startsWith(`application/`))return{};let n=new Set,a=t(),o=null;function s(e){o??=e}let c=b(e,(t,o)=>{if(x(t))return;let c=r(a,e.filename,o),l=c&&i(e,c),u=l?.match(/^domain\/([^/]+)\/(?:commands|queries|rules)(?:\.[cm]?[jt]s)?$/);u&&u[1]!==`shared`&&n.add(u[1]),l?.startsWith(`domain/`)&&!u&&!l.startsWith(`domain/shared/`)&&s(t),!l&&![`cvx-kit`,`cvx-kit/components/foundation`,`@convex-dev/workflow`,`convex/values`].includes(o)&&s(t)});return{...c,Program(e){let t=u(e);for(let n of e.body){n.type===`FunctionDeclaration`&&n.id&&!t.has(n.id.name)&&s(n);let e=n.type===`ExportNamedDeclaration`?n.declaration:n;if((e?.type===`TSTypeAliasDeclaration`||e?.type===`TSInterfaceDeclaration`)&&s(n),e?.type===`VariableDeclaration`)for(let r of e.declarations)r.init&&[`ObjectExpression`,`ArrayExpression`,`Literal`].includes(r.init.type)&&s(r),n.type===`VariableDeclaration`&&r.id.type===`Identifier`&&!t.has(r.id.name)&&[`ArrowFunctionExpression`,`FunctionExpression`].includes(r.init?.type??``)&&s(r)}},CallExpression(t){c.CallExpression?.(t),(f(t)||m(e,t))&&s(t);let n=d(e,t.callee);n&&[`zod`,`zod/v4`].includes(n.source)&&s(t)},MemberExpression(t){let r=g(t);if(r.length>=4&&r[1]===`domain`){let i=d(e,y(t));i?.name===`internal`&&/_generated\/api/.test(i.source)&&n.add(r[2])}},NewExpression:s,IfStatement:s,SwitchStatement:s,ConditionalExpression:s,"Program:exit"(t){(n.size<2||o)&&e.report({node:o??t,messageId:`ownership`})}}}},"internal-function-ownership":{meta:_(`Place internal handlers with their owner, never in root, api/, or shared/.`,{ownership:`Declare system/internal handlers in domain/<module>/, cross-domain application/, migrations/, or an isolated component.`}),create(e){let t=i(e)??``;return o(e,e.filename)||/^(?:domain\/(?!shared\/)[^/]+\/|application\/|migrations\/)/.test(t)?{}:{CallExpression(t){if(t.callee.type===`CallExpression`)return;let n=d(e,t.callee);n&&(h(e,n.source)&&/^system(Query|Mutation|Action)$/.test(n.name)||/(?:_generated\/server|convex\/server)/.test(n.source)&&/^internal(Query|Mutation|Action)(Generic)?$/.test(n.name))&&e.report({node:t,messageId:`ownership`})}}}},"domain-file-responsibilities":{meta:_(`Keep invariants pure, reads read-only, shapes declarative, and I/O action-side.`,{ownership:`Respect this domain file responsibility: pure rules, read-only queries, declarative schemas/tables/constants, and external I/O only in actions or integrations.`}),create(e){let n=i(e)??``,o=n===`domain/table.ts`;if(!o&&!/^domain\/[^/]+\//.test(n))return{};let s=S(n).replace(/\.[cm]?[jt]s$/,``),c=s===`rules`,l=s===`queries`,u=[`schema`,`table`,`constants`].includes(s),g=/(?:^|\/)(?:actions|integrations|providers)(?:\/|\.)/.test(n)||/(?:^|_)actions\./.test(n),_=t(),y=null;function C(e){y??=e}let w=b(e,(t,n)=>{if(x(t))return;let a=r(_,e.filename,n),s=a&&i(e,a);o&&n!==`cvx-kit/zod-table`&&!/^domain\/[^/]+\/table(?:\.[cm]?[jt]s)?$/.test(s??``)&&C(t),c&&s&&!/(?:^|\/)(?:rules|constants)(?:\.[cm]?[jt]s)?$/.test(s)&&C(t),(c||l||u)&&!s&&![`zod`,`zod/v4`,`convex/values`,`cvx-kit`,`cvx-kit/zod-table`,`cvx-kit/errors`].includes(n)&&C(t),l&&s&&/(?:^|\/)(?:commands|actions|integrations|providers)(?:[./]|$)/.test(s)&&C(t)});return{...w,Program(e){if(o)for(let t of e.body){if(t.type===`ImportDeclaration`||t.type===`EmptyStatement`)continue;let e=t.type===`ExportNamedDeclaration`?t.declaration:t;(e?.type!==`VariableDeclaration`||e.declarations.some(e=>e.init?.type!==`CallExpression`))&&C(t)}},CallExpression(t){if(w.CallExpression?.(t),o){let n=d(e,t.callee);(n?.source!==`cvx-kit/zod-table`||n.name!==`createModule`)&&C(t)}let n=t.callee.type===`MemberExpression`?v(t.callee):null;(c||u)&&(f(t)||[`runQuery`,`runMutation`,`runAction`,`runAfter`,`runAt`].includes(n??``))&&C(t),l&&(p(t)||[`runMutation`,`runAction`,`runAfter`,`runAt`].includes(n??``))&&C(t);let r=d(e,t.callee);r&&h(e,r.source)&&(c||u||l&&/(?:Mutation|Action)$/.test(r.name))&&C(t),l&&t.callee.type===`MemberExpression`&&v(t.callee.object)===`storage`&&[`store`,`delete`,`generateUploadUrl`].includes(n??``)&&C(t),!g&&m(e,t)&&C(t),c&&t.callee.type===`MemberExpression`&&t.callee.object.type===`Identifier`&&!a(e,t.callee.object)?.defs.length&&(t.callee.object.name===`Date`&&n===`now`||t.callee.object.name===`Math`&&n===`random`)&&C(t)},MemberExpression(e){c&&[`db`,`storage`,`scheduler`,`env`].includes(v(e)??``)&&C(e)},NewExpression(t){(c||l||u)&&(t.callee.type===`Identifier`&&!a(e,t.callee)?.defs.length&&[`Error`,`TypeError`,`RangeError`,`Set`,`Map`,`RegExp`].includes(t.callee.name)||C(t))},"Program:exit"(){y&&e.report({node:y,messageId:`ownership`})}}}}};export{E as ownershipRules};
|
|
2
|
+
//# sourceMappingURL=ownership.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.mjs","names":[],"sources":["../../src/oxlint/ownership.ts"],"sourcesContent":["import { existsSync, realpathSync } from 'node:fs'\nimport { basename, resolve } from 'node:path'\nimport type { CreateRule, ESTree } from '@oxlint/plugins'\nimport {\n\tarchitectureAnchor,\n\tinspectArchitecture,\n\tcreateResolver,\n\tresolveImport,\n} from './architecture'\nimport {\n\tconsumerOptions,\n\tappFile,\n\timported,\n\tsourceVisitors,\n\tisFunctionsImport,\n\ttypeOnlySource,\n\tisDbAccess,\n\tisExternalCall,\n\tcontainsComponentReference,\n\trootMember,\n\tmemberPath,\n\tisDbWrite,\n\tmetadata,\n\tcomponentRoot,\n\tpropertyName,\n\tbinding,\n\texportedNames,\n\tdelegatingBody,\n} from './ast'\n\nexport const ownershipRules = {\n\t'project-structure': {\n\t\tmeta: metadata('Validate the application tree, colocated tests, and module cycles.', {\n\t\t\tstructure: '{{file}}: {{message}}',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tconst settings = consumerOptions.parse(context.options[0] ?? {})\n\t\t\tconst root = resolve(context.cwd, settings.convexDir)\n\t\t\tif (\n\t\t\t\t(existsSync(context.filename)\n\t\t\t\t\t? realpathSync(context.filename)\n\t\t\t\t\t: resolve(context.filename)) !== architectureAnchor(root)\n\t\t\t)\n\t\t\t\treturn {}\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tfor (const issue of inspectArchitecture({\n\t\t\t\t\t\tcwd: context.cwd,\n\t\t\t\t\t\tconvexDir: settings.convexDir,\n\t\t\t\t\t}))\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'structure',\n\t\t\t\t\t\t\tdata: { file: issue.file, message: issue.message },\n\t\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'root-wiring-only': {\n\t\tmeta: metadata('Keep root files limited to configuration, assembly, and component facades.', {\n\t\t\townership:\n\t\t\t\t'Root files only wire configuration and components. Move behavior into domain/<module>/: {{reason}}.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tconst file = appFile(context)\n\t\t\tif (!file || file.includes('/')) return {}\n\t\t\tlet issue: ESTree.Node | null = null\n\t\t\tlet reason = 'business implementation'\n\t\t\tlet componentFacade = false\n\t\t\tconst standard =\n\t\t\t\t/^(?:convex\\.config|auth\\.config|vite\\.config|vitest\\.config|auth|schema|functions|triggers|foundation|http|crons)\\.[cm]?[jt]s$/.test(\n\t\t\t\t\tfile,\n\t\t\t\t)\n\t\t\tfunction flag(node: ESTree.Node) {\n\t\t\t\tissue ??= node\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.body.some(\n\t\t\t\t\t\t\t(statement) =>\n\t\t\t\t\t\t\t\tstatement.type === 'FunctionDeclaration' ||\n\t\t\t\t\t\t\t\t(statement.type === 'ExportNamedDeclaration' &&\n\t\t\t\t\t\t\t\t\tstatement.declaration?.type === 'FunctionDeclaration'),\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t},\n\t\t\t\tNewExpression(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\timported(context, node.callee) &&\n\t\t\t\t\t\tnode.arguments.some((argument) => containsComponentReference(context, argument))\n\t\t\t\t\t)\n\t\t\t\t\t\tcomponentFacade = true\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\timported(context, node.callee) &&\n\t\t\t\t\t\tnode.arguments.some((argument) => containsComponentReference(context, argument))\n\t\t\t\t\t)\n\t\t\t\t\t\tcomponentFacade = true\n\t\t\t\t\tif (\n\t\t\t\t\t\tisDbWrite(node) ||\n\t\t\t\t\t\tisExternalCall(context, node) ||\n\t\t\t\t\t\t(file === 'schema.ts' && !(imported(context, node.callee)?.name === 'defineSchema'))\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t},\n\t\t\t\tArrowFunctionExpression(node) {\n\t\t\t\t\tif (!node.body || !delegatingBody(node.body)) flag(node)\n\t\t\t\t},\n\t\t\t\tFunctionExpression(node) {\n\t\t\t\t\tif (!node.body || !delegatingBody(node.body)) flag(node)\n\t\t\t\t},\n\t\t\t\tIfStatement: flag,\n\t\t\t\tSwitchStatement: flag,\n\t\t\t\tConditionalExpression: flag,\n\t\t\t\tForStatement: flag,\n\t\t\t\tForOfStatement: flag,\n\t\t\t\tWhileStatement: flag,\n\t\t\t\t'Program:exit'(node) {\n\t\t\t\t\tif (!standard && !componentFacade) {\n\t\t\t\t\t\tissue = node\n\t\t\t\t\t\treason = 'unknown root file; only declared component facades may add root source files'\n\t\t\t\t\t}\n\t\t\t\t\tif (issue) context.report({ node: issue, messageId: 'ownership', data: { reason } })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'application-orchestration': {\n\t\tmeta: metadata('Reserve application/ for thin cross-domain coordination.', {\n\t\t\townership:\n\t\t\t\t'application/ only coordinates two or more domains. Move schemas, providers, storage access, helpers, and business decisions into their owning domain.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!appFile(context)?.startsWith('application/')) return {}\n\t\t\tconst owners = new Set<string>()\n\t\t\tconst resolver = createResolver()\n\t\t\tlet issue: ESTree.Node | null = null\n\t\t\tfunction flag(node: ESTree.Node) {\n\t\t\t\tissue ??= node\n\t\t\t}\n\t\t\tconst sources = sourceVisitors(context, (node, source) => {\n\t\t\t\tif (typeOnlySource(node)) return\n\t\t\t\tconst target = resolveImport(resolver, context.filename, source)\n\t\t\t\tconst path = target && appFile(context, target)\n\t\t\t\tconst match = path?.match(/^domain\\/([^/]+)\\/(?:commands|queries|rules)(?:\\.[cm]?[jt]s)?$/)\n\t\t\t\tif (match && match[1] !== 'shared') owners.add(match[1])\n\t\t\t\tif (path?.startsWith('domain/') && !match && !path.startsWith('domain/shared/')) flag(node)\n\t\t\t\tif (\n\t\t\t\t\t!path &&\n\t\t\t\t\t![\n\t\t\t\t\t\t'cvx-kit',\n\t\t\t\t\t\t'cvx-kit/components/foundation',\n\t\t\t\t\t\t'@convex-dev/workflow',\n\t\t\t\t\t\t'convex/values',\n\t\t\t\t\t].includes(source)\n\t\t\t\t)\n\t\t\t\t\tflag(node)\n\t\t\t})\n\t\t\treturn {\n\t\t\t\t...sources,\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst publicNames = exportedNames(node)\n\t\t\t\t\tfor (const statement of node.body) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstatement.type === 'FunctionDeclaration' &&\n\t\t\t\t\t\t\tstatement.id &&\n\t\t\t\t\t\t\t!publicNames.has(statement.id.name)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\tflag(statement)\n\t\t\t\t\t\tconst declaration =\n\t\t\t\t\t\t\tstatement.type === 'ExportNamedDeclaration' ? statement.declaration : statement\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tdeclaration?.type === 'TSTypeAliasDeclaration' ||\n\t\t\t\t\t\t\tdeclaration?.type === 'TSInterfaceDeclaration'\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\tflag(statement)\n\t\t\t\t\t\tif (declaration?.type === 'VariableDeclaration')\n\t\t\t\t\t\t\tfor (const item of declaration.declarations) {\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\titem.init &&\n\t\t\t\t\t\t\t\t\t['ObjectExpression', 'ArrayExpression', 'Literal'].includes(item.init.type)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\tflag(item)\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\tstatement.type === 'VariableDeclaration' &&\n\t\t\t\t\t\t\t\t\titem.id.type === 'Identifier' &&\n\t\t\t\t\t\t\t\t\t!publicNames.has(item.id.name) &&\n\t\t\t\t\t\t\t\t\t['ArrowFunctionExpression', 'FunctionExpression'].includes(item.init?.type ?? '')\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\tflag(item)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tsources.CallExpression?.(node)\n\t\t\t\t\tif (isDbAccess(node) || isExternalCall(context, node)) flag(node)\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tif (ref && ['zod', 'zod/v4'].includes(ref.source)) flag(node)\n\t\t\t\t},\n\t\t\t\tMemberExpression(node) {\n\t\t\t\t\tconst path = memberPath(node)\n\t\t\t\t\tif (path.length >= 4 && path[1] === 'domain') {\n\t\t\t\t\t\tconst ref = imported(context, rootMember(node))\n\t\t\t\t\t\tif (ref?.name === 'internal' && /_generated\\/api/.test(ref.source)) owners.add(path[2])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tNewExpression: flag,\n\t\t\t\tIfStatement: flag,\n\t\t\t\tSwitchStatement: flag,\n\t\t\t\tConditionalExpression: flag,\n\t\t\t\t'Program:exit'(node) {\n\t\t\t\t\tif (owners.size < 2 || issue)\n\t\t\t\t\t\tcontext.report({ node: issue ?? node, messageId: 'ownership' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'internal-function-ownership': {\n\t\tmeta: metadata('Place internal handlers with their owner, never in root, api/, or shared/.', {\n\t\t\townership:\n\t\t\t\t'Declare system/internal handlers in domain/<module>/, cross-domain application/, migrations/, or an isolated component.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tconst file = appFile(context) ?? ''\n\t\t\tif (\n\t\t\t\tcomponentRoot(context, context.filename) ||\n\t\t\t\t/^(?:domain\\/(?!shared\\/)[^/]+\\/|application\\/|migrations\\/)/.test(file)\n\t\t\t)\n\t\t\t\treturn {}\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (node.callee.type === 'CallExpression') return\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tif (\n\t\t\t\t\t\tref &&\n\t\t\t\t\t\t((isFunctionsImport(context, ref.source) &&\n\t\t\t\t\t\t\t/^system(Query|Mutation|Action)$/.test(ref.name)) ||\n\t\t\t\t\t\t\t(/(?:_generated\\/server|convex\\/server)/.test(ref.source) &&\n\t\t\t\t\t\t\t\t/^internal(Query|Mutation|Action)(Generic)?$/.test(ref.name)))\n\t\t\t\t\t)\n\t\t\t\t\t\tcontext.report({ node, messageId: 'ownership' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'domain-file-responsibilities': {\n\t\tmeta: metadata(\n\t\t\t'Keep invariants pure, reads read-only, shapes declarative, and I/O action-side.',\n\t\t\t{\n\t\t\t\townership:\n\t\t\t\t\t'Respect this domain file responsibility: pure rules, read-only queries, declarative schemas/tables/constants, and external I/O only in actions or integrations.',\n\t\t\t},\n\t\t),\n\t\tcreate(context) {\n\t\t\tconst file = appFile(context) ?? ''\n\t\t\tconst assembly = file === 'domain/table.ts'\n\t\t\tif (!assembly && !/^domain\\/[^/]+\\//.test(file)) return {}\n\t\t\tconst role = basename(file).replace(/\\.[cm]?[jt]s$/, '')\n\t\t\tconst pure = role === 'rules'\n\t\t\tconst read = role === 'queries'\n\t\t\tconst declarative = ['schema', 'table', 'constants'].includes(role)\n\t\t\tconst actionSide =\n\t\t\t\t/(?:^|\\/)(?:actions|integrations|providers)(?:\\/|\\.)/.test(file) ||\n\t\t\t\t/(?:^|_)actions\\./.test(file)\n\t\t\tconst resolver = createResolver()\n\t\t\tlet issue: ESTree.Node | null = null\n\t\t\tfunction flag(node: ESTree.Node) {\n\t\t\t\tissue ??= node\n\t\t\t}\n\t\t\tconst sources = sourceVisitors(context, (node, source) => {\n\t\t\t\tif (typeOnlySource(node)) return\n\t\t\t\tconst target = resolveImport(resolver, context.filename, source)\n\t\t\t\tconst path = target && appFile(context, target)\n\t\t\t\tif (\n\t\t\t\t\tassembly &&\n\t\t\t\t\tsource !== 'cvx-kit/zod-table' &&\n\t\t\t\t\t!/^domain\\/[^/]+\\/table(?:\\.[cm]?[jt]s)?$/.test(path ?? '')\n\t\t\t\t)\n\t\t\t\t\tflag(node)\n\t\t\t\tif (pure && path && !/(?:^|\\/)(?:rules|constants)(?:\\.[cm]?[jt]s)?$/.test(path)) flag(node)\n\t\t\t\tif (\n\t\t\t\t\t(pure || read || declarative) &&\n\t\t\t\t\t!path &&\n\t\t\t\t\t![\n\t\t\t\t\t\t'zod',\n\t\t\t\t\t\t'zod/v4',\n\t\t\t\t\t\t'convex/values',\n\t\t\t\t\t\t'cvx-kit',\n\t\t\t\t\t\t'cvx-kit/zod-table',\n\t\t\t\t\t\t'cvx-kit/errors',\n\t\t\t\t\t].includes(source)\n\t\t\t\t)\n\t\t\t\t\tflag(node)\n\t\t\t\tif (\n\t\t\t\t\tread &&\n\t\t\t\t\tpath &&\n\t\t\t\t\t/(?:^|\\/)(?:commands|actions|integrations|providers)(?:[./]|$)/.test(path)\n\t\t\t\t)\n\t\t\t\t\tflag(node)\n\t\t\t})\n\t\t\treturn {\n\t\t\t\t...sources,\n\t\t\t\tProgram(node) {\n\t\t\t\t\tif (!assembly) return\n\t\t\t\t\tfor (const statement of node.body) {\n\t\t\t\t\t\tif (statement.type === 'ImportDeclaration' || statement.type === 'EmptyStatement')\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\tconst declaration =\n\t\t\t\t\t\t\tstatement.type === 'ExportNamedDeclaration' ? statement.declaration : statement\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tdeclaration?.type !== 'VariableDeclaration' ||\n\t\t\t\t\t\t\tdeclaration.declarations.some((item) => item.init?.type !== 'CallExpression')\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\tflag(statement)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tsources.CallExpression?.(node)\n\t\t\t\t\tif (assembly) {\n\t\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\t\tif (ref?.source !== 'cvx-kit/zod-table' || ref.name !== 'createModule') flag(node)\n\t\t\t\t\t}\n\t\t\t\t\tconst method = node.callee.type === 'MemberExpression' ? propertyName(node.callee) : null\n\t\t\t\t\tif (\n\t\t\t\t\t\t(pure || declarative) &&\n\t\t\t\t\t\t(isDbAccess(node) ||\n\t\t\t\t\t\t\t['runQuery', 'runMutation', 'runAction', 'runAfter', 'runAt'].includes(method ?? ''))\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t\tif (\n\t\t\t\t\t\tread &&\n\t\t\t\t\t\t(isDbWrite(node) ||\n\t\t\t\t\t\t\t['runMutation', 'runAction', 'runAfter', 'runAt'].includes(method ?? ''))\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t\tconst constructor = imported(context, node.callee)\n\t\t\t\t\tif (\n\t\t\t\t\t\tconstructor &&\n\t\t\t\t\t\tisFunctionsImport(context, constructor.source) &&\n\t\t\t\t\t\t(pure || declarative || (read && /(?:Mutation|Action)$/.test(constructor.name)))\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t\tif (\n\t\t\t\t\t\tread &&\n\t\t\t\t\t\tnode.callee.type === 'MemberExpression' &&\n\t\t\t\t\t\tpropertyName(node.callee.object) === 'storage' &&\n\t\t\t\t\t\t['store', 'delete', 'generateUploadUrl'].includes(method ?? '')\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t\tif (!actionSide && isExternalCall(context, node)) flag(node)\n\t\t\t\t\tif (\n\t\t\t\t\t\tpure &&\n\t\t\t\t\t\tnode.callee.type === 'MemberExpression' &&\n\t\t\t\t\t\tnode.callee.object.type === 'Identifier' &&\n\t\t\t\t\t\t!binding(context, node.callee.object)?.defs.length &&\n\t\t\t\t\t\t((node.callee.object.name === 'Date' && method === 'now') ||\n\t\t\t\t\t\t\t(node.callee.object.name === 'Math' && method === 'random'))\n\t\t\t\t\t)\n\t\t\t\t\t\tflag(node)\n\t\t\t\t},\n\t\t\t\tMemberExpression(node) {\n\t\t\t\t\tif (pure && ['db', 'storage', 'scheduler', 'env'].includes(propertyName(node) ?? ''))\n\t\t\t\t\t\tflag(node)\n\t\t\t\t},\n\t\t\t\tNewExpression(node) {\n\t\t\t\t\tif (!(pure || read || declarative)) return\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.callee.type === 'Identifier' &&\n\t\t\t\t\t\t!binding(context, node.callee)?.defs.length &&\n\t\t\t\t\t\t['Error', 'TypeError', 'RangeError', 'Set', 'Map', 'RegExp'].includes(node.callee.name)\n\t\t\t\t\t)\n\t\t\t\t\t\treturn\n\t\t\t\t\tflag(node)\n\t\t\t\t},\n\t\t\t\t'Program:exit'() {\n\t\t\t\t\tif (issue) context.report({ node: issue, messageId: 'ownership' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n} satisfies Record<string, CreateRule>\n\nexport type OwnershipRuleName = keyof typeof ownershipRules\n"],"mappings":"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"}
|
package/dist/oxlint.d.mts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { ArchitectureDiagnostic, ArchitectureOptions } from "./oxlint/architecture.mjs";
|
|
2
|
+
import { OwnershipRuleName } from "./oxlint/ownership.mjs";
|
|
1
3
|
import { Plugin, Rule } from "@oxlint/plugins";
|
|
2
4
|
//#region src/oxlint.d.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
declare function checkArchitecture(options?: ArchitectureOptions): ArchitectureDiagnostic[];
|
|
6
|
+
type RuleName = OwnershipRuleName | 'component-boundaries' | 'no-component-env' | 'schema-file-boundaries' | 'no-internal-reexports' | 'public-api-first' | 'named-private-helpers' | 'no-raw-builders' | 'no-handwritten-references' | 'domain-import-boundaries' | 'public-functions-in-api' | 'thin-api-adapters' | 'no-inline-enums' | 'no-unbounded-reads' | 'no-manual-timestamps' | 'root-facade-ownership';
|
|
5
7
|
declare const rules: Record<RuleName, Rule>;
|
|
6
8
|
declare const plugin: Plugin;
|
|
7
9
|
//#endregion
|
|
8
|
-
export { RuleName, plugin as default, rules };
|
|
10
|
+
export { type ArchitectureDiagnostic, type ArchitectureOptions, RuleName, checkArchitecture, plugin as default, rules };
|
|
9
11
|
//# sourceMappingURL=oxlint.d.mts.map
|
package/dist/oxlint.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{z as e}from"zod";import{basename as t,dirname as n,relative as r,resolve as i,sep as a}from"node:path";const o=e.object({entryPoints:e.array(e.string()).default([`src/index.ts`])}),s=e.object({packageName:e.string().min(1).optional()}),c={"component-boundaries":{meta:{...u(`Keep portable components isolated and consume their client facades.`,{boundary:`Components may import only their own files and npm dependencies; inject host policy through the client API.`,facade:`Import a component through its client.ts facade, not its private implementation.`}),schema:[{type:`object`,properties:{packageName:{type:`string`,minLength:1}},additionalProperties:!1}]},create(e){let o=d(e.filename),{packageName:c}=s.parse(e.options[0]??{});function l(s,l){let u=f(l);if(!u)return;if(!u.startsWith(`.`)){o&&c&&(u===c||u.startsWith(`${c}/`))&&e.report({node:s,messageId:`boundary`});return}let p=i(n(e.filename),u);o&&(r(o,p).startsWith(`..${a}`)||p===n(o))?e.report({node:s,messageId:`boundary`}):!o&&d(p)&&!/^client(?:\.[cm]?[jt]s)?$/.test(t(p))&&e.report({node:s,messageId:`facade`})}return{ImportDeclaration(e){l(e,e.source)},ExportNamedDeclaration(e){l(e,e.source)},ExportAllDeclaration(e){l(e,e.source)},ImportExpression(e){l(e,e.source)},TSImportType(e){l(e,e.source)},CallExpression(t){t.callee.type===`Identifier`&&t.callee.name===`require`&&!m(e,t.callee)?.defs.length&&l(t,t.arguments[0])}}}},"no-component-env":{meta:u(`Pass environment configuration into portable components.`,{env:`Portable components must receive configuration through arguments; do not read process.env.`}),create(e){if(!d(e.filename))return{};function t(t){if(t.type!==`Identifier`)return!1;let n=m(e,t);return t.name===`process`&&!n?.defs.length||n?.defs.some(e=>e.type===`ImportBinding`&&e.parent?.type===`ImportDeclaration`&&[`node:process`,`process`].includes(e.parent.source.value)&&[`ImportDefaultSpecifier`,`ImportNamespaceSpecifier`].includes(e.node.type))}return{ImportDeclaration(t){if([`node:process`,`process`].includes(t.source.value))for(let n of t.specifiers)n.type===`ImportSpecifier`&&g(n.imported)===`env`&&e.report({node:n,messageId:`env`})},MemberExpression(n){if(p(n)!==`env`)return;let r=n.object,i=r.type===`MemberExpression`&&p(r)===`process`&&r.object.type===`Identifier`&&r.object.name===`globalThis`&&!m(e,r.object)?.defs.length;(t(r)||i)&&e.report({node:n,messageId:`env`})},VariableDeclarator(n){if(n.init&&t(n.init)&&n.id.type===`ObjectPattern`)for(let t of n.id.properties)t.type===`Property`&&(t.computed?f(t.key):g(t.key))===`env`&&e.report({node:t,messageId:`env`})}}}},"schema-file-boundaries":{meta:u(`Keep component storage definitions in schema.ts or table.ts.`,{schema:`{{builder}} belongs in {{file}}, not a function or client module.`}),create(e){return d(e.filename)?{CallExpression(n){let r=n.callee,i=r.type===`Identifier`?r:r.type===`MemberExpression`?r.object:null;if(i?.type!==`Identifier`)return;let a=m(e,i)?.defs.find(e=>e.type===`ImportBinding`&&e.parent?.type===`ImportDeclaration`&&e.parent.source.value===`convex/server`);if(!a)return;let o=a.node.type===`ImportSpecifier`&&r.type===`Identifier`?g(a.node.imported):a.node.type===`ImportNamespaceSpecifier`?p(r):null,s=t(e.filename);o===`defineSchema`&&s!==`schema.ts`?e.report({node:n,messageId:`schema`,data:{builder:o,file:`schema.ts`}}):o===`defineTable`&&![`schema.ts`,`table.ts`].includes(s)&&e.report({node:n,messageId:`schema`,data:{builder:o,file:`schema.ts or table.ts`}})}}:{}}},"no-internal-reexports":{meta:{...u(`Keep re-exports at package entry points and component client facades.`,{reexport:`Import from the owning file directly; only configured entry points and component client.ts facades may re-export.`}),schema:[{type:`object`,properties:{entryPoints:{type:`array`,items:{type:`string`}}},additionalProperties:!1}]},create(e){let t=e.filename.split(a).join(`/`),{entryPoints:n}=o.parse(e.options[0]??{});if(n.some(t=>i(e.cwd,t)===i(e.filename))||/\/src\/components\/[^/]+\/client\.ts$/.test(t))return{};function r(t){if(t.source){e.report({node:t,messageId:`reexport`});return}for(let n of t.type===`ExportNamedDeclaration`?t.specifiers:[])m(e,n.local)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:n,messageId:`reexport`})}return{ExportNamedDeclaration:r,ExportAllDeclaration:r,ExportDefaultDeclaration(t){t.declaration.type===`Identifier`&&m(e,t.declaration)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:t,messageId:`reexport`})}}}},"public-api-first":{meta:u(`Place private helper implementations after the exported API.`,{order:`Move this private helper below the exported API so the file reads from public purpose to implementation.`}),create(e){return{Program(t){let n=h(t),r=t.body.findLastIndex(e=>e.type.startsWith(`Export`));for(let i of t.body.slice(0,r<0?0:r))i.type===`FunctionDeclaration`&&i.id&&!n.has(i.id.name)&&e.report({node:i,messageId:`order`})}}}},"named-private-helpers":{meta:u(`Use named declarations for top-level private helper functions.`,{helper:`Declare this private helper with function name(...) at the bottom of the file.`}),create(e){return{Program(t){let n=h(t);for(let r of t.body)if(r.type===`VariableDeclaration`)for(let t of r.declarations)t.id.type===`Identifier`&&!n.has(t.id.name)&&(t.init?.type===`ArrowFunctionExpression`||t.init?.type===`FunctionExpression`)&&e.report({node:t,messageId:`helper`})}}}}},l={meta:{name:`cvx`},rules:c};function u(e,t){return{type:`problem`,docs:{description:e},schema:[],messages:t}}function d(e){let t=e.split(a).join(`/`).match(/^(.*\/src\/components\/[^/]+)(?:\/|$)/);return t?i(t[1]):null}function f(e){return e?.type===`Literal`&&`value`in e&&String(e.value)===e.value?String(e.value):e?.type===`TemplateLiteral`&&e.expressions.length===0?e.quasis[0].value.cooked:null}function p(e){return e.type===`MemberExpression`?e.computed?f(e.property):e.property.name:null}function m(e,t){if(t.type!==`Identifier`)return null;let n=e.sourceCode.getScope(t);for(;n;){let e=n.set.get(t.name);if(e)return e;n=n.upper}return null}function h(e){let t=new Set;for(let n of e.body)if(n.type===`ExportNamedDeclaration`&&!n.source)for(let e of n.specifiers)t.add(g(e.local)??``);else n.type===`ExportDefaultDeclaration`&&n.declaration.type===`Identifier`&&t.add(n.declaration.name);return t}function g(e){return e.type===`Identifier`?e.name:f(e)}export{l as default,c as rules};
|
|
1
|
+
import{createResolver as e,inspectArchitecture as t,resolveImport as n}from"./oxlint/architecture.mjs";import{appFile as r,binding as i,componentRoot as a,exportName as o,exportedNames as s,hasCall as c,imported as l,isDbWrite as u,isFunctionsImport as d,isTableChain as f,metadata as p,propertyName as m,sourceVisitors as h,staticString as g}from"./oxlint/ast.mjs";import{ownershipRules as _}from"./oxlint/ownership.mjs";import{basename as v,dirname as y,relative as b,sep as x}from"node:path";function S(e={}){return t(e)}const C=T({..._,"root-facade-ownership":{meta:p(`Configure shared kit infrastructure once in its root facade.`,{facade:`Configure {{name}} once in convex/{{file}}; import that facade elsewhere.`}),create(e){if(a(e,e.filename))return{};let t=new Set;function n(n){let i=l(e,n.callee);if(!i)return;let a=null;[`cvx-kit`,`cvx-kit/auth`].includes(i.source)&&i.name===`createAuthFunctions`&&(a=`functions.ts`),[`cvx-kit`,`cvx-kit/triggers`].includes(i.source)&&i.name===`createTriggers`&&(a=`triggers.ts`),[`cvx-kit`,`cvx-kit/components/foundation`].includes(i.source)&&i.name===`Foundation`&&(a=`foundation.ts`),a&&((r(e)!==a||t.has(i.name))&&e.report({node:n,messageId:`facade`,data:{name:i.name,file:a}}),t.add(i.name))}return{CallExpression:n,NewExpression:n}}},"no-raw-builders":{meta:p(`Build host functions through the auth-aware root constructors.`,{builder:`Import raw builders only in convex/functions.ts; use auth*/role*/admin* or system* elsewhere.`}),create(t){if(r(t)===`functions.ts`||a(t,t.filename))return{};let i=e();return{ImportDeclaration(e){if(e.importKind===`type`)return;let a=e.source.value,s=n(i,t.filename,a),c=s&&r(t,s)?.startsWith(`_generated/`)?r(t,s):a;if(!(!/(?:^|\/)_generated\/server(?:\.[cm]?[jt]s)?$/.test(c)&&c!==`convex/server`))for(let n of e.specifiers){if(n.type===`ImportSpecifier`&&n.importKind===`type`)continue;let e=n.type===`ImportSpecifier`?o(n.imported):null;(e===null&&c!==`convex/server`||e&&/^(?:internal)?(?:Query|Mutation|Action)(?:Generic)?$|^(?:query|mutation|action)(?:Generic)?$/.test(e))&&t.report({node:n,messageId:`builder`})}},CallExpression(e){let n=l(t,e.callee);n?.source===`convex/server`&&/^(?:internal)?(?:Query|Mutation|Action)Generic$|^(?:query|mutation|action)Generic$/.test(n.name)&&e.callee.type===`MemberExpression`&&t.report({node:e,messageId:`builder`})}}}},"no-handwritten-references":{meta:p(`Use generated api/internal references.`,{reference:`Use generated api/internal references instead of makeFunctionReference.`}),create(e){return{CallExpression(t){let n=l(e,t.callee);n?.source===`convex/server`&&n.name===`makeFunctionReference`&&e.report({node:t,messageId:`reference`})}}}},"domain-import-boundaries":{meta:p(`Keep api, application, domain, and shared dependencies directed.`,{boundary:`Follow api → application/domain → shared; sibling modules expose only rules.ts and queries.ts.`}),create(t){let i=r(t),a=e();return h(t,(e,o)=>{if(!i)return;let s=n(a,t.filename,o);if(!s)return;let c=r(t,s)?.replace(/\.[cm]?[jt]s$/,``);if(!c){s.split(x).includes(`node_modules`)||t.report({node:e,messageId:`boundary`});return}let l=i.split(`/`),u=c.split(`/`),d=!1;l[0]===`domain`?(d=[`api`,`application`,`migrations`].includes(u[0]),u[0]===`domain`&&l[1]!==u[1]&&u[1]!==`shared`&&l[1]!==`table.ts`&&(d=l[1]===`shared`||![`rules`,`queries`].includes(u.slice(2).join(`/`)))):l[0]===`api`?d=[`api`,`migrations`].includes(u[0])||u[0]===`domain`&&u[1]!==`shared`&&u[1]!==(l.length>2?l[1]:v(i).replace(/\.[cm]?[jt]s$/,``)):l[0]===`application`&&(d=[`api`,`migrations`].includes(u[0])),u[0]===`migrations`&&l[0]!==`migrations`&&(d=!0),[`api`,`domain`,`application`].includes(l[0])&&u.length>1&&![`api`,`domain`,`application`,`_generated`,`components`,`migrations`].includes(u[0])&&(d=!0),d&&t.report({node:e,messageId:`boundary`})})}},"public-functions-in-api":{meta:p(`Expose authenticated public functions only through api/.`,{public:`Declare public auth*/role*/admin* functions in convex/api/<module>.ts.`}),create(e){return r(e)?.startsWith(`api/`)?{}:{CallExpression(t){let n=l(e,t.callee);t.callee.type!==`CallExpression`&&n&&d(e,n.source)&&/^(auth|role|admin)(Query|Mutation|Action)$/.test(n.name)&&e.report({node:t,messageId:`public`})}}}},"thin-api-adapters":{meta:p(`Keep business decisions and writes in domain executors.`,{adapter:`Move business branches, private helpers, and database writes into the owning domain executor.`}),create(e){return r(e)?.startsWith(`api/`)?{IfStatement(t){e.report({node:t,messageId:`adapter`})},SwitchStatement(t){e.report({node:t,messageId:`adapter`})},ConditionalExpression(t){e.report({node:t,messageId:`adapter`})},Program(t){let n=s(t);for(let r of t.body)if(r.type===`FunctionDeclaration`&&r.id&&!n.has(r.id.name)&&e.report({node:r,messageId:`adapter`}),r.type===`VariableDeclaration`)for(let t of r.declarations)t.id.type===`Identifier`&&!n.has(t.id.name)&&[`ArrowFunctionExpression`,`FunctionExpression`].includes(t.init?.type??``)&&e.report({node:t,messageId:`adapter`})},CallExpression(t){u(t)&&e.report({node:t,messageId:`adapter`})}}:{}}},"no-inline-enums":{meta:p(`Keep vocabularies in named constants.ts tuples.`,{vocabulary:`Define a named readonly tuple in constants.ts and pass it to z.enum().`}),create(e){return{CallExpression(t){let n=l(e,t.callee),r=t.arguments[0];for(;r?.type===`TSAsExpression`||r?.type===`TSSatisfiesExpression`;)r=r.expression;n&&/^zod(?:\/v4)?$/.test(n.source)&&n.name===`enum`&&r?.type===`ArrayExpression`&&e.report({node:t,messageId:`vocabulary`})}}}},"no-unbounded-reads":{meta:p(`Bound collection reads through include or pagination.`,{bounded:`Use include(...).execute(limit) or bounded pagination; do not collect() or resolve() an unbounded query.`}),create(e){return a(e,e.filename)?{}:{CallExpression(t){if(t.callee.type!==`MemberExpression`)return;let n=m(t.callee);(n===`collect`&&c(t.callee.object,`query`)||n===`resolve`&&c(t.callee.object,`include`))&&e.report({node:t,messageId:`bounded`})}}}},"no-manual-timestamps":{meta:p(`Let timestamps triggers own createdAt and updatedAt.`,{timestamp:`Remove {{field}} from this write; register the timestamps trigger instead.`}),create(e){return a(e,e.filename)?{}:{CallExpression(t){if(!u(t))return;let n=t.arguments.at(-1);if(n?.type===`ObjectExpression`)for(let t of n.properties){if(t.type!==`Property`)continue;let n=t.computed?g(t.key):o(t.key);(n===`createdAt`||n===`updatedAt`)&&e.report({node:t,messageId:`timestamp`,data:{field:n}})}}}}},"component-boundaries":{meta:p(`Keep portable components isolated and consume their client facades.`,{boundary:`Components may import only their own files and npm dependencies; inject host policy through the client API.`,facade:`Import a component through its client.ts facade, not its private implementation.`}),create(t){let i=a(t,t.filename),o=e();return h(t,(e,s)=>{s.startsWith(`.`)||/^cvx-kit\/components\/[^/]+\//.test(s)&&!/^cvx-kit\/components\/[^/]+\/convex\.config(?:\.js)?$/.test(s)&&t.report({node:e,messageId:`facade`});let c=n(o,t.filename,s);!c||c.split(x).includes(`node_modules`)||(i&&(b(i,c).startsWith(`..${x}`)||c===y(i))?t.report({node:e,messageId:`boundary`}):!i&&a(t,c)&&!/^client(?:\.[cm]?[jt]s)?$/.test(v(c))&&!(r(t)===`convex.config.ts`&&/^convex\.config(?:\.[cm]?[jt]s)?$/.test(v(c)))&&t.report({node:e,messageId:`facade`}))})}},"no-component-env":{meta:p(`Pass environment configuration into portable components.`,{env:`Portable components must receive configuration through arguments; do not read process.env.`}),create(e){if(!a(e,e.filename))return{};function t(t){if(t.type!==`Identifier`)return!1;let n=i(e,t);return t.name===`process`&&!n?.defs.length||n?.defs.some(e=>e.type===`ImportBinding`&&e.parent?.type===`ImportDeclaration`&&[`node:process`,`process`].includes(e.parent.source.value)&&[`ImportDefaultSpecifier`,`ImportNamespaceSpecifier`].includes(e.node.type))}return{ImportDeclaration(t){if([`node:process`,`process`].includes(t.source.value))for(let n of t.specifiers)n.type===`ImportSpecifier`&&o(n.imported)===`env`&&e.report({node:n,messageId:`env`})},MemberExpression(n){if(m(n)!==`env`)return;let r=n.object,a=r.type===`MemberExpression`&&m(r)===`process`&&r.object.type===`Identifier`&&r.object.name===`globalThis`&&!i(e,r.object)?.defs.length;(t(r)||a)&&e.report({node:n,messageId:`env`})},VariableDeclarator(n){if(n.init&&t(n.init)&&n.id.type===`ObjectPattern`)for(let t of n.id.properties)t.type===`Property`&&(t.computed?g(t.key):o(t.key))===`env`&&e.report({node:t,messageId:`env`})}}}},"schema-file-boundaries":{meta:p(`Keep storage shapes and indexes in their documented owners.`,{schema:`{{builder}} belongs in {{file}}.`}),create(e){return{CallExpression(t){let n=l(e,t.callee),i=r(e);if(!i)return;let o=null;n?.source===`convex/server`&&(n.name===`defineSchema`&&i!==`schema.ts`&&!(a(e,e.filename)&&v(i)===`schema.ts`)&&(o=`the root schema.ts (or a component schema.ts)`),n.name===`defineTable`&&!(a(e,e.filename)&&[`schema.ts`,`table.ts`].includes(v(i)))&&(o=`a component schema.ts/table.ts; host tables use zodTable in domain/<module>/schema.ts`)),n&&[`cvx-kit`,`cvx-kit/zod-table`].includes(n.source)&&[`zodTable`,`tenantTable`,`zodVariantTable`].includes(n.name)&&!/^domain\/[^/]+\/schema\.ts$/.test(i)&&(o=`domain/<module>/schema.ts`),o&&e.report({node:t,messageId:`schema`,data:{builder:n?.name??`table`,file:o}}),t.callee.type===`MemberExpression`&&[`index`,`searchIndex`,`vectorIndex`].includes(m(t.callee)??``)&&f(t.callee.object)&&!a(e,e.filename)&&!/^domain\/[^/]+\/table\.ts$/.test(i)&&e.report({node:t,messageId:`schema`,data:{builder:`Table indexes`,file:`domain/<module>/table.ts`}})}}}},"no-internal-reexports":{meta:p(`Import from owning modules directly; consumer apps have no barrels.`,{reexport:`Import from the owning file directly; do not re-export imports inside convex/.`}),create(e){function t(t){if(t.source){e.report({node:t,messageId:`reexport`});return}for(let n of t.type===`ExportNamedDeclaration`?t.specifiers:[])i(e,n.local)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:n,messageId:`reexport`})}return{ExportNamedDeclaration:t,ExportAllDeclaration:t,ExportDefaultDeclaration(t){t.declaration.type===`Identifier`&&i(e,t.declaration)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:t,messageId:`reexport`})}}}},"public-api-first":{meta:p(`Place private helper implementations after the exported API.`,{order:`Move this private helper below the exported API so the file reads from public purpose to implementation.`}),create(e){return{Program(t){let n=s(t),r=t.body.findLastIndex(e=>e.type.startsWith(`Export`));for(let i of t.body.slice(0,r<0?0:r))i.type===`FunctionDeclaration`&&i.id&&!n.has(i.id.name)&&e.report({node:i,messageId:`order`})}}}},"named-private-helpers":{meta:p(`Use named declarations for top-level private helper functions.`,{helper:`Declare this private helper with function name(...) at the bottom of the file.`}),create(e){return{Program(t){let n=s(t);for(let r of t.body)if(r.type===`VariableDeclaration`)for(let t of r.declarations)t.id.type===`Identifier`&&!n.has(t.id.name)&&(t.init?.type===`ArrowFunctionExpression`||t.init?.type===`FunctionExpression`)&&e.report({node:t,messageId:`helper`})}}}}}),w={meta:{name:`cvx`},rules:C};function T(e){let t={...e},n=Object.keys(e);for(let i of n){let n=e[i];t[i]={meta:{...n.meta,schema:[{type:`object`,properties:{convexDir:{type:`string`,minLength:1}},additionalProperties:!1}]},create(e){let t=r(e);return!t||/(?:^|\/)(?:_generated|__tests__)(?:\/|$)/.test(t)||/\.(?:test|spec)\.[cm]?[jt]s$/.test(t)?{}:n.create(e)}}}return t}export{S as checkArchitecture,w as default,C as rules};
|
|
2
2
|
//# sourceMappingURL=oxlint.mjs.map
|
package/dist/oxlint.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oxlint.mjs","names":[],"sources":["../src/oxlint.ts"],"sourcesContent":["import { basename, dirname, relative, resolve, sep } from 'node:path'\nimport type { Context, ESTree, Plugin, Rule, RuleMeta, Scope } from '@oxlint/plugins'\nimport { z } from 'zod'\n\nconst entryPointOptions = z.object({ entryPoints: z.array(z.string()).default(['src/index.ts']) })\nconst componentOptions = z.object({ packageName: z.string().min(1).optional() })\n\nexport type RuleName =\n\t| 'component-boundaries'\n\t| 'no-component-env'\n\t| 'schema-file-boundaries'\n\t| 'no-internal-reexports'\n\t| 'public-api-first'\n\t| 'named-private-helpers'\n\n/** Library rules derived from src/docs/conventions.md and architecture.md. */\nexport const rules: Record<RuleName, Rule> = {\n\t'component-boundaries': {\n\t\tmeta: {\n\t\t\t...metadata('Keep portable components isolated and consume their client facades.', {\n\t\t\t\tboundary:\n\t\t\t\t\t'Components may import only their own files and npm dependencies; inject host policy through the client API.',\n\t\t\t\tfacade: 'Import a component through its client.ts facade, not its private implementation.',\n\t\t\t}),\n\t\t\tschema: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: { packageName: { type: 'string', minLength: 1 } },\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreate(context) {\n\t\t\tconst owner = componentRoot(context.filename)\n\t\t\tconst { packageName } = componentOptions.parse(context.options[0] ?? {})\n\t\t\tfunction check(node: ESTree.Node, source: ESTree.Node | null | undefined) {\n\t\t\t\tconst specifier = staticString(source)\n\t\t\t\tif (!specifier) return\n\t\t\t\tif (!specifier.startsWith('.')) {\n\t\t\t\t\tif (\n\t\t\t\t\t\towner &&\n\t\t\t\t\t\tpackageName &&\n\t\t\t\t\t\t(specifier === packageName || specifier.startsWith(`${packageName}/`))\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'boundary' })\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst target = resolve(dirname(context.filename), specifier)\n\t\t\t\tif (\n\t\t\t\t\towner &&\n\t\t\t\t\t(relative(owner, target).startsWith(`..${sep}`) || target === dirname(owner))\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({ node, messageId: 'boundary' })\n\t\t\t\t} else if (\n\t\t\t\t\t!owner &&\n\t\t\t\t\tcomponentRoot(target) &&\n\t\t\t\t\t!/^client(?:\\.[cm]?[jt]s)?$/.test(basename(target))\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({ node, messageId: 'facade' })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tExportNamedDeclaration(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tExportAllDeclaration(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tImportExpression(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tTSImportType(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.callee.type === 'Identifier' &&\n\t\t\t\t\t\tnode.callee.name === 'require' &&\n\t\t\t\t\t\t!binding(context, node.callee)?.defs.length\n\t\t\t\t\t) {\n\t\t\t\t\t\tcheck(node, node.arguments[0])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-component-env': {\n\t\tmeta: metadata('Pass environment configuration into portable components.', {\n\t\t\tenv: 'Portable components must receive configuration through arguments; do not read process.env.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!componentRoot(context.filename)) return {}\n\t\t\tfunction isProcess(node: ESTree.Node) {\n\t\t\t\tif (node.type !== 'Identifier') return false\n\t\t\t\tconst variable = binding(context, node)\n\t\t\t\tif (node.name === 'process' && !variable?.defs.length) return true\n\t\t\t\treturn variable?.defs.some(\n\t\t\t\t\t(def) =>\n\t\t\t\t\t\tdef.type === 'ImportBinding' &&\n\t\t\t\t\t\tdef.parent?.type === 'ImportDeclaration' &&\n\t\t\t\t\t\t['node:process', 'process'].includes(def.parent.source.value) &&\n\t\t\t\t\t\t['ImportDefaultSpecifier', 'ImportNamespaceSpecifier'].includes(def.node.type),\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tif (['node:process', 'process'].includes(node.source.value)) {\n\t\t\t\t\t\tfor (const specifier of node.specifiers) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tspecifier.type === 'ImportSpecifier' &&\n\t\t\t\t\t\t\t\texportName(specifier.imported) === 'env'\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'env' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tMemberExpression(node) {\n\t\t\t\t\tif (propertyName(node) !== 'env') return\n\t\t\t\t\tconst object = node.object\n\t\t\t\t\tconst globalProcess =\n\t\t\t\t\t\tobject.type === 'MemberExpression' &&\n\t\t\t\t\t\tpropertyName(object) === 'process' &&\n\t\t\t\t\t\tobject.object.type === 'Identifier' &&\n\t\t\t\t\t\tobject.object.name === 'globalThis' &&\n\t\t\t\t\t\t!binding(context, object.object)?.defs.length\n\t\t\t\t\tif (isProcess(object) || globalProcess) context.report({ node, messageId: 'env' })\n\t\t\t\t},\n\t\t\t\tVariableDeclarator(node) {\n\t\t\t\t\tif (node.init && isProcess(node.init) && node.id.type === 'ObjectPattern') {\n\t\t\t\t\t\tfor (const property of node.id.properties) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tproperty.type === 'Property' &&\n\t\t\t\t\t\t\t\t(property.computed ? staticString(property.key) : exportName(property.key)) ===\n\t\t\t\t\t\t\t\t\t'env'\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: property, messageId: 'env' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'schema-file-boundaries': {\n\t\tmeta: metadata('Keep component storage definitions in schema.ts or table.ts.', {\n\t\t\tschema: '{{builder}} belongs in {{file}}, not a function or client module.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!componentRoot(context.filename)) return {}\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst callee = node.callee\n\t\t\t\t\tconst identifier =\n\t\t\t\t\t\tcallee.type === 'Identifier'\n\t\t\t\t\t\t\t? callee\n\t\t\t\t\t\t\t: callee.type === 'MemberExpression'\n\t\t\t\t\t\t\t\t? callee.object\n\t\t\t\t\t\t\t\t: null\n\t\t\t\t\tif (identifier?.type !== 'Identifier') return\n\t\t\t\t\tconst definition = binding(context, identifier)?.defs.find(\n\t\t\t\t\t\t(def) =>\n\t\t\t\t\t\t\tdef.type === 'ImportBinding' &&\n\t\t\t\t\t\t\tdef.parent?.type === 'ImportDeclaration' &&\n\t\t\t\t\t\t\tdef.parent.source.value === 'convex/server',\n\t\t\t\t\t)\n\t\t\t\t\tif (!definition) return\n\t\t\t\t\tconst builder =\n\t\t\t\t\t\tdefinition.node.type === 'ImportSpecifier' && callee.type === 'Identifier'\n\t\t\t\t\t\t\t? exportName(definition.node.imported)\n\t\t\t\t\t\t\t: definition.node.type === 'ImportNamespaceSpecifier'\n\t\t\t\t\t\t\t\t? propertyName(callee)\n\t\t\t\t\t\t\t\t: null\n\t\t\t\t\tconst file = basename(context.filename)\n\t\t\t\t\tif (builder === 'defineSchema' && file !== 'schema.ts') {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'schema', data: { builder, file: 'schema.ts' } })\n\t\t\t\t\t} else if (builder === 'defineTable' && !['schema.ts', 'table.ts'].includes(file)) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'schema',\n\t\t\t\t\t\t\tdata: { builder, file: 'schema.ts or table.ts' },\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-internal-reexports': {\n\t\tmeta: {\n\t\t\t...metadata('Keep re-exports at package entry points and component client facades.', {\n\t\t\t\treexport:\n\t\t\t\t\t'Import from the owning file directly; only configured entry points and component client.ts facades may re-export.',\n\t\t\t}),\n\t\t\tschema: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: { entryPoints: { type: 'array', items: { type: 'string' } } },\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreate(context) {\n\t\t\tconst file = context.filename.split(sep).join('/')\n\t\t\tconst { entryPoints } = entryPointOptions.parse(context.options[0] ?? {})\n\t\t\tconst isEntryPoint = entryPoints.some(\n\t\t\t\t(entry) => resolve(context.cwd, entry) === resolve(context.filename),\n\t\t\t)\n\t\t\tif (isEntryPoint || /\\/src\\/components\\/[^/]+\\/client\\.ts$/.test(file)) return {}\n\t\t\tfunction check(node: ESTree.ExportNamedDeclaration | ESTree.ExportAllDeclaration) {\n\t\t\t\tif (node.source) {\n\t\t\t\t\tcontext.report({ node, messageId: 'reexport' })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor (const specifier of node.type === 'ExportNamedDeclaration' ? node.specifiers : []) {\n\t\t\t\t\tif (binding(context, specifier.local)?.defs.some((def) => def.type === 'ImportBinding')) {\n\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'reexport' })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tExportNamedDeclaration: check,\n\t\t\t\tExportAllDeclaration: check,\n\t\t\t\tExportDefaultDeclaration(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.declaration.type === 'Identifier' &&\n\t\t\t\t\t\tbinding(context, node.declaration)?.defs.some((def) => def.type === 'ImportBinding')\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'reexport' })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'public-api-first': {\n\t\tmeta: metadata('Place private helper implementations after the exported API.', {\n\t\t\torder:\n\t\t\t\t'Move this private helper below the exported API so the file reads from public purpose to implementation.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exports = exportedNames(node)\n\t\t\t\t\tconst lastExport = node.body.findLastIndex((statement) =>\n\t\t\t\t\t\tstatement.type.startsWith('Export'),\n\t\t\t\t\t)\n\t\t\t\t\tfor (const statement of node.body.slice(0, lastExport < 0 ? 0 : lastExport)) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstatement.type === 'FunctionDeclaration' &&\n\t\t\t\t\t\t\tstatement.id &&\n\t\t\t\t\t\t\t!exports.has(statement.id.name)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tcontext.report({ node: statement, messageId: 'order' })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'named-private-helpers': {\n\t\tmeta: metadata('Use named declarations for top-level private helper functions.', {\n\t\t\thelper: 'Declare this private helper with function name(...) at the bottom of the file.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exports = exportedNames(node)\n\t\t\t\t\tfor (const statement of node.body) {\n\t\t\t\t\t\tif (statement.type !== 'VariableDeclaration') continue\n\t\t\t\t\t\tfor (const declaration of statement.declarations) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tdeclaration.id.type === 'Identifier' &&\n\t\t\t\t\t\t\t\t!exports.has(declaration.id.name) &&\n\t\t\t\t\t\t\t\t(declaration.init?.type === 'ArrowFunctionExpression' ||\n\t\t\t\t\t\t\t\t\tdeclaration.init?.type === 'FunctionExpression')\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: declaration, messageId: 'helper' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n}\n\nconst plugin: Plugin = { meta: { name: 'cvx' }, rules }\nexport default plugin\n\nfunction metadata(description: string, messages: Record<string, string>): RuleMeta {\n\treturn { type: 'problem', docs: { description }, schema: [], messages }\n}\n\nfunction componentRoot(filename: string) {\n\tconst match = filename\n\t\t.split(sep)\n\t\t.join('/')\n\t\t.match(/^(.*\\/src\\/components\\/[^/]+)(?:\\/|$)/)\n\treturn match ? resolve(match[1]) : null\n}\n\nfunction staticString(node: ESTree.Node | null | undefined) {\n\tif (node?.type === 'Literal' && 'value' in node && String(node.value) === node.value)\n\t\treturn String(node.value)\n\tif (node?.type === 'TemplateLiteral' && node.expressions.length === 0)\n\t\treturn node.quasis[0].value.cooked\n\treturn null\n}\n\nfunction propertyName(node: ESTree.Node): string | null {\n\tif (node.type !== 'MemberExpression') return null\n\treturn node.computed ? staticString(node.property) : node.property.name\n}\n\nfunction binding(context: Context, node: ESTree.Node) {\n\tif (node.type !== 'Identifier') return null\n\tlet scope: Scope | null = context.sourceCode.getScope(node)\n\twhile (scope) {\n\t\tconst variable = scope.set.get(node.name)\n\t\tif (variable) return variable\n\t\tscope = scope.upper\n\t}\n\treturn null\n}\n\nfunction exportedNames(program: ESTree.Program) {\n\tconst names = new Set<string>()\n\tfor (const statement of program.body) {\n\t\tif (statement.type === 'ExportNamedDeclaration' && !statement.source) {\n\t\t\tfor (const specifier of statement.specifiers) names.add(exportName(specifier.local) ?? '')\n\t\t} else if (\n\t\t\tstatement.type === 'ExportDefaultDeclaration' &&\n\t\t\tstatement.declaration.type === 'Identifier'\n\t\t) {\n\t\t\tnames.add(statement.declaration.name)\n\t\t}\n\t}\n\treturn names\n}\n\nfunction exportName(node: ESTree.Node): string | null {\n\treturn node.type === 'Identifier' ? node.name : staticString(node)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oxlint.mjs","names":[],"sources":["../src/oxlint.ts"],"sourcesContent":["import { basename, dirname, relative, sep } from 'node:path'\nimport type { CreateRule, ESTree, Plugin, Rule } from '@oxlint/plugins'\nimport {\n\tinspectArchitecture,\n\tcreateResolver,\n\tresolveImport,\n\ttype ArchitectureOptions,\n\ttype ArchitectureDiagnostic,\n} from './oxlint/architecture'\nimport { ownershipRules, type OwnershipRuleName } from './oxlint/ownership'\nimport {\n\tappFile,\n\timported,\n\tsourceVisitors,\n\tisFunctionsImport,\n\tisTableChain,\n\tisDbWrite,\n\thasCall,\n\tmetadata,\n\tcomponentRoot,\n\tstaticString,\n\tpropertyName,\n\tbinding,\n\texportedNames,\n\texportName,\n} from './oxlint/ast'\n\nexport type { ArchitectureOptions, ArchitectureDiagnostic } from './oxlint/architecture'\n\nexport function checkArchitecture(options: ArchitectureOptions = {}): ArchitectureDiagnostic[] {\n\treturn inspectArchitecture(options)\n}\n\nexport type RuleName =\n\t| OwnershipRuleName\n\t| 'component-boundaries'\n\t| 'no-component-env'\n\t| 'schema-file-boundaries'\n\t| 'no-internal-reexports'\n\t| 'public-api-first'\n\t| 'named-private-helpers'\n\t| 'no-raw-builders'\n\t| 'no-handwritten-references'\n\t| 'domain-import-boundaries'\n\t| 'public-functions-in-api'\n\t| 'thin-api-adapters'\n\t| 'no-inline-enums'\n\t| 'no-unbounded-reads'\n\t| 'no-manual-timestamps'\n\t| 'root-facade-ownership'\n\n/** Consumer application rules derived from docs/conventions.md. */\nconst applicationRules: Record<RuleName, CreateRule> = {\n\t...ownershipRules,\n\t'root-facade-ownership': {\n\t\tmeta: metadata('Configure shared kit infrastructure once in its root facade.', {\n\t\t\tfacade: 'Configure {{name}} once in convex/{{file}}; import that facade elsewhere.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (componentRoot(context, context.filename)) return {}\n\t\t\tconst seen = new Set<string>()\n\t\t\tfunction check(node: ESTree.CallExpression | ESTree.NewExpression) {\n\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\tif (!ref) return\n\t\t\t\tlet file: string | null = null\n\t\t\t\tif (['cvx-kit', 'cvx-kit/auth'].includes(ref.source) && ref.name === 'createAuthFunctions')\n\t\t\t\t\tfile = 'functions.ts'\n\t\t\t\tif (['cvx-kit', 'cvx-kit/triggers'].includes(ref.source) && ref.name === 'createTriggers')\n\t\t\t\t\tfile = 'triggers.ts'\n\t\t\t\tif (\n\t\t\t\t\t['cvx-kit', 'cvx-kit/components/foundation'].includes(ref.source) &&\n\t\t\t\t\tref.name === 'Foundation'\n\t\t\t\t)\n\t\t\t\t\tfile = 'foundation.ts'\n\t\t\t\tif (!file) return\n\t\t\t\tif (appFile(context) !== file || seen.has(ref.name))\n\t\t\t\t\tcontext.report({ node, messageId: 'facade', data: { name: ref.name, file } })\n\t\t\t\tseen.add(ref.name)\n\t\t\t}\n\t\t\treturn { CallExpression: check, NewExpression: check }\n\t\t},\n\t},\n\t'no-raw-builders': {\n\t\tmeta: metadata('Build host functions through the auth-aware root constructors.', {\n\t\t\tbuilder:\n\t\t\t\t'Import raw builders only in convex/functions.ts; use auth*/role*/admin* or system* elsewhere.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (appFile(context) === 'functions.ts' || componentRoot(context, context.filename)) return {}\n\t\t\tconst resolver = createResolver()\n\t\t\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tif (node.importKind === 'type') return\n\t\t\t\t\tconst importedSource = node.source.value\n\t\t\t\t\tconst target = resolveImport(resolver, context.filename, importedSource)\n\t\t\t\t\tconst source =\n\t\t\t\t\t\ttarget && appFile(context, target)?.startsWith('_generated/')\n\t\t\t\t\t\t\t? appFile(context, target)!\n\t\t\t\t\t\t\t: importedSource\n\t\t\t\t\tif (\n\t\t\t\t\t\t!/(?:^|\\/)_generated\\/server(?:\\.[cm]?[jt]s)?$/.test(source) &&\n\t\t\t\t\t\tsource !== 'convex/server'\n\t\t\t\t\t)\n\t\t\t\t\t\treturn\n\t\t\t\t\tfor (const specifier of node.specifiers) {\n\t\t\t\t\t\tif (specifier.type === 'ImportSpecifier' && specifier.importKind === 'type') continue\n\t\t\t\t\t\tconst name =\n\t\t\t\t\t\t\tspecifier.type === 'ImportSpecifier' ? exportName(specifier.imported) : null\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t(name === null && source !== 'convex/server') ||\n\t\t\t\t\t\t\t(name &&\n\t\t\t\t\t\t\t\t/^(?:internal)?(?:Query|Mutation|Action)(?:Generic)?$|^(?:query|mutation|action)(?:Generic)?$/.test(\n\t\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t))\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'builder' })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tif (\n\t\t\t\t\t\tref?.source === 'convex/server' &&\n\t\t\t\t\t\t/^(?:internal)?(?:Query|Mutation|Action)Generic$|^(?:query|mutation|action)Generic$/.test(\n\t\t\t\t\t\t\tref.name,\n\t\t\t\t\t\t) &&\n\t\t\t\t\t\tnode.callee.type === 'MemberExpression'\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'builder' })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-handwritten-references': {\n\t\tmeta: metadata('Use generated api/internal references.', {\n\t\t\treference: 'Use generated api/internal references instead of makeFunctionReference.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tif (ref?.source === 'convex/server' && ref.name === 'makeFunctionReference')\n\t\t\t\t\t\tcontext.report({ node, messageId: 'reference' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'domain-import-boundaries': {\n\t\tmeta: metadata('Keep api, application, domain, and shared dependencies directed.', {\n\t\t\tboundary:\n\t\t\t\t'Follow api → application/domain → shared; sibling modules expose only rules.ts and queries.ts.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tconst file = appFile(context)\n\t\t\tconst resolver = createResolver()\n\t\t\treturn sourceVisitors(context, (node, source) => {\n\t\t\t\tif (!file) return\n\t\t\t\tconst resolved = resolveImport(resolver, context.filename, source)\n\t\t\t\tif (!resolved) return\n\t\t\t\tconst target = appFile(context, resolved)?.replace(/\\.[cm]?[jt]s$/, '')\n\t\t\t\tif (!target) {\n\t\t\t\t\tif (!resolved.split(sep).includes('node_modules'))\n\t\t\t\t\t\tcontext.report({ node, messageId: 'boundary' })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst from = file.split('/')\n\t\t\t\tconst to = target.split('/')\n\t\t\t\tlet forbidden = false\n\t\t\t\tif (from[0] === 'domain') {\n\t\t\t\t\tforbidden = ['api', 'application', 'migrations'].includes(to[0])\n\t\t\t\t\tif (\n\t\t\t\t\t\tto[0] === 'domain' &&\n\t\t\t\t\t\tfrom[1] !== to[1] &&\n\t\t\t\t\t\tto[1] !== 'shared' &&\n\t\t\t\t\t\tfrom[1] !== 'table.ts'\n\t\t\t\t\t) {\n\t\t\t\t\t\tforbidden =\n\t\t\t\t\t\t\tfrom[1] === 'shared' || !['rules', 'queries'].includes(to.slice(2).join('/'))\n\t\t\t\t\t}\n\t\t\t\t} else if (from[0] === 'api') {\n\t\t\t\t\tforbidden =\n\t\t\t\t\t\t['api', 'migrations'].includes(to[0]) ||\n\t\t\t\t\t\t(to[0] === 'domain' &&\n\t\t\t\t\t\t\tto[1] !== 'shared' &&\n\t\t\t\t\t\t\tto[1] !== (from.length > 2 ? from[1] : basename(file).replace(/\\.[cm]?[jt]s$/, '')))\n\t\t\t\t} else if (from[0] === 'application') forbidden = ['api', 'migrations'].includes(to[0])\n\t\t\t\tif (to[0] === 'migrations' && from[0] !== 'migrations') forbidden = true\n\t\t\t\tif (\n\t\t\t\t\t['api', 'domain', 'application'].includes(from[0]) &&\n\t\t\t\t\tto.length > 1 &&\n\t\t\t\t\t!['api', 'domain', 'application', '_generated', 'components', 'migrations'].includes(\n\t\t\t\t\t\tto[0],\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t\t\tforbidden = true\n\t\t\t\tif (forbidden) context.report({ node, messageId: 'boundary' })\n\t\t\t})\n\t\t},\n\t},\n\t'public-functions-in-api': {\n\t\tmeta: metadata('Expose authenticated public functions only through api/.', {\n\t\t\tpublic: 'Declare public auth*/role*/admin* functions in convex/api/<module>.ts.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (appFile(context)?.startsWith('api/')) return {}\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.callee.type !== 'CallExpression' &&\n\t\t\t\t\t\tref &&\n\t\t\t\t\t\tisFunctionsImport(context, ref.source) &&\n\t\t\t\t\t\t/^(auth|role|admin)(Query|Mutation|Action)$/.test(ref.name)\n\t\t\t\t\t)\n\t\t\t\t\t\tcontext.report({ node, messageId: 'public' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'thin-api-adapters': {\n\t\tmeta: metadata('Keep business decisions and writes in domain executors.', {\n\t\t\tadapter:\n\t\t\t\t'Move business branches, private helpers, and database writes into the owning domain executor.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!appFile(context)?.startsWith('api/')) return {}\n\t\t\treturn {\n\t\t\t\tIfStatement(node) {\n\t\t\t\t\tcontext.report({ node, messageId: 'adapter' })\n\t\t\t\t},\n\t\t\t\tSwitchStatement(node) {\n\t\t\t\t\tcontext.report({ node, messageId: 'adapter' })\n\t\t\t\t},\n\t\t\t\tConditionalExpression(node) {\n\t\t\t\t\tcontext.report({ node, messageId: 'adapter' })\n\t\t\t\t},\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exported = exportedNames(node)\n\t\t\t\t\tfor (const statement of node.body) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstatement.type === 'FunctionDeclaration' &&\n\t\t\t\t\t\t\tstatement.id &&\n\t\t\t\t\t\t\t!exported.has(statement.id.name)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\tcontext.report({ node: statement, messageId: 'adapter' })\n\t\t\t\t\t\tif (statement.type === 'VariableDeclaration')\n\t\t\t\t\t\t\tfor (const declaration of statement.declarations) {\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\tdeclaration.id.type === 'Identifier' &&\n\t\t\t\t\t\t\t\t\t!exported.has(declaration.id.name) &&\n\t\t\t\t\t\t\t\t\t['ArrowFunctionExpression', 'FunctionExpression'].includes(\n\t\t\t\t\t\t\t\t\t\tdeclaration.init?.type ?? '',\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\tcontext.report({ node: declaration, messageId: 'adapter' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (isDbWrite(node)) context.report({ node, messageId: 'adapter' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-inline-enums': {\n\t\tmeta: metadata('Keep vocabularies in named constants.ts tuples.', {\n\t\t\tvocabulary: 'Define a named readonly tuple in constants.ts and pass it to z.enum().',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tlet values = node.arguments[0]\n\t\t\t\t\twhile (values?.type === 'TSAsExpression' || values?.type === 'TSSatisfiesExpression')\n\t\t\t\t\t\tvalues = values.expression\n\t\t\t\t\tif (\n\t\t\t\t\t\tref &&\n\t\t\t\t\t\t/^zod(?:\\/v4)?$/.test(ref.source) &&\n\t\t\t\t\t\tref.name === 'enum' &&\n\t\t\t\t\t\tvalues?.type === 'ArrayExpression'\n\t\t\t\t\t)\n\t\t\t\t\t\tcontext.report({ node, messageId: 'vocabulary' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-unbounded-reads': {\n\t\tmeta: metadata('Bound collection reads through include or pagination.', {\n\t\t\tbounded:\n\t\t\t\t'Use include(...).execute(limit) or bounded pagination; do not collect() or resolve() an unbounded query.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (componentRoot(context, context.filename)) return {}\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (node.callee.type !== 'MemberExpression') return\n\t\t\t\t\tconst name = propertyName(node.callee)\n\t\t\t\t\tif (\n\t\t\t\t\t\t(name === 'collect' && hasCall(node.callee.object, 'query')) ||\n\t\t\t\t\t\t(name === 'resolve' && hasCall(node.callee.object, 'include'))\n\t\t\t\t\t)\n\t\t\t\t\t\tcontext.report({ node, messageId: 'bounded' })\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-manual-timestamps': {\n\t\tmeta: metadata('Let timestamps triggers own createdAt and updatedAt.', {\n\t\t\ttimestamp: 'Remove {{field}} from this write; register the timestamps trigger instead.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (componentRoot(context, context.filename)) return {}\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (!isDbWrite(node)) return\n\t\t\t\t\tconst data = node.arguments.at(-1)\n\t\t\t\t\tif (data?.type !== 'ObjectExpression') return\n\t\t\t\t\tfor (const property of data.properties) {\n\t\t\t\t\t\tif (property.type !== 'Property') continue\n\t\t\t\t\t\tconst field = property.computed ? staticString(property.key) : exportName(property.key)\n\t\t\t\t\t\tif (field === 'createdAt' || field === 'updatedAt')\n\t\t\t\t\t\t\tcontext.report({ node: property, messageId: 'timestamp', data: { field } })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'component-boundaries': {\n\t\tmeta: metadata('Keep portable components isolated and consume their client facades.', {\n\t\t\tboundary:\n\t\t\t\t'Components may import only their own files and npm dependencies; inject host policy through the client API.',\n\t\t\tfacade: 'Import a component through its client.ts facade, not its private implementation.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tconst owner = componentRoot(context, context.filename)\n\t\t\tconst resolver = createResolver()\n\n\t\t\treturn sourceVisitors(context, (node, specifier) => {\n\t\t\t\tif (!specifier.startsWith('.')) {\n\t\t\t\t\tif (\n\t\t\t\t\t\t/^cvx-kit\\/components\\/[^/]+\\//.test(specifier) &&\n\t\t\t\t\t\t!/^cvx-kit\\/components\\/[^/]+\\/convex\\.config(?:\\.js)?$/.test(specifier)\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'facade' })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst target = resolveImport(resolver, context.filename, specifier)\n\t\t\t\tif (!target || target.split(sep).includes('node_modules')) return\n\t\t\t\tif (\n\t\t\t\t\towner &&\n\t\t\t\t\t(relative(owner, target).startsWith(`..${sep}`) || target === dirname(owner))\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({ node, messageId: 'boundary' })\n\t\t\t\t} else if (\n\t\t\t\t\t!owner &&\n\t\t\t\t\tcomponentRoot(context, target) &&\n\t\t\t\t\t!/^client(?:\\.[cm]?[jt]s)?$/.test(basename(target)) &&\n\t\t\t\t\t!(\n\t\t\t\t\t\tappFile(context) === 'convex.config.ts' &&\n\t\t\t\t\t\t/^convex\\.config(?:\\.[cm]?[jt]s)?$/.test(basename(target))\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({ node, messageId: 'facade' })\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\t'no-component-env': {\n\t\tmeta: metadata('Pass environment configuration into portable components.', {\n\t\t\tenv: 'Portable components must receive configuration through arguments; do not read process.env.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!componentRoot(context, context.filename)) return {}\n\t\t\tfunction isProcess(node: ESTree.Node) {\n\t\t\t\tif (node.type !== 'Identifier') return false\n\t\t\t\tconst variable = binding(context, node)\n\t\t\t\tif (node.name === 'process' && !variable?.defs.length) return true\n\t\t\t\treturn variable?.defs.some(\n\t\t\t\t\t(def) =>\n\t\t\t\t\t\tdef.type === 'ImportBinding' &&\n\t\t\t\t\t\tdef.parent?.type === 'ImportDeclaration' &&\n\t\t\t\t\t\t['node:process', 'process'].includes(def.parent.source.value) &&\n\t\t\t\t\t\t['ImportDefaultSpecifier', 'ImportNamespaceSpecifier'].includes(def.node.type),\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tif (['node:process', 'process'].includes(node.source.value)) {\n\t\t\t\t\t\tfor (const specifier of node.specifiers) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tspecifier.type === 'ImportSpecifier' &&\n\t\t\t\t\t\t\t\texportName(specifier.imported) === 'env'\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'env' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tMemberExpression(node) {\n\t\t\t\t\tif (propertyName(node) !== 'env') return\n\t\t\t\t\tconst object = node.object\n\t\t\t\t\tconst globalProcess =\n\t\t\t\t\t\tobject.type === 'MemberExpression' &&\n\t\t\t\t\t\tpropertyName(object) === 'process' &&\n\t\t\t\t\t\tobject.object.type === 'Identifier' &&\n\t\t\t\t\t\tobject.object.name === 'globalThis' &&\n\t\t\t\t\t\t!binding(context, object.object)?.defs.length\n\t\t\t\t\tif (isProcess(object) || globalProcess) context.report({ node, messageId: 'env' })\n\t\t\t\t},\n\t\t\t\tVariableDeclarator(node) {\n\t\t\t\t\tif (node.init && isProcess(node.init) && node.id.type === 'ObjectPattern') {\n\t\t\t\t\t\tfor (const property of node.id.properties) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tproperty.type === 'Property' &&\n\t\t\t\t\t\t\t\t(property.computed ? staticString(property.key) : exportName(property.key)) ===\n\t\t\t\t\t\t\t\t\t'env'\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: property, messageId: 'env' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'schema-file-boundaries': {\n\t\tmeta: metadata('Keep storage shapes and indexes in their documented owners.', {\n\t\t\tschema: '{{builder}} belongs in {{file}}.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst ref = imported(context, node.callee)\n\t\t\t\t\tconst file = appFile(context)\n\t\t\t\t\tif (!file) return\n\t\t\t\t\tlet expected: string | null = null\n\t\t\t\t\tif (ref?.source === 'convex/server') {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tref.name === 'defineSchema' &&\n\t\t\t\t\t\t\tfile !== 'schema.ts' &&\n\t\t\t\t\t\t\t!(componentRoot(context, context.filename) && basename(file) === 'schema.ts')\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\texpected = 'the root schema.ts (or a component schema.ts)'\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tref.name === 'defineTable' &&\n\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\tcomponentRoot(context, context.filename) &&\n\t\t\t\t\t\t\t\t['schema.ts', 'table.ts'].includes(basename(file))\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\texpected =\n\t\t\t\t\t\t\t\t'a component schema.ts/table.ts; host tables use zodTable in domain/<module>/schema.ts'\n\t\t\t\t\t}\n\t\t\t\t\tif (\n\t\t\t\t\t\tref &&\n\t\t\t\t\t\t['cvx-kit', 'cvx-kit/zod-table'].includes(ref.source) &&\n\t\t\t\t\t\t['zodTable', 'tenantTable', 'zodVariantTable'].includes(ref.name) &&\n\t\t\t\t\t\t!/^domain\\/[^/]+\\/schema\\.ts$/.test(file)\n\t\t\t\t\t)\n\t\t\t\t\t\texpected = 'domain/<module>/schema.ts'\n\t\t\t\t\tif (expected)\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'schema',\n\t\t\t\t\t\t\tdata: { builder: ref?.name ?? 'table', file: expected },\n\t\t\t\t\t\t})\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.callee.type === 'MemberExpression' &&\n\t\t\t\t\t\t['index', 'searchIndex', 'vectorIndex'].includes(propertyName(node.callee) ?? '') &&\n\t\t\t\t\t\tisTableChain(node.callee.object) &&\n\t\t\t\t\t\t!componentRoot(context, context.filename) &&\n\t\t\t\t\t\t!/^domain\\/[^/]+\\/table\\.ts$/.test(file)\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'schema',\n\t\t\t\t\t\t\tdata: { builder: 'Table indexes', file: 'domain/<module>/table.ts' },\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-internal-reexports': {\n\t\tmeta: metadata('Import from owning modules directly; consumer apps have no barrels.', {\n\t\t\treexport: 'Import from the owning file directly; do not re-export imports inside convex/.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tfunction check(node: ESTree.ExportNamedDeclaration | ESTree.ExportAllDeclaration) {\n\t\t\t\tif (node.source) {\n\t\t\t\t\tcontext.report({ node, messageId: 'reexport' })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor (const specifier of node.type === 'ExportNamedDeclaration' ? node.specifiers : []) {\n\t\t\t\t\tif (binding(context, specifier.local)?.defs.some((def) => def.type === 'ImportBinding')) {\n\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'reexport' })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tExportNamedDeclaration: check,\n\t\t\t\tExportAllDeclaration: check,\n\t\t\t\tExportDefaultDeclaration(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.declaration.type === 'Identifier' &&\n\t\t\t\t\t\tbinding(context, node.declaration)?.defs.some((def) => def.type === 'ImportBinding')\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'reexport' })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'public-api-first': {\n\t\tmeta: metadata('Place private helper implementations after the exported API.', {\n\t\t\torder:\n\t\t\t\t'Move this private helper below the exported API so the file reads from public purpose to implementation.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exports = exportedNames(node)\n\t\t\t\t\tconst lastExport = node.body.findLastIndex((statement) =>\n\t\t\t\t\t\tstatement.type.startsWith('Export'),\n\t\t\t\t\t)\n\t\t\t\t\tfor (const statement of node.body.slice(0, lastExport < 0 ? 0 : lastExport)) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstatement.type === 'FunctionDeclaration' &&\n\t\t\t\t\t\t\tstatement.id &&\n\t\t\t\t\t\t\t!exports.has(statement.id.name)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tcontext.report({ node: statement, messageId: 'order' })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'named-private-helpers': {\n\t\tmeta: metadata('Use named declarations for top-level private helper functions.', {\n\t\t\thelper: 'Declare this private helper with function name(...) at the bottom of the file.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exports = exportedNames(node)\n\t\t\t\t\tfor (const statement of node.body) {\n\t\t\t\t\t\tif (statement.type !== 'VariableDeclaration') continue\n\t\t\t\t\t\tfor (const declaration of statement.declarations) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tdeclaration.id.type === 'Identifier' &&\n\t\t\t\t\t\t\t\t!exports.has(declaration.id.name) &&\n\t\t\t\t\t\t\t\t(declaration.init?.type === 'ArrowFunctionExpression' ||\n\t\t\t\t\t\t\t\t\tdeclaration.init?.type === 'FunctionExpression')\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: declaration, messageId: 'helper' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n}\n\nexport const rules: Record<RuleName, Rule> = scopeRules(applicationRules)\nconst plugin: Plugin = { meta: { name: 'cvx' }, rules }\nexport default plugin\n\nfunction scopeRules(source: Record<RuleName, CreateRule>): Record<RuleName, Rule> {\n\tconst scoped = { ...source }\n\t// SAFETY: source is the closed applicationRules record; its own keys are exactly RuleName.\n\tconst names = Object.keys(source) as RuleName[]\n\tfor (const name of names) {\n\t\tconst rule = source[name]\n\t\tscoped[name] = {\n\t\t\tmeta: {\n\t\t\t\t...rule.meta,\n\t\t\t\tschema: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: { convexDir: { type: 'string', minLength: 1 } },\n\t\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tcreate(context) {\n\t\t\t\tconst file = appFile(context)\n\t\t\t\tif (\n\t\t\t\t\t!file ||\n\t\t\t\t\t/(?:^|\\/)(?:_generated|__tests__)(?:\\/|$)/.test(file) ||\n\t\t\t\t\t/\\.(?:test|spec)\\.[cm]?[jt]s$/.test(file)\n\t\t\t\t)\n\t\t\t\t\treturn {}\n\t\t\t\treturn rule.create(context)\n\t\t\t},\n\t\t}\n\t}\n\treturn scoped\n}\n"],"mappings":"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"}
|
package/docs/conventions.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This is the kit's doctrine. Not a style suggestion: the structure below is
|
|
4
4
|
what the kit's structural guarantees (auth, triggers, bounded reads, audited
|
|
5
|
-
commands) assume, and what its
|
|
5
|
+
commands) assume, and what its packaged architecture rules enforce. Follow
|
|
6
6
|
it as written; deviate only with a documented reason and a matching change to
|
|
7
7
|
the tests.
|
|
8
8
|
|
|
@@ -33,10 +33,13 @@ convex/
|
|
|
33
33
|
rules.ts shared.ts actions.ts
|
|
34
34
|
<workflow>.ts <workflow>_functions.ts
|
|
35
35
|
__tests__/
|
|
36
|
+
application/ (optional cross-module orchestration only)
|
|
37
|
+
migrations/ (optional backfills and migrations)
|
|
38
|
+
_generated/ (generated; exempt)
|
|
36
39
|
components/ (only if you author portable components)
|
|
37
40
|
<name>/
|
|
38
41
|
__tests__/
|
|
39
|
-
setup.ts
|
|
42
|
+
setup.ts facades.test.ts
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
Three zones, three privileges:
|
|
@@ -49,7 +52,8 @@ Three zones, three privileges:
|
|
|
49
52
|
helpers, or reads/writes beyond delegation.
|
|
50
53
|
- **`domain/`** — all business logic. May import `functions.ts`,
|
|
51
54
|
`foundation.ts`, facades, `domain/shared/`, and its own entity directory.
|
|
52
|
-
May NOT import `api/`,
|
|
55
|
+
May NOT import `api/`, `application/`, migrations, sibling implementations
|
|
56
|
+
(except the rules/queries contract below), `_generated/server` builders, or
|
|
53
57
|
component internals.
|
|
54
58
|
|
|
55
59
|
Dependency direction is one-way: `api → domain → shared → kit`. Anything
|
|
@@ -75,6 +79,18 @@ Cross-module rules:
|
|
|
75
79
|
`application/` layer above `domain/` (`application/<workflow>.ts`); `api/`
|
|
76
80
|
adapters then import `application/*` and their own module, never a second
|
|
77
81
|
module.
|
|
82
|
+
- `application/` is not a service or helper layer. Each executable file must
|
|
83
|
+
coordinate at least two domain modules through their `commands.ts`, `queries.ts`,
|
|
84
|
+
or `rules.ts`, or generated `internal.domain` references. No direct database
|
|
85
|
+
access, provider construction, validators, private helpers, or business branches.
|
|
86
|
+
Keep a workflow owned by one module inside that module, including its integrations.
|
|
87
|
+
- The root directory allowlist is exhaustive: `api`, `domain`, `application`,
|
|
88
|
+
`migrations`, `components`, `__tests__`, and `_generated`. Do not add root
|
|
89
|
+
`host`, `internal`, `lib`, `utils`, `services`, or `helpers` directories.
|
|
90
|
+
- A module may contain nested `internal/`, `integrations/`, provider, or workflow
|
|
91
|
+
directories. Its responsibilities stay local. Do not create unused scaffold files.
|
|
92
|
+
- Application code must not import migrations. Migrations can coordinate data
|
|
93
|
+
changes across modules, but remain isolated from ordinary execution.
|
|
78
94
|
|
|
79
95
|
---
|
|
80
96
|
|
|
@@ -107,7 +123,7 @@ another file's function to shorten an import.
|
|
|
107
123
|
| `triggers.ts` | `createTriggers()`, `timestamps`/`appendOnly`/`noDelete` registrations, calls to per-entity `register<Entity>Triggers` | trigger _logic_ for a specific entity (that lives in the entity) |
|
|
108
124
|
| `foundation.ts` | the single `new Foundation(...)`, destructured exports | command definitions |
|
|
109
125
|
| `<component>.ts` | `new <Client>(components.<name>)` + minimal admin plumbing | workflow/business definitions |
|
|
110
|
-
| `http.ts` |
|
|
126
|
+
| `http.ts` | route registration delegating request handling to the owning domain | webhook business logic |
|
|
111
127
|
| `crons.ts` | `cronJobs()` declarations targeting `internal.domain.<entity>...` | handler logic |
|
|
112
128
|
|
|
113
129
|
### `api/<entity>.ts` — the adapter file
|
|
@@ -202,9 +218,11 @@ it, it moves to `domain/shared/` — never a sibling import.
|
|
|
202
218
|
|
|
203
219
|
### `domain/<entity>/actions.ts` — external side effects
|
|
204
220
|
|
|
205
|
-
Action-side logic
|
|
206
|
-
`
|
|
207
|
-
|
|
221
|
+
Action-side logic owns external side effects. Put a provider used by one module
|
|
222
|
+
in that module's `integrations/` or `providers/` directory. Share configured clients
|
|
223
|
+
in `domain/shared/` only when multiple modules need them. Commands and queries
|
|
224
|
+
must not perform network I/O; pure rules receive values instead of context or
|
|
225
|
+
clock/random/environment dependencies.
|
|
208
226
|
|
|
209
227
|
### `domain/<entity>/<workflow>.ts` + `<workflow>_functions.ts`
|
|
210
228
|
|
|
@@ -214,21 +232,31 @@ targets built with `system*` constructors. Callbacks re-validate everything:
|
|
|
214
232
|
|
|
215
233
|
### `domain/shared/` — cross-entity, and only cross-entity
|
|
216
234
|
|
|
217
|
-
|
|
218
|
-
|
|
235
|
+
Configured provider clients and infrastructure genuinely used by multiple modules.
|
|
236
|
+
Shared infrastructure cannot import business modules. This directory is not a dumping
|
|
219
237
|
ground: code with one consumer moves back to that consumer.
|
|
220
238
|
|
|
221
239
|
### Tests
|
|
222
240
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
241
|
+
Every directory containing executable application source must have a direct
|
|
242
|
+
`__tests__/` child with at least one `.test.ts` or `.spec.ts` file (JS variants
|
|
243
|
+
also work). The same requirement covers `api/`, nested domain directories,
|
|
244
|
+
`application/`, `migrations/`, and local components. Parent and child suites do
|
|
245
|
+
not substitute for that directory's own tests.
|
|
246
|
+
|
|
247
|
+
Generated code, declaration/type-only files, test fixtures inside `__tests__/`,
|
|
248
|
+
and folders that only group child directories are exempt. `domain/table.ts` is
|
|
249
|
+
an assembly exception; root wiring shares `convex/__tests__/`. Do not create
|
|
250
|
+
empty directories or placeholder tests just to satisfy the checker.
|
|
251
|
+
|
|
252
|
+
The package checks for a recognized, nonempty test callback outside skipped or
|
|
253
|
+
TODO suites. It does not execute tests or prove coverage: run the application's
|
|
254
|
+
behavioral tests in CI. Conditional test registration is not counted as the
|
|
255
|
+
required runnable test. Keep fixtures and setup helpers under `__tests__/`.
|
|
256
|
+
|
|
257
|
+
`cvx-kit/oxlint` supplies the structural checks. Consumers write behavioral tests,
|
|
258
|
+
including meaningful facade/wiring tests where needed; they do not copy or
|
|
259
|
+
maintain an architecture-testing framework.
|
|
232
260
|
|
|
233
261
|
---
|
|
234
262
|
|
|
@@ -255,8 +283,8 @@ ground: code with one consumer moves back to that consumer.
|
|
|
255
283
|
| Public function path | `api/<entity>:<fn>` — clients never call a root path | — |
|
|
256
284
|
|
|
257
285
|
The two regexes are enforced at runtime: observability silently drops events
|
|
258
|
-
whose operation/classification/errorCode don't match.
|
|
259
|
-
|
|
286
|
+
whose operation/classification/errorCode don't match. Other naming conventions remain review guidance unless a documented package
|
|
287
|
+
lint rule explicitly enforces them.
|
|
260
288
|
|
|
261
289
|
---
|
|
262
290
|
|
|
@@ -268,7 +296,7 @@ Allowed, per zone (anything not listed is forbidden):
|
|
|
268
296
|
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
269
297
|
| `api/<entity>.ts` | `functions.ts`, `domain/<entity>/*`, `domain/shared/*`, zod |
|
|
270
298
|
| `domain/<entity>/*` | `functions.ts`, `foundation.ts`, component facades, `domain/shared/*`, own directory, generated `internal`/`api` (references only), kit modules |
|
|
271
|
-
| `domain/shared/*` | kit modules
|
|
299
|
+
| `domain/shared/*` | kit modules and infrastructure shared by multiple modules; module-specific clients stay local |
|
|
272
300
|
| root facades | kit modules, `components` from generated api |
|
|
273
301
|
| `functions.ts` | `_generated/server` (the only file allowed to) |
|
|
274
302
|
| `components/<name>/**` | its own directory + npm deps only — no host imports, no `process.env` |
|
package/docs/llms.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
> authoritative. Restructuring an existing raw project → `migration.md`.
|
|
10
10
|
> Upgrading from 0.0.x → `upgrading.md`. New 0.1.0 helpers: `crud.md`,
|
|
11
11
|
> `webhooks.md`, `agent-tools.md`; pagination in `zod-table.md`/`auth.md`.
|
|
12
|
+
> Consumer lint configuration and coverage → `oxlint.md`. The optional
|
|
13
|
+
> `cvx-kit/oxlint` plugin checks the statically detectable conventions;
|
|
14
|
+
> behavioral guarantees still need architecture and runtime tests.
|
|
12
15
|
|
|
13
16
|
> **Placeholders.** Names like `documents`, `users`, `history`, `title`,
|
|
14
17
|
> `ownerId`, and `documentPublish` in the examples below are illustrative
|
package/docs/oxlint.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Oxlint rules for cvx-kit applications
|
|
2
|
+
|
|
3
|
+
`cvx-kit/oxlint` is a TypeScript-authored plugin bundled with the package. It
|
|
4
|
+
checks applications using the kit against `conventions.md`, the authoritative
|
|
5
|
+
source for folder structure and file anatomy. The package repository has a
|
|
6
|
+
separate, unpublished plugin for its own `src/` layout.
|
|
7
|
+
|
|
8
|
+
The consumer plugin was introduced in 0.1.5; the initial 0.1.4 plugin targeted
|
|
9
|
+
library authoring. The five architecture rules below were added in 0.1.6. See `upgrading.md` when migrating that configuration.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Install Oxlint as a development dependency and load the plugin in
|
|
14
|
+
`.oxlintrc.json`. The root README includes a configuration enabling all rules.
|
|
15
|
+
Loading a plugin does not enable its rules. For example:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"jsPlugins": [{ "name": "cvx", "specifier": "cvx-kit/oxlint" }],
|
|
20
|
+
"rules": {
|
|
21
|
+
"cvx/no-raw-builders": "error",
|
|
22
|
+
"cvx/domain-import-boundaries": "error",
|
|
23
|
+
"cvx/thin-api-adapters": "error"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With Vite+, place those fields under `lint` in `vite.config.ts`. The plugin
|
|
29
|
+
exports a default plugin object, named `rules`, and the `RuleName` type. It is
|
|
30
|
+
tested with Oxlint 1.79.0 and Vite+ 0.3.0.
|
|
31
|
+
|
|
32
|
+
All rules default to `convex/` relative to the linter's working directory.
|
|
33
|
+
For another location, set `convexDir` on **each enabled rule**:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "cvx/no-raw-builders": ["error", { "convexDir": "src/server/convex" }] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Source rules ignore files outside that directory, `_generated/`, `__tests__/`,
|
|
40
|
+
and test/spec files. `project-structure` reads the whole backend, including tests. They do not
|
|
41
|
+
offer automatic fixes for architectural moves.
|
|
42
|
+
|
|
43
|
+
## Coverage
|
|
44
|
+
|
|
45
|
+
Every name below has the `cvx/` prefix in lint configuration.
|
|
46
|
+
|
|
47
|
+
| Rule | Detects | Documentation |
|
|
48
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
49
|
+
| `project-structure` | Root folder allowlist, domain ownership, colocated runnable tests, misplaced tests, symlinks, parse failures, runtime domain-module cycles, and shared implementations with only one domain consumer. | Folder structure and Tests |
|
|
50
|
+
| `root-wiring-only` | Unknown root files without a component facade, root function declarations, nondelegating callbacks, branches, loops, direct writes/network calls, and inline root schema construction. | Root files |
|
|
51
|
+
| `application-orchestration` | Files without two domain dependencies, provider construction, external runtime dependencies, database access, validator construction, and business branches. | Module grain |
|
|
52
|
+
| `internal-function-ownership` | Internal/system registrations at root, in public API adapters, or in domain/shared. | Folder structure |
|
|
53
|
+
| `domain-file-responsibilities` | Impure rules, query writes/mutations, database access in declarative files, misplaced direct external I/O, and imports that violate these roles. | File anatomy |
|
|
54
|
+
| `root-facade-ownership` | `createAuthFunctions`, `createTriggers`, and `new Foundation` outside their root files, or repeated in one file. | `conventions.md`, Root files |
|
|
55
|
+
| `no-raw-builders` | Runtime imports of raw Convex builders outside `functions.ts`; namespace calls to raw generic builders. | `auth.md`; `llms.md`, Non-negotiable rules |
|
|
56
|
+
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules |
|
|
57
|
+
| `domain-import-boundaries` | Reverse dependencies into `api/`, domain imports of `application/`, forbidden sibling imports, and API adapters importing another domain module. | `conventions.md`, Module grain and Import rules |
|
|
58
|
+
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure |
|
|
59
|
+
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file |
|
|
60
|
+
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies |
|
|
61
|
+
| `no-unbounded-reads` | Direct `ctx.db.query(...).collect()` chains and `ctx.include(...).resolve()` chains in host code. | `llms.md`, Bounded reads; `zod-table.md` |
|
|
62
|
+
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md` |
|
|
63
|
+
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import |
|
|
64
|
+
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules |
|
|
65
|
+
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology |
|
|
66
|
+
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order |
|
|
67
|
+
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order |
|
|
68
|
+
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order |
|
|
69
|
+
|
|
70
|
+
## Allowed patterns
|
|
71
|
+
|
|
72
|
+
- `functions.ts` imports raw builders to configure `createAuthFunctions`.
|
|
73
|
+
Type-only imports such as `MutationCtx` from `_generated/server` are valid
|
|
74
|
+
elsewhere. `httpAction` remains available for HTTP routing.
|
|
75
|
+
- Domain modules may import sibling `rules.ts` and `queries.ts`, as explicitly
|
|
76
|
+
allowed by the module-grain section. Cross-module writes go through the
|
|
77
|
+
optional `application/` layer. `domain/table.ts` only imports module table maps
|
|
78
|
+
and combines them with `createModule` from `cvx-kit/zod-table`.
|
|
79
|
+
- API adapters may delegate and perform the documented bounded inline query
|
|
80
|
+
and DTO projection. `system*` constructors may live in domain modules.
|
|
81
|
+
- Authored components under `convex/components/<name>/` may use their own raw
|
|
82
|
+
builders and raw tables. Host-specific facade, timestamp, and bounded-read
|
|
83
|
+
rules do not apply there. Configuration imports for mounting components are
|
|
84
|
+
allowed from root `convex.config.ts`.
|
|
85
|
+
- Host schemas use kit table constructors in `domain/<module>/schema.ts` and
|
|
86
|
+
attach indexes in `domain/<module>/table.ts`. `schema.ts` assembles them.
|
|
87
|
+
- `archivedAt` remains application-controlled. Singular `db.get` reads and
|
|
88
|
+
bounded `execute`/`paginate` calls are allowed.
|
|
89
|
+
|
|
90
|
+
## What still needs review and tests
|
|
91
|
+
|
|
92
|
+
These are syntax and lexical-binding checks, not whole-program validation.
|
|
93
|
+
Import-boundary rules resolve relative imports and tsconfig aliases (including
|
|
94
|
+
extended configs) using the package resolver. Symlinked source is rejected by
|
|
95
|
+
the project scan. Computed import paths are not resolved. Named import aliases and namespaces are recognized where documented;
|
|
96
|
+
arbitrary variable reassignment, wrapper functions, destructured database
|
|
97
|
+
methods, and calls split across variables are not traced. Database checks
|
|
98
|
+
recognize the documented `ctx.db` member shape, not its runtime type.
|
|
99
|
+
|
|
100
|
+
The plugin does not prove DTO redaction, complete command/audit coverage,
|
|
101
|
+
tenant/RLS correctness, unique table ownership across files, required facade
|
|
102
|
+
existence, exactly one component mount, numeric read limits, or vocabulary
|
|
103
|
+
tuple ownership. Run the behavioral tests described in `maintainability.md`. The package owns
|
|
104
|
+
structural enforcement; applications do not need copied architecture tests. A clean lint run is not
|
|
105
|
+
evidence of those runtime guarantees.
|
|
106
|
+
|
|
107
|
+
## Whole-project validation
|
|
108
|
+
|
|
109
|
+
Enable `cvx/project-structure` and lint the **whole** configured backend. The
|
|
110
|
+
scan reports on `convex.config.ts`, or the first non-test source file if no
|
|
111
|
+
configuration file exists. That anchor must be part of the lint invocation;
|
|
112
|
+
changed-file-only runs and cached lint runs can skip project checks. Use a full,
|
|
113
|
+
uncached lint run in CI. The scan ignores generated code, reads current files on
|
|
114
|
+
every invocation, and does not execute application code.
|
|
115
|
+
|
|
116
|
+
For standalone tooling, including an empty/missing backend, use the same packaged
|
|
117
|
+
checker directly:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { checkArchitecture } from 'cvx-kit/oxlint'
|
|
121
|
+
|
|
122
|
+
const issues = checkArchitecture({ cwd: process.cwd(), convexDir: 'convex' })
|
|
123
|
+
for (const issue of issues) console.error(`${issue.file}: ${issue.message}`)
|
|
124
|
+
if (issues.length) process.exitCode = 1
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
No fixed module scaffold is required. Type-only modules and grouping folders
|
|
128
|
+
need no test directory. Runtime schema/constants files do; root wiring shares
|
|
129
|
+
root tests. Test files must be directly inside the owning `__tests__/` directory.
|
|
130
|
+
Recognized test entrypoints are `test`/`it` globals and imports from Vitest,
|
|
131
|
+
Vite+, Bun, Node, or Jest, including import aliases and namespaces. Empty,
|
|
132
|
+
TODO, skipped, and conditionally skipped/run tests do not satisfy the minimum.
|
|
133
|
+
A nonempty callback is a presence check, not evidence of meaningful assertions,
|
|
134
|
+
execution, or coverage. Run the tests in CI.
|
|
135
|
+
|
|
136
|
+
Cycle detection operates on runtime ESM imports/re-exports and literal dynamic
|
|
137
|
+
imports between `domain/<module>` owners, including nested files. Type-only
|
|
138
|
+
imports do not create runtime cycle edges. Arbitrary CommonJS wrappers, computed
|
|
139
|
+
imports, implicit generated function-reference calls, and value flow are not
|
|
140
|
+
whole-program analyzed. Rules report recognizable violations; clean lint does
|
|
141
|
+
not prove arbitrary business logic is absent.
|
|
142
|
+
|
|
143
|
+
Shared ownership follows static runtime imports through shared helpers. A shared
|
|
144
|
+
file reached from only one domain is reported for relocation. Unused code,
|
|
145
|
+
type-only consumers, and computed dependencies are not treated as proof of shared
|
|
146
|
+
ownership; their ownership still needs review.
|
package/docs/upgrading.md
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
# Upgrading to 0.1.0 — the new way of things
|
|
2
2
|
|
|
3
|
+
## 0.1.6 — stricter architecture checks
|
|
4
|
+
|
|
5
|
+
Enable `project-structure`, `root-wiring-only`, `application-orchestration`,
|
|
6
|
+
`internal-function-ownership`, and `domain-file-responsibilities` alongside the
|
|
7
|
+
existing rules. These are opt-in additions; existing enabled boundary rules now
|
|
8
|
+
also resolve tsconfig aliases and reject migration dependencies.
|
|
9
|
+
|
|
10
|
+
Move root implementation folders into their owning domains. Keep `application/`
|
|
11
|
+
limited to multi-domain coordination. Move centralized domain tests beside their
|
|
12
|
+
owners and add real tests for nested executable directories. Do not create empty
|
|
13
|
+
scaffold files. Replace copied structural architecture suites with the packaged
|
|
14
|
+
checks; retain tests that exercise actual application behavior and wiring.
|
|
15
|
+
|
|
16
|
+
## 0.1.5 — consumer Oxlint rules
|
|
17
|
+
|
|
18
|
+
The initial 0.1.4 plugin incorrectly targeted this package's `src/` layout.
|
|
19
|
+
`cvx-kit/oxlint` now targets consuming applications under `convex/`, following
|
|
20
|
+
`conventions.md`. The six existing rule names remain, with consumer semantics;
|
|
21
|
+
nine additional rules cover builders, generated references, domain imports,
|
|
22
|
+
public adapters, facade ownership, vocabularies, reads, and timestamps.
|
|
23
|
+
|
|
24
|
+
Remove `entryPoints` and `packageName` options from your consumer lint config.
|
|
25
|
+
Applications have no barrel entry points inside `convex/`. Every rule now
|
|
26
|
+
accepts only an optional `convexDir`, relative to the linter's working directory
|
|
27
|
+
(default: `convex`). Enable the new rules explicitly; loading the plugin alone
|
|
28
|
+
does not enable them. See `oxlint.md` for coverage and the root README for a
|
|
29
|
+
complete configuration. Repository authoring checks remain local tooling.
|
|
30
|
+
|
|
31
|
+
No runtime API or storage migration is part of this correction.
|
|
32
|
+
|
|
3
33
|
## 0.1.4
|
|
4
34
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
automatically. Select rules in your lint configuration and list your public
|
|
8
|
-
entry points in `cvx/no-internal-reexports`.
|
|
35
|
+
This release introduced the optional `cvx-kit/oxlint` export. Its original
|
|
36
|
+
rules targeted library authoring; see the correction above for consumer use.
|
|
9
37
|
|
|
10
38
|
This patch tightens several TypeScript contracts during the anti-slop cleanup:
|
|
11
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cvx-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"scripts": {
|
|
110
110
|
"lint": "vp lint",
|
|
111
111
|
"check": "vp check",
|
|
112
|
-
"test:lint": "vp test run tools/oxlint/cvx
|
|
112
|
+
"test:lint": "vp test run tools/oxlint/cvx",
|
|
113
113
|
"build": "vp pack && node scripts/convex-config-bridge.mjs && publint",
|
|
114
114
|
"typecheck": "tsc --noEmit",
|
|
115
115
|
"test": "vp test run --exclude test/package-contents.test.mjs",
|
|
@@ -127,7 +127,9 @@
|
|
|
127
127
|
"dependencies": {
|
|
128
128
|
"@convex-dev/workflow": "0.4.6",
|
|
129
129
|
"@oxlint/plugins": "1.79.0",
|
|
130
|
-
"convex-audit-log": "^0.2.0"
|
|
130
|
+
"convex-audit-log": "^0.2.0",
|
|
131
|
+
"oxc-parser": "0.150.0",
|
|
132
|
+
"oxc-resolver": "11.24.2"
|
|
131
133
|
},
|
|
132
134
|
"devDependencies": {
|
|
133
135
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -9,13 +9,15 @@ const root = join(import.meta.dirname, '..')
|
|
|
9
9
|
const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-oxlint-'))
|
|
10
10
|
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
11
11
|
const ruleNames = Object.keys(rules)
|
|
12
|
-
assert.equal(ruleNames.length,
|
|
12
|
+
assert.equal(ruleNames.length, 20)
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
|
|
16
16
|
for (const installer of ['npm', 'bun']) {
|
|
17
17
|
const fixture = join(temporary, installer)
|
|
18
|
-
mkdirSync(join(fixture, '
|
|
18
|
+
mkdirSync(join(fixture, 'convex/components/example'), { recursive: true })
|
|
19
|
+
mkdirSync(join(fixture, 'convex/api'), { recursive: true })
|
|
20
|
+
mkdirSync(join(fixture, 'convex/domain/orders'), { recursive: true })
|
|
19
21
|
writeFileSync(
|
|
20
22
|
join(fixture, 'package.json'),
|
|
21
23
|
JSON.stringify({
|
|
@@ -35,24 +37,28 @@ try {
|
|
|
35
37
|
join(fixture, '.oxlintrc.json'),
|
|
36
38
|
JSON.stringify({
|
|
37
39
|
jsPlugins: [{ name: 'cvx', specifier: 'cvx-kit/oxlint' }],
|
|
38
|
-
rules: {
|
|
39
|
-
...Object.fromEntries(ruleNames.map((name) => [`cvx/${name}`, 'error'])),
|
|
40
|
-
'cvx/no-internal-reexports': ['error', { entryPoints: ['src/public.ts'] }],
|
|
41
|
-
},
|
|
40
|
+
rules: Object.fromEntries(ruleNames.map((name) => [`cvx/${name}`, 'error'])),
|
|
42
41
|
}),
|
|
43
42
|
)
|
|
44
43
|
writeFileSync(
|
|
45
|
-
join(fixture, '
|
|
46
|
-
"
|
|
44
|
+
join(fixture, 'convex/api/orders.ts'),
|
|
45
|
+
"import { authMutation } from '../functions'; import { executeSaveOrder } from '../domain/orders/commands'; export const save = authMutation({ handler: (ctx, args) => executeSaveOrder(ctx, args) })\n",
|
|
47
46
|
)
|
|
48
47
|
writeFileSync(
|
|
49
|
-
join(fixture, '
|
|
48
|
+
join(fixture, 'convex/components/example/client.ts'),
|
|
50
49
|
'export function run() { return 1 }\n',
|
|
51
50
|
)
|
|
51
|
+
for (const directory of ['api', 'components/example']) {
|
|
52
|
+
mkdirSync(join(fixture, 'convex', directory, '__tests__'), { recursive: true })
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(fixture, 'convex', directory, '__tests__/behavior.test.ts'),
|
|
55
|
+
'test("behavior", () => { expect(1).toBe(1) })',
|
|
56
|
+
)
|
|
57
|
+
}
|
|
52
58
|
const oxlint = join(fixture, 'node_modules/.bin/oxlint')
|
|
53
|
-
run(oxlint, ['--config', '.oxlintrc.json', '
|
|
59
|
+
run(oxlint, ['--config', '.oxlintrc.json', 'convex'], fixture)
|
|
54
60
|
writeFileSync(
|
|
55
|
-
join(fixture, '
|
|
61
|
+
join(fixture, 'convex/components/example/bad.ts'),
|
|
56
62
|
`
|
|
57
63
|
import { defineTable } from 'convex/server'
|
|
58
64
|
import { host } from '../../host'
|
|
@@ -64,13 +70,47 @@ export const value = privateHelper()
|
|
|
64
70
|
`,
|
|
65
71
|
)
|
|
66
72
|
writeFileSync(
|
|
67
|
-
join(fixture, '
|
|
68
|
-
"export const host = 1
|
|
73
|
+
join(fixture, 'convex/host.ts'),
|
|
74
|
+
"import { authMutation } from './functions'; export const save = authMutation({}); export const host = 1; import './components/example/bad'\n",
|
|
75
|
+
)
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(fixture, 'convex/api/orders.ts'),
|
|
78
|
+
`import { mutation } from '../_generated/server'
|
|
79
|
+
import { makeFunctionReference as ref } from 'convex/server'
|
|
80
|
+
import { z } from 'zod'
|
|
81
|
+
import { createAuthFunctions } from 'cvx-kit/auth'
|
|
82
|
+
import { other } from '../domain/users/commands'
|
|
83
|
+
export const save = mutation({ handler: async (ctx) => {
|
|
84
|
+
await ctx.db.insert('orders', { updatedAt: Date.now() })
|
|
85
|
+
return ctx.db.query('orders').collect()
|
|
86
|
+
}})
|
|
87
|
+
export const constructors = createAuthFunctions({})
|
|
88
|
+
export const state = z.enum(['draft'])
|
|
89
|
+
export const target = ref('orders:save')
|
|
90
|
+
export const value = other
|
|
91
|
+
`,
|
|
92
|
+
)
|
|
93
|
+
mkdirSync(join(fixture, 'convex/application'), { recursive: true })
|
|
94
|
+
writeFileSync(
|
|
95
|
+
join(fixture, 'convex/application/provider.ts'),
|
|
96
|
+
"import Stripe from 'stripe'; export const provider = new Stripe('key')",
|
|
97
|
+
)
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(fixture, 'convex/domain/orders/queries.ts'),
|
|
100
|
+
"export function save(ctx) { return ctx.db.insert('orders', {}) }",
|
|
101
|
+
)
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(fixture, 'convex/api/internal.ts'),
|
|
104
|
+
"import { systemMutation } from '../functions'; export const internal = systemMutation({})",
|
|
105
|
+
)
|
|
106
|
+
const invalid = spawnSync(
|
|
107
|
+
oxlint,
|
|
108
|
+
['--config', '.oxlintrc.json', '--format', 'json', 'convex'],
|
|
109
|
+
{
|
|
110
|
+
cwd: fixture,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
},
|
|
69
113
|
)
|
|
70
|
-
const invalid = spawnSync(oxlint, ['--config', '.oxlintrc.json', '--format', 'json', 'src'], {
|
|
71
|
-
cwd: fixture,
|
|
72
|
-
encoding: 'utf8',
|
|
73
|
-
})
|
|
74
114
|
assert.ifError(invalid.error)
|
|
75
115
|
assert.equal(invalid.status, 1, invalid.stderr)
|
|
76
116
|
const diagnostics = JSON.parse(invalid.stdout).diagnostics
|
|
@@ -83,11 +123,13 @@ export const value = privateHelper()
|
|
|
83
123
|
writeFileSync(
|
|
84
124
|
join(fixture, 'plugin-types.ts'),
|
|
85
125
|
`
|
|
86
|
-
import plugin, { rules, type RuleName } from 'cvx-kit/oxlint'
|
|
126
|
+
import plugin, { rules, checkArchitecture, type ArchitectureDiagnostic, type RuleName } from 'cvx-kit/oxlint'
|
|
87
127
|
import type { Plugin, Rule } from '@oxlint/plugins'
|
|
88
128
|
const compatible: Plugin = plugin
|
|
89
129
|
const name: RuleName = 'component-boundaries'
|
|
90
130
|
const rule: Rule = rules[name]
|
|
131
|
+
const issues: ArchitectureDiagnostic[] = checkArchitecture()
|
|
132
|
+
void issues
|
|
91
133
|
void compatible
|
|
92
134
|
void rule
|
|
93
135
|
`,
|
|
@@ -109,7 +151,7 @@ void rule
|
|
|
109
151
|
fixture,
|
|
110
152
|
)
|
|
111
153
|
console.log(
|
|
112
|
-
`${installer}: packed Oxlint plugin loads, all
|
|
154
|
+
`${installer}: packed Oxlint plugin loads, all consumer rules report, and declarations typecheck`,
|
|
113
155
|
)
|
|
114
156
|
}
|
|
115
157
|
} finally {
|