zitejs 0.10.0-start.1 → 0.10.0-start.2

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.
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.runCheck = runCheck;
4
37
  const child_process_1 = require("child_process");
@@ -58,15 +91,60 @@ function bundleFailures(output) {
58
91
  }
59
92
  return failures;
60
93
  }
94
+ const indent = (output) => output.trim().split('\n').map(l => ` ${l}`).join('\n');
95
+ /**
96
+ * A Start app: `vite build` first, since it regenerates the route tree tsc
97
+ * reads, then tsc, then describe, which reports what the platform's build
98
+ * refuses, read the same way.
99
+ */
100
+ async function checkStartApp(appPath) {
101
+ process.stdout.write(' vite build ... ');
102
+ const vite = run('npx', ['vite', 'build'], appPath);
103
+ console.log(vite.ok ? '✓' : '✗');
104
+ if (!vite.ok) {
105
+ console.log(indent(vite.output));
106
+ return false;
107
+ }
108
+ process.stdout.write(' tsc --noEmit ... ');
109
+ // --pretty for `file:line:col - error` lines, the shape the platform re-roots.
110
+ const tsc = run('npx', ['tsc', '--noEmit', '--pretty', '-p', 'tsconfig.app.json'], appPath);
111
+ console.log(tsc.ok ? '✓' : '✗');
112
+ if (!tsc.ok)
113
+ console.log(indent(tsc.output));
114
+ process.stdout.write(' describe ... ');
115
+ try {
116
+ const { describeBuiltApp } = await Promise.resolve().then(() => __importStar(require('../start/describeBuilt.js')));
117
+ const { descriptionProblems } = await Promise.resolve().then(() => __importStar(require('../start/problems.js')));
118
+ const problems = descriptionProblems((await describeBuiltApp(appPath)));
119
+ console.log(problems.length ? '✗' : '✓');
120
+ if (problems.length)
121
+ console.log(problems.map(p => ` ${p}`).join('\n'));
122
+ return tsc.ok && problems.length === 0;
123
+ }
124
+ catch (err) {
125
+ console.log('✗');
126
+ console.log(` ${err instanceof Error ? err.message : String(err)}`);
127
+ return false;
128
+ }
129
+ }
61
130
  async function runCheck() {
62
- const appDirs = (0, sourceRoots_js_1.listSourceDirs)();
63
- if (appDirs.length === 0) {
131
+ const { isStartAppDir } = await Promise.resolve().then(() => __importStar(require('../start/entries.js')));
132
+ // Run inside a Start app, its own zitejs checks just that app: the workspace
133
+ // root's is a classic release that doesn't know Start.
134
+ const inStartApp = isStartAppDir('.');
135
+ const appDirs = inStartApp ? [] : (0, sourceRoots_js_1.listSourceDirs)();
136
+ if (!inStartApp && appDirs.length === 0) {
64
137
  console.error('No apps or automations found under apps/ or automations/.');
65
138
  process.exit(1);
66
139
  }
67
- let allPassed = true;
140
+ let allPassed = inStartApp ? await checkStartApp('.') : true;
68
141
  for (const { root, dir: app, path: appPath } of appDirs) {
69
142
  console.log(`\n── ${appPath} ──`);
143
+ if (isStartAppDir(appPath)) {
144
+ if (!(await checkStartApp(appPath)))
145
+ allPassed = false;
146
+ continue;
147
+ }
70
148
  // Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
71
149
  // solution-style config — `"files": []` plus a reference — so
72
150
  // `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
@@ -8,4 +8,9 @@ export declare const renderStartInstance: (hasAppStart: boolean) => string;
8
8
  export declare const hasAppStart: (appDir: string) => boolean;
9
9
  /** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
10
10
  export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
11
+ export declare const isStartAppDir: (appPath: string) => boolean;
11
12
  export declare function regenerateStartEntries(app: SourceDir): void;
13
+ /** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
14
+ export declare function workflowFiles(appDir: string): string[];
15
+ /** The registry zite() builds in place of zitejs's empty one. */
16
+ export declare const renderWorkflowModules: (appDir: string, files: string[]) => string;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.writeIfChanged = exports.hasAppStart = exports.renderStartInstance = exports.renderStartServerEntry = exports.START_INSTANCE_ENTRY = exports.START_SERVER_ENTRY = void 0;
3
+ exports.renderWorkflowModules = exports.isStartAppDir = exports.writeIfChanged = exports.hasAppStart = exports.renderStartInstance = exports.renderStartServerEntry = exports.START_INSTANCE_ENTRY = exports.START_SERVER_ENTRY = void 0;
4
4
  exports.regenerateStartEntries = regenerateStartEntries;
5
+ exports.workflowFiles = workflowFiles;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_path_1 = require("node:path");
7
8
  exports.START_SERVER_ENTRY = ".zite/server.ts";
@@ -42,14 +43,16 @@ const writeIfChanged = (appDir, file, content) => {
42
43
  return true;
43
44
  };
44
45
  exports.writeIfChanged = writeIfChanged;
45
- const isStartApp = (app) => {
46
+ const isStartAppDir = (appPath) => {
46
47
  try {
47
- return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(app.path, "zite.config.json"), "utf-8"))?.runtime === "start";
48
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(appPath, "zite.config.json"), "utf-8"))?.runtime === "start";
48
49
  }
49
50
  catch {
50
51
  return false;
51
52
  }
52
53
  };
54
+ exports.isStartAppDir = isStartAppDir;
55
+ const isStartApp = (app) => (0, exports.isStartAppDir)(app.path);
53
56
  function regenerateStartEntries(app) {
54
57
  if (!isStartApp(app))
55
58
  return;
@@ -62,3 +65,29 @@ function regenerateStartEntries(app) {
62
65
  console.log(`Wrote ${(0, node_path_1.join)(app.path, file)}`);
63
66
  }
64
67
  }
