@voltro/cli 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +513 -0
- package/THIRD-PARTY-NOTICES.md +88 -2
- package/dist/{apiBuild-D22_EpoR.js → apiBuild-BESZbTjA.js} +4 -4
- package/dist/apiBuild-C8d74T6g.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-BPAtyWs8.js +2420 -0
- package/dist/checkCommand-DBvZvCh1.js +2 -0
- package/dist/cliRuntime-Oh517vCV.js +96 -0
- package/dist/{commands-jBX8no1I.js → commands-82BDEktg.js} +6476 -7672
- package/dist/dbCommand-CaIVTp-a.js +2 -0
- package/dist/{dbCommand-uuNCrFAb.js → dbCommand-bCBbk6tz.js} +3 -3
- package/dist/{dev-DcbIJrWg.js → dev-Cg4BKLmi.js} +3034 -2619
- package/dist/dev-DeEJj5T4.js +3 -0
- package/dist/fileConventions-Cof68_BL.js +33 -0
- package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-4Db40V23.js} +2 -2
- package/dist/frameworkTableAssembly-BTNirAAk.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspect-CUCCzw2I.js → inspect-BMvQpwqy.js} +211 -190
- package/dist/inspect-DHeaMZak.js +2 -0
- package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-DHYygE4s.js} +999 -961
- package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-C4R1EdHn.js} +1 -1
- package/dist/manifestBuild-Ck_90gZy.js +2 -0
- package/dist/sdkgen-CW5NbYF0.js +582 -0
- package/dist/{seedRunner-D6eu-u5U.js → seedRunner-Bqxgp7HZ.js} +60 -59
- package/dist/serveCommand-BXl8mhZD.js +1489 -0
- package/dist/serveEntry.js +3 -3
- package/dist/{start-BGXIf6zT.js → start-CsCIaW4-.js} +282 -269
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +2 -1
- package/templates/agent-docs/_index.md +2 -1
- package/templates/agent-docs/_manifest.json +10 -1
- package/templates/agent-docs/ai.md +135 -0
- package/templates/agent-docs/cli.md +124 -6
- package/templates/agent-docs/configuration.md +32 -0
- package/templates/agent-docs/data.md +282 -0
- package/templates/agent-docs/database/migrations.md +47 -0
- package/templates/agent-docs/database/misc.md +57 -0
- package/templates/agent-docs/database/schema.md +3 -1
- package/templates/agent-docs/deployment.md +56 -0
- package/templates/agent-docs/internationalization.md +161 -1
- package/templates/agent-docs/local-first-mobile.md +414 -0
- package/templates/agent-docs/routing.md +93 -2
- package/templates/agent-docs/schema-driven-ui.md +12 -0
- package/templates/agent-docs/templates/apibackends.md +1 -1
- package/templates/agent-docs/testing.md +42 -0
- package/templates/agent-docs/whats-new.md +165 -131
- package/templates/agent-docs/workflows.md +11 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/.env.example +19 -0
- package/templates/apps/api-backend-sqlite/README.md +38 -0
- package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
- package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-sqlite/package.json +30 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-sqlite/template.json +6 -0
- package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/README.md +104 -0
- package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
- package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
- package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
- package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
- package/templates/apps/api-cms/actions/me.action.ts +16 -0
- package/templates/apps/api-cms/app.config.ts +61 -0
- package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
- package/templates/apps/api-cms/content/index.ts +18 -0
- package/templates/apps/api-cms/content/page.contentType.ts +24 -0
- package/templates/apps/api-cms/database/schema.ts +64 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
- package/templates/apps/api-cms/package.json +32 -0
- package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
- package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
- package/templates/apps/api-cms/template.json +6 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
- package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
- package/templates/apps/api-cms/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/README.md +103 -0
- package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
- package/templates/apps/api-saas-starter/app.config.ts +87 -0
- package/templates/apps/api-saas-starter/database/schema.ts +57 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas-starter/package.json +32 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas-starter/template.json +6 -0
- package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
- package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
- package/templates/apps/api-saas-starter/tsconfig.json +5 -0
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/README.md +88 -0
- package/templates/apps/api-status/app.config.ts +36 -0
- package/templates/apps/api-status/authz.ts +33 -0
- package/templates/apps/api-status/database/schema.ts +70 -0
- package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
- package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
- package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
- package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
- package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
- package/templates/apps/api-status/package.json +30 -0
- package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/components.list.query.ts +13 -0
- package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
- package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
- package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
- package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
- package/templates/apps/api-status/template.json +6 -0
- package/templates/apps/api-status/tests/status.test.ts +70 -0
- package/templates/apps/api-status/tsconfig.json +5 -0
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/README.md +78 -0
- package/templates/apps/frontend-auth/app.config.ts +34 -0
- package/templates/apps/frontend-auth/package.json +32 -0
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
- package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
- package/templates/apps/frontend-auth/src/config.ts +11 -0
- package/templates/apps/frontend-auth/src/globals.css +105 -0
- package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
- package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
- package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
- package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
- package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
- package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
- package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
- package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
- package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
- package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
- package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
- package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
- package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
- package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
- package/templates/apps/frontend-auth/template.json +6 -0
- package/templates/apps/frontend-auth/tsconfig.json +5 -0
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/README.md +47 -0
- package/templates/apps/frontend-cms/app.config.ts +37 -0
- package/templates/apps/frontend-cms/package.json +33 -0
- package/templates/apps/frontend-cms/src/config.ts +8 -0
- package/templates/apps/frontend-cms/src/globals.css +105 -0
- package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
- package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
- package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
- package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
- package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
- package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
- package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
- package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
- package/templates/apps/frontend-cms/template.json +6 -0
- package/templates/apps/frontend-cms/tsconfig.json +5 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/README.md +71 -0
- package/templates/apps/frontend-portal/app.config.ts +37 -0
- package/templates/apps/frontend-portal/package.json +32 -0
- package/templates/apps/frontend-portal/src/config.ts +8 -0
- package/templates/apps/frontend-portal/src/globals.css +93 -0
- package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
- package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
- package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
- package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
- package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
- package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
- package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
- package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
- package/templates/apps/frontend-portal/template.json +6 -0
- package/templates/apps/frontend-portal/tsconfig.json +5 -0
- package/templates/apps/frontend-saas/README.md +73 -0
- package/templates/apps/frontend-saas/app.config.ts +46 -0
- package/templates/apps/frontend-saas/package.json +32 -0
- package/templates/apps/frontend-saas/src/config.ts +8 -0
- package/templates/apps/frontend-saas/src/globals.css +85 -0
- package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
- package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
- package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
- package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
- package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
- package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
- package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-saas/template.json +6 -0
- package/templates/apps/frontend-saas/tsconfig.json +5 -0
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/README.md +51 -0
- package/templates/apps/frontend-status/app.config.ts +39 -0
- package/templates/apps/frontend-status/package.json +32 -0
- package/templates/apps/frontend-status/src/config.ts +7 -0
- package/templates/apps/frontend-status/src/globals.css +70 -0
- package/templates/apps/frontend-status/src/globals.d.ts +6 -0
- package/templates/apps/frontend-status/src/lib/status.ts +81 -0
- package/templates/apps/frontend-status/src/locales/de.ts +41 -0
- package/templates/apps/frontend-status/src/locales/en.ts +45 -0
- package/templates/apps/frontend-status/src/locales/index.ts +13 -0
- package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
- package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
- package/templates/apps/frontend-status/template.json +6 -0
- package/templates/apps/frontend-status/tsconfig.json +11 -0
- package/templates/baselines/compose/docker-compose.prod.yml +15 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
- package/dist/apiBuild-BrjrVJJh.js +0 -2
- package/dist/dbCommand-DrzXimKf.js +0 -2
- package/dist/dev-DNkso403.js +0 -3
- package/dist/fileConventions-3bffWssN.js +0 -30
- package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
- package/dist/inspect-gt8bq-Tz.js +0 -2
- package/dist/manifestBuild-ifczArzr.js +0 -2
- package/dist/serveCommand-DfkisVWP.js +0 -1310
|
@@ -0,0 +1,2420 @@
|
|
|
1
|
+
import { R as e, f as t, ht as n, n as r, o as i } from "./inspectMetrics-DHYygE4s.js";
|
|
2
|
+
import { Mt as a, jt as o } from "./dev-Cg4BKLmi.js";
|
|
3
|
+
import { C as s, E as c, O as l, T as u, d, h as f, i as p, l as m, m as h, r as g, u as _ } from "./fileConventions-Cof68_BL.js";
|
|
4
|
+
import { r as v, u as y } from "./apiBuild-BESZbTjA.js";
|
|
5
|
+
import { dirname as b, isAbsolute as ee, join as x, relative as S, resolve as C, sep as w } from "node:path";
|
|
6
|
+
import { routeDiagnosticsToStderr as te } from "@voltro/logger";
|
|
7
|
+
import { existsSync as T, readFileSync as E, readdirSync as D, statSync as O, writeFileSync as ne } from "node:fs";
|
|
8
|
+
import { Node as k, Project as A, SyntaxKind as j } from "ts-morph";
|
|
9
|
+
//#region src/cliOutput.ts
|
|
10
|
+
var re = () => {
|
|
11
|
+
te();
|
|
12
|
+
}, M = (e, t = {}) => {
|
|
13
|
+
console.log(JSON.stringify(e, null, t.pretty === !1 ? 0 : 2));
|
|
14
|
+
}, ie = (e) => {
|
|
15
|
+
for (let t = 0; t < e.length; t++) {
|
|
16
|
+
let n = e[t];
|
|
17
|
+
if (n === "--json" || n === "--format=json" || n === "--format" && e[t + 1] === "json") return !0;
|
|
18
|
+
}
|
|
19
|
+
return !1;
|
|
20
|
+
}, ae = (e, t, n = !1) => {
|
|
21
|
+
let r = { ...e ?? {} }, i = t ?? process.env.VOLTRO_INSPECT_TOKEN;
|
|
22
|
+
if (i !== void 0 && i !== "" && (r.authorization = `Bearer ${i}`), n) {
|
|
23
|
+
let e = process.env.VOLTRO_INSPECT_WRITE_TOKEN;
|
|
24
|
+
e !== void 0 && e !== "" && (r["x-voltro-inspect-write"] = e);
|
|
25
|
+
}
|
|
26
|
+
return r;
|
|
27
|
+
}, oe = /* @__PURE__ */ new Set([
|
|
28
|
+
"POST",
|
|
29
|
+
"PUT",
|
|
30
|
+
"PATCH",
|
|
31
|
+
"DELETE"
|
|
32
|
+
]), N = async (e, t, n) => {
|
|
33
|
+
let r = `${e.url.replace(/\/+$/, "")}${t.startsWith("/") ? t : `/${t}`}`, i = n?.method ?? "GET", a = n?.body !== void 0;
|
|
34
|
+
try {
|
|
35
|
+
let t = await fetch(r, {
|
|
36
|
+
method: i,
|
|
37
|
+
headers: ae(a ? { "content-type": "application/json" } : void 0, e.inspectToken, oe.has(i)),
|
|
38
|
+
...a ? { body: JSON.stringify(n.body) } : {}
|
|
39
|
+
}), o = await t.text(), s;
|
|
40
|
+
if (o.length > 0) try {
|
|
41
|
+
s = JSON.parse(o);
|
|
42
|
+
} catch {
|
|
43
|
+
s = o;
|
|
44
|
+
}
|
|
45
|
+
if (!t.ok) {
|
|
46
|
+
let n = s, r = typeof n == "object" && n ? String(n.message ?? n.reason ?? n.error ?? `HTTP ${t.status}`) : `HTTP ${t.status}`, a = oe.has(i) && (process.env.VOLTRO_INSPECT_WRITE_TOKEN ?? "") === "" && /write/i.test(r);
|
|
47
|
+
return {
|
|
48
|
+
kind: "err",
|
|
49
|
+
name: e.name,
|
|
50
|
+
error: a ? `${r}\n VOLTRO_INSPECT_WRITE_TOKEN is not set in this shell. \`voltro dev\` mints it into the project's \`.env.local\`; export it (or run from a shell that loaded it).` : r
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
kind: "ok",
|
|
55
|
+
name: e.name,
|
|
56
|
+
status: t.status,
|
|
57
|
+
data: s
|
|
58
|
+
};
|
|
59
|
+
} catch (t) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "err",
|
|
62
|
+
name: e.name,
|
|
63
|
+
error: t.message || String(t)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}, se = (e, t = process.env) => {
|
|
67
|
+
let n = [], r, i;
|
|
68
|
+
for (let t = 0; t < e.length; t++) {
|
|
69
|
+
let a = e[t];
|
|
70
|
+
if (a === "--url") {
|
|
71
|
+
r = e[++t];
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (a.startsWith("--url=")) {
|
|
75
|
+
r = a.slice(6);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (a === "--token") {
|
|
79
|
+
i = e[++t];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (a.startsWith("--token=")) {
|
|
83
|
+
i = a.slice(8);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
n.push(a);
|
|
87
|
+
}
|
|
88
|
+
if (r ??= t.VOLTRO_INSPECT_URL, r === void 0 || r === "") return {
|
|
89
|
+
target: void 0,
|
|
90
|
+
rest: n
|
|
91
|
+
};
|
|
92
|
+
let a;
|
|
93
|
+
try {
|
|
94
|
+
a = new URL(r);
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
target: void 0,
|
|
98
|
+
rest: n,
|
|
99
|
+
error: `--url: not a valid URL: "${r}"`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
let o = i ?? t.VOLTRO_INSPECT_TOKEN;
|
|
103
|
+
return {
|
|
104
|
+
target: {
|
|
105
|
+
url: r.replace(/\/+$/, ""),
|
|
106
|
+
name: a.host,
|
|
107
|
+
...o !== void 0 && o !== "" ? { token: o } : {}
|
|
108
|
+
},
|
|
109
|
+
rest: n
|
|
110
|
+
};
|
|
111
|
+
}, ce = (e) => ({
|
|
112
|
+
id: `remote:${e.url}`,
|
|
113
|
+
name: e.name,
|
|
114
|
+
kind: "api",
|
|
115
|
+
url: e.url,
|
|
116
|
+
pid: 0,
|
|
117
|
+
registeredAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
118
|
+
heartbeatAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
119
|
+
status: "live",
|
|
120
|
+
logFile: "",
|
|
121
|
+
...e.token === void 0 ? {} : { inspectToken: e.token }
|
|
122
|
+
}), le = async (e, t) => t === void 0 ? (await n()).apps.filter((t) => t.kind === "api" && t.status === "live" && t.url.length > 0 && (e === void 0 || t.name === e)) : [ce(t)], ue = (e) => {
|
|
123
|
+
let t = "pretty", n = [];
|
|
124
|
+
for (let r = 0; r < e.length; r++) {
|
|
125
|
+
let i = e[r];
|
|
126
|
+
if (i === "--format") {
|
|
127
|
+
let n = e[++r];
|
|
128
|
+
if (n !== "pretty" && n !== "json") return { error: "--format: must be 'pretty' | 'json'" };
|
|
129
|
+
t = n;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (i === "--json" || i === "--format=json") {
|
|
133
|
+
t = "json";
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (i === "--format=pretty") {
|
|
137
|
+
t = "pretty";
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
n.push(i);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
format: t,
|
|
144
|
+
rest: n
|
|
145
|
+
};
|
|
146
|
+
}, de = (e, t = process.env) => {
|
|
147
|
+
let { target: n, rest: r, error: i } = se(e, t), a, o = [];
|
|
148
|
+
for (let e = 0; e < r.length; e++) {
|
|
149
|
+
let t = r[e];
|
|
150
|
+
if (t === "--process") {
|
|
151
|
+
a = r[++e];
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (t.startsWith("--process=")) {
|
|
155
|
+
a = t.slice(10);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
o.push(t);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
process: a,
|
|
162
|
+
target: n,
|
|
163
|
+
rest: o,
|
|
164
|
+
...i === void 0 ? {} : { error: i }
|
|
165
|
+
};
|
|
166
|
+
}, P = (e) => e.source === void 0 ? [] : typeof e.source == "string" ? [e.source] : e.source, fe = (e) => ({
|
|
167
|
+
tables: new Set(e.tables),
|
|
168
|
+
procedures: new Map(e.procedures.map((e) => [e.tag, e])),
|
|
169
|
+
scopes: new Set(e.scopes ?? []),
|
|
170
|
+
input: e
|
|
171
|
+
}), pe = (e) => {
|
|
172
|
+
let t = [], n = /* @__PURE__ */ new Set();
|
|
173
|
+
for (let r of e.input.procedures) {
|
|
174
|
+
for (let i of P(r)) n.add(i), e.tables.has(i) || t.push({
|
|
175
|
+
rule: "reference/dangling-source",
|
|
176
|
+
node: `${r.kind}(${r.tag})`,
|
|
177
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
178
|
+
breaks: [`reads table '${i}' which does not exist`],
|
|
179
|
+
fix: `declare a '${i}.entity.ts' table or fix the query's source`,
|
|
180
|
+
severity: "error"
|
|
181
|
+
});
|
|
182
|
+
for (let i of r.targets ?? []) n.add(i.table), e.tables.has(i.table) || t.push({
|
|
183
|
+
rule: "reference/dangling-target",
|
|
184
|
+
node: `${r.kind}(${r.tag})`,
|
|
185
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
186
|
+
breaks: [`targets table '${i.table}' (op '${i.op}') which does not exist`],
|
|
187
|
+
fix: `declare a '${i.table}.entity.ts' table or fix the mutation's target`,
|
|
188
|
+
severity: "error"
|
|
189
|
+
});
|
|
190
|
+
for (let n of r.requiresScopes ?? []) e.scopes.size > 0 && !e.scopes.has(n) && t.push({
|
|
191
|
+
rule: "rbac/unknown-scope",
|
|
192
|
+
node: `${r.kind}(${r.tag})`,
|
|
193
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
194
|
+
breaks: [`requires scope '${n}' which is not declared`],
|
|
195
|
+
fix: `declare the scope '${n}' or fix the requireScope(...) call`,
|
|
196
|
+
severity: "error"
|
|
197
|
+
});
|
|
198
|
+
r.kind === "mutation" && r.hasGuards === !1 && t.push({
|
|
199
|
+
rule: "rbac/unguarded-mutation",
|
|
200
|
+
node: `${r.kind}(${r.tag})`,
|
|
201
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
202
|
+
breaks: [`'${r.tag}' changes state but declares no guard — any caller who can reach the rpc surface can invoke it`],
|
|
203
|
+
fix: "add a guard (e.g. `.guard(requireScope('…'))`), or confirm it is intentionally public",
|
|
204
|
+
severity: "warn"
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
for (let n of e.input.routes ?? []) for (let r of n.binds) e.procedures.has(r) || t.push({
|
|
208
|
+
rule: "binding/unknown-tag",
|
|
209
|
+
node: `route(${n.pattern})`,
|
|
210
|
+
...n.file === void 0 ? {} : { file: n.file },
|
|
211
|
+
breaks: [`binds rpc tag '${r}' which does not exist`],
|
|
212
|
+
fix: `bind an existing tag, or restore '${r}' (it may have been renamed/removed)`,
|
|
213
|
+
severity: "error"
|
|
214
|
+
});
|
|
215
|
+
for (let t of e.input.procedures) for (let e of t.targets ?? []) n.add(e.table);
|
|
216
|
+
let r = new Set(e.input.frameworkOwnedTables ?? []), i = e.input.procedures.filter((e) => e.kind === "action" && P(e).length === 0 && (e.targets ?? []).length === 0).length;
|
|
217
|
+
for (let a of e.tables) r.has(a) || n.has(a) || t.push({
|
|
218
|
+
rule: "orphan/unread-table",
|
|
219
|
+
node: `table(${a})`,
|
|
220
|
+
breaks: i > 0 ? [`no query reads and no mutation writes '${a}'`, `${i} action(s) declare neither source nor target, so one of them may still use it`] : [`no query reads and no mutation writes '${a}'`],
|
|
221
|
+
fix: i > 0 ? "declare `source`/`target` on your actions so this can be answered, then re-run" : `add a query/mutation for '${a}', or remove the table if it's dead`,
|
|
222
|
+
severity: "warn"
|
|
223
|
+
});
|
|
224
|
+
if (e.scopes.size > 0) {
|
|
225
|
+
let n = /* @__PURE__ */ new Set();
|
|
226
|
+
for (let t of e.input.procedures) for (let e of t.requiresScopes ?? []) n.add(e);
|
|
227
|
+
for (let r of [...e.scopes].sort()) r === "admin:full" || n.has(r) || t.push({
|
|
228
|
+
rule: "rbac/unenforced-scope",
|
|
229
|
+
node: `scope(${r})`,
|
|
230
|
+
breaks: [`'${r}' is granted by a role but NO query/mutation/action/stream guards on it`, "a scope nobody checks grants nothing and withholds nothing — the role map reads stricter than the app is"],
|
|
231
|
+
fix: `add \`.guard(requireScope('${r}'))\` to the handlers it is meant to protect — or, if a plugin route, a REST route, or a UI-only affordance enforces it, say so where the role is declared`,
|
|
232
|
+
severity: "warn"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return t;
|
|
236
|
+
}, me = (e, t) => {
|
|
237
|
+
let n = [];
|
|
238
|
+
if (t.kind === "removeTable") for (let r of e.input.procedures) {
|
|
239
|
+
let e = P(r).includes(t.name), i = (r.targets ?? []).some((e) => e.table === t.name);
|
|
240
|
+
(e || i) && n.push({
|
|
241
|
+
rule: "blast/table-removed",
|
|
242
|
+
node: `${r.kind}(${r.tag})`,
|
|
243
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
244
|
+
breaks: [`${e ? "reads" : "writes"} table '${t.name}' — would break if removed`],
|
|
245
|
+
fix: `migrate or remove ${r.tag} before dropping '${t.name}'`,
|
|
246
|
+
severity: "error"
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
else if (t.kind === "removeProcedure") for (let r of e.input.routes ?? []) r.binds.includes(t.tag) && n.push({
|
|
250
|
+
rule: "blast/procedure-removed",
|
|
251
|
+
node: `route(${r.pattern})`,
|
|
252
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
253
|
+
breaks: [`binds '${t.tag}' — would break if removed`],
|
|
254
|
+
fix: `rebind ${r.pattern} to another tag before removing '${t.tag}'`,
|
|
255
|
+
severity: "error"
|
|
256
|
+
});
|
|
257
|
+
else for (let r of e.input.procedures) (r.requiresScopes ?? []).includes(t.name) && n.push({
|
|
258
|
+
rule: "blast/scope-removed",
|
|
259
|
+
node: `${r.kind}(${r.tag})`,
|
|
260
|
+
...r.file === void 0 ? {} : { file: r.file },
|
|
261
|
+
breaks: [`requires scope '${t.name}' — would become unguarded/dangling if removed`],
|
|
262
|
+
fix: `remove the requireScope('${t.name}') call or keep the scope`,
|
|
263
|
+
severity: "error"
|
|
264
|
+
});
|
|
265
|
+
return n;
|
|
266
|
+
}, he = (e, t) => {
|
|
267
|
+
let n = fe(e), r = [...pe(n), ...t === void 0 ? [] : me(n, t)];
|
|
268
|
+
return {
|
|
269
|
+
ok: !r.some((e) => e.severity === "error"),
|
|
270
|
+
diagnostics: r
|
|
271
|
+
};
|
|
272
|
+
}, ge = (e) => {
|
|
273
|
+
let t = "", n = !1;
|
|
274
|
+
for (let r = 0; r < e.length; r++) {
|
|
275
|
+
let i = e[r];
|
|
276
|
+
if (n) {
|
|
277
|
+
if (t += i, i === "\\") {
|
|
278
|
+
t += e[r + 1] ?? "", r++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
i === "\"" && (n = !1);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (i === "\"") {
|
|
285
|
+
n = !0, t += i;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (i === "/" && e[r + 1] === "*") {
|
|
289
|
+
let t = e.indexOf("*/", r + 2);
|
|
290
|
+
r = t === -1 ? e.length : t + 1;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (i === "/" && e[r + 1] === "/") {
|
|
294
|
+
let t = e.indexOf("\n", r);
|
|
295
|
+
r = t === -1 ? e.length : t - 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
t += i;
|
|
299
|
+
}
|
|
300
|
+
return JSON.parse(_e(t));
|
|
301
|
+
}, _e = (e) => {
|
|
302
|
+
let t = "", n = !1;
|
|
303
|
+
for (let r = 0; r < e.length; r++) {
|
|
304
|
+
let i = e[r];
|
|
305
|
+
if (n) {
|
|
306
|
+
if (t += i, i === "\\") {
|
|
307
|
+
t += e[r + 1] ?? "", r++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
i === "\"" && (n = !1);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (i === "\"") {
|
|
314
|
+
n = !0, t += i;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (i === ",") {
|
|
318
|
+
let t = e.slice(r + 1);
|
|
319
|
+
if (/^\s*[}\]]/.exec(t)) continue;
|
|
320
|
+
}
|
|
321
|
+
t += i;
|
|
322
|
+
}
|
|
323
|
+
return t;
|
|
324
|
+
}, F = (e, t) => {
|
|
325
|
+
if (t.has(e) || !T(e)) return;
|
|
326
|
+
t.add(e);
|
|
327
|
+
let n;
|
|
328
|
+
try {
|
|
329
|
+
n = ge(E(e, "utf8"));
|
|
330
|
+
} catch {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (n.extends === void 0) return n;
|
|
334
|
+
let r = n.extends;
|
|
335
|
+
if (!r.startsWith(".")) return n;
|
|
336
|
+
let i = F(C(b(e), r), t);
|
|
337
|
+
return i === void 0 ? n : { compilerOptions: {
|
|
338
|
+
...i.compilerOptions,
|
|
339
|
+
...n.compilerOptions,
|
|
340
|
+
paths: {
|
|
341
|
+
...i.compilerOptions?.paths,
|
|
342
|
+
...n.compilerOptions?.paths
|
|
343
|
+
}
|
|
344
|
+
} };
|
|
345
|
+
}, ve = (e, t = "tsconfig.json") => {
|
|
346
|
+
let n = ee(t) ? t : C(e, t), r = F(n, /* @__PURE__ */ new Set()), i = r?.compilerOptions?.paths;
|
|
347
|
+
if (i === void 0) return;
|
|
348
|
+
let a = {};
|
|
349
|
+
for (let [e, t] of Object.entries(i)) Array.isArray(t) && (a[e] = t.filter((e) => typeof e == "string"));
|
|
350
|
+
if (Object.keys(a).length !== 0) return {
|
|
351
|
+
baseUrl: C(b(n), r?.compilerOptions?.baseUrl ?? "."),
|
|
352
|
+
paths: a
|
|
353
|
+
};
|
|
354
|
+
}, I = (e, t = "tsconfig.json") => {
|
|
355
|
+
let n = ee(t) ? t : C(e, t), r = F(n, /* @__PURE__ */ new Set()), i = r?.compilerOptions?.paths;
|
|
356
|
+
if (i === void 0) return [];
|
|
357
|
+
let a = C(b(n), r?.compilerOptions?.baseUrl ?? "."), o = [];
|
|
358
|
+
for (let [e, t] of Object.entries(i)) {
|
|
359
|
+
let n = t[0];
|
|
360
|
+
if (typeof n != "string") continue;
|
|
361
|
+
let r = e.endsWith("*");
|
|
362
|
+
o.push({
|
|
363
|
+
prefix: r ? e.slice(0, -1) : e,
|
|
364
|
+
target: C(a, r ? n.replace(/\*$/, "") : n),
|
|
365
|
+
wildcard: r
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return o.sort((e, t) => t.prefix.length - e.prefix.length);
|
|
369
|
+
}, ye = (e, t) => {
|
|
370
|
+
for (let n of t) if (n.wildcard) {
|
|
371
|
+
if (e.startsWith(n.prefix)) return x(n.target, e.slice(n.prefix.length));
|
|
372
|
+
} else if (e === n.prefix) return n.target;
|
|
373
|
+
}, L = ".voltro-vendored", be = (e, t) => {
|
|
374
|
+
let n = [], r = /* @__PURE__ */ new Set(), i = (e, t) => {
|
|
375
|
+
let i = C(e);
|
|
376
|
+
r.has(i) || !T(i) || (r.add(i), n.push({
|
|
377
|
+
dir: i,
|
|
378
|
+
source: t
|
|
379
|
+
}));
|
|
380
|
+
}, a = x(e, "components.json");
|
|
381
|
+
if (T(a)) try {
|
|
382
|
+
let t = JSON.parse(E(a, "utf8"));
|
|
383
|
+
for (let n of ["ui"]) {
|
|
384
|
+
let r = t.aliases?.[n];
|
|
385
|
+
if (typeof r != "string" || r === "") continue;
|
|
386
|
+
let a = r.replace(/^@\//, "").replace(/^~\//, "");
|
|
387
|
+
for (let t of [x(e, a), x(e, "src", a)]) if (T(t)) {
|
|
388
|
+
i(t, `components.json (aliases.${n})`);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch {}
|
|
393
|
+
for (let e of t) {
|
|
394
|
+
let t = e.slice(0, e.lastIndexOf(w)), n = x(t, L);
|
|
395
|
+
if (!r.has(C(t)) && T(n)) {
|
|
396
|
+
let e = "";
|
|
397
|
+
try {
|
|
398
|
+
e = E(n, "utf8").trim().split("\n")[0] ?? "";
|
|
399
|
+
} catch {}
|
|
400
|
+
i(t, e === "" ? L : `${L}: ${e}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return n;
|
|
404
|
+
}, xe = (e, t) => t.some((t) => e === t.dir || e.startsWith(`${t.dir}${w}`)), Se = (e, t) => t.map((t) => `${S(e, t.dir) || "."} (${t.source})`), Ce = [
|
|
405
|
+
{
|
|
406
|
+
id: "component.ui",
|
|
407
|
+
re: p,
|
|
408
|
+
needsTest: !0
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: "component",
|
|
412
|
+
re: g,
|
|
413
|
+
needsTest: !0
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "hook",
|
|
417
|
+
re: _,
|
|
418
|
+
needsTest: !0
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: "tracking",
|
|
422
|
+
re: c,
|
|
423
|
+
needsTest: !0
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: "store",
|
|
427
|
+
re: s,
|
|
428
|
+
needsTest: !0
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "internal",
|
|
432
|
+
re: d,
|
|
433
|
+
needsTest: !0
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: "fixture",
|
|
437
|
+
re: m,
|
|
438
|
+
needsTest: !1
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: "types",
|
|
442
|
+
re: l,
|
|
443
|
+
needsTest: !1
|
|
444
|
+
}
|
|
445
|
+
], R = (e) => Ce.find((t) => t.re.test(e))?.id ?? null, z = (e) => u.test(e), we = /* @__PURE__ */ new Set([
|
|
446
|
+
"useMutation",
|
|
447
|
+
"useAction",
|
|
448
|
+
"useUpload",
|
|
449
|
+
"useUndo",
|
|
450
|
+
"useOutbox",
|
|
451
|
+
"useAgentChat",
|
|
452
|
+
"useAgentStream",
|
|
453
|
+
"useResumableAgentStream",
|
|
454
|
+
"useWorkflow"
|
|
455
|
+
]), Te = /* @__PURE__ */ new Set(["defineTracking"]), Ee = /* @__PURE__ */ new Set([
|
|
456
|
+
"useSubscription",
|
|
457
|
+
"useWindowedSubscription",
|
|
458
|
+
"useRecord",
|
|
459
|
+
"useQuery"
|
|
460
|
+
]), De = /* @__PURE__ */ new Set([
|
|
461
|
+
"node_modules",
|
|
462
|
+
"dist",
|
|
463
|
+
"build",
|
|
464
|
+
".framework",
|
|
465
|
+
".turbo",
|
|
466
|
+
".vite",
|
|
467
|
+
".next",
|
|
468
|
+
".git",
|
|
469
|
+
"coverage",
|
|
470
|
+
".publish"
|
|
471
|
+
]), Oe = /\.tsx?$/, B = (e, t = [], n = 0) => {
|
|
472
|
+
if (n > 12) return t;
|
|
473
|
+
let r;
|
|
474
|
+
try {
|
|
475
|
+
r = D(e);
|
|
476
|
+
} catch {
|
|
477
|
+
return t;
|
|
478
|
+
}
|
|
479
|
+
for (let i of r) {
|
|
480
|
+
if (De.has(i)) continue;
|
|
481
|
+
let r = x(e, i), a;
|
|
482
|
+
try {
|
|
483
|
+
a = O(r);
|
|
484
|
+
} catch {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
a.isDirectory() ? B(r, t, n + 1) : a.isFile() && Oe.test(i) && !i.endsWith(".d.ts") && t.push(r);
|
|
488
|
+
}
|
|
489
|
+
return t;
|
|
490
|
+
}, ke = /\b(?:FC|FunctionComponent|ComponentType|ForwardRefExoticComponent|MemoExoticComponent)\b/, Ae = /\b(?:memo|forwardRef|observer|styled)\b/, V = (e) => k.isArrowFunction(e) || k.isFunctionExpression(e) || k.isFunctionDeclaration(e) || k.isClassDeclaration(e) || k.isClassExpression(e), H = (e) => {
|
|
491
|
+
if (V(e)) return !0;
|
|
492
|
+
if (k.isExportAssignment(e) || k.isAsExpression(e) || k.isSatisfiesExpression(e) || k.isParenthesizedExpression(e)) return H(e.getExpression());
|
|
493
|
+
if (k.isVariableDeclaration(e)) {
|
|
494
|
+
let t = e.getTypeNode();
|
|
495
|
+
if (t !== void 0 && ke.test(t.getText())) return !0;
|
|
496
|
+
let n = e.getInitializer();
|
|
497
|
+
return n === void 0 || H(n);
|
|
498
|
+
}
|
|
499
|
+
return k.isCallExpression(e) ? Ae.test(e.getExpression().getText()) ? !0 : e.getArguments().some((e) => V(e) || k.isIdentifier(e)) : k.isTaggedTemplateExpression(e) ? !0 : !(k.isObjectLiteralExpression(e) || k.isArrayLiteralExpression(e) || k.isStringLiteral(e) || k.isNoSubstitutionTemplateLiteral(e) || k.isNumericLiteral(e) || k.isTemplateExpression(e) || k.isNewExpression(e) || k.isPropertyAccessExpression(e) || e.getKind() === j.TrueKeyword || e.getKind() === j.FalseKeyword || e.getKind() === j.NullKeyword);
|
|
500
|
+
}, je = (e) => {
|
|
501
|
+
let t = [], n = [], r = [], i = /* @__PURE__ */ new Set();
|
|
502
|
+
for (let [a, o] of e.getExportedDeclarations()) {
|
|
503
|
+
let e = o[0];
|
|
504
|
+
if (e === void 0 || k.isInterfaceDeclaration(e) || k.isTypeAliasDeclaration(e) || i.has(e)) continue;
|
|
505
|
+
i.add(e);
|
|
506
|
+
let s = a === "default" ? (k.isFunctionDeclaration(e) || k.isClassDeclaration(e) || k.isVariableDeclaration(e) ? e.getName() : void 0) ?? "Default" : a;
|
|
507
|
+
if (/^use[A-Z]/.test(s)) {
|
|
508
|
+
n.push(s);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (/^[A-Z]/.test(s) && H(e)) {
|
|
512
|
+
t.push(s);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
r.push(s);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
components: t,
|
|
519
|
+
hooks: n,
|
|
520
|
+
others: r,
|
|
521
|
+
hasRuntimeExport: t.length + n.length + r.length > 0
|
|
522
|
+
};
|
|
523
|
+
}, Me = [], Ne = (e, t, n) => {
|
|
524
|
+
let r = b(e), i = [];
|
|
525
|
+
for (let e = r;; e = b(e)) {
|
|
526
|
+
let r = n.get(e);
|
|
527
|
+
if (r !== void 0) {
|
|
528
|
+
for (let e of i) n.set(e, r);
|
|
529
|
+
return r;
|
|
530
|
+
}
|
|
531
|
+
if (i.push(e), T(x(e, "tsconfig.json"))) {
|
|
532
|
+
let t = I(e);
|
|
533
|
+
if (t.length > 0) {
|
|
534
|
+
for (let e of i) n.set(e, t);
|
|
535
|
+
return t;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (e === t || b(e) === e) break;
|
|
539
|
+
}
|
|
540
|
+
for (let e of i) n.set(e, Me);
|
|
541
|
+
return Me;
|
|
542
|
+
}, Pe = (e, t, n) => {
|
|
543
|
+
let r = t.startsWith(".") ? C(b(e), t) : ye(t, n);
|
|
544
|
+
if (r === void 0) return null;
|
|
545
|
+
for (let e of [
|
|
546
|
+
r,
|
|
547
|
+
`${r}.tsx`,
|
|
548
|
+
`${r}.ts`,
|
|
549
|
+
x(r, "index.tsx"),
|
|
550
|
+
x(r, "index.ts")
|
|
551
|
+
]) try {
|
|
552
|
+
if (O(e).isFile()) return e;
|
|
553
|
+
} catch {}
|
|
554
|
+
return null;
|
|
555
|
+
}, Fe = (e, t, n) => {
|
|
556
|
+
let r = [], i = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new Map(), o = (e) => {
|
|
557
|
+
let i = Pe(t, e, n);
|
|
558
|
+
return i !== null && r.push(i), i;
|
|
559
|
+
};
|
|
560
|
+
for (let t of e.getImportDeclarations()) if (o(t.getModuleSpecifierValue()), !t.isTypeOnly()) for (let e of t.getNamedImports()) e.isTypeOnly() || i.add(e.getName());
|
|
561
|
+
for (let t of e.getExportDeclarations()) {
|
|
562
|
+
let e = t.getModuleSpecifierValue();
|
|
563
|
+
if (e === void 0) continue;
|
|
564
|
+
let n = o(e);
|
|
565
|
+
if (n === null || t.isTypeOnly()) continue;
|
|
566
|
+
let r = t.getNamedExports();
|
|
567
|
+
if (r.length === 0) {
|
|
568
|
+
a.set(n, "*");
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
let i = r.filter((e) => !e.isTypeOnly()).map((e) => e.getAliasNode()?.getText() ?? e.getName()), s = a.get(n);
|
|
572
|
+
a.set(n, s === "*" ? "*" : [...s ?? [], ...i]);
|
|
573
|
+
}
|
|
574
|
+
for (let t of e.getDescendantsOfKind(j.CallExpression)) {
|
|
575
|
+
if (t.getExpression().getKind() !== j.ImportKeyword) continue;
|
|
576
|
+
let e = t.getArguments()[0];
|
|
577
|
+
e !== void 0 && k.isStringLiteral(e) && o(e.getLiteralValue());
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
localTargets: r,
|
|
581
|
+
namedBindings: i,
|
|
582
|
+
reExports: a
|
|
583
|
+
};
|
|
584
|
+
}, Ie = (e) => {
|
|
585
|
+
let t = /* @__PURE__ */ new Map();
|
|
586
|
+
for (let n of e.getDescendantsOfKind(j.CallExpression)) {
|
|
587
|
+
let e = n.getExpression(), r = k.isPropertyAccessExpression(e) ? e.getName() : e.getText();
|
|
588
|
+
t.set(r, (t.get(r) ?? 0) + 1);
|
|
589
|
+
}
|
|
590
|
+
return t;
|
|
591
|
+
}, Le = (e) => {
|
|
592
|
+
if (e.length <= 1) return e.length === 1;
|
|
593
|
+
let t = [...e].sort((e, t) => e.length - t.length)[0];
|
|
594
|
+
return e.every((e) => e.startsWith(t));
|
|
595
|
+
}, Re = (e, t) => {
|
|
596
|
+
let n = b(t);
|
|
597
|
+
return e.startsWith(`${n}${w}`) || b(e) === n;
|
|
598
|
+
}, U = (e) => {
|
|
599
|
+
let t = B(e);
|
|
600
|
+
if (t.length === 0) return {
|
|
601
|
+
findings: [],
|
|
602
|
+
declared: 0,
|
|
603
|
+
scanned: 0,
|
|
604
|
+
vendored: []
|
|
605
|
+
};
|
|
606
|
+
let n = be(e, t), r = t.filter((e) => !xe(e, n));
|
|
607
|
+
if (r.length === 0) return {
|
|
608
|
+
findings: [],
|
|
609
|
+
declared: 0,
|
|
610
|
+
scanned: 0,
|
|
611
|
+
vendored: n
|
|
612
|
+
};
|
|
613
|
+
let i = new A({
|
|
614
|
+
useInMemoryFileSystem: !0,
|
|
615
|
+
skipAddingFilesFromTsConfig: !0
|
|
616
|
+
}), a = [], o = (t) => S(e, t), s = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map();
|
|
617
|
+
for (let t of r) {
|
|
618
|
+
let n;
|
|
619
|
+
try {
|
|
620
|
+
n = E(t, "utf8");
|
|
621
|
+
} catch {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
let r;
|
|
625
|
+
try {
|
|
626
|
+
r = i.createSourceFile(t.endsWith("x") ? "/scan.tsx" : "/scan.ts", n, { overwrite: !0 });
|
|
627
|
+
} catch {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
s.set(t, je(r)), c.set(t, Fe(r, t, Ne(t, e, u))), l.set(t, Ie(r));
|
|
631
|
+
}
|
|
632
|
+
let d = /* @__PURE__ */ new Map();
|
|
633
|
+
for (let [e, t] of c) for (let n of t.localTargets) {
|
|
634
|
+
let t = d.get(n) ?? [];
|
|
635
|
+
t.push(e), d.set(n, t);
|
|
636
|
+
}
|
|
637
|
+
let p = (e) => {
|
|
638
|
+
let t = s.get(e), n = /* @__PURE__ */ new Set([
|
|
639
|
+
...t?.components ?? [],
|
|
640
|
+
...t?.hooks ?? [],
|
|
641
|
+
...t?.others ?? []
|
|
642
|
+
]), r = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set([e]), a = (e, t) => {
|
|
643
|
+
for (let o of d.get(e) ?? []) {
|
|
644
|
+
if (i.has(o)) continue;
|
|
645
|
+
let s = c.get(o);
|
|
646
|
+
if (s === void 0) continue;
|
|
647
|
+
let l = s.reExports.get(e);
|
|
648
|
+
if (l !== void 0) {
|
|
649
|
+
i.add(o), r.add(o);
|
|
650
|
+
let e = l === "*" ? t ?? n : new Set(l), s = t === null ? e : new Set([...e].filter((e) => t.has(e)));
|
|
651
|
+
s.size > 0 && a(o, s);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
(t === null || [...s.namedBindings].some((e) => t.has(e))) && r.add(o);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
return a(e, null), [...r];
|
|
658
|
+
}, m = 0;
|
|
659
|
+
for (let e of r) {
|
|
660
|
+
let t = R(e), n = s.get(e), r = c.get(e);
|
|
661
|
+
if (n === void 0 || r === void 0) continue;
|
|
662
|
+
let i = z(e);
|
|
663
|
+
t !== null && !i && (m += 1);
|
|
664
|
+
let u = d.get(e) ?? [];
|
|
665
|
+
if ((t === "component" || t === "component.ui") && !i) {
|
|
666
|
+
let t = Le(n.components);
|
|
667
|
+
(n.components.length === 0 || !t) && a.push({
|
|
668
|
+
rule: "component/one-per-file",
|
|
669
|
+
file: o(e),
|
|
670
|
+
severity: "error",
|
|
671
|
+
detail: n.components.length === 0 ? "declares a component file but exports no component" : `exports ${n.components.length} unrelated components: ${n.components.join(", ")}`,
|
|
672
|
+
fix: "a component file exports ONE component — or one NAMESPACED family sharing its name (`Card`, `CardHeader`, `CardTitle`), which is one component with parts. Types alongside are fine. Unrelated components belong in their own files"
|
|
673
|
+
}), n.hooks.length > 0 && a.push({
|
|
674
|
+
rule: "component/no-hook-export",
|
|
675
|
+
file: o(e),
|
|
676
|
+
severity: "error",
|
|
677
|
+
detail: `also exports hook(s): ${n.hooks.join(", ")}`,
|
|
678
|
+
fix: "move the hook into its own *.hook.ts — a file exporting both is two files wearing one name, and neither can be reused without dragging the other"
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
if (t === "hook" && !i && (n.hooks.length !== 1 && a.push({
|
|
682
|
+
rule: "hook/one-per-file",
|
|
683
|
+
file: o(e),
|
|
684
|
+
severity: "error",
|
|
685
|
+
detail: n.hooks.length === 0 ? "declares a hook file but exports no use* hook" : `exports ${n.hooks.length} hooks: ${n.hooks.join(", ")}`,
|
|
686
|
+
fix: "a hook file exports exactly ONE use* hook (types alongside are fine)"
|
|
687
|
+
}), n.components.length > 0 && a.push({
|
|
688
|
+
rule: "hook/no-component-export",
|
|
689
|
+
file: o(e),
|
|
690
|
+
severity: "error",
|
|
691
|
+
detail: `also exports component(s): ${n.components.join(", ")}`,
|
|
692
|
+
fix: "move the component into its own *.component.tsx"
|
|
693
|
+
})), t === "types" && n.hasRuntimeExport && a.push({
|
|
694
|
+
rule: "types/runtime-export",
|
|
695
|
+
file: o(e),
|
|
696
|
+
severity: "error",
|
|
697
|
+
detail: `exports runtime value(s): ${[
|
|
698
|
+
...n.components,
|
|
699
|
+
...n.hooks,
|
|
700
|
+
...n.others
|
|
701
|
+
].join(", ")}`,
|
|
702
|
+
fix: "a *.types.ts has NO runtime export. That is what makes importing it provably free AND makes it impossible for it to sit in a runtime import cycle — the guarantee is the point, so move the value to a sibling module"
|
|
703
|
+
}), t === "internal") {
|
|
704
|
+
let t = u.filter((t) => !Re(t, e));
|
|
705
|
+
t.length > 0 && a.push({
|
|
706
|
+
rule: "internal/foreign-import",
|
|
707
|
+
file: o(e),
|
|
708
|
+
severity: "error",
|
|
709
|
+
detail: `imported from outside its own directory by: ${t.slice(0, 5).map(o).join(", ")}${t.length > 5 ? ` (+${t.length - 5})` : ""}`,
|
|
710
|
+
fix: "export it through the feature's public module, or move the importer inside. `.internal` is the promise that refactoring within this directory breaks nobody — a foreign import revokes it silently, which is how a feature boundary rots without a single review comment"
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
if (t === "fixture") {
|
|
714
|
+
let t = u.filter((e) => !z(e) && R(e) !== "fixture");
|
|
715
|
+
t.length > 0 && a.push({
|
|
716
|
+
rule: "fixture/production-import",
|
|
717
|
+
file: o(e),
|
|
718
|
+
severity: "error",
|
|
719
|
+
detail: `imported by non-test file(s): ${t.slice(0, 5).map(o).join(", ")}`,
|
|
720
|
+
fix: "test material must not be reachable from a production path — it ships in the bundle, and it is written to be convenient rather than correct"
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (t === "component.ui" && !i) {
|
|
724
|
+
let t = [...r.namedBindings].filter((e) => we.has(e)).sort();
|
|
725
|
+
t.length > 0 && a.push({
|
|
726
|
+
rule: "ui/no-write",
|
|
727
|
+
file: o(e),
|
|
728
|
+
severity: "error",
|
|
729
|
+
detail: `imports write hook(s): ${t.join(", ")}`,
|
|
730
|
+
fix: "a *.component.ui.tsx renders; it does not write. Lift the mutation into the *.component.tsx that owns it and pass a handler down. Reading (useT / useCan / usePermissions) stays allowed — threading those through props is prop-drilling, not reusability"
|
|
731
|
+
});
|
|
732
|
+
let n = p(e), i = n.filter((e) => {
|
|
733
|
+
let t = R(e);
|
|
734
|
+
return t === "component" || t === "component.ui" || h.test(e) || f.has(e.slice(e.lastIndexOf(w) + 1));
|
|
735
|
+
});
|
|
736
|
+
n.length === 0 ? a.push({
|
|
737
|
+
rule: "ui/orphaned",
|
|
738
|
+
file: o(e),
|
|
739
|
+
severity: "warn",
|
|
740
|
+
detail: "nothing imports it",
|
|
741
|
+
fix: "render it from a *.component.tsx or a page, or delete it — an unrendered presentational component is carried, reviewed and refactored forever without reaching a user"
|
|
742
|
+
}) : i.length === 0 && a.push({
|
|
743
|
+
rule: "ui/unlinked",
|
|
744
|
+
file: o(e),
|
|
745
|
+
severity: "warn",
|
|
746
|
+
detail: `reached only by: ${n.slice(0, 5).map(o).join(", ")}${n.length > 5 ? ` (+${n.length - 5})` : ""} — barrels resolved through`,
|
|
747
|
+
fix: "a presentational component should be reached from a *.component.tsx, another *.component.ui.tsx, or a page"
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (t === "store" && !i) {
|
|
751
|
+
let t = l.get(e)?.get("defineStore") ?? 0;
|
|
752
|
+
t !== 1 && a.push({
|
|
753
|
+
rule: "store/one-per-file",
|
|
754
|
+
file: o(e),
|
|
755
|
+
severity: "error",
|
|
756
|
+
detail: t === 0 ? "declares a store file but calls no defineStore()" : `declares ${t} stores`,
|
|
757
|
+
fix: "a store file defines exactly ONE store — the SSR seed pass and devtools address stores by name, so two in a file makes both harder to find and neither easier to reason about"
|
|
758
|
+
});
|
|
759
|
+
let n = [...l.get(e)?.keys() ?? []].filter((e) => Ee.has(e)).sort();
|
|
760
|
+
n.length > 0 && a.push({
|
|
761
|
+
rule: "store/mirrors-server-state",
|
|
762
|
+
file: o(e),
|
|
763
|
+
severity: "error",
|
|
764
|
+
detail: `reads server state here: ${n.join(", ")}`,
|
|
765
|
+
fix: "a store holds CLIENT state. A subscription is already live server state; copying it into a store gives you a second copy that does not live, and the page then shows the stale one — a bug that reads as \"reactivity is broken\". Read the subscription where you render it"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (t !== "tracking" && !i) {
|
|
769
|
+
let t = [...l.get(e)?.keys() ?? []].filter((e) => Te.has(e)).sort();
|
|
770
|
+
t.length > 0 && a.push({
|
|
771
|
+
rule: "tracking/outside-tracking-file",
|
|
772
|
+
file: o(e),
|
|
773
|
+
severity: "error",
|
|
774
|
+
detail: `calls ${t.join(", ")} outside a *.tracking.ts`,
|
|
775
|
+
fix: "move the defineTracking(...) spec into a *.tracking.ts and import it here. The event catalogue — names, property bags, the decisions about what personal data goes out — belongs in one place you can list; calling useTracking with a spec from there is fine"
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (Ce.find((e) => e.id === t)?.needsTest === !0 && !i) {
|
|
779
|
+
let t = e.replace(/\.tsx?$/, ""), n = e.endsWith("x") ? "tsx" : "ts";
|
|
780
|
+
["test", "spec"].flatMap((e) => [`${t}.${e}.ts`, `${t}.${e}.tsx`]).some((e) => {
|
|
781
|
+
try {
|
|
782
|
+
return O(e).isFile();
|
|
783
|
+
} catch {
|
|
784
|
+
return !1;
|
|
785
|
+
}
|
|
786
|
+
}) || a.push({
|
|
787
|
+
rule: "convention/missing-test",
|
|
788
|
+
file: o(e),
|
|
789
|
+
severity: "warn",
|
|
790
|
+
detail: "no test beside it",
|
|
791
|
+
fix: `add ${o(t)}.test.${n} — the name is derivable, so write it with the file. A shallow test earns its place: what it buys is that something MOUNTS the component, and a render loop, a crashing effect or a missing provider is invisible until then. One app wrote 251 of these and the first run found a render loop that made a page take 423 s and exhaust the heap — in a browser it had looked usable`
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return a.sort((e, t) => e.file === t.file ? e.rule.localeCompare(t.rule) : e.file.localeCompare(t.file)), {
|
|
796
|
+
findings: a,
|
|
797
|
+
declared: m,
|
|
798
|
+
scanned: r.length,
|
|
799
|
+
vendored: n
|
|
800
|
+
};
|
|
801
|
+
}, ze = 10, Be = (e) => {
|
|
802
|
+
let { findings: t, declared: n, scanned: r, vendored: i } = U(e);
|
|
803
|
+
console.log("");
|
|
804
|
+
for (let t of Se(e, i)) console.log(`· not ours, exempt: ${t}`);
|
|
805
|
+
if (r === 0) return console.log("• file-taxonomy scan: no sources here — nothing to check."), 0;
|
|
806
|
+
if (t.length === 0) return console.log(`✓ file-taxonomy scan: ${n} declared file(s) keep their contract (${r} scanned).`), 0;
|
|
807
|
+
let a = /* @__PURE__ */ new Map();
|
|
808
|
+
for (let e of t) {
|
|
809
|
+
let t = a.get(e.rule) ?? [];
|
|
810
|
+
t.push(e), a.set(e.rule, t);
|
|
811
|
+
}
|
|
812
|
+
let o = t.filter((e) => e.severity === "error").length;
|
|
813
|
+
console.log(`• file taxonomy: ${t.length} finding(s) — ${o} error, ${t.length - o} advisory (${n} declared / ${r} scanned):`);
|
|
814
|
+
for (let [e, t] of [...a].sort()) {
|
|
815
|
+
console.log(` ${t[0].severity === "error" ? "✗" : "·"} ${e} (${t.length})`);
|
|
816
|
+
for (let e of t.slice(0, ze)) console.log(` ${e.file} — ${e.detail}`);
|
|
817
|
+
let n = t.length - ze;
|
|
818
|
+
n > 0 && console.log(` (+${n} more — \`voltro doctor --json\` prints every one.)`), console.log(` → ${t[0].fix}`);
|
|
819
|
+
}
|
|
820
|
+
return t.length;
|
|
821
|
+
}, Ve = /* @__PURE__ */ new Set([
|
|
822
|
+
"eq",
|
|
823
|
+
"ne",
|
|
824
|
+
"gt",
|
|
825
|
+
"gte",
|
|
826
|
+
"lt",
|
|
827
|
+
"lte",
|
|
828
|
+
"isNull",
|
|
829
|
+
"isNotNull",
|
|
830
|
+
"inSet",
|
|
831
|
+
"notInSet",
|
|
832
|
+
"like",
|
|
833
|
+
"ilike",
|
|
834
|
+
"contains",
|
|
835
|
+
"startsWith",
|
|
836
|
+
"endsWith",
|
|
837
|
+
"between",
|
|
838
|
+
"jsonField"
|
|
839
|
+
]), He = (e) => {
|
|
840
|
+
if (k.isPropertyAccessExpression(e)) return e.getName();
|
|
841
|
+
if (k.isIdentifier(e)) return e.getText();
|
|
842
|
+
}, Ue = (e) => {
|
|
843
|
+
let t = e;
|
|
844
|
+
for (; k.isCallExpression(t);) {
|
|
845
|
+
let e = t.getExpression();
|
|
846
|
+
if (!k.isPropertyAccessExpression(e)) break;
|
|
847
|
+
t = e.getExpression();
|
|
848
|
+
}
|
|
849
|
+
return t;
|
|
850
|
+
}, We = (e, t, n) => {
|
|
851
|
+
let r = new Map(t.map((e) => [e.name, new Set(e.columns)])), i = new Map(t.map((e) => [e.name, e.columns])), a = [], o = 0, s = new A({
|
|
852
|
+
useInMemoryFileSystem: !0,
|
|
853
|
+
skipAddingFilesFromTsConfig: !0
|
|
854
|
+
});
|
|
855
|
+
for (let t of e) {
|
|
856
|
+
let e;
|
|
857
|
+
try {
|
|
858
|
+
let n = E(t, "utf8");
|
|
859
|
+
if (!n.includes(".where(") && !n.includes(".select(")) continue;
|
|
860
|
+
e = s.createSourceFile(`/scan${t.endsWith("x") ? ".tsx" : ".ts"}`, n, { overwrite: !0 });
|
|
861
|
+
} catch {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
let c = S(n, t);
|
|
865
|
+
for (let t of e.getDescendantsOfKind(j.CallExpression)) {
|
|
866
|
+
let e = t.getExpression();
|
|
867
|
+
if (!k.isPropertyAccessExpression(e) || e.getName() !== "where") continue;
|
|
868
|
+
let n = He(Ue(e.getExpression()));
|
|
869
|
+
if (n === void 0 || !r.has(n)) {
|
|
870
|
+
let t = e.getExpression();
|
|
871
|
+
if (k.isCallExpression(t)) {
|
|
872
|
+
let e = t.getExpression();
|
|
873
|
+
if (k.isPropertyAccessExpression(e) && e.getName() === "select") {
|
|
874
|
+
let e = t.getArguments()[0];
|
|
875
|
+
e !== void 0 && k.isStringLiteral(e) && (n = e.getLiteralValue());
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (n === void 0) continue;
|
|
880
|
+
let s = r.get(n);
|
|
881
|
+
if (s === void 0) continue;
|
|
882
|
+
let l = [], [u] = t.getArguments();
|
|
883
|
+
if (u !== void 0) {
|
|
884
|
+
if (k.isStringLiteral(u)) l.push({
|
|
885
|
+
column: u.getLiteralValue(),
|
|
886
|
+
line: u.getStartLineNumber()
|
|
887
|
+
});
|
|
888
|
+
else for (let e of [u, ...u.getDescendantsOfKind(j.CallExpression)]) {
|
|
889
|
+
if (!k.isCallExpression(e)) continue;
|
|
890
|
+
let t = e.getExpression(), n = k.isIdentifier(t) ? t.getText() : k.isPropertyAccessExpression(t) ? t.getName() : void 0;
|
|
891
|
+
if (n === void 0 || !Ve.has(n)) continue;
|
|
892
|
+
let r = e.getArguments()[0];
|
|
893
|
+
r !== void 0 && k.isStringLiteral(r) && l.push({
|
|
894
|
+
column: r.getLiteralValue(),
|
|
895
|
+
line: r.getStartLineNumber()
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
for (let { column: e, line: t } of l) o++, !s.has(e) && a.push({
|
|
899
|
+
file: c,
|
|
900
|
+
line: t,
|
|
901
|
+
table: n,
|
|
902
|
+
column: e,
|
|
903
|
+
available: i.get(n) ?? []
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
issues: a,
|
|
910
|
+
checked: o
|
|
911
|
+
};
|
|
912
|
+
}, W = 12, Ge = (e) => {
|
|
913
|
+
if (e.checked === 0) return null;
|
|
914
|
+
if (e.issues.length === 0) return `predicate columns: ${e.checked} checked — every filtered column exists on its table`;
|
|
915
|
+
let t = [`predicate columns: ${e.issues.length} filter${e.issues.length, "s"} on a column that does not exist (${e.checked} checked)`];
|
|
916
|
+
for (let n of e.issues) {
|
|
917
|
+
let e = n.available.slice(0, W).join(", "), r = n.available.length > W ? `, … (${n.available.length - W} more)` : "";
|
|
918
|
+
t.push(` ${n.file}:${n.line} '${n.table}' has no column '${n.column}'`), t.push(` columns: ${e}${r}`);
|
|
919
|
+
}
|
|
920
|
+
return t.push(" This type-checks today and fails at runtime as a bare SQL error."), t.join("\n");
|
|
921
|
+
}, Ke = /* @__PURE__ */ new Set([
|
|
922
|
+
"id",
|
|
923
|
+
"tenantId",
|
|
924
|
+
"tenant_id",
|
|
925
|
+
"traceId",
|
|
926
|
+
"trace_id",
|
|
927
|
+
"spanId",
|
|
928
|
+
"span_id",
|
|
929
|
+
"requestId",
|
|
930
|
+
"request_id",
|
|
931
|
+
"sessionId",
|
|
932
|
+
"session_id",
|
|
933
|
+
"correlationId",
|
|
934
|
+
"correlation_id",
|
|
935
|
+
"externalId",
|
|
936
|
+
"external_id",
|
|
937
|
+
"idempotencyId",
|
|
938
|
+
"idempotency_id",
|
|
939
|
+
"idempotencyKey",
|
|
940
|
+
"idempotency_key"
|
|
941
|
+
]), qe = /* @__PURE__ */ new Set([
|
|
942
|
+
"id",
|
|
943
|
+
"createdAt",
|
|
944
|
+
"created_at",
|
|
945
|
+
"updatedAt",
|
|
946
|
+
"updated_at",
|
|
947
|
+
"deletedAt",
|
|
948
|
+
"deleted_at",
|
|
949
|
+
"tenantId",
|
|
950
|
+
"tenant_id"
|
|
951
|
+
]), Je = (e) => Ke.has(e) ? !1 : /[a-z0-9]Id$/.test(e) || /_id$/.test(e), Ye = (e) => e.type === "reference", Xe = (e) => {
|
|
952
|
+
let t = [];
|
|
953
|
+
for (let n of e) {
|
|
954
|
+
let e = n.columns.filter((e) => Je(e.name)), r = e.filter((e) => !Ye(e)).map((e) => e.name);
|
|
955
|
+
if (r.length === 0) continue;
|
|
956
|
+
let i = new Set(n.primaryKey), a = r.filter((e) => i.has(e));
|
|
957
|
+
if (n.primaryKey.length >= 2 && a.length > 0) {
|
|
958
|
+
t.push({
|
|
959
|
+
table: n.name,
|
|
960
|
+
columns: a,
|
|
961
|
+
signal: "composite-pk"
|
|
962
|
+
});
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (e.some(Ye) && e.length >= 2) {
|
|
966
|
+
t.push({
|
|
967
|
+
table: n.name,
|
|
968
|
+
columns: r,
|
|
969
|
+
signal: "reference-sibling"
|
|
970
|
+
});
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
e.length >= 2 && n.columns.filter((e) => !Je(e.name) && !qe.has(e.name)).length === 0 && t.push({
|
|
974
|
+
table: n.name,
|
|
975
|
+
columns: r,
|
|
976
|
+
signal: "link-only"
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
issues: t,
|
|
981
|
+
tablesChecked: e.length
|
|
982
|
+
};
|
|
983
|
+
}, Ze = {
|
|
984
|
+
"composite-pk": "part of a composite primary key",
|
|
985
|
+
"reference-sibling": "sits beside a wired reference() on this table",
|
|
986
|
+
"link-only": "this table is nothing but link columns"
|
|
987
|
+
}, Qe = (e) => {
|
|
988
|
+
if (e.tablesChecked === 0) return null;
|
|
989
|
+
if (e.issues.length === 0) return `junction FKs: ${e.tablesChecked} table${e.tablesChecked === 1 ? "" : "s"} checked — every link column is a wired reference()`;
|
|
990
|
+
let t = e.issues.length, n = [`junction FKs: ${t} junction table${t === 1 ? "" : "s"} with an id column that is a plain text() and not a reference()`];
|
|
991
|
+
for (let t of e.issues) {
|
|
992
|
+
let e = t.columns.map((e) => `'${e}'`).join(", ");
|
|
993
|
+
n.push(` '${t.table}': ${e} (${Ze[t.signal]})`);
|
|
994
|
+
}
|
|
995
|
+
return n.push(" Declare each as reference(() => <table>): the FK is enforced, the column is auto-indexed,"), n.push(" and the relationship becomes walkable (a plain text() id column is an invisible edge)."), n.join("\n");
|
|
996
|
+
}, $e = /* @__PURE__ */ new Set(/* @__PURE__ */ "position.positions.move.moved.moving.cursor.pointer.mouse.drag.dragging.hover.frame.frames.tick.ticks.ping.heartbeat.typing.scroll.scrolling.zoom.pan.coord.coords.coordinate.coordinates.telemetry.sensor.sample.samples.viewport".split(".")), et = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter((e) => e.length > 0).map((e) => e.toLowerCase()), tt = (e) => et(e).some((e) => $e.has(e)), nt = (e) => {
|
|
997
|
+
let t = [], n = [];
|
|
998
|
+
for (let r of e) t.push({
|
|
999
|
+
event: r.name,
|
|
1000
|
+
delivery: r.delivery
|
|
1001
|
+
}), r.keyFieldCount >= 3 && n.push({
|
|
1002
|
+
event: r.name,
|
|
1003
|
+
kind: "wide-key",
|
|
1004
|
+
keyFieldCount: r.keyFieldCount
|
|
1005
|
+
}), r.hasWebhook && tt(r.name) && n.push({
|
|
1006
|
+
event: r.name,
|
|
1007
|
+
kind: "webhook-high-rate"
|
|
1008
|
+
});
|
|
1009
|
+
return {
|
|
1010
|
+
eventsChecked: e.length,
|
|
1011
|
+
modes: t,
|
|
1012
|
+
warnings: n
|
|
1013
|
+
};
|
|
1014
|
+
}, rt = (e) => {
|
|
1015
|
+
if (e.eventsChecked === 0) return null;
|
|
1016
|
+
let t = e.eventsChecked, n = [`event delivery: ${t} declared event${t === 1 ? "" : "s"} — the mode decides what a MISSING message means`];
|
|
1017
|
+
for (let t of e.modes) n.push(` '${t.event}': ${t.delivery}`);
|
|
1018
|
+
return n.push(" each — every delivery matters; a slow subscriber loses the oldest and is TOLD how many (the default)."), n.push(" latest — a newer delivery supersedes a pending one; a slow subscriber gets the current value, told nothing."), n.push(" Two events with identical route/subscriber/buffer numbers mean OPPOSITE things about a drop — the mode is the difference."), n.join("\n");
|
|
1019
|
+
}, it = (e) => {
|
|
1020
|
+
if (e.warnings.length === 0) return null;
|
|
1021
|
+
let t = e.warnings.length, n = [`events that may not scale as declared: ${t} finding${t === 1 ? "" : "s"}`];
|
|
1022
|
+
for (let t of e.warnings) t.kind === "wide-key" ? (n.push(` '${t.event}': routing key has ${t.keyFieldCount ?? 0} fields — every field FRAGMENTS the subscriber set (distinct routes are the PRODUCT of the fields’ value spaces).`), n.push(" Check each is an ADDRESS the delivery is decided by (arenaId), not a discriminator the handler reads (gameType) — the latter is payload, not key.")) : (n.push(` '${t.event}': a webhook block on a per-frame-looking event — every publish is N HTTP deliveries per second per subscribed target.`), n.push(" The webhook rate limit DEFERS the excess as pending rows rather than failing, so the symptom is a growing table. Publish a coarser event (a summary / state change) for the outside world."));
|
|
1023
|
+
return n.push(" WARNING, not a fault — a producer may intend the fan-out. `voltro doctor --json` prints every finding."), n.join("\n");
|
|
1024
|
+
}, at = /* @__PURE__ */ new Set([
|
|
1025
|
+
"start",
|
|
1026
|
+
"run",
|
|
1027
|
+
"child"
|
|
1028
|
+
]), ot = (e) => k.isCallExpression(e) && (() => {
|
|
1029
|
+
let t = e.getExpression();
|
|
1030
|
+
if (!k.isPropertyAccessExpression(t) || !at.has(t.getName())) return !1;
|
|
1031
|
+
let n = t.getExpression();
|
|
1032
|
+
return k.isPropertyAccessExpression(n) ? n.getName() === "workflows" : k.isIdentifier(n) && n.getText() === "workflows";
|
|
1033
|
+
})(), st = (e, t, n) => {
|
|
1034
|
+
let r = new Map(t.map((e) => [e.name, e])), i = [...r.keys()].sort(), a = [], o = [], s = 0, c = 0, l = new A({
|
|
1035
|
+
useInMemoryFileSystem: !0,
|
|
1036
|
+
skipAddingFilesFromTsConfig: !0
|
|
1037
|
+
});
|
|
1038
|
+
for (let t of e) {
|
|
1039
|
+
let e;
|
|
1040
|
+
try {
|
|
1041
|
+
let n = E(t, "utf8");
|
|
1042
|
+
if (!n.includes("workflows.")) continue;
|
|
1043
|
+
e = l.createSourceFile(`/scan${t.endsWith("x") ? ".tsx" : ".ts"}`, n, { overwrite: !0 });
|
|
1044
|
+
} catch {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
let u = S(n, t);
|
|
1048
|
+
for (let t of e.getDescendantsOfKind(j.CallExpression)) {
|
|
1049
|
+
if (!ot(t)) continue;
|
|
1050
|
+
c++;
|
|
1051
|
+
let e = t.getStartLineNumber(), [n, l] = t.getArguments();
|
|
1052
|
+
if (n === void 0 || !k.isStringLiteral(n)) {
|
|
1053
|
+
o.push({
|
|
1054
|
+
file: u,
|
|
1055
|
+
line: e,
|
|
1056
|
+
reason: "workflow name is not a literal"
|
|
1057
|
+
});
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
let d = n.getLiteralValue(), f = r.get(d);
|
|
1061
|
+
if (f === void 0) {
|
|
1062
|
+
a.push({
|
|
1063
|
+
kind: "unknown-workflow",
|
|
1064
|
+
file: u,
|
|
1065
|
+
line: e,
|
|
1066
|
+
name: d,
|
|
1067
|
+
known: i
|
|
1068
|
+
});
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (l === void 0 || !k.isObjectLiteralExpression(l)) {
|
|
1072
|
+
o.push({
|
|
1073
|
+
file: u,
|
|
1074
|
+
line: e,
|
|
1075
|
+
reason: "payload is not an object literal"
|
|
1076
|
+
});
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (l.getProperties().some((e) => k.isSpreadAssignment(e))) {
|
|
1080
|
+
o.push({
|
|
1081
|
+
file: u,
|
|
1082
|
+
line: e,
|
|
1083
|
+
reason: "payload spreads a value"
|
|
1084
|
+
});
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
let p = /* @__PURE__ */ new Set(), m = !1;
|
|
1088
|
+
for (let e of l.getProperties()) if (k.isPropertyAssignment(e) || k.isShorthandPropertyAssignment(e)) {
|
|
1089
|
+
let t = (k.isPropertyAssignment(e), e.getNameNode());
|
|
1090
|
+
if (k.isComputedPropertyName(t)) {
|
|
1091
|
+
m = !0;
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
p.add(e.getName().replace(/^['"]|['"]$/g, ""));
|
|
1095
|
+
} else (k.isMethodDeclaration(e) || k.isGetAccessorDeclaration(e)) && p.add(e.getName());
|
|
1096
|
+
if (m) {
|
|
1097
|
+
o.push({
|
|
1098
|
+
file: u,
|
|
1099
|
+
line: e,
|
|
1100
|
+
reason: "payload is not an object literal"
|
|
1101
|
+
});
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
s++;
|
|
1105
|
+
let h = f.required.filter((e) => !p.has(e));
|
|
1106
|
+
h.length > 0 && a.push({
|
|
1107
|
+
kind: "missing-fields",
|
|
1108
|
+
file: u,
|
|
1109
|
+
line: e,
|
|
1110
|
+
name: d,
|
|
1111
|
+
missing: h
|
|
1112
|
+
});
|
|
1113
|
+
let g = /* @__PURE__ */ new Set([...f.required, ...f.optional]);
|
|
1114
|
+
if (g.size > 0) {
|
|
1115
|
+
let t = [...p].filter((e) => !g.has(e));
|
|
1116
|
+
t.length > 0 && a.push({
|
|
1117
|
+
kind: "unknown-fields",
|
|
1118
|
+
file: u,
|
|
1119
|
+
line: e,
|
|
1120
|
+
name: d,
|
|
1121
|
+
unknown: t,
|
|
1122
|
+
accepted: [...g].sort()
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
issues: a,
|
|
1129
|
+
unchecked: o,
|
|
1130
|
+
checked: s,
|
|
1131
|
+
callSites: c
|
|
1132
|
+
};
|
|
1133
|
+
}, ct = (e) => {
|
|
1134
|
+
if (e.callSites === 0) return null;
|
|
1135
|
+
let t = [];
|
|
1136
|
+
t.push(`workflow starts: ${e.checked}/${e.callSites} call sites checked`);
|
|
1137
|
+
for (let n of e.issues) {
|
|
1138
|
+
let e = ` ${n.file}:${n.line}`;
|
|
1139
|
+
n.kind === "unknown-workflow" ? (t.push(`${e} starts unknown workflow '${n.name}'`), t.push(` registered: ${n.known.length > 0 ? n.known.join(", ") : "(none)"}`)) : n.kind === "missing-fields" ? t.push(`${e} '${n.name}' payload is missing: ${n.missing.join(", ")}`) : (t.push(`${e} '${n.name}' payload has unknown field(s): ${n.unknown.join(", ")}`), t.push(` accepted: ${n.accepted.join(", ")}`));
|
|
1140
|
+
}
|
|
1141
|
+
if (e.unchecked.length > 0) {
|
|
1142
|
+
t.push(` ${e.unchecked.length} UNCHECKED (not verified — not a pass):`);
|
|
1143
|
+
for (let n of e.unchecked) t.push(` ${n.file}:${n.line} — ${n.reason}`);
|
|
1144
|
+
}
|
|
1145
|
+
return e.issues.length === 0 && e.unchecked.length === 0 && t.push(" every call site matches its workflow"), t.join("\n");
|
|
1146
|
+
}, lt = /(^|[/\\])(\.env(\.[\w.-]+)?|.*\.ya?ml|Dockerfile|.*\.tf|.*\.toml|Procfile)$/i, G = "VOLTRO_FIELD_ENCRYPTION_KEY", ut = (e) => {
|
|
1147
|
+
let t;
|
|
1148
|
+
for (let [n, r] of e.sources) if (t === void 0 && /\bfieldEncryption\b/.test(r) && (t = {
|
|
1149
|
+
reason: "fieldEncryption",
|
|
1150
|
+
file: n
|
|
1151
|
+
}), t === void 0 && /\.encrypted\s*\(/.test(r) && (t = {
|
|
1152
|
+
reason: "encrypted-column",
|
|
1153
|
+
file: n
|
|
1154
|
+
}), r.includes(G)) return;
|
|
1155
|
+
if (t !== void 0) {
|
|
1156
|
+
for (let [, t] of e.envFiles) if (t.includes(G)) return;
|
|
1157
|
+
return t;
|
|
1158
|
+
}
|
|
1159
|
+
}, dt = (e) => [
|
|
1160
|
+
`✗ ${G} is named nowhere under this app root, and the app encrypts columns at rest`,
|
|
1161
|
+
e.reason === "fieldEncryption" ? ` (\`fieldEncryption\` in ${e.file})` : ` (an \`.encrypted()\` column in ${e.file})`,
|
|
1162
|
+
"",
|
|
1163
|
+
" A deploy without it does not degrade — it refuses to boot at `onActivate`,",
|
|
1164
|
+
" in the container, after you have pushed. Mint the key with",
|
|
1165
|
+
"",
|
|
1166
|
+
" voltro secret generate field-encryption",
|
|
1167
|
+
"",
|
|
1168
|
+
" and put it wherever the session secret already lives. LOSE THE KEY, LOSE",
|
|
1169
|
+
" THE DATA: a column encrypted under a key you no longer have has no",
|
|
1170
|
+
" recovery path, so it belongs in the same backup.",
|
|
1171
|
+
"",
|
|
1172
|
+
" This checks only that the name appears SOMEWHERE in the repo. Whether it",
|
|
1173
|
+
" reaches your deploy target is not knowable from here — a secret manager",
|
|
1174
|
+
" or a CI variable is not a file. Naming it nowhere is conclusive; naming",
|
|
1175
|
+
" it somewhere is not a guarantee."
|
|
1176
|
+
].join("\n"), ft = (e) => e === "effect" || e.startsWith("@effect/") || e.startsWith("@voltro/") || e === "react" || e === "react-dom", pt = /(^|\/)(\.bin|\.cache|\.pnpm-store|dist|build)(\/|$)/, mt = (e) => {
|
|
1177
|
+
let t = [], n = e;
|
|
1178
|
+
for (let e = 0; e < 12; e++) {
|
|
1179
|
+
let e = x(n, "node_modules");
|
|
1180
|
+
T(e) && t.push(e);
|
|
1181
|
+
let r = b(n);
|
|
1182
|
+
if (r === n) break;
|
|
1183
|
+
n = r;
|
|
1184
|
+
}
|
|
1185
|
+
return t;
|
|
1186
|
+
}, ht = (e) => {
|
|
1187
|
+
try {
|
|
1188
|
+
let t = JSON.parse(E(x(e, "package.json"), "utf8"));
|
|
1189
|
+
return typeof t.version == "string" ? t.version : void 0;
|
|
1190
|
+
} catch {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
}, gt = (e) => {
|
|
1194
|
+
let t = [], n;
|
|
1195
|
+
try {
|
|
1196
|
+
n = D(e);
|
|
1197
|
+
} catch {
|
|
1198
|
+
return t;
|
|
1199
|
+
}
|
|
1200
|
+
for (let r of n) {
|
|
1201
|
+
if (r.startsWith(".") || pt.test(r)) continue;
|
|
1202
|
+
let n = x(e, r);
|
|
1203
|
+
if (r.startsWith("@")) {
|
|
1204
|
+
let e;
|
|
1205
|
+
try {
|
|
1206
|
+
e = D(n);
|
|
1207
|
+
} catch {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
for (let i of e) t.push({
|
|
1211
|
+
pkg: `${r}/${i}`,
|
|
1212
|
+
dir: x(n, i)
|
|
1213
|
+
});
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
if (!O(n).isDirectory()) continue;
|
|
1218
|
+
} catch {
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
t.push({
|
|
1222
|
+
pkg: r,
|
|
1223
|
+
dir: n
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
return t;
|
|
1227
|
+
}, _t = (e) => {
|
|
1228
|
+
let t = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (let n of mt(e)) for (let { pkg: e, dir: r } of gt(n)) {
|
|
1230
|
+
if (!ft(e)) continue;
|
|
1231
|
+
let n = ht(r);
|
|
1232
|
+
if (n === void 0) continue;
|
|
1233
|
+
let i = t.get(e) ?? /* @__PURE__ */ new Map();
|
|
1234
|
+
i.has(n) || i.set(n, r), t.set(e, i);
|
|
1235
|
+
}
|
|
1236
|
+
let n = [];
|
|
1237
|
+
for (let [r, i] of t) i.size < 2 || n.push({
|
|
1238
|
+
pkg: r,
|
|
1239
|
+
versions: [...i.keys()].sort(),
|
|
1240
|
+
paths: [...i.values()].map((t) => S(e, t)).sort()
|
|
1241
|
+
});
|
|
1242
|
+
return n.sort((e, t) => e.pkg.localeCompare(t.pkg));
|
|
1243
|
+
}, vt = (e) => {
|
|
1244
|
+
if (e.length !== 0) return [
|
|
1245
|
+
`• ${e.length} package(s) resolved at more than one version:`,
|
|
1246
|
+
...e.flatMap((e) => [` ${e.pkg} — ${e.versions.join(", ")}`, ...e.paths.map((e) => ` ${e}`)]),
|
|
1247
|
+
"",
|
|
1248
|
+
" Two copies of `effect` (or `@effect/*`) is the usual cause of type errors",
|
|
1249
|
+
" inside the GENERATED rpcGroup.generated.ts that look like framework bugs:",
|
|
1250
|
+
" \"Property '[TypeId]' is missing\", \"typeof Never is not assignable to All\",",
|
|
1251
|
+
" or an `Rpc<…, Stream<…>, …>` that will not go where `Any` is expected.",
|
|
1252
|
+
" Effect types are nominal, so a Schema built by one copy is not the same",
|
|
1253
|
+
" type as a Schema expected by the other — and the RUNTIME often still works,",
|
|
1254
|
+
" which is why this shows up as a red tsc against a green server.",
|
|
1255
|
+
"",
|
|
1256
|
+
" Fix it in the install, not the code: align the version across the workspace",
|
|
1257
|
+
" (a root `pnpm.overrides` / `resolutions` entry for `effect` is the blunt",
|
|
1258
|
+
" instrument), then reinstall. Do not silence the generated file — it is where",
|
|
1259
|
+
" a genuine mistake in your own descriptors surfaces."
|
|
1260
|
+
].join("\n");
|
|
1261
|
+
}, yt = (e) => e.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1"), bt = /* @__PURE__ */ new Set([
|
|
1262
|
+
"requireScope",
|
|
1263
|
+
"requireAnyScope",
|
|
1264
|
+
"requireAuth",
|
|
1265
|
+
"assertScope",
|
|
1266
|
+
"assertCan",
|
|
1267
|
+
"can"
|
|
1268
|
+
]), xt = (e) => {
|
|
1269
|
+
let t = /* @__PURE__ */ new Set();
|
|
1270
|
+
for (let n of e) for (let e of yt(n).matchAll(/\bexport\s+(?:const|function|async\s+function)\s+((?:require|assert)[A-Z]\w*)/g)) t.add(e[1]);
|
|
1271
|
+
return t;
|
|
1272
|
+
}, St = (e, t) => {
|
|
1273
|
+
for (let n of t) if (RegExp(`\\b${n}\\s*\\(`).test(e)) return n;
|
|
1274
|
+
}, Ct = (e) => {
|
|
1275
|
+
let t = /\bsubject\s*\.\s*id\b[^\n]*?(?:!==|===|!=|==)|(?:!==|===|!=|==)[^\n]*?\bsubject\s*\.\s*id\b/.test(e), n = /(?:Error|Denied|Forbidden|Unauthorized)\b|\b(?:fail|die|throw)\b/.test(e);
|
|
1276
|
+
return t && n;
|
|
1277
|
+
}, wt = (e, t, n) => {
|
|
1278
|
+
if (e.hasDeclaredGuards) return { kind: "declared-guard" };
|
|
1279
|
+
let r = yt(e.src), i = St(r, t);
|
|
1280
|
+
return i === void 0 ? Ct(r) ? { kind: "inline-ownership" } : n.has(e.tag) ? { kind: "allowlisted" } : { kind: "unchecked" } : {
|
|
1281
|
+
kind: "vocabulary-guard",
|
|
1282
|
+
name: i
|
|
1283
|
+
};
|
|
1284
|
+
}, Tt = (e, t, n) => {
|
|
1285
|
+
let r = e.op === "delete" ? 3 : e.op === "update" ? 2 : 1;
|
|
1286
|
+
return e.table !== void 0 && (t.has(e.table) && (r += 2), n.has(e.table) && e.op === "delete" && (r += 2)), r;
|
|
1287
|
+
}, Et = (e, t) => {
|
|
1288
|
+
switch (t.kind) {
|
|
1289
|
+
case "declared-guard": return "guards: on the descriptor";
|
|
1290
|
+
case "vocabulary-guard": return `calls ${t.name}()`;
|
|
1291
|
+
case "inline-ownership": return "compares the caller to the row and refuses — correct, but only this file knows it";
|
|
1292
|
+
case "allowlisted": return "accepted by a previous run";
|
|
1293
|
+
case "unchecked": return e.op === "read" ? "returns a row the client selected, with nothing compared to the caller" : `${e.op}s${e.table ? ` ${e.table}` : ""} with nothing constraining WHICH row`;
|
|
1294
|
+
}
|
|
1295
|
+
}, Dt = (e) => {
|
|
1296
|
+
let t = e.executors.map((t) => {
|
|
1297
|
+
let n = wt(t, e.guardVocabulary, e.allowlist);
|
|
1298
|
+
return {
|
|
1299
|
+
tag: t.tag,
|
|
1300
|
+
path: t.path,
|
|
1301
|
+
verdict: n,
|
|
1302
|
+
severity: Tt(t, e.tenantScopedTables, e.referencedTables),
|
|
1303
|
+
why: Et(t, n)
|
|
1304
|
+
};
|
|
1305
|
+
}), n = (e) => t.filter((t) => t.verdict.kind === e).length;
|
|
1306
|
+
return {
|
|
1307
|
+
findings: t,
|
|
1308
|
+
counts: {
|
|
1309
|
+
declaredGuard: n("declared-guard"),
|
|
1310
|
+
vocabularyGuard: n("vocabulary-guard"),
|
|
1311
|
+
inlineOwnership: n("inline-ownership"),
|
|
1312
|
+
allowlisted: n("allowlisted"),
|
|
1313
|
+
unchecked: n("unchecked")
|
|
1314
|
+
},
|
|
1315
|
+
unchecked: t.filter((e) => e.verdict.kind === "unchecked").sort((e, t) => t.severity - e.severity || e.tag.localeCompare(t.tag))
|
|
1316
|
+
};
|
|
1317
|
+
}, Ot = (e) => [
|
|
1318
|
+
"# voltro doctor — authz allowlist",
|
|
1319
|
+
"#",
|
|
1320
|
+
"# Executors with no access check that a previous run accepted. This is DEBT,",
|
|
1321
|
+
"# not approval: every line is an executor nobody has confirmed is safe.",
|
|
1322
|
+
"#",
|
|
1323
|
+
"# Doctor fails on any executor NOT listed here, so the list can only shrink",
|
|
1324
|
+
"# unless someone adds a line deliberately. Delete a line once its executor",
|
|
1325
|
+
"# has a guard — doctor reports it as guarded from then on, whether or not the",
|
|
1326
|
+
"# line is still here.",
|
|
1327
|
+
"",
|
|
1328
|
+
...[...e].sort(),
|
|
1329
|
+
""
|
|
1330
|
+
].join("\n"), kt = (e) => new Set(e.split("\n").map((e) => e.trim()).filter((e) => e.length > 0 && !e.startsWith("#"))), At = (e) => {
|
|
1331
|
+
let t = e.indexOf(".");
|
|
1332
|
+
return t <= 0 ? void 0 : e.slice(0, t);
|
|
1333
|
+
}, jt = (e, t) => {
|
|
1334
|
+
let n = new Set(t.tables), r = new Set(t.rpcTags), i = /* @__PURE__ */ new Map();
|
|
1335
|
+
for (let e of t.rpcTags) {
|
|
1336
|
+
let t = At(e);
|
|
1337
|
+
if (t === void 0) continue;
|
|
1338
|
+
let n = i.get(t);
|
|
1339
|
+
n ? n.push(e) : i.set(t, [e]);
|
|
1340
|
+
}
|
|
1341
|
+
let a = [];
|
|
1342
|
+
for (let o of e) {
|
|
1343
|
+
for (let e of o.tables) {
|
|
1344
|
+
if (e.renamedFrom === void 0 || !n.has(e.renamedFrom)) continue;
|
|
1345
|
+
let r = t.rowCounts?.[e.renamedFrom];
|
|
1346
|
+
a.push({
|
|
1347
|
+
kind: "renamed-from-yours",
|
|
1348
|
+
plugin: o.plugin,
|
|
1349
|
+
detail: `plugin table \`${e.tableName}\` declares .renamedFrom('${e.renamedFrom}'), which you also declare` + (r === void 0 ? "" : ` (${r} row${r === 1 ? "" : "s"})`),
|
|
1350
|
+
fix: `your table keeps its rows and \`${e.tableName}\` is created EMPTY — that is intended, not a failed migration. ` + (o.canDisableTables ? "Pass `tables: false` if you want the plugin to bring no persistence at all." : "This plugin has no `tables` option, so its table is created either way; it is inert if nothing reads it."),
|
|
1351
|
+
fatal: !1
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
let e = o.rpcTags.filter((e) => r.has(e));
|
|
1355
|
+
if (e.length > 0) {
|
|
1356
|
+
a.push({
|
|
1357
|
+
kind: "rpc-tag-collision",
|
|
1358
|
+
plugin: o.plugin,
|
|
1359
|
+
detail: `rpc tag${e.length === 1 ? "" : "s"} ${e.map((e) => `\`${e}\``).join(", ")} ${e.length === 1 ? "is" : "are"} declared by BOTH this plugin and your app`,
|
|
1360
|
+
fix: o.canAlias ? "pass `alias: '<prefix>'` to move the plugin's namespace." : "this plugin has no `alias` option — rename your procedure, or do not install it.",
|
|
1361
|
+
fatal: !0
|
|
1362
|
+
});
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
let s = new Set(o.rpcTags.map(At).filter((e) => e !== void 0));
|
|
1366
|
+
for (let e of s) {
|
|
1367
|
+
let t = i.get(e);
|
|
1368
|
+
t !== void 0 && a.push({
|
|
1369
|
+
kind: "rpc-namespace",
|
|
1370
|
+
plugin: o.plugin,
|
|
1371
|
+
detail: `rpc namespace \`${e}.*\` is shared with ${t.length} of your procedure${t.length === 1 ? "" : "s"}`,
|
|
1372
|
+
fix: o.canAlias ? "pass `alias: '<prefix>'` so one namespace has one owner." : "this plugin has no `alias` option yet — the namespace stays split until it does.",
|
|
1373
|
+
fatal: !1
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return a;
|
|
1378
|
+
}, Mt = (e, t) => {
|
|
1379
|
+
let n = e.reduce((e, t) => e + t.tables.length, 0), r = [`plugin surface · ${e.length} plugin(s) contribute ${n} table(s)`];
|
|
1380
|
+
if (t.length === 0) return r.push(" ✓ no overlap with your own tables or rpc namespaces"), r;
|
|
1381
|
+
for (let e of t) r.push(` ${e.fatal ? "✗" : "⚠"} ${e.plugin} — ${e.detail}`), r.push(` ${e.fix}`);
|
|
1382
|
+
return r;
|
|
1383
|
+
}, Nt = (e) => T(C(e, "app.config.ts")) || T(C(e, "app.config.js")), Pt = (e) => {
|
|
1384
|
+
let t = y(e), n = v(e);
|
|
1385
|
+
return {
|
|
1386
|
+
root: e,
|
|
1387
|
+
serveBundle: t,
|
|
1388
|
+
serveBundlePresent: T(t),
|
|
1389
|
+
apiBundle: n,
|
|
1390
|
+
apiBundlePresent: T(n)
|
|
1391
|
+
};
|
|
1392
|
+
}, Ft = [
|
|
1393
|
+
"Production `voltro serve` requires a precompiled serve bundle and never transpiles on demand.",
|
|
1394
|
+
"Add a `voltro build .` step BEFORE `voltro serve .` in your Dockerfile / start script.",
|
|
1395
|
+
"(Generated Dockerfiles from `voltro create-project` already do this.)"
|
|
1396
|
+
].join("\n "), It = (e, t = !1) => {
|
|
1397
|
+
let n = Pt(e);
|
|
1398
|
+
if (console.log(`voltro doctor — serve preflight for ${n.root}`), !Nt(e)) return console.log("• no app.config found here — nothing to check (run against an app root)."), 0;
|
|
1399
|
+
console.log(`${n.serveBundlePresent ? "✓" : "✗"} serve bundle: ${n.serveBundlePresent ? "present" : "MISSING"} (${n.serveBundle})`), console.log(`${n.apiBundlePresent ? "✓" : "•"} api bundle: ${n.apiBundlePresent ? "present" : "absent"} (${n.apiBundle})`);
|
|
1400
|
+
let r = process.env.NODE_ENV === "production";
|
|
1401
|
+
if (n.serveBundlePresent) return console.log("✓ serve preflight: ready for `voltro serve`."), 0;
|
|
1402
|
+
let i = t || r;
|
|
1403
|
+
return console.error(`${i ? "✗" : "•"} serve bundle missing — ${i ? "production `voltro serve` will FAIL." : "a non-production `voltro serve` still runs via tsx."}`), console.error(` ${Ft}`), +!!i;
|
|
1404
|
+
}, Lt = (e, t, n) => {
|
|
1405
|
+
let r = new Set(e), i = I(n), a = (e) => (/\.(tsx?|jsx?)$/.test(e) ? [e] : [
|
|
1406
|
+
`${e}.ts`,
|
|
1407
|
+
`${e}.tsx`,
|
|
1408
|
+
`${e}.js`,
|
|
1409
|
+
`${e}.jsx`,
|
|
1410
|
+
x(e, "index.ts"),
|
|
1411
|
+
x(e, "index.tsx")
|
|
1412
|
+
]).find((e) => r.has(e)), o = (e) => {
|
|
1413
|
+
let n = (t.get(e) ?? "").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/[^\n]*/g, "$1"), r = [];
|
|
1414
|
+
for (let e of [
|
|
1415
|
+
/(?:^|[\n;}])\s*(?:import|export)\s+[^;'"]*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
1416
|
+
/(?:^|[\n;}])\s*import\s+['"]([^'"]+)['"]/g,
|
|
1417
|
+
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
1418
|
+
]) {
|
|
1419
|
+
let t;
|
|
1420
|
+
for (; (t = e.exec(n)) !== null;) r.push(t[1]);
|
|
1421
|
+
}
|
|
1422
|
+
let o = [];
|
|
1423
|
+
for (let t of r) {
|
|
1424
|
+
let n = t.startsWith(".") ? C(b(e), t) : ye(t, i);
|
|
1425
|
+
if (n === void 0) continue;
|
|
1426
|
+
let r = a(n);
|
|
1427
|
+
r !== void 0 && o.push(r);
|
|
1428
|
+
}
|
|
1429
|
+
return o;
|
|
1430
|
+
}, s = /* @__PURE__ */ new Set(), c = e.filter((e) => Ut.test(S(n, e))), l = new Set(c);
|
|
1431
|
+
for (; c.length > 0;) {
|
|
1432
|
+
let e = c.shift();
|
|
1433
|
+
if (!s.has(e)) {
|
|
1434
|
+
s.add(e);
|
|
1435
|
+
for (let t of o(e)) s.has(t) || c.push(t);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
let u = /* @__PURE__ */ new Set();
|
|
1439
|
+
for (let t of e) if (!s.has(t)) for (let e of o(t)) u.add(e);
|
|
1440
|
+
let d = /* @__PURE__ */ new Set();
|
|
1441
|
+
for (let e of s) l.has(e) || u.has(e) || d.add(e);
|
|
1442
|
+
return d;
|
|
1443
|
+
}, Rt = (e) => e.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/'(?:[^'\\\n]|\\.)*'/g, "''").replace(/"(?:[^"\\\n]|\\.)*"/g, "\"\""), zt = (e, t) => (e.match(t) ?? []).length, K = (e, t) => e.getDescendantsOfKind(j.CallExpression).filter((e) => t.test(e.getExpression().getText())).length, Bt = (e, t) => {
|
|
1444
|
+
if (e.src.includes(t)) return !0;
|
|
1445
|
+
let n = t.replace(/^@voltro\/plugin-/, "");
|
|
1446
|
+
return n !== t && K(e.file, RegExp(`^${n}Plugin$`)) > 0;
|
|
1447
|
+
}, Vt = (e, t) => {
|
|
1448
|
+
let n = [], r = RegExp(`\\b${t}\\s*(?:<[^()]*>)?\\s*\\(`, "g"), i;
|
|
1449
|
+
for (; (i = r.exec(e)) !== null;) {
|
|
1450
|
+
let t = i.index + i[0].length, r = 1, a = t;
|
|
1451
|
+
for (; a < e.length && r > 0; a++) e[a] === "(" ? r++ : e[a] === ")" && r--;
|
|
1452
|
+
n.push(e.slice(t, a - 1));
|
|
1453
|
+
}
|
|
1454
|
+
return n;
|
|
1455
|
+
}, Ht = (e, t) => {
|
|
1456
|
+
let n = t.getSymbol()?.getDeclarations()?.[0];
|
|
1457
|
+
return n === void 0 ? [] : e.getDescendantsOfKind(j.Identifier).filter((e) => e.getSymbol()?.getDeclarations()?.[0] === n);
|
|
1458
|
+
}, Ut = /\.(server|cron|workflow\.server|subscribe|reaction|outbox|startup|trigger|webhook|migration|seed)\.tsx?$/, Wt = /\.(query|mutation|action|stream)\.server\.tsx?$/, Gt = [
|
|
1459
|
+
"Id",
|
|
1460
|
+
"ID",
|
|
1461
|
+
"Hash",
|
|
1462
|
+
"HASH",
|
|
1463
|
+
"Digest",
|
|
1464
|
+
"Fingerprint",
|
|
1465
|
+
"Name",
|
|
1466
|
+
"Type",
|
|
1467
|
+
"Kind",
|
|
1468
|
+
"Label",
|
|
1469
|
+
"Prefix",
|
|
1470
|
+
"Suffix",
|
|
1471
|
+
"Status",
|
|
1472
|
+
"State",
|
|
1473
|
+
"Scope",
|
|
1474
|
+
"Scopes",
|
|
1475
|
+
"Algorithm",
|
|
1476
|
+
"Algo",
|
|
1477
|
+
"Version",
|
|
1478
|
+
"Count",
|
|
1479
|
+
"Note",
|
|
1480
|
+
"Description",
|
|
1481
|
+
"Comment",
|
|
1482
|
+
"Provider",
|
|
1483
|
+
"Format"
|
|
1484
|
+
], Kt = RegExp(`(?:${Gt.join("|")})$|_(?:${Gt.map((e) => e.toLowerCase()).join("|")})$`), qt = [
|
|
1485
|
+
{
|
|
1486
|
+
id: "executor-without-descriptor",
|
|
1487
|
+
scope: "server",
|
|
1488
|
+
smell: "an executor that never names its own descriptor — its input type is hand-written, so it can drift from the contract it implements",
|
|
1489
|
+
use: "import the descriptor and type the input as `ExecutorInput<typeof descriptor>`, so the wire contract and the handler cannot disagree",
|
|
1490
|
+
match: ({ src: e, path: t }) => Wt.test(t) && !/\bimport\s[^;]*\bfrom\s*['"][^'"]*['"]/.test((e.match(/^\s*import\s[^;]*from\s*['"]\.[^'"]*['"]/gm) ?? []).join("\n"))
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
id: "subject-write-no-guard",
|
|
1494
|
+
scope: "server",
|
|
1495
|
+
smell: "writes ctx.request.subject.id into the database with no guard on the descriptor — an anonymous caller reaches the write and gets a raw statement failure instead of a typed refusal",
|
|
1496
|
+
use: "add a guard (e.g. `.guard(requireScope('…'))`) or an explicit auth check, so an unauthenticated caller is refused BEFORE the write",
|
|
1497
|
+
match: ({ src: e, path: t }) => {
|
|
1498
|
+
if (!Wt.test(t)) return !1;
|
|
1499
|
+
let n = Rt(e);
|
|
1500
|
+
return /\bsubject\s*\.\s*id\b/.test(n) && /\b(?:insert|update|upsert|insertMany|updateMany|insertRow|upsertRow|updateManyRow)\s*\(/.test(n) && !/\b(?:guard|guards|requireScope|requireAnyScope|assertCan|assertScope|requireAuth|Unauthenticated)\b/.test(n);
|
|
1501
|
+
}
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
id: "raw-fetch",
|
|
1505
|
+
scope: "server",
|
|
1506
|
+
smell: "raw fetch() for outbound HTTP on the server (no SSRF guard, no traceparent, no retry)",
|
|
1507
|
+
use: "yield* HttpClient — the framework client. It blocks loopback / RFC-1918 / link-local targets (incl. 169.254.169.254) on the initial request AND every redirect hop, injects traceparent, and honours `http.allowHosts` for deliberate internal targets",
|
|
1508
|
+
match: ({ file: e, path: t, serverOnly: n }) => (Ut.test(t) || n) && K(e, /^(?:globalThis\.|window\.|self\.)?fetch$/) > 0
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
id: "form",
|
|
1512
|
+
scope: "client",
|
|
1513
|
+
smell: "per-field useState + a submit flag (hand-rolled form)",
|
|
1514
|
+
use: "useFormBinding — fields + validation from the mutation input Schema; server ValidationError({ field }) routes to the field",
|
|
1515
|
+
match: ({ src: e, file: t }) => K(t, /^useState$/) >= 4 && /onSubmit|handleSubmit|isSubmitting/.test(e)
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
id: "table",
|
|
1519
|
+
scope: "client",
|
|
1520
|
+
smell: "hand-rolled table with local sort/filter state",
|
|
1521
|
+
use: "useDataTable — columns from the query output Schema, live rows, sort/filter/pagination",
|
|
1522
|
+
match: ({ src: e }) => /<TableHeader|<TableBody/.test(e) && /sort(By|Key|Field|Direction)|setSort/i.test(e)
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
id: "upload",
|
|
1526
|
+
scope: "client",
|
|
1527
|
+
smell: "FileReader / base64 upload path",
|
|
1528
|
+
use: "useUpload — progress + cancel, works on every storage provider (no base64 ceiling)",
|
|
1529
|
+
match: ({ src: e }) => /new FileReader\(|readAsDataURL\(/.test(e)
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
id: "debounce",
|
|
1533
|
+
scope: "client",
|
|
1534
|
+
smell: "setTimeout debounce inside useEffect",
|
|
1535
|
+
use: "useDebounced (and useAsyncValidation for checks that need the DB)",
|
|
1536
|
+
match: ({ src: e }) => /useEffect\(/.test(e) && /setTimeout\(/.test(e) && /clearTimeout\(/.test(e)
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
id: "derived",
|
|
1540
|
+
scope: "client",
|
|
1541
|
+
smell: "useMemo fanning in multiple subscriptions (hand-maintained dep array)",
|
|
1542
|
+
use: "useDerived — dependency-tracked, referentially stable",
|
|
1543
|
+
match: ({ src: e }) => zt(e, /\buseSubscription\s*[(<]/g) >= 3 && zt(e, /\buseMemo\s*\(/g) >= 3
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
id: "next-shim",
|
|
1547
|
+
scope: "client",
|
|
1548
|
+
smell: "local Next.js compat shim",
|
|
1549
|
+
use: "the native @voltro/web exports (useSearchParams, redirect, notFound, useServerRequest, …)",
|
|
1550
|
+
match: ({ src: e }) => /from\s+['"][^'"]*next-(compat|shims)/.test(e)
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
id: "presence",
|
|
1554
|
+
scope: "client",
|
|
1555
|
+
smell: "hand-rolled presence heartbeat/roster",
|
|
1556
|
+
use: "@voltro/plugin-presence (server table + RPCs AND the client hook)",
|
|
1557
|
+
plugin: "@voltro/plugin-presence",
|
|
1558
|
+
match: ({ src: e }) => /presence/i.test(e) && /heartbeat/i.test(e)
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
id: "subscription-undefined-check",
|
|
1562
|
+
scope: "client",
|
|
1563
|
+
smell: "checking a subscription's `data === undefined` (or `!data`) instead of its `loading` / `idle` flag",
|
|
1564
|
+
use: "branch on `loading` (and `idle` when you pass `skip`) — `loading` narrows `data`, so the settled branch needs no `?? []` and no `!`",
|
|
1565
|
+
match: ({ file: e }) => e.getDescendantsOfKind(j.CallExpression).filter((e) => /(^|\.)useSubscription$/.test(e.getExpression().getText())).flatMap((e) => {
|
|
1566
|
+
let t = e.getFirstAncestorByKind(j.VariableDeclaration)?.getNameNode();
|
|
1567
|
+
return t === void 0 ? [] : k.isObjectBindingPattern(t) ? t.getElements().filter((e) => (e.getPropertyNameNode() ?? e.getNameNode()).getText() === "data").map((e) => e.getNameNode()) : [];
|
|
1568
|
+
}).some((t) => Ht(e, t).some((e) => {
|
|
1569
|
+
let t = e.getParent();
|
|
1570
|
+
if (k.isBinaryExpression(t)) {
|
|
1571
|
+
let n = t.getOperatorToken().getText(), r = t.getLeft() === e ? t.getRight() : t.getLeft();
|
|
1572
|
+
return /^[!=]==?$/.test(n) && /^(undefined|null)$/.test(r.getText());
|
|
1573
|
+
}
|
|
1574
|
+
return k.isPrefixUnaryExpression(t) && t.getOperatorToken() === j.ExclamationToken;
|
|
1575
|
+
}))
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
id: "row-not-found",
|
|
1579
|
+
scope: "server",
|
|
1580
|
+
smell: "hand-written not-found branch on rows[0]",
|
|
1581
|
+
use: "branch fails with YOUR typed error → .maybeOne(builder) and keep the Effect.fail (removes the rows[0] hop, error contract unchanged); branch throws something generic → .one(), which raises the typed NoRowFound on zero rows AND on more than one",
|
|
1582
|
+
match: ({ src: e }) => /if\s*\(\s*!\s*[A-Za-z_$][\w$]*\s*\[\s*0\s*\]\s*\)/.test(e) || /if\s*\(\s*[A-Za-z_$][\w$]*\s*\[\s*0\s*\]\s*===?\s*(undefined|null)\s*\)/.test(e) || /\bstore\.query\s*\(/.test(e) && /if\s*\(\s*[A-Za-z_$][\w$]*\s*\.length\s*===?\s*0\s*\)/.test(e)
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
id: "row-value-read",
|
|
1586
|
+
scope: "server",
|
|
1587
|
+
smell: "first row bound to a variable (const x = rows[0]) — the same hop, without a not-found branch",
|
|
1588
|
+
use: ".maybeOne(builder) — typed first-or-null, drops the array hop entirely; .one() if the row is genuinely required and a typed NoRowFound is acceptable to the caller",
|
|
1589
|
+
match: ({ src: e }) => /\bstore\.query\s*\(/.test(e) && (/\b(?:const|let)\s+[A-Za-z_$][\w$]*\s*=\s*[A-Za-z_$][\w$]*[Rr]ows\s*\[\s*0\s*\]/.test(e) || /\breturn\s+[A-Za-z_$][\w$]*[Rr]ows\s*\[\s*0\s*\]/.test(e))
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
id: "n-plus-one",
|
|
1593
|
+
scope: "server",
|
|
1594
|
+
smell: "several sequential store reads assembling related data (an N+1 you are writing by hand)",
|
|
1595
|
+
use: "reads are a parent→child walk on ONE key → relations() in a *.relations.ts + .with({ … }) (ONE JSON-aggregate query, every dialect); reads collect ids from SEVERAL sources → keep the assembly and run the independent leading reads under Effect.all (same queries, same results, only concurrent — zero parity risk)",
|
|
1596
|
+
match: (e) => K(e.file, /\bstore\.query$/) >= 3 || K(e.file, /\bstore\.query$/) >= 2 && /\bnew Map\s*\(/.test(e.src)
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
id: "store-effect-lift",
|
|
1600
|
+
scope: "server",
|
|
1601
|
+
smell: "ctx.store lifted into Effect with Effect.promise / Effect.tryPromise",
|
|
1602
|
+
use: "yield* EffectStore — its failures land on the typed StoreError channel instead of becoming a defect",
|
|
1603
|
+
match: ({ src: e }) => /Effect\.(promise|tryPromise)\s*\(\s*(\{[^}]*try:\s*)?\(\s*\)\s*=>\s*ctx\.store\./.test(e)
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
id: "imperative-scope",
|
|
1607
|
+
scope: "server",
|
|
1608
|
+
smell: "imperative scope check at the top of an executor",
|
|
1609
|
+
use: "guards: on the descriptor — enforced BEFORE the executor (and before a mutation opens its transaction), typed ScopeError. A check that needs a LOADED row is still declarative: a policy guard names { action, resourceType, resource } and the registered resolver loads the row (an app whose relationships live in its own tables registers its own tuple source). Keep ctx.access for what genuinely cannot be answered before the handler runs",
|
|
1610
|
+
match: ({ src: e }) => /\b(requireScope|assertScope|ensureScope)\s*\(/.test(e)
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
id: "plaintext-secret",
|
|
1614
|
+
scope: "server",
|
|
1615
|
+
smell: "a credential-shaped column stored without field encryption",
|
|
1616
|
+
use: ".encrypted() on the column — boot fails loudly if no cipher is configured, so it can never silently persist plaintext (a *Hash column and a vault* handle are NOT plaintext secrets: the hash IS the protection — often a unique lookup key — and the handle only names a secret held elsewhere, so both are skipped)",
|
|
1617
|
+
match: ({ src: e }) => e.split("\n").some((e) => [...e.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*(?:text|varchar|string|longtext)\s*\(/g)].some((t) => {
|
|
1618
|
+
let n = t[1];
|
|
1619
|
+
return !/token|secret|credential|password|apikey|accesskey/i.test(n) || Kt.test(n) || /^vault/i.test(n) ? !1 : !/\.encrypted\s*\(/.test(e);
|
|
1620
|
+
}))
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
id: "mutation-tail-effect",
|
|
1624
|
+
scope: "server",
|
|
1625
|
+
smell: "a notify / webhook helper called at the tail of a mutation",
|
|
1626
|
+
use: "defineSubscriber (*.subscribe.ts) or defineReaction (*.reaction.tsx) — declarative, post-commit, with the old/new diff, instead of reactivity hidden inside every executor",
|
|
1627
|
+
match: ({ src: e, path: t }) => /\b(notify|sendNotification|sendWebhook|dispatchWebhook|trigger[A-Z]\w*)\s*\(/.test(e) && (/\bexport\s+default\b/.test(e) || /\.(mutation|action)\.server\.tsx?$/.test(t))
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
id: "hand-cursor",
|
|
1631
|
+
scope: "server",
|
|
1632
|
+
smell: "hand-rolled cursor pagination (limit+1 / slice / hasMore)",
|
|
1633
|
+
use: "paginateById — the cursor, the over-fetch probe and hasMore in one call",
|
|
1634
|
+
match: ({ src: e }) => /\bhasMore\b/.test(e) && /(limit|take|pageSize)\s*\+\s*1/.test(e)
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
id: "offset-pagination",
|
|
1638
|
+
scope: "server",
|
|
1639
|
+
smell: "OFFSET pagination (`input.offset` / `.skip(...)`) — O(n) in the page number",
|
|
1640
|
+
use: "paginateBy / paginateById — keyset pagination, constant-time per page. Sortable ids (typeid/ulid) need no schema change",
|
|
1641
|
+
match: ({ src: e, path: t }) => /\.(query|stream)\.server\.tsx?$/.test(t) && (/\.offset\s*\(/.test(e) || /\binput\.offset\b/.test(e) || /\.skip\s*\(/.test(e))
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
id: "oauth-token-table",
|
|
1645
|
+
scope: "server",
|
|
1646
|
+
smell: "a hand-rolled per-user OAuth credential store (access/refresh token columns)",
|
|
1647
|
+
use: "defineConnection({ kind: 'oauth2' }) — encrypted per-user token store, authorize/callback routes, refresh-before-use, useConnection + <ConnectAccount>",
|
|
1648
|
+
match: ({ src: e, path: t }) => /\.(entity|schema)\.tsx?$|schema\.tsx?$/.test(t) && /\b(accessToken|refreshToken|access_token|refresh_token)\b/.test(e)
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
id: "non-incremental-aggregate",
|
|
1652
|
+
scope: "server",
|
|
1653
|
+
smell: "an aggregate rescanned in full on every refresh",
|
|
1654
|
+
use: "incremental: true — count/sum/avg/min/max group-bys are maintained on write instead of rescanned",
|
|
1655
|
+
match: ({ src: e, path: t }) => /\.aggregate\.tsx?$/.test(t) && /\b(count|sum|avg|min|max)\s*\(/.test(e) && !/\bincremental\s*:/.test(e)
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
id: "external-state-library",
|
|
1659
|
+
scope: "client",
|
|
1660
|
+
smell: "a third-party client state library (jotai / zustand / redux / mobx)",
|
|
1661
|
+
use: "defineStore — reactive, participates in the subscription graph, no second runtime to keep in sync",
|
|
1662
|
+
match: ({ src: e }) => /from\s+['"](jotai|zustand|mobx|@reduxjs\/toolkit|react-redux)(\/[^'"]*)?['"]/.test(e)
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
id: "hand-route-module",
|
|
1666
|
+
scope: "client",
|
|
1667
|
+
smell: "a hand-maintained route/url module",
|
|
1668
|
+
use: ".framework/routes.generated — a per-route typed builder, so a renamed page is a compile error rather than a 404",
|
|
1669
|
+
match: ({ src: e }) => /from\s+['"][^'"]*\/(routes|route-?paths|urls|app-?url)['"]/.test(e) && !/routes\.generated/.test(e)
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
id: "hand-permission-check",
|
|
1673
|
+
scope: "client",
|
|
1674
|
+
smell: "reading a permission bag by hand in a component",
|
|
1675
|
+
use: "useCan / useCanAny / useResourceCan from @voltro/client — they read the SAME effective scopes the server guards on",
|
|
1676
|
+
match: ({ src: e, path: t }) => /\.tsx$/.test(t) && /\bpermissions\s*[?.[]/.test(e) && !/\buseCan\b|\buseResourceCan\b/.test(e)
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
id: "unexercised-row-filter",
|
|
1680
|
+
scope: "server",
|
|
1681
|
+
smell: "setRowFilter is registered, but no test passes `rowFilter:` to makeTestContext",
|
|
1682
|
+
use: "wire the filter into at least one test (`makeTestContext({ subject, store, rowFilter })`) and sabotage the predicate once to watch it go red — an untested row filter is a visibility rule nothing verifies",
|
|
1683
|
+
match: ({ file: e, testSources: t }) => K(e, /^setRowFilter$/) > 0 && ![...t.values()].some((e) => {
|
|
1684
|
+
let t = Rt(e);
|
|
1685
|
+
return Vt(t, "makeTestContext").length > 0 && /\browFilter\b/.test(t);
|
|
1686
|
+
})
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
id: "hand-serialized-date",
|
|
1690
|
+
scope: "server",
|
|
1691
|
+
smell: "Date converted by hand on the way out of a handler",
|
|
1692
|
+
use: "timestampMs / timestampMsOrNull from @voltro/database/wire in the descriptor's output struct — `output` IS the serializer (it is the rpc success schema), so a Date field declared as timestampMs crosses as epoch ms on its own; declaring Schema.Number describes the WIRE type and leaves nothing to convert",
|
|
1693
|
+
match: ({ src: e }) => /\.(getTime|toISOString)\s*\(\s*\)/.test(e) && /\.map\s*\(|=>\s*\(\{/.test(e) && /\bexport\s+default\b/.test(e)
|
|
1694
|
+
}
|
|
1695
|
+
], Jt = (e) => J(e), Yt = (e, t) => {
|
|
1696
|
+
let n = /* @__PURE__ */ new Set();
|
|
1697
|
+
for (let r of t) {
|
|
1698
|
+
let t = S(e, r), i = t.split(/[\\/]/)[0];
|
|
1699
|
+
n.add(i !== void 0 && i !== t ? i : ".");
|
|
1700
|
+
}
|
|
1701
|
+
return [...n].sort();
|
|
1702
|
+
}, Xt = /\.(tsx?|jsx?)$/, q = /(^|\/)(node_modules|dist|build|\.framework|\.next|coverage)(\/|$)/, Zt = /(^|[\\/])[^\\/]*\.generated\.[cm]?[jt]sx?$|\.d\.[cm]?ts$/, Qt = (e, t = [], n = 0) => {
|
|
1703
|
+
if (n > 12 || q.test(e)) return t;
|
|
1704
|
+
let r;
|
|
1705
|
+
try {
|
|
1706
|
+
r = D(e, { withFileTypes: !0 });
|
|
1707
|
+
} catch {
|
|
1708
|
+
return t;
|
|
1709
|
+
}
|
|
1710
|
+
for (let i of r) {
|
|
1711
|
+
let r = x(e, i.name);
|
|
1712
|
+
if (i.isDirectory()) {
|
|
1713
|
+
if (i.name.startsWith(".")) continue;
|
|
1714
|
+
Qt(r, t, n + 1);
|
|
1715
|
+
} else lt.test(i.name) && t.push(r);
|
|
1716
|
+
}
|
|
1717
|
+
return t;
|
|
1718
|
+
}, J = (e, t = [], n = 0) => {
|
|
1719
|
+
if (n > 12 || q.test(e)) return t;
|
|
1720
|
+
let r;
|
|
1721
|
+
try {
|
|
1722
|
+
r = D(e);
|
|
1723
|
+
} catch {
|
|
1724
|
+
return t;
|
|
1725
|
+
}
|
|
1726
|
+
for (let i of r) {
|
|
1727
|
+
let r = x(e, i), a;
|
|
1728
|
+
try {
|
|
1729
|
+
a = O(r);
|
|
1730
|
+
} catch {
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
a.isDirectory() ? J(r, t, n + 1) : Xt.test(i) && !/\.(test|spec)\./.test(i) && !Zt.test(i) && t.push(r);
|
|
1734
|
+
}
|
|
1735
|
+
return t;
|
|
1736
|
+
}, $t = (e, t = [], n = 0) => {
|
|
1737
|
+
if (n > 12 || q.test(e)) return t;
|
|
1738
|
+
let r;
|
|
1739
|
+
try {
|
|
1740
|
+
r = D(e);
|
|
1741
|
+
} catch {
|
|
1742
|
+
return t;
|
|
1743
|
+
}
|
|
1744
|
+
for (let i of r) {
|
|
1745
|
+
let r = x(e, i), a;
|
|
1746
|
+
try {
|
|
1747
|
+
a = O(r);
|
|
1748
|
+
} catch {
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
a.isDirectory() ? $t(r, t, n + 1) : Xt.test(i) && /\.(test|spec)\./.test(i) && t.push(r);
|
|
1752
|
+
}
|
|
1753
|
+
return t;
|
|
1754
|
+
}, en = (e) => {
|
|
1755
|
+
let t = Jt(e);
|
|
1756
|
+
return {
|
|
1757
|
+
dirs: Yt(e, t),
|
|
1758
|
+
fileCount: t.length
|
|
1759
|
+
};
|
|
1760
|
+
}, tn = (e) => {
|
|
1761
|
+
let t = Jt(e);
|
|
1762
|
+
if (t.length === 0) return [];
|
|
1763
|
+
let n = new A({
|
|
1764
|
+
useInMemoryFileSystem: !0,
|
|
1765
|
+
skipAddingFilesFromTsConfig: !0
|
|
1766
|
+
}), r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map();
|
|
1767
|
+
for (let e of t) try {
|
|
1768
|
+
i.set(e, E(e, "utf8"));
|
|
1769
|
+
} catch {}
|
|
1770
|
+
let a = Lt([...i.keys()], i, e), o = /* @__PURE__ */ new Map();
|
|
1771
|
+
for (let t of $t(e)) try {
|
|
1772
|
+
o.set(t, E(t, "utf8"));
|
|
1773
|
+
} catch {}
|
|
1774
|
+
for (let s of t) {
|
|
1775
|
+
let t = i.get(s);
|
|
1776
|
+
if (t === void 0) continue;
|
|
1777
|
+
let c;
|
|
1778
|
+
try {
|
|
1779
|
+
c = {
|
|
1780
|
+
src: t,
|
|
1781
|
+
file: n.createSourceFile(`/scan${s.endsWith("x") ? ".tsx" : ".ts"}`, t, { overwrite: !0 }),
|
|
1782
|
+
path: S(e, s),
|
|
1783
|
+
serverOnly: a.has(s),
|
|
1784
|
+
testSources: o
|
|
1785
|
+
};
|
|
1786
|
+
} catch {
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
for (let t of qt) {
|
|
1790
|
+
if (t.plugin !== void 0 && Bt(c, t.plugin) || !t.match(c)) continue;
|
|
1791
|
+
let n = r.get(t.id) ?? [];
|
|
1792
|
+
n.push(S(e, s)), r.set(t.id, n);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return qt.filter((e) => r.has(e.id)).map((e) => ({
|
|
1796
|
+
rule: e,
|
|
1797
|
+
files: r.get(e.id)
|
|
1798
|
+
})).sort((e, t) => t.files.length - e.files.length || e.rule.id.localeCompare(t.rule.id));
|
|
1799
|
+
}, nn = (e) => {
|
|
1800
|
+
let t = en(e);
|
|
1801
|
+
return {
|
|
1802
|
+
root: e,
|
|
1803
|
+
scannedFiles: t.fileCount,
|
|
1804
|
+
scannedDirs: t.dirs,
|
|
1805
|
+
findings: tn(e).map(({ rule: e, files: t }) => ({
|
|
1806
|
+
id: e.id,
|
|
1807
|
+
scope: e.scope,
|
|
1808
|
+
smell: e.smell,
|
|
1809
|
+
use: e.use,
|
|
1810
|
+
files: t
|
|
1811
|
+
}))
|
|
1812
|
+
};
|
|
1813
|
+
}, rn = 3, an = (e) => {
|
|
1814
|
+
let t = /* @__PURE__ */ new Map();
|
|
1815
|
+
for (let n of J(e)) try {
|
|
1816
|
+
t.set(S(e, n), E(n, "utf8"));
|
|
1817
|
+
} catch {}
|
|
1818
|
+
let n = /* @__PURE__ */ new Map();
|
|
1819
|
+
for (let t of Qt(e)) try {
|
|
1820
|
+
n.set(S(e, t), E(t, "utf8"));
|
|
1821
|
+
} catch {}
|
|
1822
|
+
let r = ut({
|
|
1823
|
+
sources: t,
|
|
1824
|
+
envFiles: n
|
|
1825
|
+
});
|
|
1826
|
+
r !== void 0 && (console.log(""), console.log(dt(r)));
|
|
1827
|
+
}, on = (e) => {
|
|
1828
|
+
let t = nn(e);
|
|
1829
|
+
if (console.log(""), t.scannedFiles === 0) return console.log(`\u2022 hand-roll scan: no source directories found under ${e}.`), console.log(" Walked the app root recursively (node_modules / dist / .framework pruned). Run `voltro doctor <app>` against an app root."), 0;
|
|
1830
|
+
if (t.findings.length === 0) return console.log(`\u2713 hand-roll scan: ${t.scannedFiles} file${t.scannedFiles === 1 ? "" : "s"} across ${t.scannedDirs.join(", ")} \u2014 no hand-rolled primitives found.`), 0;
|
|
1831
|
+
console.log(`\u2022 Shipped primitives you may be hand-rolling (scanned ${t.scannedFiles} files across ${t.scannedDirs.join(", ")}):`);
|
|
1832
|
+
let n = 0;
|
|
1833
|
+
for (let e of ["server", "client"]) {
|
|
1834
|
+
let r = t.findings.filter((t) => t.scope === e);
|
|
1835
|
+
if (r.length !== 0) {
|
|
1836
|
+
console.log(` [${e}]`);
|
|
1837
|
+
for (let e of r) {
|
|
1838
|
+
let t = e.files.slice(0, rn).join(", "), r = e.files.length - rn;
|
|
1839
|
+
r > 0 && (n += r);
|
|
1840
|
+
let i = r > 0 ? `, +${r} more` : "";
|
|
1841
|
+
console.log(` ${e.smell} \u2014 ${e.files.length} file(s): ${t}${i}`), console.log(` \u2192 ${e.use}`);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return n > 0 && console.log(` (${n} more file path${n === 1 ? "" : "s"} not shown \u2014 \`voltro doctor --json\` prints every one.)`), console.log(" (See the \"Pick the SERVER primitive\" / \"Pick the CLIENT primitive\" rubrics in AGENTS.md / CLAUDE.md.)"), t.findings.length;
|
|
1846
|
+
}, sn = "renderMode:'spa' skips this page's SSR compile while its layout shell still renders server-side — adopt it if the page BODY does not need SSR (internal/authenticated pages); keep 'ssr' if the page content needs SEO or server first-paint.", cn = /export\s+const\s+renderMode\s*(?::[^=]+)?=\s*['"]([^'"]*)['"]/, ln = /export\s+const\s+dynamic\s*=\s*['"]force-(dynamic|static)['"]/, un = (e) => {
|
|
1847
|
+
let t = cn.exec(e)?.[1];
|
|
1848
|
+
if (t !== void 0) return t === "ssr" ? "ssr" : null;
|
|
1849
|
+
let n = ln.exec(e)?.[1];
|
|
1850
|
+
return n === "dynamic" ? "ssr" : n === "static" ? null : "default";
|
|
1851
|
+
}, dn = async (e) => {
|
|
1852
|
+
let n = x(e, "src", "pages");
|
|
1853
|
+
if (!T(n)) return [];
|
|
1854
|
+
let { pages: a, dirs: o } = await t(n), s = [];
|
|
1855
|
+
for (let t of a) {
|
|
1856
|
+
if (!i(o, t.dirChain).some((e) => e.hasLayout)) continue;
|
|
1857
|
+
let a;
|
|
1858
|
+
try {
|
|
1859
|
+
a = E(x(n, t.file), "utf8");
|
|
1860
|
+
} catch {
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (r.test(a)) continue;
|
|
1864
|
+
let c = un(a);
|
|
1865
|
+
c !== null && s.push({
|
|
1866
|
+
file: S(e, x(n, t.file)),
|
|
1867
|
+
pattern: t.pattern,
|
|
1868
|
+
currentMode: c
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
return s;
|
|
1872
|
+
}, fn = 10, pn = async (e) => {
|
|
1873
|
+
if (console.log(""), !T(x(e, "src", "pages"))) return console.log("• spa-candidate scan: no src/pages here — nothing to check (web apps only)."), 0;
|
|
1874
|
+
let t = await dn(e);
|
|
1875
|
+
if (t.length === 0) return console.log("✓ spa-candidate scan: no loader-free, layout-backed pages to flag."), 0;
|
|
1876
|
+
console.log(`• renderMode:'spa' candidates (${t.length} page${t.length === 1 ? "" : "s"} — loader-free, under a layout, currently ssr/default):`);
|
|
1877
|
+
for (let e of t.slice(0, fn)) {
|
|
1878
|
+
let t = e.currentMode === "ssr" ? "renderMode:'ssr'" : "default renderMode";
|
|
1879
|
+
console.log(` ${e.file} (${e.pattern}) — ${t}`);
|
|
1880
|
+
}
|
|
1881
|
+
let n = t.length - fn;
|
|
1882
|
+
return n > 0 && console.log(` (+${n} more page${n === 1 ? "" : "s"} not shown — \`voltro doctor --json\` prints every one.)`), console.log(` → ${sn}`), t.length;
|
|
1883
|
+
}, mn = /* @__PURE__ */ new Set([
|
|
1884
|
+
"node_modules",
|
|
1885
|
+
"dist",
|
|
1886
|
+
"build",
|
|
1887
|
+
".turbo",
|
|
1888
|
+
".vite",
|
|
1889
|
+
".framework",
|
|
1890
|
+
".git"
|
|
1891
|
+
]), hn = (e, t, n) => {
|
|
1892
|
+
let r;
|
|
1893
|
+
try {
|
|
1894
|
+
r = D(e, { withFileTypes: !0 });
|
|
1895
|
+
} catch {
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
for (let i of r) {
|
|
1899
|
+
if (mn.has(i.name)) continue;
|
|
1900
|
+
let r = x(e, i.name);
|
|
1901
|
+
i.isDirectory() ? hn(r, t, n) : i.isFile() && i.name.endsWith(".tsx") && n.push(S(t, r));
|
|
1902
|
+
}
|
|
1903
|
+
}, gn = (e, t) => {
|
|
1904
|
+
let n = /* @__PURE__ */ new Set(), r = /(?:from|import)\s*\(?\s*['"](\.[^'"]*)['"]/g;
|
|
1905
|
+
for (let i of t) {
|
|
1906
|
+
let t;
|
|
1907
|
+
try {
|
|
1908
|
+
t = E(x(e, i), "utf8");
|
|
1909
|
+
} catch {
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
let a = b(x(e, i));
|
|
1913
|
+
for (let i of t.matchAll(r)) {
|
|
1914
|
+
let t = C(a, i[1]);
|
|
1915
|
+
for (let r of [
|
|
1916
|
+
t,
|
|
1917
|
+
`${t}.tsx`,
|
|
1918
|
+
`${t}.ts`,
|
|
1919
|
+
x(t, "index.tsx"),
|
|
1920
|
+
x(t, "index.ts")
|
|
1921
|
+
]) if (T(r) && O(r).isFile()) {
|
|
1922
|
+
n.add(S(e, r));
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return n;
|
|
1928
|
+
}, _n = (e, t) => {
|
|
1929
|
+
let n = e.slice(t.length).split(w).slice(0, -1)[0];
|
|
1930
|
+
return n !== void 0 && n.startsWith("[");
|
|
1931
|
+
}, Y = (t) => {
|
|
1932
|
+
let n = [], r = x(t, "src", "pages"), i = x(t, "src");
|
|
1933
|
+
if (!T(i)) return n;
|
|
1934
|
+
let a = [];
|
|
1935
|
+
hn(i, t, a), a.sort();
|
|
1936
|
+
let o = `${S(t, r)}${w}`;
|
|
1937
|
+
gn(t, a);
|
|
1938
|
+
for (let r of a) {
|
|
1939
|
+
let i = r.slice(r.lastIndexOf(w) + 1);
|
|
1940
|
+
if (r.startsWith(o)) {
|
|
1941
|
+
if (i !== "page.tsx") continue;
|
|
1942
|
+
T(x(t, e(r))) || n.push({
|
|
1943
|
+
kind: "page-without-test",
|
|
1944
|
+
file: r
|
|
1945
|
+
}), _n(r, o) && n.push({
|
|
1946
|
+
kind: "top-level-dynamic-route",
|
|
1947
|
+
file: r
|
|
1948
|
+
});
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
i === "page.tsx" && n.push({
|
|
1952
|
+
kind: "suffixed-outside-pages",
|
|
1953
|
+
file: r
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
return n;
|
|
1957
|
+
}, X = 10, Z = (e, t, n) => {
|
|
1958
|
+
if (t.length === 0) return;
|
|
1959
|
+
console.log(`• ${e} (${t.length}):`);
|
|
1960
|
+
for (let e of t.slice(0, X)) console.log(` ${e}`);
|
|
1961
|
+
let r = t.length - X;
|
|
1962
|
+
r > 0 && console.log(` (+${r} more not shown — \`voltro doctor --json\` prints every one.)`), console.log(` → ${n}`);
|
|
1963
|
+
}, vn = (e) => {
|
|
1964
|
+
if (console.log(""), !T(x(e, "src", "pages"))) return console.log("• page-convention scan: no src/pages here — nothing to check (web apps only)."), 0;
|
|
1965
|
+
let t = Y(e);
|
|
1966
|
+
if (t.length === 0) return console.log("✓ page-convention scan: every route is a page.tsx in its own directory, with a test beside it."), 0;
|
|
1967
|
+
let n = (e) => t.filter((t) => t.kind === e).map((e) => e.file);
|
|
1968
|
+
return Z("will NEVER route — page.tsx outside src/pages/", n("suffixed-outside-pages"), "the name says \"route\", the location says otherwise, and the location wins: the URL comes from the directory chain under src/pages/. Move it into the directory whose route it serves, or rename it."), Z("pages with no test beside them", n("page-without-test"), "a page and its test belong together — `users/[id].page.tsx` → `users/[id].page.test.tsx`. Advisory only; a missing test is a gap, not a defect."), Z("routes whose FIRST segment is dynamic — they claim every URL of that depth", n("top-level-dynamic-route"), "a route like `[id]/[code]` answers `/api/health`, `/internal/readiness` and anything else with that many segments on this origin. That is what the pattern means, so it is not a bug — but it is invisible until something requests such a URL, and then the page renders, its loader runs, and the failure reads as an application error rather than as a route claiming a path nobody meant it to. Give the route a literal first segment (`scan/[id]/[code]`) if you did not intend to own the whole origin at that depth. Advisory."), t.length;
|
|
1969
|
+
}, yn = "voltro doctor [app] [flags]\n\nPreflight an app root: verify it can boot under production `voltro serve`,\nreport shipped primitives the source looks to be hand-rolling, and flag pages\nthat are safe `renderMode:'spa'` candidates.\n\nArguments:\n [app] App directory to check (default: the current directory).\n\nFlags:\n --strict Treat a missing serve bundle as blocking regardless of NODE_ENV\n (the `voltro serve --preflight` behavior). Without it, a missing\n bundle only fails when NODE_ENV=production.\n --json Print the complete scan as JSON — hand-roll findings AND spa\n candidates, every file path, nothing elided. Skips the preflight output.\n --write-authz-allowlist\n Record today's executors that reference NO access check into\n voltro-authz-allowlist.txt, so every later run fails only on\n ADDITIONS. The list is DEBT, not approval — an entry stops\n mattering as soon as its executor gains a guard.\n -h, --help Print this help.", bn = /* @__PURE__ */ new Map(), Q = (e) => {
|
|
1970
|
+
let t = bn.get(e);
|
|
1971
|
+
if (t !== void 0) return t;
|
|
1972
|
+
let n = (async () => {
|
|
1973
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-DeEJj5T4.js"), r = await t(e);
|
|
1974
|
+
return {
|
|
1975
|
+
files: r,
|
|
1976
|
+
discovered: await n(r)
|
|
1977
|
+
};
|
|
1978
|
+
})();
|
|
1979
|
+
return bn.set(e, n), n;
|
|
1980
|
+
}, xn = async (e) => {
|
|
1981
|
+
let t;
|
|
1982
|
+
try {
|
|
1983
|
+
let { files: n, discovered: r } = await Q(e), i = r.tables.map((e) => {
|
|
1984
|
+
let t = e;
|
|
1985
|
+
return {
|
|
1986
|
+
name: t.tableName,
|
|
1987
|
+
columns: Object.keys(t.fields ?? {})
|
|
1988
|
+
};
|
|
1989
|
+
});
|
|
1990
|
+
if (i.length === 0) return;
|
|
1991
|
+
t = Ge(We(n, i, e));
|
|
1992
|
+
} catch {
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
t !== null && (console.log(""), console.log(`${t.includes("does not exist") ? "✗" : "✓"} ${t}`));
|
|
1996
|
+
}, Sn = (e) => e.map((e) => {
|
|
1997
|
+
let t = e, n = Object.entries(t.fields ?? {}).map(([e, t]) => ({
|
|
1998
|
+
name: e,
|
|
1999
|
+
type: typeof t?.type == "string" ? t.type : ""
|
|
2000
|
+
}));
|
|
2001
|
+
return {
|
|
2002
|
+
name: t.tableName,
|
|
2003
|
+
columns: n,
|
|
2004
|
+
primaryKey: t.appliedPrimaryKey ?? []
|
|
2005
|
+
};
|
|
2006
|
+
}), Cn = async (e) => {
|
|
2007
|
+
try {
|
|
2008
|
+
let { discovered: t } = await Q(e);
|
|
2009
|
+
return Xe(Sn(t.tables));
|
|
2010
|
+
} catch {
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
}, wn = async (e) => {
|
|
2014
|
+
let t = await Cn(e);
|
|
2015
|
+
if (t === null || t.tablesChecked === 0) return;
|
|
2016
|
+
let n = Qe(t);
|
|
2017
|
+
n !== null && (console.log(""), console.log(`${t.issues.length > 0 ? "✗" : "✓"} ${n}`));
|
|
2018
|
+
}, Tn = async (e) => {
|
|
2019
|
+
try {
|
|
2020
|
+
let { payloadPropertyNames: t } = await import("@voltro/runtime"), { discovered: n } = await Q(e);
|
|
2021
|
+
return nt(n.events.map((e) => {
|
|
2022
|
+
let n = e.descriptor, r = t(n.key);
|
|
2023
|
+
return {
|
|
2024
|
+
name: n.name,
|
|
2025
|
+
delivery: n.delivery ?? "each",
|
|
2026
|
+
keyFieldCount: r.required.length + r.optional.length,
|
|
2027
|
+
hasWebhook: n.webhook !== void 0
|
|
2028
|
+
};
|
|
2029
|
+
}));
|
|
2030
|
+
} catch {
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
}, En = async (e) => {
|
|
2034
|
+
let t = await Tn(e);
|
|
2035
|
+
if (t === null || t.eventsChecked === 0) return;
|
|
2036
|
+
let n = rt(t);
|
|
2037
|
+
n !== null && (console.log(""), console.log(`• ${n}`));
|
|
2038
|
+
let r = it(t);
|
|
2039
|
+
r !== null && (console.log(""), console.log(`⚠️ ${r}`));
|
|
2040
|
+
}, Dn = (e) => {
|
|
2041
|
+
let t = e, n = typeof t?.message == "string" && t.message !== "" ? t.message : String(e), r = typeof t?.code == "string" ? t.code : void 0;
|
|
2042
|
+
return r === void 0 ? n : `${r}: ${n}`;
|
|
2043
|
+
}, On = async (e) => {
|
|
2044
|
+
try {
|
|
2045
|
+
let { formatServerOnlyLeaks: t } = await import("@voltro/runtime"), n = (await Q(e)).discovered.serverOnlyLeaks, r = t(n);
|
|
2046
|
+
return {
|
|
2047
|
+
status: {
|
|
2048
|
+
checked: !0,
|
|
2049
|
+
leaks: n.length
|
|
2050
|
+
},
|
|
2051
|
+
...r === void 0 ? {} : { message: r }
|
|
2052
|
+
};
|
|
2053
|
+
} catch (e) {
|
|
2054
|
+
return { status: {
|
|
2055
|
+
checked: !1,
|
|
2056
|
+
reason: Dn(e)
|
|
2057
|
+
} };
|
|
2058
|
+
}
|
|
2059
|
+
}, kn = async (e) => {
|
|
2060
|
+
let { status: t, message: n } = await On(e);
|
|
2061
|
+
return t.checked ? n === void 0 ? !1 : (console.log(""), console.log(`✗ [serverOnly] ${n}`), !0) : (console.log(""), console.log("• serverOnly: NOT CHECKED — the app's descriptors could not be loaded (not a pass)"), console.log(` reason: ${t.reason ?? "unknown"}`), console.log(" This audit is the automated check that a hash or a token cannot reach the wire, so"), console.log(" an app that leans on `.serverOnly()` should treat a persistent skip as a failure."), console.log(" `voltro doctor --json` reports it as `serverOnly.checked: false` for a CI gate."), !1);
|
|
2062
|
+
}, An = async (e) => {
|
|
2063
|
+
let t;
|
|
2064
|
+
try {
|
|
2065
|
+
let { payloadPropertyNames: n } = await import("@voltro/runtime"), { files: r, discovered: i } = await Q(e);
|
|
2066
|
+
t = st(r, i.workflows.map((e) => {
|
|
2067
|
+
let t = e.definition, r = t.payloadSchema === void 0 ? {
|
|
2068
|
+
required: [],
|
|
2069
|
+
optional: []
|
|
2070
|
+
} : n(t.payloadSchema);
|
|
2071
|
+
return {
|
|
2072
|
+
name: t.name ?? "",
|
|
2073
|
+
required: r.required,
|
|
2074
|
+
optional: r.optional
|
|
2075
|
+
};
|
|
2076
|
+
}), e);
|
|
2077
|
+
} catch {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
let n = ct(t);
|
|
2081
|
+
if (n === null) return;
|
|
2082
|
+
console.log("");
|
|
2083
|
+
let r = t.issues.length > 0 ? "✗" : t.unchecked.length > 0 ? "•" : "✓";
|
|
2084
|
+
console.log(`${r} ${n}`);
|
|
2085
|
+
}, $ = "voltro-authz-allowlist.txt", jn = (e, t = (e) => {
|
|
2086
|
+
try {
|
|
2087
|
+
return E(e, "utf8");
|
|
2088
|
+
} catch {
|
|
2089
|
+
return "";
|
|
2090
|
+
}
|
|
2091
|
+
}) => /* @__PURE__ */ new Set([...bt, ...xt(J(e).map(t))]), Mn = async (e) => {
|
|
2092
|
+
let { discovered: t } = await Q(e), n = [];
|
|
2093
|
+
try {
|
|
2094
|
+
let { loadApiConfig: t } = await import("./dev-DeEJj5T4.js");
|
|
2095
|
+
n = ((await t(e))?.plugins ?? []).map((e) => {
|
|
2096
|
+
let t = (e.extendSchema?.tables ?? []).map((e) => {
|
|
2097
|
+
let t = e;
|
|
2098
|
+
return {
|
|
2099
|
+
tableName: typeof t.tableName == "string" ? t.tableName : "",
|
|
2100
|
+
renamedFrom: typeof t.renamedFrom == "string" ? t.renamedFrom : void 0
|
|
2101
|
+
};
|
|
2102
|
+
}).filter((e) => e.tableName !== ""), n = (e.rpcClientDescriptors ?? []).map((e) => {
|
|
2103
|
+
let t = e, n = t.descriptor?.name ?? t.name;
|
|
2104
|
+
return typeof n == "string" ? n : "";
|
|
2105
|
+
}).filter((e) => e !== "");
|
|
2106
|
+
return {
|
|
2107
|
+
plugin: e.name,
|
|
2108
|
+
tables: t,
|
|
2109
|
+
rpcTags: n,
|
|
2110
|
+
canDisableTables: Nn.has(e.name),
|
|
2111
|
+
canAlias: Pn.has(e.name)
|
|
2112
|
+
};
|
|
2113
|
+
});
|
|
2114
|
+
} catch {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (n.length === 0) return;
|
|
2118
|
+
let r = [
|
|
2119
|
+
...t.queries,
|
|
2120
|
+
...t.mutations,
|
|
2121
|
+
...t.actions,
|
|
2122
|
+
...t.streams ?? []
|
|
2123
|
+
].map((e) => String(e.descriptor.name)), i = jt(n, {
|
|
2124
|
+
tables: t.tables.map((e) => e.tableName),
|
|
2125
|
+
rpcTags: r
|
|
2126
|
+
});
|
|
2127
|
+
console.log();
|
|
2128
|
+
for (let e of Mt(n, i)) console.log(e);
|
|
2129
|
+
}, Nn = /* @__PURE__ */ new Set(["@voltro/plugin-rbac"]), Pn = /* @__PURE__ */ new Set(["@voltro/plugin-ai-flows"]), Fn = async (e, t) => {
|
|
2130
|
+
let { discovered: n } = await Q(e), r = /* @__PURE__ */ new Map(), i = (e) => {
|
|
2131
|
+
if (!r.has(e)) try {
|
|
2132
|
+
r.set(e, E(e, "utf8"));
|
|
2133
|
+
} catch {
|
|
2134
|
+
r.set(e, "");
|
|
2135
|
+
}
|
|
2136
|
+
return r.get(e);
|
|
2137
|
+
}, a = jn(e, i), o = /* @__PURE__ */ new Set();
|
|
2138
|
+
for (let e of n.tables) for (let t of Object.values(e.fields ?? {})) try {
|
|
2139
|
+
let e = t.references?.();
|
|
2140
|
+
e && o.add(e.tableName);
|
|
2141
|
+
} catch {}
|
|
2142
|
+
let s = [
|
|
2143
|
+
{
|
|
2144
|
+
list: n.queries,
|
|
2145
|
+
op: "read"
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
list: n.mutations,
|
|
2149
|
+
op: "update"
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
list: n.actions,
|
|
2153
|
+
op: "update"
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
list: n.streams,
|
|
2157
|
+
op: "read"
|
|
2158
|
+
}
|
|
2159
|
+
].flatMap(({ list: e, op: t }) => e.map((e) => {
|
|
2160
|
+
let n = e.descriptor, r = n.target, a = Array.isArray(r) ? r[0] : r, o = e.file.replace(/\.(tsx?)$/, ".server.$1");
|
|
2161
|
+
return {
|
|
2162
|
+
tag: String(n.name ?? e.file),
|
|
2163
|
+
path: o,
|
|
2164
|
+
src: i(o),
|
|
2165
|
+
hasDeclaredGuards: Array.isArray(n.guards) && n.guards.length > 0,
|
|
2166
|
+
op: a?.op === "delete" || a?.op === "insert" ? a.op : t,
|
|
2167
|
+
...a?.table === void 0 ? {} : { table: a.table }
|
|
2168
|
+
};
|
|
2169
|
+
})), c = x(e, $), l = T(c) ? kt(E(c, "utf8")) : void 0, u = Dt({
|
|
2170
|
+
executors: s,
|
|
2171
|
+
guardVocabulary: a,
|
|
2172
|
+
tenantScopedTables: n.tenantScopedTables,
|
|
2173
|
+
referencedTables: o,
|
|
2174
|
+
allowlist: l ?? /* @__PURE__ */ new Set()
|
|
2175
|
+
}), d = u.counts;
|
|
2176
|
+
console.log(`\nauthz scan · ${s.length} executor(s)`);
|
|
2177
|
+
let f = [...a].filter((e) => !bt.has(e)).sort();
|
|
2178
|
+
if (console.log(f.length === 0 ? " guard vocabulary: framework names only — no exported require*/assert* found in this app" : ` guard vocabulary: ${f.length} from your source (${f.slice(0, 6).join(", ")}${f.length > 6 ? ", …" : ""})`), console.log(` ✗ no access check ${String(d.unchecked).padStart(4)}`), console.log(` ⚠ inline ownership check, no named guard${String(d.inlineOwnership).padStart(4)} (informational)`), console.log(` ✓ guards: on the descriptor ${String(d.declaredGuard).padStart(4)}`), console.log(` ✓ calls a guard from the vocabulary ${String(d.vocabularyGuard).padStart(4)}`), console.log(` – accepted as recorded debt ${String(d.allowlisted).padStart(4)} (${$})`), t) return ne(c, Ot(u.unchecked.map((e) => e.tag)), "utf8"), console.log(`\nwrote ${u.unchecked.length} tag(s) to ${$}. Doctor now fails on ADDITIONS only.`), !1;
|
|
2179
|
+
if (u.unchecked.length === 0) return !1;
|
|
2180
|
+
console.log("");
|
|
2181
|
+
for (let e of u.unchecked.slice(0, 20)) console.log(` ✗ ${e.tag} — ${e.why}\n ${e.path}`);
|
|
2182
|
+
return u.unchecked.length > 20 && console.log(` … and ${u.unchecked.length - 20} more`), l === void 0 ? (console.log(`\nNo ${$} yet, so this run is advisory. To start the ratchet:`), console.log("\n voltro doctor --write-authz-allowlist\n"), console.log("That records today's unchecked handlers as DEBT and fails every run after on"), console.log("anything NEW. Fix a handler and its line stops mattering — the scan reports it as"), console.log("guarded whether or not the line is still there."), !1) : (console.log(`\n${u.unchecked.length} executor(s) with no access check are not in ${$}.`), console.log("Add a guard, or add the tag to that file if it is genuinely open."), console.log("\nBefore hand-rolling another check: `guards: [{ action, resourceType, resource }]`"), console.log("answers \"may this subject act on THIS row\" declaratively, and an app whose"), console.log("relationships live in its own tables registers its own tuple source rather than"), console.log("copying data into a framework table. See the authorization docs."), !0);
|
|
2183
|
+
}, In = async (e) => {
|
|
2184
|
+
if (e.includes("--help") || e.includes("-h")) return console.log(yn), 0;
|
|
2185
|
+
let t = e.includes("--strict"), n = C(e.find((e) => !e.startsWith("-")) ?? process.cwd());
|
|
2186
|
+
if (ie(e)) return re(), M({
|
|
2187
|
+
...nn(n),
|
|
2188
|
+
spaCandidates: await dn(n),
|
|
2189
|
+
pageConvention: Y(n),
|
|
2190
|
+
fileTaxonomy: U(n).findings,
|
|
2191
|
+
junctionFks: (await Cn(n))?.issues ?? [],
|
|
2192
|
+
eventDelivery: await Tn(n),
|
|
2193
|
+
duplicateInstances: _t(n),
|
|
2194
|
+
serverOnly: (await On(n)).status
|
|
2195
|
+
}), 0;
|
|
2196
|
+
let r = It(n, t);
|
|
2197
|
+
on(n), an(n), vn(n), Be(n), await pn(n), await An(n), await xn(n), await wn(n), await En(n), await Mn(n);
|
|
2198
|
+
let i = await kn(n), a = await Fn(n, e.includes("--write-authz-allowlist")), o = vt(_t(n));
|
|
2199
|
+
return o !== void 0 && console.log(`\n${o}`), i || a ? Math.max(r, 1) : r;
|
|
2200
|
+
}, Ln = (e) => e === void 0 ? [] : typeof e == "string" ? [e] : e, Rn = /* @__PURE__ */ new Set([
|
|
2201
|
+
"insert",
|
|
2202
|
+
"update",
|
|
2203
|
+
"delete",
|
|
2204
|
+
"upsert"
|
|
2205
|
+
]), zn = (e, t) => {
|
|
2206
|
+
let n = [], r = new Set(t.exercised), i = /* @__PURE__ */ new Map();
|
|
2207
|
+
for (let e of t.edges) {
|
|
2208
|
+
let t = i.get(e.tag);
|
|
2209
|
+
t === void 0 ? i.set(e.tag, [e]) : t.push(e);
|
|
2210
|
+
}
|
|
2211
|
+
let a = [], o = [];
|
|
2212
|
+
for (let t of e) {
|
|
2213
|
+
if (!r.has(t.tag)) {
|
|
2214
|
+
a.push(t.tag);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
let e = `${t.kind}(${t.tag})`, s = t.file, c = i.get(t.tag) ?? [], l = new Set(Ln(t.source)), u = new Map((t.targets ?? []).map((e) => [e.table, e.op]));
|
|
2218
|
+
for (let t of c) if (Rn.has(t.op)) {
|
|
2219
|
+
let r = u.get(t.table);
|
|
2220
|
+
r === void 0 ? n.push({
|
|
2221
|
+
rule: "observed/undeclared-write",
|
|
2222
|
+
node: e,
|
|
2223
|
+
...s === void 0 ? {} : { file: s },
|
|
2224
|
+
breaks: [`writes '${t.table}' (${t.op}) but declares no target for it`, "subscriptions on that table are not invalidated by this mutation"],
|
|
2225
|
+
fix: `add { table: '${t.table}', op: '${t.op}' } to this procedure's targets`,
|
|
2226
|
+
severity: "warn"
|
|
2227
|
+
}) : r !== t.op && n.push({
|
|
2228
|
+
rule: "observed/wrong-op",
|
|
2229
|
+
node: e,
|
|
2230
|
+
...s === void 0 ? {} : { file: s },
|
|
2231
|
+
breaks: [`declares op '${r}' on '${t.table}' but performed '${t.op}'`, "the synthesized optimistic patch has the wrong shape"],
|
|
2232
|
+
fix: `change the target op for '${t.table}' to '${t.op}'`,
|
|
2233
|
+
severity: "warn"
|
|
2234
|
+
});
|
|
2235
|
+
} else !l.has(t.table) && !u.has(t.table) && n.push({
|
|
2236
|
+
rule: "observed/undeclared-read",
|
|
2237
|
+
node: e,
|
|
2238
|
+
...s === void 0 ? {} : { file: s },
|
|
2239
|
+
breaks: [`reads '${t.table}' but declares no source for it`],
|
|
2240
|
+
fix: `add '${t.table}' to this procedure's source`,
|
|
2241
|
+
severity: "warn"
|
|
2242
|
+
});
|
|
2243
|
+
if (c.length === 0) {
|
|
2244
|
+
o.push(t.tag);
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
let d = new Set(c.map((e) => e.table));
|
|
2248
|
+
for (let t of l) d.has(t) || n.push({
|
|
2249
|
+
rule: "observed/declared-but-unobserved",
|
|
2250
|
+
node: e,
|
|
2251
|
+
...s === void 0 ? {} : { file: s },
|
|
2252
|
+
breaks: [`declares source '${t}' but never read it while running`],
|
|
2253
|
+
fix: `remove '${t}' from source, or check whether the handler still reads it`,
|
|
2254
|
+
severity: "warn"
|
|
2255
|
+
});
|
|
2256
|
+
for (let [t] of u) d.has(t) || n.push({
|
|
2257
|
+
rule: "observed/declared-but-unobserved",
|
|
2258
|
+
node: e,
|
|
2259
|
+
...s === void 0 ? {} : { file: s },
|
|
2260
|
+
breaks: [`declares target '${t}' but never wrote it while running`],
|
|
2261
|
+
fix: `remove the '${t}' target, or check whether the handler still writes it`,
|
|
2262
|
+
severity: "warn"
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
diagnostics: n,
|
|
2267
|
+
unexercised: a.sort(),
|
|
2268
|
+
unobservedReads: o.sort(),
|
|
2269
|
+
coverage: {
|
|
2270
|
+
exercised: e.length - a.length - o.length,
|
|
2271
|
+
total: e.length
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
}, Bn = (e, t = [], n = 0) => {
|
|
2275
|
+
if (n > 12) return t;
|
|
2276
|
+
let r;
|
|
2277
|
+
try {
|
|
2278
|
+
r = D(e);
|
|
2279
|
+
} catch {
|
|
2280
|
+
return t;
|
|
2281
|
+
}
|
|
2282
|
+
for (let i of r) {
|
|
2283
|
+
if ([
|
|
2284
|
+
"node_modules",
|
|
2285
|
+
"dist",
|
|
2286
|
+
"build",
|
|
2287
|
+
".framework",
|
|
2288
|
+
".turbo",
|
|
2289
|
+
".git",
|
|
2290
|
+
"coverage"
|
|
2291
|
+
].includes(i)) continue;
|
|
2292
|
+
let r = `${e}/${i}`, a;
|
|
2293
|
+
try {
|
|
2294
|
+
a = O(r);
|
|
2295
|
+
} catch {
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
a.isDirectory() ? Bn(r, t, n + 1) : a.isFile() && o.test(i) && t.push(r);
|
|
2299
|
+
}
|
|
2300
|
+
return t;
|
|
2301
|
+
}, Vn = /* @__PURE__ */ new Set(["actors", "tenants"]), Hn = async (e) => {
|
|
2302
|
+
try {
|
|
2303
|
+
let { readFile: t } = await import("node:fs/promises"), { join: n } = await import("node:path"), r = await t(n(e, "app.graph.observed.generated.json"), "utf8"), i = JSON.parse(r);
|
|
2304
|
+
return i?.version !== 1 || !Array.isArray(i.edges) || !Array.isArray(i.exercised) ? null : i;
|
|
2305
|
+
} catch {
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
}, Un = (e) => {
|
|
2309
|
+
let { exercised: t, total: n } = e.coverage, r = `\nobserved: ${t}/${n} procedures exercised (${n === 0 ? 0 : Math.round(t / n * 100)}%)`;
|
|
2310
|
+
if (e.diagnostics.length === 0) return `${r}\n no declared/observed mismatches${e.unexercised.length > 0 ? `\n ${e.unexercised.length} unexercised — no recording, so nothing is claimed about them` : ""}`;
|
|
2311
|
+
let i = e.diagnostics.map((e) => ` ${e.node}${e.file === void 0 ? "" : ` (${e.file})`}\n` + e.breaks.map((e) => ` ${e}`).join("\n") + `\n fix: ${e.fix}`), a = e.unexercised.length > 0 ? `\n ${e.unexercised.length} unexercised — no recording, so nothing is claimed about them` : "";
|
|
2312
|
+
return `${r}\n${i.join("\n")}${a}`;
|
|
2313
|
+
}, Wn = (e, t) => ({
|
|
2314
|
+
tables: (t.tables ?? []).map((e) => e.name),
|
|
2315
|
+
frameworkOwnedTables: (t.tables ?? []).filter((e) => e.framework === !0 || e.name.startsWith("_voltro_") || Vn.has(e.name)).map((e) => e.name),
|
|
2316
|
+
scopes: e.scopes ?? [],
|
|
2317
|
+
procedures: (e.procedures ?? []).map((e) => {
|
|
2318
|
+
let t = (e.guards ?? []).flatMap((e) => e.kind === "scope" ? e.scope ?? [] : []);
|
|
2319
|
+
return {
|
|
2320
|
+
tag: e.tag,
|
|
2321
|
+
kind: e.kind,
|
|
2322
|
+
...e.source === void 0 ? {} : { source: e.source },
|
|
2323
|
+
...e.targets === void 0 ? {} : { targets: e.targets },
|
|
2324
|
+
...t.length > 0 ? { requiresScopes: [...new Set(t)] } : {},
|
|
2325
|
+
...e.guards === void 0 ? {} : { hasGuards: e.guards.length > 0 }
|
|
2326
|
+
};
|
|
2327
|
+
})
|
|
2328
|
+
}), Gn = (e) => e.length === 0 ? "✓ voltro check: no issues" : e.map((e) => {
|
|
2329
|
+
let t = e.severity === "error" ? "✗ error" : "⚠ warn", n = e.file ? ` (${e.file})` : "";
|
|
2330
|
+
return `${t} ${e.rule} ${e.node}${n}\n ${e.breaks.join("; ")}\n fix: ${e.fix}`;
|
|
2331
|
+
}).join("\n"), Kn = async (e) => {
|
|
2332
|
+
let { walk: t, loadDiscovered: n, loadApiConfig: r } = await import("./dev-DeEJj5T4.js"), { buildRpcEntry: i } = await import("./manifestBuild-Ck_90gZy.js"), { buildCapabilityManifest: a } = await import("./inspect-DHeaMZak.js"), { relative: o } = await import("node:path"), s = await n(await t(e)), c = (t, n, r) => i(t, n, o(e, r)), l = [
|
|
2333
|
+
...s.queries.map((e) => c(e.descriptor, "query", e.file)),
|
|
2334
|
+
...s.mutations.map((e) => c(e.descriptor, "mutation", e.file)),
|
|
2335
|
+
...s.actions.map((e) => c(e.descriptor, "action", e.file)),
|
|
2336
|
+
...s.streams.map((e) => c(e.descriptor, "stream", e.file))
|
|
2337
|
+
], { assembleFrameworkTables: u } = await import("./frameworkTableAssembly-BTNirAAk.js"), d = await u({ root: e }).catch(() => []), f = new Set(s.tables.map((e) => e.tableName)), p = [...[...f].map((e) => ({ name: e })), ...d.map((e) => e.tableName).filter((e) => !f.has(e)).map((e) => ({
|
|
2338
|
+
name: e,
|
|
2339
|
+
framework: !0
|
|
2340
|
+
}))], m = await r(e).then((e) => e?.plugins ?? []).catch(() => []);
|
|
2341
|
+
return {
|
|
2342
|
+
manifest: a(l, s.workflows.map((t) => ({
|
|
2343
|
+
tag: t.definition.name ?? "",
|
|
2344
|
+
file: o(e, t.file)
|
|
2345
|
+
})), [], m),
|
|
2346
|
+
tables: { tables: p }
|
|
2347
|
+
};
|
|
2348
|
+
}, qn = async (e) => {
|
|
2349
|
+
let t = ue(e.flatMap((e) => e === "--json" ? ["--format", "json"] : [e]));
|
|
2350
|
+
if ("error" in t) return console.error(`voltro check: ${t.error}`), 1;
|
|
2351
|
+
let { process: n, target: r, rest: i } = de(t.rest), o;
|
|
2352
|
+
for (let e = 0; e < i.length; e++) if (i[e] === "--diff") {
|
|
2353
|
+
let [t, n] = (i[++e] ?? "").split(":");
|
|
2354
|
+
if (t === "removeTable" && n) o = {
|
|
2355
|
+
kind: "removeTable",
|
|
2356
|
+
name: n
|
|
2357
|
+
};
|
|
2358
|
+
else if (t === "removeProcedure" && n) o = {
|
|
2359
|
+
kind: "removeProcedure",
|
|
2360
|
+
tag: n
|
|
2361
|
+
};
|
|
2362
|
+
else if (t === "removeScope" && n) o = {
|
|
2363
|
+
kind: "removeScope",
|
|
2364
|
+
name: n
|
|
2365
|
+
};
|
|
2366
|
+
else return console.error("voltro check --diff: expected removeTable:<name> | removeProcedure:<tag> | removeScope:<name>"), 1;
|
|
2367
|
+
}
|
|
2368
|
+
let s = i.find((e) => !e.startsWith("-")) ?? process.cwd(), c = i.includes("--offline"), l = c ? [] : await le(n, r), u, d;
|
|
2369
|
+
if (l.length === 0) {
|
|
2370
|
+
if (!c && r !== void 0) return console.error("voltro check: --url given but no api reachable there."), 1;
|
|
2371
|
+
try {
|
|
2372
|
+
let e = await Kn(C(s));
|
|
2373
|
+
u = e.manifest, d = e.tables;
|
|
2374
|
+
} catch (e) {
|
|
2375
|
+
return console.error(`voltro check: could not assemble the graph from source: ${e.message}`), console.error(" Start `voltro dev` to check against a running api instead."), 1;
|
|
2376
|
+
}
|
|
2377
|
+
} else {
|
|
2378
|
+
let e = l[0], [t, n] = await Promise.all([N(e, "/_voltro/inspect/manifest"), N(e, "/_voltro/inspect/data/tables")]);
|
|
2379
|
+
if (t.kind === "err") return console.error(`voltro check: manifest fetch failed: ${t.error}`), 1;
|
|
2380
|
+
u = t.data, d = n.kind === "ok" ? n.data : { tables: [] };
|
|
2381
|
+
}
|
|
2382
|
+
let f = Wn(u, d), p = he(f, o), m = [...U(s).findings.map((e) => ({
|
|
2383
|
+
rule: e.rule,
|
|
2384
|
+
node: e.file,
|
|
2385
|
+
file: e.file,
|
|
2386
|
+
breaks: [e.detail],
|
|
2387
|
+
fix: e.fix,
|
|
2388
|
+
severity: e.severity
|
|
2389
|
+
})), ...Y(s).map((e) => ({
|
|
2390
|
+
rule: `page/${e.kind}`,
|
|
2391
|
+
node: e.file,
|
|
2392
|
+
file: e.file,
|
|
2393
|
+
breaks: [e.kind === "suffixed-outside-pages" ? "a page.tsx outside src/pages/ never routes" : "no test beside it"],
|
|
2394
|
+
fix: e.kind === "suffixed-outside-pages" ? "move it under src/pages/ into the directory whose route it serves, or rename it — page.tsx is reserved" : "write the test with the file — the name is derivable",
|
|
2395
|
+
severity: e.kind === "suffixed-outside-pages" ? "error" : "warn"
|
|
2396
|
+
}))], h = (await a(s, Bn(s))).map((e) => ({
|
|
2397
|
+
rule: "client/not-browser-safe",
|
|
2398
|
+
node: e.file,
|
|
2399
|
+
file: e.file,
|
|
2400
|
+
breaks: [`reaches the server-only '${e.violation.specifier}' — chain: ${e.violation.chain.join(" → ")}`],
|
|
2401
|
+
fix: "move the server-only code behind a *.server.ts, or drop the .client suffix — an unmarked file makes no claim",
|
|
2402
|
+
severity: "error"
|
|
2403
|
+
})), g = [
|
|
2404
|
+
...p.diagnostics,
|
|
2405
|
+
...m,
|
|
2406
|
+
...h
|
|
2407
|
+
], _ = p.ok && !m.some((e) => e.severity === "error") && h.length === 0, v = await Hn(C(s)), y = v === null ? null : zn(f.procedures, v);
|
|
2408
|
+
return t.format === "json" ? M(y === null ? {
|
|
2409
|
+
...p,
|
|
2410
|
+
ok: _,
|
|
2411
|
+
diagnostics: g
|
|
2412
|
+
} : {
|
|
2413
|
+
...p,
|
|
2414
|
+
ok: _,
|
|
2415
|
+
diagnostics: g,
|
|
2416
|
+
observed: y
|
|
2417
|
+
}) : (console.log(Gn(g)), y !== null && console.log(Un(y)), console.log(_ ? "\nvoltro check: OK" : "\nvoltro check: FAILED (errors above)")), +!_;
|
|
2418
|
+
};
|
|
2419
|
+
//#endregion
|
|
2420
|
+
export { re as _, Le as a, I as c, N as d, ue as f, le as g, ce as h, It as i, ve as l, se as m, qn as n, be as o, de as p, In as r, xe as s, Kn as t, he as u, M as v, ie as y };
|