Setting the file. One moment.
URL Redirects · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page scripts/ url-redirects.js
JavaScript · 261 lines · 13 KB
14 // verify --project <dir> --stage preparation --connect-host <host> [--connect-port <n>]
15 // verify --project <dir> --stage verified_public --after-dns-switch
16 // retire-import-plan --project <dir> --wix-env <file> --source-key <key>
17 // --from <path> --to <path> --evidence <reference> (local retirement; no remote delete)
18 //
19 // Optional: --concurrency <n> --timeout-ms <n> --retries <n>
20 //
21 // Credentials (from --wix-env): WIX_SITE_ID plus WIX_API_KEY or WIX_AUTH_TOKEN for reads.
22 // Writes need Manage SEO Settings and an explicit identity declaration:
23 // APP: WIX_REDIRECTS_APP_TOKEN, WIX_REDIRECTS_APP_SITE_ID, WIX_REDIRECTS_CREDENTIAL_IDENTITY=APP.
24 // Wix user: WIX_AUTH_TOKEN, WIX_SITE_ID, WIX_REDIRECTS_CREDENTIAL_IDENTITY=WIX_USER.
25 // A site-member token is not a Wix user credential; never infer identity from token presence.
26
27 const path = require ( 'node:path' );
28 const fs = require ( 'node:fs/promises' );
29 const state = require ( '../lib/url-preservation-state.js' );
30 const planLib = require ( '../lib/url-redirect-plan.js' );
31 const verification = require ( '../lib/url-verification.js' );
32 const reportLib = require ( '../lib/url-preservation-report.js' );
33 const importedTargets = require ( '../lib/imported-redirect-targets.js' );
34 const { readEnvFile } = require ( '../lib/config-env.js' );
35 const wixAuth = require ( '../lib/wix-auth.js' );
36
37 function parseArgs ( argv ) {
38 const [ mode , ... rest ] = argv;
39 const args = { mode, flags: new Set () };
40 for ( let i = 0 ; i < rest. length ; i += 1 ) {
41 const token = rest[i];
42 if ([ '--exclusive-edit-window-confirmed' , '--after-dns-switch' ]. includes (token)) args.flags. add (token);
43 else if (token. startsWith ( '--' )) args[token. slice ( 2 )] = rest[ ++ i];
44 else throw new Error ( `Unexpected argument: ${ token }` );
45 }
46 if ( ! [ 'sources' , 'import-targets' , 'report' , 'plan' , 'apply' , 'verify' , 'retire-import-plan' ]. includes (mode)) throw new Error ( 'unsupported mode' );
47 if ( ! args.project) throw new Error ( '--project <dir> is required' );
48 args.project = path. resolve (args.project);
49 return args;
50 }
51
52 async function loadInputs ( projectDir ) {
53 // Replay all four import-era artifacts (latest row per key), so a recorded redirect
54 // requirement or unresolved/manual decision cannot drop out of the plan.
55 const ledger = await state. loadUrlLedger (projectDir);
56 const importRedirects = await state. loadRedirectsForPlanning (projectDir);
57 const importUnresolved = await state. loadUnresolved (projectDir);
58 const basePaths = await state. loadBasePaths (projectDir);
59 const sources = await state. loadSourceRedirects (projectDir);
60 const importedRedirectOutcomes = await importedTargets. currentAuditRows (projectDir);
61 return {
62 ledgerRows: Object. values (ledger.byKey),
63 importRedirects: Object. values (importRedirects.byKey),
64 invalidImportRedirects: importRedirects.rejected,
65 importUnresolved: Object. values (importUnresolved.byKey),
66 basePaths: basePaths.entries,
67 sourceRules: sources.latest,
68 importedRedirectOutcomes,
69 coverage: await state. loadStateJson (projectDir, 'sourceRedirectCoverage' ),
70 sourceDomains: await state. loadStateJson (projectDir, 'sourceDomains' ),
71 finalRoutes: await state. loadStateJson (projectDir, 'finalRoutes' ),
72 launchPlan: await state. loadStateJson (projectDir, 'launchPlan' ),
73 applyStatus: await state. loadStateJson (projectDir, 'applyStatus' ),
74 };
75 }
76
77 function siteIdOf ( inputs , env ) {
78 return (inputs.finalRoutes && inputs.finalRoutes.siteId) || (inputs.launchPlan && inputs.launchPlan.siteId) || (env && env. WIX_SITE_ID ) || 'unbound-site' ;
79 }
80
81 function readClientFrom ( env , projectDir ) {
82 const { createWixClient } = require ( '../resources/rp-target-wix/lib/wix-writers.js' );
83 const token = env. WIX_API_KEY || env. WIX_AUTH_TOKEN || env. WIX_REDIRECTS_APP_TOKEN ;
84 if ( ! token || ! env. WIX_SITE_ID ) throw new Error ( '--wix-env needs WIX_SITE_ID and WIX_API_KEY or WIX_AUTH_TOKEN' );
85 return createWixClient ({ authToken: token, siteId: env. WIX_SITE_ID , ... (projectDir ? { projectDir } : {}) });
86 }
87
88 function writeCredentialFrom ( env ) {
89 if (env. WIX_REDIRECTS_APP_TOKEN ) {
90 return { token: env. WIX_REDIRECTS_APP_TOKEN , siteId: env. WIX_REDIRECTS_APP_SITE_ID , identity: env. WIX_REDIRECTS_CREDENTIAL_IDENTITY };
91 }
92 if (env. WIX_AUTH_TOKEN && env. WIX_REDIRECTS_CREDENTIAL_IDENTITY === 'WIX_USER' ) {
93 return { token: env. WIX_AUTH_TOKEN , siteId: env. WIX_SITE_ID , identity: 'WIX_USER' };
94 }
95 return null ;
96 }
97
98 async function writeReport ( projectDir , plan , inputs ) {
99 const execution = await state. loadRedirectExecution (projectDir);
100 const verified = await state. loadUrlVerification (projectDir);
101 const report = reportLib. buildUrlPreservationReport ({
102 plan,
103 executionRows: execution.rows,
104 verificationRows: verified.rows,
105 launchPlan: inputs.launchPlan,
106 applyStatus: inputs.applyStatus && inputs.applyStatus.planDigest === plan.planDigest ? inputs.applyStatus : null ,
107 });
108 const paths = await reportLib. writeUrlPreservationReport (projectDir, report);
109 await importedTargets. updateVerificationOutcomes (projectDir, plan, verified.rows);
110 return { report, paths, rejectedRows: execution.rejected. length + verified.rejected. length };
111 }
112
113 function summaryOf ( report ) {
114 return {
115 status: report.status,
116 counts: report.counts,
117 obligations: report.obligationCount,
118 remoteRules: report.remoteRuleCount,
119 coverageComplete: report.coverage.complete,
120 operatorActions: report.operatorActions. map (( a ) => ({ code: a.code, severity: a.severity, affectedCount: a.affectedCount })),
121 report: reportLib. REPORT_MD ,
122 };
123 }
124
125 async function main () {
126 const args = parseArgs (process.argv. slice ( 2 ));
127 const projectDir = args.project;
128 if (args.mode === 'retire-import-plan' ) {
129 if ( ! args[ 'wix-env' ]) throw new Error ( 'retire-import-plan needs --wix-env for a current destination inventory' );
130 const env = await readEnvFile (path. resolve (args[ 'wix-env' ]));
131 const seo = require ( '../resources/rp-target-wix/lib/seo-redirects.js' );
132 const targetInventory = await seo. listWithBackoff ( readClientFrom (env, projectDir), {
133 sleep : ( ms ) => new Promise (( resolve ) => setTimeout (resolve, ms)),
134 });
135 const retired = await importedTargets. retireImportedPlan ({ projectDir,
136 sourceStableKey: args[ 'source-key' ], sourceRelativeUrl: args.from,
137 targetRelativeUrl: args.to, evidence: args.evidence, targetInventory });
138 console. log ( JSON . stringify (retired));
139 return ;
140 }
141 if (args.mode === 'sources' ) {
142 const { extractRedirectSources } = require ( '../resources/rp-source-wordpress/lib/wp-redirect-sources.js' );
143 const { recordSourceRedirectCapture } = require ( '../lib/source-redirect-capture.js' );
144 const input = JSON . parse ( await fs. readFile (path. resolve (args.input), 'utf8' ));
145 const summary = await recordSourceRedirectCapture ({ projectDir, result: extractRedirectSources (input), runId: args.run || input.runId || null });
146 console. log ( JSON . stringify ({ rules: summary.rules, appended: summary.appended, coverage: summary.coverage }, null , 2 ));
147 return ;
148 }
149
150 if (args.mode === 'import-targets' ) {
151 const sourceDomains = await state. loadStateJson (projectDir, 'sourceDomains' );
152 const coverage = await state. loadStateJson (projectDir, 'sourceRedirectCoverage' );
153 if ( ! sourceDomains || ! coverage?.generatedAt) throw new Error ( 'import-targets needs declared source domains and captured source redirect coverage' );
154 const sourceRules = ( await state. loadSourceRedirects (projectDir)).latest
155 . filter (( rule ) => rule.disabledReason !== 'removed_from_complete_capture' );
156 const complete = coverage.providers. length > 0 &&
157 coverage.providers. every (( row ) => [ 'extracted' , 'confirmed_empty' ]. includes (row.status));
158 const report = await importedTargets. planImportedRedirectTargets ({ projectDir,
159 runId: args.run || coverage.generatedAt, captureRevision: coverage.generatedAt,
160 rules: importedTargets. adaptSourceRedirects (sourceRules, {
161 captureRevision: coverage.generatedAt, sourceDomains,
162 }), fullCapture: complete });
163 console. log ( JSON . stringify ({ captureComplete: report.captureComplete,
164 rules: report.rules. length , actionCount: report.operatorActions. length }));
165 return ;
166 }
167
168 const inputs = await loadInputs (projectDir);
169 const env = args[ 'wix-env' ] ? await readEnvFile (path. resolve (args[ 'wix-env' ])) : null ;
170 const siteId = siteIdOf (inputs, env);
171 let inventory = null ;
172 let inventoryError = null ;
173 let readClient = null ;
174 if (env && (args.mode === 'plan' || args.mode === 'apply' )) {
175 readClient = readClientFrom (env, projectDir);
176 if (env. WIX_SITE_ID !== siteId) throw new Error ( 'WIX_SITE_ID does not match the destination site in final-routes.json/launch-plan.json' );
177 const seo = require ( '../resources/rp-target-wix/lib/seo-redirects.js' );
178 try {
179 inventory = await seo. listWithBackoff (readClient, { sleep : ( ms ) => new Promise (( r ) => setTimeout (r, ms)) });
180 } catch (error) {
181 if (wixAuth. isRunInterruption (error)) throw error;
182 if (args.mode !== 'apply' ) throw error;
183 inventoryError = error;
184 }
185 }
186 const plan = planLib. planRedirects ({ siteId, ... inputs, targetInventory: inventory });
187 if (args.mode !== 'plan' && inventory === null && ! inventoryError) {
188 const earlier = await state. loadStateJson (projectDir, 'redirectPlan' );
189 if (earlier?.planDigest === plan.planDigest) {
190 const setupByKey = new Map ((earlier.obligations || []). filter (( ob ) => ob.externalSetup)
191 . map (( ob ) => [ob.obligationKey, ob.externalSetup]));
192 for ( const obligation of plan.obligations) if (obligation.externalTarget) {
193 obligation.externalSetup = setupByKey. get (obligation.obligationKey) || null ;
194 }
195 }
196 }
197 await state. writeStateJson (projectDir, 'redirectPlan' , plan);
198
199 if (args.mode === 'apply' ) {
200 if ( ! env) throw new Error ( 'apply needs --wix-env' );
201 if ( ! plan.websiteBuilt) throw new Error ( 'apply needs final-routes.json from the website build' );
202 const seo = require ( '../resources/rp-target-wix/lib/seo-redirects.js' );
203 const credential = writeCredentialFrom (env);
204 let writeClient = null ;
205 if ( ! seo. credentialProblem (credential, plan.siteId)) {
206 const { createWixClient } = require ( '../resources/rp-target-wix/lib/wix-writers.js' );
207 writeClient = createWixClient ({ authToken: credential.token, siteId: credential.siteId, projectDir });
208 }
209 const result = await seo. applyRedirectPlan ({
210 projectDir,
211 plan,
212 readClient,
213 initialInventory: inventory,
214 initialInventoryError: inventoryError,
215 writeClient,
216 credential,
217 launchPlan: inputs.launchPlan,
218 exclusiveEditWindowConfirmed: args.flags. has ( '--exclusive-edit-window-confirmed' ),
219 runId: args.run || `url-redirects-${ Date . now () }` ,
220 });
221 const applyStatus = { schemaVersion: 1 , planDigest: plan.planDigest, recordedAt: new Date (). toISOString (), summary: result.summary, operatorActions: result.operatorActions };
222 await state. writeStateJson (projectDir, 'applyStatus' , applyStatus);
223 inputs.applyStatus = applyStatus;
224 const { report } = await writeReport (projectDir, plan, inputs);
225 console. log ( JSON . stringify ({ apply: result.summary, ... summaryOf (report) }, null , 2 ));
226 return ;
227 }
228
229 if (args.mode === 'verify' ) {
230 const endpoint = args[ 'connect-host' ]
231 ? { mode: 'connect_override' , connectHost: args[ 'connect-host' ], connectPort: args[ 'connect-port' ] ? Number (args[ 'connect-port' ]) : undefined }
232 : { mode: 'public' };
233 const result = await verification. verifyUrls ({
234 projectDir,
235 plan,
236 sourceDomains: inputs.sourceDomains,
237 stage: args.stage,
238 endpoint,
239 afterDnsSwitch: args.flags. has ( '--after-dns-switch' ),
240 concurrency: args.concurrency ? Number (args.concurrency) : undefined ,
241 timeoutMs: args[ 'timeout-ms' ] ? Number (args[ 'timeout-ms' ]) : undefined ,
242 retryBudget: args.retries ? Number (args.retries) : undefined ,
243 runId: args.run || `url-verify-${ Date . now () }` ,
244 });
245 const { report } = await writeReport (projectDir, plan, inputs);
246 console. log ( JSON . stringify ({ verify: result.summary, exceptionCount: result.exceptionCount, exceptions: result.exceptions, evidence: result.evidence, ... summaryOf (report) }, null , 2 ));
247 return ;
248 }
249
250 const { report , rejectedRows } = await writeReport (projectDir, plan, inputs);
251 console. log ( JSON . stringify ({ ... summaryOf (report), rejectedStateRows: rejectedRows }, null , 2 ));
252 }
253
254 if (require.main === module ) {
255 main (). catch (( error ) => {
256 console. error ( `url-redirects: ${ error . message }` );
257 process. exit ( 1 );
258 });
259 }
260
261 module . exports = { parseArgs, loadInputs, writeCredentialFrom };