@exposify/next 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +328 -2
- package/dist/chunk-D5TQF5DY.js +53 -0
- package/dist/config.cjs +2046 -0
- package/dist/config.d.cts +191 -0
- package/dist/config.d.ts +191 -0
- package/dist/config.js +1978 -0
- package/dist/exposify-next.js +2651 -0
- package/dist/fields-olvyFnxj.d.cts +23 -0
- package/dist/fields-olvyFnxj.d.ts +23 -0
- package/dist/index.cjs +1179 -0
- package/dist/index.d.cts +201 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +1186 -0
- package/package.json +58 -3
|
@@ -0,0 +1,2651 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/exposify-next.ts
|
|
4
|
+
import { relative as relative4 } from "path";
|
|
5
|
+
import { runCli } from "@exposify/cli";
|
|
6
|
+
|
|
7
|
+
// src/codegen.ts
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { existsSync as existsSync2, readFileSync as readFileSync6, readdirSync, writeFileSync } from "fs";
|
|
10
|
+
import { dirname as dirname2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
11
|
+
import { DESCRIPTIONS_FILE, authStacksOf, dependencyNames, parseDescriptions } from "@exposify/core";
|
|
12
|
+
import { globSync } from "tinyglobby";
|
|
13
|
+
|
|
14
|
+
// src/ast.ts
|
|
15
|
+
import { parse } from "@babel/parser";
|
|
16
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
17
|
+
function parseModule(source, file) {
|
|
18
|
+
const plugins = /\.tsx$/.test(file) ? ["typescript", "jsx"] : /\.[mc]?ts$/.test(file) ? ["typescript"] : ["jsx"];
|
|
19
|
+
try {
|
|
20
|
+
return parse(source, { sourceType: "module", plugins, errorRecovery: true }).program;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function walk(node, visit, parents = []) {
|
|
26
|
+
if (!node || typeof node !== "object") return;
|
|
27
|
+
if (Array.isArray(node)) {
|
|
28
|
+
for (const child of node) walk(child, visit, parents);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const n = node;
|
|
32
|
+
if (typeof n.type !== "string") return;
|
|
33
|
+
visit(n, parents);
|
|
34
|
+
parents.push(n);
|
|
35
|
+
for (const key of Object.keys(n)) {
|
|
36
|
+
if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
37
|
+
const value = n[key];
|
|
38
|
+
if (value && typeof value === "object") walk(value, visit, parents);
|
|
39
|
+
}
|
|
40
|
+
parents.pop();
|
|
41
|
+
}
|
|
42
|
+
var text = (source, n) => n && typeof n.start === "number" && typeof n.end === "number" ? source.slice(n.start, n.end) : void 0;
|
|
43
|
+
var stringValue = (n) => n?.type === "StringLiteral" ? n.value : n?.type === "TemplateLiteral" && n.expressions.length === 0 ? n.quasis[0]?.value?.cooked : void 0;
|
|
44
|
+
var keyName = (n) => n?.type === "Identifier" ? n.name : stringValue(n);
|
|
45
|
+
function unwrapTs(n) {
|
|
46
|
+
while (n && (n.type === "TSAsExpression" || n.type === "TSSatisfiesExpression" || n.type === "TSNonNullExpression" || n.type === "ParenthesizedExpression")) n = n.expression;
|
|
47
|
+
return n;
|
|
48
|
+
}
|
|
49
|
+
var paramName = (p) => {
|
|
50
|
+
const id = p.type === "AssignmentPattern" ? p.left : p.type === "TSParameterProperty" ? p.parameter : p;
|
|
51
|
+
return id?.type === "Identifier" ? id.name : void 0;
|
|
52
|
+
};
|
|
53
|
+
var lineOf = (n) => n.loc?.start.line ?? 1;
|
|
54
|
+
|
|
55
|
+
// src/fields.ts
|
|
56
|
+
var listScope = (program, file, load) => ({ program, file, lists: /* @__PURE__ */ new Map(), ...load ? { load } : {} });
|
|
57
|
+
var FORM_READS = /* @__PURE__ */ new Set(["get", "getAll", "has"]);
|
|
58
|
+
var FORM_ITERATION = /* @__PURE__ */ new Set(["entries", "keys", "values", "forEach"]);
|
|
59
|
+
var STRING_METHODS = /* @__PURE__ */ new Set(["trim", "trimStart", "trimEnd", "toString", "toLowerCase", "toUpperCase", "normalize", "valueOf"]);
|
|
60
|
+
var NUMBER_FNS = /* @__PURE__ */ new Set(["Number", "parseInt", "parseFloat"]);
|
|
61
|
+
var LOOP_METHODS = /* @__PURE__ */ new Set(["map", "forEach", "flatMap", "filter", "some", "every", "find", "reduce"]);
|
|
62
|
+
var RANK = { string: 0, number: 1, boolean: 2, json: 3, file: 4 };
|
|
63
|
+
var SEARCH_DEPTH = 3;
|
|
64
|
+
var MAX_EXPANDED = 200;
|
|
65
|
+
function merge(a, b) {
|
|
66
|
+
const type = !a.type ? b.type : !b.type ? a.type : RANK[b.type] > RANK[a.type] ? b.type : a.type;
|
|
67
|
+
const values = a.enum ?? b.enum;
|
|
68
|
+
return { ...type ? { type } : {}, ...values ? { enum: values } : {} };
|
|
69
|
+
}
|
|
70
|
+
function constLists(program) {
|
|
71
|
+
const out = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const stmt of program.body) {
|
|
73
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
74
|
+
if (decl?.type !== "VariableDeclaration" || decl.kind !== "const") continue;
|
|
75
|
+
for (const d of decl.declarations) {
|
|
76
|
+
const init = unwrapTs(d.init);
|
|
77
|
+
if (d.id.type !== "Identifier" || init?.type !== "ArrayExpression") continue;
|
|
78
|
+
const values = init.elements.map((e) => stringValue(e));
|
|
79
|
+
if (values.length > 0 && values.every((v) => v !== void 0)) out.set(d.id.name, values);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function listNamed(name, st) {
|
|
85
|
+
if (st.lists.has(name)) return st.lists.get(name) ?? void 0;
|
|
86
|
+
let found = constLists(st.program).get(name);
|
|
87
|
+
if (!found && st.load) {
|
|
88
|
+
for (const stmt of st.program.body) {
|
|
89
|
+
if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
|
|
90
|
+
const spec = stmt.specifiers.find((s) => s.type === "ImportSpecifier" && s.local.name === name);
|
|
91
|
+
if (!spec) continue;
|
|
92
|
+
const mod = st.load(stmt.source.value, st.file);
|
|
93
|
+
const program = mod && parseModule(mod.source, mod.file);
|
|
94
|
+
if (program) found = constLists(program).get(keyName(spec.imported) ?? name);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
st.lists.set(name, found ?? null);
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
function listOf(n, st) {
|
|
102
|
+
n = unwrapTs(n);
|
|
103
|
+
if (n?.type === "Identifier") return listNamed(n.name, st);
|
|
104
|
+
if (n?.type === "ArrayExpression") {
|
|
105
|
+
const values = n.elements.map((e) => stringValue(e));
|
|
106
|
+
return values.length > 0 && values.every((v) => v !== void 0) ? values : void 0;
|
|
107
|
+
}
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
var isFileType = (t) => t?.type === "TSTypeReference" && t.typeName?.type === "Identifier" && ["File", "Blob"].includes(t.typeName.name);
|
|
111
|
+
function referencesTo(root, name) {
|
|
112
|
+
const out = [];
|
|
113
|
+
walk(root, (n, parents) => {
|
|
114
|
+
if (n.type !== "Identifier" || n.name !== name) return;
|
|
115
|
+
const p = parents[parents.length - 1];
|
|
116
|
+
if ((p?.type === "MemberExpression" || p?.type === "OptionalMemberExpression") && p.property === n && !p.computed) return;
|
|
117
|
+
if (p?.type === "ObjectProperty" && p.key === n && !p.computed) return;
|
|
118
|
+
if (p?.type === "VariableDeclarator" && p.id === n) return;
|
|
119
|
+
if (p && FUNCTION_TYPES.has(p.type) && p.params.includes(n)) return;
|
|
120
|
+
out.push({ node: n, parents: [...parents] });
|
|
121
|
+
});
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function classify(node, parents, st, fns, depth) {
|
|
125
|
+
let cur = node;
|
|
126
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
127
|
+
const p = parents[i];
|
|
128
|
+
if (FUNCTION_TYPES.has(p.type)) break;
|
|
129
|
+
if (p.type === "TSAsExpression" || p.type === "TSTypeAssertion") {
|
|
130
|
+
if (isFileType(p.typeAnnotation)) return { type: "file" };
|
|
131
|
+
cur = p;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (p.type === "TSSatisfiesExpression" || p.type === "TSNonNullExpression" || p.type === "ParenthesizedExpression") {
|
|
135
|
+
cur = p;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (p.type === "LogicalExpression" && p.left === cur && (p.operator === "??" || p.operator === "||")) {
|
|
139
|
+
cur = p;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (p.type === "UnaryExpression" && p.operator === "+") return { type: "number" };
|
|
143
|
+
if (p.type === "BinaryExpression") {
|
|
144
|
+
const other = unwrapTs(p.left === cur ? p.right : p.left);
|
|
145
|
+
if (p.operator === "instanceof" && other?.type === "Identifier" && ["File", "Blob"].includes(other.name)) return { type: "file" };
|
|
146
|
+
if (["===", "==", "!==", "!="].includes(p.operator) && stringValue(other) === "on") return { type: "boolean" };
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const call = parents[i - 1];
|
|
150
|
+
if (p.type === "MemberExpression" && p.object === cur && !p.computed && STRING_METHODS.has(keyName(p.property) ?? "") && call?.type === "CallExpression" && call.callee === p) {
|
|
151
|
+
cur = call;
|
|
152
|
+
i--;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (p.type === "CallExpression" && p.arguments.includes(cur)) {
|
|
156
|
+
const callee = unwrapTs(p.callee);
|
|
157
|
+
const index = p.arguments.indexOf(cur);
|
|
158
|
+
if (callee.type === "Identifier") {
|
|
159
|
+
if (callee.name === "String") {
|
|
160
|
+
cur = p;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (NUMBER_FNS.has(callee.name)) return { type: "number" };
|
|
164
|
+
const local = fns.get(callee.name);
|
|
165
|
+
if (local && depth < SEARCH_DEPTH) return followParam(local, index, st, fns, depth + 1);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (callee.type === "MemberExpression") {
|
|
169
|
+
const object = unwrapTs(callee.object);
|
|
170
|
+
const method = keyName(callee.property);
|
|
171
|
+
if (object?.type === "Identifier" && object.name === "JSON" && method === "parse") return { type: "json" };
|
|
172
|
+
if (object?.type === "Identifier" && object.name === "Number" && (method === "parseInt" || method === "parseFloat")) return { type: "number" };
|
|
173
|
+
if (method === "includes" && index === 0) {
|
|
174
|
+
const values = listOf(object, st);
|
|
175
|
+
if (values) return { enum: values };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (p.type === "VariableDeclarator" && p.init === cur && p.id.type === "Identifier") {
|
|
181
|
+
if (depth >= SEARCH_DEPTH) break;
|
|
182
|
+
let scope = parents[0] ?? st.program;
|
|
183
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
184
|
+
if (FUNCTION_TYPES.has(parents[j].type)) {
|
|
185
|
+
scope = parents[j].body;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let hint = {};
|
|
190
|
+
for (const ref of referencesTo(scope, p.id.name)) hint = merge(hint, classify(ref.node, ref.parents, st, fns, depth + 1));
|
|
191
|
+
return hint;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
function followParam(fn, index, st, fns, depth) {
|
|
198
|
+
const param = fn.params[index];
|
|
199
|
+
const name = param && paramName(param);
|
|
200
|
+
if (!name) return {};
|
|
201
|
+
let hint = {};
|
|
202
|
+
for (const ref of referencesTo(fn.body, name)) hint = merge(hint, classify(ref.node, ref.parents, st, fns, depth));
|
|
203
|
+
return hint;
|
|
204
|
+
}
|
|
205
|
+
function loopValues(name, parents, st) {
|
|
206
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
207
|
+
const p = parents[i];
|
|
208
|
+
if (p.type === "ForOfStatement") {
|
|
209
|
+
const id = p.left.type === "VariableDeclaration" ? p.left.declarations[0]?.id : p.left;
|
|
210
|
+
if (id?.type === "Identifier" && id.name === name) return listOf(p.right, st);
|
|
211
|
+
}
|
|
212
|
+
if (FUNCTION_TYPES.has(p.type) && p.params[0] && paramName(p.params[0]) === name) {
|
|
213
|
+
const call = parents[i - 1];
|
|
214
|
+
const callee = call?.type === "CallExpression" && call.arguments[0] === p ? unwrapTs(call.callee) : void 0;
|
|
215
|
+
if (callee?.type === "MemberExpression" && LOOP_METHODS.has(keyName(callee.property) ?? "")) return listOf(callee.object, st);
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return void 0;
|
|
220
|
+
}
|
|
221
|
+
function namesOf(arg, parents, st) {
|
|
222
|
+
arg = unwrapTs(arg);
|
|
223
|
+
const literal = stringValue(arg);
|
|
224
|
+
if (literal !== void 0) return [literal];
|
|
225
|
+
if (arg?.type !== "TemplateLiteral") return void 0;
|
|
226
|
+
let names = [arg.quasis[0].value.cooked];
|
|
227
|
+
for (let k = 0; k < arg.expressions.length; k++) {
|
|
228
|
+
const e = unwrapTs(arg.expressions[k]);
|
|
229
|
+
const values = e?.type === "Identifier" ? loopValues(e.name, parents, st) : void 0;
|
|
230
|
+
if (!values || names.length * values.length > MAX_EXPANDED) return void 0;
|
|
231
|
+
const after = arg.quasis[k + 1].value.cooked;
|
|
232
|
+
names = names.flatMap((prefix) => values.map((v) => `${prefix}${v}${after}`));
|
|
233
|
+
}
|
|
234
|
+
return names;
|
|
235
|
+
}
|
|
236
|
+
function addField(st, name, hint, array) {
|
|
237
|
+
const existing = st.fields.get(name);
|
|
238
|
+
const merged = merge(existing ?? {}, hint);
|
|
239
|
+
st.fields.set(name, {
|
|
240
|
+
name,
|
|
241
|
+
type: merged.type ?? "string",
|
|
242
|
+
...array || existing?.array ? { array: true } : {},
|
|
243
|
+
...merged.enum ? { enum: merged.enum } : {},
|
|
244
|
+
...existing?.required ? { required: true } : {}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function nestedFunctions(fn) {
|
|
248
|
+
const out = /* @__PURE__ */ new Map();
|
|
249
|
+
walk(fn.body, (n) => {
|
|
250
|
+
if (n.type === "FunctionDeclaration" && n.id) out.set(n.id.name, n);
|
|
251
|
+
if (n.type === "VariableDeclarator" && n.id.type === "Identifier") {
|
|
252
|
+
const init = unwrapTs(n.init);
|
|
253
|
+
if (init && FUNCTION_TYPES.has(init.type)) out.set(n.id.name, init);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
var formRead = (n, formNames) => {
|
|
259
|
+
if (n.type !== "CallExpression") return void 0;
|
|
260
|
+
const callee = n.callee;
|
|
261
|
+
if (callee.type !== "MemberExpression" && callee.type !== "OptionalMemberExpression") return void 0;
|
|
262
|
+
const object = unwrapTs(callee.object);
|
|
263
|
+
const method = keyName(callee.property);
|
|
264
|
+
return object?.type === "Identifier" && formNames.has(object.name) && method && FORM_READS.has(method) ? method : void 0;
|
|
265
|
+
};
|
|
266
|
+
function asHelper(h, closure, st, fns, depth) {
|
|
267
|
+
const params = h.params.map(paramName);
|
|
268
|
+
let found;
|
|
269
|
+
walk(h.body, (n, parents) => {
|
|
270
|
+
if (found || n.type !== "CallExpression") return;
|
|
271
|
+
const callee = n.callee;
|
|
272
|
+
if (callee.type !== "MemberExpression" && callee.type !== "OptionalMemberExpression") return;
|
|
273
|
+
const method = keyName(callee.property);
|
|
274
|
+
const object = unwrapTs(callee.object);
|
|
275
|
+
const arg = unwrapTs(n.arguments[0]);
|
|
276
|
+
if (!method || !FORM_READS.has(method) || object?.type !== "Identifier" || arg?.type !== "Identifier") return;
|
|
277
|
+
const nameIndex = params.indexOf(arg.name);
|
|
278
|
+
if (nameIndex < 0) return;
|
|
279
|
+
const formIndex = params.indexOf(object.name);
|
|
280
|
+
if (formIndex < 0 && !closure.has(object.name)) return;
|
|
281
|
+
found = { nameIndex, ...formIndex >= 0 ? { formIndex } : {}, method, hint: method === "get" ? classify(n, parents, st, fns, depth) : {} };
|
|
282
|
+
});
|
|
283
|
+
return found;
|
|
284
|
+
}
|
|
285
|
+
function readsOf(fn, formNames, st, depth) {
|
|
286
|
+
if (st.visited.has(fn)) return;
|
|
287
|
+
st.visited.add(fn);
|
|
288
|
+
const names = new Set(formNames);
|
|
289
|
+
walk(fn.body, (n) => {
|
|
290
|
+
const init = unwrapTs(n.init);
|
|
291
|
+
if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && init?.type === "Identifier" && names.has(init.name)) names.add(n.id.name);
|
|
292
|
+
});
|
|
293
|
+
const nested = nestedFunctions(fn);
|
|
294
|
+
const fns = new Map([...st.topLevel, ...nested]);
|
|
295
|
+
const helpers = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const [name, h] of fns) {
|
|
297
|
+
if (h === fn) continue;
|
|
298
|
+
const helper = asHelper(h, nested.has(name) ? names : /* @__PURE__ */ new Set(), st, fns, depth);
|
|
299
|
+
if (helper) helpers.set(name, helper);
|
|
300
|
+
}
|
|
301
|
+
const unresolved = (n, what) => st.unresolved.push(`line ${lineOf(n)}: ${what}`);
|
|
302
|
+
walk(fn.body, (n, parents) => {
|
|
303
|
+
if (n.type === "ForOfStatement" && unwrapTs(n.right)?.type === "Identifier" && names.has(unwrapTs(n.right).name)) st.readsAll = true;
|
|
304
|
+
if (n.type === "SpreadElement" && unwrapTs(n.argument)?.type === "Identifier" && names.has(unwrapTs(n.argument).name)) st.readsAll = true;
|
|
305
|
+
if (n.type !== "CallExpression") return;
|
|
306
|
+
const callee = unwrapTs(n.callee);
|
|
307
|
+
const args = n.arguments;
|
|
308
|
+
const method = formRead(n, names);
|
|
309
|
+
if (method) {
|
|
310
|
+
const arg = unwrapTs(args[0]);
|
|
311
|
+
const enclosing = [...parents].reverse().find((p) => FUNCTION_TYPES.has(p.type));
|
|
312
|
+
if (arg?.type === "Identifier" && enclosing && enclosing.params.some((p) => paramName(p) === arg.name) && [...helpers.keys()].some((h) => fns.get(h) === enclosing)) return;
|
|
313
|
+
const fieldNames = namesOf(arg, parents, st);
|
|
314
|
+
if (!fieldNames) return unresolved(n, `a field name worked out at runtime: ${text(st.source, n)}`);
|
|
315
|
+
const hint = method === "has" ? { type: "boolean" } : method === "getAll" ? {} : classify(n, parents, st, fns, depth);
|
|
316
|
+
for (const name of fieldNames) addField(st, name, hint, method === "getAll");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (callee.type === "MemberExpression" && unwrapTs(callee.object)?.type === "Identifier" && names.has(unwrapTs(callee.object).name) && FORM_ITERATION.has(keyName(callee.property) ?? "")) {
|
|
320
|
+
st.readsAll = true;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const helper = callee.type === "Identifier" ? helpers.get(callee.name) : void 0;
|
|
324
|
+
if (helper) {
|
|
325
|
+
const form = helper.formIndex === void 0 ? void 0 : unwrapTs(args[helper.formIndex]);
|
|
326
|
+
if (helper.formIndex !== void 0 && !(form?.type === "Identifier" && names.has(form.name))) return;
|
|
327
|
+
const fieldNames = namesOf(args[helper.nameIndex], parents, st);
|
|
328
|
+
if (!fieldNames) return unresolved(n, `a field name worked out at runtime: ${text(st.source, n)}`);
|
|
329
|
+
const hint = helper.method === "has" ? { type: "boolean" } : merge(helper.hint, classify(n, parents, st, fns, depth));
|
|
330
|
+
for (const name of fieldNames) addField(st, name, hint, helper.method === "getAll");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const index = args.findIndex((a) => unwrapTs(a)?.type === "Identifier" && names.has(unwrapTs(a).name));
|
|
334
|
+
if (index < 0) return;
|
|
335
|
+
if (callee.type === "MemberExpression" && keyName(callee.object) === "Object" && keyName(callee.property) === "fromEntries") {
|
|
336
|
+
st.readsAll = true;
|
|
337
|
+
const decl = [...parents].reverse().find((p) => !["TSAsExpression", "TSSatisfiesExpression", "ParenthesizedExpression"].includes(p.type));
|
|
338
|
+
if (decl?.type === "VariableDeclarator" && decl.id.type === "ObjectPattern") {
|
|
339
|
+
for (const prop of decl.id.properties) {
|
|
340
|
+
const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
|
|
341
|
+
if (key) addField(st, key, {}, false);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const local = callee.type === "Identifier" ? fns.get(callee.name) : void 0;
|
|
347
|
+
const param = local && local.params[index];
|
|
348
|
+
const paramAs = param && paramName(param);
|
|
349
|
+
if (local && paramAs && depth < SEARCH_DEPTH) return readsOf(local, /* @__PURE__ */ new Set([paramAs]), st, depth + 1);
|
|
350
|
+
unresolved(n, `the form goes to ${text(st.source, callee) ?? "a function"}(), which isn't a function in this file`);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function annotated(t) {
|
|
354
|
+
if (t?.type === "TSStringKeyword") return { type: "string" };
|
|
355
|
+
if (t?.type === "TSNumberKeyword") return { type: "number" };
|
|
356
|
+
if (t?.type === "TSBooleanKeyword") return { type: "boolean" };
|
|
357
|
+
if (t?.type === "TSUnionType") {
|
|
358
|
+
const values = t.types.map((m) => m.type === "TSLiteralType" ? stringValue(m.literal) : void 0);
|
|
359
|
+
if (values.every((v) => v !== void 0)) return { type: "string", enum: values };
|
|
360
|
+
}
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
function analyzeFields(fn, roles, ctx) {
|
|
364
|
+
const st = { ...ctx, lists: /* @__PURE__ */ new Map(), fields: /* @__PURE__ */ new Map(), readsAll: false, unresolved: [], visited: /* @__PURE__ */ new Set() };
|
|
365
|
+
const params = fn.params.filter((p) => p.type !== "RestElement");
|
|
366
|
+
params.forEach((p, i) => {
|
|
367
|
+
const role = roles[i];
|
|
368
|
+
if (!role || role.role === "prev") return;
|
|
369
|
+
const id = p.type === "AssignmentPattern" ? p.left : p;
|
|
370
|
+
if (role.role === "formData") return readsOf(fn, /* @__PURE__ */ new Set([role.name]), st, 0);
|
|
371
|
+
if (role.role === "input") {
|
|
372
|
+
const hint = annotated(id.typeAnnotation?.typeAnnotation);
|
|
373
|
+
if (!hint) return st.unresolved.push(`line ${lineOf(p)}: parameter ${role.name} has no type it can read (string, number, boolean or a union of strings)`);
|
|
374
|
+
st.fields.set(role.name, { name: role.name, type: hint.type, ...hint.enum ? { enum: hint.enum } : {}, required: true });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const literal = id.typeAnnotation?.typeAnnotation;
|
|
378
|
+
const members = /* @__PURE__ */ new Map();
|
|
379
|
+
if (literal?.type === "TSTypeLiteral") {
|
|
380
|
+
for (const m of literal.members) if (m.type === "TSPropertySignature" && keyName(m.key)) members.set(keyName(m.key), m);
|
|
381
|
+
}
|
|
382
|
+
for (const prop of id.properties) {
|
|
383
|
+
const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
|
|
384
|
+
if (!key) {
|
|
385
|
+
st.unresolved.push(`line ${lineOf(prop)}: a rest element in parameter ${role.name}`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const member = members.get(key);
|
|
389
|
+
const hint = annotated(member?.typeAnnotation?.typeAnnotation);
|
|
390
|
+
if (!hint) {
|
|
391
|
+
st.unresolved.push(`line ${lineOf(prop)}: ${key} has no type it can read (string, number, boolean or a union of strings)`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
st.fields.set(key, { name: key, type: hint.type, ...hint.enum ? { enum: hint.enum } : {}, ...member?.optional || prop.value.type === "AssignmentPattern" ? {} : { required: true } });
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return { fields: [...st.fields.values()], readsAll: st.readsAll, unresolved: st.unresolved };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/forms.ts
|
|
401
|
+
var STATE_HOOKS = /* @__PURE__ */ new Set(["useActionState", "useFormState"]);
|
|
402
|
+
var FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
403
|
+
var COMPONENT_DEPTH = 2;
|
|
404
|
+
var tagName = (el) => el.openingElement?.name?.type === "JSXIdentifier" ? el.openingElement.name.name : void 0;
|
|
405
|
+
var attrName = (a) => a.name?.type === "JSXIdentifier" ? a.name.name : a.name?.type === "JSXNamespacedName" ? `${a.name.namespace.name}:${a.name.name.name}` : void 0;
|
|
406
|
+
var isComponent = (tag) => tag !== void 0 && /^[A-Z]/.test(tag);
|
|
407
|
+
function attrValue(attr, parents, scope) {
|
|
408
|
+
const v = attr.value;
|
|
409
|
+
if (v === null) return { kind: "bool", value: true };
|
|
410
|
+
if (v.type === "StringLiteral") return { kind: "string", value: v.value };
|
|
411
|
+
if (v.type !== "JSXExpressionContainer") return void 0;
|
|
412
|
+
const e = unwrapTs(v.expression);
|
|
413
|
+
if (!e || e.type === "JSXEmptyExpression") return void 0;
|
|
414
|
+
if (e.type === "BooleanLiteral") return { kind: "bool", value: e.value };
|
|
415
|
+
if (e.type === "NumericLiteral") return { kind: "number", value: e.value };
|
|
416
|
+
const s = stringValue(e);
|
|
417
|
+
if (s !== void 0) return { kind: "string", value: s };
|
|
418
|
+
return { kind: "expr", node: e, parents, scope };
|
|
419
|
+
}
|
|
420
|
+
function attributes(el, parents, scope) {
|
|
421
|
+
return el.openingElement.attributes.map((a) => a.type === "JSXSpreadAttribute" ? null : [attrName(a) ?? "", attrValue(a, parents, scope)]);
|
|
422
|
+
}
|
|
423
|
+
function moduleFor(file, source, ctx) {
|
|
424
|
+
if (ctx.modules.has(file)) return ctx.modules.get(file);
|
|
425
|
+
const program = source === void 0 ? null : parseModule(source, file);
|
|
426
|
+
const out = program ? { program, scope: listScope(program, file, ctx.load) } : null;
|
|
427
|
+
ctx.modules.set(file, out);
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
function componentIn(program, name) {
|
|
431
|
+
for (const stmt of program.body) {
|
|
432
|
+
const decl = stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration" ? stmt.declaration : stmt;
|
|
433
|
+
if (name === "default" && stmt.type === "ExportDefaultDeclaration" && decl && FUNCTION_TYPES.has(decl.type)) return decl;
|
|
434
|
+
if (decl?.type === "FunctionDeclaration" && decl.id?.name === name) return decl;
|
|
435
|
+
if (decl?.type === "VariableDeclaration") {
|
|
436
|
+
for (const d of decl.declarations) {
|
|
437
|
+
if (d.id.type !== "Identifier" || d.id.name !== name) continue;
|
|
438
|
+
let init = unwrapTs(d.init);
|
|
439
|
+
while (init?.type === "CallExpression" && init.arguments.length > 0) init = unwrapTs(init.arguments[0]);
|
|
440
|
+
if (init && FUNCTION_TYPES.has(init.type)) return init;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return void 0;
|
|
445
|
+
}
|
|
446
|
+
function findComponent(tag, mod, ctx) {
|
|
447
|
+
const local = componentIn(mod.program, tag);
|
|
448
|
+
if (local) return { fn: local, mod };
|
|
449
|
+
if (!ctx.load) return void 0;
|
|
450
|
+
for (const stmt of mod.program.body) {
|
|
451
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
452
|
+
const spec = stmt.specifiers.find((s) => s.local.name === tag);
|
|
453
|
+
if (!spec) continue;
|
|
454
|
+
const loaded = ctx.load(stmt.source.value, mod.scope.file);
|
|
455
|
+
if (!loaded) return void 0;
|
|
456
|
+
const other = moduleFor(loaded.file, loaded.source, ctx);
|
|
457
|
+
const fn = other && componentIn(other.program, spec.type === "ImportDefaultSpecifier" ? "default" : keyName(spec.imported) ?? tag);
|
|
458
|
+
return fn && other ? { fn, mod: other } : void 0;
|
|
459
|
+
}
|
|
460
|
+
return void 0;
|
|
461
|
+
}
|
|
462
|
+
function renderedAttributes(el, fn, use, mod) {
|
|
463
|
+
const param = fn.params[0];
|
|
464
|
+
const pattern = param?.type === "AssignmentPattern" ? param.left : param;
|
|
465
|
+
const propsName = pattern?.type === "Identifier" ? pattern.name : void 0;
|
|
466
|
+
let restName;
|
|
467
|
+
const locals = /* @__PURE__ */ new Map();
|
|
468
|
+
if (pattern?.type === "ObjectPattern") {
|
|
469
|
+
for (const p of pattern.properties) {
|
|
470
|
+
if (p.type === "RestElement" && p.argument.type === "Identifier") restName = p.argument.name;
|
|
471
|
+
if (p.type !== "ObjectProperty") continue;
|
|
472
|
+
const prop = keyName(p.key);
|
|
473
|
+
const value = p.value.type === "AssignmentPattern" ? p.value.left : p.value;
|
|
474
|
+
if (!prop || value.type !== "Identifier") continue;
|
|
475
|
+
const fallback = p.value.type === "AssignmentPattern" ? attrValue({ type: "JSXAttribute", value: { type: "JSXExpressionContainer", expression: p.value.right } }, [], mod.scope) : void 0;
|
|
476
|
+
locals.set(value.name, { prop, ...fallback ? { fallback } : {} });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const named2 = new Set([...locals.values()].map((l) => l.prop));
|
|
480
|
+
const out = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const a of el.openingElement.attributes) {
|
|
482
|
+
if (a.type === "JSXSpreadAttribute") {
|
|
483
|
+
const arg = unwrapTs(a.argument);
|
|
484
|
+
if (arg?.type === "Identifier" && (arg.name === restName || arg.name === propsName)) {
|
|
485
|
+
for (const [k, v] of use) if (arg.name === propsName || !named2.has(k)) out.set(k, v);
|
|
486
|
+
}
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const key = attrName(a);
|
|
490
|
+
if (!key) continue;
|
|
491
|
+
const expr = a.value?.type === "JSXExpressionContainer" ? unwrapTs(a.value.expression) : void 0;
|
|
492
|
+
if (expr?.type === "Identifier" && locals.has(expr.name)) {
|
|
493
|
+
const { prop, fallback } = locals.get(expr.name);
|
|
494
|
+
const v = use.has(prop) ? use.get(prop) : fallback;
|
|
495
|
+
if (v !== void 0) out.set(key, v);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (expr?.type === "MemberExpression" && unwrapTs(expr.object)?.type === "Identifier" && unwrapTs(expr.object).name === propsName) {
|
|
499
|
+
const prop = keyName(expr.property);
|
|
500
|
+
if (prop && use.has(prop)) out.set(key, use.get(prop));
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
out.set(key, attrValue(a, [], mod.scope));
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
function receivesProps(el, fn) {
|
|
508
|
+
const param = fn.params[0];
|
|
509
|
+
const pattern = param?.type === "AssignmentPattern" ? param.left : param;
|
|
510
|
+
const names = /* @__PURE__ */ new Set();
|
|
511
|
+
if (pattern?.type === "Identifier") names.add(pattern.name);
|
|
512
|
+
if (pattern?.type === "ObjectPattern") {
|
|
513
|
+
for (const p of pattern.properties) {
|
|
514
|
+
if (p.type === "RestElement" && p.argument.type === "Identifier") names.add(p.argument.name);
|
|
515
|
+
const value = p.type === "ObjectProperty" ? p.value.type === "AssignmentPattern" ? p.value.left : p.value : void 0;
|
|
516
|
+
if (value?.type === "Identifier" && keyName(p.key) === "name") names.add(value.name);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return el.openingElement.attributes.some((a) => {
|
|
520
|
+
if (a.type === "JSXSpreadAttribute") return unwrapTs(a.argument)?.type === "Identifier" && names.has(unwrapTs(a.argument).name);
|
|
521
|
+
if (attrName(a) !== "name") return false;
|
|
522
|
+
const e = a.value?.type === "JSXExpressionContainer" ? unwrapTs(a.value.expression) : void 0;
|
|
523
|
+
return e?.type === "Identifier" && names.has(e.name) || e?.type === "MemberExpression" && unwrapTs(e.object)?.type === "Identifier" && names.has(unwrapTs(e.object).name);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
function throughComponent(tag, use, mod, ctx, depth) {
|
|
527
|
+
if (depth > COMPONENT_DEPTH) return void 0;
|
|
528
|
+
const found = findComponent(tag, mod, ctx);
|
|
529
|
+
if (!found) return void 0;
|
|
530
|
+
let result;
|
|
531
|
+
walk(found.fn.body, (n) => {
|
|
532
|
+
if (result || n.type !== "JSXElement") return;
|
|
533
|
+
const inner = tagName(n);
|
|
534
|
+
if (!inner || !(FIELD_TAGS.has(inner) || isComponent(inner)) || !receivesProps(n, found.fn)) return;
|
|
535
|
+
const attrs = renderedAttributes(n, found.fn, use, found.mod);
|
|
536
|
+
result = FIELD_TAGS.has(inner) ? { tag: inner, attrs } : throughComponent(inner, attrs, found.mod, ctx, depth + 1);
|
|
537
|
+
});
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
var num = (v) => v?.kind === "number" ? v.value : v?.kind === "string" && v.value.trim() !== "" && !Number.isNaN(Number(v.value)) ? Number(v.value) : void 0;
|
|
541
|
+
var str = (v) => v?.kind === "string" ? v.value : void 0;
|
|
542
|
+
var truthy = (v) => v?.kind === "bool" ? v.value : v?.kind === "string" ? v.value !== "false" : false;
|
|
543
|
+
function selectOptions(el, scope) {
|
|
544
|
+
const values = [];
|
|
545
|
+
let open = false;
|
|
546
|
+
walk(el.children, (n, parents) => {
|
|
547
|
+
if (n.type !== "JSXElement" || tagName(n) !== "option") return;
|
|
548
|
+
const attr = n.openingElement.attributes.find((a) => a.type === "JSXAttribute" && attrName(a) === "value");
|
|
549
|
+
const v = attr && attrValue(attr, [...parents], scope);
|
|
550
|
+
if (v?.kind === "string") return void values.push(v.value);
|
|
551
|
+
const names = v?.kind !== "expr" ? void 0 : v.node.type === "Identifier" ? loopValues(v.node.name, [...parents], scope) : namesOf(v.node, [...parents], scope);
|
|
552
|
+
if (names) values.push(...names);
|
|
553
|
+
else open = true;
|
|
554
|
+
});
|
|
555
|
+
return open || values.length === 0 ? void 0 : [...new Set(values)];
|
|
556
|
+
}
|
|
557
|
+
function formFields(form, mod, ctx, unresolved) {
|
|
558
|
+
const fields = /* @__PURE__ */ new Map();
|
|
559
|
+
const radios = /* @__PURE__ */ new Map();
|
|
560
|
+
walk(form.children, (n, parents) => {
|
|
561
|
+
if (n.type !== "JSXElement") return;
|
|
562
|
+
const tag = tagName(n);
|
|
563
|
+
if (!tag || !(FIELD_TAGS.has(tag) || isComponent(tag))) return;
|
|
564
|
+
const own = attributes(n, [...parents], mod.scope);
|
|
565
|
+
const use = new Map(own.filter((a) => a !== null));
|
|
566
|
+
if (!use.has("name")) return;
|
|
567
|
+
let el = { tag, attrs: use };
|
|
568
|
+
if (isComponent(tag)) el = throughComponent(tag, use, mod, ctx, 1) ?? { tag: "input", attrs: use };
|
|
569
|
+
const nameVal = el.attrs.get("name");
|
|
570
|
+
const names = nameVal?.kind === "string" ? [nameVal.value] : nameVal?.kind === "expr" ? namesOf(nameVal.node, nameVal.parents, nameVal.scope) : void 0;
|
|
571
|
+
if (!names) return void unresolved.push(`${mod.scope.file}:${lineOf(n)}: a field name from runtime data`);
|
|
572
|
+
const conditional = parents.some((p) => p.type === "LogicalExpression" || p.type === "ConditionalExpression");
|
|
573
|
+
const type = str(el.attrs.get("type"))?.toLowerCase();
|
|
574
|
+
const value = el.attrs.get("value");
|
|
575
|
+
const field = {};
|
|
576
|
+
if (el.tag === "input" && (type === "number" || type === "range")) field.type = "number";
|
|
577
|
+
if (el.tag === "input" && type === "checkbox") field.type = "boolean";
|
|
578
|
+
if (el.tag === "input" && type === "file") field.type = "file";
|
|
579
|
+
const stringified = value?.kind === "expr" && value.node.type === "CallExpression" && keyName(value.node.callee?.object) === "JSON" && keyName(value.node.callee?.property) === "stringify";
|
|
580
|
+
if (stringified) field.type = "json";
|
|
581
|
+
if (el.tag === "select" && !isComponent(tag)) {
|
|
582
|
+
const options = selectOptions(n, mod.scope);
|
|
583
|
+
if (options) field.enum = options;
|
|
584
|
+
}
|
|
585
|
+
if (el.tag === "input" && type === "radio" && str(value) !== void 0) {
|
|
586
|
+
for (const name of names) radios.set(name, [...radios.get(name) ?? [], str(value)]);
|
|
587
|
+
}
|
|
588
|
+
const required = truthy(el.attrs.get("required")) || el.tag === "input" && type === "hidden";
|
|
589
|
+
if (required && !conditional && type !== "checkbox") field.required = true;
|
|
590
|
+
for (const k of ["min", "max", "minLength", "maxLength"]) {
|
|
591
|
+
const v = num(el.attrs.get(k) ?? el.attrs.get(k.toLowerCase()));
|
|
592
|
+
if (v !== void 0) field[k] = v;
|
|
593
|
+
}
|
|
594
|
+
const pattern = str(el.attrs.get("pattern"));
|
|
595
|
+
if (pattern) field.pattern = pattern;
|
|
596
|
+
for (const name of names) fields.set(name, { name, ...fields.get(name), ...field });
|
|
597
|
+
});
|
|
598
|
+
for (const [name, values] of radios) {
|
|
599
|
+
const f = fields.get(name);
|
|
600
|
+
if (f) f.enum = [...new Set(values)];
|
|
601
|
+
}
|
|
602
|
+
return [...fields.values()];
|
|
603
|
+
}
|
|
604
|
+
function findFormUses(source, file, actions, load, ctx = { ...load ? { load } : {}, modules: /* @__PURE__ */ new Map() }) {
|
|
605
|
+
const mod = moduleFor(file, source, ctx);
|
|
606
|
+
if (!mod) return [];
|
|
607
|
+
const { program } = mod;
|
|
608
|
+
const imported = /* @__PURE__ */ new Map();
|
|
609
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
610
|
+
for (const stmt of program.body) {
|
|
611
|
+
if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type" || !load) continue;
|
|
612
|
+
const target = load(stmt.source.value, file);
|
|
613
|
+
const exports = target && actions.get(target.file);
|
|
614
|
+
if (!target || !exports) continue;
|
|
615
|
+
for (const spec of stmt.specifiers) {
|
|
616
|
+
if (spec.type === "ImportNamespaceSpecifier") namespaces.set(spec.local.name, target.file);
|
|
617
|
+
const name = spec.type === "ImportDefaultSpecifier" ? "default" : spec.type === "ImportSpecifier" ? keyName(spec.imported) : void 0;
|
|
618
|
+
if (name && exports.has(name)) imported.set(spec.local.name, { file: target.file, export: name });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (imported.size === 0 && namespaces.size === 0) return [];
|
|
622
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
623
|
+
const refOf = (e) => {
|
|
624
|
+
e = unwrapTs(e);
|
|
625
|
+
if (!e) return void 0;
|
|
626
|
+
if (e.type === "Identifier") return aliases.get(e.name) ?? (imported.has(e.name) ? { ...imported.get(e.name), bound: 0, prev: false } : void 0);
|
|
627
|
+
if (e.type === "MemberExpression" && unwrapTs(e.object)?.type === "Identifier" && namespaces.has(unwrapTs(e.object).name)) {
|
|
628
|
+
const name = keyName(e.property);
|
|
629
|
+
const target = namespaces.get(unwrapTs(e.object).name);
|
|
630
|
+
return name && actions.get(target)?.has(name) ? { file: target, export: name, bound: 0, prev: false } : void 0;
|
|
631
|
+
}
|
|
632
|
+
if (e.type === "CallExpression" && e.callee.type === "MemberExpression" && keyName(e.callee.property) === "bind") {
|
|
633
|
+
const inner = refOf(e.callee.object);
|
|
634
|
+
return inner && { ...inner, bound: inner.bound + Math.max(0, e.arguments.length - 1) };
|
|
635
|
+
}
|
|
636
|
+
return void 0;
|
|
637
|
+
};
|
|
638
|
+
const refIn = (e) => {
|
|
639
|
+
let found;
|
|
640
|
+
walk(e, (n, parents) => {
|
|
641
|
+
if (found || !(n.type === "Identifier" || n.type === "MemberExpression" || n.type === "CallExpression")) return;
|
|
642
|
+
const ref = refOf(n);
|
|
643
|
+
if (!ref) return;
|
|
644
|
+
const call = parents[parents.length - 1];
|
|
645
|
+
const called = call?.type === "CallExpression" && call.callee === n && !ref.prev;
|
|
646
|
+
found = called ? { ...ref, bound: ref.bound + Math.max(0, call.arguments.length - 1) } : ref;
|
|
647
|
+
});
|
|
648
|
+
return found;
|
|
649
|
+
};
|
|
650
|
+
walk(program, (n) => {
|
|
651
|
+
if (n.type !== "VariableDeclarator") return;
|
|
652
|
+
const init = unwrapTs(n.init);
|
|
653
|
+
if (n.id.type === "ArrayPattern" && init?.type === "CallExpression") {
|
|
654
|
+
const hook = init.callee.type === "Identifier" ? init.callee.name : keyName(init.callee.property);
|
|
655
|
+
const ref = hook && STATE_HOOKS.has(hook) ? refOf(init.arguments[0]) : void 0;
|
|
656
|
+
const id = n.id.elements[1];
|
|
657
|
+
if (ref && id?.type === "Identifier") aliases.set(id.name, { ...ref, prev: true });
|
|
658
|
+
}
|
|
659
|
+
if (n.id.type === "Identifier" && init) {
|
|
660
|
+
const ref = refOf(init);
|
|
661
|
+
if (ref) aliases.set(n.id.name, ref);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
const out = [];
|
|
665
|
+
const unresolvedFor = /* @__PURE__ */ new Map();
|
|
666
|
+
const fieldsFor = (form) => {
|
|
667
|
+
if (!unresolvedFor.has(form)) unresolvedFor.set(form, []);
|
|
668
|
+
return formFields(form, mod, ctx, unresolvedFor.get(form));
|
|
669
|
+
};
|
|
670
|
+
walk(program, (n, parents) => {
|
|
671
|
+
if (n.type !== "JSXElement") return;
|
|
672
|
+
const tag = tagName(n);
|
|
673
|
+
const attr = (name) => n.openingElement.attributes.find((a) => a.type === "JSXAttribute" && attrName(a) === name);
|
|
674
|
+
const refs = [];
|
|
675
|
+
if (tag === "form") {
|
|
676
|
+
for (const name of ["action", "onSubmit"]) {
|
|
677
|
+
const ref = refIn(attr(name)?.value);
|
|
678
|
+
if (ref) refs.push(ref);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const formAction = (tag === "button" || tag === "input") && refIn(attr("formAction")?.value);
|
|
682
|
+
const form = formAction ? [...parents].reverse().find((p) => p.type === "JSXElement" && tagName(p) === "form") : n;
|
|
683
|
+
if (formAction) refs.push(formAction);
|
|
684
|
+
if (!form || refs.length === 0) return;
|
|
685
|
+
const fields = fieldsFor(form);
|
|
686
|
+
for (const ref of refs.filter((r, i) => refs.findIndex((o) => o.file === r.file && o.export === r.export) === i)) {
|
|
687
|
+
out.push({ action: { file: ref.file, export: ref.export }, file, line: lineOf(form), bound: ref.bound, prev: ref.prev, fields, unresolved: unresolvedFor.get(form) });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
function linkForms(action, uses) {
|
|
693
|
+
if (uses.length === 0) return action;
|
|
694
|
+
const fromForms = /* @__PURE__ */ new Map();
|
|
695
|
+
for (const use of uses) {
|
|
696
|
+
for (const f of use.fields) {
|
|
697
|
+
const seen = fromForms.get(f.name);
|
|
698
|
+
fromForms.set(f.name, { ...seen, ...f, enum: seen?.enum ?? f.enum, requiredIn: (seen?.requiredIn ?? 0) + (f.required ? 1 : 0) });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const formsWith = (name) => uses.filter((u) => u.fields.some((f) => f.name === name)).length;
|
|
702
|
+
const merged = (base, f) => {
|
|
703
|
+
const type = f.type === "file" ? "file" : base?.type ?? f.type ?? "string";
|
|
704
|
+
const values = base?.enum ?? f.enum;
|
|
705
|
+
const required = base?.required || f.requiredIn === formsWith(f.name);
|
|
706
|
+
return {
|
|
707
|
+
name: f.name,
|
|
708
|
+
type,
|
|
709
|
+
...base?.array ? { array: true } : {},
|
|
710
|
+
...values ? { enum: values } : {},
|
|
711
|
+
...required ? { required: true } : {},
|
|
712
|
+
...type === "number" && f.min !== void 0 ? { min: f.min } : {},
|
|
713
|
+
...type === "number" && f.max !== void 0 ? { max: f.max } : {},
|
|
714
|
+
...type === "string" && f.minLength !== void 0 ? { minLength: f.minLength } : {},
|
|
715
|
+
...type === "string" && f.maxLength !== void 0 ? { maxLength: f.maxLength } : {},
|
|
716
|
+
...type === "string" && f.pattern !== void 0 ? { pattern: f.pattern } : {},
|
|
717
|
+
origin: base ? "both" : "form"
|
|
718
|
+
};
|
|
719
|
+
};
|
|
720
|
+
const fields = action.fields.map((a) => {
|
|
721
|
+
const f = fromForms.get(a.name);
|
|
722
|
+
return f ? merged(a, f) : { ...a, origin: "action" };
|
|
723
|
+
});
|
|
724
|
+
if (action.readsAll) {
|
|
725
|
+
for (const f of fromForms.values()) if (!action.fields.some((a) => a.name === f.name)) fields.push(merged(void 0, f));
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
...action,
|
|
729
|
+
fields,
|
|
730
|
+
// A form's unreadable names matter only when the action reads whatever the form sends.
|
|
731
|
+
unresolved: [...action.unresolved, ...action.readsAll ? uses.flatMap((u) => u.unresolved) : []],
|
|
732
|
+
forms: uses.map((u) => ({ file: u.file, line: u.line }))
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/hosts.ts
|
|
737
|
+
import { readFileSync } from "fs";
|
|
738
|
+
import { join } from "path";
|
|
739
|
+
|
|
740
|
+
// src/routes.ts
|
|
741
|
+
var HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
742
|
+
var DEFAULT_PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
|
|
743
|
+
function appRoute(dirSegments) {
|
|
744
|
+
const parts = [];
|
|
745
|
+
const params = [];
|
|
746
|
+
for (const seg of dirSegments) {
|
|
747
|
+
if (seg.startsWith("_") || seg.startsWith("@") || seg.startsWith("(.")) return null;
|
|
748
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
749
|
+
const part = dynamicSegment(seg, params);
|
|
750
|
+
if (part === null) return null;
|
|
751
|
+
parts.push(part);
|
|
752
|
+
}
|
|
753
|
+
return { path: `/${parts.join("/")}`, params };
|
|
754
|
+
}
|
|
755
|
+
function pagesApiRoute(segments) {
|
|
756
|
+
if (segments[0] !== "api") return null;
|
|
757
|
+
const segs = segments.at(-1) === "index" ? segments.slice(0, -1) : segments;
|
|
758
|
+
const parts = [];
|
|
759
|
+
const params = [];
|
|
760
|
+
for (const seg of segs) {
|
|
761
|
+
if (seg.startsWith("_")) return null;
|
|
762
|
+
const part = dynamicSegment(seg, params);
|
|
763
|
+
if (part === null) return null;
|
|
764
|
+
parts.push(part);
|
|
765
|
+
}
|
|
766
|
+
return { path: `/${parts.join("/")}`, params };
|
|
767
|
+
}
|
|
768
|
+
function dynamicSegment(seg, params) {
|
|
769
|
+
const optionalCatchAll = /^\[\[\.\.\.([A-Za-z0-9_]+)\]\]$/.exec(seg);
|
|
770
|
+
if (optionalCatchAll) {
|
|
771
|
+
params.push(optionalCatchAll[1]);
|
|
772
|
+
return `:${optionalCatchAll[1]}*?`;
|
|
773
|
+
}
|
|
774
|
+
const catchAll = /^\[\.\.\.([A-Za-z0-9_]+)\]$/.exec(seg);
|
|
775
|
+
if (catchAll) {
|
|
776
|
+
params.push(catchAll[1]);
|
|
777
|
+
return `:${catchAll[1]}*`;
|
|
778
|
+
}
|
|
779
|
+
const dynamic = /^\[([A-Za-z0-9_]+)\]$/.exec(seg);
|
|
780
|
+
if (dynamic) {
|
|
781
|
+
params.push(dynamic[1]);
|
|
782
|
+
return `:${dynamic[1]}`;
|
|
783
|
+
}
|
|
784
|
+
return seg.includes("[") ? null : seg;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/hosts.ts
|
|
788
|
+
function edgesOf(source, file, load) {
|
|
789
|
+
const program = parseModule(source, file);
|
|
790
|
+
if (!program) return [];
|
|
791
|
+
const edges = [];
|
|
792
|
+
const add = (specifier2, names) => {
|
|
793
|
+
if (typeof specifier2 !== "string") return;
|
|
794
|
+
const target = load(specifier2, file);
|
|
795
|
+
if (target) edges.push({ file: target.file, names });
|
|
796
|
+
};
|
|
797
|
+
for (const stmt of program.body) {
|
|
798
|
+
if (stmt.type === "ImportDeclaration" && stmt.importKind !== "type") {
|
|
799
|
+
const specs = stmt.specifiers;
|
|
800
|
+
if (specs.some((s) => s.type === "ImportNamespaceSpecifier")) add(stmt.source.value, "*");
|
|
801
|
+
else add(stmt.source.value, new Set(specs.filter((s) => s.importKind !== "type").map((s) => s.type === "ImportDefaultSpecifier" ? "default" : keyName(s.imported) ?? "").filter(Boolean)));
|
|
802
|
+
} else if (stmt.type === "ExportNamedDeclaration" && stmt.source && stmt.exportKind !== "type") {
|
|
803
|
+
add(stmt.source.value, new Set(stmt.specifiers.map((s) => s.type === "ExportNamespaceSpecifier" ? "*" : keyName(s.local) ?? "").filter(Boolean)));
|
|
804
|
+
} else if (stmt.type === "ExportAllDeclaration" && stmt.exportKind !== "type") {
|
|
805
|
+
add(stmt.source.value, "*");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
walk(program, (n) => {
|
|
809
|
+
if (n.type === "CallExpression" && n.callee?.type === "Import") add(n.arguments?.[0]?.value, "*");
|
|
810
|
+
if (n.type === "ImportExpression") add(n.source?.value, "*");
|
|
811
|
+
});
|
|
812
|
+
return edges;
|
|
813
|
+
}
|
|
814
|
+
function moduleGraph(root, load) {
|
|
815
|
+
const cache = /* @__PURE__ */ new Map();
|
|
816
|
+
const edges = (file) => {
|
|
817
|
+
let e = cache.get(file);
|
|
818
|
+
if (!e) {
|
|
819
|
+
let source;
|
|
820
|
+
try {
|
|
821
|
+
source = readFileSync(join(root, file), "utf8");
|
|
822
|
+
} catch {
|
|
823
|
+
source = void 0;
|
|
824
|
+
}
|
|
825
|
+
e = source === void 0 ? [] : edgesOf(source, file, load);
|
|
826
|
+
cache.set(file, e);
|
|
827
|
+
}
|
|
828
|
+
return e;
|
|
829
|
+
};
|
|
830
|
+
return {
|
|
831
|
+
edges,
|
|
832
|
+
reachable(roots) {
|
|
833
|
+
const seen = /* @__PURE__ */ new Set();
|
|
834
|
+
const queue = [...roots];
|
|
835
|
+
while (queue.length > 0 && seen.size < 2e3) {
|
|
836
|
+
const file = queue.shift();
|
|
837
|
+
if (seen.has(file)) continue;
|
|
838
|
+
seen.add(file);
|
|
839
|
+
for (const edge of edges(file)) if (!seen.has(edge.file)) queue.push(edge.file);
|
|
840
|
+
}
|
|
841
|
+
return seen;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function pageEntries(appDir, files, exts) {
|
|
846
|
+
const extPattern = exts.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
847
|
+
const entry = new RegExp(`^(page|layout|template)\\.(${extPattern})$`);
|
|
848
|
+
const base = (f) => f.slice(f.lastIndexOf("/") + 1);
|
|
849
|
+
const inApp = files.filter((f) => f.startsWith(`${appDir}/`) && entry.test(base(f)));
|
|
850
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
851
|
+
for (const f of inApp) {
|
|
852
|
+
if (/^page\./.test(base(f))) continue;
|
|
853
|
+
const dir = f.slice(0, f.lastIndexOf("/"));
|
|
854
|
+
layouts.set(dir, [...layouts.get(dir) ?? [], f]);
|
|
855
|
+
}
|
|
856
|
+
const out = [];
|
|
857
|
+
for (const page of inApp.filter((f) => /^page\./.test(base(f)))) {
|
|
858
|
+
const dir = page.slice(0, page.lastIndexOf("/"));
|
|
859
|
+
const route = appRoute(dir === appDir ? [] : dir.slice(appDir.length + 1).split("/"));
|
|
860
|
+
if (!route) continue;
|
|
861
|
+
const roots = [page];
|
|
862
|
+
for (let d = dir; d.length >= appDir.length; d = d.slice(0, Math.max(d.lastIndexOf("/"), 0))) {
|
|
863
|
+
roots.push(...layouts.get(d) ?? []);
|
|
864
|
+
if (d === appDir) break;
|
|
865
|
+
}
|
|
866
|
+
out.push({ file: page, path: route.path, params: route.params, roots });
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
function actionHosts(pages, graph, actionExports) {
|
|
871
|
+
const best = /* @__PURE__ */ new Map();
|
|
872
|
+
for (const page of pages) {
|
|
873
|
+
const used = /* @__PURE__ */ new Set();
|
|
874
|
+
for (const file of graph.reachable(page.roots)) {
|
|
875
|
+
for (const edge of graph.edges(file)) {
|
|
876
|
+
const exported = actionExports.get(edge.file);
|
|
877
|
+
if (exported) {
|
|
878
|
+
for (const name of edge.names === "*" ? exported : edge.names) if (exported.has(name)) used.add(`${edge.file}#${name}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const key of used) {
|
|
883
|
+
const current = best.get(key);
|
|
884
|
+
const candidate = { path: page.path, params: page.params.length };
|
|
885
|
+
if (!current || candidate.params < current.params || candidate.params === current.params && (candidate.path.length < current.path.length || candidate.path.length === current.path.length && candidate.path < current.path)) {
|
|
886
|
+
best.set(key, candidate);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return new Map([...best].map(([k, v]) => [k, v.path]));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/login.ts
|
|
894
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
895
|
+
import { join as join2 } from "path";
|
|
896
|
+
import { LOGIN_NAME, NOT_A_LOGIN, loginFields } from "@exposify/core";
|
|
897
|
+
var SESSION_CODE = /cookies\(\)\s*\.set|\.cookies\.set\(|cookieStore\.set|set-cookie|setCookie|\bjwt\.sign\(|new\s+SignJWT|\bsign\(|createSession|session\.(save|create)|\.login\(/i;
|
|
898
|
+
function routeBodyFields(source) {
|
|
899
|
+
const names = /* @__PURE__ */ new Set();
|
|
900
|
+
let body = null;
|
|
901
|
+
for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?\w+\.formData\(\)/g)) {
|
|
902
|
+
body ??= "form";
|
|
903
|
+
for (const read2 of source.matchAll(new RegExp(`\\b${m[1]}\\.get\\(\\s*["'\`]([A-Za-z0-9_.\\-]+)["'\`]`, "g"))) names.add(read2[1]);
|
|
904
|
+
}
|
|
905
|
+
for (const m of source.matchAll(/(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:await\s+)?\w+\.(json|formData)\(\)/g)) {
|
|
906
|
+
body = m[2] === "json" ? "json" : "form";
|
|
907
|
+
for (const part of m[1].split(",")) {
|
|
908
|
+
const name = part.split(/[:=]/)[0].trim().replace(/^\.\.\./, "");
|
|
909
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) names.add(name);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?\w+\.json\(\)/g)) {
|
|
913
|
+
body ??= "json";
|
|
914
|
+
for (const read2 of source.matchAll(new RegExp(`\\b${m[1]}\\.(\\w+)`, "g"))) names.add(read2[1]);
|
|
915
|
+
}
|
|
916
|
+
return names.size && body ? { fields: [...names], body } : null;
|
|
917
|
+
}
|
|
918
|
+
function actionCandidate(a) {
|
|
919
|
+
if (NOT_A_LOGIN.test(a.key) || NOT_A_LOGIN.test(a.action.name)) return null;
|
|
920
|
+
if (!a.action.params.some((p) => p.role === "formData" || p.role === "object")) return null;
|
|
921
|
+
const fields = loginFields(a.action.fields.map((f) => f.name));
|
|
922
|
+
if (!fields) return null;
|
|
923
|
+
if (!a.action.setsCookies && !LOGIN_NAME.test(a.action.name)) return null;
|
|
924
|
+
return { kind: "action", key: a.key, export: a.action.name, file: a.file, fields, ...a.host ? { host: a.host } : {} };
|
|
925
|
+
}
|
|
926
|
+
function routeCandidate(root, r) {
|
|
927
|
+
if (r.router !== "app" || !r.module.methods.includes("POST") || r.params.length > 0 || NOT_A_LOGIN.test(r.path)) return null;
|
|
928
|
+
let source;
|
|
929
|
+
try {
|
|
930
|
+
source = readFileSync2(join2(root, r.file), "utf8");
|
|
931
|
+
} catch {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
const read2 = routeBodyFields(source);
|
|
935
|
+
const fields = read2 ? loginFields(read2.fields) : null;
|
|
936
|
+
if (!read2 || !fields) return null;
|
|
937
|
+
if (!LOGIN_NAME.test(r.path) && !SESSION_CODE.test(source)) return null;
|
|
938
|
+
return { kind: "route", path: r.path, file: r.file, fields, body: read2.body };
|
|
939
|
+
}
|
|
940
|
+
var named = (l) => LOGIN_NAME.test(l.kind === "action" ? l.export : l.path);
|
|
941
|
+
function findLogin(d) {
|
|
942
|
+
const candidates = [];
|
|
943
|
+
for (const a of d.actions) {
|
|
944
|
+
const c = actionCandidate(a);
|
|
945
|
+
if (c) candidates.push(c);
|
|
946
|
+
}
|
|
947
|
+
for (const r of d.routes) {
|
|
948
|
+
const c = routeCandidate(d.root, r);
|
|
949
|
+
if (c) candidates.push(c);
|
|
950
|
+
}
|
|
951
|
+
if (candidates.length === 0) return null;
|
|
952
|
+
const score = (l) => (named(l) ? 0 : 4) + (l.kind === "action" && l.host ? 0 : 2) + l.fields.length / 100;
|
|
953
|
+
candidates.sort((a, b) => score(a) - score(b));
|
|
954
|
+
return { login: candidates[0], others: candidates.slice(1) };
|
|
955
|
+
}
|
|
956
|
+
function loginLabel(l) {
|
|
957
|
+
return l.kind === "action" ? `server action ${l.export} (${l.file})` : `POST ${l.path} (${l.file})`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/guards.ts
|
|
961
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
962
|
+
import { join as join3 } from "path";
|
|
963
|
+
var STOPS = /* @__PURE__ */ new Set(["notFound", "redirect", "permanentRedirect", "forbidden", "unauthorized"]);
|
|
964
|
+
var MAX_DEPTH = 4;
|
|
965
|
+
function analyse(source, file) {
|
|
966
|
+
const program = parseModule(source, file);
|
|
967
|
+
if (!program) return null;
|
|
968
|
+
const imports = /* @__PURE__ */ new Map();
|
|
969
|
+
const stops = /* @__PURE__ */ new Set();
|
|
970
|
+
const functions = /* @__PURE__ */ new Map();
|
|
971
|
+
const exports = /* @__PURE__ */ new Map();
|
|
972
|
+
const fn = (n) => n && FUNCTION_TYPES.has(n.type) ? n : void 0;
|
|
973
|
+
for (const stmt of program.body) {
|
|
974
|
+
if (stmt.type === "ImportDeclaration" && stmt.importKind !== "type") {
|
|
975
|
+
const specifier2 = stmt.source.value;
|
|
976
|
+
for (const s of stmt.specifiers) {
|
|
977
|
+
const local = s.local?.name;
|
|
978
|
+
if (!local) continue;
|
|
979
|
+
const name = s.type === "ImportDefaultSpecifier" ? "default" : s.type === "ImportNamespaceSpecifier" ? "*" : keyName(s.imported) ?? local;
|
|
980
|
+
if (specifier2 === "next/navigation" && STOPS.has(name)) stops.add(local);
|
|
981
|
+
else imports.set(local, { specifier: specifier2, name });
|
|
982
|
+
}
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const decl = stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration" ? stmt.declaration : stmt;
|
|
986
|
+
if (decl?.type === "FunctionDeclaration" && decl.id) {
|
|
987
|
+
functions.set(decl.id.name, decl);
|
|
988
|
+
if (stmt.type === "ExportNamedDeclaration") exports.set(decl.id.name, decl.id.name);
|
|
989
|
+
}
|
|
990
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
991
|
+
if (decl?.type === "FunctionDeclaration" && decl.id) exports.set("default", decl.id.name);
|
|
992
|
+
else if (fn(decl)) {
|
|
993
|
+
functions.set("default", decl);
|
|
994
|
+
exports.set("default", "default");
|
|
995
|
+
} else if (decl?.type === "Identifier") exports.set("default", decl.name);
|
|
996
|
+
}
|
|
997
|
+
if (decl?.type === "VariableDeclaration") {
|
|
998
|
+
for (const d of decl.declarations) {
|
|
999
|
+
const init = fn(d.init) ?? fn(d.init?.arguments?.[0]);
|
|
1000
|
+
if (d.id?.type === "Identifier" && init) {
|
|
1001
|
+
functions.set(d.id.name, init);
|
|
1002
|
+
if (stmt.type === "ExportNamedDeclaration") exports.set(d.id.name, d.id.name);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (stmt.type === "ExportNamedDeclaration" && !stmt.source) {
|
|
1007
|
+
for (const s of stmt.specifiers ?? []) {
|
|
1008
|
+
const exported = keyName(s.exported);
|
|
1009
|
+
if (exported && s.local?.name) exports.set(exported, s.local.name);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return { program, imports, stops, functions, exports };
|
|
1014
|
+
}
|
|
1015
|
+
function guardFinder(root, load) {
|
|
1016
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1017
|
+
const known = /* @__PURE__ */ new Map();
|
|
1018
|
+
const parse2 = (file, source) => {
|
|
1019
|
+
if (!parsed.has(file)) {
|
|
1020
|
+
let text2 = source;
|
|
1021
|
+
if (text2 === void 0) {
|
|
1022
|
+
try {
|
|
1023
|
+
text2 = readFileSync3(join3(root, file), "utf8");
|
|
1024
|
+
} catch {
|
|
1025
|
+
text2 = void 0;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
parsed.set(file, text2 === void 0 ? null : analyse(text2, file));
|
|
1029
|
+
}
|
|
1030
|
+
return parsed.get(file);
|
|
1031
|
+
};
|
|
1032
|
+
const stopsLocally = (file, p, local, depth, seen) => {
|
|
1033
|
+
const body = p.functions.get(local);
|
|
1034
|
+
if (!body || seen.has(`${file}#${local}`)) return false;
|
|
1035
|
+
seen.add(`${file}#${local}`);
|
|
1036
|
+
let found = false;
|
|
1037
|
+
walk(body, (n) => {
|
|
1038
|
+
if (found || n.type !== "CallExpression" || n.callee?.type !== "Identifier") return;
|
|
1039
|
+
const name = n.callee.name;
|
|
1040
|
+
if (p.stops.has(name)) found = true;
|
|
1041
|
+
else if (p.functions.has(name) && name !== local) found = stopsLocally(file, p, name, depth, seen);
|
|
1042
|
+
else if (p.imports.has(name)) found = guardKey(file, p, name, depth + 1) !== null;
|
|
1043
|
+
});
|
|
1044
|
+
return found;
|
|
1045
|
+
};
|
|
1046
|
+
const guardKey = (file, p, local, depth) => {
|
|
1047
|
+
if (depth > MAX_DEPTH) return null;
|
|
1048
|
+
const imp = p.imports.get(local);
|
|
1049
|
+
if (!imp || imp.name === "*") return null;
|
|
1050
|
+
const target = load(imp.specifier, file);
|
|
1051
|
+
if (!target) return null;
|
|
1052
|
+
const key = `${target.file}#${imp.name}`;
|
|
1053
|
+
const cached = known.get(key);
|
|
1054
|
+
if (cached !== void 0) return cached ? key : null;
|
|
1055
|
+
known.set(key, false);
|
|
1056
|
+
const tp = parse2(target.file, target.source);
|
|
1057
|
+
const localName = tp?.exports.get(imp.name);
|
|
1058
|
+
const is = Boolean(tp && localName && stopsLocally(target.file, tp, localName, depth, /* @__PURE__ */ new Set()));
|
|
1059
|
+
known.set(key, is);
|
|
1060
|
+
return is ? key : null;
|
|
1061
|
+
};
|
|
1062
|
+
const guardsUnder = (file, p, node, visited = /* @__PURE__ */ new Set()) => {
|
|
1063
|
+
const out = /* @__PURE__ */ new Set();
|
|
1064
|
+
visited.add(node);
|
|
1065
|
+
walk(node, (n) => {
|
|
1066
|
+
if (n.type !== "CallExpression" || n.callee?.type !== "Identifier") return;
|
|
1067
|
+
const name = n.callee.name;
|
|
1068
|
+
if (p.imports.has(name)) {
|
|
1069
|
+
const key = guardKey(file, p, name, 0);
|
|
1070
|
+
if (key) out.add(key);
|
|
1071
|
+
} else if (p.functions.has(name)) {
|
|
1072
|
+
const body = p.functions.get(name);
|
|
1073
|
+
if (!visited.has(body)) for (const k of guardsUnder(file, p, body, visited)) out.add(k);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
return [...out];
|
|
1077
|
+
};
|
|
1078
|
+
return {
|
|
1079
|
+
/** Guards a page runs: in its file and the layouts above it (every function there, so a check inside a Suspense'd component counts). */
|
|
1080
|
+
page(files) {
|
|
1081
|
+
const out = /* @__PURE__ */ new Set();
|
|
1082
|
+
for (const file of files) {
|
|
1083
|
+
const p = parse2(file);
|
|
1084
|
+
if (p) for (const k of guardsUnder(file, p, p.program)) out.add(k);
|
|
1085
|
+
}
|
|
1086
|
+
return [...out].sort();
|
|
1087
|
+
},
|
|
1088
|
+
/** Guards a Server Action runs: in its own body. */
|
|
1089
|
+
action(file, exportName) {
|
|
1090
|
+
const p = parse2(file);
|
|
1091
|
+
const local = p?.exports.get(exportName);
|
|
1092
|
+
const body = local ? p.functions.get(local) : void 0;
|
|
1093
|
+
return p && body ? guardsUnder(file, p, body).sort() : [];
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/pages.ts
|
|
1099
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1100
|
+
import { join as join4 } from "path";
|
|
1101
|
+
var read = (root, file) => {
|
|
1102
|
+
try {
|
|
1103
|
+
return readFileSync4(join4(root, file), "utf8");
|
|
1104
|
+
} catch {
|
|
1105
|
+
return void 0;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
function defaultExportFunction(program) {
|
|
1109
|
+
const locals = /* @__PURE__ */ new Map();
|
|
1110
|
+
for (const stmt of program.body) {
|
|
1111
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) locals.set(stmt.id.name, stmt);
|
|
1112
|
+
if (stmt.type === "VariableDeclaration") {
|
|
1113
|
+
for (const d of stmt.declarations) if (d.id?.type === "Identifier" && FUNCTION_TYPES.has(unwrapTs(d.init)?.type ?? "")) locals.set(d.id.name, unwrapTs(d.init));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
for (const stmt of program.body) {
|
|
1117
|
+
if (stmt.type !== "ExportDefaultDeclaration") continue;
|
|
1118
|
+
const d = unwrapTs(stmt.declaration);
|
|
1119
|
+
if (d && FUNCTION_TYPES.has(d.type)) return d;
|
|
1120
|
+
if (d?.type === "Identifier") return locals.get(d.name);
|
|
1121
|
+
}
|
|
1122
|
+
return void 0;
|
|
1123
|
+
}
|
|
1124
|
+
var AWAITED = (n) => {
|
|
1125
|
+
const u = unwrapTs(n);
|
|
1126
|
+
if (u?.type === "AwaitExpression") return unwrapTs(u.argument);
|
|
1127
|
+
if (u?.type === "CallExpression" && u.callee?.type === "Identifier" && u.callee.name === "use") return unwrapTs(u.arguments?.[0]);
|
|
1128
|
+
return u;
|
|
1129
|
+
};
|
|
1130
|
+
var patternKeys = (p) => p.type === "ObjectPattern" ? p.properties.filter((x) => x.type === "ObjectProperty").map((x) => keyName(x.key)).filter((k) => Boolean(k)) : [];
|
|
1131
|
+
function pageSearchParams(program) {
|
|
1132
|
+
const names = /* @__PURE__ */ new Set();
|
|
1133
|
+
const sources = /* @__PURE__ */ new Set();
|
|
1134
|
+
const propsNames = /* @__PURE__ */ new Set();
|
|
1135
|
+
walk(program, (fn) => {
|
|
1136
|
+
if (!FUNCTION_TYPES.has(fn.type)) return;
|
|
1137
|
+
const param = fn.params?.[0];
|
|
1138
|
+
if (param?.type === "ObjectPattern") {
|
|
1139
|
+
for (const prop of param.properties) {
|
|
1140
|
+
if (prop.type !== "ObjectProperty" || keyName(prop.key) !== "searchParams") continue;
|
|
1141
|
+
const value = prop.value.type === "AssignmentPattern" ? prop.value.left : prop.value;
|
|
1142
|
+
if (value.type === "Identifier") sources.add(value.name);
|
|
1143
|
+
for (const k of patternKeys(value)) names.add(k);
|
|
1144
|
+
}
|
|
1145
|
+
} else if (param?.type === "Identifier" && fn === defaultExportFunction(program)) propsNames.add(param.name);
|
|
1146
|
+
});
|
|
1147
|
+
const isSource = (n) => {
|
|
1148
|
+
const a = AWAITED(n);
|
|
1149
|
+
if (!a) return false;
|
|
1150
|
+
if (a.type === "Identifier") return sources.has(a.name);
|
|
1151
|
+
return a.type === "MemberExpression" && unwrapTs(a.object)?.type === "Identifier" && propsNames.has(unwrapTs(a.object).name) && keyName(a.property) === "searchParams";
|
|
1152
|
+
};
|
|
1153
|
+
const promiseAll = (n) => {
|
|
1154
|
+
const a = AWAITED(n);
|
|
1155
|
+
const callee = a?.type === "CallExpression" ? a.callee : void 0;
|
|
1156
|
+
return callee?.type === "MemberExpression" && keyName(callee.object) === "Promise" && keyName(callee.property) === "all" && a.arguments?.[0]?.type === "ArrayExpression" ? a.arguments[0].elements : void 0;
|
|
1157
|
+
};
|
|
1158
|
+
walk(program, (n) => {
|
|
1159
|
+
if (n.type !== "VariableDeclarator") return;
|
|
1160
|
+
if (isSource(n.init)) {
|
|
1161
|
+
if (n.id.type === "Identifier") sources.add(n.id.name);
|
|
1162
|
+
for (const k of patternKeys(n.id)) names.add(k);
|
|
1163
|
+
}
|
|
1164
|
+
const all = promiseAll(n.init);
|
|
1165
|
+
if (all && n.id.type === "ArrayPattern") {
|
|
1166
|
+
n.id.elements.forEach((el, i) => {
|
|
1167
|
+
if (!el || !isSource(all[i])) return;
|
|
1168
|
+
if (el.type === "Identifier") sources.add(el.name);
|
|
1169
|
+
for (const k of patternKeys(el)) names.add(k);
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
walk(program, (n) => {
|
|
1174
|
+
if (n.type !== "MemberExpression" || !isSource(n.object)) return;
|
|
1175
|
+
const k = n.computed ? stringValue(n.property) : keyName(n.property);
|
|
1176
|
+
if (k) names.add(k);
|
|
1177
|
+
});
|
|
1178
|
+
return [...names];
|
|
1179
|
+
}
|
|
1180
|
+
function hookSearchParams(program) {
|
|
1181
|
+
const names = /* @__PURE__ */ new Set();
|
|
1182
|
+
const holders = /* @__PURE__ */ new Set();
|
|
1183
|
+
const isHook = (n) => {
|
|
1184
|
+
const u = unwrapTs(n);
|
|
1185
|
+
return u?.type === "CallExpression" && u.callee?.type === "Identifier" && u.callee.name === "useSearchParams";
|
|
1186
|
+
};
|
|
1187
|
+
walk(program, (n) => {
|
|
1188
|
+
if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && isHook(n.init)) holders.add(n.id.name);
|
|
1189
|
+
});
|
|
1190
|
+
walk(program, (n) => {
|
|
1191
|
+
if (n.type !== "CallExpression" || n.callee?.type !== "MemberExpression") return;
|
|
1192
|
+
const method = keyName(n.callee.property);
|
|
1193
|
+
if (method !== "get" && method !== "getAll" && method !== "has") return;
|
|
1194
|
+
const target = unwrapTs(n.callee.object);
|
|
1195
|
+
const fromHook = isHook(target) || target?.type === "Identifier" && holders.has(target.name);
|
|
1196
|
+
const name = stringValue(n.arguments?.[0]);
|
|
1197
|
+
if (fromHook && name) names.add(name);
|
|
1198
|
+
});
|
|
1199
|
+
return [...names];
|
|
1200
|
+
}
|
|
1201
|
+
function staticMetadata(program) {
|
|
1202
|
+
for (const stmt of program.body) {
|
|
1203
|
+
if (stmt.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
|
|
1204
|
+
for (const d of stmt.declaration.declarations) {
|
|
1205
|
+
const init = unwrapTs(d.init);
|
|
1206
|
+
if (d.id?.type !== "Identifier" || d.id.name !== "metadata" || init?.type !== "ObjectExpression") continue;
|
|
1207
|
+
const out = {};
|
|
1208
|
+
for (const prop of init.properties) {
|
|
1209
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1210
|
+
const key = keyName(prop.key);
|
|
1211
|
+
const value = unwrapTs(prop.value);
|
|
1212
|
+
if (key === "description") {
|
|
1213
|
+
const s = stringValue(value);
|
|
1214
|
+
if (s) out.description = s;
|
|
1215
|
+
}
|
|
1216
|
+
if (key === "title") {
|
|
1217
|
+
let s = stringValue(value);
|
|
1218
|
+
if (!s && value?.type === "ObjectExpression") {
|
|
1219
|
+
const byKey = new Map(value.properties.filter((p) => p.type === "ObjectProperty").map((p) => [keyName(p.key), stringValue(unwrapTs(p.value))]));
|
|
1220
|
+
s = byKey.get("absolute") ?? byKey.get("default");
|
|
1221
|
+
}
|
|
1222
|
+
if (s) out.title = s;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return out;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return {};
|
|
1229
|
+
}
|
|
1230
|
+
function readsData(program, file, load, actionFiles) {
|
|
1231
|
+
const imported = /* @__PURE__ */ new Set();
|
|
1232
|
+
for (const stmt of program.body) {
|
|
1233
|
+
if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
|
|
1234
|
+
const source = stmt.source.value;
|
|
1235
|
+
if (/^next(\/|$)/.test(source) || source === "react") continue;
|
|
1236
|
+
if (actionFiles.has(load(source, file)?.file ?? "")) continue;
|
|
1237
|
+
for (const s of stmt.specifiers) if (!/^[A-Z]/.test(s.local.name)) imported.add(s.local.name);
|
|
1238
|
+
}
|
|
1239
|
+
let reads = false;
|
|
1240
|
+
walk(program, (n, parents) => {
|
|
1241
|
+
if (reads) return;
|
|
1242
|
+
if (n.type === "AwaitExpression") reads = true;
|
|
1243
|
+
if (n.type !== "Identifier" || !imported.has(n.name)) return;
|
|
1244
|
+
const parent = parents[parents.length - 1];
|
|
1245
|
+
if (!parent || parent.type.startsWith("Import")) return;
|
|
1246
|
+
if (parent.type === "CallExpression" && parent.callee === n) return;
|
|
1247
|
+
if (parent.type === "MemberExpression" && parent.property === n && !parent.computed || parent.type === "ObjectProperty" && parent.key === n && !parent.computed) return;
|
|
1248
|
+
reads = true;
|
|
1249
|
+
});
|
|
1250
|
+
return reads;
|
|
1251
|
+
}
|
|
1252
|
+
var directive = (program) => program.directives?.[0]?.value?.value;
|
|
1253
|
+
function pageToolName(path, snake) {
|
|
1254
|
+
const words = path.split("/").filter(Boolean).map((seg) => seg.replace(/^:/, "").replace(/\*\??$/, ""));
|
|
1255
|
+
return `view_${snake(words.join("_")) || "home"}`;
|
|
1256
|
+
}
|
|
1257
|
+
function discoverPages(root, entries, graph, load, actionFiles, formFiles, taken, snake) {
|
|
1258
|
+
const hookCache = /* @__PURE__ */ new Map();
|
|
1259
|
+
const hookNames = (file) => {
|
|
1260
|
+
let names = hookCache.get(file);
|
|
1261
|
+
if (!names) {
|
|
1262
|
+
const source = read(root, file);
|
|
1263
|
+
const program = source?.includes("useSearchParams") ? parseModule(source, file) : null;
|
|
1264
|
+
names = program ? hookSearchParams(program) : [];
|
|
1265
|
+
hookCache.set(file, names);
|
|
1266
|
+
}
|
|
1267
|
+
return names;
|
|
1268
|
+
};
|
|
1269
|
+
const used = new Set(taken);
|
|
1270
|
+
const out = [];
|
|
1271
|
+
for (const entry of [...entries].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
1272
|
+
const source = read(root, entry.file);
|
|
1273
|
+
const program = source ? parseModule(source, entry.file) : null;
|
|
1274
|
+
const reachable = graph.reachable([entry.file]);
|
|
1275
|
+
const search = new Set(program ? pageSearchParams(program) : []);
|
|
1276
|
+
for (const file of reachable) for (const n of hookNames(file)) search.add(n);
|
|
1277
|
+
const meta = program ? staticMetadata(program) : {};
|
|
1278
|
+
const hasForm = [...reachable].some((f) => formFiles.has(f));
|
|
1279
|
+
const formOnly = hasForm && Boolean(program) && ![...reachable].some((f) => {
|
|
1280
|
+
if (!/\.[jt]sx$/.test(f) && f !== entry.file) return false;
|
|
1281
|
+
const prog = f === entry.file ? program : parseModule(read(root, f) ?? "", f);
|
|
1282
|
+
return prog ? directive(prog) === void 0 && readsData(prog, f, load, actionFiles) : false;
|
|
1283
|
+
});
|
|
1284
|
+
let name = pageToolName(entry.path, snake);
|
|
1285
|
+
for (let i = 2; used.has(name); i++) name = `${pageToolName(entry.path, snake)}_${i}`;
|
|
1286
|
+
used.add(name);
|
|
1287
|
+
out.push({
|
|
1288
|
+
file: entry.file,
|
|
1289
|
+
path: entry.path,
|
|
1290
|
+
params: entry.params,
|
|
1291
|
+
name,
|
|
1292
|
+
search: [...search].filter((s) => !entry.params.includes(s)).sort(),
|
|
1293
|
+
...meta,
|
|
1294
|
+
...formOnly ? { formOnly: true } : {}
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
return out;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/resolve.ts
|
|
1301
|
+
import { existsSync, readFileSync as readFileSync5, statSync } from "fs";
|
|
1302
|
+
import { dirname, join as join5, relative, resolve, sep } from "path";
|
|
1303
|
+
var EXTENSIONS = [".ts", ".tsx", ".mts", ".js", ".jsx", ".mjs"];
|
|
1304
|
+
function readTsconfig(root) {
|
|
1305
|
+
try {
|
|
1306
|
+
const raw = readFileSync5(join5(root, "tsconfig.json"), "utf8").replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_m, str2) => str2 ?? "").replace(/,(\s*[}\]])/g, "$1");
|
|
1307
|
+
const options = JSON.parse(raw).compilerOptions ?? {};
|
|
1308
|
+
return { ...options.baseUrl ? { baseUrl: options.baseUrl } : {}, ...options.paths ? { paths: options.paths } : {} };
|
|
1309
|
+
} catch {
|
|
1310
|
+
return {};
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function asFile(path) {
|
|
1314
|
+
const candidates = [path, ...EXTENSIONS.map((e) => path + e), ...EXTENSIONS.map((e) => join5(path, `index${e}`))];
|
|
1315
|
+
if (/\.[mc]?jsx?$/.test(path)) candidates.push(...EXTENSIONS.map((e) => path.replace(/\.[mc]?jsx?$/, e)));
|
|
1316
|
+
return candidates.find((c) => existsSync(c) && statSync(c).isFile());
|
|
1317
|
+
}
|
|
1318
|
+
function moduleLoader(root) {
|
|
1319
|
+
const { baseUrl, paths } = readTsconfig(root);
|
|
1320
|
+
const base = join5(root, baseUrl ?? ".");
|
|
1321
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1322
|
+
const candidates = (specifier2, fromFile) => {
|
|
1323
|
+
if (specifier2.startsWith(".")) return [resolve(root, dirname(fromFile), specifier2)];
|
|
1324
|
+
for (const [pattern, targets] of Object.entries(paths ?? {})) {
|
|
1325
|
+
const star = pattern.indexOf("*");
|
|
1326
|
+
const prefix = star < 0 ? pattern : pattern.slice(0, star);
|
|
1327
|
+
const suffix = star < 0 ? "" : pattern.slice(star + 1);
|
|
1328
|
+
const matches = star < 0 ? specifier2 === pattern : specifier2.startsWith(prefix) && specifier2.endsWith(suffix) && specifier2.length >= prefix.length + suffix.length;
|
|
1329
|
+
if (!matches) continue;
|
|
1330
|
+
const middle = star < 0 ? "" : specifier2.slice(prefix.length, specifier2.length - suffix.length);
|
|
1331
|
+
return targets.map((t) => join5(base, t.replace("*", middle)));
|
|
1332
|
+
}
|
|
1333
|
+
return baseUrl ? [join5(base, specifier2)] : [];
|
|
1334
|
+
};
|
|
1335
|
+
return (specifier2, fromFile) => {
|
|
1336
|
+
const key = `${fromFile}\0${specifier2}`;
|
|
1337
|
+
if (cache.has(key)) return cache.get(key);
|
|
1338
|
+
let found;
|
|
1339
|
+
for (const candidate of candidates(specifier2, fromFile)) {
|
|
1340
|
+
const path = asFile(candidate);
|
|
1341
|
+
const rel = path && relative(root, path);
|
|
1342
|
+
if (!path || !rel || rel.startsWith("..") || rel.split(sep).includes("node_modules")) continue;
|
|
1343
|
+
found = { file: rel.split(sep).join("/"), source: readFileSync5(path, "utf8") };
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
cache.set(key, found);
|
|
1347
|
+
return found;
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/parse.ts
|
|
1352
|
+
var SOURCE_CAP = 6e3;
|
|
1353
|
+
var PACKAGES = /* @__PURE__ */ new Set(["@exposify/next", "@exposify/core"]);
|
|
1354
|
+
var METHOD_SET = new Set(HTTP_METHODS);
|
|
1355
|
+
var cap = (s) => s === void 0 ? void 0 : s.length > SOURCE_CAP ? `${s.slice(0, SOURCE_CAP)}
|
|
1356
|
+
/* \u2026 truncated */` : s;
|
|
1357
|
+
function ourImports(program) {
|
|
1358
|
+
const locals = /* @__PURE__ */ new Map();
|
|
1359
|
+
for (const stmt of program.body) {
|
|
1360
|
+
if (stmt.type !== "ImportDeclaration" || !PACKAGES.has(stmt.source.value)) continue;
|
|
1361
|
+
for (const spec of stmt.specifiers) {
|
|
1362
|
+
if (spec.type === "ImportSpecifier") locals.set(spec.local.name, keyName(spec.imported) ?? spec.local.name);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return locals;
|
|
1366
|
+
}
|
|
1367
|
+
function writtenInput(n) {
|
|
1368
|
+
n = unwrapTs(n);
|
|
1369
|
+
if (n?.type !== "ObjectExpression") return "opaque";
|
|
1370
|
+
const out = {};
|
|
1371
|
+
for (const prop of n.properties) {
|
|
1372
|
+
const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
|
|
1373
|
+
if (!key) continue;
|
|
1374
|
+
let values;
|
|
1375
|
+
walk(prop.value, (c) => {
|
|
1376
|
+
if (values || c.type !== "CallExpression" || keyName(c.callee?.property) !== "enum") return;
|
|
1377
|
+
const list = unwrapTs(c.arguments[0]);
|
|
1378
|
+
const items = list?.type === "ArrayExpression" ? list.elements.map((e) => stringValue(e)) : [];
|
|
1379
|
+
if (items.length > 0 && items.every((v) => v !== void 0)) values = items;
|
|
1380
|
+
});
|
|
1381
|
+
out[key] = values ? { enum: values } : {};
|
|
1382
|
+
}
|
|
1383
|
+
return out;
|
|
1384
|
+
}
|
|
1385
|
+
function markerOf(init, imports) {
|
|
1386
|
+
const call = unwrapTs(init);
|
|
1387
|
+
if (call?.type !== "CallExpression" || call.callee.type !== "Identifier") return { handler: call };
|
|
1388
|
+
const imported = imports.get(call.callee.name);
|
|
1389
|
+
if (imported !== "exposed" && imported !== "hidden" && imported !== "describe") return { handler: call };
|
|
1390
|
+
const handler = call.arguments[0];
|
|
1391
|
+
if (imported !== "describe") return { marker: { kind: imported }, handler };
|
|
1392
|
+
const meta = call.arguments[1];
|
|
1393
|
+
const marker = { kind: "described" };
|
|
1394
|
+
if (meta?.type === "ObjectExpression") {
|
|
1395
|
+
for (const prop of meta.properties) {
|
|
1396
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1397
|
+
const key = keyName(prop.key);
|
|
1398
|
+
const value = stringValue(prop.value);
|
|
1399
|
+
if (key === "name" && value) marker.name = value;
|
|
1400
|
+
if (key === "description" && value) marker.description = value;
|
|
1401
|
+
if (key === "input") marker.input = writtenInput(prop.value);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return { marker, handler };
|
|
1405
|
+
}
|
|
1406
|
+
function analyzeRouteModule(source, file) {
|
|
1407
|
+
const out = { methods: [], edge: false, mcpRoute: false, markers: {}, sources: {} };
|
|
1408
|
+
const program = parseModule(source, file);
|
|
1409
|
+
if (!program) return out;
|
|
1410
|
+
const imports = ourImports(program);
|
|
1411
|
+
out.mcpRoute = [...imports.values()].includes("aiReady");
|
|
1412
|
+
const found = /* @__PURE__ */ new Set();
|
|
1413
|
+
const record = (name, init, whole) => {
|
|
1414
|
+
if (!METHOD_SET.has(name)) return;
|
|
1415
|
+
found.add(name);
|
|
1416
|
+
const { marker, handler } = markerOf(init, imports);
|
|
1417
|
+
if (marker) out.markers[name] = marker;
|
|
1418
|
+
const src = cap(text(source, handler ?? whole));
|
|
1419
|
+
if (src) out.sources[name] = src;
|
|
1420
|
+
};
|
|
1421
|
+
for (const stmt of program.body) {
|
|
1422
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
1423
|
+
const decl = stmt.declaration;
|
|
1424
|
+
if (decl?.type === "FunctionDeclaration" && decl.id) record(decl.id.name, void 0, decl);
|
|
1425
|
+
if (decl?.type === "VariableDeclaration") {
|
|
1426
|
+
for (const d of decl.declarations) {
|
|
1427
|
+
if (d.id.type === "Identifier") {
|
|
1428
|
+
record(d.id.name, d.init, d.init);
|
|
1429
|
+
if (d.id.name === "runtime" && stringValue(d.init) === "edge") out.edge = true;
|
|
1430
|
+
} else if (d.id.type === "ObjectPattern") {
|
|
1431
|
+
for (const p of d.id.properties) if (p.type === "ObjectProperty" && p.value.type === "Identifier") record(p.value.name);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
1436
|
+
const exported = keyName(spec.exported);
|
|
1437
|
+
if (exported) record(exported);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
out.methods = HTTP_METHODS.filter((m) => found.has(m));
|
|
1441
|
+
return out;
|
|
1442
|
+
}
|
|
1443
|
+
function detectPagesMethods(source, file) {
|
|
1444
|
+
const program = parseModule(source, file);
|
|
1445
|
+
if (!program) return null;
|
|
1446
|
+
const usesNextConnect = program.body.some((s) => s.type === "ImportDeclaration" && s.source.value === "next-connect");
|
|
1447
|
+
if (usesNextConnect) {
|
|
1448
|
+
const methods2 = /* @__PURE__ */ new Set();
|
|
1449
|
+
walk(program, (n) => {
|
|
1450
|
+
if (n.type !== "CallExpression" || n.callee.type !== "MemberExpression") return;
|
|
1451
|
+
const name = keyName(n.callee.property)?.toUpperCase();
|
|
1452
|
+
if (name && METHOD_SET.has(name) && name !== "OPTIONS") methods2.add(name);
|
|
1453
|
+
});
|
|
1454
|
+
if (methods2.size > 0) return { methods: HTTP_METHODS.filter((m) => methods2.has(m)), via: "next-connect" };
|
|
1455
|
+
}
|
|
1456
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1457
|
+
walk(program, (n) => {
|
|
1458
|
+
if (n.type !== "VariableDeclarator") return;
|
|
1459
|
+
if (n.id.type === "ObjectPattern" && n.init?.type === "Identifier") {
|
|
1460
|
+
for (const p of n.id.properties) if (p.type === "ObjectProperty" && keyName(p.key) === "method" && p.value.type === "Identifier") aliases.add(p.value.name);
|
|
1461
|
+
}
|
|
1462
|
+
if (n.id.type === "Identifier" && isReqMethod(n.init, aliases)) aliases.add(n.id.name);
|
|
1463
|
+
});
|
|
1464
|
+
const methods = /* @__PURE__ */ new Set();
|
|
1465
|
+
let via = "if";
|
|
1466
|
+
const add = (value) => {
|
|
1467
|
+
const m = value?.toUpperCase();
|
|
1468
|
+
if (m && METHOD_SET.has(m)) methods.add(m);
|
|
1469
|
+
};
|
|
1470
|
+
walk(program, (n) => {
|
|
1471
|
+
if (n.type === "BinaryExpression" && ["===", "==", "!==", "!="].includes(n.operator)) {
|
|
1472
|
+
if (isReqMethod(n.left, aliases)) add(stringValue(n.right));
|
|
1473
|
+
else if (isReqMethod(n.right, aliases)) add(stringValue(n.left));
|
|
1474
|
+
} else if (n.type === "SwitchStatement" && isReqMethod(n.discriminant, aliases)) {
|
|
1475
|
+
for (const c of n.cases) add(stringValue(c.test));
|
|
1476
|
+
if (methods.size > 0) via = "switch";
|
|
1477
|
+
} else if (n.type === "CallExpression" && n.callee.type === "MemberExpression" && keyName(n.callee.property) === "includes" && n.callee.object.type === "ArrayExpression" && isReqMethod(n.arguments[0], aliases)) {
|
|
1478
|
+
for (const el of n.callee.object.elements) add(stringValue(el));
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
methods.delete("OPTIONS");
|
|
1482
|
+
return methods.size > 0 ? { methods: HTTP_METHODS.filter((m) => methods.has(m)), via } : null;
|
|
1483
|
+
}
|
|
1484
|
+
function isReqMethod(n, aliases) {
|
|
1485
|
+
n = unwrapTs(n);
|
|
1486
|
+
if (!n) return false;
|
|
1487
|
+
if (n.type === "Identifier") return aliases.has(n.name);
|
|
1488
|
+
if (n.type === "OptionalMemberExpression" || n.type === "MemberExpression") return keyName(n.property) === "method" && !n.computed;
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
function defaultExportSource(source, file) {
|
|
1492
|
+
const program = parseModule(source, file);
|
|
1493
|
+
const stmt = program?.body?.find((s) => s.type === "ExportDefaultDeclaration");
|
|
1494
|
+
if (!stmt) return void 0;
|
|
1495
|
+
let decl = stmt.declaration;
|
|
1496
|
+
if (decl.type === "Identifier") {
|
|
1497
|
+
for (const s of program.body) {
|
|
1498
|
+
const d = s.type === "ExportNamedDeclaration" ? s.declaration : s;
|
|
1499
|
+
if (d?.type === "FunctionDeclaration" && d.id?.name === decl.name) return cap(text(source, d));
|
|
1500
|
+
if (d?.type === "VariableDeclaration") {
|
|
1501
|
+
const v = d.declarations.find((x) => x.id.type === "Identifier" && x.id.name === decl.name);
|
|
1502
|
+
if (v) return cap(text(source, v.init));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return cap(text(source, decl));
|
|
1507
|
+
}
|
|
1508
|
+
var FORM_READS2 = /* @__PURE__ */ new Set(["get", "getAll", "has"]);
|
|
1509
|
+
var PREV_STATE_NAME = /^_*(prev|previous|state|prevState|previousState|initialState|currentState)?$/i;
|
|
1510
|
+
var SEARCH_DEPTH2 = 3;
|
|
1511
|
+
function isActionModule(program) {
|
|
1512
|
+
return program.directives?.[0]?.value?.value === "use server";
|
|
1513
|
+
}
|
|
1514
|
+
function localFunctions(program, imports) {
|
|
1515
|
+
const out = /* @__PURE__ */ new Map();
|
|
1516
|
+
for (const stmt of program.body) {
|
|
1517
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
1518
|
+
if (decl?.type === "FunctionDeclaration" && decl.id) out.set(decl.id.name, { fn: decl });
|
|
1519
|
+
if (decl?.type === "VariableDeclaration") {
|
|
1520
|
+
for (const d of decl.declarations) {
|
|
1521
|
+
if (d.id.type !== "Identifier") continue;
|
|
1522
|
+
const { handler } = markerOf(d.init, imports);
|
|
1523
|
+
const fn = unwrapTs(handler);
|
|
1524
|
+
if (fn && FUNCTION_TYPES.has(fn.type)) out.set(d.id.name, { fn, init: d.init });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return out;
|
|
1529
|
+
}
|
|
1530
|
+
var typedFormData = (p) => {
|
|
1531
|
+
const id = p.type === "AssignmentPattern" ? p.left : p;
|
|
1532
|
+
const t = id?.typeAnnotation?.typeAnnotation;
|
|
1533
|
+
return t?.type === "TSTypeReference" && t.typeName?.type === "Identifier" && t.typeName.name === "FormData";
|
|
1534
|
+
};
|
|
1535
|
+
function references(body, name) {
|
|
1536
|
+
let count = 0;
|
|
1537
|
+
walk(body, (n) => {
|
|
1538
|
+
if (n.type === "Identifier" && n.name === name) count++;
|
|
1539
|
+
if ((n.type === "MemberExpression" || n.type === "OptionalMemberExpression") && !n.computed && n.property.type === "Identifier" && n.property.name === name) count--;
|
|
1540
|
+
if (n.type === "ObjectProperty" && !n.computed && !n.shorthand && n.key.type === "Identifier" && n.key.name === name) count--;
|
|
1541
|
+
});
|
|
1542
|
+
return count;
|
|
1543
|
+
}
|
|
1544
|
+
function readsForm(body, name) {
|
|
1545
|
+
let found = false;
|
|
1546
|
+
walk(body, (n) => {
|
|
1547
|
+
if (found || n.type !== "CallExpression") return;
|
|
1548
|
+
const callee = n.callee;
|
|
1549
|
+
if (callee.type !== "MemberExpression" || callee.object.type !== "Identifier" || callee.object.name !== name) return;
|
|
1550
|
+
const method = keyName(callee.property);
|
|
1551
|
+
if (method && FORM_READS2.has(method) && stringValue(n.arguments[0]) !== void 0) found = true;
|
|
1552
|
+
});
|
|
1553
|
+
return found;
|
|
1554
|
+
}
|
|
1555
|
+
function actionParams(fn, shape) {
|
|
1556
|
+
const raw = fn.params.filter((p) => p.type !== "RestElement");
|
|
1557
|
+
if (shape) {
|
|
1558
|
+
const formIndex2 = shape.bound + (shape.prev ? 1 : 0);
|
|
1559
|
+
return raw.map((p, i) => {
|
|
1560
|
+
const name = paramName(p) ?? `arg${i}`;
|
|
1561
|
+
if (i < shape.bound) return { name, role: "input" };
|
|
1562
|
+
if (i === formIndex2) return { name, role: "formData" };
|
|
1563
|
+
if (shape.prev && i === shape.bound) return { name, role: "prev" };
|
|
1564
|
+
return { name, role: "input" };
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
let formIndex = raw.findIndex(typedFormData);
|
|
1568
|
+
if (formIndex < 0) formIndex = raw.findIndex((p) => {
|
|
1569
|
+
const name = paramName(p);
|
|
1570
|
+
return name !== void 0 && readsForm(fn.body, name);
|
|
1571
|
+
});
|
|
1572
|
+
return raw.map((p, i) => {
|
|
1573
|
+
const name = paramName(p) ?? `arg${i}`;
|
|
1574
|
+
if (i === formIndex) return { name, role: "formData" };
|
|
1575
|
+
if (p.type === "ObjectPattern" || p.type === "AssignmentPattern" && p.left.type === "ObjectPattern") return { name: "input", role: "object" };
|
|
1576
|
+
if (formIndex === 1 && i === 0 && (PREV_STATE_NAME.test(name) || references(fn.body, name) === 0)) return { name, role: "prev" };
|
|
1577
|
+
return { name, role: "input" };
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
function setsCookies(fn, program, functions) {
|
|
1581
|
+
const cookiesLocal = program.body.filter((s) => s.type === "ImportDeclaration" && s.source.value === "next/headers").flatMap((s) => s.specifiers).find((s) => s.type === "ImportSpecifier" && keyName(s.imported) === "cookies")?.local?.name;
|
|
1582
|
+
if (!cookiesLocal) return false;
|
|
1583
|
+
const isCookiesCall = (n) => {
|
|
1584
|
+
n = unwrapTs(n);
|
|
1585
|
+
if (n?.type === "AwaitExpression") n = unwrapTs(n.argument);
|
|
1586
|
+
return n?.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === cookiesLocal;
|
|
1587
|
+
};
|
|
1588
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1589
|
+
const search = (f, depth) => {
|
|
1590
|
+
if (visited.has(f) || depth > SEARCH_DEPTH2) return false;
|
|
1591
|
+
visited.add(f);
|
|
1592
|
+
const jars = /* @__PURE__ */ new Set();
|
|
1593
|
+
walk(f.body, (n) => {
|
|
1594
|
+
if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && isCookiesCall(n.init)) jars.add(n.id.name);
|
|
1595
|
+
});
|
|
1596
|
+
let found = false;
|
|
1597
|
+
const callees = [];
|
|
1598
|
+
walk(f.body, (n) => {
|
|
1599
|
+
if (found || n.type !== "CallExpression") return;
|
|
1600
|
+
const callee = n.callee;
|
|
1601
|
+
if (callee.type === "Identifier") callees.push(callee.name);
|
|
1602
|
+
if (callee.type !== "MemberExpression" || !["set", "delete"].includes(keyName(callee.property) ?? "")) return;
|
|
1603
|
+
const target = unwrapTs(callee.object);
|
|
1604
|
+
if (isCookiesCall(target) || target?.type === "Identifier" && jars.has(target.name)) found = true;
|
|
1605
|
+
});
|
|
1606
|
+
if (found) return true;
|
|
1607
|
+
return callees.some((name) => {
|
|
1608
|
+
const local = functions.get(name);
|
|
1609
|
+
return local !== void 0 && search(local.fn, depth + 1);
|
|
1610
|
+
});
|
|
1611
|
+
};
|
|
1612
|
+
return search(fn, 0);
|
|
1613
|
+
}
|
|
1614
|
+
function analyzeActionModule(source, file, load, shapes) {
|
|
1615
|
+
const program = parseModule(source, file);
|
|
1616
|
+
if (!program || !isActionModule(program)) return null;
|
|
1617
|
+
const imports = ourImports(program);
|
|
1618
|
+
const functions = localFunctions(program, imports);
|
|
1619
|
+
const topLevel = new Map([...functions].map(([name, f]) => [name, f.fn]));
|
|
1620
|
+
const out = [];
|
|
1621
|
+
const record = (name, at, local) => {
|
|
1622
|
+
const line = lineOf(at);
|
|
1623
|
+
if (!local) {
|
|
1624
|
+
out.push({ name, line, params: [], setsCookies: false, resolved: false, fields: [], readsAll: false, unresolved: [`line ${line}: it's defined in another module, so its code wasn't read`] });
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const { marker } = markerOf(local.init, imports);
|
|
1628
|
+
const src = cap(text(source, local.fn));
|
|
1629
|
+
const params = actionParams(local.fn, shapes?.get(name));
|
|
1630
|
+
out.push({
|
|
1631
|
+
name,
|
|
1632
|
+
line,
|
|
1633
|
+
params,
|
|
1634
|
+
...analyzeFields(local.fn, params, { program, source, file, topLevel, ...load ? { load } : {} }),
|
|
1635
|
+
setsCookies: setsCookies(local.fn, program, functions),
|
|
1636
|
+
resolved: true,
|
|
1637
|
+
...marker ? { marker } : {},
|
|
1638
|
+
...src ? { source: src } : {}
|
|
1639
|
+
});
|
|
1640
|
+
};
|
|
1641
|
+
for (const stmt of program.body) {
|
|
1642
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
1643
|
+
const decl2 = stmt.declaration;
|
|
1644
|
+
if (FUNCTION_TYPES.has(decl2.type)) record("default", stmt, { fn: decl2 });
|
|
1645
|
+
else if (decl2.type === "Identifier") record("default", stmt, functions.get(decl2.name));
|
|
1646
|
+
else {
|
|
1647
|
+
const fn = unwrapTs(markerOf(decl2, imports).handler);
|
|
1648
|
+
record("default", stmt, fn && FUNCTION_TYPES.has(fn.type) ? { fn, init: decl2 } : void 0);
|
|
1649
|
+
}
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
1653
|
+
if (stmt.exportKind === "type") continue;
|
|
1654
|
+
const decl = stmt.declaration;
|
|
1655
|
+
if (decl?.type === "FunctionDeclaration" && decl.id) record(decl.id.name, decl, functions.get(decl.id.name));
|
|
1656
|
+
if (decl?.type === "VariableDeclaration") {
|
|
1657
|
+
for (const d of decl.declarations) if (d.id.type === "Identifier") record(d.id.name, d, functions.get(d.id.name));
|
|
1658
|
+
}
|
|
1659
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
1660
|
+
if (spec.type !== "ExportSpecifier" || spec.exportKind === "type") continue;
|
|
1661
|
+
const exported = keyName(spec.exported);
|
|
1662
|
+
if (!exported) continue;
|
|
1663
|
+
record(exported, spec, stmt.source ? void 0 : functions.get(spec.local.name));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return out;
|
|
1667
|
+
}
|
|
1668
|
+
function findInlineActions(source, file) {
|
|
1669
|
+
const program = parseModule(source, file);
|
|
1670
|
+
if (!program) return [];
|
|
1671
|
+
const out = [];
|
|
1672
|
+
const named2 = /* @__PURE__ */ new Map();
|
|
1673
|
+
walk(program, (n) => {
|
|
1674
|
+
if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && n.init) named2.set(unwrapTs(n.init), n.id.name);
|
|
1675
|
+
if (!FUNCTION_TYPES.has(n.type) || n.body?.type !== "BlockStatement") return;
|
|
1676
|
+
if (!n.body.directives.some((d) => d.value?.value === "use server")) return;
|
|
1677
|
+
if (isActionModule(program) && program.body.some((s) => s === n || s.declaration === n || s.declaration?.declarations?.some((d) => unwrapTs(d.init) === n))) return;
|
|
1678
|
+
const name = n.id?.name ?? named2.get(n);
|
|
1679
|
+
out.push({ ...name ? { name } : {}, line: n.loc?.start.line ?? 1 });
|
|
1680
|
+
});
|
|
1681
|
+
return out;
|
|
1682
|
+
}
|
|
1683
|
+
function sourceAtColumn0(source, n) {
|
|
1684
|
+
const raw = text(source, n) ?? "";
|
|
1685
|
+
const column = n.loc?.start.column ?? 0;
|
|
1686
|
+
return raw.split("\n").map((line, i) => i === 0 ? line : line.slice(Math.min(column, line.length - line.trimStart().length))).join("\n");
|
|
1687
|
+
}
|
|
1688
|
+
function findAiReadyCall(source, file) {
|
|
1689
|
+
const program = parseModule(source, file);
|
|
1690
|
+
if (!program) return null;
|
|
1691
|
+
const local = [...ourImports(program)].find(([, imported]) => imported === "aiReady")?.[0];
|
|
1692
|
+
if (!local) return null;
|
|
1693
|
+
let found = null;
|
|
1694
|
+
walk(program, (n) => {
|
|
1695
|
+
if (found || n.type !== "CallExpression" || n.callee.type !== "Identifier" || n.callee.name !== local) return;
|
|
1696
|
+
const call = { line: n.loc?.start.line ?? 1, otherKeys: [] };
|
|
1697
|
+
const options = unwrapTs(n.arguments[1]);
|
|
1698
|
+
if (options && options.type !== "ObjectExpression") call.otherKeys.push("(options variable)");
|
|
1699
|
+
for (const prop of options?.type === "ObjectExpression" ? options.properties : []) {
|
|
1700
|
+
const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
|
|
1701
|
+
if (!key) {
|
|
1702
|
+
call.otherKeys.push("(spread)");
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
const value = unwrapTs(prop.value);
|
|
1706
|
+
if (key === "expose" && stringValue(value) === "*") call.expose = "*";
|
|
1707
|
+
else if ((key === "expose" || key === "exclude") && value?.type === "ArrayExpression") {
|
|
1708
|
+
call[key] = value.elements.map((e) => stringValue(e)).filter((s) => s !== void 0);
|
|
1709
|
+
} else if (key === "startupLog" && value?.type === "BooleanLiteral") call.startupLog = value.value;
|
|
1710
|
+
else if (key === "actions") {
|
|
1711
|
+
if (value?.type !== "ObjectExpression") call.actions = "opaque";
|
|
1712
|
+
else {
|
|
1713
|
+
const actions = {};
|
|
1714
|
+
for (const entry of value.properties) {
|
|
1715
|
+
const name = entry.type === "ObjectProperty" ? keyName(entry.key) : void 0;
|
|
1716
|
+
if (!name) continue;
|
|
1717
|
+
const conf = { source: sourceAtColumn0(source, entry) };
|
|
1718
|
+
const body = unwrapTs(entry.value);
|
|
1719
|
+
for (const p of body?.type === "ObjectExpression" ? body.properties : []) {
|
|
1720
|
+
const k = p.type === "ObjectProperty" ? keyName(p.key) : void 0;
|
|
1721
|
+
if (k === "name" && stringValue(p.value)) conf.name = stringValue(p.value);
|
|
1722
|
+
if (k === "description" && stringValue(p.value)) conf.description = stringValue(p.value);
|
|
1723
|
+
if (k === "input") conf.input = writtenInput(p.value);
|
|
1724
|
+
}
|
|
1725
|
+
actions[name] = conf;
|
|
1726
|
+
}
|
|
1727
|
+
call.actions = actions;
|
|
1728
|
+
}
|
|
1729
|
+
} else call.otherKeys.push(key);
|
|
1730
|
+
}
|
|
1731
|
+
found = call;
|
|
1732
|
+
});
|
|
1733
|
+
return found;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/codegen.ts
|
|
1737
|
+
var DEFAULT_DOCS = ["README.md", "docs/**/*.md"];
|
|
1738
|
+
var actionRoute = (mcpPath, key) => `POST ${(mcpPath ?? "/mcp").replace(/\/$/, "")}/actions/${key}`;
|
|
1739
|
+
function actionSignature(route, source) {
|
|
1740
|
+
const [method, path] = route.split(" ");
|
|
1741
|
+
return createHash("sha256").update(JSON.stringify([method, path, [], source ?? ""])).digest("hex").slice(0, 16);
|
|
1742
|
+
}
|
|
1743
|
+
var posix = (p) => p.split(sep2).join("/");
|
|
1744
|
+
function firstDir(root, names) {
|
|
1745
|
+
return names.find((n) => existsSync2(join6(root, n)));
|
|
1746
|
+
}
|
|
1747
|
+
function filesUnder(root, dir) {
|
|
1748
|
+
const out = [];
|
|
1749
|
+
const visit = (rel) => {
|
|
1750
|
+
let entries;
|
|
1751
|
+
try {
|
|
1752
|
+
entries = readdirSync(join6(root, rel), { withFileTypes: true });
|
|
1753
|
+
} catch {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
for (const e of entries) {
|
|
1757
|
+
if (e.name === "node_modules") continue;
|
|
1758
|
+
const child = `${rel}/${e.name}`;
|
|
1759
|
+
if (e.isDirectory()) visit(child);
|
|
1760
|
+
else out.push(child);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
visit(dir);
|
|
1764
|
+
return out.sort();
|
|
1765
|
+
}
|
|
1766
|
+
function discover(root, opts = {}) {
|
|
1767
|
+
const exts = opts.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;
|
|
1768
|
+
const extPattern = new RegExp(`\\.(${exts.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`);
|
|
1769
|
+
const appDir = firstDir(root, ["app", "src/app"]);
|
|
1770
|
+
const pagesDir = firstDir(root, ["pages", "src/pages"]);
|
|
1771
|
+
const out = { root, routes: [], actions: [], inlineActions: [], pages: [], ...appDir ? { appDir } : {}, ...pagesDir ? { pagesDir } : {} };
|
|
1772
|
+
if (appDir) {
|
|
1773
|
+
for (const file of filesUnder(root, appDir)) {
|
|
1774
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
1775
|
+
if (!/^route\./.test(name) || !extPattern.test(name)) continue;
|
|
1776
|
+
const dirSegs = file.slice(appDir.length + 1).split("/").slice(0, -1);
|
|
1777
|
+
const route = appRoute(dirSegs);
|
|
1778
|
+
if (!route) continue;
|
|
1779
|
+
const source = readFileSync6(join6(root, file), "utf8");
|
|
1780
|
+
const module = analyzeRouteModule(source, file);
|
|
1781
|
+
if (module.mcpRoute) {
|
|
1782
|
+
out.mcpRoute ??= { file, source };
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (module.methods.length === 0) continue;
|
|
1786
|
+
out.routes.push({ router: "app", file, ...route, module });
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (pagesDir) {
|
|
1790
|
+
for (const file of filesUnder(root, `${pagesDir}/api`)) {
|
|
1791
|
+
if (!extPattern.test(file) || /\.d\.ts$/.test(file)) continue;
|
|
1792
|
+
const segs = file.slice(pagesDir.length + 1).replace(extPattern, "").split("/");
|
|
1793
|
+
const route = pagesApiRoute(segs);
|
|
1794
|
+
if (!route) continue;
|
|
1795
|
+
const source = readFileSync6(join6(root, file), "utf8");
|
|
1796
|
+
const src = defaultExportSource(source, file);
|
|
1797
|
+
out.routes.push({ router: "pages", file, ...route, detected: detectPagesMethods(source, file), ...src ? { source: src } : {} });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
out.routes.sort((a, b) => a.path.localeCompare(b.path) || a.file.localeCompare(b.file));
|
|
1801
|
+
if (appDir && out.mcpRoute) {
|
|
1802
|
+
const segs = out.mcpRoute.file.slice(appDir.length + 1).split("/").slice(0, -1);
|
|
1803
|
+
const route = appRoute(segs);
|
|
1804
|
+
const catchAll = route ? /^(.*)\/:[A-Za-z0-9_]+\*\??$/.exec(route.path) : null;
|
|
1805
|
+
if (catchAll) out.mcpPath = catchAll[1] || "/";
|
|
1806
|
+
}
|
|
1807
|
+
discoverActions(root, out, exts);
|
|
1808
|
+
const found = findLogin(out);
|
|
1809
|
+
if (found) {
|
|
1810
|
+
out.login = found.login;
|
|
1811
|
+
out.loginOthers = found.others;
|
|
1812
|
+
}
|
|
1813
|
+
return out;
|
|
1814
|
+
}
|
|
1815
|
+
var SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs}";
|
|
1816
|
+
var NOT_SOURCE = ["**/node_modules/**", ".next/**", "out/**", "build/**", "dist/**", "coverage/**", "**/*.d.ts"];
|
|
1817
|
+
function snakeCase(name) {
|
|
1818
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
1819
|
+
}
|
|
1820
|
+
function discoverActions(root, out, exts) {
|
|
1821
|
+
const files = globSync(SOURCE_GLOB, { cwd: root, ignore: NOT_SOURCE }).sort();
|
|
1822
|
+
const load = moduleLoader(root);
|
|
1823
|
+
const modules = [];
|
|
1824
|
+
const components = /* @__PURE__ */ new Map();
|
|
1825
|
+
for (const file of files) {
|
|
1826
|
+
let source;
|
|
1827
|
+
try {
|
|
1828
|
+
source = readFileSync6(join6(root, file), "utf8");
|
|
1829
|
+
} catch {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (/\.[jt]sx$/.test(file)) components.set(file, source);
|
|
1833
|
+
if (!source.includes("use server")) continue;
|
|
1834
|
+
const actions = analyzeActionModule(source, file, load);
|
|
1835
|
+
if (actions) modules.push({ file, source, actions });
|
|
1836
|
+
for (const inline of findInlineActions(source, file)) out.inlineActions.push({ file, ...inline });
|
|
1837
|
+
}
|
|
1838
|
+
const exportsByFile = new Map(modules.map((m) => [m.file, new Set(m.actions.map((a) => a.name))]));
|
|
1839
|
+
const names = [...new Set(modules.flatMap((m) => m.actions.map((a) => a.name)))];
|
|
1840
|
+
const uses = [];
|
|
1841
|
+
const ctx = { load, modules: /* @__PURE__ */ new Map() };
|
|
1842
|
+
for (const [file, source] of components) {
|
|
1843
|
+
if (names.some((n) => source.includes(n === "default" ? "import" : n))) uses.push(...findFormUses(source, file, exportsByFile, load, ctx));
|
|
1844
|
+
}
|
|
1845
|
+
const found = [];
|
|
1846
|
+
for (const m of modules) {
|
|
1847
|
+
const mine = uses.filter((u) => u.action.file === m.file);
|
|
1848
|
+
const shapes = /* @__PURE__ */ new Map();
|
|
1849
|
+
for (const u of mine) if (!shapes.has(u.action.export)) shapes.set(u.action.export, { bound: u.bound, prev: u.prev });
|
|
1850
|
+
const actions = shapes.size > 0 ? analyzeActionModule(m.source, m.file, load, shapes) ?? m.actions : m.actions;
|
|
1851
|
+
for (const action of actions) found.push({ file: m.file, action: linkForms(action, mine.filter((u) => u.action.export === action.name)) });
|
|
1852
|
+
}
|
|
1853
|
+
const pages = out.appDir ? pageEntries(out.appDir, files, exts) : [];
|
|
1854
|
+
const graph = moduleGraph(root, load);
|
|
1855
|
+
const hosts = actionHosts(pages, graph, exportsByFile);
|
|
1856
|
+
const proposals = readProposedInputs(root);
|
|
1857
|
+
const stem = (file) => snakeCase(file.slice(file.lastIndexOf("/") + 1).replace(/\.[^.]+$/, ""));
|
|
1858
|
+
const base = (f) => (f.action.name === "default" ? stem(f.file) : snakeCase(f.action.name)) || "action";
|
|
1859
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1860
|
+
for (const f of found) counts.set(base(f), (counts.get(base(f)) ?? 0) + 1);
|
|
1861
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1862
|
+
for (const f of found) {
|
|
1863
|
+
let key = base(f);
|
|
1864
|
+
if (counts.get(key) > 1 && f.action.name !== "default") key = `${stem(f.file)}_${key}`;
|
|
1865
|
+
let candidate = key;
|
|
1866
|
+
for (let i = 2; taken.has(candidate); i++) candidate = `${key}_${i}`;
|
|
1867
|
+
taken.add(candidate);
|
|
1868
|
+
const route = actionRoute(out.mcpPath, candidate);
|
|
1869
|
+
const entry = proposals[route];
|
|
1870
|
+
const proposed = entry ? { fields: entry.fields, status: entry.signature === actionSignature(route, f.action.source) ? "current" : "stale" } : void 0;
|
|
1871
|
+
const host = hosts.get(`${f.file}#${f.action.name}`);
|
|
1872
|
+
out.actions.push({ ...f, key: candidate, ...proposed ? { proposed } : {}, ...host ? { host } : {} });
|
|
1873
|
+
}
|
|
1874
|
+
out.pages = discoverPages(root, pages, graph, load, new Set(modules.map((m) => m.file)), new Set(uses.map((u) => u.file)), new Set(out.actions.map((a) => a.key)), snakeCase);
|
|
1875
|
+
const guards = guardFinder(root, load);
|
|
1876
|
+
const rootsOf = new Map(pages.map((p) => [p.file, p.roots]));
|
|
1877
|
+
for (const page of out.pages) {
|
|
1878
|
+
const found2 = guards.page(rootsOf.get(page.file) ?? [page.file]);
|
|
1879
|
+
if (found2.length > 0) page.guards = found2;
|
|
1880
|
+
}
|
|
1881
|
+
for (const a of out.actions) {
|
|
1882
|
+
const found2 = guards.action(a.file, a.action.name);
|
|
1883
|
+
if (found2.length > 0) a.guards = found2;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function readJson(path) {
|
|
1887
|
+
try {
|
|
1888
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1889
|
+
} catch {
|
|
1890
|
+
return void 0;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function readProposedInputs(root) {
|
|
1894
|
+
const path = join6(root, DESCRIPTIONS_FILE);
|
|
1895
|
+
if (!existsSync2(path)) return {};
|
|
1896
|
+
const parsed = parseDescriptions(readFileSync6(path, "utf8"));
|
|
1897
|
+
return typeof parsed === "string" ? {} : parsed.inputs ?? {};
|
|
1898
|
+
}
|
|
1899
|
+
function readDescriptions(root, warn) {
|
|
1900
|
+
const path = join6(root, DESCRIPTIONS_FILE);
|
|
1901
|
+
if (!existsSync2(path)) return {};
|
|
1902
|
+
const parsed = parseDescriptions(readFileSync6(path, "utf8"));
|
|
1903
|
+
if (typeof parsed === "string") {
|
|
1904
|
+
warn(`ignoring ${DESCRIPTIONS_FILE}: ${parsed}`);
|
|
1905
|
+
return {};
|
|
1906
|
+
}
|
|
1907
|
+
return Object.fromEntries(Object.entries(parsed.descriptions).map(([k, e]) => [k, e.text.trim()]));
|
|
1908
|
+
}
|
|
1909
|
+
function readDocs(root, globs) {
|
|
1910
|
+
const paths = globSync(globs, { cwd: root, ignore: ["**/node_modules/**", ".next/**"] }).sort();
|
|
1911
|
+
const docs = [];
|
|
1912
|
+
for (const path of paths) {
|
|
1913
|
+
try {
|
|
1914
|
+
docs.push({ path, content: readFileSync6(join6(root, path), "utf8") });
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return docs;
|
|
1919
|
+
}
|
|
1920
|
+
function defaultOutFile(root) {
|
|
1921
|
+
return existsSync2(join6(root, "tsconfig.json")) ? "exposify.generated.ts" : "exposify.generated.js";
|
|
1922
|
+
}
|
|
1923
|
+
function specifier(outFile, file) {
|
|
1924
|
+
const rel = posix(relative2(dirname2(outFile), file)).replace(/\.[^./]+$/, "");
|
|
1925
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1926
|
+
}
|
|
1927
|
+
function manifestLogin(l) {
|
|
1928
|
+
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {}, where: `${l.export} in ${l.file}` } : { kind: "route", path: l.path, fields: l.fields, body: l.body, where: `POST ${l.path} in ${l.file}` };
|
|
1929
|
+
}
|
|
1930
|
+
function renderManifest(d, opts) {
|
|
1931
|
+
const ts = opts.outFile.endsWith(".ts");
|
|
1932
|
+
const j = (v) => JSON.stringify(v);
|
|
1933
|
+
const routes = d.routes.map((r) => {
|
|
1934
|
+
const common = `router: ${j(r.router)}, path: ${j(r.path)}, params: ${j(r.params)}, file: ${j(r.file)}`;
|
|
1935
|
+
if (r.router === "pages") return ` { ${common} },`;
|
|
1936
|
+
const methods = `methods: ${j(r.module.methods)}`;
|
|
1937
|
+
if (r.module.edge) return ` { ${common}, ${methods}, edge: true },`;
|
|
1938
|
+
return ` { ${common}, ${methods}, load: () => import(${j(specifier(opts.outFile, r.file))}) },`;
|
|
1939
|
+
});
|
|
1940
|
+
const actions = d.actions.map(({ file, key, action, proposed, host, guards }) => {
|
|
1941
|
+
const flags = [
|
|
1942
|
+
action.setsCookies ? ", setsCookies: true" : "",
|
|
1943
|
+
`, fields: ${j(action.fields)}`,
|
|
1944
|
+
action.readsAll ? ", readsAll: true" : "",
|
|
1945
|
+
action.unresolved.length > 0 ? `, unresolved: ${j(action.unresolved)}` : "",
|
|
1946
|
+
action.forms && action.forms.length > 0 ? `, forms: ${j(action.forms)}` : "",
|
|
1947
|
+
proposed?.status === "current" ? `, proposed: ${j(proposed.fields)}` : "",
|
|
1948
|
+
host ? `, host: ${j(host)}` : "",
|
|
1949
|
+
guards ? `, guards: ${j(guards)}` : ""
|
|
1950
|
+
].join("");
|
|
1951
|
+
return ` { key: ${j(key)}, file: ${j(file)}, export: ${j(action.name)}, params: ${j(action.params)}${flags}, load: () => import(${j(specifier(opts.outFile, file))}) },`;
|
|
1952
|
+
});
|
|
1953
|
+
const pages = d.pages.map((p) => {
|
|
1954
|
+
const extra = [
|
|
1955
|
+
p.search.length > 0 ? `, search: ${j(p.search)}` : "",
|
|
1956
|
+
p.title ? `, title: ${j(p.title)}` : "",
|
|
1957
|
+
p.description ? `, description: ${j(p.description)}` : "",
|
|
1958
|
+
p.formOnly ? ", formOnly: true" : "",
|
|
1959
|
+
p.guards ? `, guards: ${j(p.guards)}` : ""
|
|
1960
|
+
].join("");
|
|
1961
|
+
return ` { name: ${j(p.name)}, path: ${j(p.path)}, params: ${j(p.params)}, file: ${j(p.file)}${extra} },`;
|
|
1962
|
+
});
|
|
1963
|
+
const lines = [
|
|
1964
|
+
"// Generated by exposify-next from your route files. Don't edit: withExposify()",
|
|
1965
|
+
"// in next.config regenerates it on `next dev` and `next build`, or run",
|
|
1966
|
+
"// `npx exposify-next generate`.",
|
|
1967
|
+
"/* eslint-disable */",
|
|
1968
|
+
...ts ? ['import type { ExposifyManifest } from "@exposify/next";', ""] : ["", '/** @type {import("@exposify/next").ExposifyManifest} */'],
|
|
1969
|
+
`export const exposify${ts ? ": ExposifyManifest" : ""} = {`,
|
|
1970
|
+
` info: ${j(opts.info)},`,
|
|
1971
|
+
...opts.basePath ? [` basePath: ${j(opts.basePath)},`] : [],
|
|
1972
|
+
...opts.trailingSlash ? [" trailingSlash: true,"] : [],
|
|
1973
|
+
...d.mcpPath && (actions.length > 0 || d.login) ? [` mcpPath: ${j(d.mcpPath)},`] : [],
|
|
1974
|
+
" routes: [",
|
|
1975
|
+
...routes,
|
|
1976
|
+
" ],",
|
|
1977
|
+
...actions.length > 0 ? [" actions: [", ...actions, " ],"] : [],
|
|
1978
|
+
...pages.length > 0 ? [" pages: [", ...pages, " ],"] : [],
|
|
1979
|
+
...d.login ? [` login: ${j(manifestLogin(d.login))},`] : [],
|
|
1980
|
+
...opts.authStacks?.length ? [` authStacks: ${j(opts.authStacks)},`] : [],
|
|
1981
|
+
` docs: ${j(opts.docs)},`,
|
|
1982
|
+
...Object.keys(opts.descriptions).length > 0 ? [` descriptions: ${JSON.stringify(opts.descriptions, null, 2).replace(/\n/g, "\n ")},`] : [],
|
|
1983
|
+
"};",
|
|
1984
|
+
""
|
|
1985
|
+
];
|
|
1986
|
+
return lines.join("\n");
|
|
1987
|
+
}
|
|
1988
|
+
function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)) {
|
|
1989
|
+
const outFile = opts.out ?? defaultOutFile(root);
|
|
1990
|
+
const discovery = discover(root, opts);
|
|
1991
|
+
const pkg = readJson(join6(root, "package.json"));
|
|
1992
|
+
const info = { name: typeof pkg?.name === "string" ? pkg.name : "next-app", version: typeof pkg?.version === "string" ? pkg.version : "0.0.0" };
|
|
1993
|
+
const content = renderManifest(discovery, {
|
|
1994
|
+
outFile,
|
|
1995
|
+
docs: readDocs(root, opts.docs ?? DEFAULT_DOCS),
|
|
1996
|
+
descriptions: readDescriptions(root, warn),
|
|
1997
|
+
info,
|
|
1998
|
+
...opts.basePath ? { basePath: opts.basePath } : {},
|
|
1999
|
+
...opts.trailingSlash ? { trailingSlash: true } : {},
|
|
2000
|
+
authStacks: authStacksOf(dependencyNames(pkg)).map((s) => s.pkg)
|
|
2001
|
+
});
|
|
2002
|
+
for (const a of discovery.actions.filter((x) => x.proposed?.status === "stale")) {
|
|
2003
|
+
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until \`exposify-next describe\` reviews it again`);
|
|
2004
|
+
}
|
|
2005
|
+
if (discovery.actions.length > 0 && !discovery.mcpPath) {
|
|
2006
|
+
warn(
|
|
2007
|
+
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx exposify-next setup)`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
const file = join6(root, outFile);
|
|
2011
|
+
let current;
|
|
2012
|
+
try {
|
|
2013
|
+
current = readFileSync6(file, "utf8");
|
|
2014
|
+
} catch {
|
|
2015
|
+
current = void 0;
|
|
2016
|
+
}
|
|
2017
|
+
if (current !== content) writeFileSync(file, content);
|
|
2018
|
+
return { file, changed: current !== content, discovery };
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// bin/adapter.ts
|
|
2022
|
+
import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
|
|
2023
|
+
import { dirname as dirname4, join as join8, relative as relative3, resolve as resolve2 } from "path";
|
|
2024
|
+
import { decideExposure, deriveDescription, resolveOptions, toolNames } from "@exposify/core";
|
|
2025
|
+
import { renderConfig, replaceAiReadyOptions } from "@exposify/cli";
|
|
2026
|
+
|
|
2027
|
+
// bin/actions.ts
|
|
2028
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
|
|
2029
|
+
|
|
2030
|
+
// src/inline.ts
|
|
2031
|
+
import { dirname as dirname3 } from "path";
|
|
2032
|
+
function patternNames(p, out = []) {
|
|
2033
|
+
if (!p) return out;
|
|
2034
|
+
if (p.type === "Identifier") out.push(p.name);
|
|
2035
|
+
else if (p.type === "AssignmentPattern") patternNames(p.left, out);
|
|
2036
|
+
else if (p.type === "RestElement") patternNames(p.argument, out);
|
|
2037
|
+
else if (p.type === "ObjectPattern") for (const prop of p.properties) patternNames(prop.type === "RestElement" ? prop : prop.value, out);
|
|
2038
|
+
else if (p.type === "ArrayPattern") for (const el of p.elements) patternNames(el ?? void 0, out);
|
|
2039
|
+
else if (p.type === "TSParameterProperty") patternNames(p.parameter, out);
|
|
2040
|
+
return out;
|
|
2041
|
+
}
|
|
2042
|
+
function declaredIn(scope, skip) {
|
|
2043
|
+
const names = /* @__PURE__ */ new Set();
|
|
2044
|
+
for (const p of scope.params ?? []) for (const n of patternNames(p)) names.add(n);
|
|
2045
|
+
walk(scope.body, (n, parents) => {
|
|
2046
|
+
if (skip && (n === skip || parents.includes(skip))) return;
|
|
2047
|
+
if (n.type === "FunctionDeclaration" && n.id && !parents.some((p) => FUNCTION_TYPES.has(p.type))) names.add(n.id.name);
|
|
2048
|
+
if (parents.some((p) => FUNCTION_TYPES.has(p.type))) return;
|
|
2049
|
+
if (n.type === "VariableDeclarator") for (const name of patternNames(n.id)) names.add(name);
|
|
2050
|
+
});
|
|
2051
|
+
return names;
|
|
2052
|
+
}
|
|
2053
|
+
function readsIn(fn) {
|
|
2054
|
+
const own = declaredIn(fn);
|
|
2055
|
+
const out = [];
|
|
2056
|
+
walk(fn.body, (n, parents) => {
|
|
2057
|
+
if (n.type !== "Identifier") return;
|
|
2058
|
+
const p = parents[parents.length - 1];
|
|
2059
|
+
if ((p?.type === "MemberExpression" || p?.type === "OptionalMemberExpression") && p.property === n && !p.computed) return;
|
|
2060
|
+
if (p?.type === "ObjectProperty" && p.key === n && !p.computed && !p.shorthand) return;
|
|
2061
|
+
if (p?.type === "VariableDeclarator" && p.id === n) return;
|
|
2062
|
+
if (own.has(n.name) || out.includes(n.name)) return;
|
|
2063
|
+
out.push(n.name);
|
|
2064
|
+
});
|
|
2065
|
+
return out;
|
|
2066
|
+
}
|
|
2067
|
+
var lowerFirst = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
2068
|
+
function suggestInlineMove(source, file, line) {
|
|
2069
|
+
const program = parseModule(source, file);
|
|
2070
|
+
if (!program) return void 0;
|
|
2071
|
+
let fn;
|
|
2072
|
+
let chain = [];
|
|
2073
|
+
walk(program, (n, parents) => {
|
|
2074
|
+
if (fn || !FUNCTION_TYPES.has(n.type) || n.body?.type !== "BlockStatement" || lineOf(n) !== line) return;
|
|
2075
|
+
if (n.body.directives.some((d) => d.value?.value === "use server")) {
|
|
2076
|
+
fn = n;
|
|
2077
|
+
chain = [...parents];
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
if (!fn) return void 0;
|
|
2081
|
+
const target = fn;
|
|
2082
|
+
const parent = chain[chain.length - 1];
|
|
2083
|
+
const declarator = parent?.type === "VariableDeclarator" && unwrapTs(parent.init) === target ? parent : void 0;
|
|
2084
|
+
const component = [...chain].reverse().find((p) => FUNCTION_TYPES.has(p.type));
|
|
2085
|
+
const componentName = component?.id?.name ?? [...chain].reverse().find((p) => p.type === "VariableDeclarator" && p.id.type === "Identifier")?.id?.name;
|
|
2086
|
+
const name = target.id?.name ?? (declarator?.id.type === "Identifier" ? declarator.id.name : `${componentName ? lowerFirst(componentName) : "inline"}Action`);
|
|
2087
|
+
const enclosing = /* @__PURE__ */ new Set();
|
|
2088
|
+
for (const scope of chain.filter((p) => FUNCTION_TYPES.has(p.type))) for (const n of declaredIn(scope, target)) enclosing.add(n);
|
|
2089
|
+
enclosing.delete(name);
|
|
2090
|
+
const reads = readsIn(target);
|
|
2091
|
+
const captured = reads.filter((n) => enclosing.has(n));
|
|
2092
|
+
const imports = [];
|
|
2093
|
+
const pageLocals = [];
|
|
2094
|
+
for (const stmt of program.body) {
|
|
2095
|
+
if (stmt.type === "ImportDeclaration") {
|
|
2096
|
+
const used = stmt.specifiers.filter((s) => reads.includes(s.local.name));
|
|
2097
|
+
if (used.length === 0) continue;
|
|
2098
|
+
const named2 = used.filter((s) => s.type === "ImportSpecifier").map((s) => s.imported.name === s.local.name ? s.local.name : `${s.imported.name} as ${s.local.name}`);
|
|
2099
|
+
const def = used.find((s) => s.type === "ImportDefaultSpecifier")?.local.name;
|
|
2100
|
+
const ns = used.find((s) => s.type === "ImportNamespaceSpecifier")?.local.name;
|
|
2101
|
+
const parts = [def, ns ? `* as ${ns}` : void 0, named2.length ? `{ ${named2.join(", ")} }` : void 0].filter(Boolean);
|
|
2102
|
+
imports.push(`import ${parts.join(", ")} from ${JSON.stringify(stmt.source.value)};`);
|
|
2103
|
+
} else {
|
|
2104
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
2105
|
+
const names = decl?.type === "VariableDeclaration" ? decl.declarations.flatMap((d) => patternNames(d.id)) : decl?.id ? [decl.id.name] : [];
|
|
2106
|
+
for (const n of names) if (reads.includes(n) && n !== componentName) pageLocals.push(n);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const ext = /\.(t|j)sx?$/.exec(file)?.[1] === "j" ? "js" : "ts";
|
|
2110
|
+
const dir = dirname3(file);
|
|
2111
|
+
const newFile = `${dir === "." ? "" : `${dir}/`}actions.${ext}`;
|
|
2112
|
+
const ownParams = target.params.map((p) => text(source, p) ?? paramName(p) ?? "arg");
|
|
2113
|
+
const directive2 = target.body.directives.find((d) => d.value?.value === "use server");
|
|
2114
|
+
const bodyText = source.slice(directive2.end, target.body.end - 1).replace(/^\s*\n/, "").replace(/\s+$/, "");
|
|
2115
|
+
const bodyLines = bodyText.split("\n");
|
|
2116
|
+
const indent = Math.min(...bodyLines.filter((l) => l.trim()).map((l) => l.length - l.trimStart().length), 99);
|
|
2117
|
+
const body = bodyLines.map((l) => ` ${l.slice(Math.min(indent, l.length - l.trimStart().length))}`.trimEnd()).join("\n");
|
|
2118
|
+
const asyncKw = target.async ? "async " : "";
|
|
2119
|
+
const newLines = [
|
|
2120
|
+
'"use server";',
|
|
2121
|
+
...imports,
|
|
2122
|
+
"",
|
|
2123
|
+
...captured.length > 0 ? [`// TODO(exposify): this was only reachable for the ${captured.join(", ")} the page had loaded; check that the caller may act on ${captured.length === 1 ? "it" : "them"}, and add ${captured.length === 1 ? "its type" : "their types"}.`] : [],
|
|
2124
|
+
...pageLocals.map((n) => `// TODO(exposify): it uses ${n} from ${file}: move or import it.`),
|
|
2125
|
+
`export ${asyncKw}function ${name}(${[...captured, ...ownParams].join(", ")}) {`,
|
|
2126
|
+
...body.split("\n"),
|
|
2127
|
+
"}"
|
|
2128
|
+
];
|
|
2129
|
+
const lines = source.split("\n");
|
|
2130
|
+
const bound = captured.length > 0 ? `${name}.bind(null, ${captured.join(", ")})` : name;
|
|
2131
|
+
const hunks = [];
|
|
2132
|
+
const lastImport = program.body.filter((s) => s.type === "ImportDeclaration").pop();
|
|
2133
|
+
const importAt = lastImport ? lastImport.loc.end.line : 0;
|
|
2134
|
+
hunks.push({ at: importAt + 1, remove: [], add: [`import { ${name} } from "./actions";`] });
|
|
2135
|
+
const removed = declarator ? chain[chain.length - 2] : target.type === "FunctionDeclaration" ? target : void 0;
|
|
2136
|
+
if (removed) {
|
|
2137
|
+
const from = lineOf(removed);
|
|
2138
|
+
const to = removed.loc.end.line;
|
|
2139
|
+
hunks.push({ at: from, remove: lines.slice(from - 1, to), add: [] });
|
|
2140
|
+
if (captured.length > 0) {
|
|
2141
|
+
const uses = /* @__PURE__ */ new Map();
|
|
2142
|
+
walk(program, (n, parents) => {
|
|
2143
|
+
if (n.type !== "Identifier" || n.name !== name || parents.includes(removed) || n === removed.id) return;
|
|
2144
|
+
const at = lineOf(n);
|
|
2145
|
+
const current = uses.get(at) ?? lines[at - 1];
|
|
2146
|
+
uses.set(at, current.replace(new RegExp(`\\b${name}\\b(?!\\.bind)`), bound));
|
|
2147
|
+
});
|
|
2148
|
+
for (const [at, next] of uses) hunks.push({ at, remove: [lines[at - 1]], add: [next] });
|
|
2149
|
+
}
|
|
2150
|
+
} else {
|
|
2151
|
+
const from = lineOf(target);
|
|
2152
|
+
const to = target.loc.end.line;
|
|
2153
|
+
const startCol = target.loc.start.column;
|
|
2154
|
+
const endCol = target.loc.end.column;
|
|
2155
|
+
const replaced = `${lines[from - 1].slice(0, startCol)}${bound}${lines[to - 1].slice(endCol)}`;
|
|
2156
|
+
hunks.push({ at: from, remove: lines.slice(from - 1, to), add: [replaced] });
|
|
2157
|
+
}
|
|
2158
|
+
hunks.sort((a, b) => a.at - b.at);
|
|
2159
|
+
const diff = [
|
|
2160
|
+
"--- /dev/null",
|
|
2161
|
+
`+++ ${newFile} (or add to it, if it exists)`,
|
|
2162
|
+
...newLines.map((l) => `+${l}`),
|
|
2163
|
+
`--- ${file}`,
|
|
2164
|
+
`+++ ${file}`,
|
|
2165
|
+
...hunks.flatMap((h) => [`@@ line ${h.at} @@`, ...h.remove.map((l) => `-${l}`), ...h.add.map((l) => `+${l}`)])
|
|
2166
|
+
].join("\n");
|
|
2167
|
+
return { name, captured, newFile, diff };
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// bin/actions.ts
|
|
2171
|
+
import { join as join7 } from "path";
|
|
2172
|
+
|
|
2173
|
+
// src/actions.ts
|
|
2174
|
+
var AUTH_LIKE = /^(log_?in|log_?out|sign_?in|sign_?out|sign_?up|register|authenticate)(_|$)/;
|
|
2175
|
+
var looksLikeAuth = (action) => Boolean(action.setsCookies) || AUTH_LIKE.test(action.key);
|
|
2176
|
+
function inputProblem(action) {
|
|
2177
|
+
if (action.unresolved && action.unresolved.length > 0) return `its input couldn't all be worked out from its code (${action.unresolved.join("; ")})`;
|
|
2178
|
+
if (action.readsAll && !action.forms?.length) return "it reads the whole form (Object.fromEntries or a loop over it) and no form that submits to it was found, so nothing says which fields exist";
|
|
2179
|
+
const files = action.fields.filter((f) => f.type === "file").map((f) => f.name);
|
|
2180
|
+
if (files.length > 0) return `it takes a file (${files.join(", ")}), which agents can't send yet`;
|
|
2181
|
+
return void 0;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// bin/actions.ts
|
|
2185
|
+
var TODO = "// TODO(exposify):";
|
|
2186
|
+
var AUTH_NOTE = "looks like it signs people in or out: a cookie it sets lands on exposify's internal call, not on the agent's client";
|
|
2187
|
+
function zodFor(f) {
|
|
2188
|
+
let base;
|
|
2189
|
+
if (f.enum && f.enum.length > 0) base = `z.enum(${JSON.stringify(f.enum)})`;
|
|
2190
|
+
else if (f.type === "number") base = `z.number()${f.min !== void 0 ? `.min(${f.min})` : ""}${f.max !== void 0 ? `.max(${f.max})` : ""}`;
|
|
2191
|
+
else if (f.type === "boolean") base = "z.boolean()";
|
|
2192
|
+
else if (f.type === "json") base = "z.unknown()";
|
|
2193
|
+
else {
|
|
2194
|
+
base = "z.string()";
|
|
2195
|
+
if (f.minLength !== void 0) base += `.min(${f.minLength})`;
|
|
2196
|
+
if (f.maxLength !== void 0) base += `.max(${f.maxLength})`;
|
|
2197
|
+
if (f.pattern !== void 0) base += `.regex(/^(?:${f.pattern.replace(/\//g, "\\/")})$/)`;
|
|
2198
|
+
}
|
|
2199
|
+
if (f.array) base = `z.array(${base})`;
|
|
2200
|
+
return f.required ? base : `${base}.optional()`;
|
|
2201
|
+
}
|
|
2202
|
+
var propKey = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
2203
|
+
var located = (file, item) => item.startsWith("line ") ? `${file}:${item.slice(5)}` : item;
|
|
2204
|
+
function stubFor(configKey, found, problem, auth) {
|
|
2205
|
+
const a = found.action;
|
|
2206
|
+
const key = propKey(configKey);
|
|
2207
|
+
const authLine = auth ? [` ${TODO} this ${AUTH_NOTE}.`] : [];
|
|
2208
|
+
if (!problem) return auth ? [`${key}: {`, ...authLine, "},"].join("\n") : `${key}: true,`;
|
|
2209
|
+
const lines = [`${key}: {`, ...authLine, " input: {"];
|
|
2210
|
+
for (const f of a.fields) {
|
|
2211
|
+
if (f.type === "file") lines.push(` ${TODO} ${f.name} is a file, which agents can't send yet: leave it out if the action works without it.`);
|
|
2212
|
+
else lines.push(` ${propKey(f.name)}: ${zodFor(f)},${f.type === "json" ? " // sent as JSON text" : ""}`);
|
|
2213
|
+
}
|
|
2214
|
+
for (const u of a.unresolved) lines.push(` ${TODO} ${located(found.file, u)}`);
|
|
2215
|
+
if (a.readsAll && !a.forms?.length) lines.push(` ${TODO} it reads the whole form, and no form that submits to it was found: list the fields it expects.`);
|
|
2216
|
+
lines.push(" },", "},");
|
|
2217
|
+
return lines.join("\n");
|
|
2218
|
+
}
|
|
2219
|
+
function driftOf(name, written, fields, complete) {
|
|
2220
|
+
if (!written || written === "opaque") return [];
|
|
2221
|
+
const out = [];
|
|
2222
|
+
for (const f of fields) {
|
|
2223
|
+
if (f.type === "file" || f.name in written) continue;
|
|
2224
|
+
out.push(f.origin === "form" ? `its form sends \`${f.name}\`, which its input doesn't list` : `${name} reads \`${f.name}\`, which its input doesn't list`);
|
|
2225
|
+
}
|
|
2226
|
+
for (const [k, w] of Object.entries(written)) {
|
|
2227
|
+
const f = fields.find((x) => x.name === k);
|
|
2228
|
+
if (!f) {
|
|
2229
|
+
if (complete) out.push(`its input lists \`${k}\`, which neither ${name} nor its form reads`);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (!w.enum || !f.enum) continue;
|
|
2233
|
+
const quote = (vs) => vs.map((v) => `\`${v}\``).join(", ");
|
|
2234
|
+
const gone = w.enum.filter((v) => !f.enum.includes(v));
|
|
2235
|
+
const added = f.enum.filter((v) => !w.enum.includes(v));
|
|
2236
|
+
if (gone.length > 0) out.push(`\`${k}\` option${gone.length === 1 ? "" : "s"} ${quote(gone)} ${gone.length === 1 ? "is" : "are"} no longer allowed by ${name} or its form`);
|
|
2237
|
+
if (added.length > 0) out.push(`\`${k}\` can now also be ${quote(added)}, which its input doesn't allow`);
|
|
2238
|
+
}
|
|
2239
|
+
return out;
|
|
2240
|
+
}
|
|
2241
|
+
function formSources(root, forms) {
|
|
2242
|
+
const files = [];
|
|
2243
|
+
const sources = [];
|
|
2244
|
+
for (const file of [...new Set(forms.map((f) => f.file))].slice(0, 2)) {
|
|
2245
|
+
try {
|
|
2246
|
+
const text2 = readFileSync7(join7(root, file), "utf8");
|
|
2247
|
+
files.push(file);
|
|
2248
|
+
sources.push(text2.length > 6e3 ? `${text2.slice(0, 6e3)}
|
|
2249
|
+
/* \u2026 truncated */` : text2);
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return { files, sources };
|
|
2254
|
+
}
|
|
2255
|
+
function crossCheck(root, d) {
|
|
2256
|
+
const path = join7(root, ".next", "server", "server-reference-manifest.json");
|
|
2257
|
+
if (!existsSync3(path)) return [];
|
|
2258
|
+
let manifest;
|
|
2259
|
+
try {
|
|
2260
|
+
manifest = JSON.parse(readFileSync7(path, "utf8"));
|
|
2261
|
+
} catch {
|
|
2262
|
+
return [];
|
|
2263
|
+
}
|
|
2264
|
+
const out = /* @__PURE__ */ new Set();
|
|
2265
|
+
for (const section of ["node", "edge"]) {
|
|
2266
|
+
for (const entry of Object.values(manifest[section] ?? {})) {
|
|
2267
|
+
const { filename, exportedName } = entry;
|
|
2268
|
+
if (!filename || !exportedName || exportedName.startsWith("$$RSC_SERVER_")) continue;
|
|
2269
|
+
const known = d.actions.some((a) => (filename === a.file || filename.endsWith(`/${a.file}`)) && a.action.name === exportedName);
|
|
2270
|
+
if (!known) out.add(`Next's last build has a Server Action ${exportedName} in ${filename} that the scan didn't find (exposify reads exports of files starting with "use server").`);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return [...out];
|
|
2274
|
+
}
|
|
2275
|
+
function scanActions(d, call, root, notes, suggestions = []) {
|
|
2276
|
+
if (call?.actions === "opaque") notes.push("aiReady's actions option isn't an object literal: which actions it lists can't be read, so they show as not listed.");
|
|
2277
|
+
const config = call?.actions && call.actions !== "opaque" ? call.actions : {};
|
|
2278
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2279
|
+
for (const a of d.actions) counts.set(a.action.name, (counts.get(a.action.name) ?? 0) + 1);
|
|
2280
|
+
const rows = d.actions.map((found) => {
|
|
2281
|
+
const a = found.action;
|
|
2282
|
+
const unique = counts.get(a.name) === 1 && a.name !== "default";
|
|
2283
|
+
const configKey = unique ? a.name : found.key;
|
|
2284
|
+
const conf = config[found.key] ?? (unique ? config[a.name] : void 0);
|
|
2285
|
+
const marker = a.marker;
|
|
2286
|
+
const written = conf?.input ?? (marker?.kind === "described" ? marker.input : void 0);
|
|
2287
|
+
const view = { key: found.key, ...a.setsCookies ? { setsCookies: true } : {}, fields: a.fields, readsAll: a.readsAll, unresolved: a.unresolved, ...a.forms ? { forms: a.forms } : {} };
|
|
2288
|
+
const problem = inputProblem(view);
|
|
2289
|
+
const auth = looksLikeAuth(view);
|
|
2290
|
+
const takesInput = a.params.some((p) => p.role !== "prev") || a.unresolved.length > 0;
|
|
2291
|
+
const status = auth || a.fields.some((f) => f.type === "file") ? "flagged" : problem ? "partial" : "complete";
|
|
2292
|
+
const route = actionRoute(d.mcpPath, found.key);
|
|
2293
|
+
const files = a.fields.filter((f) => f.type === "file").map((f) => f.name);
|
|
2294
|
+
const optedIn = marker?.kind !== "hidden" && Boolean(conf || marker);
|
|
2295
|
+
const proposable = optedIn && !written && takesInput && Boolean(problem) && files.length === 0;
|
|
2296
|
+
const accepted = proposable && found.proposed?.status === "current";
|
|
2297
|
+
const formsSent = proposable && a.forms?.length ? formSources(root, a.forms) : void 0;
|
|
2298
|
+
let exposed = false;
|
|
2299
|
+
let reason;
|
|
2300
|
+
if (marker?.kind === "hidden") reason = "hidden() in code";
|
|
2301
|
+
else if (!conf && !marker) reason = "not listed in aiReady({ actions }) (actions are writes)";
|
|
2302
|
+
else if (!written && takesInput && problem && !accepted) {
|
|
2303
|
+
reason = `opted in, but hidden until its input is written: ${problem}${proposable ? `${found.proposed?.status === "stale" ? "; the input accepted for it is out of date" : ""} (\`describe\` can propose it)` : ""}`;
|
|
2304
|
+
} else if (!d.mcpPath) reason = "can't be served: the MCP route isn't app/mcp/[[...transport]]/route.ts";
|
|
2305
|
+
else {
|
|
2306
|
+
exposed = true;
|
|
2307
|
+
const how = conf ? "listed in aiReady({ actions })" : `${marker.kind === "described" ? "describe()" : `${marker.kind}()`} in code`;
|
|
2308
|
+
const input = written ? "input written by hand" : accepted ? "input proposed by describe and accepted" : !takesInput ? "takes no input" : `input from its code${a.forms?.length ? " and form" : ""}`;
|
|
2309
|
+
reason = `${how}; ${input}`;
|
|
2310
|
+
}
|
|
2311
|
+
const rowNotes = [];
|
|
2312
|
+
for (const f of a.forms ?? []) rowNotes.push(`form: ${f.file}:${f.line}`);
|
|
2313
|
+
if (takesInput && !a.forms?.length && a.fields.some((f) => f.origin !== "action" || !f.required)) rowNotes.push("no form found that submits to it: its form fields are all optional");
|
|
2314
|
+
for (const u of a.unresolved) rowNotes.push(`can't tell: ${located(found.file, u)}`);
|
|
2315
|
+
if (a.readsAll && !a.forms?.length) rowNotes.push("reads the whole form, and no form says which fields it gets");
|
|
2316
|
+
if (files.length > 0) rowNotes.push(`takes a file (${files.join(", ")}), which agents can't send yet`);
|
|
2317
|
+
if (auth) rowNotes.push(AUTH_NOTE);
|
|
2318
|
+
return {
|
|
2319
|
+
configKey,
|
|
2320
|
+
export: a.name,
|
|
2321
|
+
file: found.file,
|
|
2322
|
+
line: a.line,
|
|
2323
|
+
tool: conf?.name ?? (marker?.kind === "described" ? marker.name : void 0) ?? found.key,
|
|
2324
|
+
route,
|
|
2325
|
+
status,
|
|
2326
|
+
notes: rowNotes,
|
|
2327
|
+
...marker ? { marker: marker.kind } : {},
|
|
2328
|
+
exposed,
|
|
2329
|
+
reason,
|
|
2330
|
+
drift: driftOf(a.name, written, a.fields, !problem),
|
|
2331
|
+
// An entry the app already has stays as written.
|
|
2332
|
+
stub: conf ? `${conf.source.replace(/,\s*$/, "")},` : stubFor(configKey, found, problem, auth),
|
|
2333
|
+
...a.source ? { source: a.source } : {},
|
|
2334
|
+
...conf?.description ?? marker?.description ? { describedText: conf?.description ?? marker?.description } : {},
|
|
2335
|
+
...proposable ? {
|
|
2336
|
+
proposal: {
|
|
2337
|
+
status: found.proposed?.status ?? "needed",
|
|
2338
|
+
...formsSent?.files.length ? { formFiles: formsSent.files } : {},
|
|
2339
|
+
request: {
|
|
2340
|
+
name: a.name,
|
|
2341
|
+
file: found.file,
|
|
2342
|
+
...a.source ? { source: a.source } : {},
|
|
2343
|
+
...formsSent?.sources.length ? { forms: formsSent.sources } : {},
|
|
2344
|
+
fields: a.fields.map((f) => ({ name: f.name, type: f.type, ...f.required ? { required: true } : {}, ...f.enum ? { enum: f.enum } : {} })),
|
|
2345
|
+
unresolved: a.unresolved.map((u) => located(found.file, u))
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
} : {}
|
|
2349
|
+
};
|
|
2350
|
+
});
|
|
2351
|
+
for (const inline of d.inlineActions) {
|
|
2352
|
+
let move;
|
|
2353
|
+
try {
|
|
2354
|
+
move = suggestInlineMove(readFileSync7(join7(root, inline.file), "utf8"), inline.file, inline.line);
|
|
2355
|
+
} catch {
|
|
2356
|
+
move = void 0;
|
|
2357
|
+
}
|
|
2358
|
+
notes.push(
|
|
2359
|
+
`${inline.file}:${inline.line}: an inline "use server" function${inline.name ? ` (${inline.name})` : ""} can't be a tool: it closes over values from the render. Move it to a "use server" file to expose it${move ? " (setup prints the change)" : ""}.`
|
|
2360
|
+
);
|
|
2361
|
+
if (move) {
|
|
2362
|
+
suggestions.push({
|
|
2363
|
+
title: `Move the inline action at ${inline.file}:${inline.line} to ${move.newFile} as ${move.name}${move.captured.length ? `, passing ${move.captured.join(", ")} with .bind` : ""}:`,
|
|
2364
|
+
diff: move.diff
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
notes.push(...crossCheck(root, d));
|
|
2369
|
+
return rows;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// bin/adapter.ts
|
|
2373
|
+
var MCP_ROUTE_DIR = "mcp/[[...transport]]";
|
|
2374
|
+
function isNextProject(dir) {
|
|
2375
|
+
try {
|
|
2376
|
+
const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf8"));
|
|
2377
|
+
const hasNext = Boolean(pkg.dependencies?.next ?? pkg.devDependencies?.next);
|
|
2378
|
+
return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(join8(dir, d)));
|
|
2379
|
+
} catch {
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
function findNextRoot(start) {
|
|
2384
|
+
for (let dir = start; ; dir = dirname4(dir)) {
|
|
2385
|
+
if (isNextProject(dir)) return dir;
|
|
2386
|
+
if (dirname4(dir) === dir) return void 0;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
var nextAdapter = {
|
|
2390
|
+
bin: "exposify-next",
|
|
2391
|
+
entryExample: "apps/web",
|
|
2392
|
+
entryHelp: "The Next.js project directory",
|
|
2393
|
+
locate(cwd, entryFlag) {
|
|
2394
|
+
const root = findNextRoot(entryFlag ? resolve2(cwd, entryFlag) : cwd);
|
|
2395
|
+
return root ? { entry: root, root, via: "package.json" } : void 0;
|
|
2396
|
+
},
|
|
2397
|
+
async scan(location) {
|
|
2398
|
+
try {
|
|
2399
|
+
return { ok: true, result: scanProject(location.root) };
|
|
2400
|
+
} catch (err) {
|
|
2401
|
+
return { ok: false, error: err.message };
|
|
2402
|
+
}
|
|
2403
|
+
},
|
|
2404
|
+
configStep({ result, config, current, root }) {
|
|
2405
|
+
const d = discover(root);
|
|
2406
|
+
const otherKeys = current?.otherKeys;
|
|
2407
|
+
let options = renderConfig(config, otherKeys ? { otherKeys } : {}).replace(/^app\.use\(aiReady\(\)\);$/, "const handler = aiReady(exposify);").replace(/^app\.use\(aiReady\(\{/, "const handler = aiReady(exposify, {").replace(/\}\)\);$/, "});");
|
|
2408
|
+
const entries = Object.fromEntries((config.actions ?? []).map((key) => [key, result.actions?.find((a) => a.configKey === key)?.stub ?? `${key}: true,`]));
|
|
2409
|
+
const stubs = Object.values(entries);
|
|
2410
|
+
if (stubs.length > 0) {
|
|
2411
|
+
const block = [" actions: {", ...stubs.flatMap((s) => s.split("\n").map((l) => ` ${l}`)), " },"].join("\n");
|
|
2412
|
+
options = options.endsWith("aiReady(exposify);") ? options.replace("aiReady(exposify);", `aiReady(exposify, {
|
|
2413
|
+
${block}
|
|
2414
|
+
});`) : options.replace(/\n\}\);$/, `
|
|
2415
|
+
${block}
|
|
2416
|
+
});`);
|
|
2417
|
+
}
|
|
2418
|
+
const usesZod = stubs.some((s) => s.includes("z."));
|
|
2419
|
+
const lines = [];
|
|
2420
|
+
if (stubs.some((s) => s.includes("TODO(exposify)"))) {
|
|
2421
|
+
lines.push("Go through the TODO(exposify) lines: an action whose input is still incomplete stays hidden until you write it out.");
|
|
2422
|
+
}
|
|
2423
|
+
if (!hasWithExposify(root)) {
|
|
2424
|
+
lines.push(
|
|
2425
|
+
'Wrap your next.config export: `export default withExposify(nextConfig)`, imported from "@exposify/next/config". It keeps exposify.generated.ts current on `next dev` and `next build`.'
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join8(root, f)).find((f) => existsSync4(f));
|
|
2429
|
+
const manifestFile = existing ?? generate(root, {}, () => {
|
|
2430
|
+
}).file;
|
|
2431
|
+
const generated = relative3(root, manifestFile);
|
|
2432
|
+
if (result.aiReady?.site) {
|
|
2433
|
+
const file = relative3(root, result.aiReady.site.file);
|
|
2434
|
+
if (usesZod) lines.push('The input stubs use zod: `import { z } from "zod";` in that file, if it isn\'t there.');
|
|
2435
|
+
return {
|
|
2436
|
+
code: options,
|
|
2437
|
+
hint: { lines: [`Replace the aiReady(...) call in ${file}:${result.aiReady.site.line} with this.`, ...lines] },
|
|
2438
|
+
apply: applyReplace(result.aiReady.site, config, entries, usesZod, file)
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
const routeFile = `${d.appDir ?? "app"}/${MCP_ROUTE_DIR}/route.${generated.endsWith(".ts") ? "ts" : "js"}`;
|
|
2442
|
+
const manifestImport = relative3(join8(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
|
|
2443
|
+
const code = [
|
|
2444
|
+
'import { aiReady } from "@exposify/next";',
|
|
2445
|
+
...usesZod ? ['import { z } from "zod";'] : [],
|
|
2446
|
+
`import { exposify } from "${manifestImport.startsWith(".") ? manifestImport : `./${manifestImport}`}";`,
|
|
2447
|
+
"",
|
|
2448
|
+
options,
|
|
2449
|
+
"",
|
|
2450
|
+
"export { handler as GET, handler as POST, handler as DELETE };"
|
|
2451
|
+
].join("\n");
|
|
2452
|
+
return {
|
|
2453
|
+
code,
|
|
2454
|
+
apply: { plan: { file: join8(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
|
|
2455
|
+
` } },
|
|
2456
|
+
hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
function hasWithExposify(root) {
|
|
2461
|
+
for (const name of ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs", "next.config.mts"]) {
|
|
2462
|
+
try {
|
|
2463
|
+
if (readFileSync8(join8(root, name), "utf8").includes("withExposify")) return true;
|
|
2464
|
+
} catch {
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return false;
|
|
2468
|
+
}
|
|
2469
|
+
function scanProject(root) {
|
|
2470
|
+
const d = discover(root);
|
|
2471
|
+
if (!d.appDir && !d.pagesDir) throw new Error("no app/ or pages/ directory");
|
|
2472
|
+
const call = d.mcpRoute ? findAiReadyCall(d.mcpRoute.source, d.mcpRoute.file) : null;
|
|
2473
|
+
const cfg = resolveOptions({ ...call?.expose ? { expose: call.expose } : {}, ...call?.exclude ? { exclude: call.exclude } : {} });
|
|
2474
|
+
const listed = Array.isArray(call?.expose) ? call.expose : [];
|
|
2475
|
+
const notes = [];
|
|
2476
|
+
const descriptions = readDescriptions(root, (m) => notes.push(m));
|
|
2477
|
+
const rows = [];
|
|
2478
|
+
for (const r of d.routes) {
|
|
2479
|
+
if (r.router === "app") {
|
|
2480
|
+
for (const method of r.module.methods.filter((m) => m !== "OPTIONS")) {
|
|
2481
|
+
const m = r.module.markers[method];
|
|
2482
|
+
const marker = m ? m.kind === "described" ? { kind: "described", ...m.name ? { name: m.name } : {}, ...m.description ? { description: m.description } : {} } : { kind: m.kind } : void 0;
|
|
2483
|
+
const source = r.module.sources[method];
|
|
2484
|
+
rows.push({ info: { method, path: r.path, params: r.params, ...marker ? { marker } : {} }, ...m?.name ? { describedName: m.name } : {}, ...m?.description ? { describedText: m.description } : {}, ...source ? { source } : {} });
|
|
2485
|
+
}
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
const methods = new Set(r.detected?.methods ?? []);
|
|
2489
|
+
for (const entry of listed) {
|
|
2490
|
+
const [method, ...rest] = entry.trim().split(/\s+/);
|
|
2491
|
+
if (method && rest.join(" ") === r.path) methods.add(method.toUpperCase());
|
|
2492
|
+
}
|
|
2493
|
+
if (methods.size === 0) {
|
|
2494
|
+
notes.push(`${r.file}: methods not detected; to expose it, add "METHOD ${r.path}" to expose by hand.`);
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
for (const method of methods) rows.push({ info: { method, path: r.path, params: r.params, optIn: true }, ...r.source ? { source: r.source } : {} });
|
|
2498
|
+
}
|
|
2499
|
+
for (const p of d.pages) {
|
|
2500
|
+
let source;
|
|
2501
|
+
try {
|
|
2502
|
+
source = readFileSync8(join8(root, p.file), "utf8");
|
|
2503
|
+
} catch {
|
|
2504
|
+
source = void 0;
|
|
2505
|
+
}
|
|
2506
|
+
if (source && source.length > 6e3) source = `${source.slice(0, 6e3)}
|
|
2507
|
+
/* \u2026 truncated */`;
|
|
2508
|
+
rows.push({
|
|
2509
|
+
info: { method: "GET", path: p.path, params: p.params, ...p.formOnly ? { optIn: true } : {} },
|
|
2510
|
+
describedName: p.name,
|
|
2511
|
+
...p.description ? { describedText: p.description } : {},
|
|
2512
|
+
...source ? { source } : {},
|
|
2513
|
+
...p.formOnly ? { formOnly: true } : {},
|
|
2514
|
+
page: true
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
const exposure = new Map(rows.map((row) => [`${row.info.method} ${row.info.path}`, decideExposure(row.info, cfg)]));
|
|
2518
|
+
const named2 = rows.map((row) => ({
|
|
2519
|
+
method: row.info.method,
|
|
2520
|
+
path: row.info.path,
|
|
2521
|
+
params: row.info.params,
|
|
2522
|
+
...row.info.marker ? { marker: row.info.marker.kind } : {},
|
|
2523
|
+
...row.describedName ? { describedName: row.describedName } : {}
|
|
2524
|
+
}));
|
|
2525
|
+
const names = toolNames(named2, (r) => exposure.get(`${r.method} ${r.path}`).exposed);
|
|
2526
|
+
for (const row of rows) if (row.page && row.describedName) names.set(`GET ${row.info.path}`, row.describedName);
|
|
2527
|
+
const suggestions = [];
|
|
2528
|
+
const actions = scanActions(d, call, root, notes, suggestions);
|
|
2529
|
+
const routes = rows.map((row) => {
|
|
2530
|
+
const key = `${row.info.method} ${row.info.path}`;
|
|
2531
|
+
const decision = exposure.get(key);
|
|
2532
|
+
const derived = deriveDescription({ ...row.info, ...descriptions[key] ? { description: descriptions[key] } : {} });
|
|
2533
|
+
return {
|
|
2534
|
+
method: row.info.method,
|
|
2535
|
+
path: row.info.path,
|
|
2536
|
+
params: row.info.params,
|
|
2537
|
+
...row.info.marker ? { marker: row.info.marker.kind } : {},
|
|
2538
|
+
...row.describedName ? { describedName: row.describedName } : {},
|
|
2539
|
+
...row.describedText ? { describedText: row.describedText } : {},
|
|
2540
|
+
tool: names.get(key),
|
|
2541
|
+
...derived ? { derived } : {},
|
|
2542
|
+
exposed: decision.exposed,
|
|
2543
|
+
reason: row.formOnly && !decision.exposed ? `a page that's just a form posting to a Server Action (the action is the tool); list "GET ${row.info.path}" in expose to read it anyway` : row.page && decision.exposed ? "a page: read like a browser reads it" : decision.reason,
|
|
2544
|
+
...row.info.optIn ? { optIn: true } : {},
|
|
2545
|
+
...row.page ? { kind: "page" } : {},
|
|
2546
|
+
...row.source ? { source: row.source } : {}
|
|
2547
|
+
};
|
|
2548
|
+
});
|
|
2549
|
+
return {
|
|
2550
|
+
ok: true,
|
|
2551
|
+
routes,
|
|
2552
|
+
...actions.length > 0 ? {
|
|
2553
|
+
actions,
|
|
2554
|
+
actionsNote: `Server Actions are called the way a browser's form calls them, on a page that uses them (one no page uses runs at ${(d.mcpPath ?? "/mcp").replace(/\/$/, "")}/actions/* instead), so each action must check auth itself, as Next advises.`
|
|
2555
|
+
} : {},
|
|
2556
|
+
...notes.length > 0 ? { notes } : {},
|
|
2557
|
+
...suggestions.length > 0 ? { suggestions } : {},
|
|
2558
|
+
...d.login ? { login: { what: loginLabel(d.login), fields: d.login.fields.map((f) => f.name), ...d.loginOthers?.length ? { others: d.loginOthers.map(loginLabel) } : {} } } : {},
|
|
2559
|
+
...call && d.mcpRoute ? {
|
|
2560
|
+
aiReady: {
|
|
2561
|
+
site: { file: join8(root, d.mcpRoute.file), line: call.line },
|
|
2562
|
+
options: {
|
|
2563
|
+
...call.expose ? { expose: call.expose } : {},
|
|
2564
|
+
...call.exclude ? { exclude: call.exclude } : {},
|
|
2565
|
+
...call.startupLog !== void 0 ? { startupLog: call.startupLog } : {},
|
|
2566
|
+
...call.actions && call.actions !== "opaque" ? { actions: Object.keys(call.actions) } : {},
|
|
2567
|
+
otherKeys: call.otherKeys
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
} : {}
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
function applyReplace(site, config, entries, usesZod, relFile) {
|
|
2574
|
+
try {
|
|
2575
|
+
const before = readFileSync8(site.file, "utf8");
|
|
2576
|
+
const edit = replaceAiReadyOptions({
|
|
2577
|
+
source: before,
|
|
2578
|
+
file: site.file,
|
|
2579
|
+
line: site.line,
|
|
2580
|
+
callNames: ["aiReady"],
|
|
2581
|
+
optionsArg: 1,
|
|
2582
|
+
config,
|
|
2583
|
+
actionEntries: entries,
|
|
2584
|
+
...usesZod ? { needsImport: { from: "zod", name: "z" } } : {}
|
|
2585
|
+
});
|
|
2586
|
+
if ("blocked" in edit) return edit;
|
|
2587
|
+
return { plan: { file: site.file, summary: `Replace the aiReady() options in ${relFile}`, before, after: edit.after } };
|
|
2588
|
+
} catch (err) {
|
|
2589
|
+
return { blocked: err.message };
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// bin/exposify-next.ts
|
|
2594
|
+
var GENERATE_USAGE = `Usage: exposify-next generate [options]
|
|
2595
|
+
|
|
2596
|
+
Writes exposify.generated.ts (the route table and docs the MCP route uses).
|
|
2597
|
+
withExposify() in next.config runs this on \`next dev\` and \`next build\`.
|
|
2598
|
+
|
|
2599
|
+
Options:
|
|
2600
|
+
--entry <path> The Next.js project directory (default: detected)
|
|
2601
|
+
--out <file> Output file (default: exposify.generated.ts, .js without tsconfig.json)
|
|
2602
|
+
--docs <glob> Docs for search_docs (repeatable; default README.md, docs/**/*.md)
|
|
2603
|
+
--base-path <p> next.config's basePath, if you don't use withExposify()`;
|
|
2604
|
+
function runGenerate(argv2, cwd) {
|
|
2605
|
+
let entry;
|
|
2606
|
+
let out;
|
|
2607
|
+
let basePath;
|
|
2608
|
+
const docs = [];
|
|
2609
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
2610
|
+
const flag = argv2[i];
|
|
2611
|
+
const value = () => {
|
|
2612
|
+
const v = argv2[++i];
|
|
2613
|
+
if (v === void 0) throw new Error(`${flag} needs a value`);
|
|
2614
|
+
return v;
|
|
2615
|
+
};
|
|
2616
|
+
if (flag === "--entry") entry = value();
|
|
2617
|
+
else if (flag === "--out") out = value();
|
|
2618
|
+
else if (flag === "--docs") docs.push(value());
|
|
2619
|
+
else if (flag === "--base-path") basePath = value();
|
|
2620
|
+
else if (flag === "--help" || flag === "-h") {
|
|
2621
|
+
console.log(GENERATE_USAGE);
|
|
2622
|
+
return 0;
|
|
2623
|
+
} else {
|
|
2624
|
+
console.error(`Unknown option: ${flag}
|
|
2625
|
+
|
|
2626
|
+
${GENERATE_USAGE}`);
|
|
2627
|
+
return 1;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
const root = findNextRoot(entry ? `${cwd}/${entry}` : cwd);
|
|
2631
|
+
if (!root) {
|
|
2632
|
+
console.error("Couldn't find a Next.js project (a package.json with next, and an app/ or pages/ directory).");
|
|
2633
|
+
return 1;
|
|
2634
|
+
}
|
|
2635
|
+
const result = generate(root, { ...out ? { out } : {}, ...docs.length > 0 ? { docs } : {}, ...basePath ? { basePath } : {} });
|
|
2636
|
+
const routes = result.discovery.routes;
|
|
2637
|
+
const app = routes.filter((r) => r.router === "app").length;
|
|
2638
|
+
console.log(
|
|
2639
|
+
`${result.changed ? "Wrote" : "Unchanged:"} ${relative4(cwd, result.file) || result.file}: ${app} App Router route file${app === 1 ? "" : "s"}, ${routes.length - app} Pages Router API file${routes.length - app === 1 ? "" : "s"}.`
|
|
2640
|
+
);
|
|
2641
|
+
if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx exposify-next setup`.");
|
|
2642
|
+
return 0;
|
|
2643
|
+
}
|
|
2644
|
+
var argv = process.argv.slice(2);
|
|
2645
|
+
(argv[0] === "generate" ? Promise.resolve().then(() => runGenerate(argv.slice(1), process.cwd())) : runCli(argv, nextAdapter)).then(
|
|
2646
|
+
(code) => process.exit(code),
|
|
2647
|
+
(err) => {
|
|
2648
|
+
console.error(err instanceof Error ? err.message : err);
|
|
2649
|
+
process.exit(1);
|
|
2650
|
+
}
|
|
2651
|
+
);
|