zitejs 0.10.0-start.1 → 0.10.0-start.3

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 (35) hide show
  1. package/dist/cjs/check/index.js +81 -3
  2. package/dist/cjs/start/entries.d.ts +5 -0
  3. package/dist/cjs/start/entries.js +32 -3
  4. package/dist/cjs/start/problems.d.ts +19 -0
  5. package/dist/cjs/start/problems.js +11 -0
  6. package/dist/esm/check/index.js +48 -3
  7. package/dist/esm/start/describe.d.ts +16 -0
  8. package/dist/esm/start/describe.js +29 -4
  9. package/dist/esm/start/descriptions.d.ts +4 -0
  10. package/dist/esm/start/descriptions.js +47 -0
  11. package/dist/esm/start/entries.d.ts +5 -0
  12. package/dist/esm/start/entries.js +30 -4
  13. package/dist/esm/start/index.d.ts +6 -0
  14. package/dist/esm/start/index.js +32 -3
  15. package/dist/esm/start/instance.d.ts +1 -1
  16. package/dist/esm/start/instance.js +1 -17
  17. package/dist/esm/start/problems.d.ts +19 -0
  18. package/dist/esm/start/problems.js +8 -0
  19. package/dist/esm/start/router.js +3 -0
  20. package/dist/esm/start/runLog.d.ts +5 -0
  21. package/dist/esm/start/runLog.js +18 -0
  22. package/dist/esm/start/scope.d.ts +2 -2
  23. package/dist/esm/start/server.d.ts +21 -31
  24. package/dist/esm/start/server.js +115 -37
  25. package/dist/esm/start/types.d.ts +53 -4
  26. package/dist/esm/start/vite.js +33 -4
  27. package/dist/esm/start/workflow.d.ts +36 -0
  28. package/dist/esm/start/workflow.js +13 -0
  29. package/dist/esm/start/workflow.test-d.d.ts +1 -0
  30. package/dist/esm/start/workflow.test-d.js +24 -0
  31. package/dist/esm/start/workflowModules.d.ts +1 -0
  32. package/dist/esm/start/workflowModules.js +3 -0
  33. package/dist/esm/start/workflowRun.d.ts +80 -0
  34. package/dist/esm/start/workflowRun.js +107 -0
  35. package/package.json +1 -1
@@ -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. */
@@ -9,6 +12,7 @@ declare module "@tanstack/start-client-core" {
9
12
  }
