fimo 0.3.1 → 0.3.2-experimental.1783354419249
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/README.md +6 -12
- package/assets/agents-starter/AGENTS.md +14 -2
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +14 -2
- package/assets/skills/fimo/references/setup-plain-vite.md +8 -3
- package/assets/skills/fimo/references/setup-react-router.md +13 -6
- package/assets/skills/fimo/references/ui.md +1 -1
- package/assets/skills/fimo-cli/SKILL.md +30 -18
- package/assets/skills/fimo-cli/references/account.md +4 -2
- package/assets/skills/fimo-cli/references/agent-setup.md +14 -16
- package/assets/skills/fimo-cli/references/agents.md +2 -0
- package/assets/skills/fimo-cli/references/analytics.md +2 -0
- package/assets/skills/fimo-cli/references/assets.md +2 -0
- package/assets/skills/fimo-cli/references/branches.md +2 -0
- package/assets/skills/fimo-cli/references/content.md +3 -1
- package/assets/skills/fimo-cli/references/credits.md +2 -0
- package/assets/skills/fimo-cli/references/deploy.md +10 -4
- package/assets/skills/fimo-cli/references/describe.md +2 -0
- package/assets/skills/fimo-cli/references/domains.md +2 -0
- package/assets/skills/fimo-cli/references/forms.md +2 -0
- package/assets/skills/fimo-cli/references/init.md +114 -0
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
- package/assets/skills/fimo-cli/references/integrations.md +2 -0
- package/assets/skills/fimo-cli/references/invite.md +2 -0
- package/assets/skills/fimo-cli/references/labels.md +2 -0
- package/assets/skills/fimo-cli/references/locales.md +2 -0
- package/assets/skills/fimo-cli/references/media.md +2 -0
- package/assets/skills/fimo-cli/references/preview.md +2 -0
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +3 -1
- package/assets/skills/fimo-cli/references/projects.md +5 -3
- package/assets/skills/fimo-cli/references/schedules.md +2 -0
- package/assets/skills/fimo-migration/SKILL.md +19 -1
- package/assets/skills/fimo-migration/references/manifest.md +40 -1
- 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 +174 -0
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/dist/build/vite/index.d.ts.map +1 -1
- package/dist/build/vite/index.js +8 -6
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.d.ts.map +1 -1
- package/dist/build/vite/plugins/translations.js +3 -121
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +5802 -3285
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
- package/dist/runtime/app/FimoProviders.js +2 -23
- package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoFileLink.d.ts +17 -0
- package/dist/runtime/next/FimoFileLink.d.ts.map +1 -0
- package/dist/runtime/next/FimoFileLink.js +19 -0
- package/dist/runtime/next/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/next/FimoMediaImage.d.ts.map +1 -0
- package/dist/runtime/next/FimoMediaImage.js +20 -0
- package/dist/runtime/next/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/next/FimoMediaVideo.d.ts.map +1 -0
- package/dist/runtime/next/FimoMediaVideo.js +20 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +30 -0
- package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextProviders.js +106 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/FimoRichTextBlock.d.ts +23 -0
- package/dist/runtime/next/FimoRichTextBlock.d.ts.map +1 -0
- package/dist/runtime/next/FimoRichTextBlock.js +22 -0
- package/dist/runtime/next/FimoText.d.ts +22 -0
- package/dist/runtime/next/FimoText.d.ts.map +1 -0
- package/dist/runtime/next/FimoText.js +20 -0
- package/dist/runtime/next/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/next/WrappedContentContext.d.ts.map +1 -0
- package/dist/runtime/next/WrappedContentContext.js +30 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.d.ts.map +1 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +33 -0
- package/dist/runtime/next/server.d.ts.map +1 -0
- package/dist/runtime/next/server.js +107 -0
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.d.ts.map +1 -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.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
- package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.test.js +66 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +8 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.d.ts.map +1 -1
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/index.d.ts +24 -0
- package/dist/runtime/react/index.d.ts.map +1 -0
- package/dist/runtime/react/index.js +28 -0
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.d.ts.map +1 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -0
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
- package/dist/runtime/shared/labels-fetch.server.d.ts +14 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
- package/dist/runtime/shared/labels-fetch.server.js +145 -0
- package/dist/scripts/detect-framework.d.ts +12 -0
- package/dist/scripts/detect-framework.js +32 -0
- package/dist/scripts/detect-framework.test.d.ts +1 -0
- package/dist/scripts/detect-framework.test.js +39 -0
- package/dist/scripts/lib/discover-next-routes.d.ts +11 -0
- package/dist/scripts/lib/discover-next-routes.js +76 -0
- package/dist/scripts/lib/discover-next-routes.test.d.ts +1 -0
- package/dist/scripts/lib/discover-next-routes.test.js +40 -0
- package/dist/scripts/render-parity/capture.d.ts +34 -0
- package/dist/scripts/render-parity/capture.js +192 -0
- package/dist/scripts/render-parity/capture.test.d.ts +1 -0
- package/dist/scripts/render-parity/capture.test.js +102 -0
- package/dist/scripts/render-parity/compare.d.ts +44 -0
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/compare.test.d.ts +1 -0
- package/dist/scripts/render-parity/compare.test.js +75 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +13 -0
- package/dist/scripts/render-parity/normalize-html.js +208 -0
- package/dist/scripts/render-parity/normalize-html.test.d.ts +1 -0
- package/dist/scripts/render-parity/normalize-html.test.js +58 -0
- package/dist/scripts/render-parity/types.d.ts +41 -0
- package/dist/scripts/render-parity/types.js +1 -0
- package/dist/scripts/scan-wrappable.d.ts +38 -0
- package/dist/scripts/scan-wrappable.js +231 -0
- package/dist/scripts/scan-wrappable.test.d.ts +1 -0
- package/dist/scripts/scan-wrappable.test.js +65 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.d.ts +13 -0
- package/dist/scripts/validate-schemas.js +113 -0
- package/dist/scripts/validate-schemas.test.d.ts +1 -0
- package/dist/scripts/validate-schemas.test.js +132 -0
- package/package.json +29 -1
- package/release.json +2 -2
- package/scripts/postinstall.mjs +10 -54
- package/templates/react-router/package.json +1 -1
- package/assets/agents-starter/claude/settings.json +0 -19
- package/assets/skills/fimo-cli/references/create.md +0 -90
|
@@ -57,5 +57,7 @@ export interface FimoConfig {
|
|
|
57
57
|
i18n?: FimoI18nConfig;
|
|
58
58
|
routes?: Record<string, FimoRouteConfig>;
|
|
59
59
|
version?: string | number;
|
|
60
|
+
/** The project's framework. Absent means the Fimo template (react-router). */
|
|
61
|
+
framework?: 'react-router' | 'next' | string;
|
|
60
62
|
}
|
|
61
63
|
//# sourceMappingURL=fimo-config.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fimo-config.types.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"fimo-config.types.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/fimo-config.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;CAC9C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LabelBundle {
|
|
2
|
+
dictionaries: Record<string, Record<string, string>>;
|
|
3
|
+
defaultLocale: string;
|
|
4
|
+
locales: string[];
|
|
5
|
+
updatedAt: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function serializeBundle(bundle: LabelBundle): string;
|
|
8
|
+
export declare function labelSignature(bundle: LabelBundle): string;
|
|
9
|
+
export declare function emptyLabelBundle(rootDir: string): LabelBundle;
|
|
10
|
+
export declare function resolveTenantApiUrl(env: Record<string, string>): string | undefined;
|
|
11
|
+
export declare function loadLabelBundle(rootDir: string): Promise<LabelBundle>;
|
|
12
|
+
export declare function fetchWithTimeout(url: URL, timeoutMs: number): Promise<Response>;
|
|
13
|
+
export declare function readProjectEnv(rootDir: string): Promise<Record<string, string>>;
|
|
14
|
+
//# sourceMappingURL=labels-fetch.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels-fetch.server.d.ts","sourceRoot":"","sources":["../../../src/runtime/shared/labels-fetch.server.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAM3D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE1D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAU7D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAOnF;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwC3E;AA+BD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQrF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWrF"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Framework-neutral label fetching (server-side). Shared by the Vite
|
|
2
|
+
// translations plugin (fimo/vite) and the Next server loaders (fimo/next/server).
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { getFimoDefaultLocale, getFimoLocales } from '../i18n/locale.js';
|
|
7
|
+
import { getConfigServer } from './fimo-config.server.js';
|
|
8
|
+
// The tenant API base URL env var, in framework flavor order. Vite projects
|
|
9
|
+
// use VITE_API_URL; wrapped Next projects use NEXT_PUBLIC_FIMO_API_URL.
|
|
10
|
+
const API_URL_ENV_KEYS = ['VITE_API_URL', 'NEXT_PUBLIC_FIMO_API_URL'];
|
|
11
|
+
export function serializeBundle(bundle) {
|
|
12
|
+
return JSON.stringify({
|
|
13
|
+
dictionaries: bundle.dictionaries,
|
|
14
|
+
defaultLocale: bundle.defaultLocale,
|
|
15
|
+
locales: bundle.locales,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function labelSignature(bundle) {
|
|
19
|
+
return Buffer.from(serializeBundle(bundle)).toString('base64');
|
|
20
|
+
}
|
|
21
|
+
export function emptyLabelBundle(rootDir) {
|
|
22
|
+
const config = getConfigServer(rootDir);
|
|
23
|
+
const defaultLocale = getFimoDefaultLocale(config);
|
|
24
|
+
const locales = getFimoLocales(config);
|
|
25
|
+
return {
|
|
26
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
27
|
+
defaultLocale,
|
|
28
|
+
locales,
|
|
29
|
+
updatedAt: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function resolveTenantApiUrl(env) {
|
|
33
|
+
for (const key of API_URL_ENV_KEYS) {
|
|
34
|
+
if (env[key]) {
|
|
35
|
+
return env[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export async function loadLabelBundle(rootDir) {
|
|
41
|
+
const config = getConfigServer(rootDir);
|
|
42
|
+
const defaultLocale = getFimoDefaultLocale(config);
|
|
43
|
+
const locales = getFimoLocales(config);
|
|
44
|
+
const env = await readProjectEnv(rootDir);
|
|
45
|
+
const apiUrl = resolveTenantApiUrl(env);
|
|
46
|
+
if (!apiUrl) {
|
|
47
|
+
return {
|
|
48
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
49
|
+
defaultLocale,
|
|
50
|
+
locales,
|
|
51
|
+
updatedAt: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const loaded = await Promise.all(locales.map((locale) => fetchLabelDictionary(apiUrl, locale).catch(() => ({ locale, labels: {}, updatedAt: null }))));
|
|
56
|
+
const sortedUpdatedAt = loaded
|
|
57
|
+
.map((entry) => entry.updatedAt)
|
|
58
|
+
.filter((value) => typeof value === 'string')
|
|
59
|
+
.sort();
|
|
60
|
+
const updatedAt = sortedUpdatedAt[sortedUpdatedAt.length - 1];
|
|
61
|
+
return {
|
|
62
|
+
dictionaries: Object.fromEntries(loaded.map((entry) => [entry.locale, entry.labels])),
|
|
63
|
+
defaultLocale,
|
|
64
|
+
locales,
|
|
65
|
+
updatedAt: updatedAt ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {
|
|
70
|
+
dictionaries: Object.fromEntries(locales.map((locale) => [locale, {}])),
|
|
71
|
+
defaultLocale,
|
|
72
|
+
locales,
|
|
73
|
+
updatedAt: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function fetchLabelDictionary(apiUrl, locale) {
|
|
78
|
+
const url = new URL('/labels', apiUrl);
|
|
79
|
+
url.searchParams.set('locale', locale);
|
|
80
|
+
const response = await fetchWithTimeout(url, 4000);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
return { locale, labels: {}, updatedAt: null };
|
|
83
|
+
}
|
|
84
|
+
const body = (await response.json());
|
|
85
|
+
if (Array.isArray(body.data)) {
|
|
86
|
+
return {
|
|
87
|
+
locale,
|
|
88
|
+
labels: Object.fromEntries(body.data.map((row) => [row.key, row.value ?? row.defaultValue ?? ''])),
|
|
89
|
+
updatedAt: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
locale,
|
|
94
|
+
labels: body.data?.labels ?? {},
|
|
95
|
+
updatedAt: body.data?.updatedAt ?? null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function fetchWithTimeout(url, timeoutMs) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
return await fetch(url, { signal: controller.signal });
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function readProjectEnv(rootDir) {
|
|
109
|
+
const values = {
|
|
110
|
+
...(await readEnvFile(join(rootDir, '.env'))),
|
|
111
|
+
...(await readEnvFile(join(rootDir, '.env.local'))),
|
|
112
|
+
};
|
|
113
|
+
for (const key of API_URL_ENV_KEYS) {
|
|
114
|
+
if (process.env[key]) {
|
|
115
|
+
values[key] = process.env[key];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return values;
|
|
119
|
+
}
|
|
120
|
+
async function readEnvFile(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
const content = await readFile(filePath, 'utf8');
|
|
123
|
+
const values = {};
|
|
124
|
+
for (const line of content.split(/\r?\n/)) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const index = trimmed.indexOf('=');
|
|
130
|
+
if (index === -1) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const key = trimmed.slice(0, index).trim();
|
|
134
|
+
const value = trimmed
|
|
135
|
+
.slice(index + 1)
|
|
136
|
+
.trim()
|
|
137
|
+
.replace(/^['"]|['"]$/g, '');
|
|
138
|
+
values[key] = value;
|
|
139
|
+
}
|
|
140
|
+
return values;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DetectedFramework = 'next' | 'react-router' | 'vite';
|
|
2
|
+
export interface FrameworkDetection {
|
|
3
|
+
framework: DetectedFramework | null;
|
|
4
|
+
/** For Next.js: the app-router directory (`app` or `src/app`) when present. */
|
|
5
|
+
appDir?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Detect the framework of an existing site from its package.json and layout.
|
|
9
|
+
* Used by `fimo init` (adopt-existing-site) and the wrap migration to decide
|
|
10
|
+
* how routes are discovered and which env flavor to write.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectFramework(cwd: string): FrameworkDetection;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Detect the framework of an existing site from its package.json and layout.
|
|
5
|
+
* Used by `fimo init` (adopt-existing-site) and the wrap migration to decide
|
|
6
|
+
* how routes are discovered and which env flavor to write.
|
|
7
|
+
*/
|
|
8
|
+
export function detectFramework(cwd) {
|
|
9
|
+
const pkgPath = join(cwd, 'package.json');
|
|
10
|
+
if (!existsSync(pkgPath)) {
|
|
11
|
+
return { framework: null };
|
|
12
|
+
}
|
|
13
|
+
let pkg;
|
|
14
|
+
try {
|
|
15
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { framework: null };
|
|
19
|
+
}
|
|
20
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
21
|
+
if (deps.next) {
|
|
22
|
+
const appDir = ['app', 'src/app'].find((dir) => existsSync(join(cwd, dir)));
|
|
23
|
+
return { framework: 'next', appDir };
|
|
24
|
+
}
|
|
25
|
+
if (deps['react-router'] || deps['@react-router/dev']) {
|
|
26
|
+
return { framework: 'react-router' };
|
|
27
|
+
}
|
|
28
|
+
if (deps.vite) {
|
|
29
|
+
return { framework: 'vite' };
|
|
30
|
+
}
|
|
31
|
+
return { framework: null };
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { detectFramework } from './detect-framework.js';
|
|
6
|
+
let dir;
|
|
7
|
+
const project = (deps, dirs = []) => {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'detect-framework-'));
|
|
9
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 't', dependencies: deps }));
|
|
10
|
+
for (const d of dirs) {
|
|
11
|
+
mkdirSync(join(dir, d), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return dir;
|
|
14
|
+
};
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (dir) {
|
|
17
|
+
rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
describe('detectFramework', () => {
|
|
21
|
+
it('detects next with the app dir', () => {
|
|
22
|
+
expect(detectFramework(project({ next: '^15.0.0' }, ['app']))).toEqual({ framework: 'next', appDir: 'app' });
|
|
23
|
+
expect(detectFramework(project({ next: '^15.0.0' }, ['src/app']))).toEqual({
|
|
24
|
+
framework: 'next',
|
|
25
|
+
appDir: 'src/app',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it('detects react-router and vite', () => {
|
|
29
|
+
expect(detectFramework(project({ 'react-router': '^7.0.0' })).framework).toBe('react-router');
|
|
30
|
+
expect(detectFramework(project({ vite: '^7.0.0' })).framework).toBe('vite');
|
|
31
|
+
});
|
|
32
|
+
it('prefers next over an incidental vite dep', () => {
|
|
33
|
+
expect(detectFramework(project({ next: '^15.0.0', vite: '^7.0.0' }, ['app'])).framework).toBe('next');
|
|
34
|
+
});
|
|
35
|
+
it('returns null without a package.json or known framework', () => {
|
|
36
|
+
expect(detectFramework(join(tmpdir(), 'nope-does-not-exist')).framework).toBeNull();
|
|
37
|
+
expect(detectFramework(project({ express: '^4.0.0' })).framework).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DiscoveredRoute {
|
|
2
|
+
id: string;
|
|
3
|
+
path: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
8
|
+
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
9
|
+
* Studio page switcher can enumerate a wrapped site's pages.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverNextRoutes(appDir: string): DiscoveredRoute[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const PAGE_FILES = new Set(['page.tsx', 'page.jsx', 'page.ts', 'page.js', 'page.mdx']);
|
|
4
|
+
const titleCase = (seg) => seg
|
|
5
|
+
.replace(/[_\-]+/g, ' ')
|
|
6
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
7
|
+
.split(/\s+/)
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
10
|
+
.join(' ');
|
|
11
|
+
function segmentToPath(segment) {
|
|
12
|
+
// Route groups `(marketing)` don't contribute URL segments. Parallel-route
|
|
13
|
+
// slots `@modal` and private folders `_lib` are not standalone pages.
|
|
14
|
+
if (segment.startsWith('(')) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
if (segment.startsWith('@') || segment.startsWith('_')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const catchAll = segment.match(/^\[\.\.\.(.+)\]$/) ?? segment.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
21
|
+
if (catchAll) {
|
|
22
|
+
return `:${catchAll[1]}*`;
|
|
23
|
+
}
|
|
24
|
+
const param = segment.match(/^\[(.+)\]$/);
|
|
25
|
+
if (param) {
|
|
26
|
+
return `:${param[1]}`;
|
|
27
|
+
}
|
|
28
|
+
return segment;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Discover page routes of a Next.js app-router directory (`app/` or
|
|
32
|
+
* `src/app/`). Returns display routes for `fimo-config.json#routes` so the
|
|
33
|
+
* Studio page switcher can enumerate a wrapped site's pages.
|
|
34
|
+
*/
|
|
35
|
+
export function discoverNextRoutes(appDir) {
|
|
36
|
+
const routes = [];
|
|
37
|
+
const walk = (dir, segments) => {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (entries.some((entry) => PAGE_FILES.has(entry))) {
|
|
46
|
+
const parts = segments.filter((s) => s.length > 0);
|
|
47
|
+
const routePath = `/${parts.join('/')}`.replace(/\/+/g, '/');
|
|
48
|
+
const lastStatic = [...parts].reverse().find((p) => !p.startsWith(':'));
|
|
49
|
+
routes.push({
|
|
50
|
+
id: routePath === '/' ? 'home' : parts.map((p) => p.replace(/^:/, '')).join('-'),
|
|
51
|
+
path: routePath,
|
|
52
|
+
label: routePath === '/' ? 'Home' : titleCase(lastStatic ?? parts[parts.length - 1] ?? routePath),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const full = join(dir, entry);
|
|
57
|
+
let isDir = false;
|
|
58
|
+
try {
|
|
59
|
+
isDir = statSync(full).isDirectory();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isDir) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const mapped = segmentToPath(entry);
|
|
68
|
+
if (mapped === null) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
walk(full, [...segments, mapped]);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
walk(appDir, []);
|
|
75
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { discoverNextRoutes } from './discover-next-routes.js';
|
|
6
|
+
let dir;
|
|
7
|
+
const app = (pages) => {
|
|
8
|
+
dir = mkdtempSync(join(tmpdir(), 'next-routes-'));
|
|
9
|
+
for (const page of pages) {
|
|
10
|
+
const full = join(dir, page);
|
|
11
|
+
mkdirSync(join(full, '..'), { recursive: true });
|
|
12
|
+
writeFileSync(full, 'export default function Page() { return null }');
|
|
13
|
+
}
|
|
14
|
+
return dir;
|
|
15
|
+
};
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (dir) {
|
|
18
|
+
rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
describe('discoverNextRoutes', () => {
|
|
22
|
+
it('discovers static routes', () => {
|
|
23
|
+
const routes = discoverNextRoutes(app(['page.tsx', 'about/page.tsx', 'pricing/page.tsx']));
|
|
24
|
+
expect(routes.map((r) => r.path)).toEqual(['/', '/about', '/pricing']);
|
|
25
|
+
expect(routes[0]).toEqual({ id: 'home', path: '/', label: 'Home' });
|
|
26
|
+
expect(routes[1]).toEqual({ id: 'about', path: '/about', label: 'About' });
|
|
27
|
+
});
|
|
28
|
+
it('maps dynamic and catch-all segments', () => {
|
|
29
|
+
const routes = discoverNextRoutes(app(['blog/[slug]/page.tsx', 'docs/[...path]/page.tsx']));
|
|
30
|
+
expect(routes.map((r) => r.path)).toEqual(['/blog/:slug', '/docs/:path*']);
|
|
31
|
+
expect(routes[0].label).toBe('Blog');
|
|
32
|
+
});
|
|
33
|
+
it('collapses route groups and skips private and parallel segments', () => {
|
|
34
|
+
const routes = discoverNextRoutes(app(['(marketing)/features/page.tsx', '_internal/secret/page.tsx', '@modal/photo/page.tsx']));
|
|
35
|
+
expect(routes.map((r) => r.path)).toEqual(['/features']);
|
|
36
|
+
});
|
|
37
|
+
it('returns [] for a missing directory', () => {
|
|
38
|
+
expect(discoverNextRoutes(join(tmpdir(), 'does-not-exist'))).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
onProgress?: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare class CaptureError extends Error {
|
|
14
|
+
readonly stderrTail: string;
|
|
15
|
+
constructor(message: string, stderrTail?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare function findFreePort(): Promise<number>;
|
|
18
|
+
interface DevServer {
|
|
19
|
+
origin: string;
|
|
20
|
+
stderrTail: () => string;
|
|
21
|
+
stop: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function startDevServer(opts: {
|
|
24
|
+
cwd: string;
|
|
25
|
+
packageManager: string;
|
|
26
|
+
bootTimeoutMs: number;
|
|
27
|
+
}): Promise<DevServer>;
|
|
28
|
+
/**
|
|
29
|
+
* Boot the project's own dev server, capture every static route's normalized
|
|
30
|
+
* signature, and tear the server down. Baseline and check both run through
|
|
31
|
+
* this identical pipeline — that symmetry is the gate's correctness property.
|
|
32
|
+
*/
|
|
33
|
+
export declare function captureParityBaseline(opts: CaptureOptions): Promise<ParityBaseline>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { spawn } 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
|
+
const DEFAULT_BOOT_TIMEOUT_MS = 90_000;
|
|
8
|
+
const DEFAULT_ROUTE_TIMEOUT_MS = 60_000;
|
|
9
|
+
const READINESS_POLL_MS = 500;
|
|
10
|
+
const KILL_GRACE_MS = 5_000;
|
|
11
|
+
export class CaptureError extends Error {
|
|
12
|
+
stderrTail;
|
|
13
|
+
constructor(message, stderrTail = '') {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'CaptureError';
|
|
16
|
+
this.stderrTail = stderrTail;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function findFreePort() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const server = net.createServer();
|
|
22
|
+
server.once('error', reject);
|
|
23
|
+
server.listen(0, '127.0.0.1', () => {
|
|
24
|
+
const address = server.address();
|
|
25
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
26
|
+
server.close(() => resolve(port));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function preflight(cwd) {
|
|
31
|
+
if (!fs.existsSync(path.join(cwd, 'node_modules'))) {
|
|
32
|
+
throw new CaptureError('node_modules is missing — run your package manager install first.');
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
36
|
+
if (!pkg.scripts?.dev) {
|
|
37
|
+
throw new CaptureError('No `dev` script in package.json — the parity capture boots your dev server.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof CaptureError) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
throw new CaptureError('Could not read package.json.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
export async function startDevServer(opts) {
|
|
49
|
+
preflight(opts.cwd);
|
|
50
|
+
const port = await findFreePort();
|
|
51
|
+
const child = spawn(opts.packageManager, ['run', 'dev'], {
|
|
52
|
+
cwd: opts.cwd,
|
|
53
|
+
detached: process.platform !== 'win32',
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
env: { ...process.env, PORT: String(port), NEXT_TELEMETRY_DISABLED: '1', BROWSER: 'none' },
|
|
56
|
+
});
|
|
57
|
+
let stdoutBuffer = '';
|
|
58
|
+
let stderrBuffer = '';
|
|
59
|
+
child.stdout?.on('data', (chunk) => {
|
|
60
|
+
stdoutBuffer = `${stdoutBuffer}${chunk.toString()}`.slice(-8_000);
|
|
61
|
+
});
|
|
62
|
+
child.stderr?.on('data', (chunk) => {
|
|
63
|
+
stderrBuffer = `${stderrBuffer}${chunk.toString()}`.slice(-8_000);
|
|
64
|
+
});
|
|
65
|
+
let exited = false;
|
|
66
|
+
child.on('exit', () => {
|
|
67
|
+
exited = true;
|
|
68
|
+
});
|
|
69
|
+
const stop = async () => {
|
|
70
|
+
if (exited || child.pid === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (process.platform === 'win32') {
|
|
75
|
+
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F']);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
child.kill('SIGTERM');
|
|
83
|
+
}
|
|
84
|
+
const deadline = Date.now() + KILL_GRACE_MS;
|
|
85
|
+
while (!exited && Date.now() < deadline) {
|
|
86
|
+
await sleep(100);
|
|
87
|
+
}
|
|
88
|
+
if (!exited && child.pid !== undefined && process.platform !== 'win32') {
|
|
89
|
+
try {
|
|
90
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// already gone
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Readiness: any HTTP response counts as listening. Dev scripts that
|
|
98
|
+
// hardcode their own port are handled by sniffing the child's stdout.
|
|
99
|
+
const probe = async (origin) => {
|
|
100
|
+
try {
|
|
101
|
+
await fetch(origin, { signal: AbortSignal.timeout(2_000) });
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const deadline = Date.now() + opts.bootTimeoutMs;
|
|
109
|
+
let origin = `http://127.0.0.1:${port}`;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
if (exited) {
|
|
112
|
+
await stop();
|
|
113
|
+
throw new CaptureError('The dev server exited before becoming ready.', stderrBuffer || stdoutBuffer);
|
|
114
|
+
}
|
|
115
|
+
if (await probe(origin)) {
|
|
116
|
+
return { origin, stderrTail: () => stderrBuffer, stop };
|
|
117
|
+
}
|
|
118
|
+
const sniffed = stdoutBuffer.match(/(?:localhost|127\.0\.0\.1):(\d{2,5})/);
|
|
119
|
+
if (sniffed && Number(sniffed[1]) !== port) {
|
|
120
|
+
const candidate = `http://127.0.0.1:${sniffed[1]}`;
|
|
121
|
+
if (await probe(candidate)) {
|
|
122
|
+
origin = candidate;
|
|
123
|
+
return { origin, stderrTail: () => stderrBuffer, stop };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await sleep(READINESS_POLL_MS);
|
|
127
|
+
}
|
|
128
|
+
await stop();
|
|
129
|
+
throw new CaptureError(`The dev server did not answer within ${Math.round(opts.bootTimeoutMs / 1000)}s.`, stderrBuffer || stdoutBuffer);
|
|
130
|
+
}
|
|
131
|
+
async function fetchRoute(origin, routePath, timeoutMs) {
|
|
132
|
+
const response = await fetch(new URL(routePath, origin), {
|
|
133
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
134
|
+
headers: { accept: 'text/html' },
|
|
135
|
+
});
|
|
136
|
+
return { status: response.status, html: await response.text() };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Boot the project's own dev server, capture every static route's normalized
|
|
140
|
+
* signature, and tear the server down. Baseline and check both run through
|
|
141
|
+
* this identical pipeline — that symmetry is the gate's correctness property.
|
|
142
|
+
*/
|
|
143
|
+
export async function captureParityBaseline(opts) {
|
|
144
|
+
const bootTimeoutMs = opts.bootTimeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS;
|
|
145
|
+
const routeTimeoutMs = opts.routeTimeoutMs ?? DEFAULT_ROUTE_TIMEOUT_MS;
|
|
146
|
+
const routes = {};
|
|
147
|
+
const staticRoutes = opts.routes.filter((route) => !route.path.includes(':'));
|
|
148
|
+
for (const route of opts.routes) {
|
|
149
|
+
if (route.path.includes(':')) {
|
|
150
|
+
routes[route.path] = { skipped: 'dynamic-params' };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
opts.onProgress?.('starting your dev server');
|
|
154
|
+
const server = await startDevServer({ cwd: opts.cwd, packageManager: opts.packageManager, bootTimeoutMs });
|
|
155
|
+
try {
|
|
156
|
+
// Sequential on purpose: dev compilers choke on parallel cold compiles.
|
|
157
|
+
for (const route of staticRoutes) {
|
|
158
|
+
opts.onProgress?.(`capturing ${route.path}`);
|
|
159
|
+
try {
|
|
160
|
+
let result = await fetchRoute(server.origin, route.path, routeTimeoutMs).catch(async () => {
|
|
161
|
+
// One retry: first-compile latency and transient dev-server flake.
|
|
162
|
+
await sleep(1_000);
|
|
163
|
+
return fetchRoute(server.origin, route.path, routeTimeoutMs);
|
|
164
|
+
});
|
|
165
|
+
if (result.status >= 500) {
|
|
166
|
+
await sleep(1_000);
|
|
167
|
+
const retried = await fetchRoute(server.origin, route.path, routeTimeoutMs).catch(() => null);
|
|
168
|
+
if (retried) {
|
|
169
|
+
result = retried;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
routes[route.path] = {
|
|
173
|
+
signature: extractRouteSignature(result.html, { origin: server.origin, status: result.status }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
routes[route.path] = { skipped: 'fetch-failed', error: error instanceof Error ? error.message : String(error) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await server.stop();
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
version: PARITY_BASELINE_VERSION,
|
|
186
|
+
capturedAt: new Date().toISOString(),
|
|
187
|
+
engine: 'dev-server',
|
|
188
|
+
framework: opts.framework,
|
|
189
|
+
command: `${opts.packageManager} run dev`,
|
|
190
|
+
routes,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|