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
package/dist/cjs/check/index.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.runCheck = runCheck;
|
|
4
37
|
const child_process_1 = require("child_process");
|
|
@@ -58,15 +91,60 @@ function bundleFailures(output) {
|
|
|
58
91
|
}
|
|
59
92
|
return failures;
|
|
60
93
|
}
|
|
94
|
+
const indent = (output) => output.trim().split('\n').map(l => ` ${l}`).join('\n');
|
|
95
|
+
/**
|
|
96
|
+
* A Start app: `vite build` first, since it regenerates the route tree tsc
|
|
97
|
+
* reads, then tsc, then describe, which reports what the platform's build
|
|
98
|
+
* refuses, read the same way.
|
|
99
|
+
*/
|
|
100
|
+
async function checkStartApp(appPath) {
|
|
101
|
+
process.stdout.write(' vite build ... ');
|
|
102
|
+
const vite = run('npx', ['vite', 'build'], appPath);
|
|
103
|
+
console.log(vite.ok ? '✓' : '✗');
|
|
104
|
+
if (!vite.ok) {
|
|
105
|
+
console.log(indent(vite.output));
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(' tsc --noEmit ... ');
|
|
109
|
+
// --pretty for `file:line:col - error` lines, the shape the platform re-roots.
|
|
110
|
+
const tsc = run('npx', ['tsc', '--noEmit', '--pretty', '-p', 'tsconfig.app.json'], appPath);
|
|
111
|
+
console.log(tsc.ok ? '✓' : '✗');
|
|
112
|
+
if (!tsc.ok)
|
|
113
|
+
console.log(indent(tsc.output));
|
|
114
|
+
process.stdout.write(' describe ... ');
|
|
115
|
+
try {
|
|
116
|
+
const { describeBuiltApp } = await Promise.resolve().then(() => __importStar(require('../start/describeBuilt.js')));
|
|
117
|
+
const { descriptionProblems } = await Promise.resolve().then(() => __importStar(require('../start/problems.js')));
|
|
118
|
+
const problems = descriptionProblems((await describeBuiltApp(appPath)));
|
|
119
|
+
console.log(problems.length ? '✗' : '✓');
|
|
120
|
+
if (problems.length)
|
|
121
|
+
console.log(problems.map(p => ` ${p}`).join('\n'));
|
|
122
|
+
return tsc.ok && problems.length === 0;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.log('✗');
|
|
126
|
+
console.log(` ${err instanceof Error ? err.message : String(err)}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
61
130
|
async function runCheck() {
|
|
62
|
-
const
|
|
63
|
-
|
|
131
|
+
const { isStartAppDir } = await Promise.resolve().then(() => __importStar(require('../start/entries.js')));
|
|
132
|
+
// Run inside a Start app, its own zitejs checks just that app: the workspace
|
|
133
|
+
// root's is a classic release that doesn't know Start.
|
|
134
|
+
const inStartApp = isStartAppDir('.');
|
|
135
|
+
const appDirs = inStartApp ? [] : (0, sourceRoots_js_1.listSourceDirs)();
|
|
136
|
+
if (!inStartApp && appDirs.length === 0) {
|
|
64
137
|
console.error('No apps or automations found under apps/ or automations/.');
|
|
65
138
|
process.exit(1);
|
|
66
139
|
}
|
|
67
|
-
let allPassed = true;
|
|
140
|
+
let allPassed = inStartApp ? await checkStartApp('.') : true;
|
|
68
141
|
for (const { root, dir: app, path: appPath } of appDirs) {
|
|
69
142
|
console.log(`\n── ${appPath} ──`);
|
|
143
|
+
if (isStartAppDir(appPath)) {
|
|
144
|
+
if (!(await checkStartApp(appPath)))
|
|
145
|
+
allPassed = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
70
148
|
// Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
|
|
71
149
|
// solution-style config — `"files": []` plus a reference — so
|
|
72
150
|
// `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
|
|
@@ -8,4 +8,9 @@ export declare const renderStartInstance: (hasAppStart: boolean) => string;
|
|
|
8
8
|
export declare const hasAppStart: (appDir: string) => boolean;
|
|
9
9
|
/** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
|
|
10
10
|
export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
|
|
11
|
+
export declare const isStartAppDir: (appPath: string) => boolean;
|
|
11
12
|
export declare function regenerateStartEntries(app: SourceDir): void;
|
|
13
|
+
/** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
|
|
14
|
+
export declare function workflowFiles(appDir: string): string[];
|
|
15
|
+
/** The registry zite() builds in place of zitejs's empty one. */
|
|
16
|
+
export declare const renderWorkflowModules: (appDir: string, files: string[]) => string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.writeIfChanged = exports.hasAppStart = exports.renderStartInstance = exports.renderStartServerEntry = exports.START_INSTANCE_ENTRY = exports.START_SERVER_ENTRY = void 0;
|
|
3
|
+
exports.renderWorkflowModules = exports.isStartAppDir = exports.writeIfChanged = exports.hasAppStart = exports.renderStartInstance = exports.renderStartServerEntry = exports.START_INSTANCE_ENTRY = exports.START_SERVER_ENTRY = void 0;
|
|
4
4
|
exports.regenerateStartEntries = regenerateStartEntries;
|
|
5
|
+
exports.workflowFiles = workflowFiles;
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
6
7
|
const node_path_1 = require("node:path");
|
|
7
8
|
exports.START_SERVER_ENTRY = ".zite/server.ts";
|
|
@@ -42,14 +43,16 @@ const writeIfChanged = (appDir, file, content) => {
|
|
|
42
43
|
return true;
|
|
43
44
|
};
|
|
44
45
|
exports.writeIfChanged = writeIfChanged;
|
|
45
|
-
const
|
|
46
|
+
const isStartAppDir = (appPath) => {
|
|
46
47
|
try {
|
|
47
|
-
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(
|
|
48
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(appPath, "zite.config.json"), "utf-8"))?.runtime === "start";
|
|
48
49
|
}
|
|
49
50
|
catch {
|
|
50
51
|
return false;
|
|
51
52
|
}
|
|
52
53
|
};
|
|
54
|
+
exports.isStartAppDir = isStartAppDir;
|
|
55
|
+
const isStartApp = (app) => (0, exports.isStartAppDir)(app.path);
|
|
53
56
|
function regenerateStartEntries(app) {
|
|
54
57
|
if (!isStartApp(app))
|
|
55
58
|
return;
|
|
@@ -62,3 +65,29 @@ function regenerateStartEntries(app) {
|
|
|
62
65
|
console.log(`Wrote ${(0, node_path_1.join)(app.path, file)}`);
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
const SOURCE_FILE = /\.[mc]?[jt]sx?$/;
|
|
69
|
+
const NOT_APP_CODE = /(\.(test|spec)\.[^.]+|\.d\.ts|routeTree\.gen\.ts)$/;
|
|
70
|
+
/** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
|
|
71
|
+
function workflowFiles(appDir) {
|
|
72
|
+
const found = [];
|
|
73
|
+
const walk = (dir) => {
|
|
74
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
75
|
+
const full = (0, node_path_1.join)(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (entry.name !== "node_modules")
|
|
78
|
+
walk(full);
|
|
79
|
+
}
|
|
80
|
+
else if (SOURCE_FILE.test(entry.name) && !NOT_APP_CODE.test(entry.name) && (0, node_fs_1.readFileSync)(full, "utf-8").includes("createWorkflow")) {
|
|
81
|
+
found.push((0, node_path_1.relative)(appDir, full).split("\\").join("/"));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(appDir, "src")))
|
|
86
|
+
walk((0, node_path_1.join)(appDir, "src"));
|
|
87
|
+
return found.sort();
|
|
88
|
+
}
|
|
89
|
+
/** The registry zite() builds in place of zitejs's empty one. */
|
|
90
|
+
const renderWorkflowModules = (appDir, files) => `export const workflowModules = {\n${files
|
|
91
|
+
.map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify((0, node_path_1.join)(appDir, file))}),`)
|
|
92
|
+
.join("\n")}\n};\n`;
|
|
93
|
+
exports.renderWorkflowModules = renderWorkflowModules;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type WithProblems = {
|
|
2
|
+
problems: string[];
|
|
3
|
+
};
|
|
4
|
+
type Described = {
|
|
5
|
+
functions: Array<WithProblems & {
|
|
6
|
+
id: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
}>;
|
|
9
|
+
routes: Array<WithProblems & {
|
|
10
|
+
path: string;
|
|
11
|
+
}>;
|
|
12
|
+
workflows?: Array<WithProblems & {
|
|
13
|
+
id: string;
|
|
14
|
+
file: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
/** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
|
|
18
|
+
export declare function descriptionProblems(description: Described): string[];
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.descriptionProblems = descriptionProblems;
|
|
4
|
+
/** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
|
|
5
|
+
function descriptionProblems(description) {
|
|
6
|
+
return [
|
|
7
|
+
...description.functions.flatMap((f) => f.problems.map((p) => `function ${f.id}${f.file ? ` (${f.file})` : ""}: ${p}`)),
|
|
8
|
+
...description.routes.flatMap((r) => r.problems.map((p) => `route ${r.path}: ${p}`)),
|
|
9
|
+
...(description.workflows ?? []).flatMap((w) => w.problems.map((p) => `workflow ${w.id} (${w.file}): ${p}`)),
|
|
10
|
+
];
|
|
11
|
+
}
|
package/dist/esm/check/index.js
CHANGED
|
@@ -55,15 +55,60 @@ function bundleFailures(output) {
|
|
|
55
55
|
}
|
|
56
56
|
return failures;
|
|
57
57
|
}
|
|
58
|
+
const indent = (output) => output.trim().split('\n').map(l => ` ${l}`).join('\n');
|
|
59
|
+
/**
|
|
60
|
+
* A Start app: `vite build` first, since it regenerates the route tree tsc
|
|
61
|
+
* reads, then tsc, then describe, which reports what the platform's build
|
|
62
|
+
* refuses, read the same way.
|
|
63
|
+
*/
|
|
64
|
+
async function checkStartApp(appPath) {
|
|
65
|
+
process.stdout.write(' vite build ... ');
|
|
66
|
+
const vite = run('npx', ['vite', 'build'], appPath);
|
|
67
|
+
console.log(vite.ok ? '✓' : '✗');
|
|
68
|
+
if (!vite.ok) {
|
|
69
|
+
console.log(indent(vite.output));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(' tsc --noEmit ... ');
|
|
73
|
+
// --pretty for `file:line:col - error` lines, the shape the platform re-roots.
|
|
74
|
+
const tsc = run('npx', ['tsc', '--noEmit', '--pretty', '-p', 'tsconfig.app.json'], appPath);
|
|
75
|
+
console.log(tsc.ok ? '✓' : '✗');
|
|
76
|
+
if (!tsc.ok)
|
|
77
|
+
console.log(indent(tsc.output));
|
|
78
|
+
process.stdout.write(' describe ... ');
|
|
79
|
+
try {
|
|
80
|
+
const { describeBuiltApp } = await import('../start/describeBuilt.js');
|
|
81
|
+
const { descriptionProblems } = await import('../start/problems.js');
|
|
82
|
+
const problems = descriptionProblems((await describeBuiltApp(appPath)));
|
|
83
|
+
console.log(problems.length ? '✗' : '✓');
|
|
84
|
+
if (problems.length)
|
|
85
|
+
console.log(problems.map(p => ` ${p}`).join('\n'));
|
|
86
|
+
return tsc.ok && problems.length === 0;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.log('✗');
|
|
90
|
+
console.log(` ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
58
94
|
export async function runCheck() {
|
|
59
|
-
const
|
|
60
|
-
|
|
95
|
+
const { isStartAppDir } = await import('../start/entries.js');
|
|
96
|
+
// Run inside a Start app, its own zitejs checks just that app: the workspace
|
|
97
|
+
// root's is a classic release that doesn't know Start.
|
|
98
|
+
const inStartApp = isStartAppDir('.');
|
|
99
|
+
const appDirs = inStartApp ? [] : listSourceDirs();
|
|
100
|
+
if (!inStartApp && appDirs.length === 0) {
|
|
61
101
|
console.error('No apps or automations found under apps/ or automations/.');
|
|
62
102
|
process.exit(1);
|
|
63
103
|
}
|
|
64
|
-
let allPassed = true;
|
|
104
|
+
let allPassed = inStartApp ? await checkStartApp('.') : true;
|
|
65
105
|
for (const { root, dir: app, path: appPath } of appDirs) {
|
|
66
106
|
console.log(`\n── ${appPath} ──`);
|
|
107
|
+
if (isStartAppDir(appPath)) {
|
|
108
|
+
if (!(await checkStartApp(appPath)))
|
|
109
|
+
allPassed = false;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
67
112
|
// Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
|
|
68
113
|
// solution-style config — `"files": []` plus a reference — so
|
|
69
114
|
// `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { LoadedWorkflow } from "./workflowRun.js";
|
|
2
|
+
import type { WorkflowConfig } from "./workflow.js";
|
|
1
3
|
export type Access = {
|
|
2
4
|
kind: "signedIn" | "anyone" | "webhook" | "none";
|
|
3
5
|
roles: string[];
|
|
@@ -7,6 +9,8 @@ export type DescribedFunction = {
|
|
|
7
9
|
name?: string;
|
|
8
10
|
file?: string;
|
|
9
11
|
method?: string;
|
|
12
|
+
/** From the factory's `description` option; display-only. */
|
|
13
|
+
description?: string;
|
|
10
14
|
access: Access;
|
|
11
15
|
input?: unknown;
|
|
12
16
|
problems: string[];
|
|
@@ -16,9 +20,18 @@ export type DescribedRoute = {
|
|
|
16
20
|
methods: Record<string, Access>;
|
|
17
21
|
problems: string[];
|
|
18
22
|
};
|
|
23
|
+
export type DescribedWorkflow = {
|
|
24
|
+
id: string;
|
|
25
|
+
file: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
schedule?: WorkflowConfig["schedule"];
|
|
28
|
+
trigger?: WorkflowConfig["trigger"];
|
|
29
|
+
problems: string[];
|
|
30
|
+
};
|
|
19
31
|
export type Description = {
|
|
20
32
|
functions: DescribedFunction[];
|
|
21
33
|
routes: DescribedRoute[];
|
|
34
|
+
workflows: DescribedWorkflow[];
|
|
22
35
|
};
|
|
23
36
|
export declare function describeFunction(id: string, request: Request): Promise<DescribedFunction>;
|
|
24
37
|
type RouteLike = {
|
|
@@ -32,9 +45,12 @@ type RouteLike = {
|
|
|
32
45
|
};
|
|
33
46
|
};
|
|
34
47
|
export declare function describeRoutes(routesById: Record<string, RouteLike>): DescribedRoute[];
|
|
48
|
+
export declare function describeWorkflows(workflows: LoadedWorkflow[]): DescribedWorkflow[];
|
|
35
49
|
export declare function describeApp(opts: {
|
|
36
50
|
fnIds: string[];
|
|
51
|
+
descriptions?: Record<string, string>;
|
|
37
52
|
getRouter: () => unknown;
|
|
38
53
|
request: Request;
|
|
54
|
+
workflows?: LoadedWorkflow[];
|
|
39
55
|
}): Promise<Description>;
|
|
40
56
|
export {};
|
|
@@ -38,6 +38,8 @@ const describeProbe = createMiddleware({ type: "function" }).server(({ serverFnM
|
|
|
38
38
|
Object.assign(probe, { meta: serverFnMeta, method, reached: true });
|
|
39
39
|
return "zite:describe";
|
|
40
40
|
});
|
|
41
|
+
const SERVER_ONLY = "server-only: nothing in the browser calls it, and a server function that only other server functions call can't run. " +
|
|
42
|
+
"Move the shared logic into a *.server.ts helper and call that.";
|
|
41
43
|
export async function describeFunction(id, request) {
|
|
42
44
|
const problems = [];
|
|
43
45
|
const seen = [];
|
|
@@ -64,7 +66,14 @@ export async function describeFunction(id, request) {
|
|
|
64
66
|
await runWithStartContext(startContext, () => createSsrRpc(id)({ data: undefined, context: {} }));
|
|
65
67
|
}
|
|
66
68
|
catch (error) {
|
|
67
|
-
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
// TanStack registers only functions something can call through it. One that
|
|
71
|
+
// only other server functions call isn't registered, and calling it fails.
|
|
72
|
+
if (message === `Server function info not found for ${id}`) {
|
|
73
|
+
probe = null;
|
|
74
|
+
return { id, access: { kind: "none", roles: [] }, problems: [SERVER_ONLY] };
|
|
75
|
+
}
|
|
76
|
+
problems.push(`probe-failed: ${message}`);
|
|
68
77
|
}
|
|
69
78
|
const state = probe;
|
|
70
79
|
probe = null;
|
|
@@ -113,10 +122,26 @@ export function describeRoutes(routesById) {
|
|
|
113
122
|
}
|
|
114
123
|
return out;
|
|
115
124
|
}
|
|
125
|
+
// A workflow's id is its export name: what the host invokes it by and the editor shows.
|
|
126
|
+
export function describeWorkflows(workflows) {
|
|
127
|
+
return workflows.map(({ id, file, workflow }) => {
|
|
128
|
+
const problems = [];
|
|
129
|
+
if (id === "default")
|
|
130
|
+
problems.push("workflow-default-export: export it by name, which becomes its id");
|
|
131
|
+
const elsewhere = workflows.find((other) => other.id === id && other.file !== file);
|
|
132
|
+
if (elsewhere)
|
|
133
|
+
problems.push(`duplicate-workflow: ${id} is also exported from ${elsewhere.file}`);
|
|
134
|
+
const { description, schedule, trigger } = workflow.config;
|
|
135
|
+
return { id, file, description, ...(schedule ? { schedule } : { trigger }), problems };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
116
138
|
export async function describeApp(opts) {
|
|
117
139
|
const functions = [];
|
|
118
|
-
for (const id of opts.fnIds)
|
|
119
|
-
|
|
140
|
+
for (const id of opts.fnIds) {
|
|
141
|
+
const described = await describeFunction(id, opts.request);
|
|
142
|
+
const description = opts.descriptions?.[id];
|
|
143
|
+
functions.push(description ? { ...described, description } : described);
|
|
144
|
+
}
|
|
120
145
|
const router = (await opts.getRouter());
|
|
121
|
-
return { functions, routes: describeRoutes(router.routesById) };
|
|
146
|
+
return { functions, routes: describeRoutes(router.routesById), workflows: describeWorkflows(opts.workflows ?? []) };
|
|
122
147
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const stringValue = (node) => {
|
|
2
|
+
if (node?.type === "Literal" && typeof node.value === "string")
|
|
3
|
+
return node.value;
|
|
4
|
+
const quasis = node?.quasis;
|
|
5
|
+
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
6
|
+
return quasis?.[0]?.value.cooked;
|
|
7
|
+
return undefined;
|
|
8
|
+
};
|
|
9
|
+
const descriptionProperty = (node) => {
|
|
10
|
+
for (const property of node.properties) {
|
|
11
|
+
const key = property.key;
|
|
12
|
+
const named = key?.type === "Identifier" ? key.name : key?.type === "Literal" ? key.value : undefined;
|
|
13
|
+
if (property.type === "Property" && named === "description")
|
|
14
|
+
return stringValue(property.value);
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
// Walks a builder chain, `signedInFn({ description }).validator(…).handler(…)`, to an object option.
|
|
19
|
+
const descriptionIn = (node) => {
|
|
20
|
+
if (node?.type === "CallExpression") {
|
|
21
|
+
for (const arg of node.arguments) {
|
|
22
|
+
const found = arg.type === "ObjectExpression" ? descriptionProperty(arg) : undefined;
|
|
23
|
+
if (found !== undefined)
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
return descriptionIn(node.callee);
|
|
27
|
+
}
|
|
28
|
+
if (node?.type === "MemberExpression")
|
|
29
|
+
return descriptionIn(node.object);
|
|
30
|
+
return undefined;
|
|
31
|
+
};
|
|
32
|
+
/** `name -> description` for each top-level const built with a description option. */
|
|
33
|
+
export function factoryDescriptions(program) {
|
|
34
|
+
const found = new Map();
|
|
35
|
+
for (const statement of program.body) {
|
|
36
|
+
const declaration = (statement.type === "ExportNamedDeclaration" ? statement.declaration : statement);
|
|
37
|
+
if (declaration?.type !== "VariableDeclaration")
|
|
38
|
+
continue;
|
|
39
|
+
for (const declarator of declaration.declarations) {
|
|
40
|
+
const id = declarator.id;
|
|
41
|
+
const description = descriptionIn(declarator.init);
|
|
42
|
+
if (id.type === "Identifier" && description)
|
|
43
|
+
found.set(id.name, description);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
@@ -8,4 +8,9 @@ export declare const renderStartInstance: (hasAppStart: boolean) => string;
|
|
|
8
8
|
export declare const hasAppStart: (appDir: string) => boolean;
|
|
9
9
|
/** Writes `file` under `appDir` when its content differs. Returns whether it wrote. */
|
|
10
10
|
export declare const writeIfChanged: (appDir: string, file: string, content: string) => boolean;
|
|
11
|
+
export declare const isStartAppDir: (appPath: string) => boolean;
|
|
11
12
|
export declare function regenerateStartEntries(app: SourceDir): void;
|
|
13
|
+
/** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
|
|
14
|
+
export declare function workflowFiles(appDir: string): string[];
|
|
15
|
+
/** The registry zite() builds in place of zitejs's empty one. */
|
|
16
|
+
export declare const renderWorkflowModules: (appDir: string, files: string[]) => string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
3
|
export const START_SERVER_ENTRY = ".zite/server.ts";
|
|
4
4
|
export const START_INSTANCE_ENTRY = ".zite/start.ts";
|
|
5
5
|
const APP_START_FILES = ["src/start.ts", "src/start.tsx"];
|
|
@@ -34,14 +34,15 @@ export const writeIfChanged = (appDir, file, content) => {
|
|
|
34
34
|
writeFileSync(target, content);
|
|
35
35
|
return true;
|
|
36
36
|
};
|
|
37
|
-
const
|
|
37
|
+
export const isStartAppDir = (appPath) => {
|
|
38
38
|
try {
|
|
39
|
-
return JSON.parse(readFileSync(join(
|
|
39
|
+
return JSON.parse(readFileSync(join(appPath, "zite.config.json"), "utf-8"))?.runtime === "start";
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
const isStartApp = (app) => isStartAppDir(app.path);
|
|
45
46
|
export function regenerateStartEntries(app) {
|
|
46
47
|
if (!isStartApp(app))
|
|
47
48
|
return;
|
|
@@ -54,3 +55,28 @@ export function regenerateStartEntries(app) {
|
|
|
54
55
|
console.log(`Wrote ${join(app.path, file)}`);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
const SOURCE_FILE = /\.[mc]?[jt]sx?$/;
|
|
59
|
+
const NOT_APP_CODE = /(\.(test|spec)\.[^.]+|\.d\.ts|routeTree\.gen\.ts)$/;
|
|
60
|
+
/** App modules that mention createWorkflow: what the workflow registry imports. Describe decides what each one really exports. */
|
|
61
|
+
export function workflowFiles(appDir) {
|
|
62
|
+
const found = [];
|
|
63
|
+
const walk = (dir) => {
|
|
64
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
const full = join(dir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
if (entry.name !== "node_modules")
|
|
68
|
+
walk(full);
|
|
69
|
+
}
|
|
70
|
+
else if (SOURCE_FILE.test(entry.name) && !NOT_APP_CODE.test(entry.name) && readFileSync(full, "utf-8").includes("createWorkflow")) {
|
|
71
|
+
found.push(relative(appDir, full).split("\\").join("/"));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
if (existsSync(join(appDir, "src")))
|
|
76
|
+
walk(join(appDir, "src"));
|
|
77
|
+
return found.sort();
|
|
78
|
+
}
|
|
79
|
+
/** The registry zite() builds in place of zitejs's empty one. */
|
|
80
|
+
export const renderWorkflowModules = (appDir, files) => `export const workflowModules = {\n${files
|
|
81
|
+
.map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(join(appDir, file))}),`)
|
|
82
|
+
.join("\n")}\n};\n`;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { User } from "./types.js";
|
|
2
2
|
export type { User, ZiteRequestContext } from "./types.js";
|
|
3
3
|
export { createZiteRouter, type ZiteRouterContext } from "./router.js";
|
|
4
|
+
export { createWorkflow, type Workflow, type WorkflowArgs, type WorkflowConfig } from "./workflow.js";
|
|
5
|
+
export type { ZiteSchedule } from "../schedules/index.js";
|
|
6
|
+
export type { ZiteTrigger } from "../backend/index.js";
|
|
4
7
|
declare module "@tanstack/start-client-core" {
|
|
5
8
|
interface ServerFnOptions {
|
|
6
9
|
/** What this function does, in a sentence. Shown in the editor. */
|
|
@@ -9,6 +12,7 @@ declare module "@tanstack/start-client-core" {
|
|
|
9
12
|
}
|
|
10
13
|
declare const STATUS: {
|
|
11
14
|
readonly BAD_REQUEST: 400;
|
|
15
|
+
readonly INVALID_INPUT: 400;
|
|
12
16
|
readonly UNAUTHORIZED: 401;
|
|
13
17
|
readonly FORBIDDEN: 403;
|
|
14
18
|
readonly NOT_FOUND: 404;
|
|
@@ -17,6 +21,8 @@ declare const STATUS: {
|
|
|
17
21
|
readonly INTERNAL_ERROR: 500;
|
|
18
22
|
};
|
|
19
23
|
export type ZiteErrorCode = keyof typeof STATUS;
|
|
24
|
+
/** @internal The HTTP status each code answers with. */
|
|
25
|
+
export declare const ZITE_ERROR_STATUS: Readonly<Record<ZiteErrorCode, number>>;
|
|
20
26
|
/** Fail with a status: `throw new ZiteError('FORBIDDEN', 'Only the owner can edit this')`. */
|
|
21
27
|
export declare class ZiteError extends Error {
|
|
22
28
|
readonly code: ZiteErrorCode;
|
package/dist/esm/start/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import { USAGE_TOKEN_QUERY_PARAM } from "../auth/constants.js";
|
|
|
4
4
|
import { currentHost } from "./host.js";
|
|
5
5
|
import { getHost, inScope, runInScope } from "./scope.js";
|
|
6
6
|
export { createZiteRouter } from "./router.js";
|
|
7
|
+
export { createWorkflow } from "./workflow.js";
|
|
7
8
|
const STATUS = {
|
|
8
9
|
BAD_REQUEST: 400,
|
|
10
|
+
INVALID_INPUT: 400,
|
|
9
11
|
UNAUTHORIZED: 401,
|
|
10
12
|
FORBIDDEN: 403,
|
|
11
13
|
NOT_FOUND: 404,
|
|
@@ -13,6 +15,8 @@ const STATUS = {
|
|
|
13
15
|
RATE_LIMITED: 429,
|
|
14
16
|
INTERNAL_ERROR: 500,
|
|
15
17
|
};
|
|
18
|
+
/** @internal The HTTP status each code answers with. */
|
|
19
|
+
export const ZITE_ERROR_STATUS = STATUS;
|
|
16
20
|
/** Fail with a status: `throw new ZiteError('FORBIDDEN', 'Only the owner can edit this')`. */
|
|
17
21
|
export class ZiteError extends Error {
|
|
18
22
|
code;
|
|
@@ -62,6 +66,29 @@ const host = () => {
|
|
|
62
66
|
throw new ZiteError("UNAUTHORIZED", "Missing platform context");
|
|
63
67
|
return current;
|
|
64
68
|
};
|
|
69
|
+
// TanStack fails a validator with a plain Error whose message is the issues as JSON.
|
|
70
|
+
const validationIssues = (error) => {
|
|
71
|
+
if (!(error instanceof Error) || error.constructor !== Error)
|
|
72
|
+
return undefined;
|
|
73
|
+
try {
|
|
74
|
+
const issues = JSON.parse(error.message);
|
|
75
|
+
return Array.isArray(issues) && issues.length > 0 && issues.every((issue) => typeof issue?.message === "string")
|
|
76
|
+
? issues
|
|
77
|
+
: undefined;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const invalidInput = (issues) => {
|
|
84
|
+
if (getHost()?.mode === "live")
|
|
85
|
+
return new ZiteError("INVALID_INPUT", "Invalid input");
|
|
86
|
+
const describe = ({ message, path = [] }) => {
|
|
87
|
+
const at = path.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
88
|
+
return at ? `${at}: ${message}` : message;
|
|
89
|
+
};
|
|
90
|
+
return new ZiteError("INVALID_INPUT", issues.map(describe).join("; "));
|
|
91
|
+
};
|
|
65
92
|
// Server functions report a status through TanStack's error envelope; server
|
|
66
93
|
// routes need a Response. A nested call's failure is the caller's to handle.
|
|
67
94
|
const withStatus = async (isServerFn, fn) => {
|
|
@@ -69,7 +96,9 @@ const withStatus = async (isServerFn, fn) => {
|
|
|
69
96
|
try {
|
|
70
97
|
return await fn();
|
|
71
98
|
}
|
|
72
|
-
catch (
|
|
99
|
+
catch (caught) {
|
|
100
|
+
const issues = validationIssues(caught);
|
|
101
|
+
const error = issues ? invalidInput(issues) : caught;
|
|
73
102
|
if (!(error instanceof ZiteError))
|
|
74
103
|
throw error;
|
|
75
104
|
if (!isServerFn)
|
|
@@ -87,10 +116,10 @@ export const signedIn = tag(createMiddleware().server(({ next, serverFnMeta }) =
|
|
|
87
116
|
return runInScope({ kind: "signedIn", user }, () => next({ context: { user } }));
|
|
88
117
|
})), { kind: "signedIn" });
|
|
89
118
|
/** Anyone, signed in or not. Handlers get `context.user: User | null`. */
|
|
90
|
-
export const anyone = tag(createMiddleware().server(({ next }) => {
|
|
119
|
+
export const anyone = tag(createMiddleware().server(({ next, serverFnMeta }) => withStatus(!!serverFnMeta, async () => {
|
|
91
120
|
const { user } = host();
|
|
92
121
|
return runInScope({ kind: "anyone", user }, () => next({ context: { user } }));
|
|
93
|
-
}), { kind: "anyone" });
|
|
122
|
+
})), { kind: "anyone" });
|
|
94
123
|
/** Webhooks delivered by the platform, which already checked the URL token. */
|
|
95
124
|
export const webhook = tag(createMiddleware().server(({ next }) => {
|
|
96
125
|
if (host().invoke?.kind !== "webhook")
|
|
@@ -7,6 +7,6 @@ import { ZiteError } from "./index.js";
|
|
|
7
7
|
* adapters first, then whatever the app's own `src/start.ts` sets.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createZiteStart<const TAdapters extends ReadonlyArray<AnySerializationAdapter> = [], TDefaultSsr extends SSROption = SSROption, const TRequest extends ReadonlyArray<AnyRequestMiddleware> = [], const TFunction extends ReadonlyArray<AnyFunctionMiddleware> = []>(app?: StartInstance<TAdapters, TDefaultSsr, TRequest, TFunction>): StartInstance<readonly [import("@tanstack/react-router").SerializationAdapter<ZiteError, {
|
|
10
|
-
code: "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "INTERNAL_ERROR";
|
|
10
|
+
code: "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "INTERNAL_ERROR" | "INVALID_INPUT";
|
|
11
11
|
message: string;
|
|
12
12
|
}, never>, ...TAdapters], TDefaultSsr, readonly [import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>, ...TRequest], readonly [import("@tanstack/react-start").FunctionMiddlewareAfterClient<{}, unknown, undefined, undefined, undefined>, import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>, ...TFunction]>;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { createCsrfMiddleware, createMiddleware, createStart } from "@tanstack/react-start";
|
|
4
4
|
import { createSerializationAdapter, redirect } from "@tanstack/react-router";
|
|
5
5
|
import { isSignInRequired, loginHref, ZiteError } from "./index.js";
|
|
6
|
+
import { errorOf, preview } from "./runLog.js";
|
|
6
7
|
import { getHost, inScope } from "./scope.js";
|
|
7
8
|
// TanStack drops its default CSRF check once an app has a start instance.
|
|
8
9
|
const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
|
|
@@ -21,23 +22,6 @@ const signInOn401 = createMiddleware({ type: "function" }).client(async ({ next
|
|
|
21
22
|
throw redirect({ href, reloadDocument: true });
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
|
-
const PREVIEW_LIMIT = 8_000;
|
|
25
|
-
const preview = (value) => {
|
|
26
|
-
if (value === undefined)
|
|
27
|
-
return undefined;
|
|
28
|
-
if (typeof value === "object" && value !== null && Symbol.asyncIterator in value)
|
|
29
|
-
return "[stream]";
|
|
30
|
-
try {
|
|
31
|
-
const json = JSON.stringify(value);
|
|
32
|
-
return json && json.length > PREVIEW_LIMIT ? `${json.slice(0, PREVIEW_LIMIT)}…` : json;
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return "[unserializable]";
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
const errorOf = (error) => error instanceof ZiteError
|
|
39
|
-
? { code: error.code, message: error.message }
|
|
40
|
-
: { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : String(error) };
|
|
41
25
|
// Records each call for the run log. The host's own record of the request is the
|
|
42
26
|
// floor; this adds what only the app sees: decoded input, output and errors.
|
|
43
27
|
const runCapture = createMiddleware({ type: "function" }).server(async ({ next, data, serverFnMeta }) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type WithProblems = {
|
|
2
|
+
problems: string[];
|
|
3
|
+
};
|
|
4
|
+
type Described = {
|
|
5
|
+
functions: Array<WithProblems & {
|
|
6
|
+
id: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
}>;
|
|
9
|
+
routes: Array<WithProblems & {
|
|
10
|
+
path: string;
|
|
11
|
+
}>;
|
|
12
|
+
workflows?: Array<WithProblems & {
|
|
13
|
+
id: string;
|
|
14
|
+
file: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
/** Every problem in a description, one line each, named by its rule. Any of them fails the build. */
|
|
18
|
+
export declare function descriptionProblems(description: Described): string[];
|
|
19
|
+
export {};
|