zitejs 0.9.140 → 0.9.142

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.
Files changed (59) hide show
  1. package/dist/cjs/integrations/rules.d.ts +2 -0
  2. package/dist/cjs/integrations/rules.js +4 -0
  3. package/dist/cjs/start/describeBuilt.d.ts +3 -0
  4. package/dist/cjs/start/describeBuilt.js +106 -0
  5. package/dist/cjs/start/entries.d.ts +20 -0
  6. package/dist/cjs/start/entries.js +104 -0
  7. package/dist/cjs/start/problems.d.ts +19 -0
  8. package/dist/cjs/start/problems.js +11 -0
  9. package/dist/esm/integrations/rules.d.ts +2 -0
  10. package/dist/esm/integrations/rules.js +4 -0
  11. package/dist/esm/start/describe.d.ts +64 -0
  12. package/dist/esm/start/describe.js +176 -0
  13. package/dist/esm/start/describeBuilt.d.ts +3 -0
  14. package/dist/esm/start/describeBuilt.js +66 -0
  15. package/dist/esm/start/descriptions.d.ts +4 -0
  16. package/dist/esm/start/descriptions.js +47 -0
  17. package/dist/esm/start/entries.d.ts +20 -0
  18. package/dist/esm/start/entries.js +93 -0
  19. package/dist/esm/start/host.d.ts +3 -0
  20. package/dist/esm/start/host.js +4 -0
  21. package/dist/esm/start/index.d.ts +79 -0
  22. package/dist/esm/start/index.js +157 -0
  23. package/dist/esm/start/instance.d.ts +12 -0
  24. package/dist/esm/start/instance.js +73 -0
  25. package/dist/esm/start/problems.d.ts +19 -0
  26. package/dist/esm/start/problems.js +8 -0
  27. package/dist/esm/start/registration.d.ts +13 -0
  28. package/dist/esm/start/registration.js +21 -0
  29. package/dist/esm/start/router.d.ts +17 -0
  30. package/dist/esm/start/router.js +37 -0
  31. package/dist/esm/start/runLog.d.ts +5 -0
  32. package/dist/esm/start/runLog.js +18 -0
  33. package/dist/esm/start/scope.d.ts +15 -0
  34. package/dist/esm/start/scope.js +58 -0
  35. package/dist/esm/start/server.d.ts +51 -0
  36. package/dist/esm/start/server.js +215 -0
  37. package/dist/esm/start/types.d.ts +94 -0
  38. package/dist/esm/start/types.js +1 -0
  39. package/dist/esm/start/vite.d.ts +9 -0
  40. package/dist/esm/start/vite.js +144 -0
  41. package/dist/esm/start/worker.d.ts +11 -0
  42. package/dist/esm/start/worker.js +20 -0
  43. package/dist/esm/start/workflow.d.ts +50 -0
  44. package/dist/esm/start/workflow.js +13 -0
  45. package/dist/esm/start/workflow.test-d.d.ts +1 -0
  46. package/dist/esm/start/workflow.test-d.js +39 -0
  47. package/dist/esm/start/workflowModules.d.ts +1 -0
  48. package/dist/esm/start/workflowModules.js +3 -0
  49. package/dist/esm/start/workflowRun.d.ts +80 -0
  50. package/dist/esm/start/workflowRun.js +107 -0
  51. package/package.json +1 -1
  52. package/dist/cjs/meta/index.d.ts +0 -14
  53. package/dist/cjs/meta/index.js +0 -12
  54. package/dist/cjs/notifications/index.d.ts +0 -25
  55. package/dist/cjs/notifications/index.js +0 -12
  56. package/dist/esm/meta/index.d.ts +0 -14
  57. package/dist/esm/meta/index.js +0 -8
  58. package/dist/esm/notifications/index.d.ts +0 -25
  59. package/dist/esm/notifications/index.js +0 -8
@@ -38,12 +38,14 @@ declare const FIELDS_BY_TYPE: {
38
38
  jira: readonly string[];
39
39
  typeform: readonly string[];
40
40
  apollo: readonly string[];
41
+ clay: readonly string[];
41
42
  attio: readonly string[];
42
43
  granola: readonly string[];
43
44
  clickup: readonly string[];
44
45
  dropbox: readonly string[];
45
46
  amazonS3: readonly string[];
46
47
  snowflake: readonly string[];
48
+ databricks: readonly string[];
47
49
  smartsuite: readonly string[];
48
50
  };