68
+ const SOURCE_FILE = /\.[mc]?[jt]sx?$/;
69
+ const NOT_APP_CODE = /(\.(test|spec)\.[^.]+|\.d\.ts|routeTree\.gen\.ts)$/;
70
+ /** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
71
+ function workflowFiles(appDir) {
72
+ const found = [];
73
+ const walk = (dir) => {
74
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
75
+ const full = (0, node_path_1.join)(dir, entry.name);
76
+ if (entry.isDirectory()) {
77
+ if (entry.name !== "node_modules")
78
+ walk(full);
79
+ }
80
+ else if (SOURCE_FILE.test(entry.name) && !NOT_APP_CODE.test(entry.name) && (0, node_fs_1.readFileSync)(full, "utf-8").includes("createWorkflow")) {
81
+ found.push((0, node_path_1.relative)(appDir, full).split("\\").join("/"));
82
+ }
83
+ }
84
+ };
85
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(appDir, "src")))
86
+ walk((0, node_path_1.join)(appDir, "src"));
87
+ return found.sort();
88
+ }
89
+ /** The registry zite() builds in place of zitejs's empty one. */
90
+ const renderWorkflowModules = (appDir, files) => `export const workflowModules = {\n${files
91
+ .map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify((0, node_path_1.join)(appDir, file))}),`)
92
+ .join("\n")}\n};\n`;
93
+ 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
+ }
@@ -55,15 +55,60 @@ function bundleFailures(output) {
55
55
  }
56
56
  return failures;
57
57
  }
58
+ const indent = (output) => output.trim().split('\n').map(l => ` ${l}`).join('\n');
59
+ /**
60
+ * A Start app: `vite build` first, since it regenerates the route tree tsc
61
+ * reads, then tsc, then describe, which reports what the platform's build
62
+ * refuses, read the same way.
63
+ */
64
+ async function checkStartApp(appPath) {
65
+ process.stdout.write(' vite build ... ');
66
+ const vite = run('npx', ['vite', 'build'], appPath);
67
+ console.log(vite.ok ? '✓' : '✗');
68
+ if (!vite.ok) {
69
+ console.log(indent(vite.output));
70
+ return false;
71
+ }
72
+ process.stdout.write(' tsc --noEmit ... ');
73
+ // --pretty for `file:line:col - error` lines, the shape the platform re-roots.
74
+ const tsc = run('npx', ['tsc', '--noEmit', '--pretty', '-p', 'tsconfig.app.json'], appPath);
75
+ console.log(tsc.ok ? '✓' : '✗');
76
+ if (!tsc.ok)
77
+ console.log(indent(tsc.output));
78
+ process.stdout.write(' describe ... ');
79
+ try {
80
+ const { describeBuiltApp } = await import('../start/describeBuilt.js');
81
+ const { descriptionProblems } = await import('../start/problems.js');
82
+ const problems = descriptionProblems((await describeBuiltApp(appPath)));
83
+ console.log(problems.length ? '✗' : '✓');
84
+ if (problems.length)
85
+ console.log(problems.map(p => ` ${p}`).join('\n'));
86
+ return tsc.ok && problems.length === 0;
87
+ }
88
+ catch (err) {
89
+ console.log('✗');
90
+ console.log(` ${err instanceof Error ? err.message : String(err)}`);
91
+ return false;
92
+ }
93
+ }
58
94
  export async function runCheck() {
59
- const appDirs = listSourceDirs();
60
- if (appDirs.length === 0) {
95
+ const { isStartAppDir } = await import('../start/entries.js');
96
+ // Run inside a Start app, its own zitejs checks just that app: the workspace
97
+ // root's is a classic release that doesn't know Start.
98
+ const inStartApp = isStartAppDir('.');
99
+ const appDirs = inStartApp ? [] : listSourceDirs();
100
+ if (!inStartApp && appDirs.length === 0) {
61
101
  console.error('No apps or automations found under apps/ or automations/.');
62
102
  process.exit(1);
63
103
  }
64
- let allPassed = true;
104
+ let allPassed = inStartApp ? await checkStartApp('.') : true;
65
105
  for (const { root, dir: app, path: appPath } of appDirs) {
66
106
  console.log(`\n── ${appPath} ──`);
107
+ if (isStartAppDir(appPath)) {
108
+ if (!(await checkStartApp(appPath)))
109
+ allPassed = false;
110
+ continue;
111
+ }
67
112
  // Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
68
113
  // solution-style config — `"files": []` plus a reference — so
69
114
  // `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
