@exposify/next 0.0.5 → 0.0.7
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 +7 -1
- package/dist/exposify-next.js +152 -17
- package/dist/index.cjs +50 -1
- package/dist/index.js +50 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -184,7 +184,13 @@ route reads `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
|
184
184
|
name them, or `supabase: false` to turn it off. The dashboard shows what was
|
|
185
185
|
found and has the switch for passwords. `inspect` says which way it went and
|
|
186
186
|
why; `npx @exposify/next test-sign-in` signs in on your running app and checks
|
|
187
|
-
a protected page accepts the cookie.
|
|
187
|
+
a protected page accepts the cookie. If your `proxy.ts` sends signed-out
|
|
188
|
+
visitors to the login page (Supabase's starter does), it would send AI
|
|
189
|
+
assistants there too, before they can sign in: `inspect` warns, and
|
|
190
|
+
`setup --apply` leaves `/mcp` and `/.well-known/oauth-protected-resource` out
|
|
191
|
+
of its matcher (a matcher that already leaves out `/mcp` gets the second one
|
|
192
|
+
added). Your pages still go through
|
|
193
|
+
it on tool calls. `@supabase/auth-helpers-nextjs` (an
|
|
188
194
|
older cookie format) isn't supported, and apps on `@supabase/supabase-js`
|
|
189
195
|
alone keep their session in the browser, where the server never sees it.
|
|
190
196
|
|
package/dist/exposify-next.js
CHANGED
|
@@ -2296,8 +2296,8 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
|
|
|
2296
2296
|
import { createInterface } from "readline";
|
|
2297
2297
|
|
|
2298
2298
|
// bin/adapter.ts
|
|
2299
|
-
import { existsSync as
|
|
2300
|
-
import { dirname as dirname4, join as
|
|
2299
|
+
import { existsSync as existsSync5, readFileSync as readFileSync10 } from "fs";
|
|
2300
|
+
import { dirname as dirname4, join as join10, relative as relative3, resolve as resolve2 } from "path";
|
|
2301
2301
|
import { decideExposure, deriveDescription, resolveOptions, toolNames } from "@exposify/core";
|
|
2302
2302
|
import { renderConfig, replaceAiReadyOptions } from "@exposify/cli";
|
|
2303
2303
|
|
|
@@ -2646,13 +2646,142 @@ function scanActions(d, call, root, notes, suggestions = []) {
|
|
|
2646
2646
|
return rows;
|
|
2647
2647
|
}
|
|
2648
2648
|
|
|
2649
|
+
// bin/proxy.ts
|
|
2650
|
+
import { existsSync as existsSync4, readFileSync as readFileSync9 } from "fs";
|
|
2651
|
+
import { join as join9 } from "path";
|
|
2652
|
+
import { PRM_PATH } from "@exposify/core";
|
|
2653
|
+
var FILES = ["", "src/"].flatMap((dir) => ["proxy", "middleware"].flatMap((name) => [".ts", ".js", ".mjs"].map((ext) => `${dir}${name}${ext}`)));
|
|
2654
|
+
var REDIRECT = /\bredirect\s*\(/;
|
|
2655
|
+
function matcherRegExp(pattern) {
|
|
2656
|
+
let re = "";
|
|
2657
|
+
for (let i = 0; i < pattern.length; ) {
|
|
2658
|
+
const c = pattern[i];
|
|
2659
|
+
if (c === "(") {
|
|
2660
|
+
let depth = 0;
|
|
2661
|
+
let j = i;
|
|
2662
|
+
for (; j < pattern.length; j++) {
|
|
2663
|
+
if (pattern[j] === "\\") j++;
|
|
2664
|
+
else if (pattern[j] === "(") depth++;
|
|
2665
|
+
else if (pattern[j] === ")" && --depth === 0) break;
|
|
2666
|
+
}
|
|
2667
|
+
re += pattern.slice(i, j + 1);
|
|
2668
|
+
i = j + 1;
|
|
2669
|
+
} else if (c === "/" && pattern[i + 1] === ":") {
|
|
2670
|
+
const m = /^\/:\w+([*+?]?)/.exec(pattern.slice(i));
|
|
2671
|
+
re += m[1] === "*" ? "(?:/.*)?" : m[1] === "+" ? "/.+" : m[1] === "?" ? "(?:/[^/]+)?" : "/[^/]+";
|
|
2672
|
+
i += m[0].length;
|
|
2673
|
+
} else {
|
|
2674
|
+
re += c.replace(/[.*+?^${}|[\]\\]/g, "\\$&");
|
|
2675
|
+
i++;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
try {
|
|
2679
|
+
return new RegExp(`^${re}/?$`);
|
|
2680
|
+
} catch {
|
|
2681
|
+
return void 0;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
function matcherNode(program) {
|
|
2685
|
+
for (const n of program.body) {
|
|
2686
|
+
const decl = n.type === "ExportNamedDeclaration" ? n.declaration : void 0;
|
|
2687
|
+
if (decl?.type !== "VariableDeclaration") continue;
|
|
2688
|
+
for (const d of decl.declarations) {
|
|
2689
|
+
if (d.id?.name !== "config") continue;
|
|
2690
|
+
const obj = unwrapTs(d.init);
|
|
2691
|
+
if (obj?.type !== "ObjectExpression") return "opaque";
|
|
2692
|
+
const prop2 = obj.properties.find((p) => p.type === "ObjectProperty" && (p.key?.name ?? p.key?.value) === "matcher");
|
|
2693
|
+
return prop2 ? unwrapTs(prop2.value) ?? "opaque" : null;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
function matcherEntries(node) {
|
|
2699
|
+
const items = node.type === "ArrayExpression" ? node.elements : [node];
|
|
2700
|
+
const out = [];
|
|
2701
|
+
for (const item of items) {
|
|
2702
|
+
const literal = item?.type === "ObjectExpression" ? unwrapTs(item.properties.find((p) => (p.key?.name ?? p.key?.value) === "source")?.value) : item;
|
|
2703
|
+
const pattern = stringValue(literal);
|
|
2704
|
+
if (pattern === void 0 || !literal) return void 0;
|
|
2705
|
+
out.push({ pattern, node: literal });
|
|
2706
|
+
}
|
|
2707
|
+
return out;
|
|
2708
|
+
}
|
|
2709
|
+
function redirects(root, file, source, program) {
|
|
2710
|
+
if (REDIRECT.test(source)) return true;
|
|
2711
|
+
const load = moduleLoader(root);
|
|
2712
|
+
for (const n of program.body) {
|
|
2713
|
+
if (n.type !== "ImportDeclaration") continue;
|
|
2714
|
+
const found = load(n.source.value, file);
|
|
2715
|
+
if (found && REDIRECT.test(found.source)) return true;
|
|
2716
|
+
}
|
|
2717
|
+
return false;
|
|
2718
|
+
}
|
|
2719
|
+
function middlewareFile(root) {
|
|
2720
|
+
return FILES.find((f) => existsSync4(join9(root, f)));
|
|
2721
|
+
}
|
|
2722
|
+
function findProxyGate(root, mcpPath = "/mcp") {
|
|
2723
|
+
const file = middlewareFile(root);
|
|
2724
|
+
if (!file) return void 0;
|
|
2725
|
+
const source = readFileSync9(join9(root, file), "utf8");
|
|
2726
|
+
const program = parseModule(source, file);
|
|
2727
|
+
if (!program || !redirects(root, file, source, program)) return void 0;
|
|
2728
|
+
const matcher = matcherNode(program);
|
|
2729
|
+
if (matcher === "opaque") return void 0;
|
|
2730
|
+
if (matcher === null) return { file, mcpPath, matching: [] };
|
|
2731
|
+
const entries = matcherEntries(matcher);
|
|
2732
|
+
if (!entries) return void 0;
|
|
2733
|
+
const probes = exclusions(mcpPath).flatMap((x) => x.probes);
|
|
2734
|
+
const matching = entries.filter((e) => probes.some((p) => matcherRegExp(e.pattern)?.test(p))).map((e) => e.pattern);
|
|
2735
|
+
return matching.length > 0 ? { file, mcpPath, matching } : void 0;
|
|
2736
|
+
}
|
|
2737
|
+
function proxyGateHint(gate) {
|
|
2738
|
+
return `${gate.file} sends signed-out requests to another page and runs on ${gate.mcpPath}, so AI assistants get that page instead of a sign-in. Leave ${gate.mcpPath} and ${PRM_PATH} out of its matcher (the MCP route checks its own callers): ` + (gate.matching.some((p) => p.startsWith("/((?!")) ? `add ${inSource(gate.mcpPath)}| after "(?!" in config.matcher.` : `matcher: ["/((?!${inSource(gate.mcpPath)}).*)"].`);
|
|
2739
|
+
}
|
|
2740
|
+
var exclusions = (mcpPath) => [
|
|
2741
|
+
{
|
|
2742
|
+
alt: `${mcpPath.replace(/^\/+|\/+$/g, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/|$)`,
|
|
2743
|
+
probes: [mcpPath, `${mcpPath.replace(/\/$/, "")}/login`]
|
|
2744
|
+
},
|
|
2745
|
+
{ alt: String.raw`\.well-known/oauth-protected-resource(?:/|$)`, probes: [`${PRM_PATH}${mcpPath}`] }
|
|
2746
|
+
];
|
|
2747
|
+
var exclusion = (mcpPath, pattern) => exclusions(mcpPath).filter((x) => pattern === void 0 || x.probes.some((p) => matcherRegExp(pattern)?.test(p))).map((x) => x.alt).join("|");
|
|
2748
|
+
var inSource = (mcpPath, pattern) => exclusion(mcpPath, pattern).replace(/\\/g, "\\\\");
|
|
2749
|
+
function excludeMcpFromProxy(root, gate) {
|
|
2750
|
+
const file = join9(root, gate.file);
|
|
2751
|
+
const before = readFileSync9(file, "utf8");
|
|
2752
|
+
const program = parseModule(before, gate.file);
|
|
2753
|
+
if (!program) return { blocked: `couldn't parse ${gate.file}` };
|
|
2754
|
+
const summary = `Leave ${gate.mcpPath} out of ${gate.file}'s matcher, so AI assistants reach the sign-in instead of your login page`;
|
|
2755
|
+
const matcher = matcherNode(program);
|
|
2756
|
+
if (matcher === null) {
|
|
2757
|
+
const after2 = `${before.replace(/\n*$/, "\n")}
|
|
2758
|
+
export const config = {
|
|
2759
|
+
matcher: ["/((?!${inSource(gate.mcpPath)}).*)"],
|
|
2760
|
+
};
|
|
2761
|
+
`;
|
|
2762
|
+
return { plan: { file, summary, before, after: after2 } };
|
|
2763
|
+
}
|
|
2764
|
+
const entries = matcher === "opaque" ? void 0 : matcherEntries(matcher);
|
|
2765
|
+
if (!entries) return { blocked: `${gate.file}'s matcher isn't a list of strings` };
|
|
2766
|
+
let after = before;
|
|
2767
|
+
for (const e of [...entries].reverse()) {
|
|
2768
|
+
if (!gate.matching.includes(e.pattern)) continue;
|
|
2769
|
+
const raw = before.slice(e.node.start, e.node.end);
|
|
2770
|
+
const at = raw.indexOf("/((?!");
|
|
2771
|
+
if (at < 0) return { blocked: `${gate.file}'s matcher "${e.pattern}" has no "(?!" to add ${gate.mcpPath} to` };
|
|
2772
|
+
const insert = e.node.start + at + "/((?!".length;
|
|
2773
|
+
after = `${after.slice(0, insert)}${inSource(gate.mcpPath, e.pattern)}|${after.slice(insert)}`;
|
|
2774
|
+
}
|
|
2775
|
+
return { plan: { file, summary, before, after } };
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2649
2778
|
// bin/adapter.ts
|
|
2650
2779
|
var MCP_ROUTE_DIR = "mcp/[[...transport]]";
|
|
2651
2780
|
function isNextProject(dir) {
|
|
2652
2781
|
try {
|
|
2653
|
-
const pkg = JSON.parse(
|
|
2782
|
+
const pkg = JSON.parse(readFileSync10(join10(dir, "package.json"), "utf8"));
|
|
2654
2783
|
const hasNext = Boolean(pkg.dependencies?.next ?? pkg.devDependencies?.next);
|
|
2655
|
-
return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) =>
|
|
2784
|
+
return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync5(join10(dir, d)));
|
|
2656
2785
|
} catch {
|
|
2657
2786
|
return false;
|
|
2658
2787
|
}
|
|
@@ -2704,7 +2833,11 @@ ${block}
|
|
|
2704
2833
|
const wrap = hasWithExposify(root) ? null : wrapNextConfig(root);
|
|
2705
2834
|
const also = wrap && "plan" in wrap ? [{ plan: wrap.plan, manual: WRAP_HINT }] : [];
|
|
2706
2835
|
if (wrap && !("plan" in wrap)) lines.push(WRAP_HINT);
|
|
2707
|
-
const
|
|
2836
|
+
const gate = findProxyGate(root, d.mcpPath ?? "/mcp");
|
|
2837
|
+
const unblock = gate ? excludeMcpFromProxy(root, gate) : null;
|
|
2838
|
+
if (gate && unblock && "plan" in unblock) also.push({ plan: unblock.plan, manual: proxyGateHint(gate) });
|
|
2839
|
+
else if (gate) lines.push(proxyGateHint(gate));
|
|
2840
|
+
const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join10(root, f)).find((f) => existsSync5(f));
|
|
2708
2841
|
const manifestFile = existing ?? generate(root, {}, () => {
|
|
2709
2842
|
}).file;
|
|
2710
2843
|
const generated = relative3(root, manifestFile);
|
|
@@ -2715,7 +2848,7 @@ ${block}
|
|
|
2715
2848
|
return { code: options, hint: { lines: [`Replace the aiReady(...) call in ${file}:${result.aiReady.site.line} with this.`, ...lines] }, apply };
|
|
2716
2849
|
}
|
|
2717
2850
|
const routeFile = `${d.appDir ?? "app"}/${MCP_ROUTE_DIR}/route.${generated.endsWith(".ts") ? "ts" : "js"}`;
|
|
2718
|
-
const manifestImport = relative3(
|
|
2851
|
+
const manifestImport = relative3(join10(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
|
|
2719
2852
|
const code = [
|
|
2720
2853
|
'import { aiReady } from "@exposify/next";',
|
|
2721
2854
|
...usesZod ? ['import { z } from "zod";'] : [],
|
|
@@ -2727,7 +2860,7 @@ ${block}
|
|
|
2727
2860
|
].join("\n");
|
|
2728
2861
|
return {
|
|
2729
2862
|
code,
|
|
2730
|
-
apply: { plan: { file:
|
|
2863
|
+
apply: { plan: { file: join10(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
|
|
2731
2864
|
` }, ...also.length ? { also } : {} },
|
|
2732
2865
|
hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
|
|
2733
2866
|
};
|
|
@@ -2741,10 +2874,10 @@ function withAlso(outcome, also, lines) {
|
|
|
2741
2874
|
return outcome;
|
|
2742
2875
|
}
|
|
2743
2876
|
function wrapNextConfig(root) {
|
|
2744
|
-
const name = NEXT_CONFIGS.find((n) =>
|
|
2877
|
+
const name = NEXT_CONFIGS.find((n) => existsSync5(join10(root, n)));
|
|
2745
2878
|
if (!name) return { blocked: "no next.config file" };
|
|
2746
|
-
const file =
|
|
2747
|
-
const before =
|
|
2879
|
+
const file = join10(root, name);
|
|
2880
|
+
const before = readFileSync10(file, "utf8");
|
|
2748
2881
|
const program = parseModule(before, name);
|
|
2749
2882
|
if (!program) return { blocked: `couldn't parse ${name}` };
|
|
2750
2883
|
const body = program.body;
|
|
@@ -2770,7 +2903,7 @@ ${wrapped}`;
|
|
|
2770
2903
|
function hasWithExposify(root) {
|
|
2771
2904
|
for (const name of NEXT_CONFIGS) {
|
|
2772
2905
|
try {
|
|
2773
|
-
if (
|
|
2906
|
+
if (readFileSync10(join10(root, name), "utf8").includes("withExposify")) return true;
|
|
2774
2907
|
} catch {
|
|
2775
2908
|
}
|
|
2776
2909
|
}
|
|
@@ -2817,7 +2950,7 @@ function scanProject(root) {
|
|
|
2817
2950
|
for (const p of d.pages) {
|
|
2818
2951
|
let source;
|
|
2819
2952
|
try {
|
|
2820
|
-
source =
|
|
2953
|
+
source = readFileSync10(join10(root, p.file), "utf8");
|
|
2821
2954
|
} catch {
|
|
2822
2955
|
source = void 0;
|
|
2823
2956
|
}
|
|
@@ -2844,6 +2977,8 @@ function scanProject(root) {
|
|
|
2844
2977
|
for (const row of rows) if (row.page && row.describedName) names.set(`GET ${row.info.path}`, row.describedName);
|
|
2845
2978
|
const suggestions = [];
|
|
2846
2979
|
const actions = scanActions(d, call, root, notes, suggestions);
|
|
2980
|
+
const gate = findProxyGate(root, d.mcpPath ?? "/mcp");
|
|
2981
|
+
if (gate) notes.push(`${proxyGateHint(gate)} setup --apply can make this change.`);
|
|
2847
2982
|
const routes = rows.map((row) => {
|
|
2848
2983
|
const key = `${row.info.method} ${row.info.path}`;
|
|
2849
2984
|
const decision = exposure.get(key);
|
|
@@ -2886,7 +3021,7 @@ function scanProject(root) {
|
|
|
2886
3021
|
} : {},
|
|
2887
3022
|
...call && d.mcpRoute ? {
|
|
2888
3023
|
aiReady: {
|
|
2889
|
-
site: { file:
|
|
3024
|
+
site: { file: join10(root, d.mcpRoute.file), line: call.line },
|
|
2890
3025
|
options: {
|
|
2891
3026
|
...call.expose ? { expose: call.expose } : {},
|
|
2892
3027
|
...call.exclude ? { exclude: call.exclude } : {},
|
|
@@ -2900,7 +3035,7 @@ function scanProject(root) {
|
|
|
2900
3035
|
}
|
|
2901
3036
|
function applyReplace(site, config, entries, usesZod, relFile) {
|
|
2902
3037
|
try {
|
|
2903
|
-
const before =
|
|
3038
|
+
const before = readFileSync10(site.file, "utf8");
|
|
2904
3039
|
const edit = replaceAiReadyOptions({
|
|
2905
3040
|
source: before,
|
|
2906
3041
|
file: site.file,
|
|
@@ -2919,8 +3054,8 @@ function applyReplace(site, config, entries, usesZod, relFile) {
|
|
|
2919
3054
|
}
|
|
2920
3055
|
|
|
2921
3056
|
// bin/test-sign-in.ts
|
|
2922
|
-
import { readFileSync as
|
|
2923
|
-
import { join as
|
|
3057
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
3058
|
+
import { join as join11 } from "path";
|
|
2924
3059
|
import { parseEnv } from "util";
|
|
2925
3060
|
import { supabaseCookieName, supabasePasswordLogin, supabaseSessionCookies } from "@exposify/core";
|
|
2926
3061
|
var TEST_SIGN_IN_USAGE = `Usage: @exposify/next test-sign-in [options]
|
|
@@ -2942,7 +3077,7 @@ environment, .env.local or .env. Signs the test session out afterwards.`;
|
|
|
2942
3077
|
function supabaseEnv(root, env) {
|
|
2943
3078
|
const files = [".env.local", ".env"].map((f) => {
|
|
2944
3079
|
try {
|
|
2945
|
-
return parseEnv(
|
|
3080
|
+
return parseEnv(readFileSync11(join11(root, f), "utf8"));
|
|
2946
3081
|
} catch {
|
|
2947
3082
|
return {};
|
|
2948
3083
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -444,9 +444,58 @@ function tokenize(html) {
|
|
|
444
444
|
if (last < html.length) out.push(html.slice(last));
|
|
445
445
|
return out;
|
|
446
446
|
}
|
|
447
|
+
function divEnd(html, from) {
|
|
448
|
+
const re = /<div\b(?:[^>"']|"[^"]*"|'[^']*')*>|<\/div\s*>/gi;
|
|
449
|
+
re.lastIndex = from;
|
|
450
|
+
let depth = 1;
|
|
451
|
+
for (let m = re.exec(html); m; m = re.exec(html)) {
|
|
452
|
+
depth += m[0][1] === "/" ? -1 : 1;
|
|
453
|
+
if (depth === 0) return re.lastIndex;
|
|
454
|
+
}
|
|
455
|
+
return html.length;
|
|
456
|
+
}
|
|
457
|
+
function boundaryEnd(html, from) {
|
|
458
|
+
const re = /<!--\$[?!]?-->|<!--\/\$-->/g;
|
|
459
|
+
re.lastIndex = from;
|
|
460
|
+
let depth = 1;
|
|
461
|
+
for (let m = re.exec(html); m; m = re.exec(html)) {
|
|
462
|
+
depth += m[0][4] === "/" ? -1 : 1;
|
|
463
|
+
if (depth === 0) return re.lastIndex;
|
|
464
|
+
}
|
|
465
|
+
return html.length;
|
|
466
|
+
}
|
|
467
|
+
function resolveStreamed(html) {
|
|
468
|
+
const chunks = [];
|
|
469
|
+
const open = /<div\s+hidden(?:="")?\s+id="S:([0-9a-f]+)"\s*>/g;
|
|
470
|
+
let out = "";
|
|
471
|
+
let last = 0;
|
|
472
|
+
for (let m = open.exec(html); m; m = open.exec(html)) {
|
|
473
|
+
const end = divEnd(html, open.lastIndex);
|
|
474
|
+
chunks.push({ id: m[1], content: html.slice(open.lastIndex, end).replace(/<\/div\s*>$/i, "") });
|
|
475
|
+
out += html.slice(last, m.index);
|
|
476
|
+
last = end;
|
|
477
|
+
open.lastIndex = end;
|
|
478
|
+
}
|
|
479
|
+
if (chunks.length === 0) return html;
|
|
480
|
+
out += html.slice(last);
|
|
481
|
+
for (let moved = true; moved; ) {
|
|
482
|
+
moved = false;
|
|
483
|
+
for (const c of chunks) {
|
|
484
|
+
const marker = `<template id="B:${c.id}"></template>`;
|
|
485
|
+
const at = out.indexOf(marker);
|
|
486
|
+
if (at < 0) continue;
|
|
487
|
+
const inBoundary = out.slice(Math.max(0, at - 9), at) === "<!--$?-->";
|
|
488
|
+
const start = inBoundary ? at - 9 : at;
|
|
489
|
+
const end = inBoundary ? boundaryEnd(out, at) : at + marker.length;
|
|
490
|
+
out = out.slice(0, start) + c.content + out.slice(end);
|
|
491
|
+
moved = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
447
496
|
var hidden = (t) => "hidden" in t.attrs || t.attrs["aria-hidden"] === "true" || /display\s*:\s*none|visibility\s*:\s*hidden/i.test(t.attrs.style ?? "") || t.name === "input" && t.attrs.type === "hidden";
|
|
448
497
|
function htmlToText(html) {
|
|
449
|
-
const tokens = tokenize(html);
|
|
498
|
+
const tokens = tokenize(resolveStreamed(html));
|
|
450
499
|
const hasMain = tokens.some((t) => typeof t !== "string" && t.name === "main" && !t.close);
|
|
451
500
|
const lines = [];
|
|
452
501
|
let line = "";
|
package/dist/index.js
CHANGED
|
@@ -454,9 +454,58 @@ function tokenize(html) {
|
|
|
454
454
|
if (last < html.length) out.push(html.slice(last));
|
|
455
455
|
return out;
|
|
456
456
|
}
|
|
457
|
+
function divEnd(html, from) {
|
|
458
|
+
const re = /<div\b(?:[^>"']|"[^"]*"|'[^']*')*>|<\/div\s*>/gi;
|
|
459
|
+
re.lastIndex = from;
|
|
460
|
+
let depth = 1;
|
|
461
|
+
for (let m = re.exec(html); m; m = re.exec(html)) {
|
|
462
|
+
depth += m[0][1] === "/" ? -1 : 1;
|
|
463
|
+
if (depth === 0) return re.lastIndex;
|
|
464
|
+
}
|
|
465
|
+
return html.length;
|
|
466
|
+
}
|
|
467
|
+
function boundaryEnd(html, from) {
|
|
468
|
+
const re = /<!--\$[?!]?-->|<!--\/\$-->/g;
|
|
469
|
+
re.lastIndex = from;
|
|
470
|
+
let depth = 1;
|
|
471
|
+
for (let m = re.exec(html); m; m = re.exec(html)) {
|
|
472
|
+
depth += m[0][4] === "/" ? -1 : 1;
|
|
473
|
+
if (depth === 0) return re.lastIndex;
|
|
474
|
+
}
|
|
475
|
+
return html.length;
|
|
476
|
+
}
|
|
477
|
+
function resolveStreamed(html) {
|
|
478
|
+
const chunks = [];
|
|
479
|
+
const open = /<div\s+hidden(?:="")?\s+id="S:([0-9a-f]+)"\s*>/g;
|
|
480
|
+
let out = "";
|
|
481
|
+
let last = 0;
|
|
482
|
+
for (let m = open.exec(html); m; m = open.exec(html)) {
|
|
483
|
+
const end = divEnd(html, open.lastIndex);
|
|
484
|
+
chunks.push({ id: m[1], content: html.slice(open.lastIndex, end).replace(/<\/div\s*>$/i, "") });
|
|
485
|
+
out += html.slice(last, m.index);
|
|
486
|
+
last = end;
|
|
487
|
+
open.lastIndex = end;
|
|
488
|
+
}
|
|
489
|
+
if (chunks.length === 0) return html;
|
|
490
|
+
out += html.slice(last);
|
|
491
|
+
for (let moved = true; moved; ) {
|
|
492
|
+
moved = false;
|
|
493
|
+
for (const c of chunks) {
|
|
494
|
+
const marker = `<template id="B:${c.id}"></template>`;
|
|
495
|
+
const at = out.indexOf(marker);
|
|
496
|
+
if (at < 0) continue;
|
|
497
|
+
const inBoundary = out.slice(Math.max(0, at - 9), at) === "<!--$?-->";
|
|
498
|
+
const start = inBoundary ? at - 9 : at;
|
|
499
|
+
const end = inBoundary ? boundaryEnd(out, at) : at + marker.length;
|
|
500
|
+
out = out.slice(0, start) + c.content + out.slice(end);
|
|
501
|
+
moved = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
457
506
|
var hidden = (t) => "hidden" in t.attrs || t.attrs["aria-hidden"] === "true" || /display\s*:\s*none|visibility\s*:\s*hidden/i.test(t.attrs.style ?? "") || t.name === "input" && t.attrs.type === "hidden";
|
|
458
507
|
function htmlToText(html) {
|
|
459
|
-
const tokens = tokenize(html);
|
|
508
|
+
const tokens = tokenize(resolveStreamed(html));
|
|
460
509
|
const hasMain = tokens.some((t) => typeof t !== "string" && t.name === "main" && !t.close);
|
|
461
510
|
const lines = [];
|
|
462
511
|
let line = "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@babel/parser": "^8.0.0",
|
|
35
|
-
"@exposify/cli": "0.0.
|
|
35
|
+
"@exposify/cli": "0.0.6",
|
|
36
36
|
"@exposify/core": "0.3.16",
|
|
37
37
|
"tinyglobby": "^0.2.10",
|
|
38
38
|
"zod": "^3.24.0"
|