49
51
  /** A service type `integrations.get` serves: one used through its npm package with a token. */
@@ -47,6 +47,7 @@ const FIELDS_BY_TYPE = {
47
47
  jira: extras("cloudId", "siteUrl"),
48
48
  typeform: extras(),
49
49
  apollo: extras(),
50
+ clay: extras(),
50
51
  attio: extras(),
51
52
  granola: extras(),
52
53
  clickup: extras(),
@@ -57,6 +58,9 @@ const FIELDS_BY_TYPE = {
57
58
  // The SQL API lives on each account's own host, and the token is pinned to
58
59
  // one role; the warehouse is optional and falls back to the user's default.
59
60
  snowflake: extras("accountUrl", "role", "warehouse"),
61
+ // A statement is addressed by workspace host and SQL warehouse, and the token
62
+ // derives neither.
63
+ databricks: extras("host", "warehouseId"),
60
64
  // Every request names the workspace in an Account-Id header beside the key.
61
65
  smartsuite: extras("workspaceId"),
62
66
  };
@@ -0,0 +1,3 @@
1
+ /** Describes a built Start app from its `dist/server`: a Workers build in workerd, a Node build in-process. */
2
+ export declare function describeBuiltApp(appDir: string): Promise<unknown>;
3
+ export declare function runDescribe(): Promise<void>;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.describeBuiltApp = describeBuiltApp;
40
+ exports.runDescribe = runDescribe;
41
+ const node_child_process_1 = require("node:child_process");
42
+ const node_fs_1 = require("node:fs");
43
+ const node_net_1 = require("node:net");
44
+ const node_path_1 = __importDefault(require("node:path"));
45
+ const node_url_1 = require("node:url");
46
+ // Only the host can set x-zite-* in production: the front strips them from public traffic.
47
+ const DESCRIBE_HEADERS = { "x-zite-invoke": "describe" };
48
+ // Not "localhost": it can resolve to ::1 for vite's listen and to 127.0.0.1 for fetch.
49
+ const HOST = "127.0.0.1";
50
+ const freePort = () => new Promise((resolve, reject) => {
51
+ const server = (0, node_net_1.createServer)();
52
+ server.once("error", reject);
53
+ server.listen(0, HOST, () => {
54
+ const { port } = server.address();
55
+ server.close(() => resolve(port));
56
+ });
57
+ });
58
+ // A Workers build runs where it will run: workerd, through the app's own
59
+ // `vite preview`, so it uses the same Cloudflare plugin and config as the build.
60
+ async function describeInWorkerd(appDir) {
61
+ const port = await freePort();
62
+ // Its own process group, so stopping it also stops vite and workerd under npx.
63
+ const preview = (0, node_child_process_1.spawn)("npx", ["--no-install", "vite", "preview", "--host", HOST, "--port", String(port), "--strictPort"], {
64
+ cwd: appDir,
65
+ stdio: ["ignore", "ignore", "pipe"],
66
+ detached: true,
67
+ });
68
+ let stderr = "";
69
+ preview.stderr.on("data", (chunk) => (stderr += chunk));
70
+ try {
71
+ const deadline = Date.now() + 60_000;
72
+ while (true) {
73
+ try {
74
+ const response = await fetch(`http://${HOST}:${port}/`, { headers: DESCRIBE_HEADERS });
75
+ // Right after start, preview can answer before the Worker does; only the
76
+ // app's own describe answer is JSON.
77
+ if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
78
+ return await response.json();
79
+ }
80
+ }
81
+ catch { }
82
+ if (preview.exitCode !== null || Date.now() > deadline) {
83
+ throw new Error(`vite preview didn't serve the app: ${stderr.trim().slice(-500)}`);
84
+ }
85
+ await new Promise((resolve) => setTimeout(resolve, 250));
86
+ }
87
+ }
88
+ finally {
89
+ try {
90
+ process.kill(-preview.pid, "SIGTERM");
91
+ }
92
+ catch { }
93
+ }
94
+ }
95
+ /** Describes a built Start app from its `dist/server`: a Workers build in workerd, a Node build in-process. */
96
+ async function describeBuiltApp(appDir) {
97
+ const serverDir = node_path_1.default.join(appDir, "dist/server");
98
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(serverDir, "wrangler.json")))
99
+ return describeInWorkerd(appDir);
100
+ const { default: server } = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(node_path_1.default.join(serverDir, "server.js")).href}`).then(s => __importStar(require(s)));
101
+ return (await server.fetch(new Request("http://describe.local/", { headers: DESCRIBE_HEADERS }))).json();
102
+ }
103
+ async function runDescribe() {
104
+ const appDir = node_path_1.default.resolve(process.argv[3] ?? ".");
105
+ process.stdout.write(`${JSON.stringify(await describeBuiltApp(appDir), null, 2)}\n`);
106
+ }
@@ -0,0 +1,20 @@
1
+ import type { SourceDir } from "../sourceRoots.js";
2
+ export declare const START_SERVER_ENTRY = ".zite/server.ts";
3
+ export declare const START_INSTANCE_ENTRY = ".zite/start.ts";
4
+ export type StartTarget = "cloudflare" | "node";
5
+ /**
6
+ * The server entry every Start app builds with. Platform-owned: regenerated,
7
+ * never edited. On Workers it's an entrypoint the host can also call over RPC.
8
+ */
9
+ export declare const renderStartServerEntry: (target?: StartTarget) => string;
10
+ /** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
11
+ export declare const renderStartInstance: (hasAppStart: boolean) => string;
12
+ export declare const hasAppStart: (appDir: string) => boolean;
13
+ /** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
14
+ export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
15
+ export declare const isStartAppDir: (appPath: string) => boolean;
16
+ export declare function regenerateStartEntries(app: SourceDir): void;
17
+ /** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
18
+ export declare function workflowFiles(appDir: string): string[];
19
+ /** The registry zite() builds in place of zitejs's empty one. */
20
+ export declare const renderWorkflowModules: (appDir: string, files: string[]) => string;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderWorkflowModules = exports.isStartAppDir = exports.writeIfChanged = exports.hasAppStart = exports.renderStartInstance = exports.renderStartServerEntry = exports.START_INSTANCE_ENTRY = exports.START_SERVER_ENTRY = void 0;
4
+ exports.regenerateStartEntries = regenerateStartEntries;
5
+ exports.workflowFiles = workflowFiles;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ exports.START_SERVER_ENTRY = ".zite/server.ts";
9
+ exports.START_INSTANCE_ENTRY = ".zite/start.ts";
10
+ const APP_START_FILES = ["src/start.ts", "src/start.tsx"];
11
+ /**
12
+ * The server entry every Start app builds with. Platform-owned: regenerated,
13
+ * never edited. On Workers it's an entrypoint the host can also call over RPC.
14
+ */
15
+ const renderStartServerEntry = (target = "cloudflare") => target === "cloudflare"
16
+ ? `// Generated by zitejs for TanStack Start apps. The platform owns this file.
17
+ import handler from '@tanstack/react-start/server-entry';
18
+ import { createZiteWorker } from 'zitejs/start/worker';
19
+ import { getRouter } from '../src/router';
20
+
21
+ export default createZiteWorker({ handler, getRouter });
22
+ `
23
+ : `// Generated by zitejs for TanStack Start apps. The platform owns this file.
24
+ import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
25
+ import { createZiteFetch } from 'zitejs/start/server';
26
+ import { getRouter } from '../src/router';
27
+
28
+ export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter }) });
29
+ `;
30
+ exports.renderStartServerEntry = renderStartServerEntry;
31
+ /** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
32
+ const renderStartInstance = (hasAppStart) => hasAppStart
33
+ ? `// Generated by zitejs for TanStack Start apps. The platform owns this file.
34
+ import { createZiteStart } from 'zitejs/start/instance';
35
+ import { startInstance as app } from '../src/start';
36
+
37
+ export const startInstance = createZiteStart(app);
38
+ `
39
+ : `// Generated by zitejs for TanStack Start apps. The platform owns this file.
40
+ import { createZiteStart } from 'zitejs/start/instance';
41
+
42
+ export const startInstance = createZiteStart();
43
+ `;
44
+ exports.renderStartInstance = renderStartInstance;
45
+ const hasAppStart = (appDir) => APP_START_FILES.some((file) => (0, node_fs_1.existsSync)((0, node_path_1.join)(appDir, file)));
46
+ exports.hasAppStart = hasAppStart;
47
+ /** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
48
+ const writeIfChanged = (appDir, file, content) => {
49
+ const target = (0, node_path_1.join)(appDir, file);
50
+ if ((0, node_fs_1.existsSync)(target) && (0, node_fs_1.readFileSync)(target, "utf-8") === content)
51
+ return false;
52
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
53
+ (0, node_fs_1.writeFileSync)(target, content);
54
+ return true;
55
+ };
56
+ exports.writeIfChanged = writeIfChanged;
57
+ const isStartAppDir = (appPath) => {
58
+ try {
59
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(appPath, "zite.config.json"), "utf-8"))?.runtime === "start";
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ };
65
+ exports.isStartAppDir = isStartAppDir;
66
+ const isStartApp = (app) => (0, exports.isStartAppDir)(app.path);
67
+ function regenerateStartEntries(app) {
68
+ if (!isStartApp(app))
69
+ return;
70
+ const entries = [
71
+ [exports.START_SERVER_ENTRY, (0, exports.renderStartServerEntry)()],
72
+ [exports.START_INSTANCE_ENTRY, (0, exports.renderStartInstance)((0, exports.hasAppStart)(app.path))],
73
+ ];
74
+ for (const [file, content] of entries) {
75
+ if ((0, exports.writeIfChanged)(app.path, file, content))
76
+ console.log(`Wrote ${(0, node_path_1.join)(app.path, file)}`);
77
+ }
78
+ }
79
+ const SOURCE_FILE = /\.[mc]?[jt]sx?$/;
80
+ const NOT_APP_CODE = /(\.(test|spec)\.[^.]+|\.d\.ts|routeTree\.gen\.ts)$/;
81
+ /** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
82
+ function workflowFiles(appDir) {
83
+ const found = [];
84
+ const walk = (dir) => {
85
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
86
+ const full = (0, node_path_1.join)(dir, entry.name);
87
+ if (entry.isDirectory()) {
88
+ if (entry.name !== "node_modules")
89
+ walk(full);
90
+ }
91
+ else if (SOURCE_FILE.test(entry.name) && !NOT_APP_CODE.test(entry.name) && (0, node_fs_1.readFileSync)(full, "utf-8").includes("createWorkflow")) {
92
+ found.push((0, node_path_1.relative)(appDir, full).split("\\").join("/"));
93
+ }
94
+ }
95
+ };
96
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(appDir, "src")))
97
+ walk((0, node_path_1.join)(appDir, "src"));
98
+ return found.sort();
99
+ }
100
+ /** The registry zite() builds in place of zitejs's empty one. */
101
+ const renderWorkflowModules = (appDir, files) => `export const workflowModules = {\n${files
102
+ .map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify((0, node_path_1.join)(appDir, file))}),`)
103
+ .join("\n")}\n};\n`;
104
+ exports.renderWorkflowModules = renderWorkflowModules;
@@ -0,0 +1,19 @@
1
+ type WithProblems = {
2
+ problems: string[];
3
+ };
4
+ type Described = {
5
+ functions: Array<WithProblems & {
6
+ id: string;
7
+ file?: string;
8
+ }>;
9
+ routes: Array<WithProblems & {
10
+ path: string;
11
+ }>;
12
+ workflows?: Array<WithProblems & {
13
+ id: string;
14
+ file: string;
15
+ }>;
16
+ };
17
+ /** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
18
+ export declare function descriptionProblems(description: Described): string[];
19
+ export {};
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.descriptionProblems = descriptionProblems;
4
+ /** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
5
+ function descriptionProblems(description) {
6
+ return [
7
+ ...description.functions.flatMap((f) => f.problems.map((p) => `function ${f.id}${f.file ? ` (${f.file})` : ""}: ${p}`)),
8
+ ...description.routes.flatMap((r) => r.problems.map((p) => `route ${r.path}: ${p}`)),
9
+ ...(description.workflows ?? []).flatMap((w) => w.problems.map((p) => `workflow ${w.id} (${w.file}): ${p}`)),
10
+ ];
11
+ }
@@ -38,12 +38,14 @@ declare const FIELDS_BY_TYPE: {
38
38
  jira: readonly string[];
39
39
  typeform: readonly string[];
40
40
  apollo: readonly string[];
41
+ clay: readonly string[];
41
42
  attio: readonly string[];
42
43
  granola: readonly string[];
43
44
  clickup: readonly string[];
44
45
  dropbox: readonly string[];
45
46
  amazonS3: readonly string[];
46
47
  snowflake: readonly string[];
48
+ databricks: readonly string[];
47
49
  smartsuite: readonly string[];
48
50
  };
49
51
  /** A service type `integrations.get` serves: one used through its npm package with a token. */
