zitejs 0.10.0-start.8 → 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/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/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/version.d.ts +1 -1
- package/dist/esm/start/version.js +1 -1
- package/dist/esm/start/workflow.d.ts +3 -0
- package/dist/esm/start/workflow.test-d.js +5 -0
- 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/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 {};
|