fimo 0.9.0-experimental.1 → 0.9.0
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/skills/fimo/SKILL.md +17 -54
- package/assets/skills/fimo-cli/SKILL.md +15 -15
- package/assets/skills/fimo-cli/references/agent-setup.md +36 -53
- package/assets/skills/fimo-cli/references/agents.md +4 -4
- package/assets/skills/fimo-cli/references/content.md +2 -2
- package/assets/skills/fimo-cli/references/deploy.md +1 -1
- package/assets/skills/fimo-cli/references/forms.md +1 -1
- package/assets/skills/fimo-cli/references/init.md +16 -24
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +14 -11
- package/assets/skills/fimo-cli/references/projects.md +1 -1
- package/assets/skills/fimo-cli/references/support.md +11 -9
- package/assets/skills/fimo-cli/references/webhooks.md +3 -1
- package/assets/skills/fimo-code/SKILL.md +67 -0
- package/assets/skills/fimo-migration/SKILL.md +2 -22
- package/assets/skills/fimo-migration/references/content.md +0 -24
- package/assets/skills/fimo-migration/references/manifest.md +0 -35
- package/assets/skills/fimo-migration/references/qa.md +2 -24
- package/assets/skills/fimo-migration/references/workflow.md +1 -12
- package/assets/skills/fimo-studio/references/visual-editing.md +3 -5
- package/assets/skills/fimo-studio/references/webhooks.md +1 -1
- package/dist/build/vite/plugins/translations.d.ts +8 -2
- package/dist/build/vite/plugins/translations.js +121 -3
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +311 -1209
- package/dist/runtime/app/FimoProviders.d.ts +11 -1
- package/dist/runtime/app/FimoProviders.js +23 -2
- package/dist/runtime/content/client/createContentClient.js +2 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +0 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +3 -5
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +4 -11
- package/dist/runtime/primitives/translations.d.ts +27 -1
- package/dist/runtime/primitives/translations.js +85 -7
- package/dist/runtime/shared/fimo-config.server.js +0 -1
- package/dist/runtime/shared/fimo-config.types.d.ts +0 -2
- package/dist/scripts/extract-translations.js +11 -16
- package/dist/scripts/lib/validation-report.d.ts +0 -1
- package/dist/scripts/lib/validation-report.js +0 -3
- package/dist/scripts/validate-route-metadata.js +0 -11
- package/package.json +4 -84
- package/release.json +2 -2
- package/scripts/postinstall.mjs +11 -65
- package/templates/react-router/AGENTS.md +44 -0
- package/templates/react-router/CLAUDE.md +5 -0
- package/templates/react-router/package.json +1 -1
- package/assets/agents-starter/AGENTS.md +0 -66
- package/assets/skills/fimo-migration/references/wrap.md +0 -617
- package/dist/runtime/astro/index.d.ts +0 -2
- package/dist/runtime/astro/index.js +0 -3
- package/dist/runtime/astro/server.d.ts +0 -3
- package/dist/runtime/astro/server.js +0 -6
- package/dist/runtime/next/FimoNextProviders.d.ts +0 -24
- package/dist/runtime/next/FimoNextProviders.js +0 -20
- package/dist/runtime/next/FimoNextScripts.d.ts +0 -10
- package/dist/runtime/next/FimoNextScripts.js +0 -19
- package/dist/runtime/next/index.d.ts +0 -14
- package/dist/runtime/next/index.js +0 -14
- package/dist/runtime/next/server.d.ts +0 -2
- package/dist/runtime/next/server.js +0 -4
- package/dist/runtime/nuxt/composables.d.ts +0 -9
- package/dist/runtime/nuxt/composables.js +0 -57
- package/dist/runtime/nuxt/index.d.ts +0 -3
- package/dist/runtime/nuxt/index.js +0 -6
- package/dist/runtime/nuxt/server.d.ts +0 -3
- package/dist/runtime/nuxt/server.js +0 -6
- package/dist/runtime/primitives/labels-hook.d.ts +0 -33
- package/dist/runtime/primitives/labels-hook.js +0 -41
- package/dist/runtime/primitives/labels-store.d.ts +0 -12
- package/dist/runtime/primitives/labels-store.js +0 -60
- package/dist/runtime/react/FimoSpaProviders.d.ts +0 -36
- package/dist/runtime/react/FimoSpaProviders.js +0 -63
- package/dist/runtime/react/index.d.ts +0 -33
- package/dist/runtime/react/index.js +0 -39
- package/dist/runtime/remix/FimoRemixProviders.d.ts +0 -16
- package/dist/runtime/remix/FimoRemixProviders.js +0 -12
- package/dist/runtime/remix/FimoRemixScripts.d.ts +0 -10
- package/dist/runtime/remix/FimoRemixScripts.js +0 -19
- package/dist/runtime/remix/index.d.ts +0 -12
- package/dist/runtime/remix/index.js +0 -11
- package/dist/runtime/remix/server.d.ts +0 -2
- package/dist/runtime/remix/server.js +0 -2
- package/dist/runtime/shared/display-routes.d.ts +0 -14
- package/dist/runtime/shared/display-routes.js +0 -25
- package/dist/runtime/shared/i18n-messages.d.ts +0 -29
- package/dist/runtime/shared/i18n-messages.js +0 -79
- package/dist/runtime/shared/labels-fetch.pure.d.ts +0 -17
- package/dist/runtime/shared/labels-fetch.pure.js +0 -71
- package/dist/runtime/shared/labels-fetch.server.d.ts +0 -7
- package/dist/runtime/shared/labels-fetch.server.js +0 -97
- package/dist/runtime/shared/labels-t.pure.d.ts +0 -2
- package/dist/runtime/shared/labels-t.pure.js +0 -10
- package/dist/runtime/strapi/client.d.ts +0 -30
- package/dist/runtime/strapi/client.js +0 -89
- package/dist/runtime/strapi/index.d.ts +0 -5
- package/dist/runtime/strapi/index.js +0 -6
- package/dist/runtime/strapi/reshape.d.ts +0 -46
- package/dist/runtime/strapi/reshape.js +0 -114
- package/dist/runtime/svelte/index.d.ts +0 -3
- package/dist/runtime/svelte/index.js +0 -7
- package/dist/runtime/svelte/server.d.ts +0 -3
- package/dist/runtime/svelte/server.js +0 -6
- package/dist/runtime/vue/index.d.ts +0 -3
- package/dist/runtime/vue/index.js +0 -5
- package/dist/runtime/vue/server.d.ts +0 -3
- package/dist/runtime/vue/server.js +0 -6
- package/dist/runtime/wrapped/FimoFileLink.d.ts +0 -17
- package/dist/runtime/wrapped/FimoFileLink.js +0 -19
- package/dist/runtime/wrapped/FimoMediaImage.d.ts +0 -14
- package/dist/runtime/wrapped/FimoMediaImage.js +0 -20
- package/dist/runtime/wrapped/FimoMediaVideo.d.ts +0 -14
- package/dist/runtime/wrapped/FimoMediaVideo.js +0 -20
- package/dist/runtime/wrapped/FimoRichTextBlock.d.ts +0 -23
- package/dist/runtime/wrapped/FimoRichTextBlock.js +0 -22
- package/dist/runtime/wrapped/FimoText.d.ts +0 -22
- package/dist/runtime/wrapped/FimoText.js +0 -20
- package/dist/runtime/wrapped/WrappedContentContext.d.ts +0 -28
- package/dist/runtime/wrapped/WrappedContentContext.js +0 -30
- package/dist/runtime/wrapped/WrappedProviders.d.ts +0 -33
- package/dist/runtime/wrapped/WrappedProviders.js +0 -101
- package/dist/runtime/wrapped/dom.d.ts +0 -4
- package/dist/runtime/wrapped/dom.js +0 -23
- package/dist/runtime/wrapped/server.d.ts +0 -77
- package/dist/runtime/wrapped/server.js +0 -237
- package/dist/scripts/apply-wrap-templates.d.ts +0 -61
- package/dist/scripts/apply-wrap-templates.js +0 -132
- package/dist/scripts/apply-wrap.d.ts +0 -46
- package/dist/scripts/apply-wrap.js +0 -278
- package/dist/scripts/cms/discover.d.ts +0 -2
- package/dist/scripts/cms/discover.js +0 -17
- package/dist/scripts/cms/example-content.d.ts +0 -33
- package/dist/scripts/cms/example-content.js +0 -162
- package/dist/scripts/cms/strapi/detect.d.ts +0 -2
- package/dist/scripts/cms/strapi/detect.js +0 -186
- package/dist/scripts/cms/strapi/index.d.ts +0 -3
- package/dist/scripts/cms/strapi/index.js +0 -25
- package/dist/scripts/cms/strapi/list-source.d.ts +0 -17
- package/dist/scripts/cms/strapi/list-source.js +0 -90
- package/dist/scripts/cms/strapi/schema-map.d.ts +0 -9
- package/dist/scripts/cms/strapi/schema-map.js +0 -277
- package/dist/scripts/cms/strapi/shapes.d.ts +0 -23
- package/dist/scripts/cms/strapi/shapes.js +0 -38
- package/dist/scripts/cms/strapi/touchpoints.d.ts +0 -31
- package/dist/scripts/cms/strapi/touchpoints.js +0 -145
- package/dist/scripts/cms/strapi/wire-client.d.ts +0 -15
- package/dist/scripts/cms/strapi/wire-client.js +0 -107
- package/dist/scripts/cms/strapi/wiring.d.ts +0 -17
- package/dist/scripts/cms/strapi/wiring.js +0 -46
- package/dist/scripts/cms/types.d.ts +0 -145
- package/dist/scripts/cms/types.js +0 -11
- package/dist/scripts/detect-framework.d.ts +0 -39
- package/dist/scripts/detect-framework.js +0 -133
- package/dist/scripts/draft-rows.d.ts +0 -19
- package/dist/scripts/draft-rows.js +0 -135
- package/dist/scripts/draft-schemas.d.ts +0 -27
- package/dist/scripts/draft-schemas.js +0 -188
- package/dist/scripts/frameworks/astro/routes.d.ts +0 -3
- package/dist/scripts/frameworks/astro/routes.js +0 -55
- package/dist/scripts/frameworks/astro/template-style.d.ts +0 -7
- package/dist/scripts/frameworks/astro/template-style.js +0 -19
- package/dist/scripts/frameworks/discover.d.ts +0 -70
- package/dist/scripts/frameworks/discover.js +0 -178
- package/dist/scripts/frameworks/next/rewire.d.ts +0 -30
- package/dist/scripts/frameworks/next/rewire.js +0 -258
- package/dist/scripts/frameworks/next/routes.d.ts +0 -17
- package/dist/scripts/frameworks/next/routes.js +0 -134
- package/dist/scripts/frameworks/next/wiring.d.ts +0 -33
- package/dist/scripts/frameworks/next/wiring.js +0 -186
- package/dist/scripts/frameworks/nuxt/routes.d.ts +0 -3
- package/dist/scripts/frameworks/nuxt/routes.js +0 -54
- package/dist/scripts/frameworks/nuxt/template-style.d.ts +0 -9
- package/dist/scripts/frameworks/nuxt/template-style.js +0 -27
- package/dist/scripts/frameworks/remix/routes.d.ts +0 -3
- package/dist/scripts/frameworks/remix/routes.js +0 -50
- package/dist/scripts/frameworks/support.d.ts +0 -75
- package/dist/scripts/frameworks/support.js +0 -165
- package/dist/scripts/frameworks/sveltekit/routes.d.ts +0 -3
- package/dist/scripts/frameworks/sveltekit/routes.js +0 -50
- package/dist/scripts/frameworks/sveltekit/template-style.d.ts +0 -9
- package/dist/scripts/frameworks/sveltekit/template-style.js +0 -54
- package/dist/scripts/frameworks/vite/routes.d.ts +0 -15
- package/dist/scripts/frameworks/vite/routes.js +0 -93
- package/dist/scripts/frameworks/vite/wiring.d.ts +0 -10
- package/dist/scripts/frameworks/vite/wiring.js +0 -99
- package/dist/scripts/i18n-namespace.d.ts +0 -27
- package/dist/scripts/i18n-namespace.js +0 -155
- package/dist/scripts/render-parity/capture.d.ts +0 -56
- package/dist/scripts/render-parity/capture.js +0 -366
- package/dist/scripts/render-parity/compare.d.ts +0 -59
- package/dist/scripts/render-parity/compare.js +0 -0
- package/dist/scripts/render-parity/normalize-html.d.ts +0 -13
- package/dist/scripts/render-parity/normalize-html.js +0 -223
- package/dist/scripts/render-parity/types.d.ts +0 -41
- package/dist/scripts/render-parity/types.js +0 -1
- package/dist/scripts/scan-wrappable.d.ts +0 -196
- package/dist/scripts/scan-wrappable.js +0 -1232
- /package/assets/skills/{fimo → fimo-code}/assets/react-router-locales/navigation.tsx +0 -0
- /package/assets/skills/{fimo → fimo-code}/assets/react-router-locales/routes.ts +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/assets.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/content.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/design.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/forms.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/labels.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/locales.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/setup-plain-vite.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/setup-react-router.md +0 -0
- /package/assets/skills/{fimo → fimo-code}/references/ui.md +0 -0
|
@@ -1,366 +0,0 @@
|
|
|
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
|
-
export function findFreePort() {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const server = net.createServer();
|
|
28
|
-
server.once('error', reject);
|
|
29
|
-
server.listen(0, '127.0.0.1', () => {
|
|
30
|
-
const address = server.address();
|
|
31
|
-
const port = typeof address === 'object' && address ? address.port : 0;
|
|
32
|
-
server.close(() => resolve(port));
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
function preflight(cwd) {
|
|
37
|
-
if (!fs.existsSync(path.join(cwd, 'node_modules'))) {
|
|
38
|
-
throw new CaptureError('node_modules is missing — run your package manager install first.');
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
42
|
-
if (!pkg.scripts?.dev) {
|
|
43
|
-
throw new CaptureError('No `dev` script in package.json — the parity capture boots your dev server.');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
if (error instanceof CaptureError) {
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
throw new CaptureError('Could not read package.json.');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
-
/** The monorepo root above `cwd` (pnpm-workspace / turbo / npm workspaces),
|
|
55
|
-
* or null when `cwd` is standalone. */
|
|
56
|
-
function workspaceRoot(cwd) {
|
|
57
|
-
let dir = cwd;
|
|
58
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
59
|
-
const parent = path.dirname(dir);
|
|
60
|
-
if (parent === dir) {
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
dir = parent;
|
|
64
|
-
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) || fs.existsSync(path.join(dir, 'turbo.json'))) {
|
|
65
|
-
return dir;
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
69
|
-
if (pkg.workspaces) {
|
|
70
|
-
return dir;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// keep walking up
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
/** Internal workspace packages this app depends on (`workspace:` / `catalog:`
|
|
80
|
-
* protocol) whose build output is missing — a bare `next dev` fails on
|
|
81
|
-
* `Cannot find module '@repo/…/dist/…'` until they are built. */
|
|
82
|
-
function unbuiltWorkspaceDeps(cwd) {
|
|
83
|
-
let pkg;
|
|
84
|
-
try {
|
|
85
|
-
pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
const deps = Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })
|
|
91
|
-
.filter(([, version]) => typeof version === 'string' && (version.startsWith('workspace:') || version.startsWith('catalog:')))
|
|
92
|
-
.map(([name]) => name);
|
|
93
|
-
return deps.filter((name) => {
|
|
94
|
-
const depDir = path.join(cwd, 'node_modules', name);
|
|
95
|
-
try {
|
|
96
|
-
const depPkg = JSON.parse(fs.readFileSync(path.join(depDir, 'package.json'), 'utf8'));
|
|
97
|
-
const entry = depPkg.module ?? depPkg.main;
|
|
98
|
-
if (typeof entry === 'string') {
|
|
99
|
-
return !fs.existsSync(path.join(depDir, entry));
|
|
100
|
-
}
|
|
101
|
-
// exports-only package: treat a missing dist/ as unbuilt.
|
|
102
|
-
return Boolean(depPkg.exports) && !fs.existsSync(path.join(depDir, 'dist'));
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return false; // can't resolve the link → don't force a build
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Build workspace packages the app needs BEFORE booting its dev server. Fimo
|
|
111
|
-
* runs the app's bare `next dev`, but in a Turborepo/pnpm monorepo the
|
|
112
|
-
* internal deps (`@repo/*`) aren't built by that — `turbo dev` would build
|
|
113
|
-
* them first. This detects unbuilt internal deps and runs the monorepo's
|
|
114
|
-
* build once (the root `build` script, else `turbo run build`); turbo's cache
|
|
115
|
-
* makes an already-built repo near-instant. A build failure is surfaced with
|
|
116
|
-
* an actionable message instead of a cryptic `Cannot find module` at boot.
|
|
117
|
-
*/
|
|
118
|
-
export function ensureWorkspaceDepsBuilt(cwd, packageManager, onProgress) {
|
|
119
|
-
const root = workspaceRoot(cwd);
|
|
120
|
-
if (!root) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const unbuilt = unbuiltWorkspaceDeps(cwd);
|
|
124
|
-
if (unbuilt.length === 0) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
onProgress?.(`building workspace packages (${unbuilt.slice(0, 3).join(', ')}${unbuilt.length > 3 ? '…' : ''})`);
|
|
128
|
-
let rootBuild = null;
|
|
129
|
-
try {
|
|
130
|
-
const rootPkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
131
|
-
if (rootPkg.scripts?.build) {
|
|
132
|
-
rootBuild = { command: packageManager, args: ['run', 'build'] };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// fall through to turbo
|
|
137
|
-
}
|
|
138
|
-
if (!rootBuild && fs.existsSync(path.join(root, 'turbo.json'))) {
|
|
139
|
-
rootBuild = { command: 'npx', args: ['turbo', 'run', 'build'] };
|
|
140
|
-
}
|
|
141
|
-
if (!rootBuild) {
|
|
142
|
-
throw new CaptureError(`Workspace packages (${unbuilt.join(', ')}) must be built before the dev server can boot, and the monorepo has no root \`build\` script or turbo config to run. Build them yourself, then re-run \`npx fimo migrate\`.`);
|
|
143
|
-
}
|
|
144
|
-
const result = spawnSync(rootBuild.command, rootBuild.args, {
|
|
145
|
-
cwd: root,
|
|
146
|
-
timeout: 300_000,
|
|
147
|
-
encoding: 'utf8',
|
|
148
|
-
env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' },
|
|
149
|
-
});
|
|
150
|
-
if (result.status !== 0) {
|
|
151
|
-
throw new CaptureError(`Workspace packages (${unbuilt.join(', ')}) need building for the dev server, but \`${rootBuild.command} ${rootBuild.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)}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
export async function startDevServer(opts) {
|
|
155
|
-
preflight(opts.cwd);
|
|
156
|
-
const port = await findFreePort();
|
|
157
|
-
const child = spawn(opts.packageManager, ['run', 'dev'], {
|
|
158
|
-
cwd: opts.cwd,
|
|
159
|
-
detached: process.platform !== 'win32',
|
|
160
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
161
|
-
env: { ...process.env, PORT: String(port), NEXT_TELEMETRY_DISABLED: '1', BROWSER: 'none' },
|
|
162
|
-
});
|
|
163
|
-
let stdoutBuffer = '';
|
|
164
|
-
let stderrBuffer = '';
|
|
165
|
-
child.stdout?.on('data', (chunk) => {
|
|
166
|
-
stdoutBuffer = `${stdoutBuffer}${chunk.toString()}`.slice(-8_000);
|
|
167
|
-
});
|
|
168
|
-
child.stderr?.on('data', (chunk) => {
|
|
169
|
-
stderrBuffer = `${stderrBuffer}${chunk.toString()}`.slice(-8_000);
|
|
170
|
-
});
|
|
171
|
-
let exited = false;
|
|
172
|
-
child.on('exit', () => {
|
|
173
|
-
exited = true;
|
|
174
|
-
});
|
|
175
|
-
const stop = async () => {
|
|
176
|
-
if (exited || child.pid === undefined) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
if (process.platform === 'win32') {
|
|
181
|
-
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F']);
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
process.kill(-child.pid, 'SIGTERM');
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
child.kill('SIGTERM');
|
|
189
|
-
}
|
|
190
|
-
const deadline = Date.now() + KILL_GRACE_MS;
|
|
191
|
-
while (!exited && Date.now() < deadline) {
|
|
192
|
-
await sleep(100);
|
|
193
|
-
}
|
|
194
|
-
if (!exited && child.pid !== undefined && process.platform !== 'win32') {
|
|
195
|
-
try {
|
|
196
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
// already gone
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
// Readiness: an HTTP response BELOW 500 counts as listening (a 404 means
|
|
204
|
-
// the router is up). Nuxt's dev server answers 503 immediately while Vite
|
|
205
|
-
// still builds — treating that as ready captured a baseline of 503s.
|
|
206
|
-
// Dev scripts that hardcode their own port are handled by sniffing stdout.
|
|
207
|
-
const probe = async (origin) => {
|
|
208
|
-
try {
|
|
209
|
-
const response = await fetch(origin, { signal: AbortSignal.timeout(2_000) });
|
|
210
|
-
return response.status < 500;
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
// Probe via the `localhost` hostname, never a numeric loopback: Vite-family
|
|
217
|
-
// servers (Astro, Nuxt, SvelteKit) may bind ::1 only, and Node's fetch
|
|
218
|
-
// tries both address families for a hostname (Happy Eyeballs).
|
|
219
|
-
const deadline = Date.now() + opts.bootTimeoutMs;
|
|
220
|
-
let origin = `http://localhost:${port}`;
|
|
221
|
-
while (Date.now() < deadline) {
|
|
222
|
-
if (exited) {
|
|
223
|
-
await stop();
|
|
224
|
-
throw new CaptureError('The dev server exited before becoming ready.', stderrBuffer || stdoutBuffer);
|
|
225
|
-
}
|
|
226
|
-
if (await probe(origin)) {
|
|
227
|
-
return { origin, stderrTail: () => stderrBuffer, stop };
|
|
228
|
-
}
|
|
229
|
-
const sniffed = stdoutBuffer.match(/(?:localhost|127\.0\.0\.1):(\d{2,5})/);
|
|
230
|
-
if (sniffed && Number(sniffed[1]) !== port) {
|
|
231
|
-
const candidate = `http://localhost:${sniffed[1]}`;
|
|
232
|
-
if (await probe(candidate)) {
|
|
233
|
-
origin = candidate;
|
|
234
|
-
return { origin, stderrTail: () => stderrBuffer, stop };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
await sleep(READINESS_POLL_MS);
|
|
238
|
-
}
|
|
239
|
-
await stop();
|
|
240
|
-
throw new CaptureError(`The dev server did not answer within ${Math.round(opts.bootTimeoutMs / 1000)}s.`, stderrBuffer || stdoutBuffer);
|
|
241
|
-
}
|
|
242
|
-
async function fetchRoute(origin, routePath, timeoutMs) {
|
|
243
|
-
const response = await fetch(new URL(routePath, origin), {
|
|
244
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
245
|
-
headers: { accept: 'text/html' },
|
|
246
|
-
});
|
|
247
|
-
return { status: response.status, html: await response.text() };
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Boot the project's own dev server, capture every static route's normalized
|
|
251
|
-
* signature, and tear the server down. Baseline and check both run through
|
|
252
|
-
* this identical pipeline — that symmetry is the gate's correctness property.
|
|
253
|
-
*/
|
|
254
|
-
export async function captureParityBaseline(opts) {
|
|
255
|
-
const bootTimeoutMs = opts.bootTimeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS;
|
|
256
|
-
const routeTimeoutMs = opts.routeTimeoutMs ?? DEFAULT_ROUTE_TIMEOUT_MS;
|
|
257
|
-
const routes = {};
|
|
258
|
-
const paramValues = opts.paramValues ?? {};
|
|
259
|
-
// Capturable = a concrete URL exists (no params, or all params resolved via
|
|
260
|
-
// paramValues). The capture is keyed by the ORIGINAL route.path so baseline
|
|
261
|
-
// and check line up; only the fetched URL is the resolved one.
|
|
262
|
-
const staticRoutes = [];
|
|
263
|
-
for (const route of opts.routes) {
|
|
264
|
-
const resolved = route.path.includes(':') ? resolveRoutePath(route.path, paramValues) : route.path;
|
|
265
|
-
if (resolved === null) {
|
|
266
|
-
routes[route.path] = { skipped: 'dynamic-params' };
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
staticRoutes.push({ path: route.path, url: resolved });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
let server;
|
|
273
|
-
if (opts.serverOrigin) {
|
|
274
|
-
const origin = opts.serverOrigin.replace(/\/+$/, '');
|
|
275
|
-
opts.onProgress?.(`using the running dev server at ${origin}`);
|
|
276
|
-
try {
|
|
277
|
-
await fetch(origin, { signal: AbortSignal.timeout(5_000) });
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
throw new CaptureError(`No dev server answered at ${origin} — start it (e.g. \`${opts.packageManager} run dev\`) or drop --server to boot one.`);
|
|
281
|
-
}
|
|
282
|
-
server = { origin, stderrTail: () => '', stop: async () => { } };
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
// Monorepo: build the app's workspace deps before the bare dev boot, or
|
|
286
|
-
// `next dev` dies on `Cannot find module '@repo/…'`.
|
|
287
|
-
ensureWorkspaceDepsBuilt(opts.cwd, opts.packageManager, opts.onProgress);
|
|
288
|
-
const bootStartedAt = Date.now();
|
|
289
|
-
opts.onProgress?.('starting your dev server');
|
|
290
|
-
const bootTicker = setInterval(() => {
|
|
291
|
-
opts.onProgress?.(`starting your dev server (${Math.round((Date.now() - bootStartedAt) / 1000)}s)`);
|
|
292
|
-
}, 5_000);
|
|
293
|
-
bootTicker.unref();
|
|
294
|
-
try {
|
|
295
|
-
server = await startDevServer({ cwd: opts.cwd, packageManager: opts.packageManager, bootTimeoutMs });
|
|
296
|
-
}
|
|
297
|
-
finally {
|
|
298
|
-
clearInterval(bootTicker);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
try {
|
|
302
|
-
// Sequential on purpose: dev compilers choke on parallel cold compiles.
|
|
303
|
-
let index = 0;
|
|
304
|
-
for (const route of staticRoutes) {
|
|
305
|
-
index += 1;
|
|
306
|
-
const routeStartedAt = Date.now();
|
|
307
|
-
const progress = () => {
|
|
308
|
-
const elapsed = Math.round((Date.now() - routeStartedAt) / 1000);
|
|
309
|
-
opts.onProgress?.(`capturing ${route.path} (${index}/${staticRoutes.length}${elapsed >= 5 ? `, ${elapsed}s` : ''})`);
|
|
310
|
-
};
|
|
311
|
-
progress();
|
|
312
|
-
const routeTicker = setInterval(progress, 5_000);
|
|
313
|
-
routeTicker.unref();
|
|
314
|
-
try {
|
|
315
|
-
let result = await fetchRoute(server.origin, route.url, routeTimeoutMs).catch(async () => {
|
|
316
|
-
// One retry: first-compile latency and transient dev-server flake.
|
|
317
|
-
await sleep(1_000);
|
|
318
|
-
return fetchRoute(server.origin, route.url, routeTimeoutMs);
|
|
319
|
-
});
|
|
320
|
-
if (result.status >= 500) {
|
|
321
|
-
await sleep(1_000);
|
|
322
|
-
const retried = await fetchRoute(server.origin, route.url, routeTimeoutMs).catch(() => null);
|
|
323
|
-
if (retried) {
|
|
324
|
-
result = retried;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
routes[route.path] = {
|
|
328
|
-
signature: extractRouteSignature(result.html, { origin: server.origin, status: result.status }),
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
routes[route.path] = { skipped: 'fetch-failed', error: error instanceof Error ? error.message : String(error) };
|
|
333
|
-
}
|
|
334
|
-
finally {
|
|
335
|
-
clearInterval(routeTicker);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
// Second pass for error pages: by now the dev server is fully warm, so a
|
|
339
|
-
// 500 recorded above was very likely a cold-compile flake. An error
|
|
340
|
-
// snapshot poisons the baseline (the parity gate would compare against
|
|
341
|
-
// the error page forever), so retry once more before accepting it.
|
|
342
|
-
for (const route of staticRoutes) {
|
|
343
|
-
const capture = routes[route.path];
|
|
344
|
-
if (capture && 'signature' in capture && capture.signature.status >= 500) {
|
|
345
|
-
opts.onProgress?.(`retrying ${route.path} (captured ${capture.signature.status})`);
|
|
346
|
-
const retried = await fetchRoute(server.origin, route.url, routeTimeoutMs).catch(() => null);
|
|
347
|
-
if (retried && retried.status < 500) {
|
|
348
|
-
routes[route.path] = {
|
|
349
|
-
signature: extractRouteSignature(retried.html, { origin: server.origin, status: retried.status }),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
finally {
|
|
356
|
-
await server.stop();
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
version: PARITY_BASELINE_VERSION,
|
|
360
|
-
capturedAt: new Date().toISOString(),
|
|
361
|
-
engine: 'dev-server',
|
|
362
|
-
framework: opts.framework,
|
|
363
|
-
command: `${opts.packageManager} run dev`,
|
|
364
|
-
routes,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
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
|
|
@@ -1,13 +0,0 @@
|
|
|
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;
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import * as cheerio from 'cheerio';
|
|
2
|
-
// `title` included: Next dev streams document metadata into the body end
|
|
3
|
-
// until the shell settles — title text is never visible page content, and
|
|
4
|
-
// whether it lands in head or body depends on layout asyncness (a wrap
|
|
5
|
-
// migration legitimately makes the root layout async).
|
|
6
|
-
const REMOVED_TAGS = new Set([
|
|
7
|
-
'script',
|
|
8
|
-
'style',
|
|
9
|
-
'noscript',
|
|
10
|
-
'template',
|
|
11
|
-
'link',
|
|
12
|
-
'meta',
|
|
13
|
-
'svg',
|
|
14
|
-
'iframe',
|
|
15
|
-
'head',
|
|
16
|
-
'title',
|
|
17
|
-
]);
|
|
18
|
-
const BLOCK_TAGS = new Set([
|
|
19
|
-
'p',
|
|
20
|
-
'h1',
|
|
21
|
-
'h2',
|
|
22
|
-
'h3',
|
|
23
|
-
'h4',
|
|
24
|
-
'h5',
|
|
25
|
-
'h6',
|
|
26
|
-
'li',
|
|
27
|
-
'td',
|
|
28
|
-
'th',
|
|
29
|
-
'dt',
|
|
30
|
-
'dd',
|
|
31
|
-
'blockquote',
|
|
32
|
-
'figcaption',
|
|
33
|
-
'pre',
|
|
34
|
-
'button',
|
|
35
|
-
'label',
|
|
36
|
-
'summary',
|
|
37
|
-
'caption',
|
|
38
|
-
'option',
|
|
39
|
-
]);
|
|
40
|
-
const FALLBACK_BLOCK_TAGS = new Set(['div', 'section', 'article', 'main', 'aside', 'header', 'footer', 'nav', 'body']);
|
|
41
|
-
const MARK_KIND = {
|
|
42
|
-
strong: 'strong',
|
|
43
|
-
b: 'strong',
|
|
44
|
-
em: 'em',
|
|
45
|
-
i: 'em',
|
|
46
|
-
u: 'u',
|
|
47
|
-
s: 's',
|
|
48
|
-
del: 's',
|
|
49
|
-
strike: 's',
|
|
50
|
-
code: 'code',
|
|
51
|
-
a: 'a',
|
|
52
|
-
};
|
|
53
|
-
function collapse(text) {
|
|
54
|
-
return text.replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
|
|
55
|
-
}
|
|
56
|
-
/** Strip the capture origin so differing ports between runs compare equal. */
|
|
57
|
-
export function normalizeHref(href, origin) {
|
|
58
|
-
if (href.startsWith(origin)) {
|
|
59
|
-
const stripped = href.slice(origin.length);
|
|
60
|
-
return stripped.startsWith('/') ? stripped : `/${stripped}`;
|
|
61
|
-
}
|
|
62
|
-
return href;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Media src normalization: origin strip + Next image-optimizer decode, so a
|
|
66
|
-
* pre-wrap `next/image` (`/_next/image?url=…&w=…`) and a post-wrap plain
|
|
67
|
-
* `<img src>` of the same asset compare equal.
|
|
68
|
-
*/
|
|
69
|
-
export function normalizeMediaSrc(src, origin) {
|
|
70
|
-
let value = normalizeHref(src, origin);
|
|
71
|
-
const optimizerMatch = value.match(/^\/_next\/image\?(.+)$/);
|
|
72
|
-
if (optimizerMatch) {
|
|
73
|
-
const params = new URLSearchParams(optimizerMatch[1]);
|
|
74
|
-
const url = params.get('url');
|
|
75
|
-
if (url) {
|
|
76
|
-
value = url;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return value;
|
|
80
|
-
}
|
|
81
|
-
function isExcluded(node) {
|
|
82
|
-
const tag = node.name?.toLowerCase() ?? '';
|
|
83
|
-
if (REMOVED_TAGS.has(tag)) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
// Hidden elements are not visible content — except Next streaming
|
|
87
|
-
// containers (div[hidden][id^="S:"]), whose content replaces Suspense
|
|
88
|
-
// holes; kept in document order so streamed content is captured
|
|
89
|
-
// symmetrically between baseline and check.
|
|
90
|
-
if (node.attribs && 'hidden' in node.attribs) {
|
|
91
|
-
const isStreamingContainer = tag === 'div' && (node.attribs.id ?? '').startsWith('S:');
|
|
92
|
-
if (!isStreamingContainer) {
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
function nearestBlock(node) {
|
|
99
|
-
let current = node.parent;
|
|
100
|
-
let fallback = null;
|
|
101
|
-
while (current) {
|
|
102
|
-
const tag = current.name?.toLowerCase();
|
|
103
|
-
if (tag && BLOCK_TAGS.has(tag)) {
|
|
104
|
-
return current;
|
|
105
|
-
}
|
|
106
|
-
if (!fallback && tag && FALLBACK_BLOCK_TAGS.has(tag)) {
|
|
107
|
-
fallback = current;
|
|
108
|
-
}
|
|
109
|
-
current = current.parent;
|
|
110
|
-
}
|
|
111
|
-
return fallback;
|
|
112
|
-
}
|
|
113
|
-
function collectText(node) {
|
|
114
|
-
if (node.type === 'text') {
|
|
115
|
-
return String(node.data ?? '');
|
|
116
|
-
}
|
|
117
|
-
if (node.type !== 'tag' || isExcluded(node)) {
|
|
118
|
-
return '';
|
|
119
|
-
}
|
|
120
|
-
return (node.children ?? []).map(collectText).join('');
|
|
121
|
-
}
|
|
122
|
-
export function extractRouteSignature(html, opts) {
|
|
123
|
-
const $ = cheerio.load(html);
|
|
124
|
-
const bodyNode = $('body').get(0);
|
|
125
|
-
const blocks = [];
|
|
126
|
-
const blockIndex = new Map();
|
|
127
|
-
const images = [];
|
|
128
|
-
const videos = [];
|
|
129
|
-
const links = [];
|
|
130
|
-
const visit = (node) => {
|
|
131
|
-
if (node.type === 'text') {
|
|
132
|
-
const text = collapse(String(node.data ?? ''));
|
|
133
|
-
if (!text) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const block = nearestBlock(node);
|
|
137
|
-
if (!block) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const existing = blockIndex.get(block);
|
|
141
|
-
if (existing === undefined) {
|
|
142
|
-
blockIndex.set(block, blocks.length);
|
|
143
|
-
blocks.push({ tag: block.name?.toLowerCase() ?? 'div', text, marks: [] });
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
blocks[existing].text = collapse(`${blocks[existing].text} ${text}`);
|
|
147
|
-
}
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (node.type !== 'tag' && node.type !== 'root') {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (node.type === 'tag' && isExcluded(node)) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const tag = node.name?.toLowerCase() ?? '';
|
|
157
|
-
const attribs = node.attribs ?? {};
|
|
158
|
-
// Inline-marks signature: the formatting that must survive wrapping and
|
|
159
|
-
// the TipTap seed round-trip. Attached to the nearest block.
|
|
160
|
-
const markKind = MARK_KIND[tag];
|
|
161
|
-
if (markKind) {
|
|
162
|
-
const text = collapse(collectText(node));
|
|
163
|
-
if (text) {
|
|
164
|
-
const block = nearestBlock(node);
|
|
165
|
-
if (block) {
|
|
166
|
-
if (!blockIndex.has(block)) {
|
|
167
|
-
blockIndex.set(block, blocks.length);
|
|
168
|
-
blocks.push({ tag: block.name?.toLowerCase() ?? 'div', text: '', marks: [] });
|
|
169
|
-
}
|
|
170
|
-
const mark = { kind: markKind, text };
|
|
171
|
-
if (markKind === 'a') {
|
|
172
|
-
if (attribs.href) {
|
|
173
|
-
mark.href = normalizeHref(attribs.href, opts.origin);
|
|
174
|
-
blocks[blockIndex.get(block)].marks.push(mark);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
blocks[blockIndex.get(block)].marks.push(mark);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (tag === 'img' && attribs.src) {
|
|
184
|
-
images.push(normalizeMediaSrc(attribs.src, opts.origin));
|
|
185
|
-
}
|
|
186
|
-
if (tag === 'source' && attribs.src) {
|
|
187
|
-
const parentTag = node.parent?.name?.toLowerCase();
|
|
188
|
-
if (parentTag === 'picture') {
|
|
189
|
-
images.push(normalizeMediaSrc(attribs.src, opts.origin));
|
|
190
|
-
}
|
|
191
|
-
else if (parentTag === 'video') {
|
|
192
|
-
videos.push(normalizeMediaSrc(attribs.src, opts.origin));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (tag === 'video') {
|
|
196
|
-
if (attribs.src) {
|
|
197
|
-
videos.push(normalizeMediaSrc(attribs.src, opts.origin));
|
|
198
|
-
}
|
|
199
|
-
if (attribs.poster) {
|
|
200
|
-
videos.push(normalizeMediaSrc(attribs.poster, opts.origin));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (tag === 'a' && attribs.href) {
|
|
204
|
-
links.push({ href: normalizeHref(attribs.href, opts.origin), text: collapse(collectText(node)) });
|
|
205
|
-
}
|
|
206
|
-
for (const child of node.children ?? []) {
|
|
207
|
-
visit(child);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
if (bodyNode) {
|
|
211
|
-
for (const child of bodyNode.children ?? []) {
|
|
212
|
-
visit(child);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
// Blocks created empty by a mark preceding any direct text (e.g. a <p>
|
|
216
|
-
// starting with <strong>) get their text from the block's full content.
|
|
217
|
-
for (const [block, index] of blockIndex) {
|
|
218
|
-
if (!blocks[index].text) {
|
|
219
|
-
blocks[index].text = collapse(collectText(block));
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return { status: opts.status, blocks: blocks.filter((block) => block.text.length > 0), images, videos, links };
|
|
223
|
-
}
|