@@ -1,3 +1,5 @@
1
+ import type { LoadedWorkflow } from "./workflowRun.js";
2
+ import type { WorkflowConfig } from "./workflow.js";
1
3
  export type Access = {
2
4
  kind: "signedIn" | "anyone" | "webhook" | "none";
3
5
  roles: string[];
@@ -7,6 +9,8 @@ export type DescribedFunction = {
7
9
  name?: string;
8
10
  file?: string;
9
11
  method?: string;
12
+ /** From the factory's `description` option; display-only. */
13
+ description?: string;
10
14
  access: Access;
11
15
  input?: unknown;
12
16
  problems: string[];
@@ -16,9 +20,18 @@ export type DescribedRoute = {
16
20
  methods: Record<string, Access>;
17
21
  problems: string[];
18
22
  };
23
+ export type DescribedWorkflow = {
24
+ id: string;
25
+ file: string;
26
+ description?: string;
27
+ schedule?: WorkflowConfig["schedule"];
28
+ trigger?: WorkflowConfig["trigger"];
29
+ problems: string[];
30
+ };
19
31
  export type Description = {
20
32
  functions: DescribedFunction[];
21
33
  routes: DescribedRoute[];
34
+ workflows: DescribedWorkflow[];
22
35
  };
23
36
  export declare function describeFunction(id: string, request: Request): Promise<DescribedFunction>;
24
37
  type RouteLike = {
@@ -32,9 +45,12 @@ type RouteLike = {
32
45
  };
33
46
  };
34
47
  export declare function describeRoutes(routesById: Record<string, RouteLike>): DescribedRoute[];
48
+ export declare function describeWorkflows(workflows: LoadedWorkflow[]): DescribedWorkflow[];
35
49
  export declare function describeApp(opts: {
36
50
  fnIds: string[];
51
+ descriptions?: Record<string, string>;
37
52
  getRouter: () => unknown;
38
53
  request: Request;
54
+ workflows?: LoadedWorkflow[];
39
55
  }): Promise<Description>;
40
56
  export {};
@@ -38,6 +38,8 @@ const describeProbe = createMiddleware({ type: "function" }).server(({ serverFnM
38
38
  Object.assign(probe, { meta: serverFnMeta, method, reached: true });
39
39
  return "zite:describe";
40
40
  });
41
+ const SERVER_ONLY = "server-only: nothing in the browser calls it, and a server function that only other server functions call can't run. " +
42
+ "Move the shared logic into a *.server.ts helper and call that.";
41
43
  export async function describeFunction(id, request) {
42
44
  const problems = [];
43
45
  const seen = [];
@@ -64,7 +66,14 @@ export async function describeFunction(id, request) {
64
66
  await runWithStartContext(startContext, () => createSsrRpc(id)({ data: undefined, context: {} }));
65
67
  }
66
68
  catch (error) {
67
- problems.push(`probe-failed: ${error instanceof Error ? error.message : String(error)}`);
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ // TanStack registers only functions something can call through it. One that
71
+ // only other server functions call isn't registered, and calling it fails.
72
+ if (message === `Server function info not found for ${id}`) {
73
+ probe = null;
74
+ return { id, access: { kind: "none", roles: [] }, problems: [SERVER_ONLY] };
75
+ }
76
+ problems.push(`probe-failed: ${message}`);
68
77
  }
69
78
  const state = probe;
70
79
  probe = null;
@@ -113,10 +122,26 @@ export function describeRoutes(routesById) {
113
122
  }
114
123
  return out;
115
124
  }
125
+ // A workflow's id is its export name: what the host invokes it by and the editor shows.
126
+ export function describeWorkflows(workflows) {
127
+ return workflows.map(({ id, file, workflow }) => {
128
+ const problems = [];
129
+ if (id === "default")
130
+ problems.push("workflow-default-export: export it by name, which becomes its id");
131
+ const elsewhere = workflows.find((other) => other.id === id && other.file !== file);
132
+ if (elsewhere)
133
+ problems.push(`duplicate-workflow: ${id} is also exported from ${elsewhere.file}`);
134
+ const { description, schedule, trigger } = workflow.config;
135
+ return { id, file, description, ...(schedule ? { schedule } : { trigger }), problems };
136
+ });
137
+ }
116
138
  export async function describeApp(opts) {
117
139
  const functions = [];
118
- for (const id of opts.fnIds)
119
- functions.push(await describeFunction(id, opts.request));
140
+ for (const id of opts.fnIds) {
141
+ const described = await describeFunction(id, opts.request);
142
+ const description = opts.descriptions?.[id];
143
+ functions.push(description ? { ...described, description } : described);
144
+ }
120
145
  const router = (await opts.getRouter());
121
- return { functions, routes: describeRoutes(router.routesById) };
146
+ return { functions, routes: describeRoutes(router.routesById), workflows: describeWorkflows(opts.workflows ?? []) };
122
147
  }
@@ -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>;
@@ -0,0 +1,47 @@
1
+ const stringValue = (node) => {
2
+ if (node?.type === "Literal" && typeof node.value === "string")
3
+ return node.value;
4
+ const quasis = node?.quasis;
5
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
6
+ return quasis?.[0]?.value.cooked;
7
+ return undefined;
8
+ };
9
+ const descriptionProperty = (node) => {
10
+ for (const property of node.properties) {
11
+ const key = property.key;
12
+ const named = key?.type === "Identifier" ? key.name : key?.type === "Literal" ? key.value : undefined;
13
+ if (property.type === "Property" && named === "description")
14
+ return stringValue(property.value);
15
+ }
16
+ return undefined;
17
+ };
18
+ // Walks a builder chain, `signedInFn({ description }).validator(…).handler(…)`, to an object option.
19
+ const descriptionIn = (node) => {
20
+ if (node?.type === "CallExpression") {
21
+ for (const arg of node.arguments) {
22
+ const found = arg.type === "ObjectExpression" ? descriptionProperty(arg) : undefined;
23
+ if (found !== undefined)
24
+ return found;
25
+ }
26
+ return descriptionIn(node.callee);
27
+ }
28
+ if (node?.type === "MemberExpression")
29
+ return descriptionIn(node.object);
30
+ return undefined;
31
+ };
32
+ /** `name -> description` for each top-level const built with a description option. */
33
+ export function factoryDescriptions(program) {
34
+ const found = new Map();
35
+ for (const statement of program.body) {
36
+ const declaration = (statement.type === "ExportNamedDeclaration" ? statement.declaration : statement);
37
+ if (declaration?.type !== "VariableDeclaration")
38
+ continue;
39
+ for (const declarator of declaration.declarations) {
40
+ const id = declarator.id;
41
+ const description = descriptionIn(declarator.init);
42
+ if (id.type === "Identifier" && description)
43
+ found.set(id.name, description);
44
+ }
45
+ }
46
+ return found;
47
+ }
@@ -8,4 +8,9 @@ export declare const renderStartInstance: (hasAppStart: boolean) => string;
8
8
  export declare const hasAppStart: (appDir: string) => boolean;
9
9
  /** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
10
10
  export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
11
+ export declare const isStartAppDir: (appPath: string) => boolean;
11
12
  export declare function regenerateStartEntries(app: SourceDir): void;
13
+ /** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
14
+ export declare function workflowFiles(appDir: string): string[];
15
+ /** The registry zite() builds in place of zitejs's empty one. */
16
+ export declare const renderWorkflowModules: (appDir: string, files: string[]) => string;
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
3
  export const START_SERVER_ENTRY = ".zite/server.ts";
4
4
  export const START_INSTANCE_ENTRY = ".zite/start.ts";
5
5
  const APP_START_FILES = ["src/start.ts", "src/start.tsx"];
@@ -34,14 +34,15 @@ export const writeIfChanged = (appDir, file, content) => {
34
34
  writeFileSync(target, content);
35
35
  return true;
36
36
  };
37
- const isStartApp = (app) => {
37
+ export const isStartAppDir = (appPath) => {
38
38
  try {
39
- return JSON.parse(readFileSync(join(app.path, "zite.config.json"), "utf-8"))?.runtime === "start";
39
+ return JSON.parse(readFileSync(join(appPath, "zite.config.json"), "utf-8"))?.runtime === "start";
40
40
  }
41
41
  catch {
42
42
  return false;
43
43
  }
44
44
  };
45
+ const isStartApp = (app) => isStartAppDir(app.path);
45
46
  export function regenerateStartEntries(app) {
46
47
  if (!isStartApp(app))
47
48
  return;
@@ -54,3 +55,28 @@ export function regenerateStartEntries(app) {
54
55
  console.log(`Wrote ${join(app.path, file)}`);
55
56
  }
56
57
  }
58
+ const SOURCE_FILE = /\.[mc]?[jt]sx?$/;
59
+ const NOT_APP_CODE = /(\.(test|spec)\.[^.]+|\.d\.ts|routeTree\.gen\.ts)$/;
60
+ /** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
61
+ export function workflowFiles(appDir) {
62
+ const found = [];
63
+ const walk = (dir) => {
64
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
65
+ const full = join(dir, entry.name);
66
+ if (entry.isDirectory()) {
67
+ if (entry.name !== "node_modules")
68
+ walk(full);
69
+ }
70
+ else if (SOURCE_FILE.test(entry.name) && !NOT_APP_CODE.test(entry.name) && readFileSync(full, "utf-8").includes("createWorkflow")) {
71
+ found.push(relative(appDir, full).split("\\").join("/"));
72
+ }
73
+ }
74
+ };
75
+ if (existsSync(join(appDir, "src")))
76
+ walk(join(appDir, "src"));
77
+ return found.sort();
78
+ }
79
+ /** The registry zite() builds in place of zitejs's empty one. */
80
+ export const renderWorkflowModules = (appDir, files) => `export const workflowModules = {\n${files
81
+ .map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(join(appDir, file))}),`)
82
+ .join("\n")}\n};\n`;
@@ -1,6 +1,9 @@
1
1
  import type { User } from "./types.js";
2
2
  export type { User, ZiteRequestContext } from "./types.js";
3
3
  export { createZiteRouter, type ZiteRouterContext } from "./router.js";
4
+ export { createWorkflow, type Workflow, type WorkflowArgs, type WorkflowConfig } from "./workflow.js";
5
+ export type { ZiteSchedule } from "../schedules/index.js";
6
+ export type { ZiteTrigger } from "../backend/index.js";
4
7
  declare module "@tanstack/start-client-core" {
5
8
  interface ServerFnOptions {
6
9
  /** What this function does, in a sentence. Shown in the editor. */
@@ -4,6 +4,7 @@ import { USAGE_TOKEN_QUERY_PARAM } from "../auth/constants.js";
4
4
  import { currentHost } from "./host.js";
5
5
  import { getHost, inScope, runInScope } from "./scope.js";
6
6
  export { createZiteRouter } from "./router.js";
7
+ export { createWorkflow } from "./workflow.js";
7
8
  const STATUS = {
8
9
  BAD_REQUEST: 400,
9
10
  UNAUTHORIZED: 401,
@@ -3,6 +3,7 @@
3
3
  import { createCsrfMiddleware, createMiddleware, createStart } from "@tanstack/react-start";
4
4
  import { createSerializationAdapter, redirect } from "@tanstack/react-router";
5
5
  import { isSignInRequired, loginHref, ZiteError } from "./index.js";
6
+ import { errorOf, preview } from "./runLog.js";
6
7
  import { getHost, inScope } from "./scope.js";
7
8
  // TanStack drops its default CSRF check once an app has a start instance.
8
9
  const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
@@ -21,23 +22,6 @@ const signInOn401 = createMiddleware({ type: "function" }).client(async ({ next
21
22
  throw redirect({ href, reloadDocument: true });
22
23
  }
23
24
  });
24
- const PREVIEW_LIMIT = 8_000;
25
- const preview = (value) => {
26
- if (value === undefined)
27
- return undefined;
28
- if (typeof value === "object" && value !== null && Symbol.asyncIterator in value)
29
- return "[stream]";
30
- try {
31
- const json = JSON.stringify(value);
32
- return json && json.length > PREVIEW_LIMIT ? `${json.slice(0, PREVIEW_LIMIT)}…` : json;
33
- }
34
- catch {
35
- return "[unserializable]";
36
- }
37
- };
38
- const errorOf = (error) => error instanceof ZiteError
39
- ? { code: error.code, message: error.message }
40
- : { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : String(error) };
41
25
  // Records each call for the run log. The host's own record of the request is the
42
26
  // floor; this adds what only the app sees: decoded input, output and errors.
43
27
  const runCapture = createMiddleware({ type: "function" }).server(async ({ next, data, serverFnMeta }) => {
@@ -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,8 @@
1
+ /** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
2
+ export function descriptionProblems(description) {
3
+ return [
4
+ ...description.functions.flatMap((f) => f.problems.map((p) => `function ${f.id}${f.file ? ` (${f.file})` : ""}: ${p}`)),
5
+ ...description.routes.flatMap((r) => r.problems.map((p) => `route ${r.path}: ${p}`)),
6
+ ...(description.workflows ?? []).flatMap((w) => w.problems.map((p) => `workflow ${w.id} (${w.file}): ${p}`)),
7
+ ];
8
+ }
@@ -0,0 +1,5 @@
1
+ export declare const preview: (value: unknown) => string | undefined;
2
+ export declare const errorOf: (error: unknown) => {
3
+ code: string;
4
+ message: string;
5
+ };
@@ -0,0 +1,18 @@
1
+ import { ZiteError } from "./index.js";
2
+ const PREVIEW_LIMIT = 8_000;
3
+ export const preview = (value) => {
4
+ if (value === undefined)
5
+ return undefined;
6
+ if (typeof value === "object" && value !== null && Symbol.asyncIterator in value)
7
+ return "[stream]";
8
+ try {
9
+ const json = JSON.stringify(value);
10
+ return json && json.length > PREVIEW_LIMIT ? `${json.slice(0, PREVIEW_LIMIT)}…` : json;
11
+ }
12
+ catch {
13
+ return "[unserializable]";
14
+ }
15
+ };
16
+ export const errorOf = (error) => error instanceof ZiteError
17
+ ? { code: error.code, message: error.message }
18
+ : { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : String(error) };
@@ -31,6 +31,8 @@ export declare function installPlatformTransport(call: (input: PlatformCallInput
31
31
  export declare function hostContextFromRequest(request: Request): ZiteRequestContext;
32
32
  /** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
33
33
  export declare const builtFunctionIds: () => string[];
34
+ /** Each server function's `description` option, written in by `zite()` at build. */
35
+ export declare const builtFunctionDescriptions: () => Record<string, string>;
34
36
  type Fetch = (request: Request, ...rest: unknown[]) => Promise<Response> | Response;
35
37
  /**
36
38
  * The fetch handler of a Start app's server entry. Describe calls get the app's
@@ -1,5 +1,7 @@
1
1
  import { describeApp } from "./describe.js";
2
+ import { preview } from "./runLog.js";
2
3
  import { getHost, requireScope, runWithHost } from "./scope.js";
4
+ import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
3
5
  export { runWithHost } from "./scope.js";
4
6
  export { describeApp } from "./describe.js";
5
7
  /**
@@ -38,6 +40,16 @@ export function hostContextFromRequest(request) {
38
40
  }
39
41
  /** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
40
42
  export const builtFunctionIds = () => "__ZITE_FN_IDS__".split(",").filter((id) => id && !id.startsWith("__ZITE"));
43
+ const parseDescriptions = (json) => {
44
+ try {
45
+ return JSON.parse(json);
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ };
51
+ /** Each server function's `description` option, written in by `zite()` at build. */
52
+ export const builtFunctionDescriptions = () => parseDescriptions("__ZITE_FN_DESCRIPTIONS__");
41
53
  // Server functions also run while a streamed page renders, so the run log is
42
54
  // complete only once the body has been read to the end.
43
55
  const onBodyEnd = (response, done) => {
@@ -47,6 +59,30 @@ const onBodyEnd = (response, done) => {
47
59
  }
48
60
  return new Response(response.body.pipeThrough(new TransformStream({ flush: done })), response);
49
61
  };
62
+ const INVOKE_PATH = "/_zite/invoke";
63
+ // The host sends `{ input }`; a platform-fired run's input carries the runner's
64
+ // `__cron`, `__poll` or `__webhook` state, exactly as for classic endpoints.
65
+ async function invokeWorkflow(request, host, workflows) {
66
+ if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
67
+ return new Response("Not found", { status: 404 });
68
+ const name = host.invoke?.name;
69
+ const found = workflows.find((w) => w.id === name);
70
+ if (!found)
71
+ return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `No workflow named ${name}` } }, { status: 404 });
72
+ const input = (await request.json().catch(() => ({})))?.input;
73
+ const startedAt = Date.now();
74
+ const result = await runWorkflow(found.workflow, input);
75
+ host.runs.push({
76
+ fn: `workflow:${found.id}`,
77
+ name: found.id,
78
+ nested: false,
79
+ startedAt,
80
+ durationMs: Date.now() - startedAt,
81
+ input: preview(loggedInput(input)),
82
+ ...(result.ok ? { output: preview(result.output) } : { error: result.error }),
83
+ });
84
+ return Response.json(result, { status: result.ok ? 200 : 500 });
85
+ }
50
86
  /**
51
87
  * The fetch handler of a Start app's server entry. Describe calls get the app's
52
88
  * description; everything else runs TanStack with the host's identity in
@@ -62,13 +98,22 @@ export function createZiteFetch(options) {
62
98
  return binding;
63
99
  };
64
100
  installPlatformTransport((call) => platform().call(call));
101
+ // Loaded on first use, so a page request never pays for importing workflow modules.
102
+ let workflows;
103
+ const loadedWorkflows = () => (workflows ??= loadWorkflows());
65
104
  return async (request, ...rest) => {
66
105
  binding ??= rest[0]?.ZITE;
67
106
  if (request.headers.get("x-zite-invoke") === "describe") {
68
- return Response.json(await describeApp({ fnIds: builtFunctionIds(), getRouter: options.getRouter, request }));
107
+ return Response.json(await describeApp({
108
+ fnIds: builtFunctionIds(),
109
+ descriptions: builtFunctionDescriptions(),
110
+ getRouter: options.getRouter,
111
+ request,
112
+ workflows: await loadedWorkflows(),
113
+ }));
69
114
  }
70
115
  const host = hostContextFromRequest(request);
71
- const response = await runWithHost(host, () => options.handler.fetch(request));
116
+ const response = await runWithHost(host, async () => host.invoke?.kind === "workflow" ? invokeWorkflow(request, host, await loadedWorkflows()) : options.handler.fetch(request));
72
117
  return onBodyEnd(response, () => {
73
118
  if (!host.runs.length || !binding)
74
119
  return;
@@ -2,9 +2,13 @@ import path from "node:path";
2
2
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3
3
  import viteReact from "@vitejs/plugin-react";
4
4
  import { ziteId } from "../vite/index.js";
5
- import { hasAppStart, renderStartInstance, renderStartServerEntry, START_INSTANCE_ENTRY, START_SERVER_ENTRY, writeIfChanged, } from "./entries.js";
6
- // Must match the literal in ./server.ts, which the build rewrites.
5
+ import { factoryDescriptions } from "./descriptions.js";
6
+ import { hasAppStart, renderStartInstance, renderStartServerEntry, renderWorkflowModules, START_INSTANCE_ENTRY, START_SERVER_ENTRY, workflowFiles, writeIfChanged, } from "./entries.js";
7
+ // Must match the literals in ./server.ts, which the build rewrites.
7
8
  const FN_IDS_TOKEN = "__ZITE_FN_IDS__";
9
+ const FN_DESCRIPTIONS_LITERAL = /(["'`])__ZITE_FN_DESCRIPTIONS__\1/;
10
+ // zitejs's empty registry, which the build replaces with the app's.
11
+ const WORKFLOW_REGISTRY = /[\\/]zitejs[\\/]dist[\\/]esm[\\/]start[\\/]workflowModules\.js$/;
8
12
  // Platform-pinned Worker settings. The deploy uses its own fixed template, so
