dh-remixer-sdk 0.0.17 → 0.0.18-aaf644b

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/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "dh-remixer-sdk",
3
- "version": "0.0.17",
3
+ "version": "0.0.18-aaf644b",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "templates",
7
7
  "scripts",
8
8
  "bin"
9
9
  ],
10
+ "bin": {
11
+ "ssg-helmet": "./ssg-helmet/index.mjs"
12
+ },
10
13
  "scripts": {
11
- "postinstall": "node scripts/postinstall.mjs"
14
+ "postinstall": "node scripts/postinstall/index.mjs"
12
15
  }
13
16
  }
@@ -95,15 +95,26 @@ async function main() {
95
95
  const projectRoot = path.resolve(process.env.INIT_CWD ?? process.cwd());
96
96
  const projPkgPath = path.join(projectRoot, "package.json");
97
97
  const projPkg = JSON.parse(await fs.readFile(projPkgPath, "utf8"));
98
- const templateType = projPkg?.remixerMetadata?.template;
98
+
99
+ if (!projPkg) {
100
+ console.error(
101
+ `Cannot find package.json at ${projPkgPath}\nContents: ${JSON.stringify(projPkg)}`,
102
+ );
103
+ return;
104
+ }
105
+
106
+ const templateType = projPkg.remixerMetadata?.template;
107
+
99
108
  if (!templateType) {
100
- console.warn("Missing remixerMetadata.template in package.json");
109
+ console.error(
110
+ `Missing remixerMetadata.template in package.json\nContents: ${JSON.stringify(projPkg)}`,
111
+ );
101
112
  return;
102
113
  }
103
114
 
104
115
  const sdkRoot = path.resolve(
105
116
  path.dirname(fileURLToPath(import.meta.url)),
106
- "..",
117
+ "../..",
107
118
  );
108
119
  const sdkPkgPath = path.join(sdkRoot, "package.json");
109
120
  const sdkVersion = JSON.parse(await fs.readFile(sdkPkgPath, "utf8")).version;
@@ -158,4 +169,3 @@ async function main() {
158
169
  }
159
170
 
160
171
  await main();
161
-
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Simplified redirect-helmet builder:
3
+ * - Extracts metadata from prerenderedHtml (<title>, <meta>, <link>, <base>, ld+json)
4
+ * - Normalizes into a single "meta bag" with common SEO keys
5
+ * - Fills missing fields from available data (og/title/twitter propagation, canonical, og:url, etc.)
6
+ * - Preserves existing application/ld+json scripts; if none exist AND indexable, emits fallback WebPage JSON-LD
7
+ * - Forces robots to "noindex, follow" when isIndexable=false
8
+ * - Ensures <html lang="en">
9
+ * - Canonical is DOMAIN + route (3rd param), not window.location
10
+ * - Avoids double-escaping (&amp;amp;) by decoding extracted text before output
11
+ * - Ensures JSON-LD contains real characters (e.g. "&" not "&amp;")
12
+ */
13
+
14
+ const DOMAIN = "example.com";
15
+
16
+ export function buildRedirectHelmetHtml(
17
+ prerenderedHtml,
18
+ route = "/",
19
+ isIndexable = true,
20
+ ) {
21
+ if (typeof prerenderedHtml !== "string") {
22
+ throw new TypeError("prerenderedHtml must be a string");
23
+ }
24
+
25
+ // --- helpers --------------------------------------------------------------
26
+ const pick = (re, s) => s.match(re)?.[1] ?? "";
27
+
28
+ const escapeAttr = (s) =>
29
+ String(s)
30
+ .replace(/&/g, "&amp;")
31
+ .replace(/"/g, "&quot;")
32
+ .replace(/</g, "&lt;")
33
+ .replace(/>/g, "&gt;");
34
+
35
+ // Decode a few common HTML entities so we don't double-escape values
36
+ // (We only use this on text we extracted from existing HTML.)
37
+ const decodeHtml = (s) =>
38
+ String(s ?? "")
39
+ .replace(/&amp;/gi, "&")
40
+ .replace(/&quot;/gi, '"')
41
+ .replace(/&#39;/gi, "'")
42
+ .replace(/&lt;/gi, "<")
43
+ .replace(/&gt;/gi, ">");
44
+
45
+ // Prevent JSON containing "</script>" from breaking the script tag
46
+ const safeJson = (obj) =>
47
+ JSON.stringify(obj).replace(/<\/script>/gi, "<\\/script>");
48
+
49
+ const parseAttrs = (tag) => {
50
+ const attrs = {};
51
+ const re = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s/>]+)))?/g;
52
+ let m;
53
+ while ((m = re.exec(tag))) {
54
+ const k = m[1].toLowerCase();
55
+ const v = m[2] ?? m[3] ?? m[4] ?? "";
56
+ attrs[k] = v;
57
+ }
58
+ return attrs;
59
+ };
60
+
61
+ const setFirst = (obj, k, v) => {
62
+ if (v == null) return;
63
+ const s = String(v).trim();
64
+ if (!s) return;
65
+ if (!obj[k]) obj[k] = s;
66
+ };
67
+
68
+ const setForce = (obj, k, v) => {
69
+ const s = String(v ?? "").trim();
70
+ if (!s) return;
71
+ obj[k] = s;
72
+ };
73
+
74
+ const normalizeDomain = (d) => {
75
+ const s = String(d || "").trim();
76
+ if (!s) return "";
77
+ if (/^https?:\/\//i.test(s)) return s.replace(/\/+$/, "");
78
+ return `https://${s.replace(/\/+$/, "")}`;
79
+ };
80
+
81
+ const normalizeRoute = (r) => {
82
+ let p = String(r || "/").trim();
83
+ if (!p) p = "/";
84
+ if (!p.startsWith("/")) p = "/" + p;
85
+ // strip query/hash, normalize trailing slash
86
+ p = p.split("?")[0].split("#")[0];
87
+ p = p.replace(/\/+$/, "") || "/";
88
+ return p;
89
+ };
90
+
91
+ const canonicalFromDomainRoute = () => normalizeDomain(DOMAIN) + normalizeRoute(route);
92
+
93
+ // --- extract head ---------------------------------------------------------
94
+ const lang = "en";
95
+
96
+ const headInner = pick(/<head\b[^>]*>([\s\S]*?)<\/head>/i, prerenderedHtml);
97
+
98
+ // decode to avoid double-escape on output
99
+ const titleText = decodeHtml(
100
+ pick(/<title\b[^>]*>([\s\S]*?)<\/title>/i, headInner).trim(),
101
+ );
102
+
103
+ const metaTags = headInner.match(/<meta\b[^>]*>/gi) || [];
104
+ const linkTags = headInner.match(/<link\b[^>]*>/gi) || [];
105
+ const baseTags = headInner.match(/<base\b[^>]*>/gi) || [];
106
+ const ldJsonScripts =
107
+ headInner.match(
108
+ /<script\b[^>]*type\s*=\s*(['"])application\/ld\+json\1[^>]*>[\s\S]*?<\/script>/gi,
109
+ ) || [];
110
+
111
+ // --- build meta bag -------------------------------------------------------
112
+ const meta = {
113
+ title: "",
114
+ description: "",
115
+ robots: "",
116
+ canonical: "",
117
+ charset: "",
118
+ viewport: "",
119
+ themeColor: "",
120
+
121
+ "og:title": "",
122
+ "og:description": "",
123
+ "og:image": "",
124
+ "og:image:alt": "",
125
+ "og:type": "",
126
+ "og:url": "",
127
+ "og:site_name": "",
128
+ "og:locale": "",
129
+
130
+ "twitter:card": "",
131
+ "twitter:title": "",
132
+ "twitter:description": "",
133
+ "twitter:image": "",
134
+ "twitter:image:alt": "",
135
+ "twitter:site": "",
136
+ "twitter:creator": "",
137
+
138
+ "application-name": "",
139
+ "apple-mobile-web-app-title": "",
140
+ "apple-mobile-web-app-capable": "",
141
+ "apple-mobile-web-app-status-bar-style": "",
142
+ "format-detection": "",
143
+ };
144
+
145
+ setFirst(meta, "title", titleText);
146
+
147
+ // metas
148
+ for (const t of metaTags) {
149
+ const a = parseAttrs(t);
150
+
151
+ if (a.charset) setFirst(meta, "charset", decodeHtml(a.charset));
152
+
153
+ const name = (a.name || "").toLowerCase();
154
+ const prop = (a.property || "").toLowerCase();
155
+ const httpEquiv = (a["http-equiv"] || "").toLowerCase();
156
+ const content = decodeHtml(a.content ?? "");
157
+
158
+ if (name === "description") setFirst(meta, "description", content);
159
+ else if (name === "robots") setFirst(meta, "robots", content);
160
+ else if (name === "viewport") setFirst(meta, "viewport", content);
161
+ else if (name === "theme-color") setFirst(meta, "themeColor", content);
162
+ else if (name && name in meta) setFirst(meta, name, content);
163
+
164
+ if (prop && prop in meta) setFirst(meta, prop, content);
165
+
166
+ if (httpEquiv === "content-language") setFirst(meta, "og:locale", content);
167
+ }
168
+
169
+ // canonical from <link rel="canonical"...> if present in source
170
+ for (const t of linkTags) {
171
+ const a = parseAttrs(t);
172
+ const rel = (a.rel || "").toLowerCase();
173
+ if (rel === "canonical" && a.href) setFirst(meta, "canonical", decodeHtml(a.href));
174
+ }
175
+
176
+ // --- fill fallbacks -------------------------------------------------------
177
+ setFirst(meta, "og:title", meta.title);
178
+ setFirst(meta, "og:description", meta.description);
179
+
180
+ // If canonical is set (or will be), use it for og:url
181
+ // (We'll set canonical later if missing.)
182
+ setFirst(meta, "twitter:title", meta["og:title"] || meta.title);
183
+ setFirst(meta, "twitter:description", meta["og:description"] || meta.description);
184
+
185
+ // image propagation
186
+ setFirst(meta, "twitter:image", meta["og:image"]);
187
+ setFirst(meta, "twitter:image:alt", meta["og:image:alt"]);
188
+ setFirst(meta, "og:image:alt", meta["twitter:image:alt"]);
189
+
190
+ // twitter card default
191
+ setFirst(
192
+ meta,
193
+ "twitter:card",
194
+ meta["twitter:image"] ? "summary_large_image" : "summary",
195
+ );
196
+
197
+ // --- canonical: always DOMAIN + route unless source provided one ----------
198
+ if (!meta.canonical) {
199
+ meta.canonical = canonicalFromDomainRoute();
200
+ }
201
+
202
+ // --- og:url: set to canonical if missing ---------------------------------
203
+ if (!meta["og:url"]) {
204
+ meta["og:url"] = meta.canonical;
205
+ }
206
+
207
+ // --- enforce indexability -------------------------------------------------
208
+ if (!isIndexable) {
209
+ setForce(meta, "robots", "noindex, follow");
210
+ }
211
+
212
+ // --- emit tags ------------------------------------------------------------
213
+ const out = [];
214
+
215
+ out.push(`<meta charset="${escapeAttr(meta.charset || "utf-8")}" />`);
216
+
217
+ if (meta.viewport)
218
+ out.push(`<meta name="viewport" content="${escapeAttr(meta.viewport)}" />`);
219
+
220
+ if (meta.robots)
221
+ out.push(`<meta name="robots" content="${escapeAttr(meta.robots)}" />`);
222
+
223
+ if (meta.description)
224
+ out.push(`<meta name="description" content="${escapeAttr(meta.description)}" />`);
225
+
226
+ if (meta.themeColor)
227
+ out.push(`<meta name="theme-color" content="${escapeAttr(meta.themeColor)}" />`);
228
+
229
+ if (meta["application-name"])
230
+ out.push(
231
+ `<meta name="application-name" content="${escapeAttr(meta["application-name"])}" />`,
232
+ );
233
+
234
+ if (meta.title) out.push(`<title>${escapeAttr(meta.title)}</title>`);
235
+
236
+ if (meta.canonical)
237
+ out.push(`<link rel="canonical" href="${escapeAttr(meta.canonical)}" />`);
238
+
239
+ for (const k of [
240
+ "og:title",
241
+ "og:description",
242
+ "og:image",
243
+ "og:image:alt",
244
+ "og:type",
245
+ "og:url",
246
+ "og:site_name",
247
+ "og:locale",
248
+ ]) {
249
+ if (meta[k]) out.push(`<meta property="${k}" content="${escapeAttr(meta[k])}" />`);
250
+ }
251
+
252
+ for (const k of [
253
+ "twitter:card",
254
+ "twitter:title",
255
+ "twitter:description",
256
+ "twitter:image",
257
+ "twitter:image:alt",
258
+ "twitter:site",
259
+ "twitter:creator",
260
+ ]) {
261
+ if (meta[k]) out.push(`<meta name="${k}" content="${escapeAttr(meta[k])}" />`);
262
+ }
263
+
264
+ // keep original link/base tags too (favicons, preconnect, etc.)
265
+ // NOTE: These tags may include &amp; already; but they're raw tags and should be kept as-is.
266
+ out.push(...linkTags);
267
+ out.push(...baseTags);
268
+
269
+ // --- JSON-LD: preserve existing; else generate fallback -------------------
270
+ if (ldJsonScripts.length) {
271
+ out.push(...ldJsonScripts);
272
+ } else if (isIndexable) {
273
+ // IMPORTANT: JSON-LD strings must NOT contain HTML entities.
274
+ const fallbackLdJson = {
275
+ "@context": "https://schema.org",
276
+ "@type": "WebPage",
277
+ name: meta.title || undefined,
278
+ description: meta.description || undefined,
279
+ url: meta.canonical || undefined,
280
+ inLanguage: "en",
281
+ };
282
+
283
+ for (const k of Object.keys(fallbackLdJson)) {
284
+ if (fallbackLdJson[k] === undefined) delete fallbackLdJson[k];
285
+ }
286
+
287
+ out.push(
288
+ `<script type="application/ld+json">${safeJson(fallbackLdJson)}</script>`,
289
+ );
290
+ }
291
+
292
+ // --- redirect script ------------------------------------------------------
293
+ out.push(
294
+ `<script type="text/javascript">
295
+ (function () {
296
+ var botPattern =
297
+ "(googlebot\\\\/|bot|bingbot|slurp|DuckDuckBot|baiduspider|yandexbot|facebookexternalhit|twitterbot|linkedinbot|pinterest|slackbot|discordbot|whatsapp|telegrambot|embedly|quora link preview|applebot|semrushbot|ahrefsbot|mj12bot|dotbot)";
298
+ var re = new RegExp(botPattern, "i");
299
+ var ua = navigator.userAgent;
300
+
301
+ if (!re.test(ua)) {
302
+ var l = window.location;
303
+ l.replace(
304
+ l.protocol +
305
+ "//" +
306
+ l.hostname +
307
+ (l.port ? ":" + l.port : "") +
308
+ "/?p=/" +
309
+ l.pathname.slice(1).replace(/&/g, "~and~") +
310
+ (l.search ? "&q=" + l.search.slice(1).replace(/&/g, "~and~") : "") +
311
+ l.hash
312
+ );
313
+ }
314
+ })();
315
+ </script>`,
316
+ );
317
+
318
+ return `<!doctype html>
319
+ <html lang="${escapeAttr(lang)}">
320
+ <head>
321
+ ${out.filter(Boolean).join("\n")}
322
+ </head>
323
+ <body></body>
324
+ </html>`;
325
+ }
@@ -0,0 +1,75 @@
1
+ const PRIORITY_HOME = 1.0;
2
+ const PRIORITY_NORMAL = 0.8;
3
+ const PRIORITY_TEMPLATED = 0.6;
4
+
5
+ const CHANGEFREQ_HOME = "daily";
6
+ const CHANGEFREQ_NORMAL = "weekly";
7
+ const CHANGEFREQ_TEMPLATED = "weekly";
8
+
9
+ export function buildSitemapXml(origin, routes) {
10
+ if (!/^https?:\/\//i.test(origin)) origin = "https://" + origin;
11
+ const cleanOrigin = origin.replace(/\/+$/, "");
12
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
13
+
14
+ const abs = (route) => `${cleanOrigin}${route === "/" ? "/" : route}`;
15
+ const esc = (s) =>
16
+ String(s)
17
+ .replace(/&/g, "&amp;")
18
+ .replace(/</g, "&lt;")
19
+ .replace(/>/g, "&gt;")
20
+ .replace(/"/g, "&quot;")
21
+ .replace(/'/g, "&apos;");
22
+
23
+ const entries = [];
24
+ for (const item of routes) {
25
+ if (typeof item === "string") {
26
+ entries.push({ path: item, kind: item === "/" ? "home" : "normal" });
27
+ } else if (item && typeof item === "object") {
28
+ for (const concrete of Object.values(item)) {
29
+ if (!Array.isArray(concrete)) continue;
30
+ for (const r of concrete) entries.push({ path: r, kind: "templated" });
31
+ }
32
+ }
33
+ }
34
+
35
+ const rank = { home: 3, normal: 2, templated: 1 };
36
+ const byPath = new Map();
37
+ for (const e of entries) {
38
+ const prev = byPath.get(e.path);
39
+ if (!prev || rank[e.kind] > rank[prev.kind]) byPath.set(e.path, e);
40
+ }
41
+
42
+ const ordered = Array.from(byPath.values()).sort((a, b) => {
43
+ const rk = rank[b.kind] - rank[a.kind];
44
+ return rk || a.path.localeCompare(b.path);
45
+ });
46
+
47
+ const node = ({ path, kind }) => {
48
+ const priority =
49
+ kind === "home"
50
+ ? PRIORITY_HOME
51
+ : kind === "templated"
52
+ ? PRIORITY_TEMPLATED
53
+ : PRIORITY_NORMAL;
54
+
55
+ const changefreq =
56
+ kind === "home"
57
+ ? CHANGEFREQ_HOME
58
+ : kind === "templated"
59
+ ? CHANGEFREQ_TEMPLATED
60
+ : CHANGEFREQ_NORMAL;
61
+
62
+ return ` <url>
63
+ <loc>${esc(abs(path))}</loc>
64
+ <lastmod>${today}</lastmod>
65
+ <changefreq>${changefreq}</changefreq>
66
+ <priority>${priority.toFixed(1)}</priority>
67
+ </url>`;
68
+ };
69
+
70
+ return `<?xml version="1.0" encoding="UTF-8"?>
71
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
72
+ ${ordered.map(node).join("\n")}
73
+ </urlset>
74
+ `;
75
+ }
@@ -0,0 +1,82 @@
1
+ // extract-routes.mjs
2
+ import fs from "node:fs/promises";
3
+ import * as acorn from "acorn";
4
+ import jsx from "acorn-jsx";
5
+ import { transform } from "sucrase";
6
+
7
+ const Parser = acorn.Parser.extend(jsx());
8
+
9
+ const tagName = (n) =>
10
+ n?.type === "JSXIdentifier" ? n.name : n?.type === "JSXMemberExpression" ? n.property?.name : null;
11
+
12
+ const attrName = (a) => (a?.name?.type === "JSXIdentifier" ? a.name.name : null);
13
+
14
+ const strVal = (v) => {
15
+ if (!v) return null;
16
+ if (v.type === "Literal" && typeof v.value === "string") return v.value;
17
+ if (v.type === "JSXExpressionContainer") {
18
+ const e = v.expression;
19
+ if (e?.type === "Literal" && typeof e.value === "string") return e.value;
20
+ if (e?.type === "TemplateLiteral" && (e.expressions?.length ?? 0) === 0)
21
+ return e.quasis?.[0]?.value?.cooked ?? null;
22
+ }
23
+ return null;
24
+ };
25
+
26
+ const walk = (n, f) => {
27
+ if (!n || typeof n !== "object") return;
28
+ f(n);
29
+ for (const k in n) {
30
+ const v = n[k];
31
+ if (Array.isArray(v)) v.forEach((x) => walk(x, f));
32
+ else if (v && typeof v.type === "string") walk(v, f);
33
+ }
34
+ };
35
+
36
+ const isTemplatedRoute = (route) => route.split("/").some((seg) => seg.startsWith(":"));
37
+
38
+ /**
39
+ * Returns routes as:
40
+ * - strings for normal routes
41
+ * - at most ONE object collecting templated routes: { "page/:id": [], "x/:slug": [] }
42
+ *
43
+ * Examples:
44
+ * ["/", "/about"]
45
+ * ["/", "/about", { "product/:id": [], "blog/:slug": [] }]
46
+ */
47
+ export async function extractRoutesFromRouterFile(filePath) {
48
+ const src = await fs.readFile(filePath, "utf8");
49
+ let code = src;
50
+ try {
51
+ code = transform(src, { transforms: ["typescript"] }).code;
52
+ } catch {}
53
+
54
+ const ast = Parser.parse(code, { ecmaVersion: "latest", sourceType: "module" });
55
+
56
+ const all = new Set();
57
+
58
+ walk(ast, (node) => {
59
+ if (node.type !== "JSXOpeningElement") return;
60
+ if (tagName(node.name) !== "Route") return;
61
+
62
+ for (const a of node.attributes ?? []) {
63
+ if (a.type !== "JSXAttribute") continue;
64
+ if (attrName(a) !== "path") continue;
65
+ const p = strVal(a.value);
66
+ if (p) all.add(p);
67
+ }
68
+ });
69
+
70
+ const routes = all.size ? [...all] : ["/"];
71
+
72
+ const normal = [];
73
+ const templated = {};
74
+
75
+ for (const r of routes) {
76
+ if (typeof r !== "string") continue;
77
+ if (isTemplatedRoute(r)) templated[r] = [];
78
+ else normal.push(r);
79
+ }
80
+
81
+ return Object.keys(templated).length ? [...normal, templated] : normal;
82
+ }
@@ -0,0 +1,26 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "path";
3
+ import { injectUnpackRedirectScript } from "./inject-unpack-redirect.mjs";
4
+ import { buildRedirectHelmetHtml } from "./build-helmet-html.mjs";
5
+
6
+ const ROOT = "/";
7
+ const OUT = path.resolve("./out");
8
+
9
+ async function writeFile(route, content) {
10
+ const productOut = path.join(OUT, route, "index.html");
11
+ await fs.mkdir(path.dirname(productOut), { recursive: true });
12
+ await fs.writeFile(productOut, content, "utf8");
13
+ }
14
+
15
+ export async function writeHelmetFile(html, currentRoute, isIndexable) {
16
+ const idxTag = isIndexable ? "indexable" : "not_indexable";
17
+ if (currentRoute === ROOT) {
18
+ await injectUnpackRedirectScript("out");
19
+ console.log(`[crawled][${idxTag}][edited] ${currentRoute}index.html`);
20
+ return;
21
+ }
22
+
23
+ const helmetHtml = buildRedirectHelmetHtml(html, currentRoute, isIndexable);
24
+ await writeFile(currentRoute, helmetHtml);
25
+ console.log(`[crawled][${idxTag}][created] ${currentRoute}/index.html`);
26
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { hasRouter } from "./has-router.mjs";
4
+ import { extractRoutesFromRouterFile } from "./extract-routes.mjs";
5
+
6
+ const SKIP_DIRS = new Set([
7
+ "node_modules",
8
+ ".git",
9
+ "dist",
10
+ "build",
11
+ "out",
12
+ ".next",
13
+ ]);
14
+
15
+ async function getRouterFilePath(dir = "./") {
16
+ const entries = await fs.readdir(dir, { withFileTypes: true });
17
+
18
+ for (const e of entries) {
19
+ if (!e.isFile() || !e.name.endsWith(".tsx")) continue;
20
+
21
+ const filePath = path.join(dir, e.name);
22
+ const content = await fs.readFile(filePath, "utf8");
23
+ if (hasRouter(content)) return filePath;
24
+ }
25
+
26
+ for (const e of entries) {
27
+ if (!e.isDirectory()) continue;
28
+ if (SKIP_DIRS.has(e.name)) continue;
29
+
30
+ const filePath = await getRouterFilePath(path.join(dir, e.name));
31
+ if (filePath) return filePath;
32
+ }
33
+
34
+ return undefined;
35
+ }
36
+
37
+ export async function getRoutes() {
38
+ const routerFilePath = await getRouterFilePath();
39
+ if (!routerFilePath) return ["/"];
40
+
41
+ return await extractRoutesFromRouterFile(routerFilePath);
42
+ }
43
+
44
+ export function routeDifference(A, B) {
45
+ const flat = (arr) =>
46
+ arr.flatMap((x) => (typeof x === "string" ? x : Object.values(x).flat()));
47
+
48
+ const ARoutes = flat(A);
49
+ const BRoutes = flat(B);
50
+
51
+ return ARoutes.filter((route) => !BRoutes.includes(route));
52
+ }