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.
- package/dist/cjs/check/index.js +81 -3
- package/dist/cjs/start/entries.d.ts +5 -0
- package/dist/cjs/start/entries.js +32 -3
- package/dist/cjs/start/problems.d.ts +19 -0
- package/dist/cjs/start/problems.js +11 -0
- package/dist/esm/check/index.js +48 -3
- package/dist/esm/start/describe.d.ts +16 -0
- package/dist/esm/start/describe.js +29 -4
- package/dist/esm/start/descriptions.d.ts +4 -0
- package/dist/esm/start/descriptions.js +47 -0
- package/dist/esm/start/entries.d.ts +5 -0
- package/dist/esm/start/entries.js +30 -4
- package/dist/esm/start/index.d.ts +6 -0
- package/dist/esm/start/index.js +32 -3
- package/dist/esm/start/instance.d.ts +1 -1
- package/dist/esm/start/instance.js +1 -17
- package/dist/esm/start/problems.d.ts +19 -0
- package/dist/esm/start/problems.js +8 -0
- package/dist/esm/start/router.js +3 -0
- package/dist/esm/start/runLog.d.ts +5 -0
- package/dist/esm/start/runLog.js +18 -0
- package/dist/esm/start/scope.d.ts +2 -2
- package/dist/esm/start/server.d.ts +21 -31
- package/dist/esm/start/server.js +115 -37
- package/dist/esm/start/types.d.ts +53 -4
- package/dist/esm/start/vite.js +33 -4
- package/dist/esm/start/workflow.d.ts +36 -0
- package/dist/esm/start/workflow.js +13 -0
- package/dist/esm/start/workflow.test-d.d.ts +1 -0
- package/dist/esm/start/workflow.test-d.js +24 -0
- package/dist/esm/start/workflowModules.d.ts +1 -0
- package/dist/esm/start/workflowModules.js +3 -0
- package/dist/esm/start/workflowRun.d.ts +80 -0
- package/dist/esm/start/workflowRun.js +107 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/esm/start/router.js
CHANGED
|
@@ -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) };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { User, ZiteRequestContext } from "./types.js";
|
|
2
|
-
export type AccessKind
|
|
1
|
+
import type { AccessKind, User, ZiteRequestContext } from "./types.js";
|
|
2
|
+
export type { AccessKind } from "./types.js";
|
|
3
3
|
export type AccessScope = {
|
|
4
4
|
kind: AccessKind;
|
|
5
5
|
user: User | null;
|
|
@@ -1,42 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { RunRecord, ZiteRequestContext } from "./types.js";
|
|
1
|
+
import type { HostProps, PlatformBinding, ZiteRequestContext } from "./types.js";
|
|
3
2
|
export { runWithHost } from "./scope.js";
|
|
4
3
|
export { describeApp } from "./describe.js";
|
|
5
|
-
export type { RunRecord } from "./types.js";
|
|
6
|
-
export type PlatformCallInput = {
|
|
7
|
-
integrationId: string;
|
|
8
|
-
className: string;
|
|
9
|
-
methodName: string;
|
|
10
|
-
params: unknown;
|
|
11
|
-
access: AccessKind;
|
|
12
|
-
userId: string | null;
|
|
13
|
-
requestId: string;
|
|
14
|
-
};
|
|
15
|
-
/** The `ZITE` service binding the host's Platform entrypoint gives every app script. */
|
|
16
|
-
export type PlatformBinding = {
|
|
17
|
-
call(call: PlatformCallInput): Promise<unknown>;
|
|
18
|
-
/** One batch per request, after its response has ended. */
|
|
19
|
-
runs(batch: {
|
|
20
|
-
requestId: string;
|
|
21
|
-
runs: RunRecord[];
|
|
22
|
-
}): Promise<void>;
|
|
23
|
-
};
|
|
4
|
+
export type { HostProps, PlatformBinding, PlatformCallInput, PlatformCallResult, PlatformSession, RunRecord } from "./types.js";
|
|
24
5
|
/**
|
|
25
|
-
* Routes every zitejs data, integration and email call
|
|
26
|
-
* inside an access scope. The access kind
|
|
27
|
-
* can record what a page read and
|
|
6
|
+
* Routes every zitejs data, integration and email call to this request's
|
|
7
|
+
* platform, on one session, and only inside an access scope. The access kind
|
|
8
|
+
* travels with the call so the platform can record what a page read and narrow
|
|
9
|
+
* anonymous reads.
|
|
28
10
|
*/
|
|
29
|
-
export declare function installPlatformTransport(
|
|
30
|
-
/**
|
|
31
|
-
export declare function
|
|
11
|
+
export declare function installPlatformTransport(): void;
|
|
12
|
+
/** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
|
|
13
|
+
export declare function hostContextFromProps(request: Request, props: HostProps): ZiteRequestContext;
|
|
14
|
+
/**
|
|
15
|
+
* A request the host didn't dispatch: `zitejs describe` under `vite preview`,
|
|
16
|
+
* and tests, which pass their own platform. Deployed builds have no public URL,
|
|
17
|
+
* so in production every request carries props.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hostContextFromRequest(request: Request, platform?: PlatformBinding): ZiteRequestContext;
|
|
32
20
|
/** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
|
|
33
21
|
export declare const builtFunctionIds: () => string[];
|
|
22
|
+
/** Each server function's `description` option, written in by `zite()` at build. */
|
|
23
|
+
export declare const builtFunctionDescriptions: () => Record<string, string>;
|
|
34
24
|
type Fetch = (request: Request, ...rest: unknown[]) => Promise<Response> | Response;
|
|
35
25
|
/**
|
|
36
|
-
* The fetch handler of a Start app's server entry.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
26
|
+
* The fetch handler of a Start app's server entry. A request the host
|
|
27
|
+
* dispatched carries its identity and platform in props; anything else (describe
|
|
28
|
+
* under `vite preview`, tests) reads `x-zite-*` headers and uses `platform`
|
|
29
|
+
* if one is given.
|
|
40
30
|
*/
|
|
41
31
|
export declare function createZiteFetch(options: {
|
|
42
32
|
handler: {
|
package/dist/esm/start/server.js
CHANGED
|
@@ -1,30 +1,52 @@
|
|
|
1
|
+
import { ZiteError, ZITE_ERROR_STATUS } from "./index.js";
|
|
1
2
|
import { describeApp } from "./describe.js";
|
|
3
|
+
import { preview } from "./runLog.js";
|
|
2
4
|
import { getHost, requireScope, runWithHost } from "./scope.js";
|
|
5
|
+
import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
|
|
3
6
|
export { runWithHost } from "./scope.js";
|
|
4
7
|
export { describeApp } from "./describe.js";
|
|
5
8
|
/**
|
|
6
|
-
* Routes every zitejs data, integration and email call
|
|
7
|
-
* inside an access scope. The access kind
|
|
8
|
-
* can record what a page read and
|
|
9
|
+
* Routes every zitejs data, integration and email call to this request's
|
|
10
|
+
* platform, on one session, and only inside an access scope. The access kind
|
|
11
|
+
* travels with the call so the platform can record what a page read and narrow
|
|
12
|
+
* anonymous reads.
|
|
9
13
|
*/
|
|
10
|
-
export function installPlatformTransport(
|
|
11
|
-
const sdkCall = (integrationId, className, methodName, params) => {
|
|
14
|
+
export function installPlatformTransport() {
|
|
15
|
+
const sdkCall = async (integrationId, className, methodName, params) => {
|
|
12
16
|
const scope = requireScope(`${className}.${methodName}`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
17
|
+
const host = getHost();
|
|
18
|
+
// Fails closed: no platform means no platform access, never a fallback.
|
|
19
|
+
if (!host?.platform)
|
|
20
|
+
throw new Error("The Zite platform binding is missing");
|
|
21
|
+
host.session ??= Promise.resolve(host.platform.session());
|
|
22
|
+
const result = await (await host.session).call({ integrationId, className, methodName, params, access: scope.kind });
|
|
23
|
+
if ("value" in result)
|
|
24
|
+
return result.value;
|
|
25
|
+
const { code, message } = result.error;
|
|
26
|
+
throw new ZiteError(code in ZITE_ERROR_STATUS ? code : "INTERNAL_ERROR", message);
|
|
22
27
|
};
|
|
23
28
|
globalThis.__wrapSdkCall = sdkCall;
|
|
24
29
|
}
|
|
25
30
|
const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "warm"]);
|
|
32
|
+
/** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
|
|
33
|
+
export function hostContextFromProps(request, props) {
|
|
34
|
+
return {
|
|
35
|
+
requestId: props.requestId,
|
|
36
|
+
url: request.url,
|
|
37
|
+
user: props.user,
|
|
38
|
+
mode: props.mode,
|
|
39
|
+
invoke: props.invoke,
|
|
40
|
+
runs: [],
|
|
41
|
+
platform: props.platform,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A request the host didn't dispatch: `zitejs describe` under `vite preview`,
|
|
46
|
+
* and tests, which pass their own platform. Deployed builds have no public URL,
|
|
47
|
+
* so in production every request carries props.
|
|
48
|
+
*/
|
|
49
|
+
export function hostContextFromRequest(request, platform) {
|
|
28
50
|
const { headers } = request;
|
|
29
51
|
const user = headers.get("x-zite-user");
|
|
30
52
|
const [kind, name = ""] = headers.get("x-zite-invoke")?.split(":") ?? [];
|
|
@@ -32,12 +54,24 @@ export function hostContextFromRequest(request) {
|
|
|
32
54
|
requestId: headers.get("x-zite-request-id") ?? crypto.randomUUID(),
|
|
33
55
|
url: request.url,
|
|
34
56
|
user: user ? decodeUser(user) : null,
|
|
35
|
-
|
|
57
|
+
mode: "live",
|
|
58
|
+
invoke: INVOKE_KINDS.has(kind) ? { kind: kind, name } : null,
|
|
36
59
|
runs: [],
|
|
60
|
+
platform,
|
|
37
61
|
};
|
|
38
62
|
}
|
|
39
63
|
/** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
|
|
40
64
|
export const builtFunctionIds = () => "__ZITE_FN_IDS__".split(",").filter((id) => id && !id.startsWith("__ZITE"));
|
|
65
|
+
const parseDescriptions = (json) => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(json);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/** Each server function's `description` option, written in by `zite()` at build. */
|
|
74
|
+
export const builtFunctionDescriptions = () => parseDescriptions("__ZITE_FN_DESCRIPTIONS__");
|
|
41
75
|
// Server functions also run while a streamed page renders, so the run log is
|
|
42
76
|
// complete only once the body has been read to the end.
|
|
43
77
|
const onBodyEnd = (response, done) => {
|
|
@@ -47,35 +81,79 @@ const onBodyEnd = (response, done) => {
|
|
|
47
81
|
}
|
|
48
82
|
return new Response(response.body.pipeThrough(new TransformStream({ flush: done })), response);
|
|
49
83
|
};
|
|
84
|
+
const INVOKE_PATH = "/_zite/invoke";
|
|
85
|
+
// The host sends `{ input }`; a platform-fired run's input carries the runner's
|
|
86
|
+
// `__cron`, `__poll` or `__webhook` state, exactly as for classic endpoints.
|
|
87
|
+
async function invokeWorkflow(request, host, workflows) {
|
|
88
|
+
if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
|
|
89
|
+
return new Response("Not found", { status: 404 });
|
|
90
|
+
const name = host.invoke?.name;
|
|
91
|
+
const found = workflows.find((w) => w.id === name);
|
|
92
|
+
if (!found)
|
|
93
|
+
return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `No workflow named ${name}` } }, { status: 404 });
|
|
94
|
+
const input = (await request.json().catch(() => ({})))?.input;
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
const result = await runWorkflow(found.workflow, input);
|
|
97
|
+
host.runs.push({
|
|
98
|
+
fn: `workflow:${found.id}`,
|
|
99
|
+
name: found.id,
|
|
100
|
+
nested: false,
|
|
101
|
+
startedAt,
|
|
102
|
+
durationMs: Date.now() - startedAt,
|
|
103
|
+
input: preview(loggedInput(input)),
|
|
104
|
+
...(result.ok ? { output: preview(result.output) } : { error: result.error }),
|
|
105
|
+
});
|
|
106
|
+
return Response.json(result, { status: result.ok ? 200 : 500 });
|
|
107
|
+
}
|
|
108
|
+
const isZiteError = (error) => error instanceof Error && error.name === "ZiteError" && error.code in ZITE_ERROR_STATUS;
|
|
109
|
+
// TanStack renders any loader error as a 500. A page refused with a ZiteError
|
|
110
|
+
// (a missing role, a record that isn't there) answers with that error's status.
|
|
111
|
+
const pageStatus = (host) => {
|
|
112
|
+
const matches = host.router?.state?.matches;
|
|
113
|
+
const failed = matches?.find((match) => match.status === "error")?.error;
|
|
114
|
+
return isZiteError(failed) ? ZITE_ERROR_STATUS[failed.code] : undefined;
|
|
115
|
+
};
|
|
50
116
|
/**
|
|
51
|
-
* The fetch handler of a Start app's server entry.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
117
|
+
* The fetch handler of a Start app's server entry. A request the host
|
|
118
|
+
* dispatched carries its identity and platform in props; anything else (describe
|
|
119
|
+
* under `vite preview`, tests) reads `x-zite-*` headers and uses `platform`
|
|
120
|
+
* if one is given.
|
|
55
121
|
*/
|
|
56
122
|
export function createZiteFetch(options) {
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw new Error("The Zite platform binding is missing");
|
|
62
|
-
return binding;
|
|
63
|
-
};
|
|
64
|
-
installPlatformTransport((call) => platform().call(call));
|
|
123
|
+
installPlatformTransport();
|
|
124
|
+
// Loaded on first use, so a page request never pays for importing workflow modules.
|
|
125
|
+
let workflows;
|
|
126
|
+
const loadedWorkflows = () => (workflows ??= loadWorkflows());
|
|
65
127
|
return async (request, ...rest) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
128
|
+
const ctx = rest[1];
|
|
129
|
+
const host = ctx?.props?.platform
|
|
130
|
+
? hostContextFromProps(request, ctx.props)
|
|
131
|
+
: hostContextFromRequest(request, options.platform);
|
|
132
|
+
// Starting this isolate was the point: the platform warms a build before its
|
|
133
|
+
// first visitors, and no app code runs.
|
|
134
|
+
if (host.invoke?.kind === "warm")
|
|
135
|
+
return new Response(null, { status: 204 });
|
|
136
|
+
if (host.invoke?.kind === "describe") {
|
|
137
|
+
return Response.json(await describeApp({
|
|
138
|
+
fnIds: builtFunctionIds(),
|
|
139
|
+
descriptions: builtFunctionDescriptions(),
|
|
140
|
+
getRouter: options.getRouter,
|
|
141
|
+
request,
|
|
142
|
+
workflows: await loadedWorkflows(),
|
|
143
|
+
}));
|
|
69
144
|
}
|
|
70
|
-
|
|
71
|
-
const
|
|
145
|
+
let response = await runWithHost(host, async () => host.invoke?.kind === "workflow" ? invokeWorkflow(request, host, await loadedWorkflows()) : options.handler.fetch(request));
|
|
146
|
+
const status = response.status === 500 ? pageStatus(host) : undefined;
|
|
147
|
+
if (status)
|
|
148
|
+
response = new Response(response.body, { status, headers: response.headers });
|
|
72
149
|
return onBodyEnd(response, () => {
|
|
73
|
-
|
|
150
|
+
const { platform } = host;
|
|
151
|
+
if (!host.runs.length || !platform)
|
|
74
152
|
return;
|
|
75
|
-
const flushed =
|
|
153
|
+
const flushed = platform.runs({ runs: host.runs }).catch((error) => {
|
|
76
154
|
console.error("[zite] couldn't record runs", error);
|
|
77
155
|
});
|
|
78
|
-
|
|
156
|
+
ctx?.waitUntil(flushed);
|
|
79
157
|
});
|
|
80
158
|
};
|
|
81
159
|
}
|
|
@@ -18,15 +18,64 @@ export type RunRecord = {
|
|
|
18
18
|
message: string;
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
|
+
export type AccessKind = "signedIn" | "anyone" | "webhook" | "workflow";
|
|
22
|
+
/** One data, integration or email call. Who is calling is the platform's to know, never the app's to say. */
|
|
23
|
+
export type PlatformCallInput = {
|
|
24
|
+
integrationId: string;
|
|
25
|
+
className: string;
|
|
26
|
+
methodName: string;
|
|
27
|
+
params: unknown;
|
|
28
|
+
/** The access decision the call ran under; the platform may narrow on it but never widens. */
|
|
29
|
+
access: AccessKind;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* A call's answer. Errors come back as values: Workers RPC keeps a thrown
|
|
33
|
+
* error's message but drops its code.
|
|
34
|
+
*/
|
|
35
|
+
export type PlatformCallResult = {
|
|
36
|
+
value: unknown;
|
|
37
|
+
} | {
|
|
38
|
+
error: {
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type PlatformSession = {
|
|
44
|
+
call(call: PlatformCallInput): Promise<PlatformCallResult>;
|
|
45
|
+
};
|
|
46
|
+
/** What the host hands each request: its platform, already scoped to the app, build, request and user. */
|
|
47
|
+
export type PlatformBinding = {
|
|
48
|
+
/** Every platform call of one request goes through one session. */
|
|
49
|
+
session(): PlatformSession | Promise<PlatformSession>;
|
|
50
|
+
/** One batch per request, after its response has ended. */
|
|
51
|
+
runs(batch: {
|
|
52
|
+
runs: RunRecord[];
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
export type HostMode = "live" | "preview" | "validate";
|
|
56
|
+
export type HostInvoke = {
|
|
57
|
+
kind: "webhook" | "describe" | "workflow" | "warm";
|
|
58
|
+
name: string;
|
|
59
|
+
};
|
|
60
|
+
/** The per-request props the host dispatches with. Only the host can set them. */
|
|
61
|
+
export type HostProps = {
|
|
62
|
+
platform: PlatformBinding;
|
|
63
|
+
requestId: string;
|
|
64
|
+
user: User | null;
|
|
65
|
+
mode: HostMode;
|
|
66
|
+
invoke: HostInvoke | null;
|
|
67
|
+
};
|
|
21
68
|
/** What the host resolved for this request. Set by the generated server entry, never by the browser. */
|
|
22
69
|
export type ZiteRequestContext = {
|
|
23
70
|
requestId: string;
|
|
24
71
|
url: string;
|
|
25
72
|
user: User | null;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
name: string;
|
|
29
|
-
} | null;
|
|
73
|
+
mode: HostMode;
|
|
74
|
+
invoke: HostInvoke | null;
|
|
30
75
|
/** Filled by run capture, flushed to the platform once the response ends. */
|
|
31
76
|
runs: RunRecord[];
|
|
77
|
+
platform?: PlatformBinding;
|
|
78
|
+
session?: Promise<PlatformSession>;
|
|
79
|
+
/** This request's router, so a page's status can follow the error it rendered. */
|
|
80
|
+
router?: unknown;
|
|
32
81
|
};
|
package/dist/esm/start/vite.js
CHANGED
|
@@ -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 {
|
|
6
|
-
|
|
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";
|
|
@@ -62,18 +66,42 @@ export function zite(options = {}) {
|
|
|
62
66
|
},
|
|
63
67
|
};
|
|
64
68
|
},
|
|
69
|
+
// Each new isolate loads the whole server bundle, so its size is cold-start
|
|
70
|
+
// time; Vite leaves SSR output unminified.
|
|
71
|
+
configEnvironment: (name, config) => name === "ssr" && options.cloudflare !== false && config.build?.minify === undefined ? { build: { minify: true } } : undefined,
|
|
65
72
|
};
|
|
73
|
+
// Each server function's `description` option, by name, for the editor.
|
|
74
|
+
const descriptions = new Map();
|
|
75
|
+
const srcDir = `${path.join(appDir, "src")}${path.sep}`;
|
|
66
76
|
const functionIds = {
|
|
67
77
|
name: "zite:function-ids",
|
|
68
78
|
apply: "build",
|
|
79
|
+
transform(code, id) {
|
|
80
|
+
if (this.environment?.config.consumer !== "server" || !id.startsWith(srcDir) || !code.includes("description"))
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
for (const [name, text] of factoryDescriptions(this.parse(code)))
|
|
84
|
+
descriptions.set(name, text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Display-only: an unreadable module just shows no descriptions.
|
|
88
|
+
}
|
|
89
|
+
},
|
|
69
90
|
generateBundle(_, bundle) {
|
|
91
|
+
const described = Object.fromEntries([...descriptions].filter(([name]) => ids.has(name)));
|
|
70
92
|
for (const chunk of Object.values(bundle)) {
|
|
71
|
-
if (chunk.type
|
|
93
|
+
if (chunk.type !== "chunk")
|
|
94
|
+
continue;
|
|
95
|
+
if (chunk.code.includes(FN_IDS_TOKEN))
|
|
72
96
|
chunk.code = chunk.code.replace(FN_IDS_TOKEN, [...ids.keys()].join(","));
|
|
73
|
-
|
|
97
|
+
chunk.code = chunk.code.replace(FN_DESCRIPTIONS_LITERAL, () => JSON.stringify(JSON.stringify(described)));
|
|
74
98
|
}
|
|
75
99
|
},
|
|
76
100
|
};
|
|
101
|
+
const workflowRegistry = {
|
|
102
|
+
name: "zite:workflow-registry",
|
|
103
|
+
load: (id) => (WORKFLOW_REGISTRY.test(id.split("?")[0]) ? renderWorkflowModules(appDir, workflowFiles(appDir)) : undefined),
|
|
104
|
+
};
|
|
77
105
|
return [
|
|
78
106
|
tanstackStart({
|
|
79
107
|
// TanStack resolves these from `src/`.
|
|
@@ -110,5 +138,6 @@ export function zite(options = {}) {
|
|
|
110
138
|
ziteId(),
|
|
111
139
|
compileZitejs,
|
|
112
140
|
functionIds,
|
|
141
|
+
workflowRegistry,
|
|
113
142
|
];
|
|
114
143
|
}
|
|
@@ -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,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;
|