zitejs 0.10.0-start.7 → 0.10.0-start.9

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 (64) hide show
  1. package/dist/cjs/auth/index.d.ts +2 -2
  2. package/dist/cjs/check/index.d.ts +33 -1
  3. package/dist/cjs/check/index.js +136 -86
  4. package/dist/cjs/check/start/describeRules.d.ts +35 -0
  5. package/dist/cjs/check/start/describeRules.js +89 -0
  6. package/dist/cjs/check/start/problem.d.ts +11 -0
  7. package/dist/cjs/check/start/problem.js +5 -0
  8. package/dist/cjs/check/start/sourceRules.d.ts +3 -0
  9. package/dist/cjs/check/start/sourceRules.js +441 -0
  10. package/dist/cjs/cli.js +1 -1
  11. package/dist/cjs/sync/lib.js +4 -7
  12. package/dist/cjs/vite/index.js +8 -3
  13. package/dist/cjs/vite/index.test.js +10 -2
  14. package/dist/esm/auth/index.d.ts +2 -2
  15. package/dist/esm/check/index.d.ts +33 -1
  16. package/dist/esm/check/index.js +135 -88
  17. package/dist/esm/check/start/describeRules.d.ts +35 -0
  18. package/dist/esm/check/start/describeRules.js +86 -0
  19. package/dist/esm/check/start/problem.d.ts +11 -0
  20. package/dist/esm/check/start/problem.js +1 -0
  21. package/dist/esm/check/start/sourceRules.d.ts +3 -0
  22. package/dist/esm/check/start/sourceRules.js +438 -0
  23. package/dist/esm/cli.js +1 -1
  24. package/dist/esm/start/airtable/arrayFields.d.ts +10 -0
  25. package/dist/esm/start/airtable/arrayFields.js +30 -0
  26. package/dist/esm/start/airtable/dispatch.d.ts +54 -0
  27. package/dist/esm/start/airtable/dispatch.js +465 -0
  28. package/dist/esm/start/airtable/formulas.d.ts +69 -0
  29. package/dist/esm/start/airtable/formulas.js +235 -0
  30. package/dist/esm/start/airtable/index.d.ts +11 -0
  31. package/dist/esm/start/airtable/index.js +70 -0
  32. package/dist/esm/start/data/baseRunner.d.ts +92 -0
  33. package/dist/esm/start/data/baseRunner.js +83 -0
  34. package/dist/esm/start/data/dispatch.d.ts +16 -0
  35. package/dist/esm/start/data/dispatch.js +198 -0
  36. package/dist/esm/start/data/index.d.ts +18 -0
  37. package/dist/esm/start/data/index.js +76 -0
  38. package/dist/esm/start/data/sqlNameMappings.d.ts +58 -0
  39. package/dist/esm/start/data/sqlNameMappings.js +92 -0
  40. package/dist/esm/start/data/transformations.d.ts +128 -0
  41. package/dist/esm/start/data/transformations.js +318 -0
  42. package/dist/esm/start/data/types.d.ts +91 -0
  43. package/dist/esm/start/data/types.js +14 -0
  44. package/dist/esm/start/describe.d.ts +5 -0
  45. package/dist/esm/start/describe.js +8 -1
  46. package/dist/esm/start/errors.d.ts +5 -0
  47. package/dist/esm/start/errors.js +5 -0
  48. package/dist/esm/start/factories.test-d.d.ts +1 -0
  49. package/dist/esm/start/factories.test-d.js +20 -0
  50. package/dist/esm/start/index.d.ts +7 -4
  51. package/dist/esm/start/index.js +2 -5
  52. package/dist/esm/start/router.d.ts +1 -0
  53. package/dist/esm/start/router.js +7 -3
  54. package/dist/esm/start/server.js +54 -5
  55. package/dist/esm/start/types.d.ts +40 -0
  56. package/dist/esm/start/version.d.ts +1 -1
  57. package/dist/esm/start/version.js +1 -1
  58. package/dist/esm/start/vite.js +2 -1
  59. package/dist/esm/start/workflow.d.ts +3 -0
  60. package/dist/esm/start/workflow.test-d.js +5 -0
  61. package/dist/esm/sync/lib.js +4 -7
  62. package/dist/esm/vite/index.js +8 -3
  63. package/dist/esm/vite/index.test.js +10 -2
  64. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from 'child_process';