9
13
  // these only have to match it; apps don't carry a wrangler config.
10
14
  const WORKER_COMPATIBILITY_DATE = "2026-09-25";
@@ -63,17 +67,38 @@ export function zite(options = {}) {
63
67
  };
64
68
  },
65
69
  };
70
+ // Each server function's `description` option, by name, for the editor.
71
+ const descriptions = new Map();
72
+ const srcDir = `${path.join(appDir, "src")}${path.sep}`;
66
73
  const functionIds = {
67
74
  name: "zite:function-ids",
68
75
  apply: "build",
76
+ transform(code, id) {
77
+ if (this.environment?.config.consumer !== "server" || !id.startsWith(srcDir) || !code.includes("description"))
78
+ return;
79
+ try {
80
+ for (const [name, text] of factoryDescriptions(this.parse(code)))
81
+ descriptions.set(name, text);
82
+ }
83
+ catch {
84
+ // Display-only: an unreadable module just shows no descriptions.
85
+ }
86
+ },
69
87
  generateBundle(_, bundle) {
88
+ const described = Object.fromEntries([...descriptions].filter(([name]) => ids.has(name)));
70
89
  for (const chunk of Object.values(bundle)) {
71
- if (chunk.type === "chunk" && chunk.code.includes(FN_IDS_TOKEN)) {
90
+ if (chunk.type !== "chunk")
91
+ continue;
92
+ if (chunk.code.includes(FN_IDS_TOKEN))
72
93
  chunk.code = chunk.code.replace(FN_IDS_TOKEN, [...ids.keys()].join(","));
73
- }
94
+ chunk.code = chunk.code.replace(FN_DESCRIPTIONS_LITERAL, () => JSON.stringify(JSON.stringify(described)));
74
95
  }
75
96
  },
76
97
  };
