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.
- 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/hooks-template.eta +4 -2
- package/assets/content-templates/types-template.eta +67 -1
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +86 -15
- package/assets/skills/fimo/references/translations.md +75 -0
- package/assets/skills/fimo-cli/SKILL.md +6 -2
- package/assets/skills/fimo-cli/references/content.md +61 -2
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +20 -4
- package/assets/skills/fimo-cli/references/integrations.md +5 -1
- package/assets/skills/fimo-cli/references/webhooks.md +162 -0
- package/assets/skills/fimo-migration/SKILL.md +24 -2
- package/assets/skills/fimo-migration/references/content.md +24 -0
- package/assets/skills/fimo-migration/references/manifest.md +46 -0
- package/assets/skills/fimo-migration/references/pages.md +234 -0
- package/assets/skills/fimo-migration/references/qa.md +24 -2
- package/assets/skills/fimo-migration/references/workflow.md +12 -1
- package/assets/skills/fimo-migration/references/wrap.md +662 -0
- package/assets/skills/fimo-studio/SKILL.md +5 -3
- package/assets/skills/fimo-studio/references/cms.md +9 -1
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/assets/skills/fimo-studio/references/webhooks.md +42 -0
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.js +31 -131
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +1337 -189
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.js +31 -30
- package/dist/runtime/astro/index.d.ts +2 -0
- package/dist/runtime/astro/index.js +3 -0
- package/dist/runtime/astro/server.d.ts +3 -0
- package/dist/runtime/astro/server.js +6 -0
- package/dist/runtime/content/client/internal.js +1 -0
- package/dist/runtime/content/client/types.d.ts +16 -0
- package/dist/runtime/core/BuiltWithBadge.d.ts +2 -0
- package/dist/runtime/core/BuiltWithBadge.js +68 -0
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +24 -0
- package/dist/runtime/next/FimoNextProviders.js +20 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +2 -0
- package/dist/runtime/next/server.js +4 -0
- package/dist/runtime/nuxt/composables.d.ts +9 -0
- package/dist/runtime/nuxt/composables.js +57 -0
- package/dist/runtime/nuxt/index.d.ts +3 -0
- package/dist/runtime/nuxt/index.js +6 -0
- package/dist/runtime/nuxt/server.d.ts +3 -0
- package/dist/runtime/nuxt/server.js +6 -0
- package/dist/runtime/primitives/components/RichText.d.ts +5 -1
- package/dist/runtime/primitives/components/RichText.js +2 -2
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.js +41 -0
- package/dist/runtime/primitives/labels-store.d.ts +12 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +11 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/FimoSpaProviders.d.ts +36 -0
- package/dist/runtime/react/FimoSpaProviders.js +122 -0
- package/dist/runtime/react/index.d.ts +35 -0
- package/dist/runtime/react/index.js +40 -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/remix/FimoRemixProviders.d.ts +16 -0
- package/dist/runtime/remix/FimoRemixProviders.js +12 -0
- package/dist/runtime/remix/FimoRemixScripts.d.ts +10 -0
- package/dist/runtime/remix/FimoRemixScripts.js +19 -0
- package/dist/runtime/remix/index.d.ts +12 -0
- package/dist/runtime/remix/index.js +11 -0
- package/dist/runtime/remix/server.d.ts +2 -0
- package/dist/runtime/remix/server.js +2 -0
- package/dist/runtime/seo/neutral.d.ts +22 -0
- package/dist/runtime/seo/neutral.js +17 -0
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts +0 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -13
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/i18n-messages.d.ts +29 -0
- package/dist/runtime/shared/i18n-messages.js +79 -0
- package/dist/runtime/shared/labels-fetch.pure.d.ts +17 -0
- package/dist/runtime/shared/labels-fetch.pure.js +71 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts +7 -0
- package/dist/runtime/shared/labels-fetch.server.js +97 -0
- package/dist/runtime/shared/labels-t.pure.d.ts +2 -0
- package/dist/runtime/shared/labels-t.pure.js +10 -0
- package/dist/runtime/strapi/client.d.ts +30 -0
- package/dist/runtime/strapi/client.js +132 -0
- package/dist/runtime/strapi/index.d.ts +5 -0
- package/dist/runtime/strapi/index.js +6 -0
- package/dist/runtime/strapi/reshape.d.ts +50 -0
- package/dist/runtime/strapi/reshape.js +121 -0
- package/dist/runtime/svelte/index.d.ts +3 -0
- package/dist/runtime/svelte/index.js +7 -0
- package/dist/runtime/svelte/server.d.ts +3 -0
- package/dist/runtime/svelte/server.js +6 -0
- package/dist/runtime/vue/index.d.ts +3 -0
- package/dist/runtime/vue/index.js +5 -0
- package/dist/runtime/vue/server.d.ts +3 -0
- package/dist/runtime/vue/server.js +6 -0
- package/dist/runtime/wrapped/FimoFileLink.d.ts +17 -0
- package/dist/runtime/wrapped/FimoFileLink.js +19 -0
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaImage.js +20 -0
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/wrapped/FimoMediaVideo.js +20 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +26 -0
- package/dist/runtime/wrapped/FimoRichTextBlock.js +22 -0
- package/dist/runtime/wrapped/FimoText.d.ts +22 -0
- package/dist/runtime/wrapped/FimoText.js +20 -0
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/wrapped/WrappedContentContext.js +30 -0
- package/dist/runtime/wrapped/WrappedProviders.d.ts +33 -0
- package/dist/runtime/wrapped/WrappedProviders.js +101 -0
- 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/dom.d.ts +4 -0
- package/dist/runtime/wrapped/dom.js +23 -0
- package/dist/runtime/wrapped/server.d.ts +80 -0
- package/dist/runtime/wrapped/server.js +207 -0
- package/dist/scripts/apply-wrap-templates.d.ts +61 -0
- package/dist/scripts/apply-wrap-templates.js +137 -0
- package/dist/scripts/apply-wrap.d.ts +73 -0
- package/dist/scripts/apply-wrap.js +340 -0
- package/dist/scripts/cms/discover.d.ts +2 -0
- package/dist/scripts/cms/discover.js +17 -0
- package/dist/scripts/cms/example-content.d.ts +33 -0
- package/dist/scripts/cms/example-content.js +165 -0
- package/dist/scripts/cms/strapi/detect.d.ts +2 -0
- package/dist/scripts/cms/strapi/detect.js +186 -0
- package/dist/scripts/cms/strapi/index.d.ts +3 -0
- package/dist/scripts/cms/strapi/index.js +25 -0
- package/dist/scripts/cms/strapi/list-source.d.ts +17 -0
- package/dist/scripts/cms/strapi/list-source.js +90 -0
- package/dist/scripts/cms/strapi/schema-map.d.ts +9 -0
- package/dist/scripts/cms/strapi/schema-map.js +289 -0
- package/dist/scripts/cms/strapi/shapes.d.ts +25 -0
- package/dist/scripts/cms/strapi/shapes.js +48 -0
- package/dist/scripts/cms/strapi/touchpoints.d.ts +31 -0
- package/dist/scripts/cms/strapi/touchpoints.js +145 -0
- package/dist/scripts/cms/strapi/wire-client.d.ts +15 -0
- package/dist/scripts/cms/strapi/wire-client.js +107 -0
- package/dist/scripts/cms/strapi/wiring.d.ts +17 -0
- package/dist/scripts/cms/strapi/wiring.js +46 -0
- package/dist/scripts/cms/types.d.ts +145 -0
- package/dist/scripts/cms/types.js +11 -0
- package/dist/scripts/detect-framework.d.ts +39 -0
- package/dist/scripts/detect-framework.js +139 -0
- package/dist/scripts/draft-rows.d.ts +19 -0
- package/dist/scripts/draft-rows.js +178 -0
- package/dist/scripts/draft-schemas.d.ts +27 -0
- package/dist/scripts/draft-schemas.js +247 -0
- package/dist/scripts/extract-translations.js +16 -11
- package/dist/scripts/frameworks/astro/routes.d.ts +3 -0
- package/dist/scripts/frameworks/astro/routes.js +44 -0
- package/dist/scripts/frameworks/astro/template-style.d.ts +7 -0
- package/dist/scripts/frameworks/astro/template-style.js +19 -0
- package/dist/scripts/frameworks/discover.d.ts +83 -0
- package/dist/scripts/frameworks/discover.js +288 -0
- 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 +43 -0
- package/dist/scripts/frameworks/next/rewire.js +282 -0
- package/dist/scripts/frameworks/next/routes.d.ts +30 -0
- package/dist/scripts/frameworks/next/routes.js +176 -0
- package/dist/scripts/frameworks/next/wiring.d.ts +33 -0
- package/dist/scripts/frameworks/next/wiring.js +186 -0
- package/dist/scripts/frameworks/nuxt/routes.d.ts +3 -0
- package/dist/scripts/frameworks/nuxt/routes.js +38 -0
- package/dist/scripts/frameworks/nuxt/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/nuxt/template-style.js +27 -0
- package/dist/scripts/frameworks/remix/routes.d.ts +3 -0
- package/dist/scripts/frameworks/remix/routes.js +36 -0
- package/dist/scripts/frameworks/support.d.ts +75 -0
- package/dist/scripts/frameworks/support.js +165 -0
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +3 -0
- package/dist/scripts/frameworks/sveltekit/routes.js +35 -0
- package/dist/scripts/frameworks/sveltekit/template-style.d.ts +9 -0
- package/dist/scripts/frameworks/sveltekit/template-style.js +54 -0
- package/dist/scripts/frameworks/vite/routes.d.ts +20 -0
- package/dist/scripts/frameworks/vite/routes.js +143 -0
- package/dist/scripts/frameworks/vite/wiring.d.ts +22 -0
- package/dist/scripts/frameworks/vite/wiring.js +144 -0
- package/dist/scripts/i18n-namespace.d.ts +27 -0
- package/dist/scripts/i18n-namespace.js +155 -0
- 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 +1 -3
- package/dist/scripts/lib/validation-report.js +3 -9
- package/dist/scripts/render-parity/capture.d.ts +64 -0
- package/dist/scripts/render-parity/capture.js +436 -0
- package/dist/scripts/render-parity/compare.d.ts +59 -0
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
- package/dist/scripts/render-parity/normalize-html.js +223 -0
- package/dist/scripts/render-parity/types.d.ts +41 -0
- package/dist/scripts/render-parity/types.js +1 -0
- 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 +200 -0
- package/dist/scripts/scan-wrappable.js +1355 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.js +42 -1
- package/package.json +88 -4
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
- 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[];
|
|
Binary file
|
|
@@ -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;
|