fimo 0.9.0-experimental.7 → 0.9.0-experimental.9
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 +359 -187
- package/dist/runtime/app/FimoProviders.js +29 -7
- package/dist/runtime/primitives/components/RichText.d.ts +5 -1
- package/dist/runtime/primitives/components/RichText.js +2 -2
- package/dist/runtime/react/FimoSpaProviders.js +82 -14
- package/dist/runtime/react/index.d.ts +1 -0
- package/dist/runtime/react/index.js +1 -0
- package/dist/runtime/react/spa-content-cache.d.ts +27 -0
- package/dist/runtime/react/spa-content-cache.js +36 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +4 -1
- package/dist/runtime/wrapped/FimoRichTextBlock.js +2 -2
- 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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
|
+
import { slugify } from './apply-wrap.js';
|
|
5
|
+
/** Display fields tried, in order, when deriving a human entry slug. */
|
|
6
|
+
const DISPLAY_SLUG_FIELDS = ['title', 'name', 'question', 'q', 'label', 'heading'];
|
|
4
7
|
/**
|
|
5
8
|
* Machine-staged entry ROWS for every drafted collection: the deterministic
|
|
6
9
|
* transformation agents kept re-implementing by hand (media envelopes,
|
|
@@ -71,6 +74,20 @@ export function writeRowsStaging(rootDir, scan, drafts) {
|
|
|
71
74
|
const bySourceLabel = new Map(drafts.map((draft) => [draft.source, draft]));
|
|
72
75
|
const provisionalByName = new Map(drafts.map((draft) => [draft.sourceName, draft.uid]));
|
|
73
76
|
const toProvisional = (name) => provisionalByName.get(name) ?? name;
|
|
77
|
+
// Types other rows REFERENCE resolve by slug server-side — their slugs must
|
|
78
|
+
// stay whatever identity the source carries. Every other type gets a HUMAN
|
|
79
|
+
// slug derived from its display field (never `faq-item-<hash>`).
|
|
80
|
+
const referencedUids = new Set();
|
|
81
|
+
for (const draft of drafts) {
|
|
82
|
+
for (const def of Object.values(draft.fields)) {
|
|
83
|
+
const field = def;
|
|
84
|
+
if (field?.type === 'reference') {
|
|
85
|
+
for (const target of field.targets ?? (field.target ? [field.target] : [])) {
|
|
86
|
+
referencedUids.add(target);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
74
91
|
let written = 0;
|
|
75
92
|
const stage = (draft, source, rawRows, references) => {
|
|
76
93
|
const fieldTypes = new Map(Object.entries(draft.fields).map(([name, def]) => [
|
|
@@ -78,12 +95,31 @@ export function writeRowsStaging(rootDir, scan, drafts) {
|
|
|
78
95
|
def && typeof def === 'object' ? (def.type ?? 'string') : 'string',
|
|
79
96
|
]));
|
|
80
97
|
const referenceFields = new Set(references.map((reference) => reference.field));
|
|
98
|
+
const usedSlugs = new Set();
|
|
81
99
|
const rows = rawRows.map((row) => {
|
|
82
100
|
const out = {};
|
|
83
101
|
for (const [key, value] of Object.entries(row)) {
|
|
84
102
|
// Reference values stay as SOURCE values for later resolution.
|
|
85
103
|
out[key] = referenceFields.has(key) ? value : transformValue(fieldTypes.get(key), value);
|
|
86
104
|
}
|
|
105
|
+
if (typeof out.slug === 'string' && out.slug) {
|
|
106
|
+
usedSlugs.add(out.slug);
|
|
107
|
+
}
|
|
108
|
+
else if (!referencedUids.has(draft.uid)) {
|
|
109
|
+
// Human slug from the display field ("who-is-behind-fimo", never
|
|
110
|
+
// "faq-item-<hash>"). Referenced types keep their source identity —
|
|
111
|
+
// the server resolves references by target slug.
|
|
112
|
+
const display = DISPLAY_SLUG_FIELDS.map((field) => row[field]).find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
|
|
113
|
+
const base = display ? slugify(display) : '';
|
|
114
|
+
if (base) {
|
|
115
|
+
let candidate = base;
|
|
116
|
+
for (let counter = 2; usedSlugs.has(candidate); counter++) {
|
|
117
|
+
candidate = `${base}-${counter}`;
|
|
118
|
+
}
|
|
119
|
+
usedSlugs.add(candidate);
|
|
120
|
+
out.slug = candidate;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
87
123
|
return out;
|
|
88
124
|
});
|
|
89
125
|
const descriptor = {};
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
.replace(/[_\-]+/g, ' ')
|
|
5
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
6
|
-
.split(/\s+/)
|
|
7
|
-
.filter(Boolean)
|
|
8
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
9
|
-
.join(' ');
|
|
10
|
-
function pushRoute(routes, parts) {
|
|
11
|
-
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
12
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
13
|
-
routes.push({
|
|
14
|
-
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
15
|
-
path: routePath,
|
|
16
|
-
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
17
|
-
});
|
|
18
|
-
}
|
|
3
|
+
import { pushRoute, toSegment } from '../discovered-route.js';
|
|
19
4
|
const PAGE_EXT = /\.(astro|md|mdx|html)$/;
|
|
20
|
-
const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
|
|
21
5
|
/** Astro file-based routes under src/pages (index files map to the dir path). */
|
|
22
6
|
export function discoverAstroRoutes(pagesDir) {
|
|
23
7
|
const routes = [];
|
|
@@ -46,7 +30,7 @@ export function discoverAstroRoutes(pagesDir) {
|
|
|
46
30
|
continue;
|
|
47
31
|
}
|
|
48
32
|
const base = entry.replace(PAGE_EXT, '');
|
|
49
|
-
pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)]);
|
|
33
|
+
pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)], full);
|
|
50
34
|
}
|
|
51
35
|
};
|
|
52
36
|
walk(pagesDir, []);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TemplateWrapStyle } from '../apply-wrap-templates.js';
|
|
2
2
|
import type { FrameworkDetection } from '../detect-framework.js';
|
|
3
|
+
import { type RouteFileResolution, type RouteLike } from '../routes/module-resolve.js';
|
|
3
4
|
import { type CollectionRewireResult } from './next/rewire.js';
|
|
4
5
|
import { type DiscoveredRoute } from './next/routes.js';
|
|
5
6
|
import { type RootWiringResult } from './next/wiring.js';
|
|
@@ -31,14 +32,26 @@ export declare function frameworkJsxWrapperModule(detection: Pick<FrameworkDetec
|
|
|
31
32
|
* server data path there is no deterministic t() wiring to inject.
|
|
32
33
|
*/
|
|
33
34
|
export declare function frameworkTemplateWrapStyle(framework: string | null | undefined): TemplateWrapStyle | null;
|
|
35
|
+
export declare function frameworkRouteFiles(framework: string | null): ((cwd: string, routes: Record<string, RouteLike>) => RouteFileResolution[]) | null;
|
|
36
|
+
export interface SectionStyle {
|
|
37
|
+
/** Module extensions a section import may resolve to. */
|
|
38
|
+
extensions: string[];
|
|
39
|
+
/** Wrapped-key scope for a component file (must match the wrap codemod's
|
|
40
|
+
* own scoping so drafted sections join their staged keys). */
|
|
41
|
+
fileScope: (relFile: string) => string;
|
|
42
|
+
/** Directories tried for auto-imported components (Nuxt convention) when
|
|
43
|
+
* a section tag has no import statement. */
|
|
44
|
+
fallbackComponentDirs: string[];
|
|
45
|
+
}
|
|
34
46
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* pre-wrap dev-server boot must not require the fimo package, and providers
|
|
39
|
-
* are render-transparent so the parity check stays clean. Template
|
|
40
|
-
* frameworks wire through their TemplateWrapStyle.ensureWiring hooks instead.
|
|
47
|
+
* How to read a page's SECTION STRUCTURE for this framework — the seam the
|
|
48
|
+
* page-structure drafter builds on. Template frameworks share the ES-import
|
|
49
|
+
* + capitalized-tag shape, differing only in extensions and key scoping.
|
|
41
50
|
*/
|
|
51
|
+
export declare function frameworkSectionStyle(framework: string | null): SectionStyle;
|
|
52
|
+
/** Client-rendered frameworks: no server pass — reads happen in the browser
|
|
53
|
+
* (the prompt's collection recipe and the SPA providers key off this). */
|
|
54
|
+
export declare function isClientRenderedFramework(framework: string | null | undefined): boolean;
|
|
42
55
|
export declare function frameworkRootWiring(detection: Pick<FrameworkDetection, 'framework' | 'react' | 'appDir'>): ((cwd: string) => RootWiringResult) | null;
|
|
43
56
|
/**
|
|
44
57
|
* Collection-page rewiring: pages that render a machine-staged collection get
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
import { keyScope, slugify } from '../apply-wrap.js';
|
|
4
|
+
import { DEFAULT_COMPONENT_EXTENSIONS } from '../routes/module-resolve.js';
|
|
2
5
|
import { discoverAstroRoutes } from './astro/routes.js';
|
|
3
6
|
import { astroTemplateWrapStyle } from './astro/template-style.js';
|
|
4
7
|
import { rewireNextCollectionPages } from './next/rewire.js';
|
|
5
|
-
import { discoverNextPagesRoutes, discoverNextRoutes } from './next/routes.js';
|
|
8
|
+
import { discoverNextPagesRoutes, discoverNextRoutes, resolveNextRouteFiles, } from './next/routes.js';
|
|
6
9
|
import { ensureNextLayoutWiring, nextRootLayoutFiles } from './next/wiring.js';
|
|
7
10
|
import { discoverNuxtRoutes } from './nuxt/routes.js';
|
|
8
11
|
import { nuxtTemplateWrapStyle } from './nuxt/template-style.js';
|
|
9
12
|
import { discoverRemixRoutes } from './remix/routes.js';
|
|
10
13
|
import { discoverSvelteKitRoutes } from './sveltekit/routes.js';
|
|
11
14
|
import { svelteTemplateWrapStyle } from './sveltekit/template-style.js';
|
|
12
|
-
import { discoverReactRouterDomRoutes } from './vite/routes.js';
|
|
15
|
+
import { discoverReactRouterDomRoutes, resolveSpaRouteFiles } from './vite/routes.js';
|
|
13
16
|
import { ensureSpaEntryWiring } from './vite/wiring.js';
|
|
14
17
|
/**
|
|
15
18
|
* Route discovery for any detected framework — the single registry generic
|
|
@@ -20,6 +23,13 @@ export function discoverFrameworkRoutes(cwd, detection) {
|
|
|
20
23
|
if (detection.framework === 'vite' && detection.react) {
|
|
21
24
|
return discoverReactRouterDomRoutes(cwd);
|
|
22
25
|
}
|
|
26
|
+
// Library-mode React Router (react-router dep on Vite, no app/ convention)
|
|
27
|
+
// declares routes in code exactly like a vite+react SPA — the Fimo-site
|
|
28
|
+
// dogfood detected `react-router` and fell to `default: []`, forcing 22
|
|
29
|
+
// manual `pages set` calls while the SPA scanner would have found them all.
|
|
30
|
+
if (detection.framework === 'react-router' && !detection.pagesDir) {
|
|
31
|
+
return discoverReactRouterDomRoutes(cwd);
|
|
32
|
+
}
|
|
23
33
|
switch (detection.framework) {
|
|
24
34
|
case 'next':
|
|
25
35
|
return [
|
|
@@ -102,6 +112,89 @@ export function frameworkTemplateWrapStyle(framework) {
|
|
|
102
112
|
* are render-transparent so the parity check stays clean. Template
|
|
103
113
|
* frameworks wire through their TemplateWrapStyle.ensureWiring hooks instead.
|
|
104
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* Registry route ids → the component files that render them — the join the
|
|
117
|
+
* label-ownership prefill and the page-structure drafter build on. Framework
|
|
118
|
+
* knowledge stays HERE: core modules call this and never test a framework
|
|
119
|
+
* name themselves. File-based frameworks resolve straight from their
|
|
120
|
+
* discovery output (the walk knows every route's file); code-declared
|
|
121
|
+
* routers (SPA) resolve through import analysis.
|
|
122
|
+
*/
|
|
123
|
+
function resolveByDiscovery(discover, conventionalDirs) {
|
|
124
|
+
return (cwd, routes) => {
|
|
125
|
+
const pagesDir = conventionalDirs.map((dir) => join(cwd, dir)).find((dir) => existsSync(dir));
|
|
126
|
+
if (!pagesDir) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const byPath = new Map();
|
|
130
|
+
for (const [routeId, route] of Object.entries(routes)) {
|
|
131
|
+
if (typeof route?.path === 'string') {
|
|
132
|
+
byPath.set(route.path, routeId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const resolutions = [];
|
|
136
|
+
for (const discovered of discover(pagesDir)) {
|
|
137
|
+
const routeId = byPath.get(discovered.path);
|
|
138
|
+
if (routeId && discovered.file) {
|
|
139
|
+
resolutions.push({ routeId, file: discovered.file });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return resolutions;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export function frameworkRouteFiles(framework) {
|
|
146
|
+
switch (framework) {
|
|
147
|
+
case 'next':
|
|
148
|
+
return resolveNextRouteFiles;
|
|
149
|
+
case 'react-router':
|
|
150
|
+
case 'vite':
|
|
151
|
+
return resolveSpaRouteFiles;
|
|
152
|
+
case 'astro':
|
|
153
|
+
return resolveByDiscovery(discoverAstroRoutes, ['src/pages', 'pages']);
|
|
154
|
+
case 'nuxt':
|
|
155
|
+
return resolveByDiscovery(discoverNuxtRoutes, ['pages', 'src/pages']);
|
|
156
|
+
case 'sveltekit':
|
|
157
|
+
return resolveByDiscovery(discoverSvelteKitRoutes, ['src/routes']);
|
|
158
|
+
case 'remix':
|
|
159
|
+
return resolveByDiscovery(discoverRemixRoutes, ['app/routes']);
|
|
160
|
+
default:
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* How to read a page's SECTION STRUCTURE for this framework — the seam the
|
|
166
|
+
* page-structure drafter builds on. Template frameworks share the ES-import
|
|
167
|
+
* + capitalized-tag shape, differing only in extensions and key scoping.
|
|
168
|
+
*/
|
|
169
|
+
export function frameworkSectionStyle(framework) {
|
|
170
|
+
const templateScope = (relFile) => {
|
|
171
|
+
const style = frameworkTemplateWrapStyle(framework);
|
|
172
|
+
return style?.fileScope?.(relFile) ?? slugify(basename(relFile).replace(/\.[a-z0-9]+$/i, '')) ?? 'shared';
|
|
173
|
+
};
|
|
174
|
+
switch (framework) {
|
|
175
|
+
case 'astro':
|
|
176
|
+
return {
|
|
177
|
+
extensions: ['.astro', '.tsx', '.jsx', '.ts', '.js'],
|
|
178
|
+
fileScope: templateScope,
|
|
179
|
+
fallbackComponentDirs: [],
|
|
180
|
+
};
|
|
181
|
+
case 'nuxt':
|
|
182
|
+
return {
|
|
183
|
+
extensions: ['.vue', '.ts', '.js'],
|
|
184
|
+
fileScope: templateScope,
|
|
185
|
+
fallbackComponentDirs: ['components'],
|
|
186
|
+
};
|
|
187
|
+
case 'sveltekit':
|
|
188
|
+
return { extensions: ['.svelte', '.ts', '.js'], fileScope: templateScope, fallbackComponentDirs: [] };
|
|
189
|
+
default:
|
|
190
|
+
return { extensions: DEFAULT_COMPONENT_EXTENSIONS.slice(), fileScope: keyScope, fallbackComponentDirs: [] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Client-rendered frameworks: no server pass — reads happen in the browser
|
|
194
|
+
* (the prompt's collection recipe and the SPA providers key off this). */
|
|
195
|
+
export function isClientRenderedFramework(framework) {
|
|
196
|
+
return framework === 'vite' || framework === 'react-router';
|
|
197
|
+
}
|
|
105
198
|
export function frameworkRootWiring(detection) {
|
|
106
199
|
switch (detection.framework) {
|
|
107
200
|
case 'next':
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DiscoveredRoute } from './next/routes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared helpers for file-based route discovery — one implementation of the
|
|
4
|
+
* segment→route push and the bracket-param normalization that astro, nuxt,
|
|
5
|
+
* sveltekit, and remix previously each carried a copy of.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pushRoute(routes: DiscoveredRoute[], parts: string[], file?: string): void;
|
|
8
|
+
/** `[slug]` → `:slug`, `[...rest]` → `:rest` (bracket file conventions). */
|
|
9
|
+
export declare const toSegment: (name: string) => string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { discoveredRouteLabel } from '../lib/humanize.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared helpers for file-based route discovery — one implementation of the
|
|
4
|
+
* segment→route push and the bracket-param normalization that astro, nuxt,
|
|
5
|
+
* sveltekit, and remix previously each carried a copy of.
|
|
6
|
+
*/
|
|
7
|
+
export function pushRoute(routes, parts, file) {
|
|
8
|
+
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
9
|
+
routes.push({
|
|
10
|
+
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
11
|
+
path: routePath,
|
|
12
|
+
label: discoveredRouteLabel(routePath),
|
|
13
|
+
...(file ? { file } : {}),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** `[slug]` → `:slug`, `[...rest]` → `:rest` (bracket file conventions). */
|
|
17
|
+
export const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { type RouteFileResolution, type RouteLike } from '../../routes/module-resolve.js';
|
|
1
2
|
export interface DiscoveredRoute {
|
|
2
3
|
id: string;
|
|
3
4
|
path: string;
|
|
5
|
+
/** The source file that renders this route (file-based frameworks set it;
|
|
6
|
+
* code-declared routers resolve files separately). */
|
|
7
|
+
file?: string;
|
|
4
8
|
label: string;
|
|
5
9
|
}
|
|
6
10
|
/**
|
|
@@ -21,3 +25,6 @@ export declare function discoverNextRoutes(appDir: string): DiscoveredRoute[];
|
|
|
21
25
|
* `:param` (the parity capture skips dynamic routes, same as app router).
|
|
22
26
|
*/
|
|
23
27
|
export declare function discoverNextPagesRoutes(pagesDir: string): DiscoveredRoute[];
|
|
28
|
+
/** Registry-routed (frameworkRouteFiles): registry route ids → the app-dir
|
|
29
|
+
* page files that render them, joined by routePathForAppPage. */
|
|
30
|
+
export declare function resolveNextRouteFiles(cwd: string, routes: Record<string, RouteLike>): RouteFileResolution[];
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { readdirSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { discoveredRouteLabel } from '../../lib/humanize.js';
|
|
4
|
+
import { walkFiles } from '../../routes/module-resolve.js';
|
|
3
5
|
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
6
|
function segmentToPath(segment) {
|
|
12
7
|
// Route groups `(marketing)` don't contribute URL segments. Parallel-route
|
|
13
8
|
// slots `@modal` and private folders `_lib` are not standalone pages.
|
|
@@ -68,11 +63,10 @@ export function discoverNextRoutes(appDir) {
|
|
|
68
63
|
if (entries.some((entry) => PAGE_FILES.has(entry))) {
|
|
69
64
|
const parts = segments.filter((s) => s.length > 0);
|
|
70
65
|
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
71
|
-
const lastStatic = [...parts].reverse().find((p) => !p.startsWith(':'));
|
|
72
66
|
routes.push({
|
|
73
67
|
id: routePath === '/' ? 'home' : parts.map((p) => p.replace(/^:/, '')).join('-'),
|
|
74
68
|
path: routePath,
|
|
75
|
-
label:
|
|
69
|
+
label: discoveredRouteLabel(routePath),
|
|
76
70
|
});
|
|
77
71
|
}
|
|
78
72
|
for (const entry of entries) {
|
|
@@ -143,11 +137,10 @@ export function discoverNextPagesRoutes(pagesDir) {
|
|
|
143
137
|
}
|
|
144
138
|
const parts = base === 'index' ? segments : [...segments, toSegment(base)];
|
|
145
139
|
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
146
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
147
140
|
routes.push({
|
|
148
141
|
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
149
142
|
path: routePath,
|
|
150
|
-
label:
|
|
143
|
+
label: discoveredRouteLabel(routePath),
|
|
151
144
|
});
|
|
152
145
|
}
|
|
153
146
|
};
|
|
@@ -155,3 +148,29 @@ export function discoverNextPagesRoutes(pagesDir) {
|
|
|
155
148
|
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
156
149
|
return routes;
|
|
157
150
|
}
|
|
151
|
+
/** Registry-routed (frameworkRouteFiles): registry route ids → the app-dir
|
|
152
|
+
* page files that render them, joined by routePathForAppPage. */
|
|
153
|
+
export function resolveNextRouteFiles(cwd, routes) {
|
|
154
|
+
const appDir = [join(cwd, 'app'), join(cwd, 'src', 'app')].find((dir) => existsSync(dir));
|
|
155
|
+
if (!appDir) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
const PAGE_FILE = /^page\.(?:tsx|jsx|ts|js|mdx)$/;
|
|
159
|
+
const pageFiles = [];
|
|
160
|
+
walkFiles(appDir, (name) => PAGE_FILE.test(name), pageFiles);
|
|
161
|
+
const byPath = new Map();
|
|
162
|
+
for (const [routeId, route] of Object.entries(routes)) {
|
|
163
|
+
if (typeof route?.path === 'string') {
|
|
164
|
+
byPath.set(route.path, routeId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const resolutions = [];
|
|
168
|
+
for (const file of pageFiles) {
|
|
169
|
+
const routePath = routePathForAppPage(appDir, file);
|
|
170
|
+
const routeId = routePath ? byPath.get(routePath) : undefined;
|
|
171
|
+
if (routeId) {
|
|
172
|
+
resolutions.push({ routeId, file });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return resolutions;
|
|
176
|
+
}
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
.replace(/[_\-]+/g, ' ')
|
|
5
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
6
|
-
.split(/\s+/)
|
|
7
|
-
.filter(Boolean)
|
|
8
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
9
|
-
.join(' ');
|
|
10
|
-
function pushRoute(routes, parts) {
|
|
11
|
-
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
12
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
13
|
-
routes.push({
|
|
14
|
-
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
15
|
-
path: routePath,
|
|
16
|
-
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
|
|
3
|
+
import { pushRoute, toSegment } from '../discovered-route.js';
|
|
20
4
|
/** Nuxt file-based routes under pages/ (index.vue maps to the dir path). */
|
|
21
5
|
export function discoverNuxtRoutes(pagesDir) {
|
|
22
6
|
const routes = [];
|
|
@@ -45,7 +29,7 @@ export function discoverNuxtRoutes(pagesDir) {
|
|
|
45
29
|
continue;
|
|
46
30
|
}
|
|
47
31
|
const base = entry.replace(/\.vue$/, '');
|
|
48
|
-
pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)]);
|
|
32
|
+
pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)], full);
|
|
49
33
|
}
|
|
50
34
|
};
|
|
51
35
|
walk(pagesDir, []);
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
5
|
-
.split(/\s+/)
|
|
6
|
-
.filter(Boolean)
|
|
7
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
8
|
-
.join(' ');
|
|
9
|
-
function pushRoute(routes, parts) {
|
|
10
|
-
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
11
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
12
|
-
routes.push({
|
|
13
|
-
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
14
|
-
path: routePath,
|
|
15
|
-
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pushRoute } from '../discovered-route.js';
|
|
18
4
|
const FILE_EXT = /\.(tsx|jsx|ts|js|mdx)$/;
|
|
19
5
|
/** Remix v2 flat routes under app/routes: dots are slashes, $param is dynamic. */
|
|
20
6
|
export function discoverRemixRoutes(routesDir) {
|
|
@@ -32,7 +18,7 @@ export function discoverRemixRoutes(routesDir) {
|
|
|
32
18
|
}
|
|
33
19
|
const base = entry.replace(FILE_EXT, '');
|
|
34
20
|
if (base === '_index') {
|
|
35
|
-
pushRoute(routes, []);
|
|
21
|
+
pushRoute(routes, [], join(routesDir, entry));
|
|
36
22
|
continue;
|
|
37
23
|
}
|
|
38
24
|
// Pathless layout segments start with _ ; trailing ._index maps to the parent.
|
|
@@ -42,7 +28,7 @@ export function discoverRemixRoutes(routesDir) {
|
|
|
42
28
|
.filter((seg) => !seg.startsWith('_'))
|
|
43
29
|
.map((seg) => (seg.startsWith('$') ? `:${seg.slice(1) || 'splat'}` : seg));
|
|
44
30
|
if (parts.length > 0) {
|
|
45
|
-
pushRoute(routes, parts);
|
|
31
|
+
pushRoute(routes, parts, join(routesDir, entry));
|
|
46
32
|
}
|
|
47
33
|
}
|
|
48
34
|
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
.replace(/[_\-]+/g, ' ')
|
|
5
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
6
|
-
.split(/\s+/)
|
|
7
|
-
.filter(Boolean)
|
|
8
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
9
|
-
.join(' ');
|
|
10
|
-
function pushRoute(routes, parts) {
|
|
11
|
-
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
12
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
13
|
-
routes.push({
|
|
14
|
-
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
15
|
-
path: routePath,
|
|
16
|
-
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
const toSegment = (name) => name.replace(/^\[\.\.\.(.+)\]$/, ':$1').replace(/^\[(.+)\]$/, ':$1');
|
|
3
|
+
import { pushRoute, toSegment } from '../discovered-route.js';
|
|
20
4
|
/** SvelteKit routes: every directory under src/routes holding a +page file. */
|
|
21
5
|
export function discoverSvelteKitRoutes(routesDir) {
|
|
22
6
|
const routes = [];
|
|
@@ -28,9 +12,10 @@ export function discoverSvelteKitRoutes(routesDir) {
|
|
|
28
12
|
catch {
|
|
29
13
|
return;
|
|
30
14
|
}
|
|
31
|
-
|
|
15
|
+
const pageFile = entries.find((entry) => /^\+page\.(svelte|md)$/.test(entry));
|
|
16
|
+
if (pageFile) {
|
|
32
17
|
// Route groups (parens) are layout-only segments.
|
|
33
|
-
pushRoute(routes, segments.filter((seg) => !seg.startsWith('(')));
|
|
18
|
+
pushRoute(routes, segments.filter((seg) => !seg.startsWith('(')), join(dir, pageFile));
|
|
34
19
|
}
|
|
35
20
|
for (const entry of entries) {
|
|
36
21
|
const full = join(dir, entry);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RouteFileResolution, type RouteLike } from '../../routes/module-resolve.js';
|
|
1
2
|
import type { DiscoveredRoute } from '../next/routes.js';
|
|
2
3
|
/**
|
|
3
4
|
* Route discovery for Vite React SPAs: there is no file convention, so the
|
|
@@ -13,3 +14,7 @@ import type { DiscoveredRoute } from '../next/routes.js';
|
|
|
13
14
|
* anything the scan cannot see, and scanned routes only ever ADD to it.
|
|
14
15
|
*/
|
|
15
16
|
export declare function discoverReactRouterDomRoutes(rootDir: string): DiscoveredRoute[];
|
|
17
|
+
/** Registry-routed (frameworkRouteFiles): registry route ids → the SPA
|
|
18
|
+
* component files that render them, via <Route element>/object-form route
|
|
19
|
+
* declarations and import resolution (lazy() included). */
|
|
20
|
+
export declare function resolveSpaRouteFiles(cwd: string, routes: Record<string, RouteLike>): RouteFileResolution[];
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
6
|
-
.split(/\s+/)
|
|
7
|
-
.filter(Boolean)
|
|
8
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
9
|
-
.join(' ');
|
|
3
|
+
import { discoveredRouteLabel } from '../../lib/humanize.js';
|
|
4
|
+
import { componentImports, resolveModuleFile, walkFiles, } from '../../routes/module-resolve.js';
|
|
10
5
|
const ROUTE_SOURCE = /\.(tsx|jsx|ts|js)$/;
|
|
11
6
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git']);
|
|
12
7
|
/**
|
|
@@ -81,13 +76,68 @@ export function discoverReactRouterDomRoutes(rootDir) {
|
|
|
81
76
|
continue;
|
|
82
77
|
}
|
|
83
78
|
const parts = path.split('/').filter(Boolean);
|
|
84
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
85
79
|
routes.set(path, {
|
|
86
80
|
id: path === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
87
81
|
path,
|
|
88
|
-
label:
|
|
82
|
+
label: discoveredRouteLabel(path),
|
|
89
83
|
});
|
|
90
84
|
}
|
|
91
85
|
}
|
|
92
86
|
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
93
87
|
}
|
|
88
|
+
/** Registry-routed (frameworkRouteFiles): registry route ids → the SPA
|
|
89
|
+
* component files that render them, via <Route element>/object-form route
|
|
90
|
+
* declarations and import resolution (lazy() included). */
|
|
91
|
+
export function resolveSpaRouteFiles(cwd, routes) {
|
|
92
|
+
const srcDir = join(cwd, 'src');
|
|
93
|
+
const files = [];
|
|
94
|
+
walkFiles(srcDir, (name) => ROUTE_SOURCE.test(name), files);
|
|
95
|
+
const byPath = new Map();
|
|
96
|
+
for (const [routeId, route] of Object.entries(routes)) {
|
|
97
|
+
if (typeof route?.path === 'string') {
|
|
98
|
+
byPath.set(route.path, routeId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const resolutions = new Map();
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
let code;
|
|
104
|
+
try {
|
|
105
|
+
code = readFileSync(file, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!/from\s+['"]react-router(?:-dom)?['"]/.test(code)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const imports = componentImports(code);
|
|
114
|
+
const pairs = [];
|
|
115
|
+
// JSX shape: one <Route …> tag carries both path and element, in either
|
|
116
|
+
// attribute order.
|
|
117
|
+
for (const tag of code.matchAll(/<Route\b((?:"[^"]*"|'[^']*'|\{[^{}]*\}|[^>"'{}])*)\/?>/g)) {
|
|
118
|
+
const attrs = tag[1];
|
|
119
|
+
const routePath = attrs.match(/\bpath\s*=\s*(?:"([^"]+)"|'([^']+)')/);
|
|
120
|
+
const element = attrs.match(/\belement\s*=\s*\{\s*<(\w+)[\s/>]/) ?? attrs.match(/\bComponent\s*=\s*\{(\w+)\}/);
|
|
121
|
+
if (routePath && element) {
|
|
122
|
+
pairs.push({ path: routePath[1] ?? routePath[2], component: element[1] });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Object shape: { path: '/x', element: <X …/> } or { path: '/x', Component: X }.
|
|
126
|
+
for (const entry of code.matchAll(/path\s*:\s*(?:"([^"]+)"|'([^']+)')\s*,\s*(?:element\s*:\s*<(\w+)[\s/>]|Component\s*:\s*(\w+))/g)) {
|
|
127
|
+
pairs.push({ path: entry[1] ?? entry[2], component: entry[3] ?? entry[4] });
|
|
128
|
+
}
|
|
129
|
+
for (const pair of pairs) {
|
|
130
|
+
const normalized = pair.path !== '/' && pair.path.endsWith('/') ? pair.path.slice(0, -1) : pair.path;
|
|
131
|
+
const routeId = byPath.get(normalized);
|
|
132
|
+
if (!routeId || resolutions.has(routeId)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const spec = imports.get(pair.component);
|
|
136
|
+
const componentFile = spec ? resolveModuleFile(cwd, file, spec) : null;
|
|
137
|
+
if (componentFile) {
|
|
138
|
+
resolutions.set(routeId, componentFile);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...resolutions.entries()].map(([routeId, file]) => ({ routeId, file }));
|
|
143
|
+
}
|
|
@@ -8,3 +8,15 @@ import type { RootWiringResult } from '../next/wiring.js';
|
|
|
8
8
|
* Anything else bails with the reason and stays agent work via the recipe.
|
|
9
9
|
*/
|
|
10
10
|
export declare function ensureSpaEntryWiring(cwd: string): RootWiringResult;
|
|
11
|
+
/**
|
|
12
|
+
* The `fimo()` Vite plugin serves the virtual modules the generated content
|
|
13
|
+
* clients import (`virtual:fimo-config`, …). Without it, the first
|
|
14
|
+
* dependency re-optimization 500s every page the moment an agent uses a
|
|
15
|
+
* generated client — both run-5 agents had to discover and add it by hand.
|
|
16
|
+
* Insert `fimo()` into the config's plugins array; unusual configs bail
|
|
17
|
+
* with the recipe.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureViteFimoPlugin(cwd: string): {
|
|
20
|
+
changes: string[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
};
|