Setting the file. One moment. Internal Link Audit · Wix Replatform · wix/skills · Skills Docsscripts/internal-link-audit.js
scripts/internal-link-audit.js
JavaScript·90 lines·7 KB
'../lib/link-plan.js'
);
8const { readLinkAudit, writeLinkAudit, linkOperatorActions } = require('../lib/link-audit.js');
9const { verifyLinkAudit } = require('../lib/link-verification.js');
10const { structuredLinkPatch } = require('../lib/structured-link-patch.js');
11const { createLinkWriteJournal } = require('../lib/link-write-journal.js');
12const { inspectRoute, configureRoute } = require('../lib/app-page-routing.js');
13const routeKnowledge = require('../resources/rp-target-wix/app-page-routing-capabilities.json');
14
15function withinProject(projectDir, relative) {
16 const resolved = path.resolve(projectDir, relative);
17 if (!resolved.startsWith(`${projectDir}${path.sep}`)) throw new Error('input must be inside project directory');
18 return resolved;
19}
20
21async function loadJson(file) { return JSON.parse(await fs.readFile(file, 'utf8')); }
22
23async function main() {
24 const [operation, projectArg, inputArg] = process.argv.slice(2);
25 if (!['plan', 'route', 'verify'].includes(operation) || !projectArg || !inputArg) {
26 throw new Error('Usage: internal-link-audit.js <plan|route|verify> <projectDir> <input.json>');
27 }
28 const projectDir = path.resolve(projectArg);
29 const config = await loadJson(withinProject(projectDir, inputArg));
30 if (operation === 'plan') {
31 const previous = await readLinkAudit(projectDir);
32 if (!Array.isArray(config.expectedFiles)) throw new Error('plan input needs the complete captured-file inventory as expectedFiles');
33 const roots = config.capturedRoots.map((item) => withinProject(projectDir, item));
34 const expectedFiles = config.expectedFiles.map((item) => withinProject(projectDir, item));
35 const containingPages = Object.fromEntries(Object.entries(config.containingPages || {}).map(([file, value]) => [withinProject(projectDir, file), value]));
36 const scanned = await census({ capturedRoots: roots, expectedFiles, sourceHosts: config.sourceHosts, destinationHosts: config.destinationHosts, containingPages, previous });
37 const routeFile = path.join(projectDir, 'state/url-preservation/app-page-routes.json');
38 const routes = await loadJson(routeFile).then((row) => row.results).catch((error) => { if (error.code === 'ENOENT') return []; throw error; });
39 const audit = buildLinkPlan({ census: scanned, targets: config.targets, samples: config.samples || [], routes, ownershipDecisions: config.ownershipDecisions || {}, activeWriters: config.activeWriters, previous, runId: config.runId, sourceSnapshot: config.sourceSnapshot, routeRevision: config.routeRevision, deploymentId: config.deploymentId || null, budgetDecision: config.budgetDecision || null });
40 await writeLinkAudit(projectDir, audit);
41 console.log(JSON.stringify({ stage: audit.stage, counts: audit.counts, pending: audit.pendingDestinations.length, unresolved: audit.unresolved.length, operatorActions: linkOperatorActions(audit).map((row) => row.code) }));
42 return;
43 }
44 if (operation === 'route') {
45 const transport = require(withinProject(projectDir, config.transportModule));
46 if (typeof transport.inspectDestination !== 'function' || (await transport.inspectDestination()).isLive !== false) {
47 throw new Error('route configuration requires an inspected non-live migration destination');
48 }
49 const results = [];
50 for (const row of config.routes) {
51 const capability = routeKnowledge.capabilities.find((item) => item.family === row.source.family && item.frontendType === row.frontendType)
52 || { family: row.source.family, frontendType: row.frontendType, supported: false, reason: 'missing-schema-backed-adapter' };
53 const existing = capability.supported ? await transport.read(capability.key, capability) : null;
54 const plan = inspectRoute(row.source, capability, existing);
55 results.push(await configureRoute(plan, capability, transport));
56 }
57 const output = withinProject(projectDir, config.output || 'state/url-preservation/app-page-routes.json');
58 await fs.mkdir(path.dirname(output), { recursive: true });
59 const temporary = `${output}.${process.pid}.tmp`;
60 await fs.writeFile(temporary, `${JSON.stringify({ schemaVersion: 1, results }, null, 2)}\n`);
61 await fs.rename(temporary, output);
62 console.log(JSON.stringify({ routeCount: results.length, configured: results.filter((row) => row.status === 'configured' || row.status === 'reused').length, unresolved: results.filter((row) => !['configured', 'reused'].includes(row.status)).length }));
63 return;
64 }
65 const audit = await readLinkAudit(projectDir);
66 if (!audit) throw new Error('link audit missing; run plan first');
67 const adapter = require(withinProject(projectDir, config.adapterModule));
68 if (typeof adapter.getActiveWriters !== 'function') throw new Error('adapter must provide getActiveWriters');
69 if (typeof adapter.inspectDestination !== 'function' || (await adapter.inspectDestination()).isLive !== false) {
70 throw new Error('link repair requires an inspected non-live migration destination');
71 }
72 const activeWriters = await adapter.getActiveWriters();
73 if (typeof adapter.readRecord !== 'function' || typeof adapter.writeRecord !== 'function' || typeof adapter.checkRenderedLink !== 'function') {
74 throw new Error('verify adapter must provide readRecord, writeRecord and checkRenderedLink; per-occurrence repair callbacks are not accepted');
75 }
76 const recordAdapter = { read: adapter.readRecord.bind(adapter), write: adapter.writeRecord.bind(adapter), patchField: structuredLinkPatch };
77 const result = await verifyLinkAudit(audit, {
78 deploymentId: config.deploymentId, destinationState: 'migration-not-live', activeWriters,
79 checkDestination: adapter.checkDestination, checkSample: adapter.checkSample,
80 recordAdapter, journal: createLinkWriteJournal(projectDir, audit.runId),
81 checkRenderedLink: adapter.checkRenderedLink.bind(adapter), checkFragment: adapter.checkFragment?.bind(adapter),
82 readBackMs: config.readBackMs,
83 writeMs: config.writeMs, writeSource: config.writeSource,
84 sessionCount: config.sessionCount, exceptionTokens: config.exceptionTokens,
85 onCheckpoint: (snapshot) => writeLinkAudit(projectDir, snapshot),
86 });
87 console.log(JSON.stringify({ stage: result.stage, estimate: result.estimate, pending: result.pendingDestinations.length, unresolved: result.unresolved.length, operatorActions: linkOperatorActions(result).map((row) => row.code) }));
88}
89
90main().catch((error) => { console.error(error && error.message || error); process.exitCode = 1; });