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 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
- ## Component authoring conventions
188
+ ## Application lint rules
189
189
 
190
- The TypeScript Oxlint plugin ships as `cvx-kit/oxlint`. Load it in your
191
- `.oxlintrc.json`:
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": ["error", { "entryPoints": ["src/index.ts"] }],
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`. Component
208
- rules target `src/components/<name>/`; set your public files in `entryPoints`
209
- (paths relative to the linter's working directory). To block imports back
210
- into your own package, configure `component-boundaries` with
211
- `["error", { "packageName": "your-package" }]`. The plugin exports its default
212
- plugin object, named `rules`, and the `RuleName` type. Verified with Oxlint
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{v as o}from"convex/values";import{AuditLog as s}from"convex-audit-log";const c=new s(e.auditLog,{samplingEnabled:!1}),l=n({args:{resourceType:o.string(),resourceRef:o.string(),limit:o.optional(o.number())},returns:o.array(o.object({action:o.string(),actorRef:o.optional(o.string()),correlationKey:o.string(),transition:o.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:o.number(),batchSize:o.number()},returns:o.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};
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
@@ -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":"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"}
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
- The optional Oxlint plugin is available through `cvx-kit/oxlint`; the root
6
- README shows consumer configuration. Loading the plugin does not enable rules
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.4",
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, 6)
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, 'src/components/example'), { recursive: true })
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, 'src/public.ts'),
46
- "export { run } from './components/example/client'\n",
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, 'src/components/example/client.ts'),
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', 'src'], fixture)
52
+ run(oxlint, ['--config', '.oxlintrc.json', 'convex'], fixture)
54
53
  writeFileSync(
55
- join(fixture, 'src/components/example/bad.ts'),
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, 'src/host.ts'),
68
- "export const host = 1\nimport './components/example/bad'\n",
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 six rules report, and declarations typecheck`,
132
+ `${installer}: packed Oxlint plugin loads, all consumer rules report, and declarations typecheck`,
113
133
  )
114
134
  }
115
135
  } finally {