@@ -44,6 +44,7 @@ const FIELDS_BY_TYPE = {
44
44
  jira: extras("cloudId", "siteUrl"),
45
45
  typeform: extras(),
46
46
  apollo: extras(),
47
+ clay: extras(),
47
48
  attio: extras(),
48
49
  granola: extras(),
49
50
  clickup: extras(),
@@ -54,6 +55,9 @@ const FIELDS_BY_TYPE = {
54
55
  // The SQL API lives on each account's own host, and the token is pinned to
55
56
  // one role; the warehouse is optional and falls back to the user's default.
56
57
  snowflake: extras("accountUrl", "role", "warehouse"),
58
+ // A statement is addressed by workspace host and SQL warehouse, and the token
59
+ // derives neither.
60
+ databricks: extras("host", "warehouseId"),
57
61
  // Every request names the workspace in an Account-Id header beside the key.
58
62
  smartsuite: extras("workspaceId"),
59
63
  };
@@ -0,0 +1,64 @@
1
+ import type { ZiteTrigger } from "../backend/index.js";
2
+ import type { LoadedWorkflow } from "./workflowRun.js";
3
+ import type { WorkflowConfig } from "./workflow.js";
4
+ export type Access = {
5
+ kind: "signedIn" | "anyone" | "webhook" | "none";
6
+ roles: string[];
7
+ paused?: true;
8
+ };
9
+ export type DescribedFunction = {
10
+ id: string;
11
+ name?: string;
12
+ file?: string;
13
+ method?: string;
14
+ /** From the factory's `description` option; display-only. */
15
+ description?: string;
16
+ access: Access;
17
+ input?: unknown;
18
+ problems: string[];
19
+ };
20
+ export type DescribedRoute = {
21
+ path: string;
22
+ methods: Record<string, Access>;
23
+ problems: string[];
24
+ };
25
+ export type DescribedWorkflow = {
26
+ id: string;
27
+ file: string;
28
+ description?: string;
29
+ schedule?: WorkflowConfig["schedule"];
30
+ /** As declared, without a provider trigger's `subscribe` and `unsubscribe`. */
31
+ trigger?: ZiteTrigger;
32
+ /** A provider trigger's: whether it can be taken down, and a hash of both functions' code, so publish registers it again when they change. */
33
+ registration?: {
34
+ unsubscribe: boolean;
35
+ hash: string;
36
+ };
37
+ problems: string[];
38
+ };
39
+ export type Description = {
40
+ functions: DescribedFunction[];
41
+ routes: DescribedRoute[];
42
+ workflows: DescribedWorkflow[];
43
+ };
44
+ export declare function describeFunction(id: string, request: Request): Promise<DescribedFunction>;
45
+ type RouteLike = {
46
+ fullPath: string;
47
+ parentRoute?: RouteLike;
48
+ options?: {
49
+ server?: {
50
+ middleware?: unknown[];
51
+ handlers?: unknown;
52
+ };
53
+ };
54
+ };
55
+ export declare function describeRoutes(routesById: Record<string, RouteLike>): DescribedRoute[];
56
+ export declare function describeWorkflows(workflows: LoadedWorkflow[]): DescribedWorkflow[];
57
+ export declare function describeApp(opts: {
58
+ fnIds: string[];
59
+ descriptions?: Record<string, string>;
60
+ getRouter: () => unknown;
61
+ request: Request;
62
+ workflows?: LoadedWorkflow[];
63
+ }): Promise<Description>;
64
+ export {};
@@ -0,0 +1,176 @@
1
+ // Describes a built app by loading it, not by reading its source: each server
2
+ // function's real middleware chain, each server route's, and input schemas.
3
+ import { createMiddleware, flattenMiddlewares } from "@tanstack/react-start";
4
+ import { createSsrRpc } from "@tanstack/react-start/ssr-rpc";
5
+ import { runWithStartContext } from "@tanstack/start-storage-context";
6
+ import { z } from "zod";
7
+ import { ziteMiddlewareKind } from "./index.js";
8
+ const accessOf = (chain, problems) => {
9
+ const kinds = chain.map(ziteMiddlewareKind).filter((k) => !!k);
10
+ const roles = kinds.flatMap((k) => (k.kind === "role" ? [k.role] : []));
11
+ const base = [...new Set(kinds.flatMap((k) => (k.kind === "signedIn" || k.kind === "anyone" || k.kind === "webhook" ? [k.kind] : [])))];
12
+ if (base.length === 0) {
13
+ problems.push("access-missing: no signedIn, anyone or webhook middleware in its chain");
14
+ return { kind: "none", roles };
15
+ }
16
+ if (base.length > 1)
17
+ problems.push(`access-conflict: ${base.join(" + ")}`);
18
+ const kind = base.includes("webhook") ? "webhook" : base.includes("signedIn") ? "signedIn" : "anyone";
19
+ const paused = kinds.some((k) => k.kind === "webhook" && k.paused === true);
20
+ return paused ? { kind, roles, paused } : { kind, roles };
21
+ };
22
+ const jsonSchemaOf = (validator) => {
23
+ if (validator && typeof validator === "object" && "_zod" in validator) {
24
+ try {
25
+ return z.toJSONSchema(validator);
26
+ }
27
+ catch {
28
+ return { unrepresentable: true };
29
+ }
30
+ }
31
+ return validator ? { opaque: true } : undefined;
32
+ };
33
+ // Describe walks functions one at a time, so one probe slot is enough.
34
+ let probe = null;
35
+ // The only global function middleware while describing: it records what it was
36
+ // handed and ends the call, so no app middleware or handler runs.
37
+ const describeProbe = createMiddleware({ type: "function" }).server(({ serverFnMeta, method }) => {
38
+ if (probe)
39
+ Object.assign(probe, { meta: serverFnMeta, method, reached: true });
40
+ return "zite:describe";
41
+ });
42
+ const SERVER_ONLY = "server-only: nothing in the browser calls it, and a server function that only other server functions call can't run. " +
43
+ "Move the shared logic into a *.server.ts helper and call that.";
44
+ export async function describeFunction(id, request) {
45
+ const problems = [];
46
+ const seen = [];
47
+ probe = { reached: false };
48
+ // Before running a chain, TanStack asks this set about every middleware in it.
49
+ // Recording the questions gives the exact chain it would have run.
50
+ const executedRequestMiddlewares = {
51
+ has: (middleware) => {
52
+ seen.push(middleware);
53
+ return false;
54
+ },
55
+ };
56
+ const startContext = {
57
+ request,
58
+ startOptions: { functionMiddleware: [describeProbe] },
59
+ executedRequestMiddlewares,
60
+ contextAfterGlobalMiddlewares: {},
61
+ handlerType: "serverFn",
62
+ getRouter: () => {
63
+ throw new Error("describe has no router");
64
+ },
65
+ };
66
+ try {
67
+ await runWithStartContext(startContext, () => createSsrRpc(id)({ data: undefined, context: {} }));
68
+ }
69
+ catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ // TanStack registers only functions something can call through it. One that
72
+ // only other server functions call isn't registered, and calling it fails.
73
+ if (message === `Server function info not found for ${id}`) {
74
+ probe = null;
75
+ return { id, access: { kind: "none", roles: [] }, problems: [SERVER_ONLY] };
76
+ }
77
+ problems.push(`probe-failed: ${message}`);
78
+ }
79
+ const state = probe;
80
+ probe = null;
81
+ // Fails closed: if TanStack stops asking, or stops running globals first, every
82
+ // function reports a problem and the build stops.
83
+ if (!state.reached || seen[0] !== describeProbe)
84
+ problems.push("probe-not-first: chain could not be read");
85
+ const chain = seen.filter((m) => m !== describeProbe);
86
+ if (!chain.some((m) => ziteMiddlewareKind(m)?.kind === "factory")) {
87
+ problems.push("raw-server-fn: define it with signedInFn or publicFn from zitejs/start");
88
+ }
89
+ // TanStack keeps the validator under `inputValidator` up to 1.168 and `validator` from 1.169 (Start 1.0).
90
+ const handlerStep = chain.at(-1);
91
+ return {
92
+ id,
93
+ name: state.meta?.name,
94
+ file: state.meta?.filename,
95
+ method: state.method,
96
+ access: accessOf(chain, problems),
97
+ input: jsonSchemaOf(handlerStep?.options?.validator ?? handlerStep?.options?.inputValidator),
98
+ problems,
99
+ };
100
+ }
101
+ export function describeRoutes(routesById) {
102
+ const out = [];
103
+ for (const route of Object.values(routesById)) {
104
+ const server = route.options?.server;
105
+ if (!server?.handlers)
106
+ continue;
107
+ const problems = [];
108
+ const ancestry = [];
109
+ for (let r = route; r; r = r.parentRoute)
110
+ ancestry.unshift(r);
111
+ const routeMiddleware = ancestry.flatMap((r) => r.options?.server?.middleware ?? []);
112
+ const handlers = (typeof server.handlers === "function"
113
+ ? server.handlers({ createHandlers: (d) => d })
114
+ : server.handlers);
115
+ const methods = {};
116
+ for (const [method, handler] of Object.entries(handlers)) {
117
+ const own = typeof handler === "function" ? [] : (handler.middleware ?? []);
118
+ const chain = flattenMiddlewares([...routeMiddleware, ...own]);
119
+ const methodProblems = [];
120
+ methods[method] = accessOf(chain, methodProblems);
121
+ problems.push(...methodProblems.map((p) => `${method}: ${p}`));
122
+ }
123
+ out.push({ path: route.fullPath, methods, problems });
124
+ }
125
+ return out;
126
+ }
127
+ // cyrb53: enough to tell when the code changed, which is all publish asks.
128
+ const sourceHash = (text) => {
129
+ let h1 = 0xdeadbeef;
130
+ let h2 = 0x41c6ce57;
131
+ for (let i = 0; i < text.length; i++) {
132
+ const ch = text.charCodeAt(i);
133
+ h1 = Math.imul(h1 ^ ch, 2654435761);
134
+ h2 = Math.imul(h2 ^ ch, 1597334677);
135
+ }
136
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
137
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
138
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
139
+ };
140
+ // A workflow's id is its export name: what the host invokes it by and the editor shows.
141
+ export function describeWorkflows(workflows) {
142
+ return workflows.map(({ id, file, workflow }) => {
143
+ const problems = [];
144
+ if (id === "default")
145
+ problems.push("workflow-default-export: export it by name, which becomes its id");
146
+ const elsewhere = workflows.find((other) => other.id === id && other.file !== file);
147
+ if (elsewhere)
148
+ problems.push(`duplicate-workflow: ${id} is also exported from ${elsewhere.file}`);
149
+ const { description, schedule, trigger } = workflow.config;
150
+ if (schedule)
151
+ return { id, file, description, schedule, problems };
152
+ if (!trigger || trigger.every !== undefined || trigger.events !== undefined) {
153
+ return { id, file, description, trigger, problems };
154
+ }
155
+ const { subscribe, unsubscribe, ...declared } = trigger;
156
+ if (typeof subscribe !== "function") {
157
+ problems.push("workflow-trigger-unregistered: a provider trigger needs `subscribe` to register it with the provider");
158
+ return { id, file, description, trigger: declared, problems };
159
+ }
160
+ const registration = {
161
+ unsubscribe: typeof unsubscribe === "function",
162
+ hash: sourceHash(`${String(subscribe)}\n${typeof unsubscribe === "function" ? String(unsubscribe) : ""}`),
163
+ };
164
+ return { id, file, description, trigger: declared, registration, problems };
165
+ });
166
+ }
167
+ export async function describeApp(opts) {
168
+ const functions = [];
169
+ for (const id of opts.fnIds) {
170
+ const described = await describeFunction(id, opts.request);
171
+ const description = opts.descriptions?.[id];
172
+ functions.push(description ? { ...described, description } : described);
173
+ }
174
+ const router = (await opts.getRouter());
175
+ return { functions, routes: describeRoutes(router.routesById), workflows: describeWorkflows(opts.workflows ?? []) };
176
+ }
@@ -0,0 +1,3 @@
1
+ /** Describes a built Start app from its `dist/server`: a Workers build in workerd, a Node build in-process. */
2
+ export declare function describeBuiltApp(appDir: string): Promise<unknown>;
3
+ export declare function runDescribe(): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { createServer } from "node:net";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ // Only the host can set x-zite-* in production: the front strips them from public traffic.
7
+ const DESCRIBE_HEADERS = { "x-zite-invoke": "describe" };
8
+ // Not "localhost": it can resolve to ::1 for vite's listen and to 127.0.0.1 for fetch.
9
+ const HOST = "127.0.0.1";
10
+ const freePort = () => new Promise((resolve, reject) => {
11
+ const server = createServer();
12
+ server.once("error", reject);
13
+ server.listen(0, HOST, () => {
14
+ const { port } = server.address();
15
+ server.close(() => resolve(port));
16
+ });
17
+ });
18
+ // A Workers build runs where it will run: workerd, through the app's own
19
+ // `vite preview`, so it uses the same Cloudflare plugin and config as the build.
20
+ async function describeInWorkerd(appDir) {
21
+ const port = await freePort();
22
+ // Its own process group, so stopping it also stops vite and workerd under npx.
23
+ const preview = spawn("npx", ["--no-install", "vite", "preview", "--host", HOST, "--port", String(port), "--strictPort"], {
24
+ cwd: appDir,
25
+ stdio: ["ignore", "ignore", "pipe"],
26
+ detached: true,
27
+ });
28
+ let stderr = "";
29
+ preview.stderr.on("data", (chunk) => (stderr += chunk));
30
+ try {
31
+ const deadline = Date.now() + 60_000;
32
+ while (true) {
33
+ try {
34
+ const response = await fetch(`http://${HOST}:${port}/`, { headers: DESCRIBE_HEADERS });
35
+ // Right after start, preview can answer before the Worker does; only the
36
+ // app's own describe answer is JSON.
37
+ if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
38
+ return await response.json();
39
+ }
40
+ }
41
+ catch { }
42
+ if (preview.exitCode !== null || Date.now() > deadline) {
43
+ throw new Error(`vite preview didn't serve the app: ${stderr.trim().slice(-500)}`);
44
+ }
45
+ await new Promise((resolve) => setTimeout(resolve, 250));
46
+ }
47
+ }
48
+ finally {
49
+ try {
50
+ process.kill(-preview.pid, "SIGTERM");
51
+ }
52
+ catch { }
53
+ }
54
+ }
55
+ /** Describes a built Start app from its `dist/server`: a Workers build in workerd, a Node build in-process. */
56
+ export async function describeBuiltApp(appDir) {
57
+ const serverDir = path.join(appDir, "dist/server");
58
+ if (existsSync(path.join(serverDir, "wrangler.json")))
59
+ return describeInWorkerd(appDir);
60
+ const { default: server } = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
61
+ return (await server.fetch(new Request("http://describe.local/", { headers: DESCRIBE_HEADERS }))).json();
62
+ }
63
+ export async function runDescribe() {
64
+ const appDir = path.resolve(process.argv[3] ?? ".");
65
+ process.stdout.write(`${JSON.stringify(await describeBuiltApp(appDir), null, 2)}\n`);
66
+ }
@@ -0,0 +1,4 @@
1
+ /** `name -> description` for each top-level const built with a description option. */
2
+ export declare function factoryDescriptions(program: {
3
+ body: unknown[];
4
+ }): Map<string, string>;