zitejs 0.10.0-start.3 → 0.10.0-start.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
1
  import type { SourceDir } from "../sourceRoots.js";
2
2
  export declare const START_SERVER_ENTRY = ".zite/server.ts";
3
3
  export declare const START_INSTANCE_ENTRY = ".zite/start.ts";
4
- /** The server entry every Start app builds with. Platform-owned: regenerated, never edited. */
5
- export declare const renderStartServerEntry: () => string;
4
+ export type StartTarget = "cloudflare" | "node";
5
+ /**
6
+ * The server entry every Start app builds with. Platform-owned: regenerated,
7
+ * never edited. On Workers it's an entrypoint the host can also call over RPC.
8
+ */
9
+ export declare const renderStartServerEntry: (target?: StartTarget) => string;
6
10
  /** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
7
11
  export declare const renderStartInstance: (hasAppStart: boolean) => string;
8
12
  export declare const hasAppStart: (appDir: string) => boolean;
@@ -8,8 +8,19 @@ const node_path_1 = require("node:path");
8
8
  exports.START_SERVER_ENTRY = ".zite/server.ts";
9
9
  exports.START_INSTANCE_ENTRY = ".zite/start.ts";
10
10
  const APP_START_FILES = ["src/start.ts", "src/start.tsx"];
11
- /** The server entry every Start app builds with. Platform-owned: regenerated, never edited. */
12
- const renderStartServerEntry = () => `// Generated by zitejs for TanStack Start apps. The platform owns this file.
11
+ /**
12
+ * The server entry every Start app builds with. Platform-owned: regenerated,
13
+ * never edited. On Workers it's an entrypoint the host can also call over RPC.
14
+ */
15
+ const renderStartServerEntry = (target = "cloudflare") => target === "cloudflare"
16
+ ? `// Generated by zitejs for TanStack Start apps. The platform owns this file.
17
+ import handler from '@tanstack/react-start/server-entry';
18
+ import { createZiteWorker } from 'zitejs/start/worker';
19
+ import { getRouter } from '../src/router';
20
+
21
+ export default createZiteWorker({ handler, getRouter });
22
+ `
23
+ : `// Generated by zitejs for TanStack Start apps. The platform owns this file.
13
24
  import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
14
25
  import { createZiteFetch } from 'zitejs/start/server';
15
26
  import { getRouter } from '../src/router';
@@ -3,6 +3,7 @@ import type { WorkflowConfig } from "./workflow.js";
3
3
  export type Access = {
4
4
  kind: "signedIn" | "anyone" | "webhook" | "none";
5
5
  roles: string[];
6
+ paused?: true;
6
7
  };
