fimo 0.9.0-experimental.1 → 0.9.0-experimental.10
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/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
- package/assets/agents-starter/claude/settings.json +19 -0
- package/assets/content-templates/types-template.eta +1 -0
- package/assets/skills/fimo/references/translations.md +75 -0
- package/assets/skills/fimo-migration/SKILL.md +2 -0
- package/assets/skills/fimo-migration/references/manifest.md +11 -0
- package/assets/skills/fimo-migration/references/pages.md +234 -0
- package/assets/skills/fimo-migration/references/wrap.md +45 -0
- package/dist/build/vite/plugins/translations.js +28 -10
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +484 -243
- package/dist/runtime/app/FimoProviders.js +29 -7
- package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
- package/dist/runtime/core/BuiltWithBadge.js +68 -0
- 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 +83 -24
- package/dist/runtime/react/index.d.ts +2 -0
- package/dist/runtime/react/index.js +1 -0
- package/dist/runtime/react/spa-content-cache.d.ts +38 -0
- package/dist/runtime/react/spa-content-cache.js +64 -0
- package/dist/runtime/seo/neutral.d.ts +22 -0
- package/dist/runtime/seo/neutral.js +17 -0
- package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
- package/dist/runtime/shared/fimo-config.server.js +0 -13
- package/dist/runtime/strapi/client.js +45 -2
- package/dist/runtime/strapi/reshape.d.ts +10 -6
- package/dist/runtime/strapi/reshape.js +7 -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 +10 -5
- package/dist/scripts/apply-wrap.d.ts +29 -2
- package/dist/scripts/apply-wrap.js +69 -7
- package/dist/scripts/cms/example-content.js +16 -13
- package/dist/scripts/cms/strapi/schema-map.js +16 -4
- package/dist/scripts/cms/strapi/shapes.d.ts +7 -5
- package/dist/scripts/cms/strapi/shapes.js +10 -0
- package/dist/scripts/cms/strapi/wiring.js +1 -1
- package/dist/scripts/detect-framework.js +6 -0
- package/dist/scripts/draft-rows.js +44 -1
- package/dist/scripts/draft-schemas.js +64 -5
- package/dist/scripts/frameworks/astro/routes.js +8 -19
- package/dist/scripts/frameworks/discover.d.ts +19 -6
- package/dist/scripts/frameworks/discover.js +113 -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/rewire.d.ts +13 -0
- package/dist/scripts/frameworks/next/rewire.js +26 -2
- package/dist/scripts/frameworks/next/routes.d.ts +13 -0
- package/dist/scripts/frameworks/next/routes.js +55 -13
- 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/lib/validation-report.d.ts +0 -3
- package/dist/scripts/lib/validation-report.js +0 -9
- package/dist/scripts/render-parity/capture.d.ts +12 -4
- package/dist/scripts/render-parity/capture.js +94 -24
- package/dist/scripts/routes/bindings.d.ts +93 -0
- package/dist/scripts/routes/bindings.js +117 -0
- 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 +5 -1
- package/dist/scripts/scan-wrappable.js +126 -3
- package/package.json +5 -1
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/translations/en.json +1 -0
|
@@ -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,
|
|
@@ -69,8 +72,29 @@ export function writeRowsStaging(rootDir, scan, drafts) {
|
|
|
69
72
|
return 0;
|
|
70
73
|
}
|
|
71
74
|
const bySourceLabel = new Map(drafts.map((draft) => [draft.source, draft]));
|
|
72
|
-
|
|
75
|
+
// First occurrence wins — the same deterministic choice writeSchemaDrafts
|
|
76
|
+
// makes for reference targets of collided source names.
|
|
77
|
+
const provisionalByName = new Map();
|
|
78
|
+
for (const draft of drafts) {
|
|
79
|
+
if (!provisionalByName.has(draft.sourceName)) {
|
|
80
|
+
provisionalByName.set(draft.sourceName, draft.uid);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
73
83
|
const toProvisional = (name) => provisionalByName.get(name) ?? name;
|
|
84
|
+
// Types other rows REFERENCE resolve by slug server-side — their slugs must
|
|
85
|
+
// stay whatever identity the source carries. Every other type gets a HUMAN
|
|
86
|
+
// slug derived from its display field (never `faq-item-<hash>`).
|
|
87
|
+
const referencedUids = new Set();
|
|
88
|
+
for (const draft of drafts) {
|
|
89
|
+
for (const def of Object.values(draft.fields)) {
|
|
90
|
+
const field = def;
|
|
91
|
+
if (field?.type === 'reference') {
|
|
92
|
+
for (const target of field.targets ?? (field.target ? [field.target] : [])) {
|
|
93
|
+
referencedUids.add(target);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
74
98
|
let written = 0;
|
|
75
99
|
const stage = (draft, source, rawRows, references) => {
|
|
76
100
|
const fieldTypes = new Map(Object.entries(draft.fields).map(([name, def]) => [
|
|
@@ -78,12 +102,31 @@ export function writeRowsStaging(rootDir, scan, drafts) {
|
|
|
78
102
|
def && typeof def === 'object' ? (def.type ?? 'string') : 'string',
|
|
79
103
|
]));
|
|
80
104
|
const referenceFields = new Set(references.map((reference) => reference.field));
|
|
105
|
+
const usedSlugs = new Set();
|
|
81
106
|
const rows = rawRows.map((row) => {
|
|
82
107
|
const out = {};
|
|
83
108
|
for (const [key, value] of Object.entries(row)) {
|
|
84
109
|
// Reference values stay as SOURCE values for later resolution.
|
|
85
110
|
out[key] = referenceFields.has(key) ? value : transformValue(fieldTypes.get(key), value);
|
|
86
111
|
}
|
|
112
|
+
if (typeof out.slug === 'string' && out.slug) {
|
|
113
|
+
usedSlugs.add(out.slug);
|
|
114
|
+
}
|
|
115
|
+
else if (!referencedUids.has(draft.uid)) {
|
|
116
|
+
// Human slug from the display field ("who-is-behind-fimo", never
|
|
117
|
+
// "faq-item-<hash>"). Referenced types keep their source identity —
|
|
118
|
+
// the server resolves references by target slug.
|
|
119
|
+
const display = DISPLAY_SLUG_FIELDS.map((field) => row[field]).find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
|
|
120
|
+
const base = display ? slugify(display) : '';
|
|
121
|
+
if (base) {
|
|
122
|
+
let candidate = base;
|
|
123
|
+
for (let counter = 2; usedSlugs.has(candidate); counter++) {
|
|
124
|
+
candidate = `${base}-${counter}`;
|
|
125
|
+
}
|
|
126
|
+
usedSlugs.add(candidate);
|
|
127
|
+
out.slug = candidate;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
87
130
|
return out;
|
|
88
131
|
});
|
|
89
132
|
const descriptor = {};
|
|
@@ -3,10 +3,34 @@ import { join } from 'node:path';
|
|
|
3
3
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ].*)?$/;
|
|
4
4
|
const MEDIA_PATH = /^\/(images|img|videos|video|media|assets|files|docs|documents)\//i;
|
|
5
5
|
const MEDIA_EXT = /\.(png|jpe?g|gif|webp|avif|svg|mp4|webm|mov|mp3|wav|pdf|zip|docx?)$/i;
|
|
6
|
-
function
|
|
7
|
-
const
|
|
6
|
+
function pascalize(name) {
|
|
7
|
+
// SCREAMING_CASE sources (const arrays like ENTRIES, TEAM_MEMBERS) lowercase
|
|
8
|
+
// first so the word structure survives — "ENTRIES" reads as "Entries", not
|
|
9
|
+
// the case-mangled "ENTRIES" verbatim.
|
|
10
|
+
const normalized = /[a-z]/.test(name) ? name : name.toLowerCase();
|
|
11
|
+
return normalized
|
|
8
12
|
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
|
9
|
-
.replace(/^(.)/, (c) => c.toUpperCase())
|
|
13
|
+
.replace(/^(.)/, (c) => c.toUpperCase())
|
|
14
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
15
|
+
}
|
|
16
|
+
/** File-derived qualifier for colliding draft names: `app/pricing/page.tsx:12`
|
|
17
|
+
* → `Pricing`. Generic stems (page, index, data) and stems that ARE the
|
|
18
|
+
* collection name (`pricing/faqItems.json` for FaqItem) defer to their
|
|
19
|
+
* directory — the qualifier must add information the base name lacks. */
|
|
20
|
+
function sourceQualifier(sourceLabel, base) {
|
|
21
|
+
const segments = sourceLabel
|
|
22
|
+
.replace(/:\d+$/, '')
|
|
23
|
+
.split('/')
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const generic = new Set(['page', 'index', 'data', 'content', 'constants', 'src', 'app', 'pages']);
|
|
26
|
+
let pick = (segments.pop() ?? '').replace(/\.[a-z0-9]+$/i, '');
|
|
27
|
+
while ((generic.has(pick.toLowerCase()) || singularPascal(pick) === base) && segments.length > 0) {
|
|
28
|
+
pick = segments.pop();
|
|
29
|
+
}
|
|
30
|
+
return pascalize(pick);
|
|
31
|
+
}
|
|
32
|
+
function singularPascal(name) {
|
|
33
|
+
const base = pascalize(name);
|
|
10
34
|
if (/ies$/i.test(base)) {
|
|
11
35
|
return base.replace(/ies$/i, 'y');
|
|
12
36
|
}
|
|
@@ -113,13 +137,48 @@ export function writeSchemaDrafts(rootDir, scan) {
|
|
|
113
137
|
return [];
|
|
114
138
|
}
|
|
115
139
|
mkdirSync(draftsDir, { recursive: true });
|
|
140
|
+
// Same-named sources (a FAQ_ITEMS array per page) must NOT collapse into
|
|
141
|
+
// one draft that keeps only the last-seen rows — a real dogfood silently
|
|
142
|
+
// dropped 92 of 98 FAQ rows this way. Every source keeps its own draft;
|
|
143
|
+
// colliding names are qualified by their source file (FaqItemPricing).
|
|
144
|
+
const byBase = new Map();
|
|
145
|
+
for (const source of sources) {
|
|
146
|
+
const base = singularPascal(source.sourceName);
|
|
147
|
+
byBase.set(base, [...(byBase.get(base) ?? []), source]);
|
|
148
|
+
}
|
|
149
|
+
const uidBySource = new Map();
|
|
150
|
+
const taken = new Set();
|
|
151
|
+
for (const [base, group] of byBase) {
|
|
152
|
+
if (group.length === 1) {
|
|
153
|
+
uidBySource.set(group[0], base);
|
|
154
|
+
taken.add(base);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const [base, group] of byBase) {
|
|
158
|
+
if (group.length === 1) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
for (const source of group) {
|
|
162
|
+
const qualified = `${base}${sourceQualifier(source.sourceLabel, base)}`;
|
|
163
|
+
let candidate = qualified;
|
|
164
|
+
for (let counter = 2; taken.has(candidate); counter++) {
|
|
165
|
+
candidate = `${qualified}${counter}`;
|
|
166
|
+
}
|
|
167
|
+
taken.add(candidate);
|
|
168
|
+
uidBySource.set(source, candidate);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Reference targets resolve by source name; for a collided name the FIRST
|
|
172
|
+
// source's uid wins (deterministic — adopt-drafts settles names anyway).
|
|
116
173
|
const provisionalNames = new Map();
|
|
117
174
|
for (const source of sources) {
|
|
118
|
-
provisionalNames.
|
|
175
|
+
if (!provisionalNames.has(source.sourceName)) {
|
|
176
|
+
provisionalNames.set(source.sourceName, uidBySource.get(source));
|
|
177
|
+
}
|
|
119
178
|
}
|
|
120
179
|
const drafts = [];
|
|
121
180
|
for (const source of sources) {
|
|
122
|
-
const uid =
|
|
181
|
+
const uid = uidBySource.get(source);
|
|
123
182
|
const referenceByField = new Map(source.references.map((reference) => [reference.field, reference]));
|
|
124
183
|
const fields = {};
|
|
125
184
|
// candidate.fields only lists STRING fields; booleans and numbers live
|
|
@@ -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 = [];
|
|
@@ -38,15 +22,20 @@ export function discoverAstroRoutes(pagesDir) {
|
|
|
38
22
|
catch {
|
|
39
23
|
continue;
|
|
40
24
|
}
|
|
25
|
+
// Astro's convention: underscore-prefixed files AND directories are
|
|
26
|
+
// excluded from routing (component folders inside src/pages).
|
|
27
|
+
if (entry.startsWith('_')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
41
30
|
if (isDir) {
|
|
42
31
|
walk(full, [...segments, toSegment(entry)]);
|
|
43
32
|
continue;
|
|
44
33
|
}
|
|
45
|
-
if (!PAGE_EXT.test(entry)
|
|
34
|
+
if (!PAGE_EXT.test(entry)) {
|
|
46
35
|
continue;
|
|
47
36
|
}
|
|
48
37
|
const base = entry.replace(PAGE_EXT, '');
|
|
49
|
-
pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)]);
|
|
38
|
+
pushRoute(routes, base === 'index' ? segments : [...segments, toSegment(base)], full);
|
|
50
39
|
}
|
|
51
40
|
};
|
|
52
41
|
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 [
|
|
@@ -66,6 +76,11 @@ export function frameworkJsxWrapperModule(detection) {
|
|
|
66
76
|
return 'fimo/next';
|
|
67
77
|
case 'remix':
|
|
68
78
|
return 'fimo/remix';
|
|
79
|
+
case 'react-router':
|
|
80
|
+
// Library-mode React Router SPAs (react-router dep on Vite) wrap with
|
|
81
|
+
// the client-side fimo/react primitives — same shape as vite+react.
|
|
82
|
+
// Proven on the Fimo marketing site (1211 items auto-wrapped).
|
|
83
|
+
return 'fimo/react';
|
|
69
84
|
case 'vite':
|
|
70
85
|
return detection.react ? 'fimo/react' : null;
|
|
71
86
|
default:
|
|
@@ -97,10 +112,97 @@ export function frameworkTemplateWrapStyle(framework) {
|
|
|
97
112
|
* are render-transparent so the parity check stays clean. Template
|
|
98
113
|
* frameworks wire through their TemplateWrapStyle.ensureWiring hooks instead.
|
|
99
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
|
+
}
|
|
100
198
|
export function frameworkRootWiring(detection) {
|
|
101
199
|
switch (detection.framework) {
|
|
102
200
|
case 'next':
|
|
103
201
|
return (cwd) => ensureNextLayoutWiring(cwd, detection);
|
|
202
|
+
case 'react-router':
|
|
203
|
+
// Same createRoot(...).render entry shape as vite+react; framework-mode
|
|
204
|
+
// RR apps have no src/main.* and bail with a reason (agent §0).
|
|
205
|
+
return ensureSpaEntryWiring;
|
|
104
206
|
case 'vite':
|
|
105
207
|
return detection.react ? ensureSpaEntryWiring : null;
|
|
106
208
|
default:
|
|
@@ -160,6 +262,14 @@ export function frameworkWiringMarker(detection, cwd) {
|
|
|
160
262
|
marker: /FimoSpaProviders/,
|
|
161
263
|
hint: 'wrap the root render with FimoSpaProviders from fimo/react (agent prompt section 0)',
|
|
162
264
|
};
|
|
265
|
+
case 'react-router':
|
|
266
|
+
// Accept either provider: FimoSpaProviders (the vite+react codemod) or
|
|
267
|
+
// FimoProviders (the fimo/react-router adapter, hand-wired on RR sites).
|
|
268
|
+
return {
|
|
269
|
+
files: ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx', 'app/root.tsx', 'app/root.jsx'],
|
|
270
|
+
marker: /Fimo(Spa)?Providers/,
|
|
271
|
+
hint: 'wrap the root render with FimoSpaProviders (fimo/react) or FimoProviders (fimo/react-router) — agent prompt section 0',
|
|
272
|
+
};
|
|
163
273
|
case 'remix':
|
|
164
274
|
return {
|
|
165
275
|
files: ['app/root.tsx', 'app/root.jsx'],
|
|
@@ -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');
|
|
@@ -17,11 +17,24 @@ import type { WrapScanResult } from '../../scan-wrappable.js';
|
|
|
17
17
|
* Uids are the PROVISIONAL draft names; `schemas adopt-drafts` rewrites the
|
|
18
18
|
* call sites through its renames.
|
|
19
19
|
*/
|
|
20
|
+
export interface RewiredPage {
|
|
21
|
+
file: string;
|
|
22
|
+
/** Collection uid the page reads through `wrapCollection` (provisional until
|
|
23
|
+
* `adopt-drafts` renames it). */
|
|
24
|
+
uid: string;
|
|
25
|
+
/** The page's registry route path (`:param` normalized), null when the file
|
|
26
|
+
* sits in a non-routable location. Joins the page back to the page registry
|
|
27
|
+
* as binding evidence. */
|
|
28
|
+
routePath: string | null;
|
|
29
|
+
}
|
|
20
30
|
export interface CollectionRewireResult {
|
|
21
31
|
changes: string[];
|
|
22
32
|
warnings: string[];
|
|
23
33
|
/** Pages successfully rewired. */
|
|
24
34
|
rewired: number;
|
|
35
|
+
/** Every page known to read a collection through `wrapCollection` — rewired
|
|
36
|
+
* this run or found already wired on a re-run. */
|
|
37
|
+
pages: RewiredPage[];
|
|
25
38
|
}
|
|
26
39
|
export declare function rewireNextCollectionPages(cwd: string, detection: Pick<FrameworkDetection, 'appDir'>, scan: WrapScanResult, drafts: Array<{
|
|
27
40
|
uid: string;
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseSync } from 'oxc-parser';
|
|
4
4
|
import { ensureFimoImport } from '../../apply-wrap.js';
|
|
5
|
+
import { routePathForAppPage } from './routes.js';
|
|
5
6
|
function applyEdits(code, edits) {
|
|
6
7
|
let next = code;
|
|
7
8
|
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
@@ -85,9 +86,10 @@ function pageFilesUnder(dir) {
|
|
|
85
86
|
export function rewireNextCollectionPages(cwd, detection, scan, drafts) {
|
|
86
87
|
const changes = [];
|
|
87
88
|
const warnings = [];
|
|
89
|
+
const pages = [];
|
|
88
90
|
let rewired = 0;
|
|
89
91
|
if (!detection.appDir) {
|
|
90
|
-
return { changes, warnings: ['Collection pages not rewired: pages-router project.'], rewired };
|
|
92
|
+
return { changes, warnings: ['Collection pages not rewired: pages-router project.'], rewired, pages };
|
|
91
93
|
}
|
|
92
94
|
const appDirAbs = path.join(cwd, detection.appDir);
|
|
93
95
|
const uidBySourceName = new Map(drafts.map((draft) => [draft.sourceName, draft.uid]));
|
|
@@ -100,6 +102,24 @@ export function rewireNextCollectionPages(cwd, detection, scan, drafts) {
|
|
|
100
102
|
const bail = (page, reason) => {
|
|
101
103
|
warnings.push(`Page not rewired (${page}): ${reason} — wire it with wrapCollection per the prompt recipe.`);
|
|
102
104
|
};
|
|
105
|
+
const recordPage = (fileAbs, uid) => {
|
|
106
|
+
pages.push({ file: path.relative(cwd, fileAbs), uid, routePath: routePathForAppPage(appDirAbs, fileAbs) });
|
|
107
|
+
};
|
|
108
|
+
// Pages wired on a PREVIOUS run still carry binding evidence (the uid in the
|
|
109
|
+
// call site is final once `adopt-drafts` renamed it) — collect them before
|
|
110
|
+
// rewiring so re-runs keep feeding the page registry.
|
|
111
|
+
for (const pageAbs of pageFilesUnder(appDirAbs)) {
|
|
112
|
+
let code;
|
|
113
|
+
try {
|
|
114
|
+
code = fs.readFileSync(pageAbs, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const match of code.matchAll(/wrapCollection\(\s*['"]([^'"]+)['"]/g)) {
|
|
120
|
+
recordPage(pageAbs, match[1]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
103
123
|
// ── In-code array literals: extract to src/data/<name>.json + wire ──
|
|
104
124
|
for (const candidate of scan.collections) {
|
|
105
125
|
const pageAbs = path.join(cwd, candidate.file);
|
|
@@ -173,6 +193,7 @@ export function rewireNextCollectionPages(cwd, detection, scan, drafts) {
|
|
|
173
193
|
next = ensureFimoImport(next, ['wrapCollection'], 'fimo/next/server');
|
|
174
194
|
fs.writeFileSync(pageAbs, next);
|
|
175
195
|
rewired += 1;
|
|
196
|
+
recordPage(pageAbs, uid);
|
|
176
197
|
changes.push(`rewired ${candidate.file}: \`${candidate.name}\` extracted to src/data/${candidate.name}.json and read through wrapCollection('${uid}')`);
|
|
177
198
|
}
|
|
178
199
|
// ── Data-file default imports: wire the importing pages ──
|
|
@@ -251,8 +272,11 @@ export function rewireNextCollectionPages(cwd, detection, scan, drafts) {
|
|
|
251
272
|
next = ensureFimoImport(next, ['wrapCollection'], 'fimo/next/server');
|
|
252
273
|
fs.writeFileSync(pageAbs, next);
|
|
253
274
|
rewired += 1;
|
|
275
|
+
for (const target of targets) {
|
|
276
|
+
recordPage(pageAbs, target.uid);
|
|
277
|
+
}
|
|
254
278
|
changes.push(`rewired ${rel}: ${targets.map((target) => `wrapCollection('${target.uid}')`).join(', ')} over the imported source rows`);
|
|
255
279
|
}
|
|
256
280
|
}
|
|
257
|
-
return { changes, warnings, rewired };
|
|
281
|
+
return { changes, warnings, rewired, pages };
|
|
258
282
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
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
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Route path for ONE app-router page file, using the same segment
|
|
12
|
+
* normalization as discovery — so a rewired page can be joined back to its
|
|
13
|
+
* registry route. Returns null for non-routable locations (`@slot`, `_lib`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function routePathForAppPage(appDir: string, pageFile: string): string | null;
|
|
6
16
|
/**
|
|
7
17
|
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
8
18
|
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
@@ -15,3 +25,6 @@ export declare function discoverNextRoutes(appDir: string): DiscoveredRoute[];
|
|
|
15
25
|
* `:param` (the parity capture skips dynamic routes, same as app router).
|
|
16
26
|
*/
|
|
17
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';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
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.
|
|
@@ -27,6 +22,29 @@ function segmentToPath(segment) {
|
|
|
27
22
|
}
|
|
28
23
|
return segment;
|
|
29
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Route path for ONE app-router page file, using the same segment
|
|
27
|
+
* normalization as discovery — so a rewired page can be joined back to its
|
|
28
|
+
* registry route. Returns null for non-routable locations (`@slot`, `_lib`).
|
|
29
|
+
*/
|
|
30
|
+
export function routePathForAppPage(appDir, pageFile) {
|
|
31
|
+
const rel = relative(appDir, pageFile);
|
|
32
|
+
if (rel.startsWith('..')) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const segments = rel.split(sep).slice(0, -1);
|
|
36
|
+
const parts = [];
|
|
37
|
+
for (const segment of segments) {
|
|
38
|
+
const mapped = segmentToPath(segment);
|
|
39
|
+
if (mapped === null) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (mapped.length > 0) {
|
|
43
|
+
parts.push(mapped);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
47
|
+
}
|
|
30
48
|
/**
|
|
31
49
|
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
32
50
|
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
@@ -45,11 +63,10 @@ export function discoverNextRoutes(appDir) {
|
|
|
45
63
|
if (entries.some((entry) => PAGE_FILES.has(entry))) {
|
|
46
64
|
const parts = segments.filter((s) => s.length > 0);
|
|
47
65
|
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
48
|
-
const lastStatic = [...parts].reverse().find((p) => !p.startsWith(':'));
|
|
49
66
|
routes.push({
|
|
50
67
|
id: routePath === '/' ? 'home' : parts.map((p) => p.replace(/^:/, '')).join('-'),
|
|
51
68
|
path: routePath,
|
|
52
|
-
label:
|
|
69
|
+
label: discoveredRouteLabel(routePath),
|
|
53
70
|
});
|
|
54
71
|
}
|
|
55
72
|
for (const entry of entries) {
|
|
@@ -120,11 +137,10 @@ export function discoverNextPagesRoutes(pagesDir) {
|
|
|
120
137
|
}
|
|
121
138
|
const parts = base === 'index' ? segments : [...segments, toSegment(base)];
|
|
122
139
|
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
123
|
-
const lastStatic = [...parts].reverse().find((part) => !part.startsWith(':'));
|
|
124
140
|
routes.push({
|
|
125
141
|
id: routePath === '/' ? 'home' : parts.map((part) => part.replace(/^:/, '')).join('-'),
|
|
126
142
|
path: routePath,
|
|
127
|
-
label:
|
|
143
|
+
label: discoveredRouteLabel(routePath),
|
|
128
144
|
});
|
|
129
145
|
}
|
|
130
146
|
};
|
|
@@ -132,3 +148,29 @@ export function discoverNextPagesRoutes(pagesDir) {
|
|
|
132
148
|
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
133
149
|
return routes;
|
|
134
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
|
+
}
|