zitejs 0.9.140 → 0.9.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/cjs/integrations/rules.d.ts +1 -0
  2. package/dist/cjs/integrations/rules.js +1 -0
  3. package/dist/cjs/start/describeBuilt.d.ts +3 -0
  4. package/dist/cjs/start/describeBuilt.js +106 -0
  5. package/dist/cjs/start/entries.d.ts +20 -0
  6. package/dist/cjs/start/entries.js +104 -0
  7. package/dist/cjs/start/problems.d.ts +19 -0
  8. package/dist/cjs/start/problems.js +11 -0
  9. package/dist/esm/integrations/rules.d.ts +1 -0
  10. package/dist/esm/integrations/rules.js +1 -0
  11. package/dist/esm/start/describe.d.ts +64 -0
  12. package/dist/esm/start/describe.js +176 -0
  13. package/dist/esm/start/describeBuilt.d.ts +3 -0
  14. package/dist/esm/start/describeBuilt.js +66 -0
  15. package/dist/esm/start/descriptions.d.ts +4 -0
  16. package/dist/esm/start/descriptions.js +47 -0
  17. package/dist/esm/start/entries.d.ts +20 -0
  18. package/dist/esm/start/entries.js +93 -0
  19. package/dist/esm/start/host.d.ts +3 -0
  20. package/dist/esm/start/host.js +4 -0
  21. package/dist/esm/start/index.d.ts +79 -0
  22. package/dist/esm/start/index.js +157 -0
  23. package/dist/esm/start/instance.d.ts +12 -0
  24. package/dist/esm/start/instance.js +73 -0
  25. package/dist/esm/start/problems.d.ts +19 -0
  26. package/dist/esm/start/problems.js +8 -0
  27. package/dist/esm/start/registration.d.ts +13 -0
  28. package/dist/esm/start/registration.js +21 -0
  29. package/dist/esm/start/router.d.ts +17 -0
  30. package/dist/esm/start/router.js +37 -0
  31. package/dist/esm/start/runLog.d.ts +5 -0
  32. package/dist/esm/start/runLog.js +18 -0
  33. package/dist/esm/start/scope.d.ts +15 -0
  34. package/dist/esm/start/scope.js +58 -0
  35. package/dist/esm/start/server.d.ts +51 -0
  36. package/dist/esm/start/server.js +215 -0
  37. package/dist/esm/start/types.d.ts +94 -0
  38. package/dist/esm/start/types.js +1 -0
  39. package/dist/esm/start/vite.d.ts +9 -0
  40. package/dist/esm/start/vite.js +144 -0
  41. package/dist/esm/start/worker.d.ts +11 -0
  42. package/dist/esm/start/worker.js +20 -0
  43. package/dist/esm/start/workflow.d.ts +50 -0
  44. package/dist/esm/start/workflow.js +13 -0
  45. package/dist/esm/start/workflow.test-d.d.ts +1 -0
  46. package/dist/esm/start/workflow.test-d.js +39 -0
  47. package/dist/esm/start/workflowModules.d.ts +1 -0
  48. package/dist/esm/start/workflowModules.js +3 -0
  49. package/dist/esm/start/workflowRun.d.ts +80 -0
  50. package/dist/esm/start/workflowRun.js +107 -0
  51. package/package.json +1 -1
  52. package/dist/cjs/meta/index.d.ts +0 -14
  53. package/dist/cjs/meta/index.js +0 -12
  54. package/dist/cjs/notifications/index.d.ts +0 -25
  55. package/dist/cjs/notifications/index.js +0 -12
  56. package/dist/esm/meta/index.d.ts +0 -14
  57. package/dist/esm/meta/index.js +0 -8
  58. package/dist/esm/notifications/index.d.ts +0 -25
  59. package/dist/esm/notifications/index.js +0 -8
