fimo 0.9.0-experimental.7 → 0.9.0-experimental.8
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/assets/skills/fimo-migration/references/pages.md +140 -1
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +335 -173
- package/dist/runtime/app/FimoProviders.js +29 -7
- package/dist/runtime/react/FimoSpaProviders.js +18 -1
- package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
- package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
- package/dist/runtime/wrapped/server.d.ts +5 -2
- package/dist/runtime/wrapped/server.js +15 -45
- package/dist/scripts/apply-wrap-templates.js +1 -1
- package/dist/scripts/apply-wrap.js +26 -2
- package/dist/scripts/cms/strapi/wiring.js +1 -1
- package/dist/scripts/detect-framework.js +6 -0
- package/dist/scripts/draft-rows.js +36 -0
- package/dist/scripts/frameworks/astro/routes.js +2 -18
- package/dist/scripts/frameworks/discover.d.ts +19 -6
- package/dist/scripts/frameworks/discover.js +96 -3
- package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
- package/dist/scripts/frameworks/discovered-route.js +17 -0
- package/dist/scripts/frameworks/next/routes.d.ts +7 -0
- package/dist/scripts/frameworks/next/routes.js +31 -12
- package/dist/scripts/frameworks/nuxt/routes.js +2 -18
- package/dist/scripts/frameworks/remix/routes.js +4 -18
- package/dist/scripts/frameworks/sveltekit/routes.js +4 -19
- package/dist/scripts/frameworks/vite/routes.d.ts +5 -0
- package/dist/scripts/frameworks/vite/routes.js +59 -9
- package/dist/scripts/frameworks/vite/wiring.d.ts +12 -0
- package/dist/scripts/frameworks/vite/wiring.js +68 -23
- package/dist/scripts/lib/humanize.d.ts +14 -0
- package/dist/scripts/lib/humanize.js +54 -0
- package/dist/scripts/render-parity/capture.d.ts +7 -0
- package/dist/scripts/render-parity/capture.js +32 -4
- package/dist/scripts/routes/label-prefixes.d.ts +49 -0
- package/dist/scripts/routes/label-prefixes.js +136 -0
- package/dist/scripts/routes/module-resolve.d.ts +20 -0
- package/dist/scripts/routes/module-resolve.js +78 -0
- package/dist/scripts/routes/page-structure.d.ts +72 -0
- package/dist/scripts/routes/page-structure.js +234 -0
- package/dist/scripts/scan-wrappable.d.ts +4 -0
- package/dist/scripts/scan-wrappable.js +15 -2
- package/package.json +1 -1
- package/templates/react-router/package.json +1 -1
- package/dist/scripts/lib/discover-next-routes.d.ts +0 -17
- package/dist/scripts/lib/discover-next-routes.js +0 -134
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { frameworkSectionStyle } from '../frameworks/discover.js';
|
|
4
|
+
import { componentImports, resolveModuleFile, resolveRouteFiles } from './label-prefixes.js';
|
|
5
|
+
const MAX_SECTION_DEPTH = 3;
|
|
6
|
+
const MAX_DRAFT_ROWS = 50;
|
|
7
|
+
const pascal = (value) => value
|
|
8
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
9
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
10
|
+
.split(/[-_.\s]/)
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
13
|
+
.join('');
|
|
14
|
+
const camel = (value) => {
|
|
15
|
+
const p = pascal(value);
|
|
16
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
17
|
+
};
|
|
18
|
+
/** Capitalized JSX tags in DOCUMENT order (deduped, first appearance). */
|
|
19
|
+
function sectionTags(code) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const tags = [];
|
|
22
|
+
for (const match of code.matchAll(/<([A-Z]\w*)[\s/>]/g)) {
|
|
23
|
+
if (!seen.has(match[1])) {
|
|
24
|
+
seen.add(match[1]);
|
|
25
|
+
tags.push(match[1]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return tags;
|
|
29
|
+
}
|
|
30
|
+
function draftSection(context, tag, file, depth) {
|
|
31
|
+
const rel = relative(context.cwd, file);
|
|
32
|
+
const prefix = context.style.fileScope(rel);
|
|
33
|
+
const labelKeys = context.keysByPrefix.get(prefix) ?? [];
|
|
34
|
+
const collections = (context.collectionsByFile.get(rel) ?? []).map((candidate) => ({
|
|
35
|
+
name: candidate.name,
|
|
36
|
+
fields: candidate.fields,
|
|
37
|
+
count: candidate.count,
|
|
38
|
+
...(candidate.typedRows ? { rows: candidate.typedRows.slice(0, MAX_DRAFT_ROWS) } : {}),
|
|
39
|
+
}));
|
|
40
|
+
const children = depth < MAX_SECTION_DEPTH ? draftChildSections(context, file, depth + 1) : [];
|
|
41
|
+
if (labelKeys.length === 0 && collections.length === 0 && children.length === 0) {
|
|
42
|
+
return null; // pure layout — nothing to model
|
|
43
|
+
}
|
|
44
|
+
const draft = { key: camel(tag), component: tag, file: rel, prefix, labelKeys, collections };
|
|
45
|
+
if (children.length > 0) {
|
|
46
|
+
draft.children = children;
|
|
47
|
+
}
|
|
48
|
+
return draft;
|
|
49
|
+
}
|
|
50
|
+
/** Auto-imported components (Nuxt's components/ convention): a tag with no
|
|
51
|
+
* import statement may still be a local file at a conventional path. */
|
|
52
|
+
function resolveAutoImportedComponent(context, tag) {
|
|
53
|
+
for (const dir of context.style.fallbackComponentDirs) {
|
|
54
|
+
for (const ext of context.style.extensions) {
|
|
55
|
+
const candidate = join(context.cwd, dir, `${tag}${ext}`);
|
|
56
|
+
try {
|
|
57
|
+
if (statSync(candidate).isFile()) {
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* try next */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function draftChildSections(context, file, depth) {
|
|
69
|
+
let code;
|
|
70
|
+
try {
|
|
71
|
+
code = readFileSync(file, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const imports = componentImports(code);
|
|
77
|
+
const sections = [];
|
|
78
|
+
for (const tag of sectionTags(code)) {
|
|
79
|
+
const spec = imports.get(tag);
|
|
80
|
+
const sectionFile = spec
|
|
81
|
+
? resolveModuleFile(context.cwd, file, spec, context.style.extensions)
|
|
82
|
+
: resolveAutoImportedComponent(context, tag);
|
|
83
|
+
if (!sectionFile || context.visited.has(sectionFile)) {
|
|
84
|
+
continue; // library components (Button, Link) have no local file
|
|
85
|
+
}
|
|
86
|
+
context.visited.add(sectionFile);
|
|
87
|
+
const section = draftSection(context, tag, sectionFile, depth);
|
|
88
|
+
if (section) {
|
|
89
|
+
sections.push(section);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return sections;
|
|
93
|
+
}
|
|
94
|
+
export function draftPageStructures(args) {
|
|
95
|
+
const { cwd, framework, routes, collections } = args;
|
|
96
|
+
const keysByPrefix = new Map();
|
|
97
|
+
for (const key of args.stagedKeys) {
|
|
98
|
+
const dot = key.indexOf('.');
|
|
99
|
+
if (dot > 0) {
|
|
100
|
+
const prefix = key.slice(0, dot);
|
|
101
|
+
keysByPrefix.set(prefix, [...(keysByPrefix.get(prefix) ?? []), key]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const collectionsByFile = new Map();
|
|
105
|
+
for (const candidate of collections) {
|
|
106
|
+
collectionsByFile.set(candidate.file, [...(collectionsByFile.get(candidate.file) ?? []), candidate]);
|
|
107
|
+
}
|
|
108
|
+
const style = frameworkSectionStyle(framework);
|
|
109
|
+
const drafts = [];
|
|
110
|
+
for (const { routeId, file } of resolveRouteFiles(cwd, framework, routes)) {
|
|
111
|
+
const context = { cwd, style, keysByPrefix, collectionsByFile, visited: new Set([file]) };
|
|
112
|
+
const sections = draftChildSections(context, file, 1);
|
|
113
|
+
if (sections.length > 1) {
|
|
114
|
+
drafts.push({ pageId: routeId, single: pascal(routeId), sections });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
markSharedSections(drafts);
|
|
118
|
+
return drafts;
|
|
119
|
+
}
|
|
120
|
+
/** A section file drafted on several pages is chrome — flag it so the agent
|
|
121
|
+
* models it once (Single Type) instead of nesting it under every page. */
|
|
122
|
+
function markSharedSections(drafts) {
|
|
123
|
+
const pagesByFile = new Map();
|
|
124
|
+
const index = (pageId, sections) => {
|
|
125
|
+
for (const section of sections) {
|
|
126
|
+
pagesByFile.set(section.file, (pagesByFile.get(section.file) ?? new Set()).add(pageId));
|
|
127
|
+
index(pageId, section.children ?? []);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
for (const draft of drafts) {
|
|
131
|
+
index(draft.pageId, draft.sections);
|
|
132
|
+
}
|
|
133
|
+
const flag = (sections) => {
|
|
134
|
+
for (const section of sections) {
|
|
135
|
+
if ((pagesByFile.get(section.file)?.size ?? 0) > 1) {
|
|
136
|
+
section.shared = true;
|
|
137
|
+
}
|
|
138
|
+
flag(section.children ?? []);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
for (const draft of drafts) {
|
|
142
|
+
flag(draft.sections);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Flat section count across the nesting — the summary's visibility number. */
|
|
146
|
+
export function countDraftSections(draft) {
|
|
147
|
+
const walk = (sections) => sections.reduce((total, section) => total + 1 + walk(section.children ?? []), 0);
|
|
148
|
+
return walk(draft.sections);
|
|
149
|
+
}
|
|
150
|
+
const labelFieldName = (key) => camel(key.split('.').pop() ?? key);
|
|
151
|
+
const scalarType = (value) => value.length > 80 || value.includes('\n') ? 'text' : 'string';
|
|
152
|
+
function sectionBlock(section, labelValues) {
|
|
153
|
+
const fields = {};
|
|
154
|
+
const value = {};
|
|
155
|
+
let fieldCount = 0;
|
|
156
|
+
for (const key of section.labelKeys) {
|
|
157
|
+
const name = labelFieldName(key);
|
|
158
|
+
if (fields[name]) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const labelValue = labelValues[key] ?? '';
|
|
162
|
+
fields[name] = { type: scalarType(labelValue) };
|
|
163
|
+
value[name] = labelValue;
|
|
164
|
+
fieldCount += 1;
|
|
165
|
+
}
|
|
166
|
+
for (const collection of section.collections) {
|
|
167
|
+
const name = camel(collection.name);
|
|
168
|
+
if (fields[name]) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const itemFields = {};
|
|
172
|
+
for (const field of collection.fields) {
|
|
173
|
+
itemFields[field] = { type: 'string' };
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(itemFields).length === 0) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
fields[name] = { type: 'blocks', schema: { fields: itemFields } };
|
|
179
|
+
value[name] = (collection.rows ?? []).map((row) => {
|
|
180
|
+
const item = {};
|
|
181
|
+
for (const field of collection.fields) {
|
|
182
|
+
const cell = row[field];
|
|
183
|
+
if (cell != null && typeof cell !== 'object') {
|
|
184
|
+
item[field] = typeof cell === 'string' ? cell : String(cell);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return item;
|
|
188
|
+
});
|
|
189
|
+
fieldCount += 1;
|
|
190
|
+
}
|
|
191
|
+
for (const child of section.children ?? []) {
|
|
192
|
+
if (child.shared) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const block = sectionBlock(child, labelValues);
|
|
196
|
+
if (!block || fields[child.key]) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
fields[child.key] = { type: 'blocks', max: 1, schema: { fields: block.fields } };
|
|
200
|
+
value[child.key] = [block.value];
|
|
201
|
+
fieldCount += block.fieldCount;
|
|
202
|
+
}
|
|
203
|
+
if (Object.keys(fields).length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return { fields, value, fieldCount };
|
|
207
|
+
}
|
|
208
|
+
export function buildPageSingleDraft(draft, labelValues) {
|
|
209
|
+
const fields = {};
|
|
210
|
+
const document = {};
|
|
211
|
+
let totalFields = 0;
|
|
212
|
+
for (const section of draft.sections) {
|
|
213
|
+
if (section.shared || fields[section.key]) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const block = sectionBlock(section, labelValues);
|
|
217
|
+
if (!block) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
fields[section.key] = { type: 'blocks', max: 1, schema: { fields: block.fields } };
|
|
221
|
+
document[section.key] = [block.value];
|
|
222
|
+
totalFields += block.fieldCount;
|
|
223
|
+
}
|
|
224
|
+
// A one-field page is a label, not a Single — only real structure drafts.
|
|
225
|
+
if (Object.keys(fields).length === 0 || totalFields < 3) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
uid: draft.single,
|
|
230
|
+
pageId: draft.pageId,
|
|
231
|
+
schema: { uid: draft.single, name: draft.single, isSingleton: true, fields },
|
|
232
|
+
document,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -97,6 +97,10 @@ export interface WrappedIds {
|
|
|
97
97
|
media: string[];
|
|
98
98
|
richText: string[];
|
|
99
99
|
files: string[];
|
|
100
|
+
/** Every wrapped key, ALL kinds, in ENCOUNTER order (per-file walk, files
|
|
101
|
+
* in walk order) — the on-page reading order the Studio Pages view sorts
|
|
102
|
+
* by. Deduplicated keeping the first occurrence; never sorted. */
|
|
103
|
+
ordered: string[];
|
|
100
104
|
}
|
|
101
105
|
export interface WrapScanResult {
|
|
102
106
|
items: WrappableItem[];
|
|
@@ -242,7 +242,7 @@ function extractTemplate(ext, code) {
|
|
|
242
242
|
}
|
|
243
243
|
function scanTemplateFile(ext, code, relPath) {
|
|
244
244
|
const items = [];
|
|
245
|
-
const wrappedIds = { labels: [], media: [], richText: [], files: [] };
|
|
245
|
+
const wrappedIds = { labels: [], media: [], richText: [], files: [], ordered: [] };
|
|
246
246
|
let wrapped = 0;
|
|
247
247
|
const { template, offset, frontmatter } = extractTemplate(ext, code);
|
|
248
248
|
// t('key', ...) in frontmatter/script and in template expressions = wrapped.
|
|
@@ -250,6 +250,7 @@ function scanTemplateFile(ext, code, relPath) {
|
|
|
250
250
|
for (const match of source.matchAll(/\bt\(\s*['"]([^'"]+)['"]/g)) {
|
|
251
251
|
wrapped += 1;
|
|
252
252
|
wrappedIds.labels.push(match[1]);
|
|
253
|
+
wrappedIds.ordered.push(match[1]);
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
// data-fimo-source attributes = wrapped media/labels rendered via attrs.
|
|
@@ -474,7 +475,7 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
474
475
|
const items = [];
|
|
475
476
|
let wrapped = 0;
|
|
476
477
|
let foreignI18n = 0;
|
|
477
|
-
const wrappedIds = { labels: [], media: [], richText: [], files: [] };
|
|
478
|
+
const wrappedIds = { labels: [], media: [], richText: [], files: [], ordered: [] };
|
|
478
479
|
const collections = [];
|
|
479
480
|
const templateExt = filePath.match(/\.(astro|vue|svelte)$/)?.[1];
|
|
480
481
|
if (templateExt) {
|
|
@@ -560,6 +561,7 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
560
561
|
const id = attributeValue(node, 'id');
|
|
561
562
|
if (kind && id) {
|
|
562
563
|
wrappedIds[kind].push(id.value);
|
|
564
|
+
wrappedIds.ordered.push(id.value);
|
|
563
565
|
}
|
|
564
566
|
}
|
|
565
567
|
if (!insideWrapped && name) {
|
|
@@ -694,6 +696,7 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
|
|
|
694
696
|
const resolved = resolveScopedKey(node.callee.name, firstArg.value, childScopes);
|
|
695
697
|
if (!resolved.unresolvable) {
|
|
696
698
|
wrappedIds.labels.push(resolved.key);
|
|
699
|
+
wrappedIds.ordered.push(resolved.key);
|
|
697
700
|
}
|
|
698
701
|
}
|
|
699
702
|
}
|
|
@@ -1176,6 +1179,8 @@ export function scanWrappable(rootDir, opts = {}) {
|
|
|
1176
1179
|
richText: new Set(),
|
|
1177
1180
|
files: new Set(),
|
|
1178
1181
|
};
|
|
1182
|
+
const orderedIds = [];
|
|
1183
|
+
const orderedSeen = new Set();
|
|
1179
1184
|
for (const file of files) {
|
|
1180
1185
|
let code;
|
|
1181
1186
|
try {
|
|
@@ -1196,6 +1201,12 @@ export function scanWrappable(rootDir, opts = {}) {
|
|
|
1196
1201
|
idSets[kind].add(id);
|
|
1197
1202
|
}
|
|
1198
1203
|
}
|
|
1204
|
+
for (const id of result.wrappedIds.ordered) {
|
|
1205
|
+
if (!orderedSeen.has(id)) {
|
|
1206
|
+
orderedSeen.add(id);
|
|
1207
|
+
orderedIds.push(id);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1199
1210
|
}
|
|
1200
1211
|
const dataFiles = [];
|
|
1201
1212
|
// Markdown/MDX documents: group per directory so a 40-post blog is ONE
|
|
@@ -1337,6 +1348,8 @@ export function scanWrappable(rootDir, opts = {}) {
|
|
|
1337
1348
|
media: [...idSets.media].sort(),
|
|
1338
1349
|
richText: [...idSets.richText].sort(),
|
|
1339
1350
|
files: [...idSets.files].sort(),
|
|
1351
|
+
// Encounter order, never sorted — the on-page reading order.
|
|
1352
|
+
ordered: orderedIds,
|
|
1340
1353
|
},
|
|
1341
1354
|
};
|
|
1342
1355
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface DiscoveredRoute {
|
|
2
|
-
id: string;
|
|
3
|
-
path: string;
|
|
4
|
-
label: string;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
8
|
-
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
9
|
-
* Studio page switcher can enumerate a wrapped site's pages.
|
|
10
|
-
*/
|
|
11
|
-
export declare function discoverNextRoutes(appDir: string): DiscoveredRoute[];
|
|
12
|
-
/**
|
|
13
|
-
* Pages-router discovery (`pages/` or `src/pages/`): every module is a route
|
|
14
|
-
* except `_app`/`_document`/`_error` and `api/`. `[param]` segments map to
|
|
15
|
-
* `:param` (the parity capture skips dynamic routes, same as app router).
|
|
16
|
-
*/
|
|
17
|
-
export declare function discoverNextPagesRoutes(pagesDir: string): DiscoveredRoute[];
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
const PAGE_FILES = new Set(['page.tsx', 'page.jsx', 'page.ts', 'page.js', 'page.mdx']);
|
|
4
|
-
const titleCase = (seg) => seg
|
|
5
|
-
.replace(/[_\-]+/g, ' ')
|
|
6
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
7
|
-
.split(/\s+/)
|
|
8
|
-
.filter(Boolean)
|
|
9
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
10
|
-
.join(' ');
|
|
11
|
-
function segmentToPath(segment) {
|
|
12
|
-
// Route groups `(marketing)` don't contribute URL segments. Parallel-route
|
|
13
|
-
// slots `@modal` and private folders `_lib` are not standalone pages.
|
|
14
|
-
if (segment.startsWith('(')) {
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
if (segment.startsWith('@') || segment.startsWith('_')) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const catchAll = segment.match(/^\[\.\.\.(.+)\]$/) ?? segment.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
21
|
-
if (catchAll) {
|
|
22
|
-
return `:${catchAll[1]}*`;
|
|
23
|
-
}
|
|
24
|
-
const param = segment.match(/^\[(.+)\]$/);
|
|
25
|
-
if (param) {
|
|
26
|
-
return `:${param[1]}`;
|
|
27
|
-
}
|
|
28
|
-
return segment;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
32
|
-
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
33
|
-
* Studio page switcher can enumerate a wrapped site's pages.
|
|
34
|
-
*/
|
|
35
|
-
export function discoverNextRoutes(appDir) {
|
|
36
|
-
const routes = [];
|
|
37
|
-
const walk = (dir, segments) => {
|
|
38
|
-
let entries;
|
|
39
|
-
try {
|
|
40
|
-
entries = readdirSync(dir);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (entries.some((entry) => PAGE_FILES.has(entry))) {
|
|
46
|
-
const parts = segments.filter((s) => s.length > 0);
|
|
47
|
-
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
48
|
-
const lastStatic = [...parts].reverse().find((p) => !p.startsWith(':'));
|
|
49
|
-
routes.push({
|
|
50
|
-
id: routePath === '/' ? 'home' : parts.map((p) => p.replace(/^:/, '')).join('-'),
|
|
51
|
-
path: routePath,
|
|
52
|
-
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
for (const entry of entries) {
|
|
56
|
-
const full = join(dir, entry);
|
|
57
|
-
let isDir = false;
|
|
58
|
-
try {
|
|
59
|
-
isDir = statSync(full).isDirectory();
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (!isDir) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const mapped = segmentToPath(entry);
|
|
68
|
-
if (mapped === null) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
walk(full, [...segments, mapped]);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
walk(appDir, []);
|
|
75
|
-
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
76
|
-
}
|
|
77
|
-
const PAGES_EXTENSIONS = /\.(tsx|jsx|ts|js)$/;
|
|
78
|
-
const PAGES_SKIP = new Set(['api']);
|
|
79
|
-
/**
|
|
80
|
-
* Pages-router discovery (`pages/` or `src/pages/`): every module is a route
|
|
81
|
-
* except `_app`/`_document`/`_error` and `api/`. `[param]` segments map to
|
|
82
|
-
* `:param` (the parity capture skips dynamic routes, same as app router).
|
|
83
|
-
*/
|
|
84
|
-
export function discoverNextPagesRoutes(pagesDir) {
|
|
85
|
-
const routes = [];
|
|
86
|
-
const toSegment = (name) => name
|
|
87
|
-
.replace(/^\[\.\.\.(.+)\]$/, ':$1')
|
|
88
|
-
.replace(/^\[\[\.\.\.(.+)\]\]$/, ':$1')
|
|
89
|
-
.replace(/^\[(.+)\]$/, ':$1');
|
|
90
|
-
const walk = (dir, segments) => {
|
|
91
|
-
let entries;
|
|
92
|
-
try {
|
|
93
|
-
entries = readdirSync(dir);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
const full = join(dir, entry);
|
|
100
|
-
let isDir = false;
|
|
101
|
-
try {
|
|
102
|
-
isDir = statSync(full).isDirectory();
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (isDir) {
|
|
108
|
-
// api/ is only special at the pages root.
|
|
109
|
-
if (!(segments.length === 0 && PAGES_SKIP.has(entry))) {
|
|
110
|
-
walk(full, [...segments, toSegment(entry)]);
|
|
111
|
-
}
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (!PAGES_EXTENSIONS.test(entry)) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const base = entry.replace(PAGES_EXTENSIONS, '');
|
|
118
|
-
if (base.startsWith('_')) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const parts = base === 'index' ? segments : [...segments, toSegment(base)];
|
|
122
|
-
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
123
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
124
|
-
routes.push({
|
|
125
|
-
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
126
|
-
path: routePath,
|
|
127
|
-
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
walk(pagesDir, []);
|
|
132
|
-
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
133
|
-
return routes;
|
|
134
|
-
}
|