@exposify/next 0.0.2 → 0.0.4
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/README.md +56 -4
- package/dist/config.cjs +365 -94
- package/dist/config.d.cts +49 -3
- package/dist/config.d.ts +49 -3
- package/dist/config.js +359 -88
- package/dist/exposify-next.js +552 -118
- package/dist/index.cjs +190 -43
- package/dist/index.d.cts +40 -120
- package/dist/index.d.ts +40 -120
- package/dist/index.js +196 -43
- package/dist/manifest-DZQCvzQy.d.cts +176 -0
- package/dist/manifest-DZQCvzQy.d.ts +176 -0
- package/package.json +3 -3
- package/dist/fields-olvyFnxj.d.cts +0 -23
- package/dist/fields-olvyFnxj.d.ts +0 -23
package/dist/exposify-next.js
CHANGED
|
@@ -6,8 +6,8 @@ import { runCli } from "@exposify/cli";
|
|
|
6
6
|
|
|
7
7
|
// src/codegen.ts
|
|
8
8
|
import { createHash } from "crypto";
|
|
9
|
-
import { existsSync as existsSync2, readFileSync as
|
|
10
|
-
import { dirname as dirname2, join as
|
|
9
|
+
import { existsSync as existsSync2, readFileSync as readFileSync7, readdirSync, writeFileSync } from "fs";
|
|
10
|
+
import { dirname as dirname2, join as join7, relative as relative2, sep as sep2 } from "path";
|
|
11
11
|
import { DESCRIPTIONS_FILE, authStacksOf, dependencyNames, parseDescriptions } from "@exposify/core";
|
|
12
12
|
import { globSync } from "tinyglobby";
|
|
13
13
|
|
|
@@ -336,8 +336,8 @@ function readsOf(fn, formNames, st, depth) {
|
|
|
336
336
|
st.readsAll = true;
|
|
337
337
|
const decl = [...parents].reverse().find((p) => !["TSAsExpression", "TSSatisfiesExpression", "ParenthesizedExpression"].includes(p.type));
|
|
338
338
|
if (decl?.type === "VariableDeclarator" && decl.id.type === "ObjectPattern") {
|
|
339
|
-
for (const
|
|
340
|
-
const key =
|
|
339
|
+
for (const prop2 of decl.id.properties) {
|
|
340
|
+
const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
|
|
341
341
|
if (key) addField(st, key, {}, false);
|
|
342
342
|
}
|
|
343
343
|
}
|
|
@@ -379,19 +379,19 @@ function analyzeFields(fn, roles, ctx) {
|
|
|
379
379
|
if (literal?.type === "TSTypeLiteral") {
|
|
380
380
|
for (const m of literal.members) if (m.type === "TSPropertySignature" && keyName(m.key)) members.set(keyName(m.key), m);
|
|
381
381
|
}
|
|
382
|
-
for (const
|
|
383
|
-
const key =
|
|
382
|
+
for (const prop2 of id.properties) {
|
|
383
|
+
const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
|
|
384
384
|
if (!key) {
|
|
385
|
-
st.unresolved.push(`line ${lineOf(
|
|
385
|
+
st.unresolved.push(`line ${lineOf(prop2)}: a rest element in parameter ${role.name}`);
|
|
386
386
|
continue;
|
|
387
387
|
}
|
|
388
388
|
const member = members.get(key);
|
|
389
389
|
const hint = annotated(member?.typeAnnotation?.typeAnnotation);
|
|
390
390
|
if (!hint) {
|
|
391
|
-
st.unresolved.push(`line ${lineOf(
|
|
391
|
+
st.unresolved.push(`line ${lineOf(prop2)}: ${key} has no type it can read (string, number, boolean or a union of strings)`);
|
|
392
392
|
continue;
|
|
393
393
|
}
|
|
394
|
-
st.fields.set(key, { name: key, type: hint.type, ...hint.enum ? { enum: hint.enum } : {}, ...member?.optional ||
|
|
394
|
+
st.fields.set(key, { name: key, type: hint.type, ...hint.enum ? { enum: hint.enum } : {}, ...member?.optional || prop2.value.type === "AssignmentPattern" ? {} : { required: true } });
|
|
395
395
|
}
|
|
396
396
|
});
|
|
397
397
|
return { fields: [...st.fields.values()], readsAll: st.readsAll, unresolved: st.unresolved };
|
|
@@ -469,20 +469,20 @@ function renderedAttributes(el, fn, use, mod) {
|
|
|
469
469
|
for (const p of pattern.properties) {
|
|
470
470
|
if (p.type === "RestElement" && p.argument.type === "Identifier") restName = p.argument.name;
|
|
471
471
|
if (p.type !== "ObjectProperty") continue;
|
|
472
|
-
const
|
|
472
|
+
const prop2 = keyName(p.key);
|
|
473
473
|
const value = p.value.type === "AssignmentPattern" ? p.value.left : p.value;
|
|
474
|
-
if (!
|
|
474
|
+
if (!prop2 || value.type !== "Identifier") continue;
|
|
475
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 } : {} });
|
|
476
|
+
locals.set(value.name, { prop: prop2, ...fallback ? { fallback } : {} });
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
|
-
const
|
|
479
|
+
const named = new Set([...locals.values()].map((l) => l.prop));
|
|
480
480
|
const out = /* @__PURE__ */ new Map();
|
|
481
481
|
for (const a of el.openingElement.attributes) {
|
|
482
482
|
if (a.type === "JSXSpreadAttribute") {
|
|
483
483
|
const arg = unwrapTs(a.argument);
|
|
484
484
|
if (arg?.type === "Identifier" && (arg.name === restName || arg.name === propsName)) {
|
|
485
|
-
for (const [k, v] of use) if (arg.name === propsName || !
|
|
485
|
+
for (const [k, v] of use) if (arg.name === propsName || !named.has(k)) out.set(k, v);
|
|
486
486
|
}
|
|
487
487
|
continue;
|
|
488
488
|
}
|
|
@@ -490,14 +490,14 @@ function renderedAttributes(el, fn, use, mod) {
|
|
|
490
490
|
if (!key) continue;
|
|
491
491
|
const expr = a.value?.type === "JSXExpressionContainer" ? unwrapTs(a.value.expression) : void 0;
|
|
492
492
|
if (expr?.type === "Identifier" && locals.has(expr.name)) {
|
|
493
|
-
const { prop, fallback } = locals.get(expr.name);
|
|
494
|
-
const v = use.has(
|
|
493
|
+
const { prop: prop2, fallback } = locals.get(expr.name);
|
|
494
|
+
const v = use.has(prop2) ? use.get(prop2) : fallback;
|
|
495
495
|
if (v !== void 0) out.set(key, v);
|
|
496
496
|
continue;
|
|
497
497
|
}
|
|
498
498
|
if (expr?.type === "MemberExpression" && unwrapTs(expr.object)?.type === "Identifier" && unwrapTs(expr.object).name === propsName) {
|
|
499
|
-
const
|
|
500
|
-
if (
|
|
499
|
+
const prop2 = keyName(expr.property);
|
|
500
|
+
if (prop2 && use.has(prop2)) out.set(key, use.get(prop2));
|
|
501
501
|
continue;
|
|
502
502
|
}
|
|
503
503
|
out.set(key, attrValue(a, [], mod.scope));
|
|
@@ -893,8 +893,8 @@ function actionHosts(pages, graph, actionExports) {
|
|
|
893
893
|
// src/login.ts
|
|
894
894
|
import { readFileSync as readFileSync2 } from "fs";
|
|
895
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;
|
|
896
|
+
import { LOGIN_NAME, NOT_A_LOGIN, NOT_A_VERIFY, START_NAME, VERIFY_NAME, loginFields, startFields, verifyFields } 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
898
|
function routeBodyFields(source) {
|
|
899
899
|
const names = /* @__PURE__ */ new Set();
|
|
900
900
|
let body = null;
|
|
@@ -915,16 +915,27 @@ function routeBodyFields(source) {
|
|
|
915
915
|
}
|
|
916
916
|
return names.size && body ? { fields: [...names], body } : null;
|
|
917
917
|
}
|
|
918
|
-
|
|
919
|
-
|
|
918
|
+
var LOGIN_RULE = { not: NOT_A_LOGIN, name: LOGIN_NAME, mustBeNamed: false, fields: (n) => loginFields(n), session: true };
|
|
919
|
+
var VERIFY_RULE = { not: NOT_A_VERIFY, name: VERIFY_NAME, mustBeNamed: true, fields: (n, named) => verifyFields(n, named), session: true };
|
|
920
|
+
var START_RULE = { not: NOT_A_LOGIN, name: new RegExp(`${LOGIN_NAME.source}|${START_NAME.source}`, "i"), mustBeNamed: true, fields: (n) => startFields(n), session: false };
|
|
921
|
+
var knownKey = (l) => l.kind === "action" ? `${l.file}#${l.export}` : l.file;
|
|
922
|
+
function actionCandidate(a, rule, known) {
|
|
923
|
+
if (rule.not.test(a.key) || rule.not.test(a.action.name)) return null;
|
|
920
924
|
if (!a.action.params.some((p) => p.role === "formData" || p.role === "object")) return null;
|
|
921
|
-
const
|
|
925
|
+
const isNamed = rule.name.test(a.action.name);
|
|
926
|
+
if (rule.mustBeNamed && !isNamed) return null;
|
|
927
|
+
const fields = rule.fields(
|
|
928
|
+
a.action.fields.map((f) => f.name),
|
|
929
|
+
isNamed
|
|
930
|
+
);
|
|
922
931
|
if (!fields) return null;
|
|
923
|
-
if (!a.action.setsCookies && !LOGIN_NAME.test(a.action.name)) return null;
|
|
932
|
+
if (rule.session && !a.action.setsCookies && !LOGIN_NAME.test(a.action.name) && !(rule === LOGIN_RULE && known.has(`${a.file}#${a.action.name}`))) return null;
|
|
924
933
|
return { kind: "action", key: a.key, export: a.action.name, file: a.file, fields, ...a.host ? { host: a.host } : {} };
|
|
925
934
|
}
|
|
926
|
-
function routeCandidate(root, r) {
|
|
927
|
-
if (r.router !== "app" || !r.module.methods.includes("POST") || r.params.length > 0 ||
|
|
935
|
+
function routeCandidate(root, r, rule, known) {
|
|
936
|
+
if (r.router !== "app" || !r.module.methods.includes("POST") || r.params.length > 0 || rule.not.test(r.path)) return null;
|
|
937
|
+
const isNamed = rule.name.test(r.path);
|
|
938
|
+
if (rule.mustBeNamed && !isNamed) return null;
|
|
928
939
|
let source;
|
|
929
940
|
try {
|
|
930
941
|
source = readFileSync2(join2(root, r.file), "utf8");
|
|
@@ -932,29 +943,47 @@ function routeCandidate(root, r) {
|
|
|
932
943
|
return null;
|
|
933
944
|
}
|
|
934
945
|
const read2 = routeBodyFields(source);
|
|
935
|
-
const fields = read2 ?
|
|
946
|
+
const fields = read2 ? rule.fields(read2.fields, isNamed) : null;
|
|
936
947
|
if (!read2 || !fields) return null;
|
|
937
|
-
if (!LOGIN_NAME.test(r.path) && !SESSION_CODE.test(source)) return null;
|
|
948
|
+
if (rule.session && !LOGIN_NAME.test(r.path) && !SESSION_CODE.test(source) && !(rule === LOGIN_RULE && known.has(r.file))) return null;
|
|
938
949
|
return { kind: "route", path: r.path, file: r.file, fields, body: read2.body };
|
|
939
950
|
}
|
|
940
|
-
var
|
|
941
|
-
function
|
|
942
|
-
const
|
|
951
|
+
var nameOf = (l) => l.kind === "action" ? l.export : l.path;
|
|
952
|
+
function candidates(d, rule, known) {
|
|
953
|
+
const out = [];
|
|
943
954
|
for (const a of d.actions) {
|
|
944
|
-
const c = actionCandidate(a);
|
|
945
|
-
if (c)
|
|
955
|
+
const c = actionCandidate(a, rule, known);
|
|
956
|
+
if (c) out.push(c);
|
|
946
957
|
}
|
|
947
958
|
for (const r of d.routes) {
|
|
948
|
-
const c = routeCandidate(d.root, r);
|
|
949
|
-
if (c)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
959
|
+
const c = routeCandidate(d.root, r, rule, known);
|
|
960
|
+
if (c) out.push(c);
|
|
961
|
+
}
|
|
962
|
+
const score = (l) => (rule === LOGIN_RULE && known.has(knownKey(l)) ? 0 : 8) + (rule.name.test(nameOf(l)) ? 0 : 4) + (l.kind === "action" && l.host ? 0 : 2) + l.fields.length / 100;
|
|
963
|
+
return out.sort((a, b) => score(a) - score(b));
|
|
964
|
+
}
|
|
965
|
+
function findLogin(d, known = /* @__PURE__ */ new Set()) {
|
|
966
|
+
const verifies = candidates(d, VERIFY_RULE, known);
|
|
967
|
+
const logins = candidates(d, LOGIN_RULE, known);
|
|
968
|
+
if (logins.length > 0) {
|
|
969
|
+
const [first2, ...others2] = logins;
|
|
970
|
+
return { login: { ...first2, ...verifies[0] ? { verify: verifies[0] } : {} }, others: others2 };
|
|
971
|
+
}
|
|
972
|
+
const pairs = [];
|
|
973
|
+
for (const start of candidates(d, START_RULE, known)) {
|
|
974
|
+
const who = new Set(start.fields.filter((f) => f.kind === "identity").map((f) => f.name));
|
|
975
|
+
const verify = verifies.find((v) => v.fields.some((f) => who.has(f.name))) ?? verifies.find((v) => !v.fields.some((f) => f.kind === "identity"));
|
|
976
|
+
if (verify) pairs.push({ ...start, passwordless: true, verify });
|
|
977
|
+
}
|
|
978
|
+
if (pairs.length === 0) return null;
|
|
979
|
+
const [first, ...others] = pairs;
|
|
980
|
+
return { login: first, others };
|
|
981
|
+
}
|
|
982
|
+
function stepLabel(l) {
|
|
983
|
+
return l.kind === "action" ? `server action ${l.export} (${l.file})` : `POST ${l.path} (${l.file})`;
|
|
955
984
|
}
|
|
956
985
|
function loginLabel(l) {
|
|
957
|
-
return l.
|
|
986
|
+
return l.verify ? `${stepLabel(l)}, then a code: ${stepLabel(l.verify)}` : stepLabel(l);
|
|
958
987
|
}
|
|
959
988
|
|
|
960
989
|
// src/guards.ts
|
|
@@ -1136,9 +1165,9 @@ function pageSearchParams(program) {
|
|
|
1136
1165
|
if (!FUNCTION_TYPES.has(fn.type)) return;
|
|
1137
1166
|
const param = fn.params?.[0];
|
|
1138
1167
|
if (param?.type === "ObjectPattern") {
|
|
1139
|
-
for (const
|
|
1140
|
-
if (
|
|
1141
|
-
const value =
|
|
1168
|
+
for (const prop2 of param.properties) {
|
|
1169
|
+
if (prop2.type !== "ObjectProperty" || keyName(prop2.key) !== "searchParams") continue;
|
|
1170
|
+
const value = prop2.value.type === "AssignmentPattern" ? prop2.value.left : prop2.value;
|
|
1142
1171
|
if (value.type === "Identifier") sources.add(value.name);
|
|
1143
1172
|
for (const k of patternKeys(value)) names.add(k);
|
|
1144
1173
|
}
|
|
@@ -1205,10 +1234,10 @@ function staticMetadata(program) {
|
|
|
1205
1234
|
const init = unwrapTs(d.init);
|
|
1206
1235
|
if (d.id?.type !== "Identifier" || d.id.name !== "metadata" || init?.type !== "ObjectExpression") continue;
|
|
1207
1236
|
const out = {};
|
|
1208
|
-
for (const
|
|
1209
|
-
if (
|
|
1210
|
-
const key = keyName(
|
|
1211
|
-
const value = unwrapTs(
|
|
1237
|
+
for (const prop2 of init.properties) {
|
|
1238
|
+
if (prop2.type !== "ObjectProperty") continue;
|
|
1239
|
+
const key = keyName(prop2.key);
|
|
1240
|
+
const value = unwrapTs(prop2.value);
|
|
1212
1241
|
if (key === "description") {
|
|
1213
1242
|
const s = stringValue(value);
|
|
1214
1243
|
if (s) out.description = s;
|
|
@@ -1311,15 +1340,15 @@ function readTsconfig(root) {
|
|
|
1311
1340
|
}
|
|
1312
1341
|
}
|
|
1313
1342
|
function asFile(path) {
|
|
1314
|
-
const
|
|
1315
|
-
if (/\.[mc]?jsx?$/.test(path))
|
|
1316
|
-
return
|
|
1343
|
+
const candidates2 = [path, ...EXTENSIONS.map((e) => path + e), ...EXTENSIONS.map((e) => join5(path, `index${e}`))];
|
|
1344
|
+
if (/\.[mc]?jsx?$/.test(path)) candidates2.push(...EXTENSIONS.map((e) => path.replace(/\.[mc]?jsx?$/, e)));
|
|
1345
|
+
return candidates2.find((c) => existsSync(c) && statSync(c).isFile());
|
|
1317
1346
|
}
|
|
1318
1347
|
function moduleLoader(root) {
|
|
1319
1348
|
const { baseUrl, paths } = readTsconfig(root);
|
|
1320
1349
|
const base = join5(root, baseUrl ?? ".");
|
|
1321
1350
|
const cache = /* @__PURE__ */ new Map();
|
|
1322
|
-
const
|
|
1351
|
+
const candidates2 = (specifier2, fromFile) => {
|
|
1323
1352
|
if (specifier2.startsWith(".")) return [resolve(root, dirname(fromFile), specifier2)];
|
|
1324
1353
|
for (const [pattern, targets] of Object.entries(paths ?? {})) {
|
|
1325
1354
|
const star = pattern.indexOf("*");
|
|
@@ -1336,7 +1365,7 @@ function moduleLoader(root) {
|
|
|
1336
1365
|
const key = `${fromFile}\0${specifier2}`;
|
|
1337
1366
|
if (cache.has(key)) return cache.get(key);
|
|
1338
1367
|
let found;
|
|
1339
|
-
for (const candidate of
|
|
1368
|
+
for (const candidate of candidates2(specifier2, fromFile)) {
|
|
1340
1369
|
const path = asFile(candidate);
|
|
1341
1370
|
const rel = path && relative(root, path);
|
|
1342
1371
|
if (!path || !rel || rel.startsWith("..") || rel.split(sep).includes("node_modules")) continue;
|
|
@@ -1368,11 +1397,11 @@ function writtenInput(n) {
|
|
|
1368
1397
|
n = unwrapTs(n);
|
|
1369
1398
|
if (n?.type !== "ObjectExpression") return "opaque";
|
|
1370
1399
|
const out = {};
|
|
1371
|
-
for (const
|
|
1372
|
-
const key =
|
|
1400
|
+
for (const prop2 of n.properties) {
|
|
1401
|
+
const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
|
|
1373
1402
|
if (!key) continue;
|
|
1374
1403
|
let values;
|
|
1375
|
-
walk(
|
|
1404
|
+
walk(prop2.value, (c) => {
|
|
1376
1405
|
if (values || c.type !== "CallExpression" || keyName(c.callee?.property) !== "enum") return;
|
|
1377
1406
|
const list = unwrapTs(c.arguments[0]);
|
|
1378
1407
|
const items = list?.type === "ArrayExpression" ? list.elements.map((e) => stringValue(e)) : [];
|
|
@@ -1392,13 +1421,13 @@ function markerOf(init, imports) {
|
|
|
1392
1421
|
const meta = call.arguments[1];
|
|
1393
1422
|
const marker = { kind: "described" };
|
|
1394
1423
|
if (meta?.type === "ObjectExpression") {
|
|
1395
|
-
for (const
|
|
1396
|
-
if (
|
|
1397
|
-
const key = keyName(
|
|
1398
|
-
const value = stringValue(
|
|
1424
|
+
for (const prop2 of meta.properties) {
|
|
1425
|
+
if (prop2.type !== "ObjectProperty") continue;
|
|
1426
|
+
const key = keyName(prop2.key);
|
|
1427
|
+
const value = stringValue(prop2.value);
|
|
1399
1428
|
if (key === "name" && value) marker.name = value;
|
|
1400
1429
|
if (key === "description" && value) marker.description = value;
|
|
1401
|
-
if (key === "input") marker.input = writtenInput(
|
|
1430
|
+
if (key === "input") marker.input = writtenInput(prop2.value);
|
|
1402
1431
|
}
|
|
1403
1432
|
}
|
|
1404
1433
|
return { marker, handler };
|
|
@@ -1669,13 +1698,13 @@ function findInlineActions(source, file) {
|
|
|
1669
1698
|
const program = parseModule(source, file);
|
|
1670
1699
|
if (!program) return [];
|
|
1671
1700
|
const out = [];
|
|
1672
|
-
const
|
|
1701
|
+
const named = /* @__PURE__ */ new Map();
|
|
1673
1702
|
walk(program, (n) => {
|
|
1674
|
-
if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && n.init)
|
|
1703
|
+
if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && n.init) named.set(unwrapTs(n.init), n.id.name);
|
|
1675
1704
|
if (!FUNCTION_TYPES.has(n.type) || n.body?.type !== "BlockStatement") return;
|
|
1676
1705
|
if (!n.body.directives.some((d) => d.value?.value === "use server")) return;
|
|
1677
1706
|
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 ??
|
|
1707
|
+
const name = n.id?.name ?? named.get(n);
|
|
1679
1708
|
out.push({ ...name ? { name } : {}, line: n.loc?.start.line ?? 1 });
|
|
1680
1709
|
});
|
|
1681
1710
|
return out;
|
|
@@ -1696,13 +1725,13 @@ function findAiReadyCall(source, file) {
|
|
|
1696
1725
|
const call = { line: n.loc?.start.line ?? 1, otherKeys: [] };
|
|
1697
1726
|
const options = unwrapTs(n.arguments[1]);
|
|
1698
1727
|
if (options && options.type !== "ObjectExpression") call.otherKeys.push("(options variable)");
|
|
1699
|
-
for (const
|
|
1700
|
-
const key =
|
|
1728
|
+
for (const prop2 of options?.type === "ObjectExpression" ? options.properties : []) {
|
|
1729
|
+
const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
|
|
1701
1730
|
if (!key) {
|
|
1702
1731
|
call.otherKeys.push("(spread)");
|
|
1703
1732
|
continue;
|
|
1704
1733
|
}
|
|
1705
|
-
const value = unwrapTs(
|
|
1734
|
+
const value = unwrapTs(prop2.value);
|
|
1706
1735
|
if (key === "expose" && stringValue(value) === "*") call.expose = "*";
|
|
1707
1736
|
else if ((key === "expose" || key === "exclude") && value?.type === "ArrayExpression") {
|
|
1708
1737
|
call[key] = value.elements.map((e) => stringValue(e)).filter((s) => s !== void 0);
|
|
@@ -1733,6 +1762,229 @@ function findAiReadyCall(source, file) {
|
|
|
1733
1762
|
return found;
|
|
1734
1763
|
}
|
|
1735
1764
|
|
|
1765
|
+
// src/supabase.ts
|
|
1766
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1767
|
+
import { join as join6 } from "path";
|
|
1768
|
+
var AUTH_CALLS = {
|
|
1769
|
+
signInWithPassword: "password",
|
|
1770
|
+
signInWithOtp: "email-code",
|
|
1771
|
+
verifyOtp: "email-code",
|
|
1772
|
+
signInWithOAuth: "social",
|
|
1773
|
+
signInWithIdToken: "other",
|
|
1774
|
+
signInWithSSO: "other",
|
|
1775
|
+
signInAnonymously: "other",
|
|
1776
|
+
signInWithWeb3: "other"
|
|
1777
|
+
};
|
|
1778
|
+
var MFA_CALLS = /* @__PURE__ */ new Set(["challenge", "verify", "challengeAndVerify"]);
|
|
1779
|
+
var APP_SERVER_FILE = /^(page|layout|template|route|default|loading|not-found|forbidden|unauthorized|error|global-error)\.[^.]+$/;
|
|
1780
|
+
var MIDDLEWARE_FILE = /^(src\/)?(middleware|proxy|instrumentation)\.[^./]+$/;
|
|
1781
|
+
var directive2 = (program) => program.directives?.[0]?.value?.value;
|
|
1782
|
+
var baseName = (f) => f.slice(f.lastIndexOf("/") + 1);
|
|
1783
|
+
function isAuthObject(n) {
|
|
1784
|
+
n = unwrapTs(n);
|
|
1785
|
+
if (n?.type === "Identifier") return n.name === "auth";
|
|
1786
|
+
return (n?.type === "MemberExpression" || n?.type === "OptionalMemberExpression") && keyName(n.property) === "auth" && !n.computed;
|
|
1787
|
+
}
|
|
1788
|
+
var propertyOf = (n) => n.computed ? stringValue(n.property) : keyName(n.property);
|
|
1789
|
+
function prop(obj, name) {
|
|
1790
|
+
obj = unwrapTs(obj);
|
|
1791
|
+
if (obj?.type !== "ObjectExpression") return void 0;
|
|
1792
|
+
return obj.properties.find((p) => p.type === "ObjectProperty" && keyName(p.key) === name)?.value;
|
|
1793
|
+
}
|
|
1794
|
+
function findSupabase(root, files, graph, dependencies, dirs) {
|
|
1795
|
+
const ssr = dependencies.includes("@supabase/ssr");
|
|
1796
|
+
if (!ssr) return dependencies.includes("@supabase/auth-helpers-nextjs") ? { authHelpers: true, signIns: [], callbacks: [] } : null;
|
|
1797
|
+
const facts = /* @__PURE__ */ new Map();
|
|
1798
|
+
const factsOf = (file) => {
|
|
1799
|
+
if (facts.has(file)) return facts.get(file);
|
|
1800
|
+
let found = null;
|
|
1801
|
+
try {
|
|
1802
|
+
const source = readFileSync6(join6(root, file), "utf8");
|
|
1803
|
+
const program = parseModule(source, file);
|
|
1804
|
+
if (program) {
|
|
1805
|
+
const d = directive2(program);
|
|
1806
|
+
found = {
|
|
1807
|
+
program,
|
|
1808
|
+
...d ? { directive: d } : {},
|
|
1809
|
+
serverClient: /\bcreateServerClient\s*\(|from\s+["']next\/headers["']/.test(source),
|
|
1810
|
+
browserClient: /\bcreateBrowserClient\s*\(/.test(source)
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
} catch {
|
|
1814
|
+
found = null;
|
|
1815
|
+
}
|
|
1816
|
+
facts.set(file, found);
|
|
1817
|
+
return found;
|
|
1818
|
+
};
|
|
1819
|
+
const isPagesPage = (f) => Boolean(dirs.pagesDir && f.startsWith(`${dirs.pagesDir}/`) && !f.startsWith(`${dirs.pagesDir}/api/`));
|
|
1820
|
+
const isPagesApi = (f) => Boolean(dirs.pagesDir && f.startsWith(`${dirs.pagesDir}/api/`));
|
|
1821
|
+
const isAppRoute = (f) => Boolean(dirs.appDir && f.startsWith(`${dirs.appDir}/`) && /^route\./.test(baseName(f)));
|
|
1822
|
+
const candidates2 = files.filter((f) => {
|
|
1823
|
+
try {
|
|
1824
|
+
return /\b(signIn\w*|verifyOtp|exchangeCodeForSession|mfa|auth-ui-react|storageKey|cookieOptions)\b/.test(readFileSync6(join6(root, f), "utf8"));
|
|
1825
|
+
} catch {
|
|
1826
|
+
return false;
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
const clientRoots = [];
|
|
1830
|
+
const serverRoots = [];
|
|
1831
|
+
for (const f of files) {
|
|
1832
|
+
if (isPagesPage(f)) {
|
|
1833
|
+
clientRoots.push(f);
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
const isEntry = isPagesApi(f) || MIDDLEWARE_FILE.test(f) || Boolean(dirs.appDir && f.startsWith(`${dirs.appDir}/`) && APP_SERVER_FILE.test(baseName(f)));
|
|
1837
|
+
let text2;
|
|
1838
|
+
try {
|
|
1839
|
+
text2 = readFileSync6(join6(root, f), "utf8");
|
|
1840
|
+
} catch {
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
const head = /^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*["']use (client|server)["']/.exec(text2)?.[1];
|
|
1844
|
+
if (head === "client") clientRoots.push(f);
|
|
1845
|
+
else if (head === "server" || isEntry) serverRoots.push(f);
|
|
1846
|
+
}
|
|
1847
|
+
const browserSet = graph.reachable(clientRoots);
|
|
1848
|
+
const serverSet = /* @__PURE__ */ new Set();
|
|
1849
|
+
const queue = [...serverRoots];
|
|
1850
|
+
while (queue.length > 0 && serverSet.size < 2e3) {
|
|
1851
|
+
const f = queue.shift();
|
|
1852
|
+
if (serverSet.has(f)) continue;
|
|
1853
|
+
if (!serverRoots.includes(f) && factsOf(f)?.directive === "use client") continue;
|
|
1854
|
+
serverSet.add(f);
|
|
1855
|
+
for (const e of graph.edges(f)) if (!serverSet.has(e.file)) queue.push(e.file);
|
|
1856
|
+
}
|
|
1857
|
+
const sideOf = (file, inServerFn) => {
|
|
1858
|
+
const f = factsOf(file);
|
|
1859
|
+
if (inServerFn || f?.directive === "use server") return { side: "server", why: "a Server Action" };
|
|
1860
|
+
if (isAppRoute(file) || isPagesApi(file)) return { side: "server", why: "a route handler" };
|
|
1861
|
+
if (f?.directive === "use client") return { side: "browser", why: '"use client"' };
|
|
1862
|
+
if (isPagesPage(file)) return { side: "browser", why: "a Pages Router page" };
|
|
1863
|
+
let server = f?.serverClient ?? false;
|
|
1864
|
+
let browser = f?.browserClient ?? false;
|
|
1865
|
+
let helper;
|
|
1866
|
+
if (!server && !browser) {
|
|
1867
|
+
for (const e of graph.edges(file)) {
|
|
1868
|
+
const g = factsOf(e.file);
|
|
1869
|
+
if (g?.serverClient && !g.browserClient) {
|
|
1870
|
+
server = true;
|
|
1871
|
+
helper = e.file;
|
|
1872
|
+
}
|
|
1873
|
+
if (g?.browserClient && !g.serverClient) {
|
|
1874
|
+
browser = true;
|
|
1875
|
+
helper = e.file;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (server !== browser) return { side: server ? "server" : "browser", why: `uses the ${server ? "server" : "browser"} client${helper ? ` from ${helper}` : ""}` };
|
|
1880
|
+
const inBrowser = browserSet.has(file);
|
|
1881
|
+
const inServer = serverSet.has(file);
|
|
1882
|
+
if (inBrowser && !inServer) return { side: "browser", why: "imported by browser code" };
|
|
1883
|
+
if (inServer && !inBrowser) return { side: "server", why: "imported by server code" };
|
|
1884
|
+
return { side: "unknown", why: inBrowser ? "imported by both browser and server code" : "not imported by a page or route we could follow" };
|
|
1885
|
+
};
|
|
1886
|
+
const out = { signIns: [], callbacks: [] };
|
|
1887
|
+
for (const file of candidates2) {
|
|
1888
|
+
const f = factsOf(file);
|
|
1889
|
+
if (!f) continue;
|
|
1890
|
+
const top = f.program;
|
|
1891
|
+
const topName = (n) => {
|
|
1892
|
+
const d = n.type === "ExportNamedDeclaration" ? n.declaration : n.type === "ExportDefaultDeclaration" ? n.declaration : n;
|
|
1893
|
+
if (n.type === "ExportDefaultDeclaration") return "default";
|
|
1894
|
+
if (d?.type === "FunctionDeclaration") return d.id?.name;
|
|
1895
|
+
if (d?.type === "VariableDeclaration") return keyName(d.declarations?.[0]?.id);
|
|
1896
|
+
return void 0;
|
|
1897
|
+
};
|
|
1898
|
+
let authUi;
|
|
1899
|
+
walk(top, (n, parents) => {
|
|
1900
|
+
if (n.type === "ImportDeclaration" && n.source?.value === "@supabase/auth-ui-react") {
|
|
1901
|
+
authUi = n.specifiers.find((s) => s.type === "ImportSpecifier" && keyName(s.imported) === "Auth")?.local?.name;
|
|
1902
|
+
}
|
|
1903
|
+
if (n.type === "ObjectProperty" && !out.cookieName) {
|
|
1904
|
+
const key = keyName(n.key);
|
|
1905
|
+
const value = stringValue(unwrapTs(n.value));
|
|
1906
|
+
const parentProp = parents.at(-2);
|
|
1907
|
+
if (key === "storageKey" && value) out.cookieName = value;
|
|
1908
|
+
if (key === "name" && value && parentProp?.type === "ObjectProperty" && keyName(parentProp.key) === "cookieOptions") out.cookieName = value;
|
|
1909
|
+
}
|
|
1910
|
+
const push = (call, method2, extra = {}) => {
|
|
1911
|
+
const inServerFn = parents.some((p) => FUNCTION_TYPES.has(p.type) && p.body?.directives?.some((d) => d.value?.value === "use server"));
|
|
1912
|
+
const { side, why } = sideOf(file, inServerFn);
|
|
1913
|
+
const holder = parents.find((p) => top.body.includes(p));
|
|
1914
|
+
const name2 = holder ? topName(holder) : void 0;
|
|
1915
|
+
out.signIns.push({
|
|
1916
|
+
method: method2,
|
|
1917
|
+
call,
|
|
1918
|
+
...extra,
|
|
1919
|
+
file,
|
|
1920
|
+
line: n.loc?.start.line ?? 1,
|
|
1921
|
+
side,
|
|
1922
|
+
why,
|
|
1923
|
+
...side === "server" && name2 && (isActionModule(top) || inServerFn) ? { action: `${file}#${name2}` } : {},
|
|
1924
|
+
...side === "server" && (isAppRoute(file) || isPagesApi(file)) ? { route: file } : {}
|
|
1925
|
+
});
|
|
1926
|
+
};
|
|
1927
|
+
if (n.type === "JSXOpeningElement" && authUi && n.name?.type === "JSXIdentifier" && n.name.name === authUi) {
|
|
1928
|
+
push("<Auth />", "password");
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (n.type !== "CallExpression" && n.type !== "OptionalCallExpression") return;
|
|
1932
|
+
const callee = unwrapTs(n.callee);
|
|
1933
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "OptionalMemberExpression") return;
|
|
1934
|
+
const name = propertyOf(callee);
|
|
1935
|
+
if (!name) return;
|
|
1936
|
+
const object = unwrapTs(callee.object);
|
|
1937
|
+
if (MFA_CALLS.has(name) && (object?.type === "MemberExpression" || object?.type === "OptionalMemberExpression") && propertyOf(object) === "mfa" && isAuthObject(object.object)) {
|
|
1938
|
+
push(`mfa.${name}`, "totp");
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (!isAuthObject(object)) return;
|
|
1942
|
+
const arg = n.arguments?.[0];
|
|
1943
|
+
if (name === "exchangeCodeForSession" || name === "verifyOtp" && prop(arg, "token_hash")) {
|
|
1944
|
+
if (!out.callbacks.includes(file)) out.callbacks.push(file);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const method = Object.hasOwn(AUTH_CALLS, name) ? AUTH_CALLS[name] : void 0;
|
|
1948
|
+
if (!method) return;
|
|
1949
|
+
const provider = name === "signInWithOAuth" ? stringValue(unwrapTs(prop(arg, "provider"))) : void 0;
|
|
1950
|
+
push(name, method, provider ? { provider } : {});
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
return out;
|
|
1954
|
+
}
|
|
1955
|
+
function signInLabel(s) {
|
|
1956
|
+
const side = s.side === "unknown" ? "side unclear, counted as server" : s.side;
|
|
1957
|
+
return `${s.call}${s.provider ? ` (${s.provider})` : ""} in ${s.file}:${s.line} (${side}: ${s.why})`;
|
|
1958
|
+
}
|
|
1959
|
+
function supabaseChoice(found, login, loginPages2 = []) {
|
|
1960
|
+
const onServer = (method) => found.signIns.filter((s) => s.method === method && s.side !== "browser");
|
|
1961
|
+
const direct = [];
|
|
1962
|
+
const unusable = [];
|
|
1963
|
+
for (const method of ["password", "email-code"]) {
|
|
1964
|
+
const server = onServer(method);
|
|
1965
|
+
if (server.length === 0) direct.push(method);
|
|
1966
|
+
else if (!login) unusable.push(...server.map(signInLabel));
|
|
1967
|
+
}
|
|
1968
|
+
direct.push("social");
|
|
1969
|
+
return {
|
|
1970
|
+
...found.cookieName ? { cookieName: found.cookieName } : {},
|
|
1971
|
+
direct,
|
|
1972
|
+
found: found.signIns.map(signInLabel),
|
|
1973
|
+
...unusable.length ? { unusable } : {},
|
|
1974
|
+
...found.callbacks.length ? { callbacks: found.callbacks } : {},
|
|
1975
|
+
...loginPages2.length ? { loginPages: loginPages2 } : {}
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
function knownLogins(found) {
|
|
1979
|
+
const out = /* @__PURE__ */ new Set();
|
|
1980
|
+
for (const s of found?.signIns ?? []) {
|
|
1981
|
+
if (s.method !== "password" || s.side === "browser") continue;
|
|
1982
|
+
if (s.action) out.add(s.action);
|
|
1983
|
+
if (s.route) out.add(s.route);
|
|
1984
|
+
}
|
|
1985
|
+
return out;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1736
1988
|
// src/codegen.ts
|
|
1737
1989
|
var DEFAULT_DOCS = ["README.md", "docs/**/*.md"];
|
|
1738
1990
|
var actionRoute = (mcpPath, key) => `POST ${(mcpPath ?? "/mcp").replace(/\/$/, "")}/actions/${key}`;
|
|
@@ -1742,14 +1994,14 @@ function actionSignature(route, source) {
|
|
|
1742
1994
|
}
|
|
1743
1995
|
var posix = (p) => p.split(sep2).join("/");
|
|
1744
1996
|
function firstDir(root, names) {
|
|
1745
|
-
return names.find((n) => existsSync2(
|
|
1997
|
+
return names.find((n) => existsSync2(join7(root, n)));
|
|
1746
1998
|
}
|
|
1747
1999
|
function filesUnder(root, dir) {
|
|
1748
2000
|
const out = [];
|
|
1749
2001
|
const visit = (rel) => {
|
|
1750
2002
|
let entries;
|
|
1751
2003
|
try {
|
|
1752
|
-
entries = readdirSync(
|
|
2004
|
+
entries = readdirSync(join7(root, rel), { withFileTypes: true });
|
|
1753
2005
|
} catch {
|
|
1754
2006
|
return;
|
|
1755
2007
|
}
|
|
@@ -1776,7 +2028,7 @@ function discover(root, opts = {}) {
|
|
|
1776
2028
|
const dirSegs = file.slice(appDir.length + 1).split("/").slice(0, -1);
|
|
1777
2029
|
const route = appRoute(dirSegs);
|
|
1778
2030
|
if (!route) continue;
|
|
1779
|
-
const source =
|
|
2031
|
+
const source = readFileSync7(join7(root, file), "utf8");
|
|
1780
2032
|
const module = analyzeRouteModule(source, file);
|
|
1781
2033
|
if (module.mcpRoute) {
|
|
1782
2034
|
out.mcpRoute ??= { file, source };
|
|
@@ -1792,7 +2044,7 @@ function discover(root, opts = {}) {
|
|
|
1792
2044
|
const segs = file.slice(pagesDir.length + 1).replace(extPattern, "").split("/");
|
|
1793
2045
|
const route = pagesApiRoute(segs);
|
|
1794
2046
|
if (!route) continue;
|
|
1795
|
-
const source =
|
|
2047
|
+
const source = readFileSync7(join7(root, file), "utf8");
|
|
1796
2048
|
const src = defaultExportSource(source, file);
|
|
1797
2049
|
out.routes.push({ router: "pages", file, ...route, detected: detectPagesMethods(source, file), ...src ? { source: src } : {} });
|
|
1798
2050
|
}
|
|
@@ -1804,14 +2056,22 @@ function discover(root, opts = {}) {
|
|
|
1804
2056
|
const catchAll = route ? /^(.*)\/:[A-Za-z0-9_]+\*\??$/.exec(route.path) : null;
|
|
1805
2057
|
if (catchAll) out.mcpPath = catchAll[1] || "/";
|
|
1806
2058
|
}
|
|
1807
|
-
discoverActions(root, out, exts);
|
|
1808
|
-
const
|
|
2059
|
+
const { files, graph, pages } = discoverActions(root, out, exts);
|
|
2060
|
+
const supabase = findSupabase(root, files, graph, dependencyNames(readJson(join7(root, "package.json"))), out);
|
|
2061
|
+
const found = findLogin(out, knownLogins(supabase));
|
|
1809
2062
|
if (found) {
|
|
1810
2063
|
out.login = found.login;
|
|
1811
2064
|
out.loginOthers = found.others;
|
|
1812
2065
|
}
|
|
2066
|
+
if (supabase && !supabase.authHelpers) out.supabase = { found: supabase, choice: supabaseChoice(supabase, out.login, loginPages(supabase, pages, graph)) };
|
|
2067
|
+
else if (supabase) out.supabase = { found: supabase, choice: { direct: [], found: [] } };
|
|
1813
2068
|
return out;
|
|
1814
2069
|
}
|
|
2070
|
+
function loginPages(found, pages, graph) {
|
|
2071
|
+
const files = new Set(found.signIns.filter((s) => s.method === "password" || s.method === "email-code").map((s) => s.file));
|
|
2072
|
+
if (files.size === 0) return [];
|
|
2073
|
+
return pages.filter((p) => p.params.length === 0 && [...graph.reachable(p.roots)].some((f) => files.has(f))).map((p) => p.path);
|
|
2074
|
+
}
|
|
1815
2075
|
var SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs}";
|
|
1816
2076
|
var NOT_SOURCE = ["**/node_modules/**", ".next/**", "out/**", "build/**", "dist/**", "coverage/**", "**/*.d.ts"];
|
|
1817
2077
|
function snakeCase(name) {
|
|
@@ -1825,7 +2085,7 @@ function discoverActions(root, out, exts) {
|
|
|
1825
2085
|
for (const file of files) {
|
|
1826
2086
|
let source;
|
|
1827
2087
|
try {
|
|
1828
|
-
source =
|
|
2088
|
+
source = readFileSync7(join7(root, file), "utf8");
|
|
1829
2089
|
} catch {
|
|
1830
2090
|
continue;
|
|
1831
2091
|
}
|
|
@@ -1882,24 +2142,25 @@ function discoverActions(root, out, exts) {
|
|
|
1882
2142
|
const found2 = guards.action(a.file, a.action.name);
|
|
1883
2143
|
if (found2.length > 0) a.guards = found2;
|
|
1884
2144
|
}
|
|
2145
|
+
return { files, graph, pages };
|
|
1885
2146
|
}
|
|
1886
2147
|
function readJson(path) {
|
|
1887
2148
|
try {
|
|
1888
|
-
return JSON.parse(
|
|
2149
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
1889
2150
|
} catch {
|
|
1890
2151
|
return void 0;
|
|
1891
2152
|
}
|
|
1892
2153
|
}
|
|
1893
2154
|
function readProposedInputs(root) {
|
|
1894
|
-
const path =
|
|
2155
|
+
const path = join7(root, DESCRIPTIONS_FILE);
|
|
1895
2156
|
if (!existsSync2(path)) return {};
|
|
1896
|
-
const parsed = parseDescriptions(
|
|
2157
|
+
const parsed = parseDescriptions(readFileSync7(path, "utf8"));
|
|
1897
2158
|
return typeof parsed === "string" ? {} : parsed.inputs ?? {};
|
|
1898
2159
|
}
|
|
1899
2160
|
function readDescriptions(root, warn) {
|
|
1900
|
-
const path =
|
|
2161
|
+
const path = join7(root, DESCRIPTIONS_FILE);
|
|
1901
2162
|
if (!existsSync2(path)) return {};
|
|
1902
|
-
const parsed = parseDescriptions(
|
|
2163
|
+
const parsed = parseDescriptions(readFileSync7(path, "utf8"));
|
|
1903
2164
|
if (typeof parsed === "string") {
|
|
1904
2165
|
warn(`ignoring ${DESCRIPTIONS_FILE}: ${parsed}`);
|
|
1905
2166
|
return {};
|
|
@@ -1911,22 +2172,28 @@ function readDocs(root, globs) {
|
|
|
1911
2172
|
const docs = [];
|
|
1912
2173
|
for (const path of paths) {
|
|
1913
2174
|
try {
|
|
1914
|
-
docs.push({ path, content:
|
|
2175
|
+
docs.push({ path, content: readFileSync7(join7(root, path), "utf8") });
|
|
1915
2176
|
} catch {
|
|
1916
2177
|
}
|
|
1917
2178
|
}
|
|
1918
2179
|
return docs;
|
|
1919
2180
|
}
|
|
1920
2181
|
function defaultOutFile(root) {
|
|
1921
|
-
return existsSync2(
|
|
2182
|
+
return existsSync2(join7(root, "tsconfig.json")) ? "exposify.generated.ts" : "exposify.generated.js";
|
|
1922
2183
|
}
|
|
1923
2184
|
function specifier(outFile, file) {
|
|
1924
2185
|
const rel = posix(relative2(dirname2(outFile), file)).replace(/\.[^./]+$/, "");
|
|
1925
2186
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1926
2187
|
}
|
|
1927
2188
|
function manifestLogin(l) {
|
|
1928
|
-
|
|
2189
|
+
const verify = l.verify ? manifestStep(l.verify) : void 0;
|
|
2190
|
+
const where = l.verify ? `${whereOf(l)}, then a code: ${whereOf(l.verify)}` : whereOf(l);
|
|
2191
|
+
return { ...manifestStep(l), where, ...l.passwordless ? { passwordless: true } : {}, ...verify ? { verify } : {} };
|
|
2192
|
+
}
|
|
2193
|
+
function manifestStep(l) {
|
|
2194
|
+
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {} } : { kind: "route", path: l.path, fields: l.fields, body: l.body };
|
|
1929
2195
|
}
|
|
2196
|
+
var whereOf = (l) => l.kind === "action" ? `${l.export} in ${l.file}` : `POST ${l.path} in ${l.file}`;
|
|
1930
2197
|
function renderManifest(d, opts) {
|
|
1931
2198
|
const ts = opts.outFile.endsWith(".ts");
|
|
1932
2199
|
const j = (v) => JSON.stringify(v);
|
|
@@ -1961,22 +2228,23 @@ function renderManifest(d, opts) {
|
|
|
1961
2228
|
return ` { name: ${j(p.name)}, path: ${j(p.path)}, params: ${j(p.params)}, file: ${j(p.file)}${extra} },`;
|
|
1962
2229
|
});
|
|
1963
2230
|
const lines = [
|
|
1964
|
-
"// Generated by exposify
|
|
2231
|
+
"// Generated by @exposify/next from your route files. Don't edit: withExposify()",
|
|
1965
2232
|
"// in next.config regenerates it on `next dev` and `next build`, or run",
|
|
1966
|
-
"// `npx exposify
|
|
2233
|
+
"// `npx @exposify/next generate`.",
|
|
1967
2234
|
"/* eslint-disable */",
|
|
1968
2235
|
...ts ? ['import type { ExposifyManifest } from "@exposify/next";', ""] : ["", '/** @type {import("@exposify/next").ExposifyManifest} */'],
|
|
1969
2236
|
`export const exposify${ts ? ": ExposifyManifest" : ""} = {`,
|
|
1970
2237
|
` info: ${j(opts.info)},`,
|
|
1971
2238
|
...opts.basePath ? [` basePath: ${j(opts.basePath)},`] : [],
|
|
1972
2239
|
...opts.trailingSlash ? [" trailingSlash: true,"] : [],
|
|
1973
|
-
...d.mcpPath && (actions.length > 0 || d.login) ? [` mcpPath: ${j(d.mcpPath)},`] : [],
|
|
2240
|
+
...d.mcpPath && (actions.length > 0 || d.login || d.supabase) ? [` mcpPath: ${j(d.mcpPath)},`] : [],
|
|
1974
2241
|
" routes: [",
|
|
1975
2242
|
...routes,
|
|
1976
2243
|
" ],",
|
|
1977
2244
|
...actions.length > 0 ? [" actions: [", ...actions, " ],"] : [],
|
|
1978
2245
|
...pages.length > 0 ? [" pages: [", ...pages, " ],"] : [],
|
|
1979
2246
|
...d.login ? [` login: ${j(manifestLogin(d.login))},`] : [],
|
|
2247
|
+
...d.supabase && !d.supabase.found.authHelpers ? [` supabase: ${j(d.supabase.choice)},`] : [],
|
|
1980
2248
|
...opts.authStacks?.length ? [` authStacks: ${j(opts.authStacks)},`] : [],
|
|
1981
2249
|
` docs: ${j(opts.docs)},`,
|
|
1982
2250
|
...Object.keys(opts.descriptions).length > 0 ? [` descriptions: ${JSON.stringify(opts.descriptions, null, 2).replace(/\n/g, "\n ")},`] : [],
|
|
@@ -1988,7 +2256,7 @@ function renderManifest(d, opts) {
|
|
|
1988
2256
|
function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)) {
|
|
1989
2257
|
const outFile = opts.out ?? defaultOutFile(root);
|
|
1990
2258
|
const discovery = discover(root, opts);
|
|
1991
|
-
const pkg = readJson(
|
|
2259
|
+
const pkg = readJson(join7(root, "package.json"));
|
|
1992
2260
|
const info = { name: typeof pkg?.name === "string" ? pkg.name : "next-app", version: typeof pkg?.version === "string" ? pkg.version : "0.0.0" };
|
|
1993
2261
|
const content = renderManifest(discovery, {
|
|
1994
2262
|
outFile,
|
|
@@ -1999,18 +2267,24 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
|
|
|
1999
2267
|
...opts.trailingSlash ? { trailingSlash: true } : {},
|
|
2000
2268
|
authStacks: authStacksOf(dependencyNames(pkg)).map((s) => s.pkg)
|
|
2001
2269
|
});
|
|
2270
|
+
if (discovery.supabase?.found.authHelpers) {
|
|
2271
|
+
warn("@supabase/auth-helpers-nextjs keeps its session in an older cookie format AI assistants can't sign in with; move to @supabase/ssr (Supabase's replacement for it) to use Connect");
|
|
2272
|
+
}
|
|
2273
|
+
for (const s of discovery.supabase?.choice.unusable ?? []) {
|
|
2274
|
+
warn(`your app signs users in to Supabase on the server (${s}), but not in a Server Action or route the sign-in form can run, so AI assistants can't sign in with a password; switch on "Sign in with Supabase" on the dashboard to have them sign in to Supabase directly`);
|
|
2275
|
+
}
|
|
2002
2276
|
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
|
|
2277
|
+
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
2278
|
}
|
|
2005
2279
|
if (discovery.actions.length > 0 && !discovery.mcpPath) {
|
|
2006
2280
|
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
|
|
2281
|
+
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
2282
|
);
|
|
2009
2283
|
}
|
|
2010
|
-
const file =
|
|
2284
|
+
const file = join7(root, outFile);
|
|
2011
2285
|
let current;
|
|
2012
2286
|
try {
|
|
2013
|
-
current =
|
|
2287
|
+
current = readFileSync7(file, "utf8");
|
|
2014
2288
|
} catch {
|
|
2015
2289
|
current = void 0;
|
|
2016
2290
|
}
|
|
@@ -2018,14 +2292,17 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
|
|
|
2018
2292
|
return { file, changed: current !== content, discovery };
|
|
2019
2293
|
}
|
|
2020
2294
|
|
|
2295
|
+
// bin/exposify-next.ts
|
|
2296
|
+
import { createInterface } from "readline";
|
|
2297
|
+
|
|
2021
2298
|
// bin/adapter.ts
|
|
2022
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2023
|
-
import { dirname as dirname4, join as
|
|
2299
|
+
import { existsSync as existsSync4, readFileSync as readFileSync9 } from "fs";
|
|
2300
|
+
import { dirname as dirname4, join as join9, relative as relative3, resolve as resolve2 } from "path";
|
|
2024
2301
|
import { decideExposure, deriveDescription, resolveOptions, toolNames } from "@exposify/core";
|
|
2025
2302
|
import { renderConfig, replaceAiReadyOptions } from "@exposify/cli";
|
|
2026
2303
|
|
|
2027
2304
|
// bin/actions.ts
|
|
2028
|
-
import { existsSync as existsSync3, readFileSync as
|
|
2305
|
+
import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
|
|
2029
2306
|
|
|
2030
2307
|
// src/inline.ts
|
|
2031
2308
|
import { dirname as dirname3 } from "path";
|
|
@@ -2034,7 +2311,7 @@ function patternNames(p, out = []) {
|
|
|
2034
2311
|
if (p.type === "Identifier") out.push(p.name);
|
|
2035
2312
|
else if (p.type === "AssignmentPattern") patternNames(p.left, out);
|
|
2036
2313
|
else if (p.type === "RestElement") patternNames(p.argument, out);
|
|
2037
|
-
else if (p.type === "ObjectPattern") for (const
|
|
2314
|
+
else if (p.type === "ObjectPattern") for (const prop2 of p.properties) patternNames(prop2.type === "RestElement" ? prop2 : prop2.value, out);
|
|
2038
2315
|
else if (p.type === "ArrayPattern") for (const el of p.elements) patternNames(el ?? void 0, out);
|
|
2039
2316
|
else if (p.type === "TSParameterProperty") patternNames(p.parameter, out);
|
|
2040
2317
|
return out;
|
|
@@ -2095,10 +2372,10 @@ function suggestInlineMove(source, file, line) {
|
|
|
2095
2372
|
if (stmt.type === "ImportDeclaration") {
|
|
2096
2373
|
const used = stmt.specifiers.filter((s) => reads.includes(s.local.name));
|
|
2097
2374
|
if (used.length === 0) continue;
|
|
2098
|
-
const
|
|
2375
|
+
const named = 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
2376
|
const def = used.find((s) => s.type === "ImportDefaultSpecifier")?.local.name;
|
|
2100
2377
|
const ns = used.find((s) => s.type === "ImportNamespaceSpecifier")?.local.name;
|
|
2101
|
-
const parts = [def, ns ? `* as ${ns}` : void 0,
|
|
2378
|
+
const parts = [def, ns ? `* as ${ns}` : void 0, named.length ? `{ ${named.join(", ")} }` : void 0].filter(Boolean);
|
|
2102
2379
|
imports.push(`import ${parts.join(", ")} from ${JSON.stringify(stmt.source.value)};`);
|
|
2103
2380
|
} else {
|
|
2104
2381
|
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
@@ -2110,8 +2387,8 @@ function suggestInlineMove(source, file, line) {
|
|
|
2110
2387
|
const dir = dirname3(file);
|
|
2111
2388
|
const newFile = `${dir === "." ? "" : `${dir}/`}actions.${ext}`;
|
|
2112
2389
|
const ownParams = target.params.map((p) => text(source, p) ?? paramName(p) ?? "arg");
|
|
2113
|
-
const
|
|
2114
|
-
const bodyText = source.slice(
|
|
2390
|
+
const directive3 = target.body.directives.find((d) => d.value?.value === "use server");
|
|
2391
|
+
const bodyText = source.slice(directive3.end, target.body.end - 1).replace(/^\s*\n/, "").replace(/\s+$/, "");
|
|
2115
2392
|
const bodyLines = bodyText.split("\n");
|
|
2116
2393
|
const indent = Math.min(...bodyLines.filter((l) => l.trim()).map((l) => l.length - l.trimStart().length), 99);
|
|
2117
2394
|
const body = bodyLines.map((l) => ` ${l.slice(Math.min(indent, l.length - l.trimStart().length))}`.trimEnd()).join("\n");
|
|
@@ -2168,7 +2445,7 @@ function suggestInlineMove(source, file, line) {
|
|
|
2168
2445
|
}
|
|
2169
2446
|
|
|
2170
2447
|
// bin/actions.ts
|
|
2171
|
-
import { join as
|
|
2448
|
+
import { join as join8 } from "path";
|
|
2172
2449
|
|
|
2173
2450
|
// src/actions.ts
|
|
2174
2451
|
var AUTH_LIKE = /^(log_?in|log_?out|sign_?in|sign_?out|sign_?up|register|authenticate)(_|$)/;
|
|
@@ -2243,7 +2520,7 @@ function formSources(root, forms) {
|
|
|
2243
2520
|
const sources = [];
|
|
2244
2521
|
for (const file of [...new Set(forms.map((f) => f.file))].slice(0, 2)) {
|
|
2245
2522
|
try {
|
|
2246
|
-
const text2 =
|
|
2523
|
+
const text2 = readFileSync8(join8(root, file), "utf8");
|
|
2247
2524
|
files.push(file);
|
|
2248
2525
|
sources.push(text2.length > 6e3 ? `${text2.slice(0, 6e3)}
|
|
2249
2526
|
/* \u2026 truncated */` : text2);
|
|
@@ -2253,11 +2530,11 @@ function formSources(root, forms) {
|
|
|
2253
2530
|
return { files, sources };
|
|
2254
2531
|
}
|
|
2255
2532
|
function crossCheck(root, d) {
|
|
2256
|
-
const path =
|
|
2533
|
+
const path = join8(root, ".next", "server", "server-reference-manifest.json");
|
|
2257
2534
|
if (!existsSync3(path)) return [];
|
|
2258
2535
|
let manifest;
|
|
2259
2536
|
try {
|
|
2260
|
-
manifest = JSON.parse(
|
|
2537
|
+
manifest = JSON.parse(readFileSync8(path, "utf8"));
|
|
2261
2538
|
} catch {
|
|
2262
2539
|
return [];
|
|
2263
2540
|
}
|
|
@@ -2351,7 +2628,7 @@ function scanActions(d, call, root, notes, suggestions = []) {
|
|
|
2351
2628
|
for (const inline of d.inlineActions) {
|
|
2352
2629
|
let move;
|
|
2353
2630
|
try {
|
|
2354
|
-
move = suggestInlineMove(
|
|
2631
|
+
move = suggestInlineMove(readFileSync8(join8(root, inline.file), "utf8"), inline.file, inline.line);
|
|
2355
2632
|
} catch {
|
|
2356
2633
|
move = void 0;
|
|
2357
2634
|
}
|
|
@@ -2373,9 +2650,9 @@ function scanActions(d, call, root, notes, suggestions = []) {
|
|
|
2373
2650
|
var MCP_ROUTE_DIR = "mcp/[[...transport]]";
|
|
2374
2651
|
function isNextProject(dir) {
|
|
2375
2652
|
try {
|
|
2376
|
-
const pkg = JSON.parse(
|
|
2653
|
+
const pkg = JSON.parse(readFileSync9(join9(dir, "package.json"), "utf8"));
|
|
2377
2654
|
const hasNext = Boolean(pkg.dependencies?.next ?? pkg.devDependencies?.next);
|
|
2378
|
-
return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(
|
|
2655
|
+
return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(join9(dir, d)));
|
|
2379
2656
|
} catch {
|
|
2380
2657
|
return false;
|
|
2381
2658
|
}
|
|
@@ -2387,7 +2664,7 @@ function findNextRoot(start) {
|
|
|
2387
2664
|
}
|
|
2388
2665
|
}
|
|
2389
2666
|
var nextAdapter = {
|
|
2390
|
-
bin: "exposify
|
|
2667
|
+
bin: "npx @exposify/next",
|
|
2391
2668
|
entryExample: "apps/web",
|
|
2392
2669
|
entryHelp: "The Next.js project directory",
|
|
2393
2670
|
locate(cwd, entryFlag) {
|
|
@@ -2425,7 +2702,7 @@ ${block}
|
|
|
2425
2702
|
'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
2703
|
);
|
|
2427
2704
|
}
|
|
2428
|
-
const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) =>
|
|
2705
|
+
const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join9(root, f)).find((f) => existsSync4(f));
|
|
2429
2706
|
const manifestFile = existing ?? generate(root, {}, () => {
|
|
2430
2707
|
}).file;
|
|
2431
2708
|
const generated = relative3(root, manifestFile);
|
|
@@ -2439,7 +2716,7 @@ ${block}
|
|
|
2439
2716
|
};
|
|
2440
2717
|
}
|
|
2441
2718
|
const routeFile = `${d.appDir ?? "app"}/${MCP_ROUTE_DIR}/route.${generated.endsWith(".ts") ? "ts" : "js"}`;
|
|
2442
|
-
const manifestImport = relative3(
|
|
2719
|
+
const manifestImport = relative3(join9(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
|
|
2443
2720
|
const code = [
|
|
2444
2721
|
'import { aiReady } from "@exposify/next";',
|
|
2445
2722
|
...usesZod ? ['import { z } from "zod";'] : [],
|
|
@@ -2451,7 +2728,7 @@ ${block}
|
|
|
2451
2728
|
].join("\n");
|
|
2452
2729
|
return {
|
|
2453
2730
|
code,
|
|
2454
|
-
apply: { plan: { file:
|
|
2731
|
+
apply: { plan: { file: join9(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
|
|
2455
2732
|
` } },
|
|
2456
2733
|
hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
|
|
2457
2734
|
};
|
|
@@ -2460,7 +2737,7 @@ ${block}
|
|
|
2460
2737
|
function hasWithExposify(root) {
|
|
2461
2738
|
for (const name of ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs", "next.config.mts"]) {
|
|
2462
2739
|
try {
|
|
2463
|
-
if (
|
|
2740
|
+
if (readFileSync9(join9(root, name), "utf8").includes("withExposify")) return true;
|
|
2464
2741
|
} catch {
|
|
2465
2742
|
}
|
|
2466
2743
|
}
|
|
@@ -2499,7 +2776,7 @@ function scanProject(root) {
|
|
|
2499
2776
|
for (const p of d.pages) {
|
|
2500
2777
|
let source;
|
|
2501
2778
|
try {
|
|
2502
|
-
source =
|
|
2779
|
+
source = readFileSync9(join9(root, p.file), "utf8");
|
|
2503
2780
|
} catch {
|
|
2504
2781
|
source = void 0;
|
|
2505
2782
|
}
|
|
@@ -2515,14 +2792,14 @@ function scanProject(root) {
|
|
|
2515
2792
|
});
|
|
2516
2793
|
}
|
|
2517
2794
|
const exposure = new Map(rows.map((row) => [`${row.info.method} ${row.info.path}`, decideExposure(row.info, cfg)]));
|
|
2518
|
-
const
|
|
2795
|
+
const named = rows.map((row) => ({
|
|
2519
2796
|
method: row.info.method,
|
|
2520
2797
|
path: row.info.path,
|
|
2521
2798
|
params: row.info.params,
|
|
2522
2799
|
...row.info.marker ? { marker: row.info.marker.kind } : {},
|
|
2523
2800
|
...row.describedName ? { describedName: row.describedName } : {}
|
|
2524
2801
|
}));
|
|
2525
|
-
const names = toolNames(
|
|
2802
|
+
const names = toolNames(named, (r) => exposure.get(`${r.method} ${r.path}`).exposed);
|
|
2526
2803
|
for (const row of rows) if (row.page && row.describedName) names.set(`GET ${row.info.path}`, row.describedName);
|
|
2527
2804
|
const suggestions = [];
|
|
2528
2805
|
const actions = scanActions(d, call, root, notes, suggestions);
|
|
@@ -2556,9 +2833,18 @@ function scanProject(root) {
|
|
|
2556
2833
|
...notes.length > 0 ? { notes } : {},
|
|
2557
2834
|
...suggestions.length > 0 ? { suggestions } : {},
|
|
2558
2835
|
...d.login ? { login: { what: loginLabel(d.login), fields: d.login.fields.map((f) => f.name), ...d.loginOthers?.length ? { others: d.loginOthers.map(loginLabel) } : {} } } : {},
|
|
2836
|
+
...d.supabase ? {
|
|
2837
|
+
supabase: {
|
|
2838
|
+
direct: d.supabase.choice.direct,
|
|
2839
|
+
found: d.supabase.choice.found,
|
|
2840
|
+
...d.supabase.choice.unusable ? { unusable: d.supabase.choice.unusable } : {},
|
|
2841
|
+
...d.supabase.choice.callbacks ? { callbacks: d.supabase.choice.callbacks } : {},
|
|
2842
|
+
...d.supabase.found.authHelpers ? { authHelpers: true } : {}
|
|
2843
|
+
}
|
|
2844
|
+
} : {},
|
|
2559
2845
|
...call && d.mcpRoute ? {
|
|
2560
2846
|
aiReady: {
|
|
2561
|
-
site: { file:
|
|
2847
|
+
site: { file: join9(root, d.mcpRoute.file), line: call.line },
|
|
2562
2848
|
options: {
|
|
2563
2849
|
...call.expose ? { expose: call.expose } : {},
|
|
2564
2850
|
...call.exclude ? { exclude: call.exclude } : {},
|
|
@@ -2572,7 +2858,7 @@ function scanProject(root) {
|
|
|
2572
2858
|
}
|
|
2573
2859
|
function applyReplace(site, config, entries, usesZod, relFile) {
|
|
2574
2860
|
try {
|
|
2575
|
-
const before =
|
|
2861
|
+
const before = readFileSync9(site.file, "utf8");
|
|
2576
2862
|
const edit = replaceAiReadyOptions({
|
|
2577
2863
|
source: before,
|
|
2578
2864
|
file: site.file,
|
|
@@ -2590,8 +2876,110 @@ function applyReplace(site, config, entries, usesZod, relFile) {
|
|
|
2590
2876
|
}
|
|
2591
2877
|
}
|
|
2592
2878
|
|
|
2879
|
+
// bin/test-sign-in.ts
|
|
2880
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2881
|
+
import { join as join10 } from "path";
|
|
2882
|
+
import { parseEnv } from "util";
|
|
2883
|
+
import { supabaseCookieName, supabasePasswordLogin, supabaseSessionCookies } from "@exposify/core";
|
|
2884
|
+
var TEST_SIGN_IN_USAGE = `Usage: @exposify/next test-sign-in [options]
|
|
2885
|
+
|
|
2886
|
+
Signs in to your app's Supabase Auth as AI assistants will (a password, then
|
|
2887
|
+
the authenticator code if you have one), and checks that a page your app
|
|
2888
|
+
protects accepts the session cookie Connect sends. Run it with your app up.
|
|
2889
|
+
|
|
2890
|
+
Options:
|
|
2891
|
+
--url <origin> Where the app runs (default: http://localhost:3000)
|
|
2892
|
+
--page <path> A page that redirects signed-out visitors (default: found)
|
|
2893
|
+
--entry <path> The Next.js project directory (default: detected)
|
|
2894
|
+
|
|
2895
|
+
Reads NEXT_PUBLIC_SUPABASE_URL and the publishable (or anon) key from the
|
|
2896
|
+
environment, .env.local or .env. Signs the test session out afterwards.`;
|
|
2897
|
+
function supabaseEnv(root, env) {
|
|
2898
|
+
const files = [".env.local", ".env"].map((f) => {
|
|
2899
|
+
try {
|
|
2900
|
+
return parseEnv(readFileSync10(join10(root, f), "utf8"));
|
|
2901
|
+
} catch {
|
|
2902
|
+
return {};
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
const read2 = (name) => env[name] || files.find((f) => f[name])?.[name];
|
|
2906
|
+
const url = read2("NEXT_PUBLIC_SUPABASE_URL");
|
|
2907
|
+
const key = read2("NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY") ?? read2("NEXT_PUBLIC_SUPABASE_ANON_KEY") ?? read2("NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY");
|
|
2908
|
+
return url && key ? { url, key } : null;
|
|
2909
|
+
}
|
|
2910
|
+
var redirected = (res) => res.status >= 300 && res.status < 400 || res.status === 401 || res.status === 403;
|
|
2911
|
+
async function testSignIn(root, opts, deps) {
|
|
2912
|
+
const d = discover(root);
|
|
2913
|
+
if (!d.supabase || d.supabase.found.authHelpers) {
|
|
2914
|
+
deps.log(d.supabase ? "Your app uses @supabase/auth-helpers-nextjs, whose cookie format isn't supported: move to @supabase/ssr." : "This app doesn't use Supabase Auth with @supabase/ssr, so there's nothing to test here.");
|
|
2915
|
+
return 1;
|
|
2916
|
+
}
|
|
2917
|
+
const project = supabaseEnv(root, opts.env);
|
|
2918
|
+
if (!project) {
|
|
2919
|
+
deps.log("Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (or _ANON_KEY), in your shell or .env.local.");
|
|
2920
|
+
return 1;
|
|
2921
|
+
}
|
|
2922
|
+
const origin = opts.url.replace(/\/+$/, "");
|
|
2923
|
+
const get = (path, cookie) => deps.fetch(`${origin}${path}`, { headers: cookie ? { cookie } : {}, redirect: "manual" }).catch(() => null);
|
|
2924
|
+
let page = opts.page;
|
|
2925
|
+
if (!page) {
|
|
2926
|
+
const skip = new Set(d.supabase.choice.loginPages ?? []);
|
|
2927
|
+
const candidates2 = d.pages.filter((p) => p.params.length === 0 && !skip.has(p.path) && !p.path.startsWith(d.mcpPath ?? "/mcp")).slice(0, 15);
|
|
2928
|
+
for (const p of candidates2) {
|
|
2929
|
+
const res2 = await get(p.path);
|
|
2930
|
+
if (res2 === null) {
|
|
2931
|
+
deps.log(`Couldn't reach ${origin}. Start your app (next dev), or pass --url.`);
|
|
2932
|
+
return 1;
|
|
2933
|
+
}
|
|
2934
|
+
if (redirected(res2)) {
|
|
2935
|
+
page = p.path;
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (!page) {
|
|
2940
|
+
deps.log("No page turned a signed-out request away, so there's nothing to check the session against. Pass --page with one that does.");
|
|
2941
|
+
return 1;
|
|
2942
|
+
}
|
|
2943
|
+
} else {
|
|
2944
|
+
const res2 = await get(page);
|
|
2945
|
+
if (res2 === null) {
|
|
2946
|
+
deps.log(`Couldn't reach ${origin}. Start your app (next dev), or pass --url.`);
|
|
2947
|
+
return 1;
|
|
2948
|
+
}
|
|
2949
|
+
if (!redirected(res2)) {
|
|
2950
|
+
deps.log(`${page} answered ${res2.status} without signing in, so it can't tell whether the session works. Pass --page with a page that redirects signed-out visitors.`);
|
|
2951
|
+
return 1;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
const login = supabasePasswordLogin({ ...project, fetch: deps.fetch });
|
|
2955
|
+
const ctx = { origin };
|
|
2956
|
+
const email = (await deps.ask("Email: ")).trim();
|
|
2957
|
+
const password = await deps.askSecret("Password: ");
|
|
2958
|
+
let result = await login.attempt({ email, password }, ctx);
|
|
2959
|
+
if (result.ok === "next") {
|
|
2960
|
+
const code = await deps.ask(`${result.message ?? "Code"} `);
|
|
2961
|
+
result = await login.verify.attempt({ code }, { ...ctx, carry: result.carry });
|
|
2962
|
+
}
|
|
2963
|
+
if (result.ok !== true || !("supabase" in result.session)) {
|
|
2964
|
+
deps.log(`Supabase didn't sign you in: ${result.ok === false ? result.message ?? "no reason given" : "no session came back"}`);
|
|
2965
|
+
return 1;
|
|
2966
|
+
}
|
|
2967
|
+
const session = result.session.supabase;
|
|
2968
|
+
const name = d.supabase.choice.cookieName ?? supabaseCookieName(project.url);
|
|
2969
|
+
const cookies = supabaseSessionCookies(name, session);
|
|
2970
|
+
const res = await get(page, cookies.map((c) => `${c.name}=${c.value}`).join("; "));
|
|
2971
|
+
await deps.fetch(`${project.url.replace(/\/+$/, "")}/auth/v1/logout?scope=local`, { method: "POST", headers: { apikey: project.key, authorization: `Bearer ${session.accessToken}` } }).catch(() => null);
|
|
2972
|
+
if (res && res.status >= 200 && res.status < 300) {
|
|
2973
|
+
deps.log(`\u2713 ${page} answered ${res.status} with the session cookie Connect sends (${cookies.length === 1 ? name : `${name}.0\u2013.${cookies.length - 1}`}), and turned you away without it.`);
|
|
2974
|
+
return 0;
|
|
2975
|
+
}
|
|
2976
|
+
deps.log(`\u2717 ${page} answered ${res?.status ?? "nothing"} with the session cookie${res?.headers.get("location") ? ` (to ${res.headers.get("location")})` : ""}: your app didn't accept it.`);
|
|
2977
|
+
if (d.supabase.choice.cookieName === void 0) deps.log(` It was named ${name}. If your app sets another cookie name, the generator didn't see it.`);
|
|
2978
|
+
return 1;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2593
2981
|
// bin/exposify-next.ts
|
|
2594
|
-
var GENERATE_USAGE = `Usage: exposify
|
|
2982
|
+
var GENERATE_USAGE = `Usage: @exposify/next generate [options]
|
|
2595
2983
|
|
|
2596
2984
|
Writes exposify.generated.ts (the route table and docs the MCP route uses).
|
|
2597
2985
|
withExposify() in next.config runs this on \`next dev\` and \`next build\`.
|
|
@@ -2638,11 +3026,57 @@ ${GENERATE_USAGE}`);
|
|
|
2638
3026
|
console.log(
|
|
2639
3027
|
`${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
3028
|
);
|
|
2641
|
-
if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx exposify
|
|
3029
|
+
if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx @exposify/next setup`.");
|
|
2642
3030
|
return 0;
|
|
2643
3031
|
}
|
|
3032
|
+
function prompt(question, secret = false) {
|
|
3033
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
3034
|
+
if (secret) {
|
|
3035
|
+
const write = rl._writeToOutput.bind(rl);
|
|
3036
|
+
rl._writeToOutput = (s) => write(s.startsWith(question) ? s : s.replace(/[^\r\n]/g, ""));
|
|
3037
|
+
}
|
|
3038
|
+
return new Promise(
|
|
3039
|
+
(resolve3) => rl.question(question, (answer) => {
|
|
3040
|
+
rl.close();
|
|
3041
|
+
if (secret) process.stdout.write("\n");
|
|
3042
|
+
resolve3(answer);
|
|
3043
|
+
})
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
async function runTestSignIn(argv2, cwd) {
|
|
3047
|
+
let entry;
|
|
3048
|
+
let url = "http://localhost:3000";
|
|
3049
|
+
let page;
|
|
3050
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
3051
|
+
const flag = argv2[i];
|
|
3052
|
+
const value = () => {
|
|
3053
|
+
const v = argv2[++i];
|
|
3054
|
+
if (v === void 0) throw new Error(`${flag} needs a value`);
|
|
3055
|
+
return v;
|
|
3056
|
+
};
|
|
3057
|
+
if (flag === "--entry") entry = value();
|
|
3058
|
+
else if (flag === "--url") url = value();
|
|
3059
|
+
else if (flag === "--page") page = value();
|
|
3060
|
+
else if (flag === "--help" || flag === "-h") {
|
|
3061
|
+
console.log(TEST_SIGN_IN_USAGE);
|
|
3062
|
+
return 0;
|
|
3063
|
+
} else {
|
|
3064
|
+
console.error(`Unknown option: ${flag}
|
|
3065
|
+
|
|
3066
|
+
${TEST_SIGN_IN_USAGE}`);
|
|
3067
|
+
return 1;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
const root = findNextRoot(entry ? `${cwd}/${entry}` : cwd);
|
|
3071
|
+
if (!root) {
|
|
3072
|
+
console.error("Couldn't find a Next.js project (a package.json with next, and an app/ or pages/ directory).");
|
|
3073
|
+
return 1;
|
|
3074
|
+
}
|
|
3075
|
+
return testSignIn(root, { url, ...page ? { page } : {}, env: process.env }, { ask: (q) => prompt(q), askSecret: (q) => prompt(q, true), log: (l) => console.log(l), fetch });
|
|
3076
|
+
}
|
|
2644
3077
|
var argv = process.argv.slice(2);
|
|
2645
|
-
|
|
3078
|
+
var run = argv[0] === "generate" ? Promise.resolve().then(() => runGenerate(argv.slice(1), process.cwd())) : argv[0] === "test-sign-in" ? runTestSignIn(argv.slice(1), process.cwd()) : runCli(argv, nextAdapter);
|
|
3079
|
+
run.then(
|
|
2646
3080
|
(code) => process.exit(code),
|
|
2647
3081
|
(err) => {
|
|
2648
3082
|
console.error(err instanceof Error ? err.message : err);
|