@@ -0,0 +1,15 @@
1
+ import type { AccessKind, User, ZiteRequestContext } from "./types.js";
2
+ export type { AccessKind } from "./types.js";
3
+ export type AccessScope = {
4
+ kind: AccessKind;
5
+ user: User | null;
6
+ };
7
+ export declare const runInScope: <T>(scope: AccessScope, fn: () => T) => T;
8
+ /** True inside another server function's scope, i.e. a nested server-side call. */
9
+ export declare const inScope: () => boolean;
10
+ export declare class NoAccessDecisionError extends Error {
11
+ constructor(what: string);
12
+ }
13
+ export declare const requireScope: (what: string) => AccessScope;
14
+ export declare const runWithHost: <T>(host: ZiteRequestContext, fn: () => T) => T;
15
+ export declare const getHost: () => ZiteRequestContext | undefined;
@@ -0,0 +1,58 @@
1
+ // Server-only. Only zitejs's access middleware opens an access scope, and the
2
+ // platform transport refuses calls outside one, so code with no access decision
3
+ // can't reach data, integrations or email however it was written.
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { HOST_STORE_KEY } from "./host.js";
6
+ // Keyed on globalThis so two copies of zitejs in one bundle share the stores.
7
+ const stores = globalThis;
8
+ const scopeStorage = (stores[Symbol.for("zitejs:access-scope")] ??=
9
+ new AsyncLocalStorage());
10
+ const hostStorage = (stores[HOST_STORE_KEY] ??=
11
+ new AsyncLocalStorage());
12
+ const isAsyncIterable = (value) => typeof value === "object" &&
13
+ value !== null &&
14
+ Symbol.asyncIterator in value &&
15
+ !(value instanceof ReadableStream);
16
+ // TanStack resumes a streamed async generator from its serializer, outside the
17
+ // middleware's async context, so each step re-enters the scope it started in.
18
+ const rescope = (scope, source) => (async function* () {
19
+ const iterator = source[Symbol.asyncIterator]();
20
+ try {
21
+ while (true) {
22
+ const step = await scopeStorage.run(scope, () => iterator.next());
23
+ if (step.done)
24
+ return step.value;
25
+ yield step.value;
26
+ }
27
+ }
28
+ finally {
29
+ await scopeStorage.run(scope, async () => iterator.return?.());
30
+ }
31
+ })();
32
+ const rescopeResult = (scope, out) => {
33
+ const result = out?.result;
34
+ return isAsyncIterable(result) ? { ...out, result: rescope(scope, result) } : out;
35
+ };
36
+ export const runInScope = (scope, fn) => {
37
+ const out = scopeStorage.run(scope, fn);
38
+ return (out instanceof Promise
39
+ ? out.then((value) => rescopeResult(scope, value))
40
+ : rescopeResult(scope, out));
41
+ };
42
+ /** True inside another server function's scope, i.e. a nested server-side call. */
43
+ export const inScope = () => scopeStorage.getStore() !== undefined;
44
+ export class NoAccessDecisionError extends Error {
45
+ constructor(what) {
46
+ super(`${what} ran outside an access decision. Define server functions with signedInFn or publicFn, ` +
47
+ "and give server routes signedIn, anyone or webhook middleware. Loaders get data by calling a server function.");
48
+ this.name = "NoAccessDecisionError";
49
+ }
50
+ }
51
+ export const requireScope = (what) => {
52
+ const scope = scopeStorage.getStore();
53
+ if (!scope)
54
+ throw new NoAccessDecisionError(what);
55
+ return scope;
56
+ };
57
+ export const runWithHost = (host, fn) => hostStorage.run(host, fn);
58
+ export const getHost = () => hostStorage.getStore();
@@ -0,0 +1,51 @@
1
+ import type { HostProps, PlatformBinding, RenderWatch, ZiteRequestContext } from "./types.js";
2
+ export { runWithHost } from "./scope.js";
3
+ export { describeApp } from "./describe.js";
4
+ export type { HostProps, PlatformBinding, PlatformCallInput, PlatformCallResult, PlatformSession, RenderWatch, RunRecord } from "./types.js";
5
+ /**
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.
10
+ */
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;
20
+ /** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
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>;
24
+ type Fetch = (request: Request, ...rest: unknown[]) => Promise<Response> | Response;
25
+ type WorkerContext = {
26
+ waitUntil(promise: Promise<unknown>): void;
27
+ };
28
+ export type ZiteServerOptions = {
29
+ handler: {
30
+ fetch: Fetch;
31
+ };
32
+ getRouter: () => unknown;
33
+ platform?: PlatformBinding;
34
+ };
35
+ type HostedContext = WorkerContext & {
36
+ props?: Partial<HostProps>;
37
+ };
38
+ export type ZiteHandlers = {
39
+ fetch: Fetch;
40
+ /** A page the host may cache: as `fetch`, with every read and outbound fetch reported to `watch`. */
41
+ render(request: Request, ctx: HostedContext, watch: RenderWatch): Promise<Response>;
42
+ };
43
+ /**
44
+ * The handlers of a Start app's server entry. A request the host dispatched
45
+ * carries its identity and platform in props; anything else (describe under
46
+ * `vite preview`, tests) reads `x-zite-*` headers and uses `platform` if one
47
+ * is given.
48
+ */
49
+ export declare function createZiteHandlers(options: ZiteServerOptions): ZiteHandlers;
50
+ /** The fetch handler alone, for entries that take no RPC. */
51
+ export declare function createZiteFetch(options: ZiteServerOptions): Fetch;
@@ -0,0 +1,215 @@
1
+ import { ZiteError, ZITE_ERROR_STATUS } from "./index.js";
2
+ import { describeApp } from "./describe.js";
3
+ import { runRegistration } from "./registration.js";
4
+ import { preview } from "./runLog.js";
5
+ import { getHost, requireScope, runWithHost } from "./scope.js";
6
+ import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
7
+ export { runWithHost } from "./scope.js";
8
+ export { describeApp } from "./describe.js";
9
+ /**
10
+ * Routes every zitejs data, integration and email call to this request's
11
+ * platform, on one session, and only inside an access scope. The access kind
12
+ * travels with the call so the platform can record what a page read and narrow
13
+ * anonymous reads.
14
+ */
15
+ export function installPlatformTransport() {
16
+ const sdkCall = async (integrationId, className, methodName, params) => {
17
+ const scope = requireScope(`${className}.${methodName}`);
18
+ const host = getHost();
19
+ // Fails closed: no platform means no platform access, never a fallback.
20
+ if (!host?.platform)
21
+ throw new Error("The Zite platform binding is missing");
22
+ host.session ??= Promise.resolve(host.watch ? host.platform.session(host.watch) : host.platform.session());
23
+ const result = await (await host.session).call({ integrationId, className, methodName, params, access: scope.kind });
24
+ if ("value" in result)
25
+ return result.value;
26
+ const { code, message } = result.error;
27
+ throw new ZiteError(code in ZITE_ERROR_STATUS ? code : "INTERNAL_ERROR", message);
28
+ };
29
+ globalThis.__wrapSdkCall = sdkCall;
30
+ watchOutboundFetches();
31
+ }
32
+ let fetchesWatched = false;
33
+ const fetchedHost = (input) => {
34
+ try {
35
+ return new URL(input instanceof Request ? input.url : input).host;
36
+ }
37
+ catch {
38
+ return "?";
39
+ }
40
+ };
41
+ // A page that fetched depends on more than the reads the host saw, so the host
42
+ // must hear of it before the fetch goes out. Next patches fetch for the same reason.
43
+ const watchOutboundFetches = () => {
44
+ if (fetchesWatched)
45
+ return;
46
+ fetchesWatched = true;
47
+ const nativeFetch = globalThis.fetch.bind(globalThis);
48
+ globalThis.fetch = async (input, init) => {
49
+ const watch = getHost()?.watch;
50
+ if (watch)
51
+ await watch.uncacheable(`fetch ${fetchedHost(input)}`);
52
+ return nativeFetch(input, init);
53
+ };
54
+ };
55
+ const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
56
+ const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "register", "warm"]);
57
+ /** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
58
+ export function hostContextFromProps(request, props) {
59
+ return {
60
+ requestId: props.requestId,
61
+ url: request.url,
62
+ user: props.user,
63
+ mode: props.mode,
64
+ invoke: props.invoke,
65
+ runs: [],
66
+ platform: props.platform,
67
+ };
68
+ }
69
+ /**
70
+ * A request the host didn't dispatch: `zitejs describe` under `vite preview`,
71
+ * and tests, which pass their own platform. Deployed builds have no public URL,
72
+ * so in production every request carries props.
73
+ */
74
+ export function hostContextFromRequest(request, platform) {
75
+ const { headers } = request;
76
+ const user = headers.get("x-zite-user");
77
+ const [kind, name = ""] = headers.get("x-zite-invoke")?.split(":") ?? [];
78
+ return {
79
+ requestId: headers.get("x-zite-request-id") ?? crypto.randomUUID(),
80
+ url: request.url,
81
+ user: user ? decodeUser(user) : null,
82
+ mode: "live",
83
+ invoke: INVOKE_KINDS.has(kind) ? { kind: kind, name } : null,
84
+ runs: [],
85
+ platform,
86
+ };
87
+ }
88
+ /** Every server function TanStack compiled, written in by `zite()` at build. Empty in dev, where TanStack doesn't call its id hook. */
89
+ export const builtFunctionIds = () => "__ZITE_FN_IDS__".split(",").filter((id) => id && !id.startsWith("__ZITE"));
90
+ const parseDescriptions = (json) => {
91
+ try {
92
+ return JSON.parse(json);
93
+ }
94
+ catch {
95
+ return {};
96
+ }
97
+ };
98
+ /** Each server function's `description` option, written in by `zite()` at build. */
99
+ export const builtFunctionDescriptions = () => parseDescriptions("__ZITE_FN_DESCRIPTIONS__");
100
+ // Server functions also run while a streamed page renders, so the run log is
101
+ // complete only once the body has been read to the end.
102
+ const onBodyEnd = (response, done) => {
103
+ if (!response.body) {
104
+ done();
105
+ return response;
106
+ }
107
+ return new Response(response.body.pipeThrough(new TransformStream({ flush: done })), response);
108
+ };
109
+ const INVOKE_PATH = "/_zite/invoke";
110
+ // The host sends `{ input }`; a platform-fired run's input carries the runner's
111
+ // `__cron`, `__poll` or `__webhook` state, exactly as for classic endpoints.
112
+ async function invokeWorkflow(request, host, workflows) {
113
+ if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
114
+ return new Response("Not found", { status: 404 });
115
+ const name = host.invoke?.name;
116
+ const found = workflows.find((w) => w.id === name);
117
+ if (!found)
118
+ return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `No workflow named ${name}` } }, { status: 404 });
119
+ const input = (await request.json().catch(() => ({})))?.input;
120
+ const startedAt = Date.now();
121
+ const result = await runWorkflow(found.workflow, input);
122
+ host.runs.push({
123
+ fn: `workflow:${found.id}`,
124
+ name: found.id,
125
+ nested: false,
126
+ startedAt,
127
+ durationMs: Date.now() - startedAt,
128
+ input: preview(loggedInput(input)),
129
+ ...(result.ok ? { output: preview(result.output) } : { error: result.error }),
130
+ });
131
+ return Response.json(result, { status: result.ok ? 200 : 500 });
132
+ }
133
+ // The host sends `{ op, input }`: `subscribe` with the platform's notification
134
+ // URL and secret, or `unsubscribe` with what `subscribe` returned.
135
+ async function invokeRegistration(request, host, workflows) {
136
+ if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
137
+ return new Response("Not found", { status: 404 });
138
+ const name = host.invoke?.name ?? "";
139
+ const body = (await request.json().catch(() => null));
140
+ const result = await runRegistration(workflows, name, body?.op, body?.input);
141
+ if (!result) {
142
+ return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `${name} has no ${String(body?.op)}` } }, { status: 404 });
143
+ }
144
+ return Response.json(result, { status: result.ok ? 200 : 500 });
145
+ }
146
+ const isZiteError = (error) => error instanceof Error && error.name === "ZiteError" && error.code in ZITE_ERROR_STATUS;
147
+ // TanStack renders any loader error as a 500. A page refused with a ZiteError
148
+ // (a missing role, a record that isn't there) answers with that error's status.
149
+ const pageStatus = (host) => {
150
+ const matches = host.router?.state?.matches;
151
+ const failed = matches?.find((match) => match.status === "error")?.error;
152
+ return isZiteError(failed) ? ZITE_ERROR_STATUS[failed.code] : undefined;
153
+ };
154
+ /**
155
+ * The handlers of a Start app's server entry. A request the host dispatched
156
+ * carries its identity and platform in props; anything else (describe under
157
+ * `vite preview`, tests) reads `x-zite-*` headers and uses `platform` if one
158
+ * is given.
159
+ */
160
+ export function createZiteHandlers(options) {
161
+ installPlatformTransport();
162
+ // Loaded on first use, so a page request never pays for importing workflow modules.
163
+ let workflows;
164
+ const loadedWorkflows = () => (workflows ??= loadWorkflows());
165
+ const serve = async (request, ctx, watch) => {
166
+ const host = ctx?.props?.platform
167
+ ? hostContextFromProps(request, ctx.props)
168
+ : hostContextFromRequest(request, options.platform);
169
+ if (watch)
170
+ host.watch = watch;
171
+ // Starting this isolate was the point: the platform warms a build before its
172
+ // first visitors, and no app code runs.
173
+ if (host.invoke?.kind === "warm")
174
+ return new Response(null, { status: 204 });
175
+ if (host.invoke?.kind === "describe") {
176
+ return Response.json(await describeApp({
177
+ fnIds: builtFunctionIds(),
178
+ descriptions: builtFunctionDescriptions(),
179
+ getRouter: options.getRouter,
180
+ request,
181
+ workflows: await loadedWorkflows(),
182
+ }));
183
+ }
184
+ let response = await runWithHost(host, async () => host.invoke?.kind === "workflow"
185
+ ? invokeWorkflow(request, host, await loadedWorkflows())
186
+ : host.invoke?.kind === "register"
187
+ ? invokeRegistration(request, host, await loadedWorkflows())
188
+ : options.handler.fetch(request));
189
+ const status = response.status === 500 ? pageStatus(host) : undefined;
190
+ if (status)
191
+ response = new Response(response.body, { status, headers: response.headers });
192
+ return onBodyEnd(response, () => {
193
+ const { platform } = host;
194
+ if (!host.runs.length || !platform)
195
+ return;
196
+ const batch = { runs: host.runs };
197
+ // On the request's session when it has one: one call, not another binding round trip.
198
+ const flushed = (async () => {
199
+ const session = host.session ? await host.session : undefined;
200
+ await (session?.runs ? session.runs(batch) : platform.runs(batch));
201
+ })().catch((error) => {
202
+ console.error("[zite] couldn't record runs", error);
203
+ });
204
+ ctx?.waitUntil(flushed);
205
+ });
206
+ };
207
+ return {
208
+ fetch: (request, ...rest) => serve(request, rest[1]),
209
+ render: (request, ctx, watch) => serve(request, ctx, watch),
210
+ };
211
+ }
212
+ /** The fetch handler alone, for entries that take no RPC. */
213
+ export function createZiteFetch(options) {
214
+ return createZiteHandlers(options).fetch;
215
+ }
@@ -0,0 +1,94 @@
1
+ export type User = {
2
+ id: string;
3
+ email: string;
4
+ name: string;
5
+ roles: ReadonlyArray<string>;
6
+ };
7
+ /** One server function call, as the platform's run log shows it. Input and output are truncated JSON. */
8
+ export type RunRecord = {
9
+ fn: string;
10
+ name?: string;
11
+ nested: boolean;
12
+ startedAt: number;
13
+ durationMs: number;
14
+ input?: string;
15
+ output?: string;
16
+ error?: {
17
+ code: string;
18
+ message: string;
19
+ };
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
+ /** As `PlatformBinding.runs`, on a session the request already holds. Hosts before 0.10.0-start.4 lack it. */
46
+ runs?(batch: {
47
+ runs: RunRecord[];
48
+ }): Promise<void>;
49
+ };
50
+ /**
51
+ * The host's ear on a page render it may cache, from `render()`. Reports can
52
+ * only make the page less cacheable, so an app gains nothing by sending them.
53
+ */
54
+ export type RenderWatch = {
55
+ uncacheable(reason: string): Promise<void>;
56
+ };
57
+ /** What the host hands each request: its platform, already scoped to the app, build, request and user. */
58
+ export type PlatformBinding = {
59
+ /** Every platform call of one request goes through one session. A render's session reports what it read to `watch`. */
60
+ session(watch?: RenderWatch): PlatformSession | Promise<PlatformSession>;
61
+ /** One batch per request, after its response has ended. */
62
+ runs(batch: {
63
+ runs: RunRecord[];
64
+ }): Promise<void>;
65
+ };
66
+ export type HostMode = "live" | "preview" | "validate";
67
+ export type HostInvoke = {
68
+ kind: "webhook" | "describe" | "workflow" | "register" | "warm";
69
+ name: string;
70
+ };
71
+ /** The per-request props the host dispatches with. Only the host can set them. */
72
+ export type HostProps = {
73
+ platform: PlatformBinding;
74
+ requestId: string;
75
+ user: User | null;
76
+ mode: HostMode;
77
+ invoke: HostInvoke | null;
78
+ };
79
+ /** What the host resolved for this request. Set by the generated server entry, never by the browser. */
80
+ export type ZiteRequestContext = {
81
+ requestId: string;
82
+ url: string;
83
+ user: User | null;
84
+ mode: HostMode;
85
+ invoke: HostInvoke | null;
86
+ /** Filled by run capture, flushed to the platform once the response ends. */
87
+ runs: RunRecord[];
88
+ platform?: PlatformBinding;
89
+ session?: Promise<PlatformSession>;
90
+ /** Set when the host renders a page it may cache. */
91
+ watch?: RenderWatch;
92
+ /** This request's router, so a page's status can follow the error it rendered. */
93
+ router?: unknown;
94
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { PluginOption } from "vite";
2
+ export type ZiteOptions = {
3
+ /** A server entry the caller owns, relative to the app root. By default zite() writes the platform's. */
4
+ serverEntry?: string;
5
+ /** Build for Cloudflare Workers, the target the platform deploys. Defaults to true; false builds for Node. */
6
+ cloudflare?: boolean;
7
+ };
8
+ /** The one plugin a Zite app's `vite.config.ts` uses. */
9
+ export declare function zite(options?: ZiteOptions): PluginOption[];
@@ -0,0 +1,144 @@
1
+ import path from "node:path";
2
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3
+ import viteReact from "@vitejs/plugin-react";
4
+ import { ziteId } from "../vite/index.js";
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.
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$/;
12
+ // Platform-pinned Worker settings. The deploy uses its own fixed template, so
13
+ // these only have to match it; apps don't carry a wrangler config.
14
+ const WORKER_COMPATIBILITY_DATE = "2026-09-25";
15
+ const WORKER_COMPATIBILITY_FLAGS = ["nodejs_compat"];
16
+ // Resolved from the app at build time; zitejs doesn't depend on it.
17
+ const loadCloudflare = async (main) => {
18
+ const { cloudflare } = (await import("@cloudflare/vite-plugin"));
19
+ return cloudflare({
20
+ viteEnvironment: { name: "ssr" },
21
+ config: {
22
+ name: "zite-app",
23
+ main,
24
+ compatibility_date: WORKER_COMPATIBILITY_DATE,
25
+ compatibility_flags: WORKER_COMPATIBILITY_FLAGS,
26
+ },
27
+ });
28
+ };
29
+ /** The one plugin a Zite app's `vite.config.ts` uses. */
30
+ export function zite(options = {}) {
31
+ const serverEntry = options.serverEntry ?? START_SERVER_ENTRY;
32
+ // Platform-owned, so they're rewritten whenever they differ. Sandboxes run the
33
+ // image's zitejs for `generate`, which may predate Start; the plugin can't.
34
+ const appDir = process.cwd();
35
+ if (!options.serverEntry) {
36
+ writeIfChanged(appDir, START_SERVER_ENTRY, renderStartServerEntry(options.cloudflare === false ? "node" : "cloudflare"));
37
+ }
38
+ writeIfChanged(appDir, START_INSTANCE_ENTRY, renderStartInstance(hasAppStart(appDir)));
39
+ // Every server function TanStack's compiler found, from its own hook, by id.
40
+ const ids = new Map();
41
+ const compileZitejs = {
42
+ name: "zite:compile-zitejs",
43
+ // The factories must go through TanStack's compiler in both environments.
44
+ // The peer dependency gets zitejs crawled already; this keeps it true if an
45
+ // install drops that. A Node build bundles everything, so externalized
46
+ // libraries can't load the root's React 18 renderer.
47
+ config: (config) => {
48
+ const root = path.resolve(config.root ?? ".");
49
+ const src = `${path.join(root, "src")}/`;
50
+ return {
51
+ ssr: { noExternal: options.cloudflare === false ? true : ["zitejs"] },
52
+ optimizeDeps: { exclude: ["zitejs"] },
53
+ resolve: {
54
+ // `@/` as in classic apps and shadcn, `~/` as in TanStack's templates.
55
+ alias: [
56
+ { find: /^@\//, replacement: src },
57
+ { find: /^~\//, replacement: src },
58
+ // The workspace SDKs `zitejs generate` writes, as classic apps map them.
59
+ { find: /^zitejs\/db$/, replacement: path.join(root, "../../.zite/db.ts") },
60
+ { find: /^zitejs\/integrations$/, replacement: path.join(root, ".zite/integrations/index.ts") },
61
+ { find: /^zitejs\/email$/, replacement: path.join(root, ".zite/integrations/email.ts") },
62
+ { find: /^zitejs\/meta$/, replacement: path.join(root, ".zite/meta.ts") },
63
+ ],
64
+ // Shared workspace libraries resolve from the root, where classic apps
65
+ // keep React 18; recharts 2 needs the app's react-is 19 or drops series.
66
+ dedupe: ["react-is"],
67
+ },
68
+ };
69
+ },
70
+ // Each new isolate loads the whole server bundle, so its size is cold-start
71
+ // time; Vite leaves SSR output unminified.
72
+ configEnvironment: (name, config) => name === "ssr" && options.cloudflare !== false && config.build?.minify === undefined ? { build: { minify: true } } : undefined,
73
+ };
74
+ // Each server function's `description` option, by name, for the editor.
75
+ const descriptions = new Map();
76
+ const srcDir = `${path.join(appDir, "src")}${path.sep}`;
77
+ const functionIds = {
78
+ name: "zite:function-ids",
79
+ apply: "build",
80
+ transform(code, id) {
81
+ if (this.environment?.config.consumer !== "server" || !id.startsWith(srcDir) || !code.includes("description"))
82
+ return;
83
+ try {
84
+ for (const [name, text] of factoryDescriptions(this.parse(code)))
85
+ descriptions.set(name, text);
86
+ }
87
+ catch {
88
+ // Display-only: an unreadable module just shows no descriptions.
89
+ }
90
+ },
91
+ generateBundle(_, bundle) {
92
+ const described = Object.fromEntries([...descriptions].filter(([name]) => ids.has(name)));
93
+ for (const chunk of Object.values(bundle)) {
94
+ if (chunk.type !== "chunk")
95
+ continue;
96
+ if (chunk.code.includes(FN_IDS_TOKEN))
97
+ chunk.code = chunk.code.replace(FN_IDS_TOKEN, [...ids.keys()].join(","));
98
+ chunk.code = chunk.code.replace(FN_DESCRIPTIONS_LITERAL, () => JSON.stringify(JSON.stringify(described)));
99
+ }
100
+ },
101
+ };
102
+ const workflowRegistry = {
103
+ name: "zite:workflow-registry",
104
+ load: (id) => (WORKFLOW_REGISTRY.test(id.split("?")[0]) ? renderWorkflowModules(appDir, workflowFiles(appDir)) : undefined),
105
+ };
106
+ return [
107
+ tanstackStart({
108
+ // TanStack resolves these from `src/`.
109
+ server: { entry: `../${serverEntry.replace(/\.ts$/, "")}` },
110
+ start: { entry: `../${START_INSTANCE_ENTRY.replace(/\.ts$/, "")}` },
111
+ serverFns: {
112
+ generateFunctionId: ({ filename, functionName }) => {
113
+ const id = functionName.replace(/_createServerFn_handler$/, "");
114
+ if (/(^|\/)node_modules\//.test(filename)) {
115
+ throw new Error(`[zite] ${filename} defines server function ${id}; dependencies can't ship server functions`);
116
+ }
117
+ const existing = ids.get(id);
118
+ if (existing && existing !== filename) {
119
+ throw new Error(`[zite] server function name ${id} is used in both ${existing} and ${filename}`);
120
+ }
121
+ ids.set(id, filename);
122
+ return id;
123
+ },
124
+ },
125
+ importProtection: {
126
+ behavior: "error",
127
+ client: {
128
+ // Appended to TanStack's own.
129
+ specifiers: ["zitejs/start/server", "zitejs/start/worker", "zitejs/start/describe", "zitejs/runtime"],
130
+ // Replaces TanStack's list, so its `**/*.server.*` is restated. File
131
+ // patterns also catch `zitejs/db` and `zitejs/integrations`, which the
132
+ // aliases above rewrite before specifiers are matched.
133
+ files: ["**/*.server.*", "**/.zite/db.ts", "**/.zite/integrations/**"],
134
+ },
135
+ },
136
+ }),
137
+ options.cloudflare !== false ? loadCloudflare(serverEntry) : null,
138
+ viteReact(),
139
+ ziteId(),
140
+ compileZitejs,
141
+ functionIds,
142
+ workflowRegistry,
143
+ ];
144
+ }
@@ -0,0 +1,11 @@
1
+ import { type ZiteServerOptions } from "./server.js";
2
+ import type { RenderWatch } from "./types.js";
3
+ export type ZiteWorker = new (ctx: unknown, env: unknown) => {
4
+ fetch(request: Request): Promise<Response>;
5
+ render(request: Request, watch: RenderWatch): Promise<Response>;
6
+ };
7
+ /**
8
+ * A Start app's Worker. The host sends most requests to `fetch`, and a page it
9
+ * may cache to `render` over RPC, with a watch for what the page depends on.
10
+ */
11
+ export declare function createZiteWorker(options: ZiteServerOptions): ZiteWorker;
@@ -0,0 +1,20 @@
1
+ // For the generated server entry of a Workers build only. Apps never import this.
2
+ import { WorkerEntrypoint } from "cloudflare:workers";
3
+ import { createZiteHandlers } from "./server.js";
4
+ /**
5
+ * A Start app's Worker. The host sends most requests to `fetch`, and a page it
6
+ * may cache to `render` over RPC, with a watch for what the page depends on.
7
+ */
8
+ export function createZiteWorker(options) {
9
+ const handlers = createZiteHandlers(options);
10
+ return class extends WorkerEntrypoint {
11
+ async fetch(request) {
12
+ return handlers.fetch(request, this.env, this.ctx);
13
+ }
14
+ // An argument's stubs are disposed when the call returns, and a streamed
15
+ // page can still read after that.
16
+ async render(request, watch) {
17
+ return handlers.render(request, this.ctx, watch.dup?.() ?? watch);
18
+ }
19
+ };
20
+ }