fimo 0.8.0 → 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.
Files changed (218) hide show
  1. package/assets/agent-templates/content-translator/scripts/write-locale-files.ts +66 -0
  2. package/assets/agents-starter/claude/settings.json +19 -0
  3. package/assets/content-templates/hooks-template.eta +4 -2
  4. package/assets/content-templates/types-template.eta +67 -1
  5. package/assets/skills/fimo/SKILL.md +1 -1
  6. package/assets/skills/fimo/references/content.md +86 -15
  7. package/assets/skills/fimo/references/translations.md +75 -0
  8. package/assets/skills/fimo-cli/SKILL.md +6 -2
  9. package/assets/skills/fimo-cli/references/content.md +61 -2
  10. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
  11. package/assets/skills/fimo-cli/references/integrations.md +5 -1
  12. package/assets/skills/fimo-cli/references/webhooks.md +162 -0
  13. package/assets/skills/fimo-migration/SKILL.md +24 -2
  14. package/assets/skills/fimo-migration/references/content.md +24 -0
  15. package/assets/skills/fimo-migration/references/manifest.md +46 -0
  16. package/assets/skills/fimo-migration/references/pages.md +234 -0
  17. package/assets/skills/fimo-migration/references/qa.md +24 -2
  18. package/assets/skills/fimo-migration/references/workflow.md +12 -1
  19. package/assets/skills/fimo-migration/references/wrap.md +662 -0
  20. package/assets/skills/fimo-studio/SKILL.md +5 -3
  21. package/assets/skills/fimo-studio/references/cms.md +9 -1
  22. package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
  23. package/assets/skills/fimo-studio/references/webhooks.md +42 -0
  24. package/dist/build/vite/plugins/translations.d.ts +2 -8
  25. package/dist/build/vite/plugins/translations.js +31 -131
  26. package/dist/cli/bundle.json +1 -1
  27. package/dist/cli/index.js +1337 -189
  28. package/dist/runtime/app/FimoProviders.d.ts +1 -11
  29. package/dist/runtime/app/FimoProviders.js +31 -30
  30. package/dist/runtime/astro/index.d.ts +2 -0
  31. package/dist/runtime/astro/index.js +3 -0
  32. package/dist/runtime/astro/server.d.ts +3 -0
  33. package/dist/runtime/astro/server.js +6 -0
  34. package/dist/runtime/content/client/internal.js +1 -0
  35. package/dist/runtime/content/client/types.d.ts +16 -0
  36. package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
  37. package/dist/runtime/core/BuiltWithBadge.js +68 -0
  38. package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
  39. package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
  40. package/dist/runtime/next/FimoNextProviders.js +20 -0
  41. package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
  42. package/dist/runtime/next/FimoNextScripts.js +19 -0
  43. package/dist/runtime/next/index.d.ts +14 -0
  44. package/dist/runtime/next/index.js +14 -0
  45. package/dist/runtime/next/server.d.ts +2 -0
  46. package/dist/runtime/next/server.js +4 -0
  47. package/dist/runtime/nuxt/composables.d.ts +9 -0
  48. package/dist/runtime/nuxt/composables.js +57 -0
  49. package/dist/runtime/nuxt/index.d.ts +3 -0
  50. package/dist/runtime/nuxt/index.js +6 -0
  51. package/dist/runtime/nuxt/server.d.ts +3 -0
  52. package/dist/runtime/nuxt/server.js +6 -0
  53. package/dist/runtime/primitives/components/RichText.d.ts +5 -1
  54. package/dist/runtime/primitives/components/RichText.js +2 -2
  55. package/dist/runtime/primitives/labels-hook.d.ts +33 -0
  56. package/dist/runtime/primitives/labels-hook.js +41 -0
  57. package/dist/runtime/primitives/labels-store.d.ts +12 -0
  58. package/dist/runtime/primitives/labels-store.js +60 -0
  59. package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
  60. package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
  61. package/dist/runtime/primitives/translations.d.ts +1 -27
  62. package/dist/runtime/primitives/translations.js +7 -85
  63. package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
  64. package/dist/runtime/react/FimoSpaProviders.js +122 -0
  65. package/dist/runtime/react/index.d.ts +35 -0
  66. package/dist/runtime/react/index.js +40 -0
  67. package/dist/runtime/react/spa-content-cache.d.ts +38 -0
  68. package/dist/runtime/react/spa-content-cache.js +64 -0
  69. package/dist/runtime/remix/FimoRemixProviders.d.ts +16 -0
  70. package/dist/runtime/remix/FimoRemixProviders.js +12 -0
  71. package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
  72. package/dist/runtime/remix/FimoRemixScripts.js +19 -0
  73. package/dist/runtime/remix/index.d.ts +12 -0
  74. package/dist/runtime/remix/index.js +11 -0
  75. package/dist/runtime/remix/server.d.ts +2 -0
  76. package/dist/runtime/remix/server.js +2 -0
  77. package/dist/runtime/seo/neutral.d.ts +22 -0
  78. package/dist/runtime/seo/neutral.js +17 -0
  79. package/dist/runtime/shared/display-routes.d.ts +14 -0
  80. package/dist/runtime/shared/display-routes.js +25 -0
  81. package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
  82. package/dist/runtime/shared/fimo-config.server.js +1 -13
  83. package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
  84. package/dist/runtime/shared/i18n-messages.d.ts +29 -0
  85. package/dist/runtime/shared/i18n-messages.js +79 -0
  86. package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
  87. package/dist/runtime/shared/labels-fetch.pure.js +71 -0
  88. package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
  89. package/dist/runtime/shared/labels-fetch.server.js +97 -0
  90. package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
  91. package/dist/runtime/shared/labels-t.pure.js +10 -0
  92. package/dist/runtime/strapi/client.d.ts +30 -0
  93. package/dist/runtime/strapi/client.js +132 -0
  94. package/dist/runtime/strapi/index.d.ts +5 -0
  95. package/dist/runtime/strapi/index.js +6 -0
  96. package/dist/runtime/strapi/reshape.d.ts +50 -0
  97. package/dist/runtime/strapi/reshape.js +121 -0
  98. package/dist/runtime/svelte/index.d.ts +3 -0
  99. package/dist/runtime/svelte/index.js +7 -0
  100. package/dist/runtime/svelte/server.d.ts +3 -0
  101. package/dist/runtime/svelte/server.js +6 -0
  102. package/dist/runtime/vue/index.d.ts +3 -0
  103. package/dist/runtime/vue/index.js +5 -0
  104. package/dist/runtime/vue/server.d.ts +3 -0
  105. package/dist/runtime/vue/server.js +6 -0
  106. package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
  107. package/dist/runtime/wrapped/FimoFileLink.js +19 -0
  108. package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
  109. package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
  110. package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
  111. package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
  112. package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
  113. package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
  114. package/dist/runtime/wrapped/FimoText.d.ts +22 -0
  115. package/dist/runtime/wrapped/FimoText.js +20 -0
  116. package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
  117. package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
  118. package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
  119. package/dist/runtime/wrapped/WrappedProviders.js +101 -0
  120. package/dist/runtime/wrapped/content-fetch.pure.d.ts +20 -0
  121. package/dist/runtime/wrapped/content-fetch.pure.js +106 -0
  122. package/dist/runtime/wrapped/dom.d.ts +4 -0
  123. package/dist/runtime/wrapped/dom.js +23 -0
  124. package/dist/runtime/wrapped/server.d.ts +80 -0
  125. package/dist/runtime/wrapped/server.js +207 -0
  126. package/dist/scripts/apply-wrap-templates.d.ts +61 -0
  127. package/dist/scripts/apply-wrap-templates.js +137 -0
  128. package/dist/scripts/apply-wrap.d.ts +73 -0
  129. package/dist/scripts/apply-wrap.js +340 -0
  130. package/dist/scripts/cms/discover.d.ts +2 -0
  131. package/dist/scripts/cms/discover.js +17 -0
  132. package/dist/scripts/cms/example-content.d.ts +33 -0
  133. package/dist/scripts/cms/example-content.js +165 -0
  134. package/dist/scripts/cms/strapi/detect.d.ts +2 -0
  135. package/dist/scripts/cms/strapi/detect.js +186 -0
  136. package/dist/scripts/cms/strapi/index.d.ts +3 -0
  137. package/dist/scripts/cms/strapi/index.js +25 -0
  138. package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
  139. package/dist/scripts/cms/strapi/list-source.js +90 -0
  140. package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
  141. package/dist/scripts/cms/strapi/schema-map.js +289 -0
  142. package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
  143. package/dist/scripts/cms/strapi/shapes.js +48 -0
  144. package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
  145. package/dist/scripts/cms/strapi/touchpoints.js +145 -0
  146. package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
  147. package/dist/scripts/cms/strapi/wire-client.js +107 -0
  148. package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
  149. package/dist/scripts/cms/strapi/wiring.js +46 -0
  150. package/dist/scripts/cms/types.d.ts +145 -0
  151. package/dist/scripts/cms/types.js +11 -0
  152. package/dist/scripts/detect-framework.d.ts +39 -0
  153. package/dist/scripts/detect-framework.js +139 -0
  154. package/dist/scripts/draft-rows.d.ts +19 -0
  155. package/dist/scripts/draft-rows.js +178 -0
  156. package/dist/scripts/draft-schemas.d.ts +27 -0
  157. package/dist/scripts/draft-schemas.js +247 -0
  158. package/dist/scripts/extract-translations.js +16 -11
  159. package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
  160. package/dist/scripts/frameworks/astro/routes.js +44 -0
  161. package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
  162. package/dist/scripts/frameworks/astro/template-style.js +19 -0
  163. package/dist/scripts/frameworks/discover.d.ts +83 -0
  164. package/dist/scripts/frameworks/discover.js +288 -0
  165. package/dist/scripts/frameworks/discovered-route.d.ts +9 -0
  166. package/dist/scripts/frameworks/discovered-route.js +17 -0
  167. package/dist/scripts/frameworks/next/rewire.d.ts +43 -0
  168. package/dist/scripts/frameworks/next/rewire.js +282 -0
  169. package/dist/scripts/frameworks/next/routes.d.ts +30 -0
  170. package/dist/scripts/frameworks/next/routes.js +176 -0
  171. package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
  172. package/dist/scripts/frameworks/next/wiring.js +186 -0
  173. package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
  174. package/dist/scripts/frameworks/nuxt/routes.js +38 -0
  175. package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
  176. package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
  177. package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
  178. package/dist/scripts/frameworks/remix/routes.js +36 -0
  179. package/dist/scripts/frameworks/support.d.ts +75 -0
  180. package/dist/scripts/frameworks/support.js +165 -0
  181. package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
  182. package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
  183. package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
  184. package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
  185. package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
  186. package/dist/scripts/frameworks/vite/routes.js +143 -0
  187. package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
  188. package/dist/scripts/frameworks/vite/wiring.js +144 -0
  189. package/dist/scripts/i18n-namespace.d.ts +27 -0
  190. package/dist/scripts/i18n-namespace.js +155 -0
  191. package/dist/scripts/lib/humanize.d.ts +14 -0
  192. package/dist/scripts/lib/humanize.js +54 -0
  193. package/dist/scripts/lib/validation-report.d.ts +1 -3
  194. package/dist/scripts/lib/validation-report.js +3 -9
  195. package/dist/scripts/render-parity/capture.d.ts +64 -0
  196. package/dist/scripts/render-parity/capture.js +436 -0
  197. package/dist/scripts/render-parity/compare.d.ts +59 -0
  198. package/dist/scripts/render-parity/compare.js +0 -0
  199. package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
  200. package/dist/scripts/render-parity/normalize-html.js +223 -0
  201. package/dist/scripts/render-parity/types.d.ts +41 -0
  202. package/dist/scripts/render-parity/types.js +1 -0
  203. package/dist/scripts/routes/bindings.d.ts +93 -0
  204. package/dist/scripts/routes/bindings.js +117 -0
  205. package/dist/scripts/routes/label-prefixes.d.ts +49 -0
  206. package/dist/scripts/routes/label-prefixes.js +136 -0
  207. package/dist/scripts/routes/module-resolve.d.ts +20 -0
  208. package/dist/scripts/routes/module-resolve.js +78 -0
  209. package/dist/scripts/routes/page-structure.d.ts +72 -0
  210. package/dist/scripts/routes/page-structure.js +234 -0
  211. package/dist/scripts/scan-wrappable.d.ts +200 -0
  212. package/dist/scripts/scan-wrappable.js +1355 -0
  213. package/dist/scripts/validate-route-metadata.js +11 -0
  214. package/dist/scripts/validate-schemas.js +42 -1
  215. package/package.json +88 -4
  216. package/release.json +2 -2
  217. package/templates/react-router/package.json +1 -1
  218. package/templates/react-router/translations/en.json +1 -0
