fimo 0.7.0-experimental.5 → 0.7.0-experimental.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +166 -5
- package/dist/scripts/frameworks/support.d.ts +75 -0
- package/dist/scripts/frameworks/support.js +165 -0
- package/dist/scripts/frameworks/support.test.d.ts +1 -0
- package/dist/scripts/frameworks/support.test.js +87 -0
- package/package.json +3 -3
- package/templates/react-router/package.json +1 -1
package/dist/cli/bundle.json
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -51619,11 +51619,17 @@ whole migration loop; the deterministic CLI does the heavy lifting.
|
|
|
51619
51619
|
env keys, and detects collections and external data sources. Recover
|
|
51620
51620
|
mechanical failures yourself (missing install, pnpm minimumReleaseAge
|
|
51621
51621
|
policy, broken dev script) and re-run it.
|
|
51622
|
-
2. That command REGENERATES this file with the full work order. Re-read it
|
|
51623
|
-
|
|
51624
|
-
|
|
51625
|
-
|
|
51626
|
-
|
|
51622
|
+
2. That command REGENERATES this file with the full work order. Re-read it and
|
|
51623
|
+
continue with the regenerated version.
|
|
51624
|
+
- If it reports a CMS scaffold available (e.g. Strapi detected), ask the user
|
|
51625
|
+
ONE question: bring the CMS content model + example content into Fimo now so
|
|
51626
|
+
the site runs on Fimo? If yes, run \`npx fimo migrate --scaffold-cms\` \u2014 that
|
|
51627
|
+
creates the content types + example content in Fimo AND wires the whole
|
|
51628
|
+
site's reads to Fimo (no manual wiring, no per-view work; unscaffolded types
|
|
51629
|
+
keep reading the CMS). Then \`pnpm dev\` shows the site on Fimo. If they'd
|
|
51630
|
+
rather keep the CMS live, skip it.
|
|
51631
|
+
- For any other DEFAULTED external data source, ask the user, then update
|
|
51632
|
+
\`.fimo/migration/manifest.json\` and re-run \`npx fimo migrate\`.
|
|
51627
51633
|
3. Loop \`npx fimo migrate wrap --check\` after every batch of fixes until it
|
|
51628
51634
|
exits 0 \u2014 that is the only definition of done.
|
|
51629
51635
|
4. Never run \`npx fimo deploy\` or \`fimo publish\` yourself: report the
|
|
@@ -115537,6 +115543,151 @@ __name(writeSchemaDrafts, "writeSchemaDrafts");
|
|
|
115537
115543
|
// src/cli/commands/migrate-wrap.ts
|
|
115538
115544
|
init_discover();
|
|
115539
115545
|
init_wiring();
|
|
115546
|
+
|
|
115547
|
+
// src/scripts/frameworks/support.ts
|
|
115548
|
+
import { existsSync as existsSync16, readFileSync as readFileSync20 } from "node:fs";
|
|
115549
|
+
import { join as join27 } from "node:path";
|
|
115550
|
+
var REACT_CHECK = {
|
|
115551
|
+
pkg: "react",
|
|
115552
|
+
min: "18.0.0",
|
|
115553
|
+
label: "React",
|
|
115554
|
+
reason: "Fimo's inline-editing runtime uses React 18 APIs (useSyncExternalStore).",
|
|
115555
|
+
remedy: "Upgrade React to 18 or newer. The content still migrates on older React; only inline editing needs 18+."
|
|
115556
|
+
};
|
|
115557
|
+
function supportChecksFor(detection) {
|
|
115558
|
+
switch (detection.framework) {
|
|
115559
|
+
case "next":
|
|
115560
|
+
case "remix":
|
|
115561
|
+
case "react-router":
|
|
115562
|
+
return [REACT_CHECK];
|
|
115563
|
+
case "vite":
|
|
115564
|
+
return detection.react ? [REACT_CHECK] : [];
|
|
115565
|
+
case "nuxt":
|
|
115566
|
+
return [
|
|
115567
|
+
{
|
|
115568
|
+
pkg: "nuxt",
|
|
115569
|
+
min: "3.0.0",
|
|
115570
|
+
label: "Nuxt",
|
|
115571
|
+
reason: "Fimo supports Nuxt 3+ (Vue 3). Nuxt 2 is end-of-life.",
|
|
115572
|
+
remedy: "Upgrade to Nuxt 3 or newer."
|
|
115573
|
+
}
|
|
115574
|
+
];
|
|
115575
|
+
case "vue":
|
|
115576
|
+
return [
|
|
115577
|
+
{
|
|
115578
|
+
pkg: "vue",
|
|
115579
|
+
min: "3.0.0",
|
|
115580
|
+
label: "Vue",
|
|
115581
|
+
reason: "Fimo supports Vue 3+. Vue 2 is end-of-life.",
|
|
115582
|
+
remedy: "Upgrade to Vue 3 or newer."
|
|
115583
|
+
}
|
|
115584
|
+
];
|
|
115585
|
+
case "astro":
|
|
115586
|
+
return [
|
|
115587
|
+
{
|
|
115588
|
+
pkg: "astro",
|
|
115589
|
+
min: "4.0.0",
|
|
115590
|
+
label: "Astro",
|
|
115591
|
+
reason: "Fimo supports Astro 4+.",
|
|
115592
|
+
remedy: "Upgrade Astro to 4 or newer."
|
|
115593
|
+
}
|
|
115594
|
+
];
|
|
115595
|
+
case "sveltekit":
|
|
115596
|
+
return [
|
|
115597
|
+
{
|
|
115598
|
+
pkg: "@sveltejs/kit",
|
|
115599
|
+
min: "2.0.0",
|
|
115600
|
+
label: "SvelteKit",
|
|
115601
|
+
reason: "Fimo supports SvelteKit 2+.",
|
|
115602
|
+
remedy: "Upgrade @sveltejs/kit to 2 or newer."
|
|
115603
|
+
}
|
|
115604
|
+
];
|
|
115605
|
+
default:
|
|
115606
|
+
return [];
|
|
115607
|
+
}
|
|
115608
|
+
}
|
|
115609
|
+
__name(supportChecksFor, "supportChecksFor");
|
|
115610
|
+
function parseVersionFloor(spec) {
|
|
115611
|
+
if (!spec) {
|
|
115612
|
+
return null;
|
|
115613
|
+
}
|
|
115614
|
+
const first2 = spec.split("||")[0];
|
|
115615
|
+
const match = first2.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
115616
|
+
if (!match) {
|
|
115617
|
+
return null;
|
|
115618
|
+
}
|
|
115619
|
+
return {
|
|
115620
|
+
major: Number(match[1]),
|
|
115621
|
+
minor: match[2] ? Number(match[2]) : 0,
|
|
115622
|
+
patch: match[3] ? Number(match[3]) : 0,
|
|
115623
|
+
raw: spec.trim()
|
|
115624
|
+
};
|
|
115625
|
+
}
|
|
115626
|
+
__name(parseVersionFloor, "parseVersionFloor");
|
|
115627
|
+
function isBelowFloor(found, min) {
|
|
115628
|
+
const floor = parseVersionFloor(min);
|
|
115629
|
+
if (!floor) {
|
|
115630
|
+
return false;
|
|
115631
|
+
}
|
|
115632
|
+
if (found.major !== floor.major) {
|
|
115633
|
+
return found.major < floor.major;
|
|
115634
|
+
}
|
|
115635
|
+
if (found.minor !== floor.minor) {
|
|
115636
|
+
return found.minor < floor.minor;
|
|
115637
|
+
}
|
|
115638
|
+
return found.patch < floor.patch;
|
|
115639
|
+
}
|
|
115640
|
+
__name(isBelowFloor, "isBelowFloor");
|
|
115641
|
+
function readHostVersion(cwd, pkg) {
|
|
115642
|
+
const installed = join27(cwd, "node_modules", pkg, "package.json");
|
|
115643
|
+
if (existsSync16(installed)) {
|
|
115644
|
+
try {
|
|
115645
|
+
const version2 = JSON.parse(readFileSync20(installed, "utf8")).version;
|
|
115646
|
+
const parsed = parseVersionFloor(String(version2));
|
|
115647
|
+
if (parsed) {
|
|
115648
|
+
return parsed;
|
|
115649
|
+
}
|
|
115650
|
+
} catch {
|
|
115651
|
+
}
|
|
115652
|
+
}
|
|
115653
|
+
const rootPkg = join27(cwd, "package.json");
|
|
115654
|
+
if (existsSync16(rootPkg)) {
|
|
115655
|
+
try {
|
|
115656
|
+
const pkgJson = JSON.parse(readFileSync20(rootPkg, "utf8"));
|
|
115657
|
+
const spec = { ...pkgJson.dependencies, ...pkgJson.devDependencies }[pkg];
|
|
115658
|
+
if (spec) {
|
|
115659
|
+
return parseVersionFloor(String(spec));
|
|
115660
|
+
}
|
|
115661
|
+
} catch {
|
|
115662
|
+
}
|
|
115663
|
+
}
|
|
115664
|
+
return null;
|
|
115665
|
+
}
|
|
115666
|
+
__name(readHostVersion, "readHostVersion");
|
|
115667
|
+
function checkHostSupport(cwd, detection) {
|
|
115668
|
+
const violations = [];
|
|
115669
|
+
const detected = [];
|
|
115670
|
+
for (const check2 of supportChecksFor(detection)) {
|
|
115671
|
+
const found = readHostVersion(cwd, check2.pkg);
|
|
115672
|
+
if (!found) {
|
|
115673
|
+
continue;
|
|
115674
|
+
}
|
|
115675
|
+
detected.push({ label: check2.label, version: found.raw });
|
|
115676
|
+
if (isBelowFloor(found, check2.min)) {
|
|
115677
|
+
violations.push({
|
|
115678
|
+
subject: check2.label,
|
|
115679
|
+
found: found.raw,
|
|
115680
|
+
min: check2.min,
|
|
115681
|
+
headline: `${check2.label} ${found.raw} is below Fimo's floor of ${check2.label} ${check2.min}. ${check2.reason}`,
|
|
115682
|
+
remedy: check2.remedy
|
|
115683
|
+
});
|
|
115684
|
+
}
|
|
115685
|
+
}
|
|
115686
|
+
return { ok: violations.length === 0, violations, detected };
|
|
115687
|
+
}
|
|
115688
|
+
__name(checkHostSupport, "checkHostSupport");
|
|
115689
|
+
|
|
115690
|
+
// src/cli/commands/migrate-wrap.ts
|
|
115540
115691
|
init_scan_wrappable();
|
|
115541
115692
|
init_fimo_dep();
|
|
115542
115693
|
init_color();
|
|
@@ -116097,6 +116248,16 @@ async function migrateWrapCommand(options) {
|
|
|
116097
116248
|
}
|
|
116098
116249
|
const detection = detectFramework(cwd);
|
|
116099
116250
|
const framework = typeof config2.framework === "string" ? config2.framework : detection.framework;
|
|
116251
|
+
const support = checkHostSupport(cwd, detection);
|
|
116252
|
+
if (!support.ok) {
|
|
116253
|
+
ui.error({
|
|
116254
|
+
command: "migrate wrap",
|
|
116255
|
+
code: "HOST_UNSUPPORTED",
|
|
116256
|
+
message: support.violations.map((violation) => violation.headline).join(" "),
|
|
116257
|
+
hint: support.violations.map((violation) => violation.remedy).join(" ")
|
|
116258
|
+
});
|
|
116259
|
+
return;
|
|
116260
|
+
}
|
|
116100
116261
|
if (!options.check && currentMode().format === "pretty") {
|
|
116101
116262
|
process.stdout.write("\n");
|
|
116102
116263
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { FrameworkDetection } from '../detect-framework.js';
|
|
2
|
+
/**
|
|
3
|
+
* A host dependency the wrap gates on, with the floor Fimo's runtime requires.
|
|
4
|
+
* The floor is honest to the API surface the in-tree runtime actually uses —
|
|
5
|
+
* React frameworks gate on `react` 18 (the runtime touches `useSyncExternalStore`),
|
|
6
|
+
* Vue/Svelte/Astro on their own framework floor. The version gate is what turns
|
|
7
|
+
* an unsupported host into one actionable sentence instead of an npm `ERESOLVE`
|
|
8
|
+
* or an install-then-crash.
|
|
9
|
+
*/
|
|
10
|
+
export interface SupportCheck {
|
|
11
|
+
pkg: string;
|
|
12
|
+
/** Floor, `X.Y.Z`. */
|
|
13
|
+
min: string;
|
|
14
|
+
label: string;
|
|
15
|
+
/** Why this floor exists — shown to the user. */
|
|
16
|
+
reason: string;
|
|
17
|
+
/** One actionable next step — shown as the error hint. */
|
|
18
|
+
remedy: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The runtime-floor checks for a detected framework. React-based frameworks all
|
|
22
|
+
* gate on `react`; the template frameworks gate on their own package. Frameworks
|
|
23
|
+
* with no runtime coupling (or none detected) return [] — nothing to gate.
|
|
24
|
+
*/
|
|
25
|
+
export declare function supportChecksFor(detection: Pick<FrameworkDetection, 'framework' | 'react'>): SupportCheck[];
|
|
26
|
+
interface ParsedVersion {
|
|
27
|
+
major: number;
|
|
28
|
+
minor: number;
|
|
29
|
+
patch: number;
|
|
30
|
+
/** The original spec, for display. */
|
|
31
|
+
raw: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The lowest concrete version a spec implies. Handles the shapes seen in real
|
|
35
|
+
* package.json / installed manifests: `^18.2.0`, `~3.4`, `>=2.0.0`, `18 || 19`,
|
|
36
|
+
* `18.3.1`, ` >= 14.0.0 `, bare `18`. Returns null for anything without a
|
|
37
|
+
* numeric floor (`latest`, `*`, `workspace:*`, `catalog:`, a git/file url) — we
|
|
38
|
+
* never gate on a version we cannot read.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseVersionFloor(spec: string): ParsedVersion | null;
|
|
41
|
+
/**
|
|
42
|
+
* The host's version of `pkg`. Prefers the INSTALLED manifest
|
|
43
|
+
* (`node_modules/<pkg>/package.json`) — the ground truth of what will run —
|
|
44
|
+
* and falls back to the declared range floor in the host package.json (a fresh
|
|
45
|
+
* clone before install). Returns null when neither is readable.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readHostVersion(cwd: string, pkg: string): ParsedVersion | null;
|
|
48
|
+
export interface HostSupportViolation {
|
|
49
|
+
subject: string;
|
|
50
|
+
/** The version we found on the host. */
|
|
51
|
+
found: string;
|
|
52
|
+
/** The floor Fimo requires. */
|
|
53
|
+
min: string;
|
|
54
|
+
/** Headline sentence for the error message. */
|
|
55
|
+
headline: string;
|
|
56
|
+
/** Actionable next step for the error hint. */
|
|
57
|
+
remedy: string;
|
|
58
|
+
}
|
|
59
|
+
export interface HostSupportResult {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
violations: HostSupportViolation[];
|
|
62
|
+
/** Versions read successfully — for the summary line ("React 18.3.1"). */
|
|
63
|
+
detected: Array<{
|
|
64
|
+
label: string;
|
|
65
|
+
version: string;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Gate the wrap on the host's framework/runtime versions. Silent for supported
|
|
70
|
+
* hosts (the common case); returns one violation per below-floor dependency so
|
|
71
|
+
* the caller can stop with a clear message. A version we cannot read is never a
|
|
72
|
+
* violation — the peer range and the install still catch a genuinely absent dep.
|
|
73
|
+
*/
|
|
74
|
+
export declare function checkHostSupport(cwd: string, detection: Pick<FrameworkDetection, 'framework' | 'react'>): HostSupportResult;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const REACT_CHECK = {
|
|
4
|
+
pkg: 'react',
|
|
5
|
+
min: '18.0.0',
|
|
6
|
+
label: 'React',
|
|
7
|
+
reason: "Fimo's inline-editing runtime uses React 18 APIs (useSyncExternalStore).",
|
|
8
|
+
remedy: 'Upgrade React to 18 or newer. The content still migrates on older React; only inline editing needs 18+.',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The runtime-floor checks for a detected framework. React-based frameworks all
|
|
12
|
+
* gate on `react`; the template frameworks gate on their own package. Frameworks
|
|
13
|
+
* with no runtime coupling (or none detected) return [] — nothing to gate.
|
|
14
|
+
*/
|
|
15
|
+
export function supportChecksFor(detection) {
|
|
16
|
+
switch (detection.framework) {
|
|
17
|
+
case 'next':
|
|
18
|
+
case 'remix':
|
|
19
|
+
case 'react-router':
|
|
20
|
+
return [REACT_CHECK];
|
|
21
|
+
case 'vite':
|
|
22
|
+
return detection.react ? [REACT_CHECK] : [];
|
|
23
|
+
case 'nuxt':
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
pkg: 'nuxt',
|
|
27
|
+
min: '3.0.0',
|
|
28
|
+
label: 'Nuxt',
|
|
29
|
+
reason: 'Fimo supports Nuxt 3+ (Vue 3). Nuxt 2 is end-of-life.',
|
|
30
|
+
remedy: 'Upgrade to Nuxt 3 or newer.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
case 'vue':
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
pkg: 'vue',
|
|
37
|
+
min: '3.0.0',
|
|
38
|
+
label: 'Vue',
|
|
39
|
+
reason: 'Fimo supports Vue 3+. Vue 2 is end-of-life.',
|
|
40
|
+
remedy: 'Upgrade to Vue 3 or newer.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
case 'astro':
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
pkg: 'astro',
|
|
47
|
+
min: '4.0.0',
|
|
48
|
+
label: 'Astro',
|
|
49
|
+
reason: 'Fimo supports Astro 4+.',
|
|
50
|
+
remedy: 'Upgrade Astro to 4 or newer.',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
case 'sveltekit':
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
pkg: '@sveltejs/kit',
|
|
57
|
+
min: '2.0.0',
|
|
58
|
+
label: 'SvelteKit',
|
|
59
|
+
reason: 'Fimo supports SvelteKit 2+.',
|
|
60
|
+
remedy: 'Upgrade @sveltejs/kit to 2 or newer.',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
default:
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The lowest concrete version a spec implies. Handles the shapes seen in real
|
|
69
|
+
* package.json / installed manifests: `^18.2.0`, `~3.4`, `>=2.0.0`, `18 || 19`,
|
|
70
|
+
* `18.3.1`, ` >= 14.0.0 `, bare `18`. Returns null for anything without a
|
|
71
|
+
* numeric floor (`latest`, `*`, `workspace:*`, `catalog:`, a git/file url) — we
|
|
72
|
+
* never gate on a version we cannot read.
|
|
73
|
+
*/
|
|
74
|
+
export function parseVersionFloor(spec) {
|
|
75
|
+
if (!spec) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// A `||` union — the floor is the lowest branch, i.e. the first token.
|
|
79
|
+
const first = spec.split('||')[0];
|
|
80
|
+
const match = first.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
81
|
+
if (!match) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
major: Number(match[1]),
|
|
86
|
+
minor: match[2] ? Number(match[2]) : 0,
|
|
87
|
+
patch: match[3] ? Number(match[3]) : 0,
|
|
88
|
+
raw: spec.trim(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function isBelowFloor(found, min) {
|
|
92
|
+
const floor = parseVersionFloor(min);
|
|
93
|
+
if (!floor) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (found.major !== floor.major) {
|
|
97
|
+
return found.major < floor.major;
|
|
98
|
+
}
|
|
99
|
+
if (found.minor !== floor.minor) {
|
|
100
|
+
return found.minor < floor.minor;
|
|
101
|
+
}
|
|
102
|
+
return found.patch < floor.patch;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The host's version of `pkg`. Prefers the INSTALLED manifest
|
|
106
|
+
* (`node_modules/<pkg>/package.json`) — the ground truth of what will run —
|
|
107
|
+
* and falls back to the declared range floor in the host package.json (a fresh
|
|
108
|
+
* clone before install). Returns null when neither is readable.
|
|
109
|
+
*/
|
|
110
|
+
export function readHostVersion(cwd, pkg) {
|
|
111
|
+
const installed = join(cwd, 'node_modules', pkg, 'package.json');
|
|
112
|
+
if (existsSync(installed)) {
|
|
113
|
+
try {
|
|
114
|
+
const version = JSON.parse(readFileSync(installed, 'utf8')).version;
|
|
115
|
+
const parsed = parseVersionFloor(String(version));
|
|
116
|
+
if (parsed) {
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// fall through to the declared range
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const rootPkg = join(cwd, 'package.json');
|
|
125
|
+
if (existsSync(rootPkg)) {
|
|
126
|
+
try {
|
|
127
|
+
const pkgJson = JSON.parse(readFileSync(rootPkg, 'utf8'));
|
|
128
|
+
const spec = { ...pkgJson.dependencies, ...pkgJson.devDependencies }[pkg];
|
|
129
|
+
if (spec) {
|
|
130
|
+
return parseVersionFloor(String(spec));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// unreadable — nothing to gate on
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Gate the wrap on the host's framework/runtime versions. Silent for supported
|
|
141
|
+
* hosts (the common case); returns one violation per below-floor dependency so
|
|
142
|
+
* the caller can stop with a clear message. A version we cannot read is never a
|
|
143
|
+
* violation — the peer range and the install still catch a genuinely absent dep.
|
|
144
|
+
*/
|
|
145
|
+
export function checkHostSupport(cwd, detection) {
|
|
146
|
+
const violations = [];
|
|
147
|
+
const detected = [];
|
|
148
|
+
for (const check of supportChecksFor(detection)) {
|
|
149
|
+
const found = readHostVersion(cwd, check.pkg);
|
|
150
|
+
if (!found) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
detected.push({ label: check.label, version: found.raw });
|
|
154
|
+
if (isBelowFloor(found, check.min)) {
|
|
155
|
+
violations.push({
|
|
156
|
+
subject: check.label,
|
|
157
|
+
found: found.raw,
|
|
158
|
+
min: check.min,
|
|
159
|
+
headline: `${check.label} ${found.raw} is below Fimo's floor of ${check.label} ${check.min}. ${check.reason}`,
|
|
160
|
+
remedy: check.remedy,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { ok: violations.length === 0, violations, detected };
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { checkHostSupport, parseVersionFloor, readHostVersion } from './support.js';
|
|
6
|
+
let dir;
|
|
7
|
+
function project(pkg, installed = {}) {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'host-support-'));
|
|
9
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg));
|
|
10
|
+
for (const [name, version] of Object.entries(installed)) {
|
|
11
|
+
const modDir = join(dir, 'node_modules', name);
|
|
12
|
+
mkdirSync(modDir, { recursive: true });
|
|
13
|
+
writeFileSync(join(modDir, 'package.json'), JSON.stringify({ name, version }));
|
|
14
|
+
}
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (dir) {
|
|
19
|
+
rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
describe('parseVersionFloor', () => {
|
|
23
|
+
it('reads the lowest concrete version from real spec shapes', () => {
|
|
24
|
+
expect(parseVersionFloor('^18.2.0')).toMatchObject({ major: 18, minor: 2, patch: 0 });
|
|
25
|
+
expect(parseVersionFloor('>=2.0.0')).toMatchObject({ major: 2, minor: 0, patch: 0 });
|
|
26
|
+
expect(parseVersionFloor('~3.4')).toMatchObject({ major: 3, minor: 4, patch: 0 });
|
|
27
|
+
expect(parseVersionFloor('18')).toMatchObject({ major: 18, minor: 0, patch: 0 });
|
|
28
|
+
// a `||` union floors on the lowest (first) branch
|
|
29
|
+
expect(parseVersionFloor('^17.0.0 || ^18.0.0')).toMatchObject({ major: 17 });
|
|
30
|
+
});
|
|
31
|
+
it('returns null for specs with no numeric floor', () => {
|
|
32
|
+
expect(parseVersionFloor('latest')).toBeNull();
|
|
33
|
+
expect(parseVersionFloor('*')).toBeNull();
|
|
34
|
+
expect(parseVersionFloor('workspace:*')).toBeNull();
|
|
35
|
+
expect(parseVersionFloor('')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('readHostVersion', () => {
|
|
39
|
+
it('prefers the installed manifest over the declared range', () => {
|
|
40
|
+
const cwd = project({ dependencies: { react: '^18.0.0' } }, { react: '18.3.1' });
|
|
41
|
+
expect(readHostVersion(cwd, 'react')).toMatchObject({ major: 18, minor: 3, patch: 1 });
|
|
42
|
+
});
|
|
43
|
+
it('falls back to the declared range when nothing is installed', () => {
|
|
44
|
+
const cwd = project({ dependencies: { react: '^19.0.0' } });
|
|
45
|
+
expect(readHostVersion(cwd, 'react')).toMatchObject({ major: 19, minor: 0, patch: 0 });
|
|
46
|
+
});
|
|
47
|
+
it('returns null when the dependency is absent', () => {
|
|
48
|
+
const cwd = project({ dependencies: {} });
|
|
49
|
+
expect(readHostVersion(cwd, 'react')).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('checkHostSupport', () => {
|
|
53
|
+
it('passes a Next.js site on React 18', () => {
|
|
54
|
+
const cwd = project({ dependencies: { next: '16.2.11', react: '^18.2.0' } }, { react: '18.3.1' });
|
|
55
|
+
const result = checkHostSupport(cwd, { framework: 'next' });
|
|
56
|
+
expect(result.ok).toBe(true);
|
|
57
|
+
expect(result.violations).toEqual([]);
|
|
58
|
+
expect(result.detected).toContainEqual({ label: 'React', version: '18.3.1' });
|
|
59
|
+
});
|
|
60
|
+
it('passes a Next.js site on React 19', () => {
|
|
61
|
+
const cwd = project({ dependencies: { next: '16.2.11', react: '^19.0.0' } }, { react: '19.0.0' });
|
|
62
|
+
expect(checkHostSupport(cwd, { framework: 'next' }).ok).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('flags a React 17 site below the floor with an actionable message', () => {
|
|
65
|
+
const cwd = project({ dependencies: { next: '13.0.0', react: '^17.0.2' } }, { react: '17.0.2' });
|
|
66
|
+
const result = checkHostSupport(cwd, { framework: 'next' });
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
expect(result.violations).toHaveLength(1);
|
|
69
|
+
expect(result.violations[0]).toMatchObject({ subject: 'React', min: '18.0.0' });
|
|
70
|
+
expect(result.violations[0].headline).toContain('React 17.0.2');
|
|
71
|
+
expect(result.violations[0].remedy).toMatch(/upgrade react/i);
|
|
72
|
+
});
|
|
73
|
+
it('flags a Nuxt 2 site below the floor', () => {
|
|
74
|
+
const cwd = project({ dependencies: { nuxt: '^2.17.0' } }, { nuxt: '2.17.0' });
|
|
75
|
+
const result = checkHostSupport(cwd, { framework: 'nuxt' });
|
|
76
|
+
expect(result.ok).toBe(false);
|
|
77
|
+
expect(result.violations[0]).toMatchObject({ subject: 'Nuxt', min: '3.0.0' });
|
|
78
|
+
});
|
|
79
|
+
it('does not gate a non-React bare Vite site', () => {
|
|
80
|
+
const cwd = project({ dependencies: { vite: '^5.0.0' } });
|
|
81
|
+
expect(checkHostSupport(cwd, { framework: 'vite', react: false }).ok).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it('does not gate on a version it cannot read', () => {
|
|
84
|
+
const cwd = project({ dependencies: { next: '16.0.0', react: 'workspace:*' } });
|
|
85
|
+
expect(checkHostSupport(cwd, { framework: 'next' }).ok).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.7.0-experimental.
|
|
3
|
+
"version": "0.7.0-experimental.7",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
"astro": ">=4.0.0",
|
|
159
159
|
"next": ">=14.0.0",
|
|
160
160
|
"nuxt": ">=3.0.0",
|
|
161
|
-
"react": "^19.0.0",
|
|
162
|
-
"react-dom": "^19.0.0",
|
|
161
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
162
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
163
163
|
"react-router": "^7.0.0",
|
|
164
164
|
"vue": ">=3.0.0"
|
|
165
165
|
},
|