98
+ const workflowRegistry = {
99
+ name: "zite:workflow-registry",
100
+ load: (id) => (WORKFLOW_REGISTRY.test(id.split("?")[0]) ? renderWorkflowModules(appDir, workflowFiles(appDir)) : undefined),
101
+ };
77
102
  return [
78
103
  tanstackStart({
79
104
  // TanStack resolves these from `src/`.
@@ -110,5 +135,6 @@ export function zite(options = {}) {
110
135
  ziteId(),
111
136
  compileZitejs,
112
137
  functionIds,
138
+ workflowRegistry,
113
139
  ];
114
140
  }
@@ -0,0 +1,36 @@
1
+ import type { ZiteTrigger, ZiteTriggerContext } from "../backend/index.js";
2
+ import type { ZiteSchedule } from "../schedules/index.js";
3
+ declare const WORKFLOW: unique symbol;
4
+ export type WorkflowConfig = {
5
+ /** What this workflow does, in a sentence. Shown in the editor. */
6
+ description?: string;
7
+ } & ({
8
+ schedule: ZiteSchedule;
9
+ trigger?: never;
10
+ } | {
11
+ trigger: ZiteTrigger;
12
+ schedule?: never;
13
+ });
14
+ /** What a workflow's handler gets. A trigger adds `trigger`, typed by the kind it declares. */
15
+ export type WorkflowArgs<TConfig extends WorkflowConfig> = {
16
+ input: unknown;
17
+ } & (TConfig extends {
18
+ trigger: infer T;
19
+ } ? {
20
+ trigger: ZiteTriggerContext<T>;
21
+ } : unknown);
22
+ export type Workflow<TConfig extends WorkflowConfig = WorkflowConfig> = {
23
+ readonly [WORKFLOW]: true;
24
+ readonly config: TConfig;
25
+ readonly handler: (args: WorkflowArgs<TConfig>) => unknown;
26
+ };
27
+ /**
28
+ * Scheduled or triggered work, in a `*.server.ts` file:
29
+ * `export const nightly = createWorkflow({ schedule }).handler(async () => { ... })`.
30
+ * Only the platform runs it. It has no user and no URL, and reads data like a server function does.
31
+ */
32
+ export declare function createWorkflow<const TConfig extends WorkflowConfig>(config: TConfig): {
33
+ handler: (fn: (args: WorkflowArgs<TConfig>) => unknown) => Workflow<TConfig>;
34
+ };
35
+ export declare const isWorkflow: (value: unknown) => value is Workflow;
36
+ export {};
@@ -0,0 +1,13 @@
1
+ // Shared across copies of zitejs in one bundle, like the access scope.
2
+ const WORKFLOW = Symbol.for("zitejs:workflow");
3
+ /**
4
+ * Scheduled or triggered work, in a `*.server.ts` file:
5
+ * `export const nightly = createWorkflow({ schedule }).handler(async () => { ... })`.
6
+ * Only the platform runs it. It has no user and no URL, and reads data like a server function does.
7
+ */
8
+ export function createWorkflow(config) {
9
+ return {
10
+ handler: (fn) => ({ [WORKFLOW]: true, config, handler: fn }),
11
+ };
12
+ }
13
+ export const isWorkflow = (value) => typeof value === "object" && value !== null && value[WORKFLOW] === true;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expectTypeOf, it } from "vitest";
2
+ import { createWorkflow } from "./workflow.js";
3
+ describe("createWorkflow handler args", () => {
4
+ it("gives a schedule no trigger", () => {
5
+ createWorkflow({ schedule: { scheduleType: "oneTime", fireAt: "2026-10-10T03:00:00Z", timezone: "UTC" } }).handler((args) => {
6
+ expectTypeOf(args).toEqualTypeOf();
7
+ });
8
+ });
9
+ it("types the trigger by the kind it declares", () => {
10
+ createWorkflow({ trigger: { every: "5m" } }).handler(({ trigger }) => {
11
+ expectTypeOf(trigger).toEqualTypeOf();
12
+ });
13
+ createWorkflow({ trigger: { integration: "slack", events: ["message.channels"] } }).handler(({ trigger }) => {
14
+ expectTypeOf(trigger).toEqualTypeOf();
15
+ });
16
+ createWorkflow({ trigger: { integration: "asana" } }).handler(({ trigger }) => {
17
+ expectTypeOf(trigger).toEqualTypeOf();
18
+ });
19
+ });
20
+ it("takes a schedule or a trigger, not both", () => {
21
+ // @ts-expect-error a workflow runs on one or the other
22
+ createWorkflow({ schedule: { scheduleType: "oneTime", fireAt: "x", timezone: "UTC" }, trigger: { every: "5m" } });
23
+ });
24
+ });
@@ -0,0 +1 @@
1
+ export declare const workflowModules: Record<string, () => Promise<Record<string, unknown>>>;
@@ -0,0 +1,3 @@
1
+ // Replaced at build by zite() with a lazy import of every app module that
2
+ // mentions createWorkflow. Empty wherever the plugin didn't build the app.
3
+ export const workflowModules = {};
@@ -0,0 +1,80 @@
1
+ import { type Workflow } from "./workflow.js";
2
+ export type TriggerReport = {
3
+ handled: string[];
4
+ cursor?: string;
5
+ };
6
+ export type WorkflowRunResult = {
7
+ ok: true;
8
+ output: unknown;
9
+ trigger?: TriggerReport;
10
+ } | {
11
+ ok: false;
12
+ error: {
13
+ code: string;
14
+ message: string;
15
+ };
16
+ trigger?: TriggerReport;
17
+ };
18
+ /**
19
+ * Splits the platform's state out of the input. A poll or a provider delivery
20
+ * with a cursor gets `seen`, `handled` and `advance`. Handled ids are reported
21
+ * whether or not the run throws, so an item acted on is never acted on again;
22
+ * the staged cursor only on success. Only ids the incoming `seen` lacked are
23
+ * reported: billing reads an empty list as a run that found nothing new.
24
+ */
25
+ export declare function triggerContext(workflow: Workflow, rawInput: unknown): {
26
+ input: unknown;
27
+ trigger?: undefined;
28
+ report?: undefined;
29
+ } | {
30
+ input: {
31
+ [x: string]: unknown;
32
+ };
33
+ trigger: {
34
+ provider: string;
35
+ deliveryId?: string | undefined;
36
+ headers: Record<string, unknown>;
37
+ } | {
38
+ headers?: undefined;
39
+ } | {
40
+ headers: {};
41
+ };
42
+ report?: undefined;
43
+ } | {
44
+ input: {
45
+ [x: string]: unknown;
46
+ };
47
+ trigger: {
48
+ provider: string;
49
+ deliveryId?: string | undefined;
50
+ headers: Record<string, unknown>;
51
+ cursor: string | null;
52
+ seen: (id: string) => boolean;
53
+ handled: (id: string) => void;
54
+ advance: (cursor: string) => void;
55
+ } | {
56
+ headers?: undefined;
57
+ cursor: string | null;
58
+ seen: (id: string) => boolean;
59
+ handled: (id: string) => void;
60
+ advance: (cursor: string) => void;
61
+ } | {
62
+ headers: {};
63
+ cursor: string | null;
64
+ seen: (id: string) => boolean;
65
+ handled: (id: string) => void;
66
+ advance: (cursor: string) => void;
67
+ };
68
+ report: (succeeded: boolean) => TriggerReport;
69
+ };
70
+ /** Runs the handler in a workflow access scope: no user, and data calls allowed. */
71
+ export declare function runWorkflow(workflow: Workflow, rawInput: unknown): Promise<WorkflowRunResult>;
72
+ export type LoadedWorkflow = {
73
+ id: string;
74
+ file: string;
75
+ workflow: Workflow;
76
+ };
77
+ /** Every exported workflow in the app, by export name. */
78
+ export declare function loadWorkflows(modules?: Record<string, () => Promise<Record<string, unknown>>>): Promise<LoadedWorkflow[]>;
79
+ /** The input as the run log keeps it: a trigger's `seen` list can hold thousands of ids. */
80
+ export declare const loggedInput: (rawInput: unknown) => unknown;
@@ -0,0 +1,107 @@
1
+ // Server-only: runs a workflow the host invoked. Takes the same platform input
2
+ // as classic triggered endpoints (`__cron`, `__poll`, `__webhook`), so the
3
+ // runner's schedules, polls and provider deliveries work unchanged.
4
+ import { errorOf } from "./runLog.js";
5
+ import { runInScope } from "./scope.js";
6
+ import { isWorkflow } from "./workflow.js";
7
+ import { workflowModules } from "./workflowModules.js";
8
+ const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
9
+ const idOf = (id) => {
10
+ if (typeof id !== "string" && typeof id !== "number")
11
+ throw new TypeError(`trigger ids must be strings, got ${typeof id}`);
12
+ const value = String(id);
13
+ if (value.length === 0 || value.length > 512)
14
+ throw new TypeError("trigger ids must be 1 to 512 characters");
15
+ return value;
16
+ };
17
+ /**
18
+ * Splits the platform's state out of the input. A poll or a provider delivery
19
+ * with a cursor gets `seen`, `handled` and `advance`. Handled ids are reported
20
+ * whether or not the run throws, so an item acted on is never acted on again;
21
+ * the staged cursor only on success. Only ids the incoming `seen` lacked are
22
+ * reported: billing reads an empty list as a run that found nothing new.
23
+ */
24
+ export function triggerContext(workflow, rawInput) {
25
+ const raw = asRecord(rawInput);
26
+ const { __cron, __poll, __webhook, ...input } = raw ?? {};
27
+ const declared = workflow.config.trigger;
28
+ if (!declared)
29
+ return { input: raw ? input : rawInput };
30
+ const poll = asRecord(__poll);
31
+ const webhook = asRecord(__webhook);
32
+ const delivery = typeof webhook?.provider === "string" ? webhook : null;
33
+ // A run with no trigger state, such as a manual one, still gets a trigger: an empty one.
34
+ const info = delivery
35
+ ? {
36
+ headers: asRecord(delivery.headers) ?? {},
37
+ ...(typeof delivery.deliveryId === "string" ? { deliveryId: delivery.deliveryId } : {}),
38
+ provider: delivery.provider,
39
+ }
40
+ : declared.every
41
+ ? {}
42
+ : { headers: {} };
43
+ if (!poll && declared.events)
44
+ return { input, trigger: info };
45
+ const state = poll ?? delivery ?? {};
46
+ const known = new Set(Array.isArray(state.seen) ? state.seen.map(String) : []);
47
+ const handled = [];
48
+ let staged;
49
+ const trigger = {
50
+ cursor: typeof state.cursor === "string" ? state.cursor : null,
51
+ seen: (id) => known.has(String(id)),
52
+ handled: (id) => {
53
+ const value = idOf(id);
54
+ if (known.has(value))
55
+ return;
56
+ known.add(value);
57
+ handled.push(value);
58
+ },
59
+ advance: (cursor) => {
60
+ if (typeof cursor !== "string" || cursor.length > 4096) {
61
+ throw new TypeError("trigger.advance takes a string of at most 4096 characters");
62
+ }
63
+ staged = cursor;
64
+ },
65
+ ...info,
66
+ };
67
+ const report = (succeeded) => ({
68
+ handled: handled.slice(-2000),
69
+ ...(succeeded && staged !== undefined ? { cursor: staged } : {}),
70
+ });
71
+ return { input, trigger, report };
72
+ }
73
+ /** Runs the handler in a workflow access scope: no user, and data calls allowed. */
74
+ export async function runWorkflow(workflow, rawInput) {
75
+ const { input, trigger, report } = triggerContext(workflow, rawInput);
76
+ const args = { input, ...(trigger ? { trigger } : {}) };
77
+ try {
78
+ const output = await runInScope({ kind: "workflow", user: null }, () => workflow.handler(args));
79
+ return { ok: true, output, ...(report ? { trigger: report(true) } : {}) };
80
+ }
81
+ catch (error) {
82
+ return { ok: false, error: errorOf(error), ...(report ? { trigger: report(false) } : {}) };
83
+ }
84
+ }
85
+ /** Every exported workflow in the app, by export name. */
86
+ export async function loadWorkflows(modules = workflowModules) {
87
+ const found = [];
88
+ for (const [file, load] of Object.entries(modules)) {
89
+ for (const [id, value] of Object.entries(await load()))
90
+ if (isWorkflow(value))
91
+ found.push({ id, file, workflow: value });
92
+ }
93
+ return found;
94
+ }
95
+ /** The input as the run log keeps it: a trigger's `seen` list can hold thousands of ids. */
96
+ export const loggedInput = (rawInput) => {
97
+ const raw = asRecord(rawInput);
98
+ if (!raw)
99
+ return rawInput;
100
+ const out = { ...raw };
101
+ for (const key of ["__poll", "__webhook"]) {
102
+ const state = asRecord(raw[key]);
103
+ if (state && Array.isArray(state.seen))
104
+ out[key] = { ...state, seen: `[${state.seen.length} ids]` };
105
+ }
106
+ return out;
107
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.10.0-start.1",
3
+ "version": "0.10.0-start.2",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,