Setting the file. One moment. Create Wix Project · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
— line 262
This file
- Number
- 28.16
- Position
- 16 of 89
- Type
- JavaScript
- Size
- 17 KB
- Lines
- 493
scripts/create-wix-project.mjs
JavaScript·493 lines·17 KB
10 ensureDir,
11 normalizeUrl,
12 parseArgs,
13 projectNameFromUrl,
14 readJson,
15 resolveOutputDir,
16 selectWixTemplate,
17 wixBusinessNameFromProject,
18 wixFolderNameFromProject,
19 writeJson,
20} from "./lib/common.mjs";
21import { clearWixStarter } from "./clear-wix-starter.mjs";
22
23async function main() {
24 const args = parseArgs();
25 const url = normalizeUrl(args._[0] || args.url).toString();
26 const outputDir = resolveOutputDir(url, args.out);
27 const projectName = path.basename(outputDir);
28 const result = await createWixProject({
29 sourceUrl: url,
30 outputDir,
31 projectName,
32 businessName: args["business-name"] || businessNameFromProject(projectNameFromUrl(url)),
33 template: args.template,
34 execute: Boolean(args.execute),
35 clearStarter: args["clear-starter"] !== "false",
36 });
37 if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
38 else console.log(result.command);
39}
40
41export async function createWixProject({ sourceUrl, outputDir, projectName, businessName, template, execute = false, clearStarter = true }) {
42 const parentDir = path.dirname(outputDir);
43 await ensureDir(parentDir);
44 const discovery = await safeRead(path.join(docsDir(outputDir), "discovery.json"), { scope: "home", pages: [] });
45 const counts = countBy(discovery.pages || [], (page) => page.area);
46 const selectedTemplate = template || selectWixTemplate(discovery.scope || "home", counts);
47 const wixFolderName = wixFolderNameFromProject(projectName);
48 const normalizedBusinessName = sanitizeBusinessName(businessName) || wixBusinessNameFromProject(projectName);
49 const wixCreateArgs = [
50 "@wix/new@latest",
51 "headless",
52 "--folder-name",
53 wixFolderName,
54 "--business-name",
55 normalizedBusinessName,
56 "--site-template",
57 selectedTemplate,
58 ];
59 const createRunner = await resolvePackageCreateRunner();
60 const scaffoldParentDir = execute
61 ? await resolveScaffoldParentDir({ preferredParentDir: parentDir, createRunner })
62 : parentDir;
63 const wixOutputDir = path.join(scaffoldParentDir, wixFolderName);
64 const localOutputRename = path.resolve(wixOutputDir) !== path.resolve(outputDir);
65 const createArgs = [...createRunner.argsPrefix, ...wixCreateArgs];
66 const result = {
67 sourceUrl,
68 outputDir,
69 projectName,
70 businessName: normalizedBusinessName,
71 template: selectedTemplate,
72 wixFolderName,
73 wixOutputDir,
74 localOutputRename,
75 clearStarter,
76 command: `${createRunner.displayName} ${createArgs.map(shellQuote).join(" ")}`,
77 executed: execute,
78 generatedAt: new Date().toISOString(),
79 };
80 // Do not write clone metadata yet when provisioning. The Wix CLI requires a
81 // clean target directory; artifacts are recorded only after it owns outputDir.
82 if (!execute) {
83 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
84 return result;
85 }
86 const createRun = await runCommand(createRunner.command, createArgs, {
87 cwd: scaffoldParentDir,
88 timeoutMs: 300000,
89 envOverrides: publicRegistryEnv(),
90 });
91 result.createRun = summarizeRun(createRun);
92 const scaffoldExists = await pathExists(wixOutputDir);
93 if (!createRun.ok) {
94 if (!scaffoldExists) {
95 result.executed = false;
96 result.error = `Wix scaffold creation failed before a local scaffold was created: ${createRun.summary}`;
97 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
98 throw new Error(result.error);
99 }
100 result.recovery = {
101 triggered: true,
102 reason: createRun.timedOut ? "wix-cli-timed-out-after-scaffold-created" : "wix-cli-failed-after-scaffold-created",
103 };
104 await normalizeNodeRuntime(wixOutputDir);
105 await ensureYarnProjectBoundary(wixOutputDir);
106 const yarnInstall = await installWithYarn(wixOutputDir);
107 result.recovery.yarnInstall = summarizeRun(yarnInstall);
108 if (!yarnInstall.ok) {
109 result.executed = false;
110 result.error = `Wix scaffold recovery failed during yarn install: ${yarnInstall.summary}`;
111 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
112 throw new Error(result.error);
113 }
114 }
115 if (localOutputRename) await moveOrMergeWixOutput({ wixOutputDir, outputDir });
116 await ensureYarnProjectBoundary(outputDir);
117 await normalizeNodeRuntime(outputDir);
118 await normalizeHostingAdapterDependencies(outputDir);
119 result.projectIdentity = await validateWixProjectIdentity(outputDir);
120 if (!result.projectIdentity.ok) {
121 result.executed = false;
122 result.error = `Wix scaffold is incomplete: ${result.projectIdentity.summary}`;
123 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
124 throw new Error(result.error);
125 }
126 if (clearStarter) await clearWixStarter({ outputDir });
127 result.runtimeContract = await verifyWixRuntimeProject(outputDir);
128 if (!result.runtimeContract.ok) {
129 result.executed = false;
130 result.error = `Generated Wix project is missing its runnable package contract: ${result.runtimeContract.summary}`;
131 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
132 throw new Error(result.error);
133 }
134 await ensureYarnProjectBoundary(outputDir);
135 const yarnInstall = await installWithYarn(outputDir);
136 result.environment = {
137 yarnInstall: summarizeRun(yarnInstall),
138 };
139 if (!yarnInstall.ok) {
140 result.executed = false;
141 result.error = `Generated Wix project failed dependency normalization: ${yarnInstall.summary}`;
142 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
143 throw new Error(result.error);
144 }
145 const yarnRunner = await resolveYarnRunner();
146 const baselineBuild = await runCommand(yarnRunner.command, [...yarnRunner.argsPrefix, "build"], { cwd: outputDir, timeoutMs: 300000 });
147 result.environment.baselineBuild = summarizeRun(baselineBuild);
148 if (!baselineBuild.ok) {
149 result.executed = false;
150 result.error = `Generated Wix project failed baseline build verification: ${baselineBuild.summary}`;
151 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
152 throw new Error(result.error);
153 }
154 result.executed = true;
155 result.finalOutputDir = outputDir;
156 await writeJson(path.join(docsDir(outputDir), "wix-project.json"), result);
157 return result;
158}
159
160export async function verifyWixRuntimeProject(projectDir) {
161 let manifest;
162 try {
163 manifest = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8"));
164 } catch (error) {
165 return { ok: false, summary: `missing or unreadable package.json: ${error.message}` };
166 }
167 const scripts = manifest.scripts || {};
168 const missing = ["dev", "build"].filter((name) => !String(scripts[name] || "").trim());
169 if (missing.length) return { ok: false, summary: `package.json is missing required scripts: ${missing.join(", ")}` };
170 const wixConfig = path.join(projectDir, "wix.config.json");
171 if (!await pathExists(wixConfig)) return { ok: false, summary: "wix.config.json is missing" };
172 return { ok: true, summary: "package.json, Wix config, and dev/build scripts are present" };
173}
174
175function sanitizeBusinessName(value) {
176 return String(value || "")
177 .replace(/[_-]+/g, " ")
178 .replace(/\s+/g, " ")
179 .trim();
180}
181
182async function moveOrMergeWixOutput({ wixOutputDir, outputDir }) {
183 try {
184 await rename(wixOutputDir, outputDir);
185 return;
186 } catch (error) {
187 if (error?.code !== "ENOTEMPTY" && error?.code !== "EEXIST") throw error;
188 }
189
190 await ensureDir(outputDir);
191 const entries = await readdir(wixOutputDir, { withFileTypes: true });
192 for (const entry of entries) {
193 const from = path.join(wixOutputDir, entry.name);
194 const to = path.join(outputDir, entry.name);
195 await copyEntryNoOverwrite({ from, to, entry });
196 }
197 await rm(wixOutputDir, { recursive: true, force: true });
198}
199
200async function copyEntryNoOverwrite({ from, to, entry }) {
201 if (entry.isDirectory()) {
202 await ensureDir(to);
203 const children = await readdir(from, { withFileTypes: true });
204 for (const child of children) {
205 await copyEntryNoOverwrite({
206 from: path.join(from, child.name),
207 to: path.join(to, child.name),
208 entry: child,
209 });
210 }
211 return;
212 }
213 await copyFile(from, to, fsConstants.COPYFILE_EXCL);
214}
215
216async function installWithYarn(projectDir) {
217 const yarnRunner = await resolveYarnRunner();
218 await ensureYarnProjectBoundary(projectDir);
219 let run = await runCommand(yarnRunner.command, [...yarnRunner.argsPrefix, "install"], { cwd: projectDir, timeoutMs: 300000 });
220 if (run.ok) return run;
221 if (needsProjectBoundaryLockfile(run)) {
222 await ensureYarnProjectBoundary(projectDir);
223 run = await runCommand(yarnRunner.command, [...yarnRunner.argsPrefix, "install"], { cwd: projectDir, timeoutMs: 300000 });
224 }
225 return run;
226}
227
228function needsProjectBoundaryLockfile(run) {
229 const text = `${run.stdout}\n${run.stderr}`;
230 return /nearest package directory|doesn't seem to be part of the project/i.test(text);
231}
232
233export async function ensureYarnProjectBoundary(projectDir) {
234 const lockfile = path.join(projectDir, "yarn.lock");
235 if (await pathExists(lockfile)) return;
236 await writeFile(lockfile, "", "utf8");
237}
238
239export async function normalizeHostingAdapterDependencies(projectDir) {
240 const packageJsonPath = path.join(projectDir, "package.json");
241 let manifest;
242 try {
243 manifest = JSON.parse(await readFile(packageJsonPath, "utf8"));
244 } catch {
245 return { ok: false, changed: false, summary: "package.json was not readable during hosting-adapter normalization" };
246 }
247
248 const devDependencies = manifest.devDependencies || {};
249 if (!devDependencies["@wix/astro-wix-hosting-adapter"]) {
250 return { ok: true, changed: false, summary: "Wix hosting adapter is not declared; no hosting dependency normalization was needed" };
251 }
252 if (!devDependencies["@astrojs/cloudflare"]) {
253 return { ok: true, changed: false, summary: "Standalone manifest already defers Cloudflare adapter ownership to the Wix hosting adapter" };
254 }
255
256 delete devDependencies["@astrojs/cloudflare"];
257 manifest.devDependencies = sortObjectKeys(devDependencies);
258 await writeFile(packageJsonPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
259 return { ok: true, changed: true, summary: "Removed direct @astrojs/cloudflare so the Wix hosting adapter owns the compatible Cloudflare adapter dependency" };
260}
261
262export async function normalizeNodeRuntime(projectDir) {
263 const packageJsonPath = path.join(projectDir, "package.json");
264 let manifest;
265 try {
266 manifest = JSON.parse(await readFile(packageJsonPath, "utf8"));
267 } catch {
268 return { ok: false, changed: false, summary: "package.json was not readable during Node runtime normalization" };
269 }
270 const major = Number(String(process.versions.node).split(".")[0]);
271 const declared = String(manifest.engines?.node || "");
272 if (!declared || declared.includes(String(major))) {
273 return { ok: true, changed: false, summary: "The generated project declares the active Node major" };
274 }
275 manifest.engines = { ...(manifest.engines || {}), node: `>=${major}.0.0 <${major + 1}.0.0-0` };
276 await writeFile(packageJsonPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
277 return { ok: true, changed: true, summary: `Normalized generated project Node engine to active major ${major}` };
278}
279
280function sortObjectKeys(record) {
281 return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
282}
283
284function summarizeRun(run) {
285 return {
286 ok: run.ok,
287 exitCode: run.exitCode,
288 timedOut: run.timedOut,
289 summary: run.summary,
290 stdoutTail: String(run.stdout || "").slice(-4000),
291 stderrTail: String(run.stderr || "").slice(-4000),
292 };
293}
294
295async function validateWixProjectIdentity(projectDir) {
296 const configPath = path.join(projectDir, "wix.config.json");
297 let config;
298 try {
299 config = JSON.parse(await readFile(configPath, "utf8"));
300 } catch (error) {
301 return {
302 ok: false,
303 summary: `missing or unreadable wix.config.json at ${configPath}: ${error.message}`,
304 };
305 }
306
307 const appId = firstNonEmptyString(config.appId, config.applicationId);
308 const siteId = firstNonEmptyString(config.siteId);
309 const errors = [];
310 if (!isGuid(appId)) errors.push("wix.config.json appId is missing or not a valid GUID");
311 if (!isGuid(siteId)) errors.push("wix.config.json siteId is missing or not a valid GUID");
312
313 const appConfigPath = path.join(projectDir, ".wix", "app.config.json");
314 const topologyPath = path.join(projectDir, ".wix", "topology.json");
315 if (!await pathExists(appConfigPath)) errors.push(".wix/app.config.json is missing");
316 if (!await pathExists(topologyPath)) errors.push(".wix/topology.json is missing");
317
318 return {
319 ok: errors.length === 0,
320 appId,
321 siteId,
322 summary: errors.length ? errors.join("; ") : `validated appId ${appId} and siteId ${siteId}`,
323 };
324}
325
326function firstNonEmptyString(...values) {
327 for (const value of values) {
328 if (typeof value === "string" && value.trim()) return value.trim();
329 }
330 return "";
331}
332
333function isGuid(value) {
334 return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || ""));
335}
336
337function runCommand(command, args, { cwd, timeoutMs, envOverrides = {} }) {
338 return new Promise((resolve, reject) => {
339 const nodeBinDir = process.execPath ? path.dirname(process.execPath) : "";
340 const pathParts = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
341 const mergedPath = [nodeBinDir, ...pathParts].filter(Boolean).join(path.delimiter);
342 const child = spawn(command, args, {
343 cwd,
344 stdio: ["ignore", "pipe", "pipe"],
345 env: {
346 ...process.env,
347 PATH: mergedPath,
348 ...envOverrides,
349 },
350 });
351 let stdout = "";
352 let stderr = "";
353 let timedOut = false;
354 const timer = setTimeout(() => {
355 timedOut = true;
356 child.kill("SIGTERM");
357 }, timeoutMs);
358 child.stdout.on("data", (chunk) => {
359 stdout += chunk;
360 process.stdout.write(chunk);
361 });
362 child.stderr.on("data", (chunk) => {
363 stderr += chunk;
364 process.stderr.write(chunk);
365 });
366 child.on("exit", (code) => {
367 clearTimeout(timer);
368 resolve({
369 ok: code === 0 && !timedOut,
370 exitCode: code,
371 timedOut,
372 stdout,
373 stderr,
374 summary: timedOut
375 ? `timed out after ${timeoutMs / 1000}s`
376 : `exited with code ${code}`,
377 });
378 });
379 child.on("error", (error) => {
380 clearTimeout(timer);
381 reject(error);
382 });
383 });
384}
385
386async function pathExists(filePath) {
387 try {
388 await access(filePath);
389 return true;
390 } catch {
391 return false;
392 }
393}
394
395async function resolveExecutable(command) {
396 const candidates = [];
397 if (path.isAbsolute(command)) candidates.push(command);
398 const pathEntries = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
399 for (const entry of pathEntries) candidates.push(path.join(entry, command));
400 if (command === "npm" && process.execPath) {
401 candidates.push(path.join(path.dirname(process.execPath), "npm"));
402 }
403 if (command === "corepack" && process.execPath) {
404 candidates.push(path.join(path.dirname(process.execPath), "corepack"));
405 }
406
407 for (const candidate of candidates) {
408 try {
409 await access(candidate, fsConstants.X_OK);
410 return candidate;
411 } catch {
412 // Keep searching.
413 }
414 }
415 throw new Error(`Unable to resolve executable "${command}" from PATH.`);
416}
417
418async function resolvePackageCreateRunner() {
419 try {
420 return {
421 command: await resolveExecutable("npm"),
422 argsPrefix: ["create"],
423 displayName: "npm",
424 };
425 } catch {
426 try {
427 return {
428 command: await resolveExecutable("pnpm"),
429 argsPrefix: ["dlx"],
430 displayName: "pnpm",
431 };
432 } catch {
433 return {
434 command: await resolveExecutable("yarn"),
435 argsPrefix: ["dlx"],
436 displayName: "yarn",
437 };
438 }
439 }
440}
441
442async function resolveYarnRunner() {
443 try {
444 return {
445 command: await resolveExecutable("corepack"),
446 argsPrefix: ["yarn"],
447 displayName: "corepack yarn",
448 };
449 } catch {
450 return {
451 command: await resolveExecutable("yarn"),
452 argsPrefix: [],
453 displayName: "yarn",
454 };
455 }
456}
457
458async function resolveScaffoldParentDir({ preferredParentDir, createRunner }) {
459 if (createRunner.displayName === "npm") {
460 return preferredParentDir;
461 }
462 const tempBaseDir = path.join("/private/tmp", "wix-headless-replatform-");
463 return mkdtemp(tempBaseDir);
464}
465
466function publicRegistryEnv() {
467 return {
468 NPM_CONFIG_REGISTRY: "https://registry.npmjs.org",
469 npm_config_registry: "https://registry.npmjs.org",
470 NPM_CONFIG_USERCONFIG: "/dev/null",
471 npm_config_userconfig: "/dev/null",
472 YARN_NPM_REGISTRY_SERVER: "https://registry.npmjs.org",
473 };
474}
475
476function shellQuote(value) {
477 return /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : JSON.stringify(value);
478}
479
480async function safeRead(filePath, fallback) {
481 try {
482 return await readJson(filePath);
483 } catch {
484 return fallback;
485 }
486}
487
488if (import.meta.url === `file://${process.argv[1]}`) {
489 main().catch((error) => {
490 console.error(error.stack || error.message);
491 process.exit(1);
492 });
493}