zitejs 0.9.138 → 0.10.0-start.0
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/cli.js +10 -3
- package/dist/cjs/dev/index.js +3 -0
- package/dist/cjs/start/describeBuilt.d.ts +3 -0
- package/dist/cjs/start/describeBuilt.js +104 -0
- package/dist/cjs/start/entries.d.ts +11 -0
- package/dist/cjs/start/entries.js +64 -0
- package/dist/esm/cli.js +10 -3
- package/dist/esm/dev/index.js +3 -0
- package/dist/esm/start/describe.d.ts +40 -0
- package/dist/esm/start/describe.js +122 -0
- package/dist/esm/start/describeBuilt.d.ts +3 -0
- package/dist/esm/start/describeBuilt.js +64 -0
- package/dist/esm/start/entries.d.ts +11 -0
- package/dist/esm/start/entries.js +56 -0
- package/dist/esm/start/host.d.ts +3 -0
- package/dist/esm/start/host.js +4 -0
- package/dist/esm/start/index.d.ts +64 -0
- package/dist/esm/start/index.js +123 -0
- package/dist/esm/start/instance.d.ts +12 -0
- package/dist/esm/start/instance.js +89 -0
- package/dist/esm/start/router.d.ts +17 -0
- package/dist/esm/start/router.js +34 -0
- package/dist/esm/start/scope.d.ts +15 -0
- package/dist/esm/start/scope.js +58 -0
- package/dist/esm/start/server.d.ts +47 -0
- package/dist/esm/start/server.js +81 -0
- package/dist/esm/start/types.d.ts +32 -0
- package/dist/esm/start/types.js +1 -0
- package/dist/esm/start/vite.d.ts +9 -0
- package/dist/esm/start/vite.js +114 -0
- package/package.json +53 -2
- package/dist/cjs/meta/index.d.ts +0 -14
- package/dist/cjs/meta/index.js +0 -12
- package/dist/cjs/notifications/index.d.ts +0 -25
- package/dist/cjs/notifications/index.js +0 -12
- package/dist/esm/meta/index.d.ts +0 -14
- package/dist/esm/meta/index.js +0 -8
- package/dist/esm/notifications/index.d.ts +0 -25
- package/dist/esm/notifications/index.js +0 -8
package/dist/cjs/cli.js
CHANGED
|
@@ -35,8 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
const dotenv_1 = require("dotenv");
|
|
38
|
-
|
|
39
|
-
(0, dotenv_1.config)({ path: '.env' });
|
|
38
|
+
// Quiet: dotenv 17 prints tips to stdout, which commands like `describe` own.
|
|
39
|
+
(0, dotenv_1.config)({ path: '.env.local', quiet: true });
|
|
40
|
+
(0, dotenv_1.config)({ path: '.env', quiet: true });
|
|
40
41
|
async function main() {
|
|
41
42
|
const command = process.argv[2];
|
|
42
43
|
switch (command) {
|
|
@@ -60,6 +61,11 @@ async function main() {
|
|
|
60
61
|
await runCheck();
|
|
61
62
|
break;
|
|
62
63
|
}
|
|
64
|
+
case 'describe': {
|
|
65
|
+
const { runDescribe } = await Promise.resolve().then(() => __importStar(require('./start/describeBuilt.js')));
|
|
66
|
+
await runDescribe();
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
63
69
|
case 'bundle': {
|
|
64
70
|
const { runBundle } = await Promise.resolve().then(() => __importStar(require('./bundle/index.js')));
|
|
65
71
|
await runBundle();
|
|
@@ -68,7 +74,7 @@ async function main() {
|
|
|
68
74
|
default:
|
|
69
75
|
console.error(command
|
|
70
76
|
? `Unknown command: ${command}`
|
|
71
|
-
: 'Usage: zitejs <sync|dev|generate|check|bundle>');
|
|
77
|
+
: 'Usage: zitejs <sync|dev|generate|check|bundle|describe>');
|
|
72
78
|
console.error('');
|
|
73
79
|
console.error('Commands:');
|
|
74
80
|
console.error(' sync Generate .zite/db.ts from database schema (single-app)');
|
|
@@ -76,6 +82,7 @@ async function main() {
|
|
|
76
82
|
console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
|
|
77
83
|
console.error(' check Run tsc --noEmit (and vite build where there is one) for every app and automation');
|
|
78
84
|
console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/index.ts) for cloudflare-lambda');
|
|
85
|
+
console.error(' describe Print what a built TanStack Start app declares: functions, routes, access');
|
|
79
86
|
process.exit(1);
|
|
80
87
|
}
|
|
81
88
|
}
|
package/dist/cjs/dev/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const fs_1 = require("fs");
|
|
|
7
7
|
const fs_2 = require("fs");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const sourceRoots_js_1 = require("../sourceRoots.js");
|
|
10
|
+
const entries_js_1 = require("../start/entries.js");
|
|
10
11
|
const index_js_1 = require("../sync/index.js");
|
|
11
12
|
const lib_js_1 = require("../sync/lib.js");
|
|
12
13
|
const rules_js_1 = require("../integrations/rules.js");
|
|
@@ -205,6 +206,7 @@ async function runGenerate() {
|
|
|
205
206
|
regenerateAppTypedWrappers(app);
|
|
206
207
|
regenerateAppAirtableSdk(app);
|
|
207
208
|
regenerateAppIntegrationsIndex(app);
|
|
209
|
+
(0, entries_js_1.regenerateStartEntries)(app);
|
|
208
210
|
}
|
|
209
211
|
console.log("Done!");
|
|
210
212
|
}
|
|
@@ -226,6 +228,7 @@ async function runDev() {
|
|
|
226
228
|
regenerateAppApiTs(app);
|
|
227
229
|
regenerateAppTypedWrappers(app);
|
|
228
230
|
regenerateAppIntegrationsIndex(app);
|
|
231
|
+
(0, entries_js_1.regenerateStartEntries)(app);
|
|
229
232
|
}
|
|
230
233
|
// 3. Watch each dir's src/api/ for endpoint changes
|
|
231
234
|
let watchingAny = false;
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.describeBuiltApp = describeBuiltApp;
|
|
40
|
+
exports.runDescribe = runDescribe;
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const node_fs_1 = require("node:fs");
|
|
43
|
+
const node_net_1 = require("node:net");
|
|
44
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
45
|
+
const node_url_1 = require("node:url");
|
|
46
|
+
// Only the host can set x-zite-* in production: the front strips them from public traffic.
|
|
47
|
+
const DESCRIBE_HEADERS = { "x-zite-invoke": "describe" };
|
|
48
|
+
const freePort = () => new Promise((resolve, reject) => {
|
|
49
|
+
const server = (0, node_net_1.createServer)();
|
|
50
|
+
server.once("error", reject);
|
|
51
|
+
server.listen(0, () => {
|
|
52
|
+
const { port } = server.address();
|
|
53
|
+
server.close(() => resolve(port));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// A Workers build runs where it will run: workerd, through the app's own
|
|
57
|
+
// `vite preview`, so it uses the same Cloudflare plugin and config as the build.
|
|
58
|
+
async function describeInWorkerd(appDir) {
|
|
59
|
+
const port = await freePort();
|
|
60
|
+
// Its own process group, so stopping it also stops vite and workerd under npx.
|
|
61
|
+
const preview = (0, node_child_process_1.spawn)("npx", ["--no-install", "vite", "preview", "--port", String(port), "--strictPort"], {
|
|
62
|
+
cwd: appDir,
|
|
63
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
64
|
+
detached: true,
|
|
65
|
+
});
|
|
66
|
+
let stderr = "";
|
|
67
|
+
preview.stderr.on("data", (chunk) => (stderr += chunk));
|
|
68
|
+
try {
|
|
69
|
+
const deadline = Date.now() + 60_000;
|
|
70
|
+
while (true) {
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(`http://localhost:${port}/`, { headers: DESCRIBE_HEADERS });
|
|
73
|
+
// Right after start, preview can answer before the Worker does; only the
|
|
74
|
+
// app's own describe answer is JSON.
|
|
75
|
+
if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
|
|
76
|
+
return await response.json();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
if (preview.exitCode !== null || Date.now() > deadline) {
|
|
81
|
+
throw new Error(`vite preview didn't serve the app: ${stderr.trim().slice(-500)}`);
|
|
82
|
+
}
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
try {
|
|
88
|
+
process.kill(-preview.pid, "SIGTERM");
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Describes a built Start app from its `dist/server`: a Workers build in workerd, a Node build in-process. */
|
|
94
|
+
async function describeBuiltApp(appDir) {
|
|
95
|
+
const serverDir = node_path_1.default.join(appDir, "dist/server");
|
|
96
|
+
if ((0, node_fs_1.existsSync)(node_path_1.default.join(serverDir, "wrangler.json")))
|
|
97
|
+
return describeInWorkerd(appDir);
|
|
98
|
+
const { default: server } = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(node_path_1.default.join(serverDir, "server.js")).href}`).then(s => __importStar(require(s)));
|
|
99
|
+
return (await server.fetch(new Request("http://describe.local/", { headers: DESCRIBE_HEADERS }))).json();
|
|
100
|
+
}
|
|
101
|
+
async function runDescribe() {
|
|
102
|
+
const appDir = node_path_1.default.resolve(process.argv[3] ?? ".");
|
|
103
|
+
process.stdout.write(`${JSON.stringify(await describeBuiltApp(appDir), null, 2)}\n`);
|
|
104
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SourceDir } from "../sourceRoots.js";
|
|
2
|
+
export declare const START_SERVER_ENTRY = ".zite/server.ts";
|
|
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;
|
|
6
|
+
/** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
|
|
7
|
+
export declare const renderStartInstance: (hasAppStart: boolean) => string;
|
|
8
|
+
export declare const hasAppStart: (appDir: string) => boolean;
|
|
9
|
+
/** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
|
|
10
|
+
export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
|
|
11
|
+
export declare function regenerateStartEntries(app: SourceDir): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
exports.regenerateStartEntries = regenerateStartEntries;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
exports.START_SERVER_ENTRY = ".zite/server.ts";
|
|
8
|
+
exports.START_INSTANCE_ENTRY = ".zite/start.ts";
|
|
9
|
+
const APP_START_FILES = ["src/start.ts", "src/start.tsx"];
|
|
10
|
+
/** The server entry every Start app builds with. Platform-owned: regenerated, never edited. */
|
|
11
|
+
const renderStartServerEntry = () => `// Generated by zitejs for TanStack Start apps. The platform owns this file.
|
|
12
|
+
import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
|
|
13
|
+
import { createZiteFetch } from 'zitejs/start/server';
|
|
14
|
+
import { getRouter } from '../src/router';
|
|
15
|
+
|
|
16
|
+
export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter }) });
|
|
17
|
+
`;
|
|
18
|
+
exports.renderStartServerEntry = renderStartServerEntry;
|
|
19
|
+
/** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
|
|
20
|
+
const renderStartInstance = (hasAppStart) => hasAppStart
|
|
21
|
+
? `// Generated by zitejs for TanStack Start apps. The platform owns this file.
|
|
22
|
+
import { createZiteStart } from 'zitejs/start/instance';
|
|
23
|
+
import { startInstance as app } from '../src/start';
|
|
24
|
+
|
|
25
|
+
export const startInstance = createZiteStart(app);
|
|
26
|
+
`
|
|
27
|
+
: `// Generated by zitejs for TanStack Start apps. The platform owns this file.
|
|
28
|
+
import { createZiteStart } from 'zitejs/start/instance';
|
|
29
|
+
|
|
30
|
+
export const startInstance = createZiteStart();
|
|
31
|
+
`;
|
|
32
|
+
exports.renderStartInstance = renderStartInstance;
|
|
33
|
+
const hasAppStart = (appDir) => APP_START_FILES.some((file) => (0, node_fs_1.existsSync)((0, node_path_1.join)(appDir, file)));
|
|
34
|
+
exports.hasAppStart = hasAppStart;
|
|
35
|
+
/** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
|
|
36
|
+
const writeIfChanged = (appDir, file, content) => {
|
|
37
|
+
const target = (0, node_path_1.join)(appDir, file);
|
|
38
|
+
if ((0, node_fs_1.existsSync)(target) && (0, node_fs_1.readFileSync)(target, "utf-8") === content)
|
|
39
|
+
return false;
|
|
40
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
41
|
+
(0, node_fs_1.writeFileSync)(target, content);
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
exports.writeIfChanged = writeIfChanged;
|
|
45
|
+
const isStartApp = (app) => {
|
|
46
|
+
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
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function regenerateStartEntries(app) {
|
|
54
|
+
if (!isStartApp(app))
|
|
55
|
+
return;
|
|
56
|
+
const entries = [
|
|
57
|
+
[exports.START_SERVER_ENTRY, (0, exports.renderStartServerEntry)()],
|
|
58
|
+
[exports.START_INSTANCE_ENTRY, (0, exports.renderStartInstance)((0, exports.hasAppStart)(app.path))],
|
|
59
|
+
];
|
|
60
|
+
for (const [file, content] of entries) {
|
|
61
|
+
if ((0, exports.writeIfChanged)(app.path, file, content))
|
|
62
|
+
console.log(`Wrote ${(0, node_path_1.join)(app.path, file)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { config } from 'dotenv';
|
|
3
|
-
|
|
4
|
-
config({ path: '.env' });
|
|
3
|
+
// Quiet: dotenv 17 prints tips to stdout, which commands like `describe` own.
|
|
4
|
+
config({ path: '.env.local', quiet: true });
|
|
5
|
+
config({ path: '.env', quiet: true });
|
|
5
6
|
async function main() {
|
|
6
7
|
const command = process.argv[2];
|
|
7
8
|
switch (command) {
|
|
@@ -25,6 +26,11 @@ async function main() {
|
|
|
25
26
|
await runCheck();
|
|
26
27
|
break;
|
|
27
28
|
}
|
|
29
|
+
case 'describe': {
|
|
30
|
+
const { runDescribe } = await import('./start/describeBuilt.js');
|
|
31
|
+
await runDescribe();
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
28
34
|
case 'bundle': {
|
|
29
35
|
const { runBundle } = await import('./bundle/index.js');
|
|
30
36
|
await runBundle();
|
|
@@ -33,7 +39,7 @@ async function main() {
|
|
|
33
39
|
default:
|
|
34
40
|
console.error(command
|
|
35
41
|
? `Unknown command: ${command}`
|
|
36
|
-
: 'Usage: zitejs <sync|dev|generate|check|bundle>');
|
|
42
|
+
: 'Usage: zitejs <sync|dev|generate|check|bundle|describe>');
|
|
37
43
|
console.error('');
|
|
38
44
|
console.error('Commands:');
|
|
39
45
|
console.error(' sync Generate .zite/db.ts from database schema (single-app)');
|
|
@@ -41,6 +47,7 @@ async function main() {
|
|
|
41
47
|
console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
|
|
42
48
|
console.error(' check Run tsc --noEmit (and vite build where there is one) for every app and automation');
|
|
43
49
|
console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/index.ts) for cloudflare-lambda');
|
|
50
|
+
console.error(' describe Print what a built TanStack Start app declares: functions, routes, access');
|
|
44
51
|
process.exit(1);
|
|
45
52
|
}
|
|
46
53
|
}
|
package/dist/esm/dev/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { watch } from "fs";
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from "fs";
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
4
|
import { findWorkspaceRoot, listSourceDirs, } from "../sourceRoots.js";
|
|
5
|
+
import { regenerateStartEntries } from "../start/entries.js";
|
|
5
6
|
import { runSync } from "../sync/index.js";
|
|
6
7
|
import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, findEmailIntegrationId, generateEmailSdk, generateIntegrationsIndex, } from "../sync/lib.js";
|
|
7
8
|
import { gettableIntegrations, } from "../integrations/rules.js";
|
|
@@ -200,6 +201,7 @@ export async function runGenerate() {
|
|
|
200
201
|
regenerateAppTypedWrappers(app);
|
|
201
202
|
regenerateAppAirtableSdk(app);
|
|
202
203
|
regenerateAppIntegrationsIndex(app);
|
|
204
|
+
regenerateStartEntries(app);
|
|
203
205
|
}
|
|
204
206
|
console.log("Done!");
|
|
205
207
|
}
|
|
@@ -221,6 +223,7 @@ export async function runDev() {
|
|
|
221
223
|
regenerateAppApiTs(app);
|
|
222
224
|
regenerateAppTypedWrappers(app);
|
|
223
225
|
regenerateAppIntegrationsIndex(app);
|
|
226
|
+
regenerateStartEntries(app);
|
|
224
227
|
}
|
|
225
228
|
// 3. Watch each dir's src/api/ for endpoint changes
|
|
226
229
|
let watchingAny = false;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type Access = {
|
|
2
|
+
kind: "signedIn" | "anyone" | "webhook" | "none";
|
|
3
|
+
roles: string[];
|
|
4
|
+
};
|
|
5
|
+
export type DescribedFunction = {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
method?: string;
|
|
10
|
+
access: Access;
|
|
11
|
+
input?: unknown;
|
|
12
|
+
problems: string[];
|
|
13
|
+
};
|
|
14
|
+
export type DescribedRoute = {
|
|
15
|
+
path: string;
|
|
16
|
+
methods: Record<string, Access>;
|
|
17
|
+
problems: string[];
|
|
18
|
+
};
|
|
19
|
+
export type Description = {
|
|
20
|
+
functions: DescribedFunction[];
|
|
21
|
+
routes: DescribedRoute[];
|
|
22
|
+
};
|
|
23
|
+
export declare function describeFunction(id: string, request: Request): Promise<DescribedFunction>;
|
|
24
|
+
type RouteLike = {
|
|
25
|
+
fullPath: string;
|
|
26
|
+
parentRoute?: RouteLike;
|
|
27
|
+
options?: {
|
|
28
|
+
server?: {
|
|
29
|
+
middleware?: unknown[];
|
|
30
|
+
handlers?: unknown;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export declare function describeRoutes(routesById: Record<string, RouteLike>): DescribedRoute[];
|
|
35
|
+
export declare function describeApp(opts: {
|
|
36
|
+
fnIds: string[];
|
|
37
|
+
getRouter: () => unknown;
|
|
38
|
+
request: Request;
|
|
39
|
+
}): Promise<Description>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Describes a built app by loading it, not by reading its source: each server
|
|
2
|
+
// function's real middleware chain, each server route's, and input schemas.
|
|
3
|
+
import { createMiddleware, flattenMiddlewares } from "@tanstack/react-start";
|
|
4
|
+
import { createSsrRpc } from "@tanstack/react-start/ssr-rpc";
|
|
5
|
+
import { runWithStartContext } from "@tanstack/start-storage-context";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ziteMiddlewareKind } from "./index.js";
|
|
8
|
+
const accessOf = (chain, problems) => {
|
|
9
|
+
const kinds = chain.map(ziteMiddlewareKind).filter((k) => !!k);
|
|
10
|
+
const roles = kinds.flatMap((k) => (k.kind === "role" ? [k.role] : []));
|
|
11
|
+
const base = [...new Set(kinds.flatMap((k) => (k.kind === "signedIn" || k.kind === "anyone" || k.kind === "webhook" ? [k.kind] : [])))];
|
|
12
|
+
if (base.length === 0) {
|
|
13
|
+
problems.push("access-missing: no signedIn, anyone or webhook middleware in its chain");
|
|
14
|
+
return { kind: "none", roles };
|
|
15
|
+
}
|
|
16
|
+
if (base.length > 1)
|
|
17
|
+
problems.push(`access-conflict: ${base.join(" + ")}`);
|
|
18
|
+
const kind = base.includes("webhook") ? "webhook" : base.includes("signedIn") ? "signedIn" : "anyone";
|
|
19
|
+
return { kind, roles };
|
|
20
|
+
};
|
|
21
|
+
const jsonSchemaOf = (validator) => {
|
|
22
|
+
if (validator && typeof validator === "object" && "_zod" in validator) {
|
|
23
|
+
try {
|
|
24
|
+
return z.toJSONSchema(validator);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { unrepresentable: true };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return validator ? { opaque: true } : undefined;
|
|
31
|
+
};
|
|
32
|
+
// Describe walks functions one at a time, so one probe slot is enough.
|
|
33
|
+
let probe = null;
|
|
34
|
+
// The only global function middleware while describing: it records what it was
|
|
35
|
+
// handed and ends the call, so no app middleware or handler runs.
|
|
36
|
+
const describeProbe = createMiddleware({ type: "function" }).server(({ serverFnMeta, method }) => {
|
|
37
|
+
if (probe)
|
|
38
|
+
Object.assign(probe, { meta: serverFnMeta, method, reached: true });
|
|
39
|
+
return "zite:describe";
|
|
40
|
+
});
|
|
41
|
+
export async function describeFunction(id, request) {
|
|
42
|
+
const problems = [];
|
|
43
|
+
const seen = [];
|
|
44
|
+
probe = { reached: false };
|
|
45
|
+
// Before running a chain, TanStack asks this set about every middleware in it.
|
|
46
|
+
// Recording the questions gives the exact chain it would have run.
|
|
47
|
+
const executedRequestMiddlewares = {
|
|
48
|
+
has: (middleware) => {
|
|
49
|
+
seen.push(middleware);
|
|
50
|
+
return false;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const startContext = {
|
|
54
|
+
request,
|
|
55
|
+
startOptions: { functionMiddleware: [describeProbe] },
|
|
56
|
+
executedRequestMiddlewares,
|
|
57
|
+
contextAfterGlobalMiddlewares: {},
|
|
58
|
+
handlerType: "serverFn",
|
|
59
|
+
getRouter: () => {
|
|
60
|
+
throw new Error("describe has no router");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
await runWithStartContext(startContext, () => createSsrRpc(id)({ data: undefined, context: {} }));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
problems.push(`probe-failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
const state = probe;
|
|
70
|
+
probe = null;
|
|
71
|
+
// Fails closed: if TanStack stops asking, or stops running globals first, every
|
|
72
|
+
// function reports a problem and the build stops.
|
|
73
|
+
if (!state.reached || seen[0] !== describeProbe)
|
|
74
|
+
problems.push("probe-not-first: chain could not be read");
|
|
75
|
+
const chain = seen.filter((m) => m !== describeProbe);
|
|
76
|
+
if (!chain.some((m) => ziteMiddlewareKind(m)?.kind === "factory")) {
|
|
77
|
+
problems.push("raw-server-fn: define it with signedInFn or publicFn from zitejs/start");
|
|
78
|
+
}
|
|
79
|
+
const handlerStep = chain.at(-1);
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
name: state.meta?.name,
|
|
83
|
+
file: state.meta?.filename,
|
|
84
|
+
method: state.method,
|
|
85
|
+
access: accessOf(chain, problems),
|
|
86
|
+
input: jsonSchemaOf(handlerStep?.options?.inputValidator),
|
|
87
|
+
problems,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function describeRoutes(routesById) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const route of Object.values(routesById)) {
|
|
93
|
+
const server = route.options?.server;
|
|
94
|
+
if (!server?.handlers)
|
|
95
|
+
continue;
|
|
96
|
+
const problems = [];
|
|
97
|
+
const ancestry = [];
|
|
98
|
+
for (let r = route; r; r = r.parentRoute)
|
|
99
|
+
ancestry.unshift(r);
|
|
100
|
+
const routeMiddleware = ancestry.flatMap((r) => r.options?.server?.middleware ?? []);
|
|
101
|
+
const handlers = (typeof server.handlers === "function"
|
|
102
|
+
? server.handlers({ createHandlers: (d) => d })
|
|
103
|
+
: server.handlers);
|
|
104
|
+
const methods = {};
|
|
105
|
+
for (const [method, handler] of Object.entries(handlers)) {
|
|
106
|
+
const own = typeof handler === "function" ? [] : (handler.middleware ?? []);
|
|
107
|
+
const chain = flattenMiddlewares([...routeMiddleware, ...own]);
|
|
108
|
+
const methodProblems = [];
|
|
109
|
+
methods[method] = accessOf(chain, methodProblems);
|
|
110
|
+
problems.push(...methodProblems.map((p) => `${method}: ${p}`));
|
|
111
|
+
}
|
|
112
|
+
out.push({ path: route.fullPath, methods, problems });
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
export async function describeApp(opts) {
|
|
117
|
+
const functions = [];
|
|
118
|
+
for (const id of opts.fnIds)
|
|
119
|
+
functions.push(await describeFunction(id, opts.request));
|
|
120
|
+
const router = (await opts.getRouter());
|
|
121
|
+
return { functions, routes: describeRoutes(router.routesById) };
|
|
122
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
// Only the host can set x-zite-* in production: the front strips them from public traffic.
|
|
7
|
+
const DESCRIBE_HEADERS = { "x-zite-invoke": "describe" };
|
|
8
|
+
const freePort = () => new Promise((resolve, reject) => {
|
|
9
|
+
const server = createServer();
|
|
10
|
+
server.once("error", reject);
|
|
11
|
+
server.listen(0, () => {
|
|
12
|
+
const { port } = server.address();
|
|
13
|
+
server.close(() => resolve(port));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
// A Workers build runs where it will run: workerd, through the app's own
|
|
17
|
+
// `vite preview`, so it uses the same Cloudflare plugin and config as the build.
|
|
18
|
+
async function describeInWorkerd(appDir) {
|
|
19
|
+
const port = await freePort();
|
|
20
|
+
// Its own process group, so stopping it also stops vite and workerd under npx.
|
|
21
|
+
const preview = spawn("npx", ["--no-install", "vite", "preview", "--port", String(port), "--strictPort"], {
|
|
22
|
+
cwd: appDir,
|
|
23
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
24
|
+
detached: true,
|
|
25
|
+
});
|
|
26
|
+
let stderr = "";
|
|
27
|
+
preview.stderr.on("data", (chunk) => (stderr += chunk));
|
|
28
|
+
try {
|
|
29
|
+
const deadline = Date.now() + 60_000;
|
|
30
|
+
while (true) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`http://localhost:${port}/`, { headers: DESCRIBE_HEADERS });
|
|
33
|
+
// Right after start, preview can answer before the Worker does; only the
|
|
34
|
+
// app's own describe answer is JSON.
|
|
35
|
+
if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
if (preview.exitCode !== null || Date.now() > deadline) {
|
|
41
|
+
throw new Error(`vite preview didn't serve the app: ${stderr.trim().slice(-500)}`);
|
|
42
|
+
}
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(-preview.pid, "SIGTERM");
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Describes a built Start app from its `dist/server`: a Workers build in workerd, a Node build in-process. */
|
|
54
|
+
export async function describeBuiltApp(appDir) {
|
|
55
|
+
const serverDir = path.join(appDir, "dist/server");
|
|
56
|
+
if (existsSync(path.join(serverDir, "wrangler.json")))
|
|
57
|
+
return describeInWorkerd(appDir);
|
|
58
|
+
const { default: server } = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
59
|
+
return (await server.fetch(new Request("http://describe.local/", { headers: DESCRIBE_HEADERS }))).json();
|
|
60
|
+
}
|
|
61
|
+
export async function runDescribe() {
|
|
62
|
+
const appDir = path.resolve(process.argv[3] ?? ".");
|
|
63
|
+
process.stdout.write(`${JSON.stringify(await describeBuiltApp(appDir), null, 2)}\n`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SourceDir } from "../sourceRoots.js";
|
|
2
|
+
export declare const START_SERVER_ENTRY = ".zite/server.ts";
|
|
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;
|
|
6
|
+
/** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
|
|
7
|
+
export declare const renderStartInstance: (hasAppStart: boolean) => string;
|
|
8
|
+
export declare const hasAppStart: (appDir: string) => boolean;
|
|
9
|
+
/** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
|
|
10
|
+
export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
|
|
11
|
+
export declare function regenerateStartEntries(app: SourceDir): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
export const START_SERVER_ENTRY = ".zite/server.ts";
|
|
4
|
+
export const START_INSTANCE_ENTRY = ".zite/start.ts";
|
|
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.
|
|
8
|
+
import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
|
|
9
|
+
import { createZiteFetch } from 'zitejs/start/server';
|
|
10
|
+
import { getRouter } from '../src/router';
|
|
11
|
+
|
|
12
|
+
export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter }) });
|
|
13
|
+
`;
|
|
14
|
+
/** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
|
|
15
|
+
export const renderStartInstance = (hasAppStart) => hasAppStart
|
|
16
|
+
? `// Generated by zitejs for TanStack Start apps. The platform owns this file.
|
|
17
|
+
import { createZiteStart } from 'zitejs/start/instance';
|
|
18
|
+
import { startInstance as app } from '../src/start';
|
|
19
|
+
|
|
20
|
+
export const startInstance = createZiteStart(app);
|
|
21
|
+
`
|
|
22
|
+
: `// Generated by zitejs for TanStack Start apps. The platform owns this file.
|
|
23
|
+
import { createZiteStart } from 'zitejs/start/instance';
|
|
24
|
+
|
|
25
|
+
export const startInstance = createZiteStart();
|
|
26
|
+
`;
|
|
27
|
+
export const hasAppStart = (appDir) => APP_START_FILES.some((file) => existsSync(join(appDir, file)));
|
|
28
|
+
/** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
|
|
29
|
+
export const writeIfChanged = (appDir, file, content) => {
|
|
30
|
+
const target = join(appDir, file);
|
|
31
|
+
if (existsSync(target) && readFileSync(target, "utf-8") === content)
|
|
32
|
+
return false;
|
|
33
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
34
|
+
writeFileSync(target, content);
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
const isStartApp = (app) => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(join(app.path, "zite.config.json"), "utf-8"))?.runtime === "start";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
export function regenerateStartEntries(app) {
|
|
46
|
+
if (!isStartApp(app))
|
|
47
|
+
return;
|
|
48
|
+
const entries = [
|
|
49
|
+
[START_SERVER_ENTRY, renderStartServerEntry()],
|
|
50
|
+
[START_INSTANCE_ENTRY, renderStartInstance(hasAppStart(app.path))],
|
|
51
|
+
];
|
|
52
|
+
for (const [file, content] of entries) {
|
|
53
|
+
if (writeIfChanged(app.path, file, content))
|
|
54
|
+
console.log(`Wrote ${join(app.path, file)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Isomorphic, so it can't import the server-only store: it reads it through the
|
|
2
|
+
// global key ./scope.ts registers it under. In the browser there is none.
|
|
3
|
+
export const HOST_STORE_KEY = Symbol.for("zitejs:host-context");
|
|
4
|
+
export const currentHost = () => globalThis[HOST_STORE_KEY]?.getStore();
|