zitejs 0.10.0-start.0 → 0.10.0-start.10
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/backend/index.d.ts +115 -3
- package/dist/cjs/backend/index.test.js +58 -0
- package/dist/cjs/check/index.d.ts +33 -1
- package/dist/cjs/check/index.js +187 -59
- 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/integrations/rules.d.ts +13 -0
- package/dist/cjs/integrations/rules.js +29 -0
- package/dist/cjs/integrations/rules.test.js +1 -0
- package/dist/cjs/start/describeBuilt.js +5 -3
- package/dist/cjs/start/entries.d.ts +11 -2
- package/dist/cjs/start/entries.js +45 -5
- package/dist/cjs/start/problems.d.ts +19 -0
- package/dist/cjs/start/problems.js +11 -0
- package/dist/cjs/sync/lib.js +10 -13
- 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/backend/index.d.ts +115 -3
- package/dist/esm/backend/index.test.js +58 -0
- package/dist/esm/check/index.d.ts +33 -1
- package/dist/esm/check/index.js +153 -61
- 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/integrations/rules.d.ts +13 -0
- package/dist/esm/integrations/rules.js +29 -0
- package/dist/esm/integrations/rules.test.js +1 -0
- 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/authHooks.d.ts +7 -0
- package/dist/esm/start/authHooks.js +10 -0
- package/dist/esm/start/authHooksModule.d.ts +1 -0
- package/dist/esm/start/authHooksModule.js +3 -0
- package/dist/esm/start/clientErrorMessage.test.d.ts +1 -0
- package/dist/esm/start/clientErrorMessage.test.js +105 -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 +46 -1
- package/dist/esm/start/describe.js +91 -9
- package/dist/esm/start/describeBuilt.js +5 -3
- package/dist/esm/start/descriptions.d.ts +4 -0
- package/dist/esm/start/descriptions.js +47 -0
- package/dist/esm/start/entries.d.ts +11 -2
- package/dist/esm/start/entries.js +43 -6
- package/dist/esm/start/errors.d.ts +51 -0
- package/dist/esm/start/errors.js +104 -0
- package/dist/esm/start/errors.test.d.ts +1 -0
- package/dist/esm/start/errors.test.js +23 -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 +24 -22
- package/dist/esm/start/index.js +61 -38
- package/dist/esm/start/instance.d.ts +2 -3
- package/dist/esm/start/instance.js +12 -21
- package/dist/esm/start/previewClaim.d.ts +10 -0
- package/dist/esm/start/previewClaim.js +32 -0
- package/dist/esm/start/problems.d.ts +19 -0
- package/dist/esm/start/problems.js +8 -0
- package/dist/esm/start/redactSecrets.d.ts +7 -0
- package/dist/esm/start/redactSecrets.js +131 -0
- package/dist/esm/start/redactSecrets.test.d.ts +1 -0
- package/dist/esm/start/redactSecrets.test.js +151 -0
- package/dist/esm/start/registration.d.ts +13 -0
- package/dist/esm/start/registration.js +21 -0
- package/dist/esm/start/router.d.ts +7 -3
- package/dist/esm/start/router.js +54 -7
- package/dist/esm/start/runLog.d.ts +8 -0
- package/dist/esm/start/runLog.js +76 -0
- package/dist/esm/start/scope.d.ts +2 -2
- package/dist/esm/start/server.d.ts +43 -35
- package/dist/esm/start/server.js +281 -42
- package/dist/esm/start/steps.d.ts +28 -0
- package/dist/esm/start/steps.js +97 -0
- package/dist/esm/start/types.d.ts +145 -5
- package/dist/esm/start/version.d.ts +1 -0
- package/dist/esm/start/version.js +2 -0
- package/dist/esm/start/vite.js +67 -8
- package/dist/esm/start/worker.d.ts +11 -0
- package/dist/esm/start/worker.js +20 -0
- package/dist/esm/start/workflow.d.ts +59 -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 +50 -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 +93 -0
- package/dist/esm/start/workflowRun.js +128 -0
- package/dist/esm/sync/lib.js +10 -13
- package/dist/esm/vite/index.js +8 -3
- package/dist/esm/vite/index.test.js +10 -2
- package/package.json +21 -8
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describedProblems = describedProblems;
|
|
4
|
+
// The host, TanStack and Cloudflare answer these before the app does.
|
|
5
|
+
const RESERVED_PATHS = [
|
|
6
|
+
/^\/_zite/,
|
|
7
|
+
/^\/__zite/,
|
|
8
|
+
/^\/_serverFn(\/|$)/,
|
|
9
|
+
/^\/cdn-cgi(\/|$)/,
|
|
10
|
+
/^\/auth(\/|$)/,
|
|
11
|
+
/^\/api\/auth(\/|$)/,
|
|
12
|
+
/^\/manifest\.webmanifest$/,
|
|
13
|
+
];
|
|
14
|
+
const OWNER_INPUTS = ["userId", "ownerId"];
|
|
15
|
+
/** `access-missing: no signedIn…`, or a route's `GET: access-missing: …`, as a named problem. */
|
|
16
|
+
const describeProblem = (problem, subject, file) => {
|
|
17
|
+
const match = /^(?:(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|ANY): )?([a-z][a-z-]*): (.*)$/s.exec(problem);
|
|
18
|
+
const [, method, rule, message] = match ?? [undefined, undefined, "describe", problem];
|
|
19
|
+
return {
|
|
20
|
+
rule: rule ?? "describe",
|
|
21
|
+
severity: "error",
|
|
22
|
+
...(file && { file }),
|
|
23
|
+
message: `${subject}${method ? ` ${method}` : ""}: ${message ?? problem}`,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const inputProperties = (input) => {
|
|
27
|
+
const properties = input?.properties;
|
|
28
|
+
return properties && typeof properties === "object" ? Object.keys(properties) : [];
|
|
29
|
+
};
|
|
30
|
+
function describedProblems(description, { internal }) {
|
|
31
|
+
const problems = [];
|
|
32
|
+
for (const fn of description.functions) {
|
|
33
|
+
const subject = `function ${fn.name ?? fn.id}`;
|
|
34
|
+
problems.push(...fn.problems.map((p) => describeProblem(p, subject, fn.file)));
|
|
35
|
+
if (internal && fn.access.kind === "anyone") {
|
|
36
|
+
problems.push({
|
|
37
|
+
rule: "public-in-internal",
|
|
38
|
+
severity: "error",
|
|
39
|
+
...(fn.file && { file: fn.file }),
|
|
40
|
+
message: `${subject} is a publicFn, and this internal app signs everyone in; make it a signedInFn`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const owner = fn.access.kind === "signedIn" && inputProperties(fn.input).find((p) => OWNER_INPUTS.includes(p));
|
|
44
|
+
if (owner) {
|
|
45
|
+
problems.push({
|
|
46
|
+
rule: "owner-from-input",
|
|
47
|
+
severity: "warning",
|
|
48
|
+
...(fn.file && { file: fn.file }),
|
|
49
|
+
message: `${subject} takes ${owner} as input, which any caller can set; use context.user.id`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const route of description.routes) {
|
|
54
|
+
const subject = `route ${route.path}`;
|
|
55
|
+
problems.push(...route.problems.map((p) => describeProblem(p, subject, route.file)));
|
|
56
|
+
const publicMethods = Object.entries(route.methods).filter(([, access]) => access.kind === "anyone");
|
|
57
|
+
if (internal && publicMethods.length > 0) {
|
|
58
|
+
problems.push({
|
|
59
|
+
rule: "public-in-internal",
|
|
60
|
+
severity: "error",
|
|
61
|
+
...(route.file && { file: route.file }),
|
|
62
|
+
message: `${subject} lets anyone call ${publicMethods.map(([m]) => m).join(", ")}, and this internal app signs everyone in; use signedIn`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!route.description) {
|
|
66
|
+
problems.push({
|
|
67
|
+
rule: "description-missing",
|
|
68
|
+
severity: "warning",
|
|
69
|
+
...(route.file && { file: route.file }),
|
|
70
|
+
message: `${subject} has no description; add staticData: { description } for the editor`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const workflow of description.workflows ?? []) {
|
|
75
|
+
problems.push(...workflow.problems.map((p) => describeProblem(p, `workflow ${workflow.id}`, workflow.file)));
|
|
76
|
+
}
|
|
77
|
+
const paths = new Map([...description.routes, ...(description.pages ?? [])].map((r) => [r.path, r.file]));
|
|
78
|
+
for (const [path, file] of paths) {
|
|
79
|
+
if (RESERVED_PATHS.some((reserved) => reserved.test(path))) {
|
|
80
|
+
problems.push({
|
|
81
|
+
rule: "reserved-path",
|
|
82
|
+
severity: "error",
|
|
83
|
+
...(file && { file }),
|
|
84
|
+
message: `${path} is the platform's; give the route another path`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return problems;
|
|
89
|
+
}
|
|
@@ -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,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatProblem = void 0;
|
|
4
|
+
const formatProblem = ({ rule, severity, file, line, message }) => `${severity} ${rule}${file ? ` ${file}${line ? `:${line}` : ""}` : ""}: ${message}`;
|
|
5
|
+
exports.formatProblem = formatProblem;
|
|
@@ -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);
|
|
@@ -38,11 +38,24 @@ declare const FIELDS_BY_TYPE: {
|
|
|
38
38
|
jira: readonly string[];
|
|
39
39
|
typeform: readonly string[];
|
|
40
40
|
apollo: readonly string[];
|
|
41
|
+
clay: readonly string[];
|
|
41
42
|
attio: readonly string[];
|
|
42
43
|
granola: readonly string[];
|
|
44
|
+
clickup: readonly string[];
|
|
43
45
|
dropbox: readonly string[];
|
|
46
|
+
zendesk: readonly string[];
|
|
47
|
+
smartsheet: readonly string[];
|
|
44
48
|
amazonS3: readonly string[];
|
|
45
49
|
snowflake: readonly string[];
|
|
50
|
+
databricks: readonly string[];
|
|
51
|
+
confluence: readonly string[];
|
|
52
|
+
pipedrive: readonly string[];
|
|
53
|
+
shopify: readonly string[];
|
|
54
|
+
sharepoint: readonly string[];
|
|
55
|
+
excel: readonly string[];
|
|
56
|
+
x: readonly string[];
|
|
57
|
+
zoomMeetings: readonly string[];
|
|
58
|
+
smartsuite: readonly string[];
|
|
46
59
|
};
|
|
47
60
|
/** A service type `integrations.get` serves: one used through its npm package with a token. */
|
|
48
61
|
export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
|
|
@@ -47,15 +47,43 @@ const FIELDS_BY_TYPE = {
|
|
|
47
47
|
jira: extras("cloudId", "siteUrl"),
|
|
48
48
|
typeform: extras(),
|
|
49
49
|
apollo: extras(),
|
|
50
|
+
clay: extras(),
|
|
50
51
|
attio: extras(),
|
|
51
52
|
granola: extras(),
|
|
53
|
+
clickup: extras(),
|
|
52
54
|
dropbox: extras(),
|
|
55
|
+
// Every path is https://{subdomain}.zendesk.com/api/v2/..., and a token is
|
|
56
|
+
// only valid on the account it was issued for, so the subdomain travels with
|
|
57
|
+
// it rather than being configured per app.
|
|
58
|
+
zendesk: extras("subdomain"),
|
|
59
|
+
// Smartsheet's US, EU, AU and Gov regions are separate, and a token is only
|
|
60
|
+
// valid on its own region's host, so the host travels with it.
|
|
61
|
+
smartsheet: extras("apiBaseUrl"),
|
|
53
62
|
// The IAM key alone reaches nothing: every call needs the region and the
|
|
54
63
|
// bucket, and S3-compatible providers need the endpoint too.
|
|
55
64
|
amazonS3: extras("keyId", "region", "bucket", "endpoint", "pathPrefix"),
|
|
56
65
|
// The SQL API lives on each account's own host, and the token is pinned to
|
|
57
66
|
// one role; the warehouse is optional and falls back to the user's default.
|
|
58
67
|
snowflake: extras("accountUrl", "role", "warehouse"),
|
|
68
|
+
// A statement is addressed by workspace host and SQL warehouse, and the token
|
|
69
|
+
// derives neither.
|
|
70
|
+
databricks: extras("host", "warehouseId"),
|
|
71
|
+
// Cloud Confluence is addressed through api.atlassian.com by cloud id, the
|
|
72
|
+
// same as Jira; the site URL is only for links a human will click.
|
|
73
|
+
confluence: extras("cloudId", "siteUrl"),
|
|
74
|
+
// OAuth returns a per-company api_domain alongside the token, so there is no
|
|
75
|
+
// one host to call.
|
|
76
|
+
pipedrive: extras("apiBaseUrl"),
|
|
77
|
+
// Shopify has no global API host: every call goes to the store's own domain
|
|
78
|
+
// under a pinned API version. The storefront pair is the shopper-facing API,
|
|
79
|
+
// which installs predating those scopes do not have.
|
|
80
|
+
shopify: extras("shopDomain", "apiBaseUrl", "storefrontApiUrl", "storefrontAccessToken"),
|
|
81
|
+
sharepoint: extras(),
|
|
82
|
+
excel: extras(),
|
|
83
|
+
x: extras(),
|
|
84
|
+
zoomMeetings: extras(),
|
|
85
|
+
// Every request names the workspace in an Account-Id header beside the key.
|
|
86
|
+
smartsuite: extras("workspaceId"),
|
|
59
87
|
};
|
|
60
88
|
/** Per service type: `accessToken`, then its extra labels sorted (Salesforce `instanceUrl`). */
|
|
61
89
|
exports.CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
|
|
@@ -68,6 +96,7 @@ exports.OPTIONAL_CREDENTIAL_FIELDS = {
|
|
|
68
96
|
// Only S3-compatible providers set an endpoint, and a prefix is opt-in.
|
|
69
97
|
amazonS3: ["endpoint", "pathPrefix"],
|
|
70
98
|
snowflake: ["warehouse"],
|
|
99
|
+
shopify: ["storefrontApiUrl", "storefrontAccessToken"],
|
|
71
100
|
};
|
|
72
101
|
/**
|
|
73
102
|
* Types the runtime never serves: those with a generated client (Airtable, email,
|
|
@@ -18,6 +18,7 @@ const index_js_1 = require("../index.js");
|
|
|
18
18
|
]);
|
|
19
19
|
(0, vitest_1.expect)(index_js_1.CREDENTIAL_FIELDS.jira).toEqual(["accessToken", "cloudId", "siteUrl"]);
|
|
20
20
|
(0, vitest_1.expect)(index_js_1.CREDENTIAL_FIELDS.granola).toEqual(["accessToken"]);
|
|
21
|
+
(0, vitest_1.expect)(index_js_1.CREDENTIAL_FIELDS.smartsheet).toEqual(["accessToken", "apiBaseUrl"]);
|
|
21
22
|
});
|
|
22
23
|
(0, vitest_1.it)("has no type the runtime never serves", () => {
|
|
23
24
|
for (const type of index_js_1.NON_CREDENTIAL_SERVICE_TYPES) {
|