@@ -0,0 +1,64 @@
1
+ import { type ParityBaseline } from './types.js';
2
+ export interface CaptureOptions {
3
+ cwd: string;
4
+ packageManager: string;
5
+ routes: Array<{
6
+ path: string;
7
+ }>;
8
+ framework: string | null;
9
+ bootTimeoutMs?: number;
10
+ routeTimeoutMs?: number;
11
+ /** Reuse an already-running dev server instead of booting one: the warm
12
+ * server recompiles incrementally, so repeated checks cost seconds. The
13
+ * server is probed once and never stopped. */
14
+ serverOrigin?: string;
15
+ /** Concrete values for dynamic route params so `[locale]`-style routes are
16
+ * actually captured instead of skipped. A route resolves (and is captured)
17
+ * only when EVERY `:param` in it has a value here; both baseline and check
18
+ * read the same values, so the comparison stays symmetric. */
19
+ paramValues?: Record<string, string>;
20
+ onProgress?: (message: string) => void;
21
+ }
22
+ /** Substitute known params into a `:param` route path; returns the concrete
23
+ * path, or null when a param has no value (still unresolvable → skip). */
24
+ export declare function resolveRoutePath(routePath: string, paramValues: Record<string, string>): string | null;
25
+ export declare class CaptureError extends Error {
26
+ readonly stderrTail: string;
27
+ constructor(message: string, stderrTail?: string);
28
+ }
29
+ /**
30
+ * pnpm's minimumReleaseAge setting refuses freshly published packages —
31
+ * exactly what a just-published fimo experimental is. The refusal surfaces
32
+ * deep in install/boot output; detect it and hand back the one-line fix
33
+ * instead of leaving an opaque boot failure.
34
+ */
35
+ export declare function pnpmReleaseAgeHint(output: string): string | null;
36
+ export declare function findFreePort(): Promise<number>;
37
+ /**
38
+ * Build workspace packages the app needs BEFORE booting its dev server. Fimo
39
+ * runs the app's bare `next dev`, but in a Turborepo/pnpm monorepo the
40
+ * internal deps (`@repo/*`) aren't built by that — `turbo dev` would build
41
+ * them first. Builds are SCOPED to the flagged packages (turbo/pnpm/npm
42
+ * filters) — the root build script builds the whole app too and fails for
43
+ * reasons unrelated to the deps; it is only the last resort when no filter
44
+ * mechanism exists. A build failure is surfaced with an actionable message
45
+ * instead of a cryptic `Cannot find module` at boot.
46
+ */
47
+ export declare function ensureWorkspaceDepsBuilt(cwd: string, packageManager: string, onProgress?: (message: string) => void): void;
48
+ interface DevServer {
49
+ origin: string;
50
+ stderrTail: () => string;
51
+ stop: () => Promise<void>;
52
+ }
53
+ export declare function startDevServer(opts: {
54
+ cwd: string;
55
+ packageManager: string;
56
+ bootTimeoutMs: number;
57
+ }): Promise<DevServer>;
58
+ /**
59
+ * Boot the project's own dev server, capture every static route's normalized
60
+ * signature, and tear the server down. Baseline and check both run through
61
+ * this identical pipeline — that symmetry is the gate's correctness property.
62
+ */
63
+ export declare function captureParityBaseline(opts: CaptureOptions): Promise<ParityBaseline>;
64
+ export {};
@@ -0,0 +1,436 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { extractRouteSignature } from './normalize-html.js';
6
+ import { PARITY_BASELINE_VERSION } from './types.js';
7
+ /** Substitute known params into a `:param` route path; returns the concrete
8
+ * path, or null when a param has no value (still unresolvable → skip). */
9
+ export function resolveRoutePath(routePath, paramValues) {
10
+ const resolved = routePath.replace(/:([A-Za-z0-9_]+)/g, (_, name) => paramValues[name] ? encodeURIComponent(paramValues[name]) : `:${name}`);
11
+ return resolved.includes(':') ? null : resolved;
12
+ }
13
+ const DEFAULT_BOOT_TIMEOUT_MS = 90_000;
14
+ const DEFAULT_ROUTE_TIMEOUT_MS = 60_000;
15
+ const READINESS_POLL_MS = 500;
16
+ const KILL_GRACE_MS = 5_000;
17
+ export class CaptureError extends Error {
18
+ stderrTail;
19
+ constructor(message, stderrTail = '') {
20
+ super(message);
21
+ this.name = 'CaptureError';
22
+ this.stderrTail = stderrTail;
23
+ }
24
+ }
25
+ /**
26
+ * pnpm's minimumReleaseAge setting refuses freshly published packages —
27
+ * exactly what a just-published fimo experimental is. The refusal surfaces
28
+ * deep in install/boot output; detect it and hand back the one-line fix
29
+ * instead of leaving an opaque boot failure.
30
+ */
31
+ export function pnpmReleaseAgeHint(output) {
32
+ if (!/minimumReleaseAge|ERR_PNPM_.*RELEASE/i.test(output)) {
33
+ return null;
34
+ }
35
+ return ('pnpm is refusing a freshly published package (minimumReleaseAge). ' +
36
+ 'Add `minimumReleaseAgeExclude: ["fimo"]` to pnpm-workspace.yaml (or set `minimumReleaseAge: 0`), ' +
37
+ 're-run your install, then re-run `npx fimo migrate`.');
38
+ }
39
+ export function findFreePort() {
40
+ return new Promise((resolve, reject) => {
41
+ const server = net.createServer();
42
+ server.once('error', reject);
43
+ server.listen(0, '127.0.0.1', () => {
44
+ const address = server.address();
45
+ const port = typeof address === 'object' && address ? address.port : 0;
46
+ server.close(() => resolve(port));
47
+ });
48
+ });
49
+ }
50
+ function preflight(cwd) {
51
+ if (!fs.existsSync(path.join(cwd, 'node_modules'))) {
52
+ throw new CaptureError('node_modules is missing — run your package manager install first.');
53
+ }
54
+ try {
55
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
56
+ if (!pkg.scripts?.dev) {
57
+ throw new CaptureError('No `dev` script in package.json — the parity capture boots your dev server.');
58
+ }
59
+ }
60
+ catch (error) {
61
+ if (error instanceof CaptureError) {
62
+ throw error;
63
+ }
64
+ throw new CaptureError('Could not read package.json.');
65
+ }
66
+ }
67
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
68
+ /** The monorepo root above `cwd` (pnpm-workspace / turbo / npm workspaces),
69
+ * or null when `cwd` is standalone. */
70
+ function workspaceRoot(cwd) {
71
+ let dir = cwd;
72
+ for (let depth = 0; depth < 6; depth += 1) {
73
+ const parent = path.dirname(dir);
74
+ if (parent === dir) {
75
+ break;
76
+ }
77
+ dir = parent;
78
+ if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) || fs.existsSync(path.join(dir, 'turbo.json'))) {
79
+ return dir;
80
+ }
81
+ try {
82
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
83
+ if (pkg.workspaces) {
84
+ return dir;
85
+ }
86
+ }
87
+ catch {
88
+ // keep walking up
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ /** Every relative path target in an exports map (conditions and subpaths
94
+ * flattened; `require`/`import`/`types`/`default` values alike). */
95
+ function exportsTargets(exportsMap) {
96
+ if (typeof exportsMap === 'string') {
97
+ return exportsMap.startsWith('./') ? [exportsMap] : [];
98
+ }
99
+ if (Array.isArray(exportsMap)) {
100
+ return exportsMap.flatMap(exportsTargets);
101
+ }
102
+ if (exportsMap && typeof exportsMap === 'object') {
103
+ return Object.values(exportsMap).flatMap(exportsTargets);
104
+ }
105
+ return [];
106
+ }
107
+ /** A target file (or, for a `*` pattern, its base directory) exists. */
108
+ function exportsTargetExists(depDir, target) {
109
+ const star = target.indexOf('*');
110
+ if (star === -1) {
111
+ return fs.existsSync(path.join(depDir, target));
112
+ }
113
+ const base = target.slice(0, star).replace(/\/[^/]*$/, '');
114
+ return fs.existsSync(path.join(depDir, base || '.'));
115
+ }
116
+ /** Internal workspace packages this app depends on (`workspace:` / `catalog:`
117
+ * protocol) whose build output is missing — a bare `next dev` fails on
118
+ * `Cannot find module '@repo/…/dist/…'` until they are built. */
119
+ function unbuiltWorkspaceDeps(cwd) {
120
+ let pkg;
121
+ try {
122
+ pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
123
+ }
124
+ catch {
125
+ return [];
126
+ }
127
+ const deps = Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })
128
+ .filter(([, version]) => typeof version === 'string' && (version.startsWith('workspace:') || version.startsWith('catalog:')))
129
+ .map(([name]) => name);
130
+ return deps.filter((name) => {
131
+ const depDir = path.join(cwd, 'node_modules', name);
132
+ try {
133
+ const depPkg = JSON.parse(fs.readFileSync(path.join(depDir, 'package.json'), 'utf8'));
134
+ const entry = depPkg.module ?? depPkg.main;
135
+ if (typeof entry === 'string') {
136
+ return !fs.existsSync(path.join(depDir, entry));
137
+ }
138
+ // Exports-only package: unbuilt when a RESOLVED target is missing — an
139
+ // exports map pointing at checked-in sources (eslint configs, type-only
140
+ // packages) never produces dist/ and must not be flagged forever.
141
+ const targets = exportsTargets(depPkg.exports);
142
+ return targets.length > 0 && targets.some((target) => !exportsTargetExists(depDir, target));
143
+ }
144
+ catch {
145
+ return false; // can't resolve the link → don't force a build
146
+ }
147
+ });
148
+ }
149
+ /**
150
+ * Build workspace packages the app needs BEFORE booting its dev server. Fimo
151
+ * runs the app's bare `next dev`, but in a Turborepo/pnpm monorepo the
152
+ * internal deps (`@repo/*`) aren't built by that — `turbo dev` would build
153
+ * them first. Builds are SCOPED to the flagged packages (turbo/pnpm/npm
154
+ * filters) — the root build script builds the whole app too and fails for
155
+ * reasons unrelated to the deps; it is only the last resort when no filter
156
+ * mechanism exists. A build failure is surfaced with an actionable message
157
+ * instead of a cryptic `Cannot find module` at boot.
158
+ */
159
+ export function ensureWorkspaceDepsBuilt(cwd, packageManager, onProgress) {
160
+ const root = workspaceRoot(cwd);
161
+ if (!root) {
162
+ return;
163
+ }
164
+ const unbuilt = unbuiltWorkspaceDeps(cwd);
165
+ if (unbuilt.length === 0) {
166
+ return;
167
+ }
168
+ onProgress?.(`building workspace packages (${unbuilt.slice(0, 3).join(', ')}${unbuilt.length > 3 ? '…' : ''})`);
169
+ let build = null;
170
+ if (fs.existsSync(path.join(root, 'turbo.json'))) {
171
+ build = { command: 'npx', args: ['turbo', 'run', 'build', ...unbuilt.map((name) => `--filter=${name}`)] };
172
+ }
173
+ else if (packageManager === 'pnpm') {
174
+ build = {
175
+ command: 'pnpm',
176
+ args: [...unbuilt.flatMap((name) => ['--filter', name]), 'run', '--if-present', 'build'],
177
+ };
178
+ }
179
+ else if (packageManager === 'npm') {
180
+ build = {
181
+ command: 'npm',
182
+ args: ['run', 'build', '--if-present', ...unbuilt.flatMap((name) => ['--workspace', name])],
183
+ };
184
+ }
185
+ else {
186
+ // No filter mechanism (yarn classic, bun) — the root build is the last resort.
187
+ try {
188
+ const rootPkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
189
+ if (rootPkg.scripts?.build) {
190
+ build = { command: packageManager, args: ['run', 'build'] };
191
+ }
192
+ }
193
+ catch {
194
+ // handled below
195
+ }
196
+ }
197
+ if (!build) {
198
+ throw new CaptureError(`Workspace packages (${unbuilt.join(', ')}) must be built before the dev server can boot, and the monorepo has no build mechanism the migration can drive. Build them yourself, then re-run \`npx fimo migrate\`.`);
199
+ }
200
+ const result = spawnSync(build.command, build.args, {
201
+ cwd: root,
202
+ timeout: 300_000,
203
+ encoding: 'utf8',
204
+ env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' },
205
+ });
206
+ if (result.status !== 0) {
207
+ throw new CaptureError(`Workspace packages (${unbuilt.join(', ')}) need building for the dev server, but \`${build.command} ${build.args.join(' ')}\` failed at ${path.relative(cwd, root) || 'the repo root'}. Build them yourself, then re-run \`npx fimo migrate\`.\n${(result.stderr || result.stdout || '').slice(-800)}`);
208
+ }
209
+ }
210
+ export async function startDevServer(opts) {
211
+ preflight(opts.cwd);
212
+ const port = await findFreePort();
213
+ const child = spawn(opts.packageManager, ['run', 'dev'], {
214
+ cwd: opts.cwd,
215
+ detached: process.platform !== 'win32',
216
+ stdio: ['ignore', 'pipe', 'pipe'],
217
+ env: { ...process.env, PORT: String(port), NEXT_TELEMETRY_DISABLED: '1', BROWSER: 'none' },
218
+ });
219
+ let stdoutBuffer = '';
220
+ let stderrBuffer = '';
221
+ child.stdout?.on('data', (chunk) => {
222
+ stdoutBuffer = `${stdoutBuffer}${chunk.toString()}`.slice(-8_000);
223
+ });
224
+ child.stderr?.on('data', (chunk) => {
225
+ stderrBuffer = `${stderrBuffer}${chunk.toString()}`.slice(-8_000);
226
+ });
227
+ let exited = false;
228
+ child.on('exit', () => {
229
+ exited = true;
230
+ });
231
+ const stop = async () => {
232
+ if (exited || child.pid === undefined) {
233
+ return;
234
+ }
235
+ try {
236
+ if (process.platform === 'win32') {
237
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F']);
238
+ }
239
+ else {
240
+ process.kill(-child.pid, 'SIGTERM');
241
+ }
242
+ }
243
+ catch {
244
+ child.kill('SIGTERM');
245
+ }
246
+ const deadline = Date.now() + KILL_GRACE_MS;
247
+ while (!exited && Date.now() < deadline) {
248
+ await sleep(100);
249
+ }
250
+ if (!exited && child.pid !== undefined && process.platform !== 'win32') {
251
+ try {
252
+ process.kill(-child.pid, 'SIGKILL');
253
+ }
254
+ catch {
255
+ // already gone
256
+ }
257
+ }
258
+ };
259
+ // Readiness: an HTTP response BELOW 500 counts as listening (a 404 means
260
+ // the router is up). Nuxt's dev server answers 503 immediately while Vite
261
+ // still builds — treating that as ready captured a baseline of 503s.
262
+ // Dev scripts that hardcode their own port are handled by sniffing stdout.
263
+ const probe = async (origin) => {
264
+ try {
265
+ // A generous per-attempt cap: connection-refused fails instantly, so
266
+ // this only holds when a server is LISTENING but slow (first SSR
267
+ // compile) — the exact case a short timeout misclassified as down.
268
+ const response = await fetch(origin, { signal: AbortSignal.timeout(15_000) });
269
+ return response.status < 500;
270
+ }
271
+ catch {
272
+ return false;
273
+ }
274
+ };
275
+ // Probe via the `localhost` hostname, never a numeric loopback: Vite-family
276
+ // servers (Astro, Nuxt, SvelteKit) may bind ::1 only, and Node's fetch
277
+ // tries both address families for a hostname (Happy Eyeballs).
278
+ const deadline = Date.now() + opts.bootTimeoutMs;
279
+ let origin = `http://localhost:${port}`;
280
+ while (Date.now() < deadline) {
281
+ if (exited) {
282
+ await stop();
283
+ throw new CaptureError('The dev server exited before becoming ready.', stderrBuffer || stdoutBuffer);
284
+ }
285
+ if (await probe(origin)) {
286
+ return { origin, stderrTail: () => stderrBuffer, stop };
287
+ }
288
+ const sniffed = stdoutBuffer.match(/(?:localhost|127\.0\.0\.1):(\d{2,5})/);
289
+ if (sniffed && Number(sniffed[1]) !== port) {
290
+ const candidate = `http://localhost:${sniffed[1]}`;
291
+ if (await probe(candidate)) {
292
+ origin = candidate;
293
+ return { origin, stderrTail: () => stderrBuffer, stop };
294
+ }
295
+ }
296
+ await sleep(READINESS_POLL_MS);
297
+ }
298
+ await stop();
299
+ throw new CaptureError(`The dev server did not answer within ${Math.round(opts.bootTimeoutMs / 1000)}s.`, stderrBuffer || stdoutBuffer);
300
+ }
301
+ async function fetchRoute(origin, routePath, timeoutMs) {
302
+ const response = await fetch(new URL(routePath, origin), {
303
+ signal: AbortSignal.timeout(timeoutMs),
304
+ headers: { accept: 'text/html' },
305
+ });
306
+ return { status: response.status, html: await response.text() };
307
+ }
308
+ /**
309
+ * Boot the project's own dev server, capture every static route's normalized
310
+ * signature, and tear the server down. Baseline and check both run through
311
+ * this identical pipeline — that symmetry is the gate's correctness property.
312
+ */
313
+ export async function captureParityBaseline(opts) {
314
+ const bootTimeoutMs = opts.bootTimeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS;
315
+ const routeTimeoutMs = opts.routeTimeoutMs ?? DEFAULT_ROUTE_TIMEOUT_MS;
316
+ const routes = {};
317
+ const paramValues = opts.paramValues ?? {};
318
+ // Capturable = a concrete URL exists (no params, or all params resolved via
319
+ // paramValues). The capture is keyed by the ORIGINAL route.path so baseline
320
+ // and check line up; only the fetched URL is the resolved one.
321
+ const staticRoutes = [];
322
+ for (const route of opts.routes) {
323
+ const resolved = route.path.includes(':') ? resolveRoutePath(route.path, paramValues) : route.path;
324
+ if (resolved === null) {
325
+ routes[route.path] = { skipped: 'dynamic-params' };
326
+ }
327
+ else {
328
+ staticRoutes.push({ path: route.path, url: resolved });
329
+ }
330
+ }
331
+ let server;
332
+ if (opts.serverOrigin) {
333
+ const origin = opts.serverOrigin.replace(/\/+$/, '');
334
+ opts.onProgress?.(`using the running dev server at ${origin}`);
335
+ // Retry across a mid-recompile stall (Turbopack holds requests for
336
+ // seconds) — one short-timeout attempt misread a busy server as down.
337
+ let answered = false;
338
+ for (let attempt = 0; attempt < 3 && !answered; attempt++) {
339
+ try {
340
+ await fetch(origin, { signal: AbortSignal.timeout(10_000) });
341
+ answered = true;
342
+ }
343
+ catch {
344
+ if (attempt < 2) {
345
+ await sleep(2_000);
346
+ }
347
+ }
348
+ }
349
+ if (!answered) {
350
+ throw new CaptureError(`No dev server answered at ${origin} — start it (e.g. \`${opts.packageManager} run dev\`) or drop --server to boot one.`);
351
+ }
352
+ server = { origin, stderrTail: () => '', stop: async () => { } };
353
+ }
354
+ else {
355
+ // Monorepo: build the app's workspace deps before the bare dev boot, or
356
+ // `next dev` dies on `Cannot find module '@repo/…'`.
357
+ ensureWorkspaceDepsBuilt(opts.cwd, opts.packageManager, opts.onProgress);
358
+ const bootStartedAt = Date.now();
359
+ opts.onProgress?.('starting your dev server');
360
+ const bootTicker = setInterval(() => {
361
+ opts.onProgress?.(`starting your dev server (${Math.round((Date.now() - bootStartedAt) / 1000)}s)`);
362
+ }, 5_000);
363
+ bootTicker.unref();
364
+ try {
365
+ server = await startDevServer({ cwd: opts.cwd, packageManager: opts.packageManager, bootTimeoutMs });
366
+ }
367
+ finally {
368
+ clearInterval(bootTicker);
369
+ }
370
+ }
371
+ try {
372
+ // Sequential on purpose: dev compilers choke on parallel cold compiles.
373
+ let index = 0;
374
+ for (const route of staticRoutes) {
375
+ index += 1;
376
+ const routeStartedAt = Date.now();
377
+ const progress = () => {
378
+ const elapsed = Math.round((Date.now() - routeStartedAt) / 1000);
379
+ opts.onProgress?.(`capturing ${route.path} (${index}/${staticRoutes.length}${elapsed >= 5 ? `, ${elapsed}s` : ''})`);
380
+ };
381
+ progress();
382
+ const routeTicker = setInterval(progress, 5_000);
383
+ routeTicker.unref();
384
+ try {
385
+ let result = await fetchRoute(server.origin, route.url, routeTimeoutMs).catch(async () => {
386
+ // One retry: first-compile latency and transient dev-server flake.
387
+ await sleep(1_000);
388
+ return fetchRoute(server.origin, route.url, routeTimeoutMs);
389
+ });
390
+ if (result.status >= 500) {
391
+ await sleep(1_000);
392
+ const retried = await fetchRoute(server.origin, route.url, routeTimeoutMs).catch(() => null);
393
+ if (retried) {
394
+ result = retried;
395
+ }
396
+ }
397
+ routes[route.path] = {
398
+ signature: extractRouteSignature(result.html, { origin: server.origin, status: result.status }),
399
+ };
400
+ }
401
+ catch (error) {
402
+ routes[route.path] = { skipped: 'fetch-failed', error: error instanceof Error ? error.message : String(error) };
403
+ }
404
+ finally {
405
+ clearInterval(routeTicker);
406
+ }
407
+ }
408
+ // Second pass for error pages: by now the dev server is fully warm, so a
409
+ // 500 recorded above was very likely a cold-compile flake. An error
410
+ // snapshot poisons the baseline (the parity gate would compare against
411
+ // the error page forever), so retry once more before accepting it.
412
+ for (const route of staticRoutes) {
413
+ const capture = routes[route.path];
414
+ if (capture && 'signature' in capture && capture.signature.status >= 500) {
415
+ opts.onProgress?.(`retrying ${route.path} (captured ${capture.signature.status})`);
416
+ const retried = await fetchRoute(server.origin, route.url, routeTimeoutMs).catch(() => null);
417
+ if (retried && retried.status < 500) {
418
+ routes[route.path] = {
419
+ signature: extractRouteSignature(retried.html, { origin: server.origin, status: retried.status }),
420
+ };
421
+ }
422
+ }
423
+ }
424
+ }
425
+ finally {
426
+ await server.stop();
427
+ }
428
+ return {
429
+ version: PARITY_BASELINE_VERSION,
430
+ capturedAt: new Date().toISOString(),
431
+ engine: 'dev-server',
432
+ framework: opts.framework,
433
+ command: `${opts.packageManager} run dev`,
434
+ routes,
435
+ };
436
+ }
@@ -0,0 +1,59 @@
1
+ import type { InlineMark, ParityBaseline } from './types.js';
2
+ export interface BlockFinding {
3
+ kind: 'text-changed' | 'block-missing' | 'block-added' | 'marks-changed';
4
+ index: number;
5
+ tag?: string;
6
+ before?: string;
7
+ after?: string;
8
+ marksBefore?: InlineMark[];
9
+ marksAfter?: InlineMark[];
10
+ }
11
+ export interface MediaFinding {
12
+ kind: 'changed' | 'missing' | 'added' | 'rehosted';
13
+ index: number;
14
+ before?: string;
15
+ after?: string;
16
+ }
17
+ export interface LinkFinding {
18
+ kind: 'changed' | 'missing' | 'added';
19
+ index: number;
20
+ before?: string;
21
+ after?: string;
22
+ }
23
+ export interface RouteDiff {
24
+ route: string;
25
+ clean: boolean;
26
+ unverified?: boolean;
27
+ statusChange?: {
28
+ before: number;
29
+ after: number;
30
+ };
31
+ blocks: BlockFinding[];
32
+ images: MediaFinding[];
33
+ videos: MediaFinding[];
34
+ links: LinkFinding[];
35
+ notes: string[];
36
+ }
37
+ export interface ParityComparison {
38
+ diffs: RouteDiff[];
39
+ clean: number;
40
+ drifted: number;
41
+ unverified: number;
42
+ }
43
+ export interface CompareOptions {
44
+ /**
45
+ * Full-match regexes for text that is dynamic BY THE SITE'S OWN DESIGN
46
+ * (live clocks, visitor counters). A block whose text fully matches the
47
+ * same pattern in baseline and current compares as equal. Recorded in
48
+ * `.fimo/migration/manifest.json` → `wrap.parityIgnores` with a reason.
49
+ */
50
+ ignoreTextPatterns?: string[];
51
+ /**
52
+ * Routes whose content drifts by DECISION — they render a keep-live
53
+ * external data source (`wrap.dataSources` → live). Counted as
54
+ * unverified, never silently clean.
55
+ */
56
+ ignoreRoutes?: string[];
57
+ }
58
+ export declare function compareBaselines(baseline: ParityBaseline, current: ParityBaseline, options?: CompareOptions): ParityComparison;
59
+ export declare function formatRouteDiff(diff: RouteDiff): string[];
@@ -0,0 +1,13 @@
1
+ import type { RouteSignature } from './types.js';
2
+ /** Strip the capture origin so differing ports between runs compare equal. */
3
+ export declare function normalizeHref(href: string, origin: string): string;
4
+ /**
5
+ * Media src normalization: origin strip + Next image-optimizer decode, so a
6
+ * pre-wrap `next/image` (`/_next/image?url=…&w=…`) and a post-wrap plain
7
+ * `<img src>` of the same asset compare equal.
8
+ */
9
+ export declare function normalizeMediaSrc(src: string, origin: string): string;
10
+ export declare function extractRouteSignature(html: string, opts: {
11
+ origin: string;
12
+ status: number;
13
+ }): RouteSignature;