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.
- package/dist/cjs/auth/index.d.ts +2 -2
- package/dist/cjs/check/index.d.ts +33 -1
- package/dist/cjs/check/index.js +136 -86
- package/dist/cjs/check/start/describeRules.d.ts +35 -0
- package/dist/cjs/check/start/describeRules.js +89 -0
- package/dist/cjs/check/start/problem.d.ts +11 -0
- package/dist/cjs/check/start/problem.js +5 -0
- package/dist/cjs/check/start/sourceRules.d.ts +3 -0
- package/dist/cjs/check/start/sourceRules.js +441 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/sync/lib.js +4 -7
- package/dist/cjs/vite/index.js +8 -3
- package/dist/cjs/vite/index.test.js +10 -2
- package/dist/esm/auth/index.d.ts +2 -2
- package/dist/esm/check/index.d.ts +33 -1
- package/dist/esm/check/index.js +135 -88
- package/dist/esm/check/start/describeRules.d.ts +35 -0
- package/dist/esm/check/start/describeRules.js +86 -0
- package/dist/esm/check/start/problem.d.ts +11 -0
- package/dist/esm/check/start/problem.js +1 -0
- package/dist/esm/check/start/sourceRules.d.ts +3 -0
- package/dist/esm/check/start/sourceRules.js +438 -0
- package/dist/esm/cli.js +1 -1
- package/dist/esm/start/airtable/arrayFields.d.ts +10 -0
- package/dist/esm/start/airtable/arrayFields.js +30 -0
- package/dist/esm/start/airtable/dispatch.d.ts +54 -0
- package/dist/esm/start/airtable/dispatch.js +465 -0
- package/dist/esm/start/airtable/formulas.d.ts +69 -0
- package/dist/esm/start/airtable/formulas.js +235 -0
- package/dist/esm/start/airtable/index.d.ts +11 -0
- package/dist/esm/start/airtable/index.js +70 -0
- package/dist/esm/start/data/baseRunner.d.ts +92 -0
- package/dist/esm/start/data/baseRunner.js +83 -0
- package/dist/esm/start/data/dispatch.d.ts +16 -0
- package/dist/esm/start/data/dispatch.js +198 -0
- package/dist/esm/start/data/index.d.ts +18 -0
- package/dist/esm/start/data/index.js +76 -0
- package/dist/esm/start/data/sqlNameMappings.d.ts +58 -0
- package/dist/esm/start/data/sqlNameMappings.js +92 -0
- package/dist/esm/start/data/transformations.d.ts +128 -0
- package/dist/esm/start/data/transformations.js +318 -0
- package/dist/esm/start/data/types.d.ts +91 -0
- package/dist/esm/start/data/types.js +14 -0
- package/dist/esm/start/describe.d.ts +5 -0
- package/dist/esm/start/describe.js +8 -1
- package/dist/esm/start/errors.d.ts +5 -0
- package/dist/esm/start/errors.js +5 -0
- package/dist/esm/start/factories.test-d.d.ts +1 -0
- package/dist/esm/start/factories.test-d.js +20 -0
- package/dist/esm/start/index.d.ts +7 -4
- package/dist/esm/start/index.js +2 -5
- package/dist/esm/start/router.d.ts +1 -0
- package/dist/esm/start/router.js +7 -3
- package/dist/esm/start/server.js +54 -5
- package/dist/esm/start/types.d.ts +40 -0
- package/dist/esm/start/version.d.ts +1 -1
- package/dist/esm/start/version.js +1 -1
- package/dist/esm/start/vite.js +2 -1
- package/dist/esm/start/workflow.d.ts +3 -0
- package/dist/esm/start/workflow.test-d.js +5 -0
- package/dist/esm/sync/lib.js +4 -7
- package/dist/esm/vite/index.js +8 -3
- package/dist/esm/vite/index.test.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sourceProblems = sourceProblems;
|
|
4
|
+
// Source hints for a Start app: line-level findings read from the code, never
|
|
5
|
+
// access decisions, which come from describe. A file that doesn't parse is
|
|
6
|
+
// skipped; tsc reports it.
|
|
7
|
+
const parser_1 = require("@babel/parser");
|
|
8
|
+
const isNode = (value) => typeof value === "object" && value !== null && typeof value.type === "string";
|
|
9
|
+
const SKIP_KEYS = new Set(["loc", "start", "end", "extra", "leadingComments", "trailingComments", "innerComments", "range"]);
|
|
10
|
+
const walk = (node, visit, ancestors = []) => {
|
|
11
|
+
visit(node, ancestors);
|
|
12
|
+
ancestors.push(node);
|
|
13
|
+
for (const key of Object.keys(node)) {
|
|
14
|
+
if (SKIP_KEYS.has(key))
|
|
15
|
+
continue;
|
|
16
|
+
const value = node[key];
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (const child of value)
|
|
19
|
+
if (isNode(child))
|
|
20
|
+
walk(child, visit, ancestors);
|
|
21
|
+
}
|
|
22
|
+
else if (isNode(value)) {
|
|
23
|
+
walk(value, visit, ancestors);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
ancestors.pop();
|
|
27
|
+
};
|
|
28
|
+
const FUNCTION_TYPES = new Set([
|
|
29
|
+
"FunctionDeclaration",
|
|
30
|
+
"FunctionExpression",
|
|
31
|
+
"ArrowFunctionExpression",
|
|
32
|
+
"ObjectMethod",
|
|
33
|
+
"ClassMethod",
|
|
34
|
+
"ClassPrivateMethod",
|
|
35
|
+
]);
|
|
36
|
+
const LOOP_TYPES = new Set(["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement"]);
|
|
37
|
+
const atModuleScope = (ancestors) => !ancestors.some((a) => FUNCTION_TYPES.has(a.type));
|
|
38
|
+
const lineOf = (node) => node.loc?.start.line;
|
|
39
|
+
const nameOf = (node) => {
|
|
40
|
+
if (!isNode(node))
|
|
41
|
+
return undefined;
|
|
42
|
+
if (node.type === "Identifier")
|
|
43
|
+
return node.name;
|
|
44
|
+
if (node.type === "StringLiteral")
|
|
45
|
+
return node.value;
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const propertyNamed = (object, name) => object.properties.find((p) => (p.type === "ObjectProperty" || p.type === "ObjectMethod") && !p.computed && nameOf(p.key) === name);
|
|
49
|
+
const unwrap = (node) => node.type === "TSNonNullExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression"
|
|
50
|
+
? unwrap(node.expression)
|
|
51
|
+
: node;
|
|
52
|
+
/** `a.b(…).c(…)`: the calls from the outside in, the member names, and the root identifier. */
|
|
53
|
+
const chainOf = (start) => {
|
|
54
|
+
const calls = [];
|
|
55
|
+
const members = [];
|
|
56
|
+
let current = start;
|
|
57
|
+
while (current) {
|
|
58
|
+
current = unwrap(current);
|
|
59
|
+
if (current.type === "CallExpression") {
|
|
60
|
+
calls.push(current);
|
|
61
|
+
current = current.callee;
|
|
62
|
+
}
|
|
63
|
+
else if (current.type === "MemberExpression" && !current.computed) {
|
|
64
|
+
const member = nameOf(current.property);
|
|
65
|
+
if (member === undefined)
|
|
66
|
+
return { calls, members };
|
|
67
|
+
members.push(member);
|
|
68
|
+
current = current.object;
|
|
69
|
+
}
|
|
70
|
+
else if (current.type === "Identifier") {
|
|
71
|
+
return { root: current.name, calls, members };
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return { calls, members };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { calls, members };
|
|
78
|
+
};
|
|
79
|
+
const isHandlerCall = (node) => node.type === "CallExpression" && isNode(node.callee) && node.callee.type === "MemberExpression" && nameOf(node.callee.property) === "handler";
|
|
80
|
+
const BUILDER_MEMBERS = new Set(["handler", "validator", "inputValidator", "middleware"]);
|
|
81
|
+
/** The call that hands the factory its options: `signedInFn({ … })`, not `.validator(…)`. */
|
|
82
|
+
const optionsCall = (calls) => calls.find((call) => {
|
|
83
|
+
const callee = call.callee;
|
|
84
|
+
return !(callee.type === "MemberExpression" && BUILDER_MEMBERS.has(nameOf(callee.property) ?? ""));
|
|
85
|
+
});
|
|
86
|
+
const hasMethodOption = (calls) => {
|
|
87
|
+
const call = optionsCall(calls);
|
|
88
|
+
const options = call && call.arguments[0];
|
|
89
|
+
return !!options && options.type === "ObjectExpression" && !!propertyNamed(options, "method");
|
|
90
|
+
};
|
|
91
|
+
const isTopLevelDeclarator = (ancestors) => {
|
|
92
|
+
const [declarator, declaration, outer, program] = ancestors.slice(-4).reverse();
|
|
93
|
+
if (declarator?.type !== "VariableDeclarator" || declaration?.type !== "VariableDeclaration")
|
|
94
|
+
return false;
|
|
95
|
+
return outer?.type === "Program" || (outer?.type === "ExportNamedDeclaration" && program?.type === "Program");
|
|
96
|
+
};
|
|
97
|
+
const parseFile = (path, code) => {
|
|
98
|
+
try {
|
|
99
|
+
const ast = (0, parser_1.parse)(code, {
|
|
100
|
+
sourceType: "module",
|
|
101
|
+
plugins: path.endsWith("x") ? ["typescript", "jsx"] : ["typescript"],
|
|
102
|
+
errorRecovery: true,
|
|
103
|
+
});
|
|
104
|
+
const imports = new Map();
|
|
105
|
+
for (const statement of ast.program.body) {
|
|
106
|
+
if (statement.type !== "ImportDeclaration")
|
|
107
|
+
continue;
|
|
108
|
+
const source = statement.source.value;
|
|
109
|
+
for (const specifier of statement.specifiers) {
|
|
110
|
+
const local = nameOf(specifier.local);
|
|
111
|
+
if (!local)
|
|
112
|
+
continue;
|
|
113
|
+
const imported = specifier.type === "ImportSpecifier" ? (nameOf(specifier.imported) ?? local) : "default";
|
|
114
|
+
imports.set(local, { source, imported });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { path, program: ast.program, imports };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const FACTORIES = ["signedInFn", "publicFn", "createServerFn"];
|
|
124
|
+
/**
|
|
125
|
+
* The names a server function can start from, app-wide: zitejs's factories,
|
|
126
|
+
* TanStack's raw one, and the app's own factories built from them
|
|
127
|
+
* (`export const managerFn = signedInFn.middleware([role("manager")])`).
|
|
128
|
+
*/
|
|
129
|
+
const factoryNames = (files) => {
|
|
130
|
+
const names = new Set(FACTORIES);
|
|
131
|
+
for (let changed = true; changed;) {
|
|
132
|
+
changed = false;
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
const isFactory = (local) => names.has(file.imports.get(local)?.imported ?? local);
|
|
135
|
+
for (const statement of file.program.body) {
|
|
136
|
+
if (statement.type === "ExportNamedDeclaration" && statement.source) {
|
|
137
|
+
for (const specifier of statement.specifiers) {
|
|
138
|
+
const exported = nameOf(specifier.exported);
|
|
139
|
+
if (exported && names.has(nameOf(specifier.local) ?? "") && !names.has(exported)) {
|
|
140
|
+
names.add(exported);
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
146
|
+
if (declaration?.type !== "VariableDeclaration")
|
|
147
|
+
continue;
|
|
148
|
+
for (const declarator of declaration.declarations) {
|
|
149
|
+
const name = nameOf(declarator.id);
|
|
150
|
+
const init = declarator.init;
|
|
151
|
+
if (!name || !init || names.has(name) || isHandlerCall(unwrap(init)))
|
|
152
|
+
continue;
|
|
153
|
+
const { root } = chainOf(init);
|
|
154
|
+
if (root && isFactory(root)) {
|
|
155
|
+
names.add(name);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return names;
|
|
163
|
+
};
|
|
164
|
+
const callsCreateWorkflow = (program) => {
|
|
165
|
+
let found = false;
|
|
166
|
+
walk(program, (node) => {
|
|
167
|
+
if (node.type === "CallExpression" && nameOf(node.callee) === "createWorkflow")
|
|
168
|
+
found = true;
|
|
169
|
+
});
|
|
170
|
+
return found;
|
|
171
|
+
};
|
|
172
|
+
const isServerReachable = (path, callsWorkflow) => /\.(functions|server)\.tsx?$/.test(path) || path.startsWith("src/routes/api/") || path === "src/start.ts" || callsWorkflow;
|
|
173
|
+
/** The names an import brings in as values; types are erased, so they never reach the runtime. */
|
|
174
|
+
const valueImports = (statement) => {
|
|
175
|
+
if (statement.importKind === "type")
|
|
176
|
+
return [];
|
|
177
|
+
const specifiers = statement.specifiers.filter((s) => s.importKind !== "type");
|
|
178
|
+
if (statement.specifiers.length === 0)
|
|
179
|
+
return "all";
|
|
180
|
+
if (specifiers.some((s) => s.type !== "ImportSpecifier"))
|
|
181
|
+
return "all";
|
|
182
|
+
return specifiers.map((s) => nameOf(s.imported) ?? "");
|
|
183
|
+
};
|
|
184
|
+
const MUTATING_METHODS = new Set(["set", "add", "push", "unshift", "splice", "delete", "clear"]);
|
|
185
|
+
const isContainer = (init) => !!init &&
|
|
186
|
+
((init.type === "NewExpression" && ["Map", "Set"].includes(nameOf(init.callee) ?? "")) ||
|
|
187
|
+
init.type === "ArrayExpression" ||
|
|
188
|
+
init.type === "ObjectExpression");
|
|
189
|
+
const callsAny = (node, matches) => {
|
|
190
|
+
let found = false;
|
|
191
|
+
walk(node, (n) => {
|
|
192
|
+
if (n.type === "CallExpression" && isNode(n.callee) && matches(n.callee))
|
|
193
|
+
found = true;
|
|
194
|
+
});
|
|
195
|
+
return found;
|
|
196
|
+
};
|
|
197
|
+
const ROOT_LAYOUTS = new Set(["src/routes/__root.tsx", "src/routes/_app.tsx", "src/routes/_app/route.tsx"]);
|
|
198
|
+
// The Start runtime reads a ZiteError thrown from any copy of zitejs, classic's included.
|
|
199
|
+
const CLASSIC_ALLOWED = { "zitejs/backend": new Set(["ZiteError"]) };
|
|
200
|
+
const CLASSIC_IMPORTS = {
|
|
201
|
+
"zitejs/backend": "server code is a signedInFn/publicFn server function from zitejs/start",
|
|
202
|
+
"zitejs/api": "pages call server functions directly, from a loader or an event handler",
|
|
203
|
+
"react-router-dom": "routing is file routes, with Link and useNavigate from @tanstack/react-router",
|
|
204
|
+
"react-router": "routing is file routes, with Link and useNavigate from @tanstack/react-router",
|
|
205
|
+
};
|
|
206
|
+
const importsModule = (source, module) => source === module || source.startsWith(`${module}/`);
|
|
207
|
+
const containsTypeofWindow = (node) => {
|
|
208
|
+
let found = false;
|
|
209
|
+
walk(node, (n) => {
|
|
210
|
+
if (n.type === "UnaryExpression" && n.operator === "typeof" && nameOf(n.argument) === "window")
|
|
211
|
+
found = true;
|
|
212
|
+
});
|
|
213
|
+
return found;
|
|
214
|
+
};
|
|
215
|
+
const GUARD_TYPES = new Set(["IfStatement", "ConditionalExpression", "LogicalExpression"]);
|
|
216
|
+
const guardedByTypeofWindow = (ancestors) => ancestors.some((a) => GUARD_TYPES.has(a.type) && containsTypeofWindow((a.test ?? a.left)));
|
|
217
|
+
const isRelativePath = (node) => (node?.type === "StringLiteral" && node.value.startsWith("/")) ||
|
|
218
|
+
(node?.type === "TemplateLiteral" && (node.quasis[0]?.value?.cooked ?? "").startsWith("/"));
|
|
219
|
+
/** Every source hint for one Start app, given its files by app-relative path. */
|
|
220
|
+
function sourceProblems(sources) {
|
|
221
|
+
const files = Object.entries(sources)
|
|
222
|
+
.filter(([path]) => /\.[cm]?[jt]sx?$/.test(path) && !path.endsWith("routeTree.gen.ts") && !path.endsWith(".d.ts"))
|
|
223
|
+
.flatMap(([path, code]) => parseFile(path, code) ?? []);
|
|
224
|
+
const factories = factoryNames(files);
|
|
225
|
+
const problems = [];
|
|
226
|
+
const report = (rule, severity, file, node, message) => problems.push({ rule, severity, file, ...(node && lineOf(node) && { line: lineOf(node) }), message });
|
|
227
|
+
const serverFnFiles = new Map();
|
|
228
|
+
if (!sources["src/router.tsx"]?.includes("createZiteRouter(")) {
|
|
229
|
+
problems.push({
|
|
230
|
+
rule: "router-file",
|
|
231
|
+
severity: "error",
|
|
232
|
+
file: "src/router.tsx",
|
|
233
|
+
message: "export getRouter from src/router.tsx, built with createZiteRouter from zitejs/start, which gives pages the signed-in user",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
const { path, program } = file;
|
|
238
|
+
const factoryRoot = (root) => !!root && factories.has(file.imports.get(root)?.imported ?? root);
|
|
239
|
+
const callsWorkflow = callsCreateWorkflow(program);
|
|
240
|
+
const serverReachable = isServerReachable(path, callsWorkflow);
|
|
241
|
+
const localServerFns = new Set();
|
|
242
|
+
const moduleContainers = new Map();
|
|
243
|
+
for (const statement of program.body) {
|
|
244
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
245
|
+
if (declaration?.type !== "VariableDeclaration")
|
|
246
|
+
continue;
|
|
247
|
+
for (const declarator of declaration.declarations) {
|
|
248
|
+
const name = nameOf(declarator.id);
|
|
249
|
+
const init = declarator.init ? unwrap(declarator.init) : undefined;
|
|
250
|
+
if (!name || !init)
|
|
251
|
+
continue;
|
|
252
|
+
if (isHandlerCall(init) && factoryRoot(chainOf(init).root))
|
|
253
|
+
localServerFns.add(name);
|
|
254
|
+
if (serverReachable && declaration.kind === "const" && isContainer(init))
|
|
255
|
+
moduleContainers.set(name, false);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const directive of program.directives ?? []) {
|
|
259
|
+
if (directive.value.value === "use server") {
|
|
260
|
+
report("use-server", "error", path, directive, "'use server' is Next.js; server code is a signedInFn/publicFn server function");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const statement of program.body) {
|
|
264
|
+
if (statement.type === "ImportDeclaration") {
|
|
265
|
+
const source = statement.source.value;
|
|
266
|
+
const values = valueImports(statement);
|
|
267
|
+
for (const [module, instead] of Object.entries(CLASSIC_IMPORTS)) {
|
|
268
|
+
const allowed = source === module ? CLASSIC_ALLOWED[module] : undefined;
|
|
269
|
+
const refused = values === "all" ? true : values.some((name) => !allowed?.has(name));
|
|
270
|
+
if (importsModule(source, module) && refused) {
|
|
271
|
+
report("classic-construct", "error", path, statement, `${module} is classic's; in a Start app, ${instead}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (source === "cloudflare:workers") {
|
|
275
|
+
report("cloudflare-env", "error", path, statement, "read the app's environment from process.env, not cloudflare:workers");
|
|
276
|
+
}
|
|
277
|
+
const typeOnly = values !== "all" && values.length === 0;
|
|
278
|
+
if (callsWorkflow && !typeOnly && (/\.functions(\.[jt]sx?)?$/.test(source) || importsModule(source, "@tanstack/react-start/server"))) {
|
|
279
|
+
report("workflow-imports", "error", path, statement, `a workflow runs without a page request, so it can't use ${source}; move what it needs into a *.server.ts helper`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const exported = statement.type === "ExportNamedDeclaration" ? statement.declaration : undefined;
|
|
283
|
+
const exportedNames = exported?.type === "FunctionDeclaration"
|
|
284
|
+
? [nameOf(exported.id)]
|
|
285
|
+
: exported?.type === "VariableDeclaration"
|
|
286
|
+
? exported.declarations.map((d) => nameOf(d.id))
|
|
287
|
+
: [];
|
|
288
|
+
if (exportedNames.includes("getServerSideProps")) {
|
|
289
|
+
report("next-construct", "error", path, statement, "getServerSideProps is Next.js; a route's loader fetches its data");
|
|
290
|
+
}
|
|
291
|
+
if (serverReachable && statement.type === "VariableDeclaration" && statement.kind === "let") {
|
|
292
|
+
report("module-state", "warning", path, statement, "a module-level let in server code is shared by every request and every user; keep per-request state inside the handler");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
walk(program, (node, ancestors) => {
|
|
296
|
+
const parent = ancestors.at(-1);
|
|
297
|
+
if (node.type === "CallExpression" && isHandlerCall(node)) {
|
|
298
|
+
const { root, calls } = chainOf(node);
|
|
299
|
+
if (factoryRoot(root)) {
|
|
300
|
+
const declarator = parent?.type === "VariableDeclarator" ? parent : undefined;
|
|
301
|
+
const name = nameOf(declarator?.id);
|
|
302
|
+
if (!declarator || !isTopLevelDeclarator(ancestors)) {
|
|
303
|
+
report("server-fn-not-top-level", "error", path, node, "assign each server function to its own top-level const: one in an object, a function or a helper's return value isn't compiled");
|
|
304
|
+
}
|
|
305
|
+
else if (name) {
|
|
306
|
+
serverFnFiles.set(name, [...(serverFnFiles.get(name) ?? []), path]);
|
|
307
|
+
}
|
|
308
|
+
if (!hasMethodOption(calls)) {
|
|
309
|
+
report("method-missing", "error", path, node, `call ${root} with { method: 'GET' | 'POST', description } before .handler()`);
|
|
310
|
+
}
|
|
311
|
+
if (file.imports.get(root)?.imported === "publicFn" || root === "publicFn") {
|
|
312
|
+
const handler = node.arguments[0];
|
|
313
|
+
if (handler && FUNCTION_TYPES.has(handler.type)) {
|
|
314
|
+
walk(handler, (inner, innerAncestors) => {
|
|
315
|
+
const returned = inner.type === "ReturnStatement"
|
|
316
|
+
? inner.argument
|
|
317
|
+
: inner === handler && handler.type === "ArrowFunctionExpression" && handler.body.type !== "BlockStatement"
|
|
318
|
+
? handler.body
|
|
319
|
+
: undefined;
|
|
320
|
+
if (!returned || innerAncestors.slice(1).some((a) => FUNCTION_TYPES.has(a.type)))
|
|
321
|
+
return;
|
|
322
|
+
const call = unwrap(returned.type === "AwaitExpression" ? returned.argument : returned);
|
|
323
|
+
if (call.type !== "CallExpression")
|
|
324
|
+
return;
|
|
325
|
+
const chain = chainOf(call);
|
|
326
|
+
const method = chain.members[0];
|
|
327
|
+
if (chain.root !== "zite" || (method !== "findAll" && method !== "findOne"))
|
|
328
|
+
return;
|
|
329
|
+
const options = call.arguments[0];
|
|
330
|
+
if (options?.type === "ObjectExpression" && propertyNamed(options, "fields"))
|
|
331
|
+
return;
|
|
332
|
+
report("public-rows", "warning", path, inner, `anyone can call ${declarator ? nameOf(declarator.id) : "this function"}, and it returns whole records; pass fields to return only what the page shows`);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (moduleContainers.size > 0 && !atModuleScope(ancestors)) {
|
|
339
|
+
const target = node.type === "CallExpression" && isNode(node.callee) && node.callee.type === "MemberExpression" &&
|
|
340
|
+
MUTATING_METHODS.has(nameOf(node.callee.property) ?? "")
|
|
341
|
+
? nameOf(node.callee.object)
|
|
342
|
+
: node.type === "AssignmentExpression" && isNode(node.left) && node.left.type === "MemberExpression"
|
|
343
|
+
? nameOf(node.left.object)
|
|
344
|
+
: undefined;
|
|
345
|
+
if (target && moduleContainers.get(target) === false) {
|
|
346
|
+
moduleContainers.set(target, true);
|
|
347
|
+
report("module-state", "warning", path, node, `${target} is module-level, so what a handler writes to it is shared by every request and every user; use the database or keep it inside the handler`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (node.type === "MemberExpression" && nameOf(node.property) === "inputValidator" && !node.computed) {
|
|
351
|
+
report("input-validator", "warning", path, node, "inputValidator is deprecated, renamed validator; call .validator(…)");
|
|
352
|
+
}
|
|
353
|
+
if (node.type === "CallExpression" && nameOf(node.callee) === "createEndpoint") {
|
|
354
|
+
report("classic-construct", "error", path, node, "createEndpoint is classic's; in a Start app, server code is a signedInFn/publicFn server function");
|
|
355
|
+
}
|
|
356
|
+
if (node.type === "CallExpression" && nameOf(node.callee) === "redirect") {
|
|
357
|
+
const options = node.arguments[0];
|
|
358
|
+
const href = options?.type === "ObjectExpression" ? propertyNamed(options, "href") : undefined;
|
|
359
|
+
const value = href && href.value;
|
|
360
|
+
const toLogin = !!value && (nameOf(value) === "loginHref" || callsAny(value, (callee) => nameOf(callee) === "loginHref"));
|
|
361
|
+
const reload = options && propertyNamed(options, "reloadDocument");
|
|
362
|
+
const reloads = !!reload && isNode(reload.value) && reload.value.type === "BooleanLiteral" && reload.value.value === true;
|
|
363
|
+
if (toLogin && !reloads) {
|
|
364
|
+
report("login-reload", "error", path, node, "the sign-in page isn't one of the app's routes; redirect to it with reloadDocument: true");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (node.type === "NewExpression" &&
|
|
368
|
+
nameOf(node.callee) === "QueryClient" &&
|
|
369
|
+
atModuleScope(ancestors)) {
|
|
370
|
+
report("module-query-client", "error", path, node, "a module-level QueryClient caches one visitor's data for the next; createZiteRouter makes one per request as context.queryClient");
|
|
371
|
+
}
|
|
372
|
+
if (atModuleScope(ancestors)) {
|
|
373
|
+
const envRead = node.type === "MemberExpression" &&
|
|
374
|
+
isNode(node.object) &&
|
|
375
|
+
node.object.type === "MemberExpression" &&
|
|
376
|
+
nameOf(node.object.object) === "process" &&
|
|
377
|
+
nameOf(node.object.property) === "env" &&
|
|
378
|
+
nameOf(node.property) !== "NODE_ENV";
|
|
379
|
+
if (envRead) {
|
|
380
|
+
report("module-env", "error", path, node, "process.env is the request's, so read it inside a handler or loader, not when the module loads");
|
|
381
|
+
}
|
|
382
|
+
if (node.type === "CallExpression" && isNode(node.callee) && node.callee.type === "MemberExpression" &&
|
|
383
|
+
nameOf(node.callee.object) === "Date" && nameOf(node.callee.property) === "now") {
|
|
384
|
+
report("module-env", "error", path, node, "Date.now() when the module loads is the server's start time; call it where the value is used");
|
|
385
|
+
}
|
|
386
|
+
const isWindowReference = node.type === "Identifier" &&
|
|
387
|
+
node.name === "window" &&
|
|
388
|
+
!(parent?.type === "UnaryExpression" && parent.operator === "typeof") &&
|
|
389
|
+
!(parent?.type === "MemberExpression" && parent.property === node && !parent.computed) &&
|
|
390
|
+
!(parent?.type === "ObjectProperty" && parent.key === node) &&
|
|
391
|
+
!parent?.type.startsWith("TS") &&
|
|
392
|
+
!guardedByTypeofWindow(ancestors);
|
|
393
|
+
if (isWindowReference) {
|
|
394
|
+
// Guards come in too many shapes to tell for sure, so only a warning.
|
|
395
|
+
report("module-env", "warning", path, node, "window doesn't exist while the server renders; use it in an effect or event handler");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (node.type === "AwaitExpression" &&
|
|
399
|
+
isNode(node.argument) &&
|
|
400
|
+
chainOf(node.argument).root === "zite") {
|
|
401
|
+
const fn = [...ancestors].reverse().findIndex((a) => FUNCTION_TYPES.has(a.type));
|
|
402
|
+
const inner = fn === -1 ? ancestors : ancestors.slice(ancestors.length - fn);
|
|
403
|
+
if (inner.some((a) => LOOP_TYPES.has(a.type))) {
|
|
404
|
+
report("await-in-loop", "warning", path, node, "each await zite.* in a loop is a round trip; batch it with one findAll, bulkCreate or bulkUpdate");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if ((node.type === "ObjectProperty" || node.type === "ObjectMethod") &&
|
|
408
|
+
(nameOf(node.key) === "loader" || nameOf(node.key) === "beforeLoad")) {
|
|
409
|
+
const body = (node.type === "ObjectMethod" ? node : node.value);
|
|
410
|
+
const isLayout = ROOT_LAYOUTS.has(path) && nameOf(node.key) === "beforeLoad";
|
|
411
|
+
const ssr = parent?.type === "ObjectExpression" ? propertyNamed(parent, "ssr")?.value : undefined;
|
|
412
|
+
const browserOnly = isNode(ssr) && ssr.type === "BooleanLiteral" && ssr.value === false;
|
|
413
|
+
walk(body, (inner) => {
|
|
414
|
+
if (inner.type !== "CallExpression")
|
|
415
|
+
return;
|
|
416
|
+
const callee = nameOf(inner.callee);
|
|
417
|
+
if (!browserOnly && callee === "fetch" && isRelativePath(inner.arguments[0])) {
|
|
418
|
+
report("relative-fetch", "error", path, inner, "a loader also runs on the server, where a relative URL has no host; call a server function instead");
|
|
419
|
+
}
|
|
420
|
+
const source = callee ? file.imports.get(callee)?.source : undefined;
|
|
421
|
+
const serverFn = !!callee && (localServerFns.has(callee) || (!!source && /\.functions(\.[jt]sx?)?$/.test(source)));
|
|
422
|
+
if (isLayout && serverFn) {
|
|
423
|
+
report("layout-server-fn", "warning", path, inner, `${callee} would run on every navigation from the root layout; the signed-in user is already context.user`);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
for (const [name, paths] of serverFnFiles) {
|
|
430
|
+
const distinct = [...new Set(paths)];
|
|
431
|
+
if (distinct.length > 1) {
|
|
432
|
+
problems.push({
|
|
433
|
+
rule: "duplicate-server-fn",
|
|
434
|
+
severity: "error",
|
|
435
|
+
file: distinct[1],
|
|
436
|
+
message: `${name} is also a server function in ${distinct[0]}; a server function's name is its id, so each is unique in the app`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return problems;
|
|
441
|
+
}
|
package/dist/cjs/cli.js
CHANGED
|
@@ -80,7 +80,7 @@ async function main() {
|
|
|
80
80
|
console.error(' sync Generate .zite/db.ts from database schema (single-app)');
|
|
81
81
|
console.error(' dev Run sync then watch for changes (like npx convex dev)');
|
|
82
82
|
console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
|
|
83
|
-
console.error(' check
|
|
83
|
+
console.error(' check Typecheck and build every app and automation; a Start app is also described and its source checked by rule (--json for a report)');
|
|
84
84
|
console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/index.ts) for cloudflare-lambda');
|
|
85
85
|
console.error(' describe Print what a built TanStack Start app declares: functions, routes, access');
|
|
86
86
|
process.exit(1);
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -83,6 +83,7 @@ const FIELD_TYPE_MAP = {
|
|
|
83
83
|
formula: "any",
|
|
84
84
|
created_at: "string",
|
|
85
85
|
updated_at: "string",
|
|
86
|
+
created_by: "string",
|
|
86
87
|
updated_by: "string",
|
|
87
88
|
};
|
|
88
89
|
/**
|
|
@@ -108,7 +109,7 @@ const FIELD_INPUT_TYPE_MAP = {
|
|
|
108
109
|
* rather than typed and then refused at runtime.
|
|
109
110
|
*
|
|
110
111
|
* Mirrors `COMPUTED_FIELD_TYPES` in the product's own `shared/bases/fields`.
|
|
111
|
-
* The first
|
|
112
|
+
* The first seven are rejected by `createRecordDataSchema` — each has a
|
|
112
113
|
* `refine(val => val == null)`, so a non-null write errors (null is a no-op
|
|
113
114
|
* clear). `created_at` / `updated_at` are refused differently: they have no
|
|
114
115
|
* column at all, so a write is silently dropped, which is the worse outcome to
|
|
@@ -120,6 +121,7 @@ const READ_ONLY_FIELD_TYPES = new Set([
|
|
|
120
121
|
"source",
|
|
121
122
|
"formula",
|
|
122
123
|
"autonumber",
|
|
124
|
+
"created_by",
|
|
123
125
|
"updated_by",
|
|
124
126
|
"created_at",
|
|
125
127
|
"updated_at",
|
|
@@ -260,13 +262,8 @@ function fieldJsdoc(schemaField, table, schema) {
|
|
|
260
262
|
const parts = [];
|
|
261
263
|
if (table.primaryFieldId === schemaField.id)
|
|
262
264
|
parts.push("Primary field");
|
|
263
|
-
if (def.type
|
|
264
|
-
def.type === "updated_at" ||
|
|
265
|
-
def.type === "lookup" ||
|
|
266
|
-
def.type === "formula" ||
|
|
267
|
-
def.type === "autonumber") {
|
|
265
|
+
if (READ_ONLY_FIELD_TYPES.has(def.type))
|
|
268
266
|
parts.push("Read-only");
|
|
269
|
-
}
|
|
270
267
|
if (def.type === "linked_record") {
|
|
271
268
|
const tpl = def.template;
|
|
272
269
|
if (tpl.tableId) {
|
package/dist/cjs/vite/index.js
CHANGED
|
@@ -75,11 +75,12 @@ function ziteId() {
|
|
|
75
75
|
name: "vite-plugin-zite-id",
|
|
76
76
|
enforce: "pre",
|
|
77
77
|
async transform(code, id) {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// TanStack's route code splitting moves a route's component into its own module, `route.tsx?tsr-split=…`.
|
|
79
|
+
const file = id.split("?")[0];
|
|
80
|
+
if (!validExtensions.has(path_1.default.extname(file)) || file.includes("node_modules")) {
|
|
80
81
|
return null;
|
|
81
82
|
}
|
|
82
|
-
const relativePath = path_1.default.relative(cwd,
|
|
83
|
+
const relativePath = path_1.default.relative(cwd, file);
|
|
83
84
|
try {
|
|
84
85
|
const ast = (0, parser_1.parse)(code, {
|
|
85
86
|
sourceType: "module",
|
|
@@ -134,6 +135,10 @@ function ziteId() {
|
|
|
134
135
|
if (!isTagged) {
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
138
|
+
// A module split from one already tagged keeps its tags.
|
|
139
|
+
if (openingElement.attributes?.some((attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-zite-id")) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
137
142
|
const line = openingElement.loc?.start?.line ?? 0;
|
|
138
143
|
const col = openingElement.loc?.start?.column ?? 0;
|
|
139
144
|
const dataComponentId = `${relativePath}|${line}|${col}`;
|
|
@@ -6,16 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const path_1 = __importDefault(require("path"));
|
|
7
7
|
const vitest_1 = require("vitest");
|
|
8
8
|
const index_js_1 = require("./index.js");
|
|
9
|
-
async function transform(code) {
|
|
9
|
+
async function transform(code, file = 'src/App.tsx') {
|
|
10
10
|
const plugin = (0, index_js_1.ziteId)();
|
|
11
11
|
const handler = plugin.transform;
|
|
12
|
-
const result = await handler(code, path_1.default.join(process.cwd(),
|
|
12
|
+
const result = await handler(code, path_1.default.join(process.cwd(), file));
|
|
13
13
|
if (!result) {
|
|
14
14
|
throw new Error('transform returned null');
|
|
15
15
|
}
|
|
16
16
|
return result.code;
|
|
17
17
|
}
|
|
18
18
|
(0, vitest_1.describe)('ziteId', () => {
|
|
19
|
+
(0, vitest_1.it)("tags a route's component that TanStack split into its own module, by the source file", async () => {
|
|
20
|
+
const code = await transform('const a = <p>Hi</p>;', 'src/routes/index.tsx?tsr-split=component');
|
|
21
|
+
(0, vitest_1.expect)(code).toContain('<p data-zite-id="src/routes/index.tsx|1|10" data-zite-editable="true">');
|
|
22
|
+
});
|
|
23
|
+
(0, vitest_1.it)('leaves an element that already has its tag', async () => {
|
|
24
|
+
const code = await transform('const a = <p data-zite-id="src/routes/index.tsx|1|10">Hi</p>;');
|
|
25
|
+
(0, vitest_1.expect)(code.match(/data-zite-id/g)).toHaveLength(1);
|
|
26
|
+
});
|
|
19
27
|
(0, vitest_1.it)('tags html and svg elements without a component name', async () => {
|
|
20
28
|
const code = await transform('const a = <div><svg><path d="M0" /></svg><p>Hello</p></div>;');
|
|
21
29
|
(0, vitest_1.expect)(code).toContain('<div data-zite-id="src/App.tsx|1|10">');
|
package/dist/esm/auth/index.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export declare const signIn: {
|
|
|
67
67
|
}, FetchOptions["throw"] extends true ? true : false>>;
|
|
68
68
|
} & {
|
|
69
69
|
social: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
|
|
70
|
-
provider: "salesforce" | "slack" | "linear" | "notion" | "linkedin" | "github" | "dropbox" | (string & {}) | "huggingface" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "spotify" | "twitch" | "twitter" | "kick" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "zoom" | "kakao" | "naver" | "
|
|
70
|
+
provider: "salesforce" | "slack" | "linear" | "notion" | "linkedin" | "github" | "dropbox" | "line" | (string & {}) | "huggingface" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "spotify" | "twitch" | "twitter" | "kick" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "zoom" | "kakao" | "naver" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat";
|
|
71
71
|
callbackURL?: string | undefined;
|
|
72
72
|
newUserCallbackURL?: string | undefined;
|
|
73
73
|
errorCallbackURL?: string | undefined;
|
|
@@ -91,7 +91,7 @@ export declare const signIn: {
|
|
|
91
91
|
loginHint?: string | undefined;
|
|
92
92
|
additionalData?: Record<string, any> | undefined;
|
|
93
93
|
}> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth/react").Prettify<{
|
|
94
|
-
provider: "salesforce" | "slack" | "linear" | "notion" | "linkedin" | "github" | "dropbox" | (string & {}) | "huggingface" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "spotify" | "twitch" | "twitter" | "kick" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "zoom" | "kakao" | "naver" | "
|
|
94
|
+
provider: "salesforce" | "slack" | "linear" | "notion" | "linkedin" | "github" | "dropbox" | "line" | (string & {}) | "huggingface" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "spotify" | "twitch" | "twitter" | "kick" | "gitlab" | "tiktok" | "reddit" | "roblox" | "vk" | "zoom" | "kakao" | "naver" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat";
|
|
95
95
|
callbackURL?: string | undefined;
|
|
96
96
|
newUserCallbackURL?: string | undefined;
|
|
97
97
|
errorCallbackURL?: string | undefined;
|
|
@@ -1 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Problem } from './start/problem.js';
|
|
2
|
+
type Step = {
|
|
3
|
+
name: string;
|
|
4
|
+
ok: boolean;
|
|
5
|
+
output?: string;
|
|
6
|
+
};
|
|
7
|
+
/** One app's check, as `--json` reports it. */
|
|
8
|
+
export type AppCheck = {
|
|
9
|
+
path: string;
|
|
10
|
+
runtime: 'classic' | 'start';
|
|
11
|
+
ok: boolean;
|
|
12
|
+
steps: Step[];
|
|
13
|
+
problems: Problem[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* While `--json` checks, stdout carries only the report: app code that logs as
|
|
17
|
+
* it's described in-process goes to stderr instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare const withStdoutOnStderr: <T>(run: () => Promise<T>) => Promise<T>;
|
|
20
|
+
/** Prints as it goes, or with `--json` stays quiet and reports everything at the end. */
|
|
21
|
+
export declare class Reporter {
|
|
22
|
+
private readonly json;
|
|
23
|
+
readonly apps: AppCheck[];
|
|
24
|
+
constructor(json: boolean);
|
|
25
|
+
app(path: string, runtime: AppCheck['runtime'], heading: boolean): AppCheck;
|
|
26
|
+
step(app: AppCheck, name: string, result: {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
output?: string;
|
|
29
|
+
}): boolean;
|
|
30
|
+
problems(app: AppCheck, problems: Problem[]): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function runCheck(args?: string[]): Promise<void>;
|
|
33
|
+
export {};
|