cvx-kit 0.1.4 → 0.1.5
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 +13 -0
- package/README.md +23 -11
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/oxlint.d.mts +1 -2
- package/dist/oxlint.mjs +1 -1
- package/dist/oxlint.mjs.map +1 -1
- package/docs/llms.md +3 -0
- package/docs/oxlint.md +98 -0
- package/docs/upgrading.md +19 -4
- package/package.json +1 -1
- package/scripts/test-packed-oxlint.mjs +38 -18
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,19 @@ are this file's matching section.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.5] - 2026-09-18
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Correct `cvx-kit/oxlint` to enforce the documented consumer `convex/` layout
|
|
20
|
+
rather than this repository's `src/` layout. Keep the six library-authoring
|
|
21
|
+
rules in repository tooling and publish 15 consumer rules, with coverage for
|
|
22
|
+
domain boundaries, auth builders, thin adapters, facade ownership, table
|
|
23
|
+
placement, vocabularies, bounded read syntax, and trigger-owned timestamps.
|
|
24
|
+
- Replace consumer `entryPoints`/`packageName` options with `convexDir`; document
|
|
25
|
+
migration, allowed exceptions, and the checks that still need runtime tests.
|
|
26
|
+
Validate the packaged plugin against consumer fixtures installed by npm and Bun.
|
|
27
|
+
|
|
15
28
|
## [0.1.4] - 2026-09-18
|
|
16
29
|
|
|
17
30
|
### Added
|
package/README.md
CHANGED
|
@@ -185,38 +185,50 @@ 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/no-raw-builders": "error",
|
|
200
|
+
"cvx/no-handwritten-references": "error",
|
|
201
|
+
"cvx/domain-import-boundaries": "error",
|
|
202
|
+
"cvx/public-functions-in-api": "error",
|
|
203
|
+
"cvx/thin-api-adapters": "error",
|
|
204
|
+
"cvx/root-facade-ownership": "error",
|
|
205
|
+
"cvx/no-inline-enums": "error",
|
|
206
|
+
"cvx/no-unbounded-reads": "error",
|
|
207
|
+
"cvx/no-manual-timestamps": "error",
|
|
197
208
|
"cvx/component-boundaries": "error",
|
|
198
209
|
"cvx/no-component-env": "error",
|
|
199
210
|
"cvx/schema-file-boundaries": "error",
|
|
200
|
-
"cvx/no-internal-reexports":
|
|
211
|
+
"cvx/no-internal-reexports": "error",
|
|
201
212
|
"cvx/public-api-first": "error",
|
|
202
213
|
"cvx/named-private-helpers": "error"
|
|
203
214
|
}
|
|
204
215
|
}
|
|
205
216
|
```
|
|
206
217
|
|
|
207
|
-
With Vite+, put those fields under `lint` in `vite.config.ts`.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
1.79.0 and Vite+ 0.3.0.
|
|
218
|
+
With Vite+, put those fields under `lint` in `vite.config.ts`. Each rule accepts
|
|
219
|
+
`{ "convexDir": "path/to/convex" }` when your backend directory differs from
|
|
220
|
+
`convex/`. Generated files and tests are excluded. The plugin exports its
|
|
221
|
+
default plugin object, named `rules`, and the `RuleName` type. Verified with
|
|
222
|
+
Oxlint 1.79.0 and Vite+ 0.3.0. See [rule coverage and limitations](./src/docs/oxlint.md)
|
|
223
|
+
and the [migration from the initial 0.1.4 plugin](./src/docs/upgrading.md).
|
|
214
224
|
|
|
215
225
|
Repository checks and the enforced library rules are documented in
|
|
216
226
|
[`tools/oxlint/README.md`](./tools/oxlint/README.md). Run `bun run lint` for
|
|
217
227
|
anti-slop and architecture checks, `bun run test:lint` to test the custom
|
|
218
228
|
rules, and `bun run check` for the full Vite+ lint and formatting check.
|
|
219
229
|
|
|
230
|
+
## Component authoring conventions
|
|
231
|
+
|
|
220
232
|
This package follows the official Convex component template
|
|
221
233
|
(`get-convex/templates/template-component`; see
|
|
222
234
|
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
|
package/dist/oxlint.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Plugin, Rule } from "@oxlint/plugins";
|
|
2
2
|
//#region src/oxlint.d.ts
|
|
3
|
-
type RuleName = 'component-boundaries' | 'no-component-env' | 'schema-file-boundaries' | 'no-internal-reexports' | 'public-api-first' | 'named-private-helpers';
|
|
4
|
-
/** Library rules derived from src/docs/conventions.md and architecture.md. */
|
|
3
|
+
type RuleName = '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
4
|
declare const rules: Record<RuleName, Rule>;
|
|
6
5
|
declare const plugin: Plugin;
|
|
7
6
|
//#endregion
|
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{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({convexDir:e.string().min(1).default(`convex`)}),s=l({"root-facade-ownership":{meta:_(`Configure shared kit infrastructure once in its root facade.`,{facade:`Configure {{name}} once in convex/{{file}}; import that facade elsewhere.`}),create(e){if(v(e,e.filename))return{};let t=new Set;function n(n){let r=d(e,n.callee);if(!r)return;let i=null;[`cvx-kit`,`cvx-kit/auth`].includes(r.source)&&r.name===`createAuthFunctions`&&(i=`functions.ts`),[`cvx-kit`,`cvx-kit/triggers`].includes(r.source)&&r.name===`createTriggers`&&(i=`triggers.ts`),[`cvx-kit`,`cvx-kit/components/foundation`].includes(r.source)&&r.name===`Foundation`&&(i=`foundation.ts`),i&&((u(e)!==i||t.has(r.name))&&e.report({node:n,messageId:`facade`,data:{name:r.name,file:i}}),t.add(r.name))}return{CallExpression:n,NewExpression:n}}},"no-raw-builders":{meta:_(`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(e){return u(e)===`functions.ts`||v(e,e.filename)?{}:{ImportDeclaration(t){if(t.importKind===`type`)return;let n=t.source.value;if(!(!/(?:^|\/)_generated\/server(?:\.[cm]?[jt]s)?$/.test(n)&&n!==`convex/server`))for(let r of t.specifiers){if(r.type===`ImportSpecifier`&&r.importKind===`type`)continue;let t=r.type===`ImportSpecifier`?C(r.imported):null;(t===null&&n!==`convex/server`||t&&/^(?:internal)?(?:Query|Mutation|Action)(?:Generic)?$|^(?:query|mutation|action)(?:Generic)?$/.test(t))&&e.report({node:r,messageId:`builder`})}},CallExpression(t){let n=d(e,t.callee);n?.source===`convex/server`&&/^(?:internal)?(?:Query|Mutation|Action)Generic$|^(?:query|mutation|action)Generic$/.test(n.name)&&t.callee.type===`MemberExpression`&&e.report({node:t,messageId:`builder`})}}}},"no-handwritten-references":{meta:_(`Use generated api/internal references.`,{reference:`Use generated api/internal references instead of makeFunctionReference.`}),create(e){return{CallExpression(t){let n=d(e,t.callee);n?.source===`convex/server`&&n.name===`makeFunctionReference`&&e.report({node:t,messageId:`reference`})}}}},"domain-import-boundaries":{meta:_(`Keep api, application, domain, and shared dependencies directed.`,{boundary:`Follow api → application/domain → shared; sibling modules expose only rules.ts and queries.ts.`}),create(e){let r=u(e);return f(e,(a,o)=>{if(!r||!o.startsWith(`.`))return;let s=u(e,i(n(e.filename),o))?.replace(/\.[cm]?[jt]s$/,``);if(!s)return;let c=r.split(`/`),l=s.split(`/`),d=!1;c[0]===`domain`?(d=[`api`,`application`].includes(l[0]),l[0]===`domain`&&c[1]!==l[1]&&l[1]!==`shared`&&c[1]!==`table.ts`&&(d=c[1]===`shared`||![`rules`,`queries`].includes(l.slice(2).join(`/`)))):c[0]===`api`?d=l[0]===`api`||l[0]===`domain`&&l[1]!==`shared`&&l[1]!==t(r).replace(/\.[cm]?[jt]s$/,``):c[0]===`application`&&(d=l[0]===`api`),d&&e.report({node:a,messageId:`boundary`})})}},"public-functions-in-api":{meta:_(`Expose authenticated public functions only through api/.`,{public:`Declare public auth*/role*/admin* functions in convex/api/<module>.ts.`}),create(e){return u(e)?.startsWith(`api/`)?{}:{CallExpression(t){let n=d(e,t.callee);t.callee.type!==`CallExpression`&&n&&p(e,n.source)&&/^(auth|role|admin)(Query|Mutation|Action)$/.test(n.name)&&e.report({node:t,messageId:`public`})}}}},"thin-api-adapters":{meta:_(`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 u(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){h(t)&&e.report({node:t,messageId:`adapter`})}}:{}}},"no-inline-enums":{meta:_(`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=d(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:_(`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 v(e,e.filename)?{}:{CallExpression(t){if(t.callee.type!==`MemberExpression`)return;let n=b(t.callee);(n===`collect`&&g(t.callee.object,`query`)||n===`resolve`&&g(t.callee.object,`include`))&&e.report({node:t,messageId:`bounded`})}}}},"no-manual-timestamps":{meta:_(`Let timestamps triggers own createdAt and updatedAt.`,{timestamp:`Remove {{field}} from this write; register the timestamps trigger instead.`}),create(e){return v(e,e.filename)?{}:{CallExpression(t){if(!h(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?y(t.key):C(t.key);(n===`createdAt`||n===`updatedAt`)&&e.report({node:t,messageId:`timestamp`,data:{field:n}})}}}}},"component-boundaries":{meta:_(`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(e){let o=v(e,e.filename);return f(e,(s,c)=>{if(!c.startsWith(`.`)){/^cvx-kit\/components\/[^/]+\//.test(c)&&!/^cvx-kit\/components\/[^/]+\/convex\.config(?:\.js)?$/.test(c)&&e.report({node:s,messageId:`facade`});return}let l=i(n(e.filename),c);o&&(r(o,l).startsWith(`..${a}`)||l===n(o))?e.report({node:s,messageId:`boundary`}):!o&&v(e,l)&&!/^client(?:\.[cm]?[jt]s)?$/.test(t(l))&&!(u(e)===`convex.config.ts`&&/^convex\.config(?:\.[cm]?[jt]s)?$/.test(t(l)))&&e.report({node:s,messageId:`facade`})})}},"no-component-env":{meta:_(`Pass environment configuration into portable components.`,{env:`Portable components must receive configuration through arguments; do not read process.env.`}),create(e){if(!v(e,e.filename))return{};function t(t){if(t.type!==`Identifier`)return!1;let n=x(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`&&C(n.imported)===`env`&&e.report({node:n,messageId:`env`})},MemberExpression(n){if(b(n)!==`env`)return;let r=n.object,i=r.type===`MemberExpression`&&b(r)===`process`&&r.object.type===`Identifier`&&r.object.name===`globalThis`&&!x(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?y(t.key):C(t.key))===`env`&&e.report({node:t,messageId:`env`})}}}},"schema-file-boundaries":{meta:_(`Keep storage shapes and indexes in their documented owners.`,{schema:`{{builder}} belongs in {{file}}.`}),create(e){return{CallExpression(n){let r=d(e,n.callee),i=u(e);if(!i)return;let a=null;r?.source===`convex/server`&&(r.name===`defineSchema`&&i!==`schema.ts`&&!(v(e,e.filename)&&t(i)===`schema.ts`)&&(a=`the root schema.ts (or a component schema.ts)`),r.name===`defineTable`&&!(v(e,e.filename)&&[`schema.ts`,`table.ts`].includes(t(i)))&&(a=`a component schema.ts/table.ts; host tables use zodTable in domain/<module>/schema.ts`)),r&&[`cvx-kit`,`cvx-kit/zod-table`].includes(r.source)&&[`zodTable`,`tenantTable`,`zodVariantTable`].includes(r.name)&&!/^domain\/[^/]+\/schema\.ts$/.test(i)&&(a=`domain/<module>/schema.ts`),a&&e.report({node:n,messageId:`schema`,data:{builder:r?.name??`table`,file:a}}),n.callee.type===`MemberExpression`&&[`index`,`searchIndex`,`vectorIndex`].includes(b(n.callee)??``)&&m(n.callee.object)&&!v(e,e.filename)&&!/^domain\/[^/]+\/table\.ts$/.test(i)&&e.report({node:n,messageId:`schema`,data:{builder:`Table indexes`,file:`domain/<module>/table.ts`}})}}}},"no-internal-reexports":{meta:_(`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:[])x(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`&&x(e,t.declaration)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:t,messageId:`reexport`})}}}},"public-api-first":{meta:_(`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:_(`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`})}}}}}),c={meta:{name:`cvx`},rules:s};function l(e){let t={...e},n=Object.keys(e);for(let r of n){let n=e[r];t[r]={meta:{...n.meta,schema:[{type:`object`,properties:{convexDir:{type:`string`,minLength:1}},additionalProperties:!1}]},create(e){let t=u(e);return!t||/(?:^|\/)(?:_generated|__tests__)(?:\/|$)/.test(t)||/\.(?:test|spec)\.[cm]?[jt]s$/.test(t)?{}:n.create(e)}}}return t}function u(e,t=e.filename){let n=i(e.cwd,o.parse(e.options[0]??{}).convexDir),s=r(n,t).split(a).join(`/`);return s===`..`||s.startsWith(`../`)||s.startsWith(`/`)?null:s}function d(e,t){if(t.type===`CallExpression`)return d(e,t.callee);if(t.type===`MemberExpression`){let n=d(e,t.object),r=b(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 x(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`?C(e.imported)??``:e.type===`ImportDefaultSpecifier`?`default`:`*`}}return null}function f(e,t){function n(e,n){let r=y(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`&&!x(e,t.callee)?.defs.length&&n(t,t.arguments[0])}}}function p(e,t){return t.startsWith(`.`)&&u(e,i(n(e.filename),t))?.replace(/\.[cm]?[jt]s$/,``)===`functions`}function m(e){return e.type===`MemberExpression`?b(e)===`table`||m(e.object):e.type===`CallExpression`&&m(e.callee)}function h(e){return e.callee.type===`MemberExpression`&&[`insert`,`patch`,`replace`,`delete`].includes(b(e.callee)??``)&&e.callee.object.type===`MemberExpression`&&b(e.callee.object)===`db`}function g(e,t){return e.type===`CallExpression`?e.callee.type===`MemberExpression`&&b(e.callee)===t&&(t!==`query`||b(e.callee.object)===`db`)||g(e.callee,t):e.type===`MemberExpression`&&g(e.object,t)}function _(e,t){return{type:`problem`,docs:{description:e},schema:[],messages:t}}function v(e,t){let n=i(e.cwd,o.parse(e.options[0]??{}).convexDir),s=r(n,t).split(a).join(`/`).match(/^components\/([^/]+)(?:\/|$)/);return s?i(n,`components`,s[1]):null}function y(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 b(e){return e.type===`MemberExpression`?e.computed?y(e.property):e.property.name:null}function x(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 S(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(C(e.local)??``);else n.type===`ExportDefaultDeclaration`&&n.declaration.type===`Identifier`&&t.add(n.declaration.name);return t}function C(e){return e.type===`Identifier`?e.name:y(e)}export{c as default,s 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, resolve, sep } from 'node:path'\nimport type {\n\tContext,\n\tCreateRule,\n\tESTree,\n\tPlugin,\n\tRule,\n\tRuleMeta,\n\tScope,\n\tVisitor,\n} from '@oxlint/plugins'\nimport { z } from 'zod'\n\nconst options = z.object({ convexDir: z.string().min(1).default('convex') })\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\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'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\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tif (node.importKind === 'type') return\n\t\t\t\t\tconst source = node.source.value\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\treturn sourceVisitors(context, (node, source) => {\n\t\t\t\tif (!file || !source.startsWith('.')) return\n\t\t\t\tconst target = appFile(context, resolve(dirname(context.filename), source))?.replace(\n\t\t\t\t\t/\\.[cm]?[jt]s$/,\n\t\t\t\t\t'',\n\t\t\t\t)\n\t\t\t\tif (!target) return\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'].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\tto[0] === 'api' ||\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] !== basename(file).replace(/\\.[cm]?[jt]s$/, ''))\n\t\t\t\t} else if (from[0] === 'application') forbidden = to[0] === 'api'\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\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\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(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\nfunction appFile(context: Context, filename = context.filename) {\n\tconst root = resolve(context.cwd, options.parse(context.options[0] ?? {}).convexDir)\n\tconst file = relative(root, filename).split(sep).join('/')\n\treturn file === '..' || file.startsWith('../') || file.startsWith('/') ? null : file\n}\n\nfunction imported(context: Context, node: ESTree.Node): { 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\nfunction 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\nfunction isFunctionsImport(context: Context, source: string) {\n\treturn (\n\t\tsource.startsWith('.') &&\n\t\tappFile(context, resolve(dirname(context.filename), source))?.replace(/\\.[cm]?[jt]s$/, '') ===\n\t\t\t'functions'\n\t)\n}\n\nfunction 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\nfunction 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\nfunction 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\nfunction metadata(description: string, messages: Record<string, string>): RuleMeta {\n\treturn { type: 'problem', docs: { description }, schema: [], messages }\n}\n\nfunction componentRoot(context: Context, filename: string) {\n\tconst root = resolve(context.cwd, options.parse(context.options[0] ?? {}).convexDir)\n\tconst file = relative(root, filename).split(sep).join('/')\n\tconst match = file.match(/^components\\/([^/]+)(?:\\/|$)/)\n\treturn match ? resolve(root, 'components', 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":"8GAaA,MAAM,EAAU,EAAE,OAAO,CAAE,UAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,QAAQ,CAAE,CAAC,EAigB9D,EAAgC,EAAW,CA5evD,wBAAyB,CACxB,KAAM,EAAS,+DAAgE,CAC9E,OAAQ,2EACT,CAAC,EACD,OAAO,EAAS,CACf,GAAI,EAAc,EAAS,EAAQ,QAAQ,EAAG,MAAO,CAAC,EACtD,IAAM,EAAO,IAAI,IACjB,SAAS,EAAM,EAAoD,CAClE,IAAM,EAAM,EAAS,EAAS,EAAK,MAAM,EACzC,GAAI,CAAC,EAAK,OACV,IAAI,EAAsB,KACtB,CAAC,UAAW,cAAc,CAAC,CAAC,SAAS,EAAI,MAAM,GAAK,EAAI,OAAS,wBACpE,EAAO,gBACJ,CAAC,UAAW,kBAAkB,CAAC,CAAC,SAAS,EAAI,MAAM,GAAK,EAAI,OAAS,mBACxE,EAAO,eAEP,CAAC,UAAW,+BAA+B,CAAC,CAAC,SAAS,EAAI,MAAM,GAChE,EAAI,OAAS,eAEb,EAAO,iBACH,KACD,EAAQ,CAAO,IAAM,GAAQ,EAAK,IAAI,EAAI,IAAI,IACjD,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,KAAM,CAAE,KAAM,EAAI,KAAM,MAAK,CAAE,CAAC,EAC7E,EAAK,IAAI,EAAI,IAAI,EAClB,CACA,MAAO,CAAE,eAAgB,EAAO,cAAe,CAAM,CACtD,CACD,EACA,kBAAmB,CAClB,KAAM,EAAS,iEAAkE,CAChF,QACC,+FACF,CAAC,EACD,OAAO,EAAS,CAEf,OADI,EAAQ,CAAO,IAAM,gBAAkB,EAAc,EAAS,EAAQ,QAAQ,EAAU,CAAC,EACtF,CACN,kBAAkB,EAAM,CACvB,GAAI,EAAK,aAAe,OAAQ,OAChC,IAAM,EAAS,EAAK,OAAO,MAE1B,MAAC,+CAA+C,KAAK,CAAM,GAC3D,IAAW,iBAGZ,IAAK,IAAM,KAAa,EAAK,WAAY,CACxC,GAAI,EAAU,OAAS,mBAAqB,EAAU,aAAe,OAAQ,SAC7E,IAAM,EACL,EAAU,OAAS,kBAAoB,EAAW,EAAU,QAAQ,EAAI,MAEvE,IAAS,MAAQ,IAAW,iBAC5B,GACA,+FAA+F,KAC9F,CACD,IAED,EAAQ,OAAO,CAAE,KAAM,EAAW,UAAW,SAAU,CAAC,CAE1D,CACD,EACA,eAAe,EAAM,CACpB,IAAM,EAAM,EAAS,EAAS,EAAK,MAAM,EAExC,GAAK,SAAW,iBAChB,qFAAqF,KACpF,EAAI,IACL,GACA,EAAK,OAAO,OAAS,oBAErB,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,CAAC,CAE/C,CACD,CACD,CACD,EACA,4BAA6B,CAC5B,KAAM,EAAS,yCAA0C,CACxD,UAAW,yEACZ,CAAC,EACD,OAAO,EAAS,CACf,MAAO,CACN,eAAe,EAAM,CACpB,IAAM,EAAM,EAAS,EAAS,EAAK,MAAM,EACrC,GAAK,SAAW,iBAAmB,EAAI,OAAS,yBACnD,EAAQ,OAAO,CAAE,OAAM,UAAW,WAAY,CAAC,CACjD,CACD,CACD,CACD,EACA,2BAA4B,CAC3B,KAAM,EAAS,mEAAoE,CAClF,SACC,gGACF,CAAC,EACD,OAAO,EAAS,CACf,IAAM,EAAO,EAAQ,CAAO,EAC5B,OAAO,EAAe,GAAU,EAAM,IAAW,CAChD,GAAI,CAAC,GAAQ,CAAC,EAAO,WAAW,GAAG,EAAG,OACtC,IAAM,EAAS,EAAQ,EAAS,EAAQ,EAAQ,EAAQ,QAAQ,EAAG,CAAM,CAAC,CAAC,EAAE,QAC5E,gBACA,EACD,EACA,GAAI,CAAC,EAAQ,OACb,IAAM,EAAO,EAAK,MAAM,GAAG,EACrB,EAAK,EAAO,MAAM,GAAG,EACvB,EAAY,GACZ,EAAK,KAAO,UACf,EAAY,CAAC,MAAO,aAAa,CAAC,CAAC,SAAS,EAAG,EAAE,EAEhD,EAAG,KAAO,UACV,EAAK,KAAO,EAAG,IACf,EAAG,KAAO,UACV,EAAK,KAAO,aAEZ,EACC,EAAK,KAAO,UAAY,CAAC,CAAC,QAAS,SAAS,CAAC,CAAC,SAAS,EAAG,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,IAEpE,EAAK,KAAO,MACtB,EACC,EAAG,KAAO,OACT,EAAG,KAAO,UACV,EAAG,KAAO,UACV,EAAG,KAAO,EAAS,CAAI,CAAC,CAAC,QAAQ,gBAAiB,EAAE,EAC5C,EAAK,KAAO,gBAAe,EAAY,EAAG,KAAO,OACxD,GAAW,EAAQ,OAAO,CAAE,OAAM,UAAW,UAAW,CAAC,CAC9D,CAAC,CACF,CACD,EACA,0BAA2B,CAC1B,KAAM,EAAS,2DAA4D,CAC1E,OAAQ,wEACT,CAAC,EACD,OAAO,EAAS,CAEf,OADI,EAAQ,CAAO,CAAC,EAAE,WAAW,MAAM,EAAU,CAAC,EAC3C,CACN,eAAe,EAAM,CACpB,IAAM,EAAM,EAAS,EAAS,EAAK,MAAM,EAExC,EAAK,OAAO,OAAS,kBACrB,GACA,EAAkB,EAAS,EAAI,MAAM,GACrC,6CAA6C,KAAK,EAAI,IAAI,GAE1D,EAAQ,OAAO,CAAE,OAAM,UAAW,QAAS,CAAC,CAC9C,CACD,CACD,CACD,EACA,oBAAqB,CACpB,KAAM,EAAS,0DAA2D,CACzE,QACC,+FACF,CAAC,EACD,OAAO,EAAS,CAEf,OADK,EAAQ,CAAO,CAAC,EAAE,WAAW,MAAM,EACjC,CACN,YAAY,EAAM,CACjB,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,CAAC,CAC9C,EACA,gBAAgB,EAAM,CACrB,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,CAAC,CAC9C,EACA,sBAAsB,EAAM,CAC3B,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,CAAC,CAC9C,EACA,QAAQ,EAAM,CACb,IAAM,EAAW,EAAc,CAAI,EACnC,IAAK,IAAM,KAAa,EAAK,KAO5B,GALC,EAAU,OAAS,uBACnB,EAAU,IACV,CAAC,EAAS,IAAI,EAAU,GAAG,IAAI,GAE/B,EAAQ,OAAO,CAAE,KAAM,EAAW,UAAW,SAAU,CAAC,EACrD,EAAU,OAAS,sBACjB,IAAA,IAAM,KAAe,EAAU,aAElC,EAAY,GAAG,OAAS,cACxB,CAAC,EAAS,IAAI,EAAY,GAAG,IAAI,GACjC,CAAC,0BAA2B,oBAAoB,CAAC,CAAC,SACjD,EAAY,MAAM,MAAQ,EAC3B,GAEA,EAAQ,OAAO,CAAE,KAAM,EAAa,UAAW,SAAU,CAAC,CAG/D,EACA,eAAe,EAAM,CAChB,EAAU,CAAI,GAAG,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,CAAC,CACnE,CACD,EApCkD,CAAC,CAqCpD,CACD,EACA,kBAAmB,CAClB,KAAM,EAAS,kDAAmD,CACjE,WAAY,wEACb,CAAC,EACD,OAAO,EAAS,CACf,MAAO,CACN,eAAe,EAAM,CACpB,IAAM,EAAM,EAAS,EAAS,EAAK,MAAM,EACrC,EAAS,EAAK,UAAU,GAC5B,KAAO,GAAQ,OAAS,kBAAoB,GAAQ,OAAS,yBAC5D,EAAS,EAAO,WAEhB,GACA,iBAAiB,KAAK,EAAI,MAAM,GAChC,EAAI,OAAS,QACb,GAAQ,OAAS,mBAEjB,EAAQ,OAAO,CAAE,OAAM,UAAW,YAAa,CAAC,CAClD,CACD,CACD,CACD,EACA,qBAAsB,CACrB,KAAM,EAAS,wDAAyD,CACvE,QACC,0GACF,CAAC,EACD,OAAO,EAAS,CAEf,OADI,EAAc,EAAS,EAAQ,QAAQ,EAAU,CAAC,EAC/C,CACN,eAAe,EAAM,CACpB,GAAI,EAAK,OAAO,OAAS,mBAAoB,OAC7C,IAAM,EAAO,EAAa,EAAK,MAAM,GAEnC,IAAS,WAAa,EAAQ,EAAK,OAAO,OAAQ,OAAO,GACzD,IAAS,WAAa,EAAQ,EAAK,OAAO,OAAQ,SAAS,IAE5D,EAAQ,OAAO,CAAE,OAAM,UAAW,SAAU,CAAC,CAC/C,CACD,CACD,CACD,EACA,uBAAwB,CACvB,KAAM,EAAS,uDAAwD,CACtE,UAAW,4EACZ,CAAC,EACD,OAAO,EAAS,CAEf,OADI,EAAc,EAAS,EAAQ,QAAQ,EAAU,CAAC,EAC/C,CACN,eAAe,EAAM,CACpB,GAAI,CAAC,EAAU,CAAI,EAAG,OACtB,IAAM,EAAO,EAAK,UAAU,GAAG,EAAE,EAC7B,MAAM,OAAS,mBACnB,IAAK,IAAM,KAAY,EAAK,WAAY,CACvC,GAAI,EAAS,OAAS,WAAY,SAClC,IAAM,EAAQ,EAAS,SAAW,EAAa,EAAS,GAAG,EAAI,EAAW,EAAS,GAAG,GAClF,IAAU,aAAe,IAAU,cACtC,EAAQ,OAAO,CAAE,KAAM,EAAU,UAAW,YAAa,KAAM,CAAE,OAAM,CAAE,CAAC,CAC5E,CACD,CACD,CACD,CACD,EACA,uBAAwB,CACvB,KAAM,EAAS,sEAAuE,CACrF,SACC,8GACD,OAAQ,kFACT,CAAC,EACD,OAAO,EAAS,CACf,IAAM,EAAQ,EAAc,EAAS,EAAQ,QAAQ,EAErD,OAAO,EAAe,GAAU,EAAM,IAAc,CACnD,GAAI,CAAC,EAAU,WAAW,GAAG,EAAG,CAE9B,gCAAgC,KAAK,CAAS,GAC9C,CAAC,wDAAwD,KAAK,CAAS,GAEvE,EAAQ,OAAO,CAAE,OAAM,UAAW,QAAS,CAAC,EAE7C,MACD,CACA,IAAM,EAAS,EAAQ,EAAQ,EAAQ,QAAQ,EAAG,CAAS,EAE1D,IACC,EAAS,EAAO,CAAM,CAAC,CAAC,WAAW,KAAK,GAAK,GAAK,IAAW,EAAQ,CAAK,GAE3E,EAAQ,OAAO,CAAE,OAAM,UAAW,UAAW,CAAC,EAE9C,CAAC,GACD,EAAc,EAAS,CAAM,GAC7B,CAAC,4BAA4B,KAAK,EAAS,CAAM,CAAC,GAClD,EACC,EAAQ,CAAO,IAAM,oBACrB,oCAAoC,KAAK,EAAS,CAAM,CAAC,IAG1D,EAAQ,OAAO,CAAE,OAAM,UAAW,QAAS,CAAC,CAE9C,CAAC,CACF,CACD,EACA,mBAAoB,CACnB,KAAM,EAAS,2DAA4D,CAC1E,IAAK,4FACN,CAAC,EACD,OAAO,EAAS,CACf,GAAI,CAAC,EAAc,EAAS,EAAQ,QAAQ,EAAG,MAAO,CAAC,EACvD,SAAS,EAAU,EAAmB,CACrC,GAAI,EAAK,OAAS,aAAc,MAAO,GACvC,IAAM,EAAW,EAAQ,EAAS,CAAI,EAEtC,OADI,EAAK,OAAS,WAAa,CAAC,GAAU,KAAK,QACxC,GAAU,KAAK,KACpB,GACA,EAAI,OAAS,iBACb,EAAI,QAAQ,OAAS,qBACrB,CAAC,eAAgB,SAAS,CAAC,CAAC,SAAS,EAAI,OAAO,OAAO,KAAK,GAC5D,CAAC,yBAA0B,0BAA0B,CAAC,CAAC,SAAS,EAAI,KAAK,IAAI,CAC/E,CACD,CACA,MAAO,CACN,kBAAkB,EAAM,CACvB,GAAI,CAAC,eAAgB,SAAS,CAAC,CAAC,SAAS,EAAK,OAAO,KAAK,EACpD,IAAA,IAAM,KAAa,EAAK,WAE3B,EAAU,OAAS,mBACnB,EAAW,EAAU,QAAQ,IAAM,OAEnC,EAAQ,OAAO,CAAE,KAAM,EAAW,UAAW,KAAM,CAAC,CAIxD,EACA,iBAAiB,EAAM,CACtB,GAAI,EAAa,CAAI,IAAM,MAAO,OAClC,IAAM,EAAS,EAAK,OACd,EACL,EAAO,OAAS,oBAChB,EAAa,CAAM,IAAM,WACzB,EAAO,OAAO,OAAS,cACvB,EAAO,OAAO,OAAS,cACvB,CAAC,EAAQ,EAAS,EAAO,MAAM,CAAC,EAAE,KAAK,QACpC,EAAU,CAAM,GAAK,IAAe,EAAQ,OAAO,CAAE,OAAM,UAAW,KAAM,CAAC,CAClF,EACA,mBAAmB,EAAM,CACxB,GAAI,EAAK,MAAQ,EAAU,EAAK,IAAI,GAAK,EAAK,GAAG,OAAS,gBACpD,IAAA,IAAM,KAAY,EAAK,GAAG,WAE7B,EAAS,OAAS,aACjB,EAAS,SAAW,EAAa,EAAS,GAAG,EAAI,EAAW,EAAS,GAAG,KACxE,OAED,EAAQ,OAAO,CAAE,KAAM,EAAU,UAAW,KAAM,CAAC,CAIvD,CACD,CACD,CACD,EACA,yBAA0B,CACzB,KAAM,EAAS,8DAA+D,CAC7E,OAAQ,kCACT,CAAC,EACD,OAAO,EAAS,CACf,MAAO,CACN,eAAe,EAAM,CACpB,IAAM,EAAM,EAAS,EAAS,EAAK,MAAM,EACnC,EAAO,EAAQ,CAAO,EAC5B,GAAI,CAAC,EAAM,OACX,IAAI,EAA0B,KAC1B,GAAK,SAAW,kBAElB,EAAI,OAAS,gBACb,IAAS,aACT,EAAE,EAAc,EAAS,EAAQ,QAAQ,GAAK,EAAS,CAAI,IAAM,eAEjE,EAAW,iDAEX,EAAI,OAAS,eACb,EACC,EAAc,EAAS,EAAQ,QAAQ,GACvC,CAAC,YAAa,UAAU,CAAC,CAAC,SAAS,EAAS,CAAI,CAAC,KAGlD,EACC,0FAGF,GACA,CAAC,UAAW,mBAAmB,CAAC,CAAC,SAAS,EAAI,MAAM,GACpD,CAAC,WAAY,cAAe,iBAAiB,CAAC,CAAC,SAAS,EAAI,IAAI,GAChE,CAAC,8BAA8B,KAAK,CAAI,IAExC,EAAW,6BACR,GACH,EAAQ,OAAO,CACd,OACA,UAAW,SACX,KAAM,CAAE,QAAS,GAAK,MAAQ,QAAS,KAAM,CAAS,CACvD,CAAC,EAED,EAAK,OAAO,OAAS,oBACrB,CAAC,QAAS,cAAe,aAAa,CAAC,CAAC,SAAS,EAAa,EAAK,MAAM,GAAK,EAAE,GAChF,EAAa,EAAK,OAAO,MAAM,GAC/B,CAAC,EAAc,EAAS,EAAQ,QAAQ,GACxC,CAAC,6BAA6B,KAAK,CAAI,GAEvC,EAAQ,OAAO,CACd,OACA,UAAW,SACX,KAAM,CAAE,QAAS,gBAAiB,KAAM,0BAA2B,CACpE,CAAC,CAEH,CACD,CACD,CACD,EACA,wBAAyB,CACxB,KAAM,EAAS,sEAAuE,CACrF,SAAU,gFACX,CAAC,EACD,OAAO,EAAS,CACf,SAAS,EAAM,EAAmE,CACjF,GAAI,EAAK,OAAQ,CAChB,EAAQ,OAAO,CAAE,OAAM,UAAW,UAAW,CAAC,EAC9C,MACD,CACA,IAAK,IAAM,KAAa,EAAK,OAAS,yBAA2B,EAAK,WAAa,CAAC,EAC/E,EAAQ,EAAS,EAAU,KAAK,CAAC,EAAE,KAAK,KAAM,GAAQ,EAAI,OAAS,eAAe,GACrF,EAAQ,OAAO,CAAE,KAAM,EAAW,UAAW,UAAW,CAAC,CAG5D,CACA,MAAO,CACN,uBAAwB,EACxB,qBAAsB,EACtB,yBAAyB,EAAM,CAE7B,EAAK,YAAY,OAAS,cAC1B,EAAQ,EAAS,EAAK,WAAW,CAAC,EAAE,KAAK,KAAM,GAAQ,EAAI,OAAS,eAAe,GAEnF,EAAQ,OAAO,CAAE,OAAM,UAAW,UAAW,CAAC,CAEhD,CACD,CACD,CACD,EACA,mBAAoB,CACnB,KAAM,EAAS,+DAAgE,CAC9E,MACC,0GACF,CAAC,EACD,OAAO,EAAS,CACf,MAAO,CACN,QAAQ,EAAM,CACb,IAAM,EAAU,EAAc,CAAI,EAC5B,EAAa,EAAK,KAAK,cAAe,GAC3C,EAAU,KAAK,WAAW,QAAQ,CACnC,EACA,IAAK,IAAM,KAAa,EAAK,KAAK,MAAM,EAAG,EAAa,EAAI,EAAI,CAAU,EAExE,EAAU,OAAS,uBACnB,EAAU,IACV,CAAC,EAAQ,IAAI,EAAU,GAAG,IAAI,GAE9B,EAAQ,OAAO,CAAE,KAAM,EAAW,UAAW,OAAQ,CAAC,CAGzD,CACD,CACD,CACD,EACA,wBAAyB,CACxB,KAAM,EAAS,iEAAkE,CAChF,OAAQ,gFACT,CAAC,EACD,OAAO,EAAS,CACf,MAAO,CACN,QAAQ,EAAM,CACb,IAAM,EAAU,EAAc,CAAI,EAClC,IAAK,IAAM,KAAa,EAAK,KACxB,KAAU,OAAS,sBACvB,IAAK,IAAM,KAAe,EAAU,aAElC,EAAY,GAAG,OAAS,cACxB,CAAC,EAAQ,IAAI,EAAY,GAAG,IAAI,IAC/B,EAAY,MAAM,OAAS,2BAC3B,EAAY,MAAM,OAAS,uBAE5B,EAAQ,OAAO,CAAE,KAAM,EAAa,UAAW,QAAS,CAAC,CAI7D,CACD,CACD,CACD,CAGuD,CAAgB,EAClE,EAAiB,CAAE,KAAM,CAAE,KAAM,KAAM,EAAG,OAAM,EAGtD,SAAS,EAAW,EAA8D,CACjF,IAAM,EAAS,CAAE,GAAG,CAAO,EAErB,EAAQ,OAAO,KAAK,CAAM,EAChC,IAAK,IAAM,KAAQ,EAAO,CACzB,IAAM,EAAO,EAAO,GACpB,EAAO,GAAQ,CACd,KAAM,CACL,GAAG,EAAK,KACR,OAAQ,CACP,CACC,KAAM,SACN,WAAY,CAAE,UAAW,CAAE,KAAM,SAAU,UAAW,CAAE,CAAE,EAC1D,qBAAsB,EACvB,CACD,CACD,EACA,OAAO,EAAS,CACf,IAAM,EAAO,EAAQ,CAAO,EAO5B,MALC,CAAC,GACD,2CAA2C,KAAK,CAAI,GACpD,+BAA+B,KAAK,CAAI,EAEjC,CAAC,EACF,EAAK,OAAO,CAAO,CAC3B,CACD,CACD,CACA,OAAO,CACR,CAEA,SAAS,EAAQ,EAAkB,EAAW,EAAQ,SAAU,CAC/D,IAAM,EAAO,EAAQ,EAAQ,IAAK,EAAQ,MAAM,EAAQ,QAAQ,IAAM,CAAC,CAAC,CAAC,CAAC,SAAS,EAC7E,EAAO,EAAS,EAAM,CAAQ,CAAC,CAAC,MAAM,CAAG,CAAC,CAAC,KAAK,GAAG,EACzD,OAAO,IAAS,MAAQ,EAAK,WAAW,KAAK,GAAK,EAAK,WAAW,GAAG,EAAI,KAAO,CACjF,CAEA,SAAS,EAAS,EAAkB,EAA4D,CAC/F,GAAI,EAAK,OAAS,iBAAkB,OAAO,EAAS,EAAS,EAAK,MAAM,EACxE,GAAI,EAAK,OAAS,mBAAoB,CACrC,IAAM,EAAQ,EAAS,EAAS,EAAK,MAAM,EACrC,EAAO,EAAa,CAAI,EAC9B,OAAO,GAAS,GAAQ,CAAC,IAAK,IAAK,SAAS,CAAC,CAAC,SAAS,EAAM,IAAI,EAC9D,CAAE,OAAQ,EAAM,OAAQ,MAAK,EAC7B,IACJ,CACA,GAAI,EAAK,OAAS,aAAc,OAAO,KACvC,IAAK,IAAM,KAAO,EAAQ,EAAS,CAAI,CAAC,EAAE,MAAQ,CAAC,EAAG,CACrD,GACC,EAAI,OAAS,iBACb,EAAI,QAAQ,OAAS,qBACrB,EAAI,OAAO,aAAe,OAE1B,SACD,IAAM,EAAY,EAAI,KAClB,KAAU,OAAS,mBAAqB,EAAU,aAAe,OACrE,MAAO,CACN,OAAQ,EAAI,OAAO,OAAO,MAC1B,KACC,EAAU,OAAS,kBACf,EAAW,EAAU,QAAQ,GAAK,GACnC,EAAU,OAAS,yBAClB,UACA,GACN,CACD,CACA,OAAO,IACR,CAEA,SAAS,EACR,EACA,EACU,CACV,SAAS,EAAM,EAAmB,EAAwC,CACzE,IAAM,EAAO,EAAa,CAAM,EAC5B,GAAM,EAAM,EAAM,CAAI,CAC3B,CACA,MAAO,CACN,kBAAkB,EAAM,CACvB,EAAM,EAAM,EAAK,MAAM,CACxB,EACA,uBAAuB,EAAM,CAC5B,EAAM,EAAM,EAAK,MAAM,CACxB,EACA,qBAAqB,EAAM,CAC1B,EAAM,EAAM,EAAK,MAAM,CACxB,EACA,iBAAiB,EAAM,CACtB,EAAM,EAAM,EAAK,MAAM,CACxB,EACA,aAAa,EAAM,CAClB,EAAM,EAAM,EAAK,MAAM,CACxB,EACA,eAAe,EAAM,CAEnB,EAAK,OAAO,OAAS,cACrB,EAAK,OAAO,OAAS,WACrB,CAAC,EAAQ,EAAS,EAAK,MAAM,CAAC,EAAE,KAAK,QAErC,EAAM,EAAM,EAAK,UAAU,EAAE,CAC/B,CACD,CACD,CAEA,SAAS,EAAkB,EAAkB,EAAgB,CAC5D,OACC,EAAO,WAAW,GAAG,GACrB,EAAQ,EAAS,EAAQ,EAAQ,EAAQ,QAAQ,EAAG,CAAM,CAAC,CAAC,EAAE,QAAQ,gBAAiB,EAAE,IACxF,WAEH,CAEA,SAAS,EAAa,EAA4B,CAGjD,OAFI,EAAK,OAAS,mBACV,EAAa,CAAI,IAAM,SAAW,EAAa,EAAK,MAAM,EAC3D,EAAK,OAAS,kBAAoB,EAAa,EAAK,MAAM,CAClE,CAEA,SAAS,EAAU,EAA6B,CAC/C,OACC,EAAK,OAAO,OAAS,oBACrB,CAAC,SAAU,QAAS,UAAW,QAAQ,CAAC,CAAC,SAAS,EAAa,EAAK,MAAM,GAAK,EAAE,GACjF,EAAK,OAAO,OAAO,OAAS,oBAC5B,EAAa,EAAK,OAAO,MAAM,IAAM,IAEvC,CAEA,SAAS,EAAQ,EAAmB,EAAuB,CAS1D,OARI,EAAK,OAAS,iBAEf,EAAK,OAAO,OAAS,oBACrB,EAAa,EAAK,MAAM,IAAM,IAC7B,IAAS,SAAW,EAAa,EAAK,OAAO,MAAM,IAAM,OAC3D,EAAQ,EAAK,OAAQ,CAAI,EAEvB,EAAK,OAAS,oBAA2B,EAAQ,EAAK,OAAQ,CAAI,CAEvE,CAEA,SAAS,EAAS,EAAqB,EAA4C,CAClF,MAAO,CAAE,KAAM,UAAW,KAAM,CAAE,aAAY,EAAG,OAAQ,CAAC,EAAG,UAAS,CACvE,CAEA,SAAS,EAAc,EAAkB,EAAkB,CAC1D,IAAM,EAAO,EAAQ,EAAQ,IAAK,EAAQ,MAAM,EAAQ,QAAQ,IAAM,CAAC,CAAC,CAAC,CAAC,SAAS,EAE7E,EADO,EAAS,EAAM,CAAQ,CAAC,CAAC,MAAM,CAAG,CAAC,CAAC,KAAK,GACrC,CAAC,CAAC,MAAM,8BAA8B,EACvD,OAAO,EAAQ,EAAQ,EAAM,aAAc,EAAM,EAAE,EAAI,IACxD,CAEA,SAAS,EAAa,EAAsC,CAK3D,OAJI,GAAM,OAAS,WAAa,UAAW,GAAQ,OAAO,EAAK,KAAK,IAAM,EAAK,MACvE,OAAO,EAAK,KAAK,EACrB,GAAM,OAAS,mBAAqB,EAAK,YAAY,SAAW,EAC5D,EAAK,OAAO,EAAE,CAAC,MAAM,OACtB,IACR,CAEA,SAAS,EAAa,EAAkC,CAEvD,OADI,EAAK,OAAS,mBACX,EAAK,SAAW,EAAa,EAAK,QAAQ,EAAI,EAAK,SAAS,KADtB,IAE9C,CAEA,SAAS,EAAQ,EAAkB,EAAmB,CACrD,GAAI,EAAK,OAAS,aAAc,OAAO,KACvC,IAAI,EAAsB,EAAQ,WAAW,SAAS,CAAI,EAC1D,KAAO,GAAO,CACb,IAAM,EAAW,EAAM,IAAI,IAAI,EAAK,IAAI,EACxC,GAAI,EAAU,OAAO,EACrB,EAAQ,EAAM,KACf,CACA,OAAO,IACR,CAEA,SAAS,EAAc,EAAyB,CAC/C,IAAM,EAAQ,IAAI,IAClB,IAAK,IAAM,KAAa,EAAQ,KAC/B,GAAI,EAAU,OAAS,0BAA4B,CAAC,EAAU,OAC7D,IAAK,IAAM,KAAa,EAAU,WAAY,EAAM,IAAI,EAAW,EAAU,KAAK,GAAK,EAAE,OAEzF,EAAU,OAAS,4BACnB,EAAU,YAAY,OAAS,cAE/B,EAAM,IAAI,EAAU,YAAY,IAAI,EAGtC,OAAO,CACR,CAEA,SAAS,EAAW,EAAkC,CACrD,OAAO,EAAK,OAAS,aAAe,EAAK,KAAO,EAAa,CAAI,CAClE"}
|
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,98 @@
|
|
|
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
|
+
These consumer rules are available from 0.1.5; the initial 0.1.4 plugin targeted
|
|
9
|
+
library authoring. 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
|
+
Rules ignore files outside that directory, `_generated/`, `__tests__/`, and
|
|
40
|
+
files ending in `.test` or `.spec` before their JS/TS extension. 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
|
+
| `root-facade-ownership` | `createAuthFunctions`, `createTriggers`, and `new Foundation` outside their root files, or repeated in one file. | `conventions.md`, Root files |
|
|
50
|
+
| `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 |
|
|
51
|
+
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules |
|
|
52
|
+
| `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 |
|
|
53
|
+
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure |
|
|
54
|
+
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file |
|
|
55
|
+
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies |
|
|
56
|
+
| `no-unbounded-reads` | Direct `ctx.db.query(...).collect()` chains and `ctx.include(...).resolve()` chains in host code. | `llms.md`, Bounded reads; `zod-table.md` |
|
|
57
|
+
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md` |
|
|
58
|
+
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import |
|
|
59
|
+
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules |
|
|
60
|
+
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology |
|
|
61
|
+
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order |
|
|
62
|
+
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order |
|
|
63
|
+
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order |
|
|
64
|
+
|
|
65
|
+
## Allowed patterns
|
|
66
|
+
|
|
67
|
+
- `functions.ts` imports raw builders to configure `createAuthFunctions`.
|
|
68
|
+
Type-only imports such as `MutationCtx` from `_generated/server` are valid
|
|
69
|
+
elsewhere. `httpAction` remains available for HTTP routing.
|
|
70
|
+
- Domain modules may import sibling `rules.ts` and `queries.ts`, as explicitly
|
|
71
|
+
allowed by the module-grain section. Cross-module writes go through the
|
|
72
|
+
optional `application/` layer. `domain/table.ts` combines module table maps.
|
|
73
|
+
- API adapters may delegate and perform the documented bounded inline query
|
|
74
|
+
and DTO projection. `system*` constructors may live in domain modules.
|
|
75
|
+
- Authored components under `convex/components/<name>/` may use their own raw
|
|
76
|
+
builders and raw tables. Host-specific facade, timestamp, and bounded-read
|
|
77
|
+
rules do not apply there. Configuration imports for mounting components are
|
|
78
|
+
allowed from root `convex.config.ts`.
|
|
79
|
+
- Host schemas use kit table constructors in `domain/<module>/schema.ts` and
|
|
80
|
+
attach indexes in `domain/<module>/table.ts`. `schema.ts` assembles them.
|
|
81
|
+
- `archivedAt` remains application-controlled. Singular `db.get` reads and
|
|
82
|
+
bounded `execute`/`paginate` calls are allowed.
|
|
83
|
+
|
|
84
|
+
## What still needs review and tests
|
|
85
|
+
|
|
86
|
+
These are syntax and lexical-binding checks, not whole-program validation.
|
|
87
|
+
They resolve relative import paths, not tsconfig aliases, symlinks, or computed
|
|
88
|
+
paths. Named import aliases and namespaces are recognized where documented;
|
|
89
|
+
arbitrary variable reassignment, wrapper functions, destructured database
|
|
90
|
+
methods, and calls split across variables are not traced. Database checks
|
|
91
|
+
recognize the documented `ctx.db` member shape, not its runtime type.
|
|
92
|
+
|
|
93
|
+
The plugin does not prove DTO redaction, complete command/audit coverage,
|
|
94
|
+
tenant/RLS correctness, unique table ownership across files, required facade
|
|
95
|
+
existence, exactly one component mount, numeric read limits, or vocabulary
|
|
96
|
+
tuple ownership. Keep the architecture tests from `conventions.md` and the
|
|
97
|
+
behavioral tests described in `maintainability.md`. A clean lint run is not
|
|
98
|
+
evidence of those runtime guarantees.
|
package/docs/upgrading.md
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# Upgrading to 0.1.0 — the new way of things
|
|
2
2
|
|
|
3
|
+
## 0.1.5 — consumer Oxlint rules
|
|
4
|
+
|
|
5
|
+
The initial 0.1.4 plugin incorrectly targeted this package's `src/` layout.
|
|
6
|
+
`cvx-kit/oxlint` now targets consuming applications under `convex/`, following
|
|
7
|
+
`conventions.md`. The six existing rule names remain, with consumer semantics;
|
|
8
|
+
nine additional rules cover builders, generated references, domain imports,
|
|
9
|
+
public adapters, facade ownership, vocabularies, reads, and timestamps.
|
|
10
|
+
|
|
11
|
+
Remove `entryPoints` and `packageName` options from your consumer lint config.
|
|
12
|
+
Applications have no barrel entry points inside `convex/`. Every rule now
|
|
13
|
+
accepts only an optional `convexDir`, relative to the linter's working directory
|
|
14
|
+
(default: `convex`). Enable the new rules explicitly; loading the plugin alone
|
|
15
|
+
does not enable them. See `oxlint.md` for coverage and the root README for a
|
|
16
|
+
complete configuration. Repository authoring checks remain local tooling.
|
|
17
|
+
|
|
18
|
+
No runtime API or storage migration is part of this correction.
|
|
19
|
+
|
|
3
20
|
## 0.1.4
|
|
4
21
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
automatically. Select rules in your lint configuration and list your public
|
|
8
|
-
entry points in `cvx/no-internal-reexports`.
|
|
22
|
+
This release introduced the optional `cvx-kit/oxlint` export. Its original
|
|
23
|
+
rules targeted library authoring; see the correction above for consumer use.
|
|
9
24
|
|
|
10
25
|
This patch tightens several TypeScript contracts during the anti-slop cleanup:
|
|
11
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cvx-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -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, 15)
|
|
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,21 @@ 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
|
)
|
|
52
51
|
const oxlint = join(fixture, 'node_modules/.bin/oxlint')
|
|
53
|
-
run(oxlint, ['--config', '.oxlintrc.json', '
|
|
52
|
+
run(oxlint, ['--config', '.oxlintrc.json', 'convex'], fixture)
|
|
54
53
|
writeFileSync(
|
|
55
|
-
join(fixture, '
|
|
54
|
+
join(fixture, 'convex/components/example/bad.ts'),
|
|
56
55
|
`
|
|
57
56
|
import { defineTable } from 'convex/server'
|
|
58
57
|
import { host } from '../../host'
|
|
@@ -64,13 +63,34 @@ export const value = privateHelper()
|
|
|
64
63
|
`,
|
|
65
64
|
)
|
|
66
65
|
writeFileSync(
|
|
67
|
-
join(fixture, '
|
|
68
|
-
"export const host = 1
|
|
66
|
+
join(fixture, 'convex/host.ts'),
|
|
67
|
+
"import { authMutation } from './functions'; export const save = authMutation({}); export const host = 1; import './components/example/bad'\n",
|
|
68
|
+
)
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(fixture, 'convex/api/orders.ts'),
|
|
71
|
+
`import { mutation } from '../_generated/server'
|
|
72
|
+
import { makeFunctionReference as ref } from 'convex/server'
|
|
73
|
+
import { z } from 'zod'
|
|
74
|
+
import { createAuthFunctions } from 'cvx-kit/auth'
|
|
75
|
+
import { other } from '../domain/users/commands'
|
|
76
|
+
export const save = mutation({ handler: async (ctx) => {
|
|
77
|
+
await ctx.db.insert('orders', { updatedAt: Date.now() })
|
|
78
|
+
return ctx.db.query('orders').collect()
|
|
79
|
+
}})
|
|
80
|
+
export const constructors = createAuthFunctions({})
|
|
81
|
+
export const state = z.enum(['draft'])
|
|
82
|
+
export const target = ref('orders:save')
|
|
83
|
+
export const value = other
|
|
84
|
+
`,
|
|
85
|
+
)
|
|
86
|
+
const invalid = spawnSync(
|
|
87
|
+
oxlint,
|
|
88
|
+
['--config', '.oxlintrc.json', '--format', 'json', 'convex'],
|
|
89
|
+
{
|
|
90
|
+
cwd: fixture,
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
},
|
|
69
93
|
)
|
|
70
|
-
const invalid = spawnSync(oxlint, ['--config', '.oxlintrc.json', '--format', 'json', 'src'], {
|
|
71
|
-
cwd: fixture,
|
|
72
|
-
encoding: 'utf8',
|
|
73
|
-
})
|
|
74
94
|
assert.ifError(invalid.error)
|
|
75
95
|
assert.equal(invalid.status, 1, invalid.stderr)
|
|
76
96
|
const diagnostics = JSON.parse(invalid.stdout).diagnostics
|
|
@@ -109,7 +129,7 @@ void rule
|
|
|
109
129
|
fixture,
|
|
110
130
|
)
|
|
111
131
|
console.log(
|
|
112
|
-
`${installer}: packed Oxlint plugin loads, all
|
|
132
|
+
`${installer}: packed Oxlint plugin loads, all consumer rules report, and declarations typecheck`,
|
|
113
133
|
)
|
|
114
134
|
}
|
|
115
135
|
} finally {
|