2
- import { existsSync } from 'fs';
3
- import { join } from 'path';
2
+ import { existsSync, readdirSync, readFileSync } from 'fs';
3
+ import { join, relative } from 'path';
4
4
  import { listEndpointNames, listSourceDirs } from '../sourceRoots.js';
5
+ import { formatProblem } from './start/problem.js';
5
6
  /**
6
7
  * An argument array, not a command string, so no shell is involved.
7
8
  *
@@ -57,41 +58,132 @@ function bundleFailures(output) {
57
58
  }
58
59
  const indent = (output) => output.trim().split('\n').map(l => ` ${l}`).join('\n');
59
60
  /**
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.
61
+ * While `--json` checks, stdout carries only the report: app code that logs as
62
+ * it's described in-process goes to stderr instead.
63
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));
64
+ export const withStdoutOnStderr = async (run) => {
65
+ const write = process.stdout.write;
66
+ process.stdout.write = process.stderr.write.bind(process.stderr);
67
+ try {
68
+ return await run();
69
+ }
70
+ finally {
71
+ process.stdout.write = write;
72
+ }
73
+ };
74
+ /** Prints as it goes, or with `--json` stays quiet and reports everything at the end. */
75
+ export class Reporter {
76
+ json;
77
+ apps = [];
78
+ constructor(json) {
79
+ this.json = json;
80
+ }
81
+ app(path, runtime, heading) {
82
+ const app = { path, runtime, ok: true, steps: [], problems: [] };
83
+ this.apps.push(app);
84
+ if (heading && !this.json)
85
+ console.log(`\n── ${path} ──`);
86
+ return app;
87
+ }
88
+ step(app, name, result) {
89
+ const output = result.output?.trim();
90
+ app.steps.push({ name, ok: result.ok, ...(output && !result.ok && { output }) });
91
+ if (!result.ok)
92
+ app.ok = false;
93
+ if (!this.json) {
94
+ console.log(` ${name} ... ${result.ok ? '✓' : '✗'}`);
95
+ if (!result.ok && output)
96
+ console.log(indent(output));
97
+ }
98
+ return result.ok;
99
+ }
100
+ problems(app, problems) {
101
+ app.problems.push(...problems);
102
+ if (problems.some(p => p.severity === 'error'))
103
+ app.ok = false;
104
+ if (!this.json && problems.length)
105
+ console.log(problems.map(p => ` ${formatProblem(p)}`).join('\n'));
106
+ }
107
+ }
108
+ const SKIPPED_DIRS = new Set(['node_modules', 'dist', '.output', '.tanstack', '.wrangler']);
109
+ /** The app's own source, by app-relative path. */
110
+ function readAppSources(appPath) {
111
+ const sources = {};
112
+ const visit = (dir) => {
113
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
114
+ if (SKIPPED_DIRS.has(entry.name))
115
+ continue;
116
+ const full = join(dir, entry.name);
117
+ if (entry.isDirectory())
118
+ visit(full);
119
+ else if (/\.[cm]?[jt]sx?$/.test(entry.name))
120
+ sources[relative(appPath, full)] = readFileSync(full, 'utf-8');
121
+ }
122
+ };
123
+ const src = join(appPath, 'src');
124
+ if (existsSync(src))
125
+ visit(src);
126
+ return sources;
127
+ }
128
+ const isInternalApp = (appPath) => {
129
+ try {
130
+ return JSON.parse(readFileSync(join(appPath, 'zite.config.json'), 'utf-8'))?.accessMode === 'internal';
131
+ }
132
+ catch {
70
133
  return false;
71
134
  }
72
- process.stdout.write(' tsc --noEmit ... ');
135
+ };
136
+ /**
137
+ * A Start app: `vite build` first, since it regenerates the route tree tsc
138
+ * reads, then tsc, then describe, which reports what the platform's build
139
+ * refuses, read the same way, then the source hints.
140
+ */
141
+ async function checkStartApp(reporter, app, appPath) {
142
+ if (!reporter.step(app, 'vite build', run('npx', ['vite', 'build'], appPath)))
143
+ return;
73
144
  // --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 ... ');
145
+ reporter.step(app, 'tsc --noEmit', run('npx', ['tsc', '--noEmit', '--pretty', '-p', 'tsconfig.app.json'], appPath));
146
+ const { describedProblems } = await import('./start/describeRules.js');
147
+ const { sourceProblems } = await import('./start/sourceRules.js');
79
148
  try {
80
149
  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;
150
+ const description = (await describeBuiltApp(appPath));
151
+ const problems = describedProblems(description, { internal: isInternalApp(appPath) });
152
+ reporter.step(app, 'describe', { ok: !problems.some(p => p.severity === 'error') });
153
+ reporter.problems(app, problems);
87
154
  }
88
155
  catch (err) {
89
- console.log('✗');
90
- console.log(` ${err instanceof Error ? err.message : String(err)}`);
91
- return false;
156
+ reporter.step(app, 'describe', { ok: false, output: err instanceof Error ? err.message : String(err) });
157
+ }
158
+ const hints = sourceProblems(readAppSources(appPath));
159
+ reporter.step(app, 'source', { ok: !hints.some(p => p.severity === 'error') });
160
+ reporter.problems(app, hints);
161
+ }
162
+ function checkClassicApp(reporter, app, root, dir, appPath) {
163
+ // Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
164
+ // solution-style config — `"files": []` plus a reference — so
165
+ // `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
166
+ // used to be the fallback, which meant an app missing tsconfig.app.json
167
+ // reported a passing typecheck it never ran.
168
+ const tsconfigAppPath = join(appPath, 'tsconfig.app.json');
169
+ reporter.step(app, 'tsc --noEmit', existsSync(tsconfigAppPath)
170
+ ? run('npx', ['tsc', '--noEmit', '-p', tsconfigAppPath], '.')
171
+ : { ok: false, output: `no ${tsconfigAppPath}` });
172
+ // Endpoints never reach vite — they are bundled separately for the lambda,
173
+ // against a different resolver. tsc and vite both pass on an endpoint whose
174
+ // `zitejs/*` alias has no generated file behind it; only the bundler knows.
175
+ if (listEndpointNames(root, appPath).length > 0) {
176
+ const bundle = run('npx', ['zitejs', 'bundle', '--app', dir], '.');
177
+ const failures = bundle.ok ? bundleFailures(bundle.output) : [bundle.output];
178
+ reporter.step(app, 'bundle endpoints', { ok: failures.length === 0, output: failures.join('\n') });
179
+ }
180
+ if (existsSync(join(appPath, 'vite.config.ts'))) {
181
+ reporter.step(app, 'vite build', run('npx', ['vite', 'build'], appPath));
92
182
  }
93
183
  }