7
8
  export type DescribedFunction = {
8
9
  id: string;
@@ -16,7 +16,8 @@ const accessOf = (chain, problems) => {
16
16
  if (base.length > 1)
17
17
  problems.push(`access-conflict: ${base.join(" + ")}`);
18
18
  const kind = base.includes("webhook") ? "webhook" : base.includes("signedIn") ? "signedIn" : "anyone";
19
- return { kind, roles };
19
+ const paused = kinds.some((k) => k.kind === "webhook" && k.paused === true);
20
+ return paused ? { kind, roles, paused } : { kind, roles };
20
21
  };
21
22
  const jsonSchemaOf = (validator) => {
22
23
  if (validator && typeof validator === "object" && "_zod" in validator) {
@@ -1,8 +1,12 @@
1
1
  import type { SourceDir } from "../sourceRoots.js";
2
2
  export declare const START_SERVER_ENTRY = ".zite/server.ts";
3
3
  export declare const START_INSTANCE_ENTRY = ".zite/start.ts";
4
- /** The server entry every Start app builds with. Platform-owned: regenerated, never edited. */
5
- export declare const renderStartServerEntry: () => string;
4
+ export type StartTarget = "cloudflare" | "node";
5
+ /**
6
+ * The server entry every Start app builds with. Platform-owned: regenerated,
7
+ * never edited. On Workers it's an entrypoint the host can also call over RPC.
8
+ */
9
+ export declare const renderStartServerEntry: (target?: StartTarget) => string;
6
10
  /** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
7
11
  export declare const renderStartInstance: (hasAppStart: boolean) => string;
8
12
  export declare const hasAppStart: (appDir: string) => boolean;
@@ -3,8 +3,19 @@ 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"];
6
- /** The server entry every Start app builds with. Platform-owned: regenerated, never edited. */
7
- export const renderStartServerEntry = () => `// Generated by zitejs for TanStack Start apps. The platform owns this file.
6
+ /**
7
+ * The server entry every Start app builds with. Platform-owned: regenerated,
8
+ * never edited. On Workers it's an entrypoint the host can also call over RPC.
9
+ */
10
+ export const renderStartServerEntry = (target = "cloudflare") => target === "cloudflare"
11
+ ? `// Generated by zitejs for TanStack Start apps. The platform owns this file.
12
+ import handler from '@tanstack/react-start/server-entry';
13
+ import { createZiteWorker } from 'zitejs/start/worker';
14
+ import { getRouter } from '../src/router';
15
+
16
+ export default createZiteWorker({ handler, getRouter });
17
+ `
18
+ : `// Generated by zitejs for TanStack Start apps. The platform owns this file.
8
19
  import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
9
20
  import { createZiteFetch } from 'zitejs/start/server';
10
21
  import { getRouter } from '../src/router';
@@ -39,7 +39,10 @@ export declare const isSignInRequired: (error: unknown) => boolean;
39
39
  export declare function loginHref(redirectTo?: string): string;
40
40
  /** What a zitejs middleware means, for describe. Keyed by identity, so composing or re-exporting keeps it. */
41
41
  export type ZiteMiddlewareKind = {
42
- kind: "signedIn" | "anyone" | "webhook" | "factory";
42
+ kind: "signedIn" | "anyone" | "factory";
43
+ } | {
44
+ kind: "webhook";
45
+ paused?: true;
43
46
  } | {
44
47
  kind: "role";
45
48
  role: string;
@@ -54,8 +57,14 @@ export declare const signedIn: import("@tanstack/react-start").RequestMiddleware
54
57
  export declare const anyone: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, {
55
58
  user: User | null;
56
59
  }>;
57
- /** Webhooks delivered by the platform, which already checked the URL token. */
58
- export declare const webhook: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
60
+ /**
61
+ * Webhooks delivered by the platform, which already checked the URL token.
62
+ * `webhook.paused` keeps the URL but runs nothing: the platform answers the
63
+ * provider 200 and drops the delivery until it's switched back.
64
+ */
65
+ export declare const webhook: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined> & {
66
+ paused: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
67
+ };
59
68
  /** Signed-in users with this role. */
60
69
  export declare const role: (name: string) => import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, readonly [import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, {
61
70
  user: User;
@@ -120,12 +120,17 @@ export const anyone = tag(createMiddleware().server(({ next, serverFnMeta }) =>
120
120
  const { user } = host();
121
121
  return runInScope({ kind: "anyone", user }, () => next({ context: { user } }));
122
122
  })), { kind: "anyone" });
123
- /** Webhooks delivered by the platform, which already checked the URL token. */
124
- export const webhook = tag(createMiddleware().server(({ next }) => {
123
+ const webhookMiddleware = (paused) => tag(createMiddleware().server(({ next }) => {
125
124
  if (host().invoke?.kind !== "webhook")
126
125
  return new Response("Not found", { status: 404 });
127
126
  return runInScope({ kind: "webhook", user: null }, () => next());
128
- }), { kind: "webhook" });
127
+ }), paused ? { kind: "webhook", paused: true } : { kind: "webhook" });
128
+ /**
129
+ * Webhooks delivered by the platform, which already checked the URL token.
130
+ * `webhook.paused` keeps the URL but runs nothing: the platform answers the
131
+ * provider 200 and drops the delivery until it's switched back.
132
+ */
133
+ export const webhook = Object.assign(webhookMiddleware(false), { paused: webhookMiddleware(true) });
129
134
  /** Signed-in users with this role. */
130
135
  export const role = (name) => tag(createMiddleware()
131
136
  .middleware([signedIn])
@@ -1,7 +1,7 @@
1
- import type { HostProps, PlatformBinding, ZiteRequestContext } from "./types.js";
1
+ import type { HostProps, PlatformBinding, RenderWatch, ZiteRequestContext } from "./types.js";
2
2
  export { runWithHost } from "./scope.js";
3
3
  export { describeApp } from "./describe.js";
4
- export type { HostProps, PlatformBinding, PlatformCallInput, PlatformCallResult, PlatformSession, RunRecord } from "./types.js";
4
+ export type { HostProps, PlatformBinding, PlatformCallInput, PlatformCallResult, PlatformSession, RenderWatch, RunRecord } from "./types.js";
5
5
  /**
6
6
  * Routes every zitejs data, integration and email call to this request's
7
7
  * platform, on one session, and only inside an access scope. The access kind
@@ -22,16 +22,30 @@ export declare const builtFunctionIds: () => string[];
22
22
  /** Each server function's `description` option, written in by `zite()` at build. */
23
23
  export declare const builtFunctionDescriptions: () => Record<string, string>;
24
24
  type Fetch = (request: Request, ...rest: unknown[]) => Promise<Response> | Response;
25
- /**
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.
30
- */
31
- export declare function createZiteFetch(options: {
25
+ type WorkerContext = {
26
+ waitUntil(promise: Promise<unknown>): void;
27
+ };
28
+ export type ZiteServerOptions = {
32
29
  handler: {
33
30
  fetch: Fetch;
34
31
  };
35
32
  getRouter: () => unknown;
36
33
  platform?: PlatformBinding;
37
- }): Fetch;
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;
@@ -18,7 +18,7 @@ export function installPlatformTransport() {
18
18
  // Fails closed: no platform means no platform access, never a fallback.
19
19
  if (!host?.platform)
20
20
  throw new Error("The Zite platform binding is missing");
21
- host.session ??= Promise.resolve(host.platform.session());
21
+ host.session ??= Promise.resolve(host.watch ? host.platform.session(host.watch) : host.platform.session());
22
22
  const result = await (await host.session).call({ integrationId, className, methodName, params, access: scope.kind });
23
23
  if ("value" in result)
24
24
  return result.value;
@@ -26,7 +26,31 @@ export function installPlatformTransport() {
26
26
  throw new ZiteError(code in ZITE_ERROR_STATUS ? code : "INTERNAL_ERROR", message);
27
27
  };
28
28
  globalThis.__wrapSdkCall = sdkCall;
29
+ watchOutboundFetches();
29
30
  }
31
+ let fetchesWatched = false;
32
+ const fetchedHost = (input) => {
33
+ try {
34
+ return new URL(input instanceof Request ? input.url : input).host;
35
+ }
36
+ catch {
37
+ return "?";
38
+ }
39
+ };
40
+ // A page that fetched depends on more than the reads the host saw, so the host
41
+ // must hear of it before the fetch goes out. Next patches fetch for the same reason.
42
+ const watchOutboundFetches = () => {
43
+ if (fetchesWatched)
44
+ return;
45
+ fetchesWatched = true;
46
+ const nativeFetch = globalThis.fetch.bind(globalThis);
47
+ globalThis.fetch = async (input, init) => {
48
+ const watch = getHost()?.watch;
49
+ if (watch)
50
+ await watch.uncacheable(`fetch ${fetchedHost(input)}`);
51
+ return nativeFetch(input, init);
52
+ };
53
+ };
30
54
  const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
31
55
  const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "warm"]);
32
56
  /** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
@@ -114,21 +138,22 @@ const pageStatus = (host) => {
114
138
  return isZiteError(failed) ? ZITE_ERROR_STATUS[failed.code] : undefined;
115
139
  };
116
140
  /**
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.
141
+ * The handlers of a Start app's server entry. A request the host dispatched
142
+ * carries its identity and platform in props; anything else (describe under
143
+ * `vite preview`, tests) reads `x-zite-*` headers and uses `platform` if one
144
+ * is given.
121
145
  */
122
- export function createZiteFetch(options) {
146
+ export function createZiteHandlers(options) {
123
147
  installPlatformTransport();
124
148
  // Loaded on first use, so a page request never pays for importing workflow modules.
125
149
  let workflows;
126
150
  const loadedWorkflows = () => (workflows ??= loadWorkflows());
127
- return async (request, ...rest) => {
128
- const ctx = rest[1];
151
+ const serve = async (request, ctx, watch) => {
129
152
  const host = ctx?.props?.platform
130
153
  ? hostContextFromProps(request, ctx.props)
131
154
  : hostContextFromRequest(request, options.platform);
155
+ if (watch)
156
+ host.watch = watch;
132
157
  // Starting this isolate was the point: the platform warms a build before its
133
158
  // first visitors, and no app code runs.
134
159
  if (host.invoke?.kind === "warm")
@@ -150,10 +175,23 @@ export function createZiteFetch(options) {
150
175
  const { platform } = host;
151
176
  if (!host.runs.length || !platform)
152
177
  return;
153
- const flushed = platform.runs({ runs: host.runs }).catch((error) => {
178
+ const batch = { runs: host.runs };
179
+ // On the request's session when it has one: one call, not another binding round trip.
180
+ const flushed = (async () => {
181
+ const session = host.session ? await host.session : undefined;
182
+ await (session?.runs ? session.runs(batch) : platform.runs(batch));
183
+ })().catch((error) => {
154
184
  console.error("[zite] couldn't record runs", error);
155
185
  });
156
186
  ctx?.waitUntil(flushed);
157
187
  });
158
188
  };
189
+ return {
190
+ fetch: (request, ...rest) => serve(request, rest[1]),
191
+ render: (request, ctx, watch) => serve(request, ctx, watch),
192
+ };
193
+ }
194
+ /** The fetch handler alone, for entries that take no RPC. */
195
+ export function createZiteFetch(options) {
196
+ return createZiteHandlers(options).fetch;
159
197
  }
@@ -42,11 +42,22 @@ export type PlatformCallResult = {
42
42
  };
43
43
  export type PlatformSession = {
44
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>;
45
56
  };
46
57
  /** What the host hands each request: its platform, already scoped to the app, build, request and user. */
47
58
  export type PlatformBinding = {
48
- /** Every platform call of one request goes through one session. */
49
- session(): PlatformSession | Promise<PlatformSession>;
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>;
50
61
  /** One batch per request, after its response has ended. */
51
62
  runs(batch: {
52
63
  runs: RunRecord[];
@@ -76,6 +87,8 @@ export type ZiteRequestContext = {
76
87
  runs: RunRecord[];
77
88
  platform?: PlatformBinding;
78
89
  session?: Promise<PlatformSession>;
90
+ /** Set when the host renders a page it may cache. */
91
+ watch?: RenderWatch;
79
92
  /** This request's router, so a page's status can follow the error it rendered. */
80
93
  router?: unknown;
81
94
  };
@@ -32,8 +32,9 @@ export function zite(options = {}) {
32
32
  // Platform-owned, so they're rewritten whenever they differ. Sandboxes run the
33
33
  // image's zitejs for `generate`, which may predate Start; the plugin can't.
34
34
  const appDir = process.cwd();
35
- if (!options.serverEntry)
36
- writeIfChanged(appDir, START_SERVER_ENTRY, renderStartServerEntry());
35
+ if (!options.serverEntry) {
36
+ writeIfChanged(appDir, START_SERVER_ENTRY, renderStartServerEntry(options.cloudflare === false ? "node" : "cloudflare"));
37
+ }
37
38
  writeIfChanged(appDir, START_INSTANCE_ENTRY, renderStartInstance(hasAppStart(appDir)));
38
39
  // Every server function TanStack's compiler found, from its own hook, by id.
39
40
  const ids = new Map();
@@ -125,7 +126,7 @@ export function zite(options = {}) {
125
126
  behavior: "error",
126
127
  client: {
127
128
  // Appended to TanStack's own.
128
- specifiers: ["zitejs/start/server", "zitejs/start/describe", "zitejs/runtime"],
129
+ specifiers: ["zitejs/start/server", "zitejs/start/worker", "zitejs/start/describe", "zitejs/runtime"],
129
130
  // Replaces TanStack's list, so its `**/*.server.*` is restated. File
130
131
  // patterns also catch `zitejs/db` and `zitejs/integrations`, which the
131
132
  // aliases above rewrite before specifiers are matched.
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.10.0-start.3",
3
+ "version": "0.10.0-start.5",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -101,6 +101,11 @@
101
101
  "import": "./dist/esm/start/server.js",
102
102
  "default": "./dist/esm/start/server.js"
103
103
  },
104
+ "./start/worker": {
105
+ "types": "./dist/esm/start/worker.d.ts",
106
+ "import": "./dist/esm/start/worker.js",
107
+ "default": "./dist/esm/start/worker.js"
108
+ },
104
109
  "./start/instance": {
105
110
  "types": "./dist/esm/start/instance.d.ts",
106
111
  "import": "./dist/esm/start/instance.js",