10
13
  declare const STATUS: {
11
14
  readonly BAD_REQUEST: 400;
15
+ readonly INVALID_INPUT: 400;
12
16
  readonly UNAUTHORIZED: 401;
13
17
  readonly FORBIDDEN: 403;
14
18
  readonly NOT_FOUND: 404;
@@ -17,6 +21,8 @@ declare const STATUS: {
17
21
  readonly INTERNAL_ERROR: 500;
18
22
  };
19
23
  export type ZiteErrorCode = keyof typeof STATUS;
24
+ /** @internal The HTTP status each code answers with. */
25
+ export declare const ZITE_ERROR_STATUS: Readonly<Record<ZiteErrorCode, number>>;
20
26
  /** Fail with a status: `throw new ZiteError('FORBIDDEN', 'Only the owner can edit this')`. */
21
27
  export declare class ZiteError extends Error {
22
28
  readonly code: ZiteErrorCode;
@@ -4,8 +4,10 @@ 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,
10
+ INVALID_INPUT: 400,
9
11
  UNAUTHORIZED: 401,
10
12
  FORBIDDEN: 403,
11
13
  NOT_FOUND: 404,
@@ -13,6 +15,8 @@ const STATUS = {
13
15
  RATE_LIMITED: 429,
14
16
  INTERNAL_ERROR: 500,
15
17
  };
18
+ /** @internal The HTTP status each code answers with. */
19
+ export const ZITE_ERROR_STATUS = STATUS;
16
20
  /** Fail with a status: `throw new ZiteError('FORBIDDEN', 'Only the owner can edit this')`. */
17
21
  export class ZiteError extends Error {
18
22
  code;
@@ -62,6 +66,29 @@ const host = () => {
62
66
  throw new ZiteError("UNAUTHORIZED", "Missing platform context");
63
67
  return current;
64
68
  };
69
+ // TanStack fails a validator with a plain Error whose message is the issues as JSON.
70
+ const validationIssues = (error) => {
71
+ if (!(error instanceof Error) || error.constructor !== Error)
72
+ return undefined;
73
+ try {
74
+ const issues = JSON.parse(error.message);
75
+ return Array.isArray(issues) && issues.length > 0 && issues.every((issue) => typeof issue?.message === "string")
76
+ ? issues
77
+ : undefined;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ };
83
+ const invalidInput = (issues) => {
84
+ if (getHost()?.mode === "live")
85
+ return new ZiteError("INVALID_INPUT", "Invalid input");
86
+ const describe = ({ message, path = [] }) => {
87
+ const at = path.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
88
+ return at ? `${at}: ${message}` : message;
89
+ };
90
+ return new ZiteError("INVALID_INPUT", issues.map(describe).join("; "));
91
+ };
65
92
  // Server functions report a status through TanStack's error envelope; server
66
93
  // routes need a Response. A nested call's failure is the caller's to handle.
67
94
  const withStatus = async (isServerFn, fn) => {
@@ -69,7 +96,9 @@ const withStatus = async (isServerFn, fn) => {
69
96
  try {
70
97
  return await fn();
71
98
  }
72
- catch (error) {
99
+ catch (caught) {
100
+ const issues = validationIssues(caught);
101
+ const error = issues ? invalidInput(issues) : caught;
73
102
  if (!(error instanceof ZiteError))
74
103
  throw error;
75
104
  if (!isServerFn)
@@ -87,10 +116,10 @@ export const signedIn = tag(createMiddleware().server(({ next, serverFnMeta }) =
87
116
  return runInScope({ kind: "signedIn", user }, () => next({ context: { user } }));
88
117
  })), { kind: "signedIn" });
89
118
  /** Anyone, signed in or not. Handlers get `context.user: User | null`. */
90
- export const anyone = tag(createMiddleware().server(({ next }) => {
119
+ export const anyone = tag(createMiddleware().server(({ next, serverFnMeta }) => withStatus(!!serverFnMeta, async () => {
91
120
  const { user } = host();
92
121
  return runInScope({ kind: "anyone", user }, () => next({ context: { user } }));
93
- }), { kind: "anyone" });
122
+ })), { kind: "anyone" });
94
123
  /** Webhooks delivered by the platform, which already checked the URL token. */
95
124
  export const webhook = tag(createMiddleware().server(({ next }) => {
96
125
  if (host().invoke?.kind !== "webhook")
@@ -7,6 +7,6 @@ import { ZiteError } from "./index.js";
7
7
  * adapters first, then whatever the app's own `src/start.ts` sets.
8
8
  */
9
9
  export declare function createZiteStart<const TAdapters extends ReadonlyArray<AnySerializationAdapter> = [], TDefaultSsr extends SSROption = SSROption, const TRequest extends ReadonlyArray<AnyRequestMiddleware> = [], const TFunction extends ReadonlyArray<AnyFunctionMiddleware> = []>(app?: StartInstance<TAdapters, TDefaultSsr, TRequest, TFunction>): StartInstance<readonly [import("@tanstack/react-router").SerializationAdapter<ZiteError, {
10
- code: "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "INTERNAL_ERROR";
10
+ code: "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "INTERNAL_ERROR" | "INVALID_INPUT";
11
11
  message: string;
12
12
  }, never>, ...TAdapters], TDefaultSsr, readonly [import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>, ...TRequest], readonly [import("@tanstack/react-start").FunctionMiddlewareAfterClient<{}, unknown, undefined, undefined, undefined>, import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>, ...TFunction]>;
@@ -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 {};