94
- export async function runCheck() {
184
+ export async function runCheck(args = process.argv.slice(3)) {
185
+ const json = args.includes('--json');
186
+ const reporter = new Reporter(json);
95
187
  const { isStartAppDir } = await import('../start/entries.js');
96
188
  // Run inside a Start app, its own zitejs checks just that app: the workspace
97
189
  // root's is a classic release that doesn't know Start.
@@ -101,72 +193,27 @@ export async function runCheck() {
101
193
  console.error('No apps or automations found under apps/ or automations/.');
102
194
  process.exit(1);
103
195
  }
104
- let allPassed = inStartApp ? await checkStartApp('.') : true;
105
- for (const { root, dir: app, path: appPath } of appDirs) {
106
- console.log(`\n── ${appPath} ──`);
107
- if (isStartAppDir(appPath)) {
108
- if (!(await checkStartApp(appPath)))
109
- allPassed = false;
110
- continue;
111
- }
112
- // Only tsconfig.app.json compiles anything. The app's tsconfig.json is a
113
- // solution-style config — `"files": []` plus a reference — so
114
- // `tsc --noEmit -p tsconfig.json` type-checks zero files and exits 0. It
115
- // used to be the fallback, which meant an app missing tsconfig.app.json
116
- // reported a passing typecheck it never ran.
117
- const tsconfigAppPath = join(appPath, 'tsconfig.app.json');
118
- if (!existsSync(tsconfigAppPath)) {
119
- console.log(` tsc --noEmit ... ✗ (no ${tsconfigAppPath})`);
120
- allPassed = false;
121
- }
122
- else {
123
- process.stdout.write(' tsc --noEmit ... ');
124
- const tsc = run('npx', ['tsc', '--noEmit', '-p', tsconfigAppPath], '.');
125
- if (tsc.ok) {
126
- console.log('✓');
127
- }
128
- else {
129
- console.log('✗');
130
- console.log(tsc.output.trim().split('\n').map(l => ` ${l}`).join('\n'));
131
- allPassed = false;
132
- }
133
- }
134
- // Endpoints never reach vite — they are bundled separately for the lambda,
135
- // against a different resolver. tsc and vite both pass on an endpoint whose
136
- // `zitejs/*` alias has no generated file behind it; only the bundler knows.
137
- if (listEndpointNames(root, appPath).length > 0) {
138
- process.stdout.write(' bundle endpoints ... ');
139
- const bundle = run('npx', ['zitejs', 'bundle', '--app', app], '.');
140
- const failures = bundle.ok ? bundleFailures(bundle.output) : [bundle.output];
141
- if (failures.length === 0) {
142
- console.log('✓');
196
+ const checkAll = async () => {
197
+ if (inStartApp)
198
+ await checkStartApp(reporter, reporter.app('.', 'start', false), '.');
199
+ for (const { root, dir, path: appPath } of appDirs) {
200
+ if (isStartAppDir(appPath)) {
201
+ await checkStartApp(reporter, reporter.app(appPath, 'start', true), appPath);
143
202
  }
144
203
  else {
145
- console.log('✗');
146
- console.log(failures.map(l => ` ${l}`).join('\n'));
147
- allPassed = false;
204
+ checkClassicApp(reporter, reporter.app(appPath, 'classic', true), root, dir, appPath);
148
205
  }
149
206
  }
150
- const viteConfig = join(appPath, 'vite.config.ts');
151
- if (existsSync(viteConfig)) {
152
- process.stdout.write(' vite build ... ');
153
- const vite = run('npx', ['vite', 'build'], appPath);
154
- if (vite.ok) {
155
- console.log('✓');
156
- }
157
- else {
158
- console.log('✗');
159
- console.log(vite.output.trim().split('\n').map(l => ` ${l}`).join('\n'));
160
- allPassed = false;
161
- }
162
- }
163
- }
164
- console.log('');
165
- if (allPassed) {
166
- console.log('All apps pass.');
207
+ };
208
+ await (json ? withStdoutOnStderr(checkAll) : checkAll());
209
+ const allPassed = reporter.apps.every(app => app.ok);
210
+ if (json) {
211
+ process.stdout.write(`${JSON.stringify({ ok: allPassed, apps: reporter.apps })}\n`);
167
212
  }
168
213
  else {
169
- console.log('Some apps failed.');
170
- process.exit(1);
214
+ console.log('');
215
+ console.log(allPassed ? 'All apps pass.' : 'Some apps failed.');
171
216
  }
217
+ if (!allPassed)
218
+ process.exit(1);
172
219
  }
@@ -0,0 +1,35 @@
1
+ import type { Problem } from "./problem.js";
2
+ type Access = {
3
+ kind: string;
4
+ roles?: string[];
5
+ };
6
+ export type CheckedDescription = {
7
+ functions: Array<{
8
+ id: string;
9
+ name?: string;
10
+ file?: string;
11
+ access: Access;
12
+ input?: unknown;
13
+ problems: string[];
14
+ }>;
15
+ routes: Array<{
16
+ path: string;
17
+ file?: string;
18
+ description?: string;
19
+ methods: Record<string, Access>;
20
+ problems: string[];
21
+ }>;
22
+ pages?: Array<{
23
+ path: string;
24
+ file?: string;
25
+ }>;
26
+ workflows?: Array<{
27
+ id: string;
28
+ file: string;
29
+ problems: string[];
30
+ }>;
31
+ };
32
+ export declare function describedProblems(description: CheckedDescription, { internal }: {
33
+ internal: boolean;
34
+ }): Problem[];
35
+ export {};
@@ -0,0 +1,86 @@
1
+ // The host, TanStack and Cloudflare answer these before the app does.
2
+ const RESERVED_PATHS = [
3
+ /^\/_zite/,
4
+ /^\/__zite/,
5
+ /^\/_serverFn(\/|$)/,
6
+ /^\/cdn-cgi(\/|$)/,
7
+ /^\/auth(\/|$)/,
8
+ /^\/api\/auth(\/|$)/,
9
+ /^\/manifest\.webmanifest$/,
10
+ ];
11
+ const OWNER_INPUTS = ["userId", "ownerId"];
12
+ /** `access-missing: no signedIn…`, or a route's `GET: access-missing: …`, as a named problem. */
13
+ const describeProblem = (problem, subject, file) => {
14
+ const match = /^(?:(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|ANY): )?([a-z][a-z-]*): (.*)$/s.exec(problem);
15
+ const [, method, rule, message] = match ?? [undefined, undefined, "describe", problem];
16
+ return {
17
+ rule: rule ?? "describe",
18
+ severity: "error",
19
+ ...(file && { file }),
20
+ message: `${subject}${method ? ` ${method}` : ""}: ${message ?? problem}`,
21
+ };
22
+ };
23
+ const inputProperties = (input) => {
24
+ const properties = input?.properties;
25
+ return properties && typeof properties === "object" ? Object.keys(properties) : [];
26
+ };
27
+ export function describedProblems(description, { internal }) {
28
+ const problems = [];
29
+ for (const fn of description.functions) {
30
+ const subject = `function ${fn.name ?? fn.id}`;
31
+ problems.push(...fn.problems.map((p) => describeProblem(p, subject, fn.file)));
32
+ if (internal && fn.access.kind === "anyone") {
33
+ problems.push({
34
+ rule: "public-in-internal",
35
+ severity: "error",
36
+ ...(fn.file && { file: fn.file }),
37
+ message: `${subject} is a publicFn, and this internal app signs everyone in; make it a signedInFn`,
38
+ });
39
+ }
40
+ const owner = fn.access.kind === "signedIn" && inputProperties(fn.input).find((p) => OWNER_INPUTS.includes(p));
41
+ if (owner) {
42
+ problems.push({
43
+ rule: "owner-from-input",
44
+ severity: "warning",
45
+ ...(fn.file && { file: fn.file }),
46
+ message: `${subject} takes ${owner} as input, which any caller can set; use context.user.id`,
47
+ });
48
+ }
49
+ }
50
+ for (const route of description.routes) {
51
+ const subject = `route ${route.path}`;
52
+ problems.push(...route.problems.map((p) => describeProblem(p, subject, route.file)));
53
+ const publicMethods = Object.entries(route.methods).filter(([, access]) => access.kind === "anyone");
54
+ if (internal && publicMethods.length > 0) {
55
+ problems.push({
56
+ rule: "public-in-internal",
57
+ severity: "error",
58
+ ...(route.file && { file: route.file }),
59
+ message: `${subject} lets anyone call ${publicMethods.map(([m]) => m).join(", ")}, and this internal app signs everyone in; use signedIn`,
60
+ });
61
+ }
62
+ if (!route.description) {
63
+ problems.push({
64
+ rule: "description-missing",
65
+ severity: "warning",
66
+ ...(route.file && { file: route.file }),
67
+ message: `${subject} has no description; add staticData: { description } for the editor`,
68
+ });
69
+ }
70
+ }
71
+ for (const workflow of description.workflows ?? []) {
72
+ problems.push(...workflow.problems.map((p) => describeProblem(p, `workflow ${workflow.id}`, workflow.file)));
73
+ }
74
+ const paths = new Map([...description.routes, ...(description.pages ?? [])].map((r) => [r.path, r.file]));
75
+ for (const [path, file] of paths) {
76
+ if (RESERVED_PATHS.some((reserved) => reserved.test(path))) {
77
+ problems.push({
78
+ rule: "reserved-path",
79
+ severity: "error",
80
+ ...(file && { file }),
81
+ message: `${path} is the platform's; give the route another path`,
82
+ });
83
+ }
84
+ }
85
+ return problems;
86
+ }
@@ -0,0 +1,11 @@
1
+ export type Severity = "error" | "warning";
2
+ /** One finding of `zitejs check`, named by its rule. Errors fail the check; warnings only report. */
3
+ export type Problem = {
4
+ rule: string;
5
+ severity: Severity;
6
+ /** App-relative. */
7
+ file?: string;
8
+ line?: number;
9
+ message: string;
10
+ };
11
+ export declare const formatProblem: ({ rule, severity, file, line, message }: Problem) => string;
@@ -0,0 +1 @@
1
+ export const formatProblem = ({ rule, severity, file, line, message }) => `${severity} ${rule}${file ? ` ${file}${line ? `:${line}` : ""}` : ""}: ${message}`;
@@ -0,0 +1,3 @@
1
+ import type { Problem } from "./problem.js";
2
+ /** Every source hint for one Start app, given its files by app-relative path. */
3
+ export declare function sourceProblems(sources: Record<string, string>): Problem[];