@salesforce/storefront-next-dev 0.1.1 → 0.2.0-alpha.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.
Files changed (50) hide show
  1. package/README.md +45 -36
  2. package/bin/run.js +12 -0
  3. package/dist/bundle.js +83 -0
  4. package/dist/cartridge-services/index.d.ts +2 -26
  5. package/dist/cartridge-services/index.d.ts.map +1 -1
  6. package/dist/cartridge-services/index.js +3 -336
  7. package/dist/cartridge-services/index.js.map +1 -1
  8. package/dist/commands/create-bundle.js +107 -0
  9. package/dist/commands/create-instructions.js +174 -0
  10. package/dist/commands/create-storefront.js +210 -0
  11. package/dist/commands/deploy-cartridge.js +52 -0
  12. package/dist/commands/dev.js +122 -0
  13. package/dist/commands/extensions/create.js +38 -0
  14. package/dist/commands/extensions/install.js +44 -0
  15. package/dist/commands/extensions/list.js +21 -0
  16. package/dist/commands/extensions/remove.js +38 -0
  17. package/dist/commands/generate-cartridge.js +35 -0
  18. package/dist/commands/prepare-local.js +30 -0
  19. package/dist/commands/preview.js +101 -0
  20. package/dist/commands/push.js +139 -0
  21. package/dist/config.js +87 -0
  22. package/dist/configs/react-router.config.js +3 -1
  23. package/dist/configs/react-router.config.js.map +1 -1
  24. package/dist/dependency-utils.js +314 -0
  25. package/dist/entry/client.d.ts +1 -0
  26. package/dist/entry/client.js +28 -0
  27. package/dist/entry/client.js.map +1 -0
  28. package/dist/entry/server.d.ts +15 -0
  29. package/dist/entry/server.d.ts.map +1 -0
  30. package/dist/entry/server.js +35 -0
  31. package/dist/entry/server.js.map +1 -0
  32. package/dist/flags.js +11 -0
  33. package/dist/generate-cartridge.js +620 -0
  34. package/dist/hooks/init.js +47 -0
  35. package/dist/index.d.ts +9 -29
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +413 -621
  38. package/dist/index.js.map +1 -1
  39. package/dist/local-dev-setup.js +176 -0
  40. package/dist/logger.js +105 -0
  41. package/dist/manage-extensions.js +329 -0
  42. package/dist/mrt/ssr.mjs +3 -3
  43. package/dist/mrt/ssr.mjs.map +1 -1
  44. package/dist/mrt/streamingHandler.mjs +4 -4
  45. package/dist/mrt/streamingHandler.mjs.map +1 -1
  46. package/dist/server.js +425 -0
  47. package/dist/utils.js +126 -0
  48. package/package.json +44 -9
  49. package/dist/cli.js +0 -3393
  50. /package/{LICENSE.txt → LICENSE} +0 -0
package/dist/cli.js DELETED
@@ -1,3393 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import fs from "fs-extra";
4
- import path, { dirname, extname } from "path";
5
- import os from "os";
6
- import archiver from "archiver";
7
- import { Minimatch, minimatch } from "minimatch";
8
- import { execSync } from "child_process";
9
- import dotenv from "dotenv";
10
- import chalk from "chalk";
11
- import { createRequire } from "module";
12
- import { URL as URL$1, fileURLToPath, pathToFileURL } from "url";
13
- import zlib from "zlib";
14
- import { promisify } from "util";
15
- import { createServer } from "vite";
16
- import express from "express";
17
- import { createRequestHandler } from "@react-router/express";
18
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
19
- import { basename, extname as extname$1, join, resolve } from "node:path";
20
- import { pathToFileURL as pathToFileURL$1 } from "node:url";
21
- import { createProxyMiddleware } from "http-proxy-middleware";
22
- import compression from "compression";
23
- import zlib$1 from "node:zlib";
24
- import morgan from "morgan";
25
- import fs$1 from "fs";
26
- import Handlebars from "handlebars";
27
- import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
28
- import { execSync as execSync$1 } from "node:child_process";
29
- import { Node, Project } from "ts-morph";
30
- import { tmpdir } from "node:os";
31
- import { randomUUID } from "node:crypto";
32
- import { npmRunPathEnv } from "npm-run-path";
33
- import prompts from "prompts";
34
- import { z } from "zod";
35
-
36
- //#region package.json
37
- var version = "0.1.1";
38
-
39
- //#endregion
40
- //#region src/utils/logger.ts
41
- /**
42
- * Get the local network IPv4 address
43
- */
44
- function getNetworkAddress() {
45
- const interfaces = os.networkInterfaces();
46
- for (const name of Object.keys(interfaces)) {
47
- const iface = interfaces[name];
48
- if (!iface) continue;
49
- for (const alias of iface) if (alias.family === "IPv4" && !alias.internal) return alias.address;
50
- }
51
- }
52
- /**
53
- * Get the version of a package from the project's package.json
54
- */
55
- function getPackageVersion(packageName, projectDir) {
56
- try {
57
- const require = createRequire(import.meta.url);
58
- return require(require.resolve(`${packageName}/package.json`, { paths: [projectDir] })).version;
59
- } catch {
60
- return "unknown";
61
- }
62
- }
63
- /**
64
- * Logger utilities
65
- */
66
- const colors = {
67
- warn: "yellow",
68
- error: "red",
69
- success: "cyan",
70
- info: "green",
71
- debug: "gray"
72
- };
73
- const fancyLog = (level, msg) => {
74
- const colorFn = chalk[colors[level]];
75
- console.log(`${colorFn(level)}: ${msg}`);
76
- };
77
- const info = (msg) => fancyLog("info", msg);
78
- const success = (msg) => fancyLog("success", msg);
79
- const warn = (msg) => fancyLog("warn", msg);
80
- const error = (msg) => fancyLog("error", msg);
81
- const debug = (msg, data) => {
82
- if (process.env.DEBUG || process.env.NODE_ENV !== "production") {
83
- fancyLog("debug", msg);
84
- if (data) console.log(data);
85
- }
86
- };
87
- /**
88
- * Print the server information banner with URLs and versions
89
- */
90
- function printServerInfo(mode, port, startTime, projectDir) {
91
- const elapsed = Date.now() - startTime;
92
- const sfnextVersion = version;
93
- const reactVersion = getPackageVersion("react", projectDir);
94
- const reactRouterVersion = getPackageVersion("react-router", projectDir);
95
- const modeLabel = mode === "development" ? "Development Mode" : "Preview Mode";
96
- console.log();
97
- console.log(` ${chalk.cyan.bold("⚡ SFCC Storefront Next")} ${chalk.dim(`v${sfnextVersion}`)}`);
98
- console.log(` ${chalk.green.bold(modeLabel)}`);
99
- console.log();
100
- console.log(` ${chalk.dim("react")} ${chalk.green(`v${reactVersion}`)} ${chalk.dim("│")} ${chalk.dim("react-router")} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim("│")} ${chalk.green(`ready in ${elapsed}ms`)}`);
101
- console.log();
102
- }
103
- /**
104
- * Print server configuration details (proxy, static, etc.)
105
- */
106
- function printServerConfig(config) {
107
- const { port, enableProxy, enableStaticServing, enableCompression, proxyPath, proxyTarget, shortCode, organizationId, clientId, siteId } = config;
108
- console.log(` ${chalk.bold("Environment Configuration:")}`);
109
- if (enableProxy && proxyPath && proxyTarget && shortCode) {
110
- console.log(` ${chalk.green("✓")} ${chalk.bold("Proxy:")} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim("→")} ${chalk.cyan(proxyTarget)}`);
111
- console.log(` ${chalk.dim("Short Code: ")} ${chalk.dim(shortCode)}`);
112
- if (organizationId) console.log(` ${chalk.dim("Organization ID:")} ${chalk.dim(organizationId)}`);
113
- if (clientId) console.log(` ${chalk.dim("Client ID: ")} ${chalk.dim(clientId)}`);
114
- if (siteId) console.log(` ${chalk.dim("Site ID: ")} ${chalk.dim(siteId)}`);
115
- } else console.log(` ${chalk.gray("○")} ${chalk.bold("Proxy: ")} ${chalk.dim("disabled")}`);
116
- if (enableStaticServing) console.log(` ${chalk.green("✓")} ${chalk.bold("Static: ")} ${chalk.dim("enabled")}`);
117
- if (enableCompression) console.log(` ${chalk.green("✓")} ${chalk.bold("Compression: ")} ${chalk.dim("enabled")}`);
118
- const localUrl = `http://localhost:${port}`;
119
- const networkAddress = getNetworkAddress();
120
- const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : null;
121
- console.log();
122
- console.log(` ${chalk.green("➜")} ${chalk.bold("Local: ")} ${chalk.cyan(localUrl)}`);
123
- if (networkUrl) console.log(` ${chalk.green("➜")} ${chalk.bold("Network:")} ${chalk.cyan(networkUrl)}`);
124
- console.log();
125
- console.log(` ${chalk.dim("Press")} ${chalk.bold("Ctrl+C")} ${chalk.dim("to stop the server")}`);
126
- console.log();
127
- }
128
- /**
129
- * Print shutdown message
130
- */
131
- function printShutdownMessage() {
132
- console.log(`\n ${chalk.yellow("⚡")} ${chalk.dim("Server shutting down...")}\n`);
133
- }
134
-
135
- //#endregion
136
- //#region src/utils.ts
137
- const DEFAULT_CLOUD_ORIGIN = "https://cloud.mobify.com";
138
- const getDefaultBuildDir = (targetDir) => path.join(targetDir, "build");
139
- const NODE_ENV = process.env.NODE_ENV || "development";
140
- /**
141
- * Get credentials file path based on cloud origin
142
- */
143
- const getCredentialsFile = (cloudOrigin, credentialsFile) => {
144
- if (credentialsFile) return credentialsFile;
145
- const host = new URL(cloudOrigin).host;
146
- const suffix = host === "cloud.mobify.com" ? "" : `--${host}`;
147
- return path.join(os.homedir(), `.mobify${suffix}`);
148
- };
149
- /**
150
- * Read credentials from file
151
- */
152
- const readCredentials = async (filepath) => {
153
- try {
154
- const data = await fs.readJSON(filepath);
155
- return {
156
- username: data.username,
157
- api_key: data.api_key
158
- };
159
- } catch {
160
- throw new Error(`Credentials file "${filepath}" not found.\nVisit https://runtime.commercecloud.com/account/settings for steps on authorizing your computer to push bundles.`);
161
- }
162
- };
163
- /**
164
- * Get project package.json
165
- */
166
- const getProjectPkg = (projectDir) => {
167
- const packagePath = path.join(projectDir, "package.json");
168
- try {
169
- return fs.readJSONSync(packagePath);
170
- } catch {
171
- throw new Error(`Could not read project package at "${packagePath}"`);
172
- }
173
- };
174
- /**
175
- * Load .env file from project directory
176
- */
177
- const loadEnvFile = (projectDir) => {
178
- const envPath = path.join(projectDir, ".env");
179
- if (fs.existsSync(envPath)) dotenv.config({ path: envPath });
180
- else warn("No .env file found");
181
- };
182
- /**
183
- * Get MRT configuration with priority logic: .env -> package.json -> defaults
184
- */
185
- const getMrtConfig = (projectDir) => {
186
- loadEnvFile(projectDir);
187
- const pkg = getProjectPkg(projectDir);
188
- const defaultMrtProject = process.env.MRT_PROJECT ?? pkg.name;
189
- if (!defaultMrtProject || defaultMrtProject.trim() === "") throw new Error("Project name couldn't be determined. Do one of these options:\n 1. Set MRT_PROJECT in your .env file, or\n 2. Ensure package.json has a valid \"name\" field.");
190
- const defaultMrtTarget = process.env.MRT_TARGET ?? void 0;
191
- debug("MRT configuration resolved", {
192
- projectDir,
193
- envMrtProject: process.env.MRT_PROJECT,
194
- envMrtTarget: process.env.MRT_TARGET,
195
- packageName: pkg.name,
196
- resolvedProject: defaultMrtProject,
197
- resolvedTarget: defaultMrtTarget
198
- });
199
- return {
200
- defaultMrtProject,
201
- defaultMrtTarget
202
- };
203
- };
204
- /**
205
- * Get project dependency tree (simplified version)
206
- */
207
- const getProjectDependencyTree = (projectDir) => {
208
- try {
209
- const tmpFile = path.join(os.tmpdir(), `npm-ls-${Date.now()}.json`);
210
- execSync(`npm ls --all --json > ${tmpFile}`, {
211
- stdio: "ignore",
212
- cwd: projectDir
213
- });
214
- const data = fs.readJSONSync(tmpFile);
215
- fs.unlinkSync(tmpFile);
216
- return data;
217
- } catch {
218
- return null;
219
- }
220
- };
221
- /**
222
- * Get PWA Kit dependencies from dependency tree
223
- */
224
- const getPwaKitDependencies = (dependencyTree) => {
225
- if (!dependencyTree) return {};
226
- const pwaKitDependencies = ["@salesforce/storefront-next-dev"];
227
- const result = {};
228
- const searchDeps = (tree) => {
229
- if (tree.dependencies) for (const [name, dep] of Object.entries(tree.dependencies)) {
230
- if (pwaKitDependencies.includes(name)) result[name] = dep.version || "unknown";
231
- if (dep.dependencies) searchDeps({ dependencies: dep.dependencies });
232
- }
233
- };
234
- searchDeps(dependencyTree);
235
- return result;
236
- };
237
- /**
238
- * Get default commit message from git
239
- */
240
- const getDefaultMessage = (projectDir) => {
241
- try {
242
- return `${execSync("git rev-parse --abbrev-ref HEAD", {
243
- encoding: "utf8",
244
- cwd: projectDir
245
- }).trim()}: ${execSync("git rev-parse --short HEAD", {
246
- encoding: "utf8",
247
- cwd: projectDir
248
- }).trim()}`;
249
- } catch {
250
- debug("Using default bundle message as no message was provided and not in a Git repo.");
251
- return "PWA Kit Bundle";
252
- }
253
- };
254
- /**
255
- * Given a project directory and a record of config overrides, generate a new .env file with the overrides based on the .env.default file.
256
- * @param projectDir
257
- * @param configOverrides
258
- */
259
- const generateEnvFile = (projectDir, configOverrides) => {
260
- const envDefaultPath = path.join(projectDir, ".env.default");
261
- const envPath = path.join(projectDir, ".env");
262
- if (!fs.existsSync(envDefaultPath)) {
263
- console.warn(`${envDefaultPath} not found`);
264
- return;
265
- }
266
- const envOutputLines = fs.readFileSync(envDefaultPath, "utf8").split("\n").map((line) => {
267
- if (!line || line.trim().startsWith("#")) return line;
268
- const eqIndex = line.indexOf("=");
269
- if (eqIndex === -1) return line;
270
- const key = line.slice(0, eqIndex);
271
- const originalValue = line.slice(eqIndex + 1);
272
- return `${key}=${(Object.prototype.hasOwnProperty.call(configOverrides, key) ? configOverrides[key] : void 0) ?? originalValue}`;
273
- });
274
- fs.writeFileSync(envPath, envOutputLines.join("\n"));
275
- };
276
-
277
- //#endregion
278
- //#region src/bundle.ts
279
- /**
280
- * Create a bundle from the build directory
281
- */
282
- const createBundle = async (options) => {
283
- const { message, ssr_parameters, ssr_only, ssr_shared, buildDirectory, projectDirectory, projectSlug } = options;
284
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "storefront-next-dev-push-"));
285
- const destination = path.join(tmpDir, "build.tar");
286
- const filesInArchive = [];
287
- if (!ssr_only || ssr_only.length === 0 || !ssr_shared || ssr_shared.length === 0) throw new Error("no ssrOnly or ssrShared files are defined");
288
- return new Promise((resolve$1, reject) => {
289
- const output = fs.createWriteStream(destination);
290
- const archive = archiver("tar");
291
- archive.pipe(output);
292
- const newRoot = path.join(projectSlug, "bld", "");
293
- const storybookExclusionMatchers = [
294
- "**/*.stories.tsx",
295
- "**/*.stories.ts",
296
- "**/*-snapshot.tsx",
297
- ".storybook/**/*",
298
- "storybook-static/**/*",
299
- "**/__mocks__/**/*",
300
- "**/__snapshots__/**/*"
301
- ].map((pattern) => new Minimatch(pattern, { nocomment: true }));
302
- archive.directory(buildDirectory, "", (entry) => {
303
- if (entry.name && storybookExclusionMatchers.some((matcher) => matcher.match(entry.name))) return false;
304
- if (entry.stats?.isFile() && entry.name) filesInArchive.push(entry.name);
305
- entry.prefix = newRoot;
306
- return entry;
307
- });
308
- archive.on("error", reject);
309
- output.on("finish", () => {
310
- try {
311
- const { dependencies = {}, devDependencies = {} } = getProjectPkg(projectDirectory);
312
- const dependencyTree = getProjectDependencyTree(projectDirectory);
313
- const pwaKitDeps = dependencyTree ? getPwaKitDependencies(dependencyTree) : {};
314
- const bundle_metadata = { dependencies: {
315
- ...dependencies,
316
- ...devDependencies,
317
- ...pwaKitDeps
318
- } };
319
- const data = fs.readFileSync(destination);
320
- const encoding = "base64";
321
- fs.rmSync(tmpDir, { recursive: true });
322
- const createGlobMatcher = (patterns) => {
323
- const allPatterns = patterns.map((pattern) => new Minimatch(pattern, { nocomment: true })).filter((pattern) => !pattern.empty);
324
- const positivePatterns = allPatterns.filter((pattern) => !pattern.negate);
325
- const negativePatterns = allPatterns.filter((pattern) => pattern.negate);
326
- return (filePath) => {
327
- if (filePath) {
328
- const positive = positivePatterns.some((pattern) => pattern.match(filePath));
329
- const negative = negativePatterns.some((pattern) => !pattern.match(filePath));
330
- return positive && !negative;
331
- }
332
- return false;
333
- };
334
- };
335
- resolve$1({
336
- message,
337
- encoding,
338
- data: data.toString(encoding),
339
- ssr_parameters,
340
- ssr_only: filesInArchive.filter(createGlobMatcher(ssr_only)),
341
- ssr_shared: filesInArchive.filter(createGlobMatcher(ssr_shared)),
342
- bundle_metadata
343
- });
344
- } catch (err) {
345
- reject(err);
346
- }
347
- });
348
- archive.finalize().catch(reject);
349
- });
350
- };
351
-
352
- //#endregion
353
- //#region src/cloud-api.ts
354
- var CloudAPIClient = class {
355
- credentials;
356
- origin;
357
- constructor({ credentials, origin }) {
358
- this.credentials = credentials;
359
- this.origin = origin;
360
- }
361
- getAuthHeader() {
362
- const { username, api_key } = this.credentials;
363
- return { Authorization: `Basic ${Buffer.from(`${username}:${api_key}`, "binary").toString("base64")}` };
364
- }
365
- getHeaders() {
366
- return {
367
- "User-Agent": `storefront-next-dev@${version}`,
368
- ...this.getAuthHeader()
369
- };
370
- }
371
- /**
372
- * Push bundle to Managed Runtime
373
- */
374
- async push(bundle, projectSlug, target) {
375
- const base = `api/projects/${projectSlug}/builds/`;
376
- const pathname = target ? `${base}${target}/` : base;
377
- const url = new URL$1(this.origin);
378
- url.pathname = pathname;
379
- const body = Buffer.from(JSON.stringify(bundle));
380
- const headers = {
381
- ...this.getHeaders(),
382
- "Content-Length": body.length.toString()
383
- };
384
- const res = await fetch(url.toString(), {
385
- body,
386
- method: "POST",
387
- headers
388
- });
389
- if (res.status >= 400) {
390
- const bodyText = await res.text();
391
- let errorData;
392
- try {
393
- errorData = JSON.parse(bodyText);
394
- } catch {
395
- errorData = { message: bodyText };
396
- }
397
- throw new Error(`HTTP ${res.status}: ${errorData.message || bodyText}\nFor more information visit https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/pushing-and-deploying-bundles.html`);
398
- }
399
- return await res.json();
400
- }
401
- /**
402
- * Wait for deployment to complete
403
- */
404
- async waitForDeploy(project, environment) {
405
- return new Promise((resolve$1, reject) => {
406
- const delay = 3e4;
407
- const check = async () => {
408
- const url = new URL$1(`/api/projects/${project}/target/${environment}`, this.origin);
409
- const res = await fetch(url, { headers: this.getHeaders() });
410
- if (!res.ok) {
411
- const text = await res.text();
412
- let json;
413
- try {
414
- if (text) json = JSON.parse(text);
415
- } catch {}
416
- const message = json?.detail ?? text;
417
- const detail = message ? `: ${message}` : "";
418
- throw new Error(`${res.status} ${res.statusText}${detail}`);
419
- }
420
- const data = await res.json();
421
- if (typeof data.state !== "string") return reject(/* @__PURE__ */ new Error("An unknown state occurred when polling the deployment."));
422
- switch (data.state) {
423
- case "CREATE_IN_PROGRESS":
424
- case "PUBLISH_IN_PROGRESS":
425
- setTimeout(() => {
426
- check().catch(reject);
427
- }, delay);
428
- return;
429
- case "CREATE_FAILED":
430
- case "PUBLISH_FAILED": return reject(/* @__PURE__ */ new Error("Deployment failed."));
431
- case "ACTIVE": return resolve$1();
432
- default: return reject(/* @__PURE__ */ new Error(`Unknown deployment state "${data.state}".`));
433
- }
434
- };
435
- setTimeout(() => {
436
- check().catch(reject);
437
- }, delay);
438
- });
439
- }
440
- };
441
-
442
- //#endregion
443
- //#region src/mrt/utils.ts
444
- const MRT_BUNDLE_TYPE_SSR = "ssr";
445
- const MRT_STREAMING_ENTRY_FILE = "streamingHandler";
446
- /**
447
- * Gets the MRT entry file for the given mode
448
- * @param mode - The mode to get the MRT entry file for
449
- * @returns The MRT entry file for the given mode
450
- */
451
- const getMrtEntryFile = (mode) => {
452
- return process.env.MRT_BUNDLE_TYPE !== MRT_BUNDLE_TYPE_SSR && mode === "production" ? MRT_STREAMING_ENTRY_FILE : MRT_BUNDLE_TYPE_SSR;
453
- };
454
-
455
- //#endregion
456
- //#region src/config.ts
457
- const CARTRIDGES_BASE_DIR = "cartridges";
458
- const SFNEXT_BASE_CARTRIDGE_NAME = "app_storefrontnext_base";
459
- const SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR = `${SFNEXT_BASE_CARTRIDGE_NAME}/cartridge/experience`;
460
- /**
461
- * When enabled, automatically generates and deploys cartridge metadata before an MRT push.
462
- * This is useful for keeping Page Designer metadata in sync with component changes.
463
- *
464
- * When enabled:
465
- * 1. Generates cartridge metadata from decorated components
466
- * 2. Deploys the cartridge to Commerce Cloud (requires dw.json configuration)
467
- * 3. Proceeds with the MRT push
468
- *
469
- * To enable: Set this to `true` in your local config.ts
470
- * Default: false (manual cartridge generation/deployment via `sfnext generate-cartridge` and `sfnext deploy-cartridge`)
471
- */
472
- const GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH = false;
473
- /**
474
- * Build MRT SSR configuration for bundle deployment
475
- *
476
- * Defines which files should be:
477
- * - Server-only (ssrOnly): Deployed only to Lambda functions
478
- * - Shared (ssrShared): Deployed to both Lambda and CDN
479
- *
480
- * @param buildDirectory - Path to the build output directory
481
- * @param projectDirectory - Path to the project root (reserved for future use)
482
- * @returns MRT SSR configuration with glob patterns
483
- */
484
- const buildMrtConfig = (_buildDirectory, _projectDirectory) => {
485
- const ssrEntryPoint = getMrtEntryFile("production");
486
- return {
487
- ssrOnly: [
488
- "server/**/*",
489
- "loader.js",
490
- `${ssrEntryPoint}.{js,mjs,cjs}`,
491
- `${ssrEntryPoint}.{js,mjs,cjs}.map`,
492
- "!static/**/*",
493
- "sfnext-server-*.mjs",
494
- "!**/*.stories.tsx",
495
- "!**/*.stories.ts",
496
- "!**/*-snapshot.tsx",
497
- "!.storybook/**/*",
498
- "!storybook-static/**/*",
499
- "!**/__mocks__/**/*",
500
- "!**/__snapshots__/**/*"
501
- ],
502
- ssrShared: [
503
- "client/**/*",
504
- "static/**/*",
505
- "**/*.css",
506
- "**/*.png",
507
- "**/*.jpg",
508
- "**/*.jpeg",
509
- "**/*.gif",
510
- "**/*.svg",
511
- "**/*.ico",
512
- "**/*.woff",
513
- "**/*.woff2",
514
- "**/*.ttf",
515
- "**/*.eot",
516
- "!**/*.stories.tsx",
517
- "!**/*.stories.ts",
518
- "!**/*-snapshot.tsx",
519
- "!.storybook/**/*",
520
- "!storybook-static/**/*",
521
- "!**/__mocks__/**/*",
522
- "!**/__snapshots__/**/*"
523
- ],
524
- ssrParameters: { ssrFunctionNodeVersion: "24.x" }
525
- };
526
- };
527
-
528
- //#endregion
529
- //#region src/commands/push.ts
530
- /**
531
- * Main function to push bundle to Managed Runtime
532
- */
533
- async function push(options) {
534
- const mrtConfig = getMrtConfig(options.projectDirectory);
535
- const resolvedTarget = options.target ?? mrtConfig.defaultMrtTarget;
536
- if (options.wait && !resolvedTarget) throw new Error("You must provide a target to deploy to when using --wait (via --target flag or .env MRT_TARGET)");
537
- if (options.user && !options.key || !options.user && options.key) throw new Error("You must provide both --user and --key together, or neither");
538
- if (!fs.existsSync(options.projectDirectory)) throw new Error(`Project directory "${options.projectDirectory}" does not exist!`);
539
- const projectSlug = options.projectSlug ?? mrtConfig.defaultMrtProject;
540
- if (!projectSlug || projectSlug.trim() === "") throw new Error("Project slug could not be determined from CLI, .env, or package.json");
541
- const target = resolvedTarget;
542
- const buildDirectory = options.buildDirectory ?? getDefaultBuildDir(options.projectDirectory);
543
- if (!fs.existsSync(buildDirectory)) throw new Error(`Build directory "${buildDirectory}" does not exist!`);
544
- try {
545
- if (target) process.env.DEPLOY_TARGET = target;
546
- let credentials;
547
- if (options.user && options.key) credentials = {
548
- username: options.user,
549
- api_key: options.key
550
- };
551
- else credentials = await readCredentials(getCredentialsFile(options.cloudOrigin ?? DEFAULT_CLOUD_ORIGIN, options.credentialsFile));
552
- const config = buildMrtConfig(buildDirectory, options.projectDirectory);
553
- const message = options.message ?? getDefaultMessage(options.projectDirectory);
554
- info(`Creating bundle for project: ${projectSlug}`);
555
- if (options.projectSlug) debug("Using project slug from CLI argument");
556
- else if (process.env.MRT_PROJECT) debug("Using project slug from .env MRT_PROJECT");
557
- else debug("Using project slug from package.json name");
558
- if (target) {
559
- info(`Target environment: ${target}`);
560
- if (options.target) debug("Using target from CLI argument");
561
- else debug("Using target from .env");
562
- }
563
- debug("SSR shared files", config.ssrShared);
564
- debug("SSR only files", config.ssrOnly);
565
- const bundle = await createBundle({
566
- message,
567
- ssr_parameters: config.ssrParameters,
568
- ssr_only: config.ssrOnly,
569
- ssr_shared: config.ssrShared,
570
- buildDirectory,
571
- projectDirectory: options.projectDirectory,
572
- projectSlug
573
- });
574
- const client = new CloudAPIClient({
575
- credentials,
576
- origin: options.cloudOrigin ?? DEFAULT_CLOUD_ORIGIN
577
- });
578
- info(`Beginning upload to ${options.cloudOrigin ?? DEFAULT_CLOUD_ORIGIN}`);
579
- const data = await client.push(bundle, projectSlug, target);
580
- debug("API response", data);
581
- (data.warnings || []).forEach(warn);
582
- if (options.wait && target) {
583
- success("Bundle uploaded - waiting for deployment to complete");
584
- await client.waitForDeploy(projectSlug, target);
585
- success("Deployment complete!");
586
- } else success("Bundle uploaded successfully!");
587
- if (data.url) info(`Bundle URL: ${data.url}`);
588
- } catch (err) {
589
- error(err.message || err?.toString() || "Unknown error");
590
- throw err;
591
- }
592
- }
593
-
594
- //#endregion
595
- //#region src/commands/create-bundle.ts
596
- const gzip = promisify(zlib.gzip);
597
- /**
598
- * Create a bundle and save it to disk without pushing to Managed Runtime
599
- */
600
- async function createBundleCommand(options) {
601
- if (!fs.existsSync(options.projectDirectory)) throw new Error(`Project directory "${options.projectDirectory}" does not exist!`);
602
- const mrtConfig = getMrtConfig(options.projectDirectory);
603
- const projectSlug = options.projectSlug ?? mrtConfig.defaultMrtProject;
604
- if (!projectSlug || projectSlug.trim() === "") throw new Error("Project slug could not be determined from CLI, .env, or package.json");
605
- const buildDirectory = options.buildDirectory ?? getDefaultBuildDir(options.projectDirectory);
606
- if (!fs.existsSync(buildDirectory)) throw new Error(`Build directory "${buildDirectory}" does not exist!`);
607
- const outputDirectory = options.outputDirectory ?? path.join(options.projectDirectory, ".bundle");
608
- await fs.ensureDir(outputDirectory);
609
- const message = options.message ?? getDefaultMessage(options.projectDirectory);
610
- const config = buildMrtConfig(buildDirectory, options.projectDirectory);
611
- info(`Creating bundle for project: ${projectSlug}`);
612
- info(`Build directory: ${buildDirectory}`);
613
- info(`Output directory: ${outputDirectory}`);
614
- const bundle = await createBundle({
615
- message,
616
- ssr_parameters: config.ssrParameters,
617
- ssr_only: config.ssrOnly,
618
- ssr_shared: config.ssrShared,
619
- buildDirectory,
620
- projectDirectory: options.projectDirectory,
621
- projectSlug
622
- });
623
- const bundleTgzPath = path.join(outputDirectory, "bundle.tgz");
624
- const bundleJsonPath = path.join(outputDirectory, "bundle.json");
625
- const bundleData = Buffer.from(bundle.data, "base64");
626
- const compressedData = await gzip(bundleData);
627
- await fs.writeFile(bundleTgzPath, compressedData);
628
- const bundleMetadata = {
629
- message: bundle.message,
630
- encoding: bundle.encoding,
631
- ssr_parameters: bundle.ssr_parameters,
632
- ssr_only: bundle.ssr_only,
633
- ssr_shared: bundle.ssr_shared,
634
- bundle_metadata: bundle.bundle_metadata,
635
- data_size: bundleData.length
636
- };
637
- await fs.writeJson(bundleJsonPath, bundleMetadata, { spaces: 2 });
638
- success(`Bundle created successfully!`);
639
- info(`Bundle tgz file: ${bundleTgzPath}`);
640
- info(`Bundle metadata: ${bundleJsonPath}`);
641
- info(`Uncompressed size: ${(bundleData.length / 1024 / 1024).toFixed(2)} MB`);
642
- info(`Compressed size: ${(compressedData.length / 1024 / 1024).toFixed(2)} MB`);
643
- }
644
-
645
- //#endregion
646
- //#region src/server/ts-import.ts
647
- /**
648
- * Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
649
- *
650
- * @param tsconfigPath - Path to tsconfig.json
651
- * @param projectDirectory - Project root directory for resolving relative paths
652
- * @returns Record of alias mappings for jiti
653
- *
654
- * @example
655
- * // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
656
- * // Returns: { "@/": "/absolute/path/to/src/" }
657
- */
658
- function parseTsconfigPaths(tsconfigPath, projectDirectory) {
659
- const alias = {};
660
- if (!existsSync(tsconfigPath)) return alias;
661
- try {
662
- const tsconfigContent = readFileSync(tsconfigPath, "utf-8");
663
- const tsconfig = JSON.parse(tsconfigContent);
664
- const paths = tsconfig.compilerOptions?.paths;
665
- const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
666
- if (paths) {
667
- for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
668
- const aliasKey = key.replace(/\/\*$/, "/");
669
- alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
670
- }
671
- }
672
- } catch {}
673
- return alias;
674
- }
675
- /**
676
- * Import a TypeScript file using jiti with proper path alias resolution.
677
- * This is a cross-platform alternative to tsx that works on Windows.
678
- *
679
- * @param filePath - Absolute path to the TypeScript file to import
680
- * @param options - Import options including project directory
681
- * @returns The imported module
682
- */
683
- async function importTypescript(filePath, options) {
684
- const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
685
- const { createJiti } = await import("jiti");
686
- const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
687
- return createJiti(import.meta.url, {
688
- fsCache: false,
689
- interopDefault: true,
690
- alias
691
- }).import(filePath);
692
- }
693
-
694
- //#endregion
695
- //#region src/server/config.ts
696
- /**
697
- * This is a temporary function before we move the config implementation from
698
- * template-retail-rsc-app to the SDK.
699
- *
700
- * @ TODO: Remove this function after we move the config implementation from
701
- * template-retail-rsc-app to the SDK.
702
- *
703
- */
704
- function loadConfigFromEnv() {
705
- const shortCode = process.env.PUBLIC__app__commerce__api__shortCode;
706
- const organizationId = process.env.PUBLIC__app__commerce__api__organizationId;
707
- const clientId = process.env.PUBLIC__app__commerce__api__clientId;
708
- const siteId = process.env.PUBLIC__app__commerce__api__siteId;
709
- const proxy = process.env.PUBLIC__app__commerce__api__proxy || "/mobify/proxy/api";
710
- if (!shortCode) throw new Error("Missing PUBLIC__app__commerce__api__shortCode environment variable.\nPlease set it in your .env file or environment.");
711
- if (!organizationId) throw new Error("Missing PUBLIC__app__commerce__api__organizationId environment variable.\nPlease set it in your .env file or environment.");
712
- if (!clientId) throw new Error("Missing PUBLIC__app__commerce__api__clientId environment variable.\nPlease set it in your .env file or environment.");
713
- if (!siteId) throw new Error("Missing PUBLIC__app__commerce__api__siteId environment variable.\nPlease set it in your .env file or environment.");
714
- return { commerce: { api: {
715
- shortCode,
716
- organizationId,
717
- clientId,
718
- siteId,
719
- proxy
720
- } } };
721
- }
722
- /**
723
- * Load storefront-next project configuration from config.server.ts.
724
- * Requires projectDirectory to be provided.
725
- *
726
- * @param projectDirectory - Project directory to load config.server.ts from
727
- * @throws Error if config.server.ts is not found or invalid
728
- */
729
- async function loadProjectConfig(projectDirectory) {
730
- const configPath = resolve(projectDirectory, "config.server.ts");
731
- const tsconfigPath = resolve(projectDirectory, "tsconfig.json");
732
- if (!existsSync(configPath)) throw new Error(`config.server.ts not found at ${configPath}.\nPlease ensure config.server.ts exists in your project root.`);
733
- const config = (await importTypescript(configPath, {
734
- projectDirectory,
735
- tsconfigPath
736
- })).default;
737
- if (!config?.app?.commerce?.api) throw new Error("Invalid config.server.ts: missing app.commerce.api configuration.\nPlease ensure your config.server.ts has the commerce API configuration.");
738
- const api = config.app.commerce.api;
739
- if (!api.shortCode) throw new Error("Missing shortCode in config.server.ts commerce.api configuration");
740
- if (!api.organizationId) throw new Error("Missing organizationId in config.server.ts commerce.api configuration");
741
- if (!api.clientId) throw new Error("Missing clientId in config.server.ts commerce.api configuration");
742
- if (!api.siteId) throw new Error("Missing siteId in config.server.ts commerce.api configuration");
743
- return { commerce: { api: {
744
- shortCode: api.shortCode,
745
- organizationId: api.organizationId,
746
- clientId: api.clientId,
747
- siteId: api.siteId,
748
- proxy: api.proxy || "/mobify/proxy/api"
749
- } } };
750
- }
751
-
752
- //#endregion
753
- //#region src/utils/paths.ts
754
- /**
755
- * Copyright 2026 Salesforce, Inc.
756
- *
757
- * Licensed under the Apache License, Version 2.0 (the "License");
758
- * you may not use this file except in compliance with the License.
759
- * You may obtain a copy of the License at
760
- *
761
- * http://www.apache.org/licenses/LICENSE-2.0
762
- *
763
- * Unless required by applicable law or agreed to in writing, software
764
- * distributed under the License is distributed on an "AS IS" BASIS,
765
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
766
- * See the License for the specific language governing permissions and
767
- * limitations under the License.
768
- */
769
- /**
770
- * Get the Commerce Cloud API URL from a short code
771
- */
772
- function getCommerceCloudApiUrl(shortCode) {
773
- return `https://${shortCode}.api.commercecloud.salesforce.com`;
774
- }
775
- /**
776
- * Get the bundle path for static assets
777
- */
778
- function getBundlePath(bundleId) {
779
- return `/mobify/bundle/${bundleId}/client/`;
780
- }
781
-
782
- //#endregion
783
- //#region src/server/middleware/proxy.ts
784
- /**
785
- * Create proxy middleware for Commerce Cloud API
786
- * Proxies requests from /mobify/proxy/api to the Commerce Cloud API
787
- */
788
- function createCommerceProxyMiddleware(config) {
789
- return createProxyMiddleware({
790
- target: getCommerceCloudApiUrl(config.commerce.api.shortCode),
791
- changeOrigin: true
792
- });
793
- }
794
-
795
- //#endregion
796
- //#region src/server/middleware/static.ts
797
- /**
798
- * Create static file serving middleware for client assets
799
- * Serves files from build/client at /mobify/bundle/{BUNDLE_ID}/client/
800
- */
801
- function createStaticMiddleware(bundleId, projectDirectory) {
802
- const bundlePath = getBundlePath(bundleId);
803
- const clientBuildDir = path.join(projectDirectory, "build", "client");
804
- info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
805
- return express.static(clientBuildDir, { setHeaders: (res) => {
806
- res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
807
- res.setHeader("x-local-static-cache-control", "1");
808
- } });
809
- }
810
-
811
- //#endregion
812
- //#region src/server/middleware/compression.ts
813
- /**
814
- * Parse and validate COMPRESSION_LEVEL environment variable
815
- * @returns Valid compression level (0-9) or default compression level
816
- */
817
- function getCompressionLevel() {
818
- const raw = process.env.COMPRESSION_LEVEL;
819
- const DEFAULT = zlib$1.constants.Z_DEFAULT_COMPRESSION;
820
- if (raw == null || raw.trim() === "") return DEFAULT;
821
- const level = Number(raw);
822
- if (!(Number.isInteger(level) && level >= 0 && level <= 9)) {
823
- warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
824
- return DEFAULT;
825
- }
826
- return level;
827
- }
828
- /**
829
- * Create compression middleware for gzip/brotli compression
830
- * Used in preview mode to optimize response sizes
831
- */
832
- function createCompressionMiddleware() {
833
- return compression({
834
- filter: (req, res) => {
835
- if (req.headers["x-no-compression"]) return false;
836
- return compression.filter(req, res);
837
- },
838
- level: getCompressionLevel()
839
- });
840
- }
841
-
842
- //#endregion
843
- //#region src/server/middleware/logging.ts
844
- /**
845
- * Patterns for URLs to skip logging (static assets and Vite internals)
846
- */
847
- const SKIP_PATTERNS = [
848
- "/@vite/**",
849
- "/@id/**",
850
- "/@fs/**",
851
- "/@react-router/**",
852
- "/src/**",
853
- "/node_modules/**",
854
- "**/*.js",
855
- "**/*.css",
856
- "**/*.ts",
857
- "**/*.tsx",
858
- "**/*.js.map",
859
- "**/*.css.map"
860
- ];
861
- /**
862
- * Create request logging middleware
863
- * Used in dev and preview modes for request visibility
864
- */
865
- function createLoggingMiddleware() {
866
- morgan.token("status-colored", (req, res) => {
867
- const status = res.statusCode;
868
- let color = chalk.green;
869
- if (status >= 500) color = chalk.red;
870
- else if (status >= 400) color = chalk.yellow;
871
- else if (status >= 300) color = chalk.cyan;
872
- return color(String(status));
873
- });
874
- morgan.token("method-colored", (req) => {
875
- const method = req.method;
876
- const colors$1 = {
877
- GET: chalk.green,
878
- POST: chalk.blue,
879
- PUT: chalk.yellow,
880
- DELETE: chalk.red,
881
- PATCH: chalk.magenta
882
- };
883
- return (method && colors$1[method] || chalk.white)(method);
884
- });
885
- return morgan((tokens, req, res) => {
886
- return [
887
- chalk.gray("["),
888
- tokens["method-colored"](req, res),
889
- chalk.gray("]"),
890
- tokens.url(req, res),
891
- "-",
892
- tokens["status-colored"](req, res),
893
- chalk.gray(`(${tokens["response-time"](req, res)}ms)`)
894
- ].join(" ");
895
- }, { skip: (req) => {
896
- return SKIP_PATTERNS.some((pattern) => minimatch(req.url, pattern, { dot: true }));
897
- } });
898
- }
899
-
900
- //#endregion
901
- //#region src/server/middleware/host-header.ts
902
- /**
903
- * Normalizes the X-Forwarded-Host header to support React Router's CSRF validation features.
904
- *
905
- * NOTE: This middleware performs header manipulation as a temporary, internal
906
- * solution for MRT/Lambda environments. It may be updated or removed if React Router
907
- * introduces a first-class configuration for validating against forwarded headers.
908
- *
909
- * React Router v7.12+ uses the X-Forwarded-Host header (preferring it over Host)
910
- * to validate request origins for security. In Managed Runtime (MRT) with a vanity
911
- * domain, the eCDN automatically sets the X-Forwarded-Host to the vanity domain.
912
- * React Router handles cases where this header contains multiple comma-separated
913
- * values by prioritizing the first entry.
914
- *
915
- * This middleware ensures that X-Forwarded-Host is always present by falling back
916
- * to a configured public domain if the header is missing (e.g., local development).
917
- * By only modifying X-Forwarded-Host, we provide a consistent environment for
918
- * React Router's security checks without modifying the internal 'Host' header,
919
- * which is required for environment-specific routing logic (e.g., Hybrid Proxy).
920
- *
921
- * Priority order:
922
- * 1. X-Forwarded-Host: Automatically set by eCDN for vanity domains.
923
- * 2. EXTERNAL_DOMAIN_NAME: Fallback environment variable for the public domain
924
- * used when no forwarded headers are present (e.g., local development).
925
- */
926
- function createHostHeaderMiddleware() {
927
- return (req, _res, next) => {
928
- if (!req.get("x-forwarded-host") && process.env.EXTERNAL_DOMAIN_NAME) req.headers["x-forwarded-host"] = process.env.EXTERNAL_DOMAIN_NAME;
929
- next();
930
- };
931
- }
932
-
933
- //#endregion
934
- //#region src/server/utils.ts
935
- /**
936
- * Patch React Router build to rewrite asset URLs with the correct bundle path
937
- * This is needed because the build output uses /assets/ but we preview at /mobify/bundle/{BUNDLE_ID}/client/assets/
938
- */
939
- function patchReactRouterBuild(build, bundleId) {
940
- const bundlePath = getBundlePath(bundleId);
941
- const patchedAssetsJson = JSON.stringify(build.assets).replace(/"\/assets\//g, `"${bundlePath}assets/`);
942
- const newAssets = JSON.parse(patchedAssetsJson);
943
- return Object.assign({}, build, {
944
- publicPath: bundlePath,
945
- assets: newAssets
946
- });
947
- }
948
-
949
- //#endregion
950
- //#region src/server/modes.ts
951
- /**
952
- * Default feature configuration for each server mode
953
- */
954
- const ServerModeFeatureMap = {
955
- development: {
956
- enableProxy: true,
957
- enableStaticServing: false,
958
- enableCompression: false,
959
- enableLogging: true,
960
- enableAssetUrlPatching: false
961
- },
962
- preview: {
963
- enableProxy: true,
964
- enableStaticServing: true,
965
- enableCompression: true,
966
- enableLogging: true,
967
- enableAssetUrlPatching: true
968
- },
969
- production: {
970
- enableProxy: false,
971
- enableStaticServing: false,
972
- enableCompression: true,
973
- enableLogging: true,
974
- enableAssetUrlPatching: true
975
- }
976
- };
977
-
978
- //#endregion
979
- //#region src/server/index.ts
980
- /** Relative path to the middleware registry TypeScript source (development). Must match appDirectory + server dir + filename used by buildMiddlewareRegistry plugin. */
981
- const RELATIVE_MIDDLEWARE_REGISTRY_SOURCE = "src/server/middleware-registry.ts";
982
- /** Extensions to try for the built middlewares module (ESM first, then CJS for backwards compatibility). */
983
- const MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS = [
984
- ".mjs",
985
- ".js",
986
- ".cjs"
987
- ];
988
- /** All paths to try when loading the built middlewares (base + extension). */
989
- const RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS = ["bld/server/middleware-registry", "build/server/middleware-registry"].flatMap((base) => MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS.map((ext) => `${base}${ext}`));
990
- /**
991
- * Create a unified Express server for development, preview, or production mode
992
- */
993
- async function createServer$1(options) {
994
- const { mode, projectDirectory = process.cwd(), config: providedConfig, vite, build, streaming = false, enableProxy = ServerModeFeatureMap[mode].enableProxy, enableStaticServing = ServerModeFeatureMap[mode].enableStaticServing, enableCompression = ServerModeFeatureMap[mode].enableCompression, enableLogging = ServerModeFeatureMap[mode].enableLogging, enableAssetUrlPatching = ServerModeFeatureMap[mode].enableAssetUrlPatching } = options;
995
- if (mode === "development" && !vite) throw new Error("Vite dev server instance is required for development mode");
996
- if ((mode === "preview" || mode === "production") && !build) throw new Error("React Router server build is required for preview/production mode");
997
- const config = providedConfig ?? loadConfigFromEnv();
998
- const bundleId = process.env.BUNDLE_ID ?? "local";
999
- const app = express();
1000
- app.disable("x-powered-by");
1001
- if (enableLogging) app.use(createLoggingMiddleware());
1002
- if (enableCompression && !streaming) app.use(createCompressionMiddleware());
1003
- if (enableStaticServing && build) {
1004
- const bundlePath = getBundlePath(bundleId);
1005
- app.use(bundlePath, createStaticMiddleware(bundleId, projectDirectory));
1006
- }
1007
- let registry = null;
1008
- if (mode === "development") {
1009
- const middlewareRegistryPath = resolve(projectDirectory, RELATIVE_MIDDLEWARE_REGISTRY_SOURCE);
1010
- if (existsSync(middlewareRegistryPath)) registry = await importTypescript(middlewareRegistryPath, { projectDirectory });
1011
- } else {
1012
- const possiblePaths = RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS.map((p) => resolve(projectDirectory, p));
1013
- let builtRegistryPath = null;
1014
- for (const path$1 of possiblePaths) if (existsSync(path$1)) {
1015
- builtRegistryPath = path$1;
1016
- break;
1017
- }
1018
- if (builtRegistryPath) registry = await import(pathToFileURL$1(builtRegistryPath).href);
1019
- }
1020
- if (registry?.customMiddlewares && Array.isArray(registry.customMiddlewares)) registry.customMiddlewares.forEach((entry) => {
1021
- app.use(entry.handler);
1022
- });
1023
- if (mode === "development" && vite) app.use(vite.middlewares);
1024
- if (enableProxy) app.use(config.commerce.api.proxy, createCommerceProxyMiddleware(config));
1025
- app.use(createHostHeaderMiddleware());
1026
- app.all("*", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
1027
- return app;
1028
- }
1029
- /**
1030
- * Create the SSR request handler based on mode
1031
- */
1032
- async function createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching) {
1033
- if (mode === "development" && vite) {
1034
- const { isRunnableDevEnvironment } = await import("vite");
1035
- return async (req, res, next) => {
1036
- try {
1037
- const ssrEnvironment = vite.environments.ssr;
1038
- if (!isRunnableDevEnvironment(ssrEnvironment)) {
1039
- next(/* @__PURE__ */ new Error("SSR environment is not runnable. Please ensure:\n 1. \"@salesforce/storefront-next-dev\" plugin is added to vite.config.ts\n 2. React Router config uses the Storefront Next preset"));
1040
- return;
1041
- }
1042
- await createRequestHandler({
1043
- build: await ssrEnvironment.runner.import("virtual:react-router/server-build"),
1044
- mode: process.env.NODE_ENV
1045
- })(req, res, next);
1046
- } catch (error$1) {
1047
- vite.ssrFixStacktrace(error$1);
1048
- next(error$1);
1049
- }
1050
- };
1051
- } else if (build) {
1052
- let patchedBuild = build;
1053
- if (enableAssetUrlPatching) patchedBuild = patchReactRouterBuild(build, bundleId);
1054
- return createRequestHandler({
1055
- build: patchedBuild,
1056
- mode: process.env.NODE_ENV
1057
- });
1058
- } else throw new Error("Invalid server configuration: no vite or build provided");
1059
- }
1060
-
1061
- //#endregion
1062
- //#region src/commands/dev.ts
1063
- /**
1064
- * Start the development server with Vite in middleware mode
1065
- */
1066
- async function dev(options = {}) {
1067
- const startTime = Date.now();
1068
- const projectDir = path.resolve(options.projectDirectory || process.cwd());
1069
- const port = options.port || 5173;
1070
- process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
1071
- process.env.EXTERNAL_DOMAIN_NAME = process.env.EXTERNAL_DOMAIN_NAME ?? `localhost:${port}`;
1072
- loadEnvFile(projectDir);
1073
- const config = await loadProjectConfig(projectDir);
1074
- const vite = await createServer({
1075
- root: projectDir,
1076
- server: { middlewareMode: true }
1077
- });
1078
- const server = (await createServer$1({
1079
- mode: "development",
1080
- projectDirectory: projectDir,
1081
- config,
1082
- port,
1083
- vite
1084
- })).listen(port, () => {
1085
- printServerInfo("development", port, startTime, projectDir);
1086
- printServerConfig({
1087
- mode: "development",
1088
- port,
1089
- enableProxy: true,
1090
- enableStaticServing: false,
1091
- enableCompression: false,
1092
- proxyPath: config.commerce.api.proxy,
1093
- proxyTarget: getCommerceCloudApiUrl(config.commerce.api.shortCode),
1094
- shortCode: config.commerce.api.shortCode,
1095
- organizationId: config.commerce.api.organizationId,
1096
- clientId: config.commerce.api.clientId,
1097
- siteId: config.commerce.api.siteId
1098
- });
1099
- });
1100
- ["SIGTERM", "SIGINT"].forEach((signal) => {
1101
- process.once(signal, () => {
1102
- printShutdownMessage();
1103
- server?.close(() => {
1104
- vite.close();
1105
- process.exit(0);
1106
- });
1107
- });
1108
- });
1109
- }
1110
-
1111
- //#endregion
1112
- //#region src/commands/preview.ts
1113
- /**
1114
- * Start the preview server with production build
1115
- */
1116
- async function preview(options = {}) {
1117
- const startTime = Date.now();
1118
- const projectDir = path.resolve(options.projectDirectory || process.cwd());
1119
- const port = options.port || 3e3;
1120
- process.env.NODE_ENV = process.env.NODE_ENV ?? "production";
1121
- process.env.EXTERNAL_DOMAIN_NAME = process.env.EXTERNAL_DOMAIN_NAME ?? `localhost:${port}`;
1122
- loadEnvFile(projectDir);
1123
- const buildPath = path.join(projectDir, "build", "server", "index.js");
1124
- if (!fs$1.existsSync(buildPath)) {
1125
- warn("Production build not found. Building project...");
1126
- info("Running: pnpm build");
1127
- try {
1128
- execSync("pnpm build", {
1129
- cwd: projectDir,
1130
- stdio: "inherit"
1131
- });
1132
- info("Build completed successfully");
1133
- } catch (err) {
1134
- error(`Build failed: ${err instanceof Error ? err.message : String(err)}`);
1135
- process.exit(1);
1136
- }
1137
- if (!fs$1.existsSync(buildPath)) {
1138
- error(`Build still not found at ${buildPath} after running build command`);
1139
- process.exit(1);
1140
- }
1141
- }
1142
- info(`Loading production build from ${buildPath}`);
1143
- const build = (await import(pathToFileURL(buildPath).href)).default;
1144
- const config = await loadProjectConfig(projectDir);
1145
- const server = (await createServer$1({
1146
- mode: "preview",
1147
- projectDirectory: projectDir,
1148
- config,
1149
- port,
1150
- build
1151
- })).listen(port, () => {
1152
- printServerInfo("preview", port, startTime, projectDir);
1153
- printServerConfig({
1154
- mode: "preview",
1155
- port,
1156
- enableProxy: true,
1157
- enableStaticServing: true,
1158
- enableCompression: true,
1159
- proxyPath: config.commerce.api.proxy,
1160
- proxyTarget: getCommerceCloudApiUrl(config.commerce.api.shortCode),
1161
- shortCode: config.commerce.api.shortCode,
1162
- organizationId: config.commerce.api.organizationId,
1163
- clientId: config.commerce.api.clientId,
1164
- siteId: config.commerce.api.siteId
1165
- });
1166
- });
1167
- ["SIGTERM", "SIGINT"].forEach((signal) => {
1168
- process.once(signal, () => {
1169
- printShutdownMessage();
1170
- server?.close(() => {
1171
- process.exit(0);
1172
- });
1173
- });
1174
- });
1175
- }
1176
-
1177
- //#endregion
1178
- //#region src/extensibility/create-instructions.ts
1179
- const SKIP_DIRS = [
1180
- "node_modules",
1181
- "dist",
1182
- "build"
1183
- ];
1184
- const INSTALL_INSTRUCTIONS_TEMPLATE = "install-instructions.mdc.hbs";
1185
- const UNINSTALL_INSTRUCTIONS_TEMPLATE = "uninstall-instructions.mdc.hbs";
1186
- /**
1187
- * Build the context for the instructions template.
1188
- */
1189
- function getContext(projectRoot, markerValue, pwaRepo = "https://github.com/SalesforceCommerceCloud/storefront-next-template.git", branch = "main", filesToCopy = [], extensionConfigPath = "") {
1190
- const extensionConfig = JSON.parse(fs$1.readFileSync(extensionConfigPath, "utf8"));
1191
- if (!extensionConfig.extensions[markerValue]) throw new Error(`Extension ${markerValue} not found in extension config`);
1192
- filesToCopy.forEach((file) => {
1193
- const fullPath = path.join(projectRoot, file);
1194
- if (!fs$1.existsSync(fullPath)) throw new Error(`File or directory ${fullPath} not found`);
1195
- });
1196
- const { mergeFiles, newFiles } = findMarkedFiles(projectRoot, markerValue);
1197
- filesToCopy.push(...newFiles);
1198
- const extensionMeta = extensionConfig.extensions[markerValue];
1199
- const dependencies = (extensionMeta.dependencies || []).map((depKey) => ({
1200
- key: depKey,
1201
- name: extensionConfig.extensions[depKey]?.name || depKey
1202
- }));
1203
- return {
1204
- extensionName: extensionMeta.name,
1205
- pwaRepo,
1206
- branch,
1207
- markerValue,
1208
- mergeFiles,
1209
- newFiles,
1210
- copy: getFilesToCopyContext(projectRoot, filesToCopy),
1211
- dependencies
1212
- };
1213
- }
1214
- /**
1215
- * Get the context for the files to copy.
1216
- */
1217
- const getFilesToCopyContext = (projectRoot, filesToCopy) => {
1218
- filesToCopy.forEach((file) => {
1219
- const fullPath = path.join(projectRoot, file);
1220
- if (!fs$1.existsSync(fullPath)) throw new Error(`File or directory ${fullPath} not found`);
1221
- });
1222
- return filesToCopy.map((file) => ({
1223
- src: file,
1224
- dest: file,
1225
- isDirectory: fs$1.statSync(path.join(projectRoot, file)).isDirectory()
1226
- }));
1227
- };
1228
- /**
1229
- * Find all the files that contain the marker value in the project folder.
1230
- * @param {string} markerValue
1231
- * @returns {string[]} The files that are marked with the marker value
1232
- */
1233
- const findMarkedFiles = (projectRoot, markerValue) => {
1234
- const fileTypes = [
1235
- "jsx",
1236
- "tsx",
1237
- "ts",
1238
- "js"
1239
- ];
1240
- const mergeFiles = [];
1241
- const newFiles = [];
1242
- const lineRegex = /* @__PURE__ */ new RegExp(`@sfdc-extension-line\\s+${markerValue}`);
1243
- const blockStartRegex = /* @__PURE__ */ new RegExp(`@sfdc-extension-block-start\\s+${markerValue}`);
1244
- const blockEndRegex = /* @__PURE__ */ new RegExp(`@sfdc-extension-block-end\\s+${markerValue}`);
1245
- const fileRegex = /* @__PURE__ */ new RegExp(`@sfdc-extension-file\\s+${markerValue}`);
1246
- const searchFiles = (dir) => {
1247
- const entries = fs$1.readdirSync(dir, { withFileTypes: true });
1248
- for (const entry of entries) {
1249
- const fullPath = path.join(dir, entry.name);
1250
- if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) searchFiles(fullPath);
1251
- else if (entry.isFile() && fileTypes.some((ext) => fullPath.endsWith(`.${ext}`))) {
1252
- const content = fs$1.readFileSync(fullPath, "utf8");
1253
- if (lineRegex.test(content) || blockStartRegex.test(content) || blockEndRegex.test(content)) mergeFiles.push(path.relative(projectRoot, fullPath));
1254
- else if (fileRegex.test(content)) newFiles.push(path.relative(projectRoot, fullPath));
1255
- }
1256
- }
1257
- };
1258
- searchFiles(projectRoot);
1259
- console.log(`Found ${mergeFiles.length} files to merge for marker value ${markerValue}:`);
1260
- console.log(mergeFiles.join("\n"));
1261
- console.log(`Found ${newFiles.length} files to add for marker value ${markerValue}:`);
1262
- console.log(newFiles.join("\n"));
1263
- return {
1264
- mergeFiles,
1265
- newFiles
1266
- };
1267
- };
1268
- /**
1269
- * Generate the MDC instructions file based on user inputs.
1270
- */
1271
- const generateInstructions = (projectRoot, markerValue, outputDir, pwaRepo, branch, filesToCopy, extensionConfig = "", templateDir = "") => {
1272
- const context = getContext(projectRoot, markerValue, pwaRepo, branch, filesToCopy, extensionConfig);
1273
- const instructionsDir = path.join(projectRoot, outputDir || "instructions");
1274
- if (!fs$1.existsSync(instructionsDir)) fs$1.mkdirSync(instructionsDir);
1275
- genertaeAndWriteInstructions(path.join(templateDir, INSTALL_INSTRUCTIONS_TEMPLATE), context, path.join(instructionsDir, `install-${context.extensionName.toLowerCase().replace(/ /g, "-")}.mdc`));
1276
- genertaeAndWriteInstructions(path.join(templateDir, UNINSTALL_INSTRUCTIONS_TEMPLATE), context, path.join(instructionsDir, `uninstall-${context.extensionName.toLowerCase().replace(/ /g, "-")}.mdc`));
1277
- };
1278
- /**
1279
- * Generate the MDC instructions file based on the template file and context.
1280
- */
1281
- const genertaeAndWriteInstructions = (templateFile, context, outputFile) => {
1282
- const templateContent = fs$1.readFileSync(templateFile, "utf8");
1283
- const mdcContent = Handlebars.compile(templateContent)(context);
1284
- fs$1.writeFileSync(outputFile, mdcContent, "utf8");
1285
- console.log(`MDC instructions written to ${outputFile}`);
1286
- };
1287
-
1288
- //#endregion
1289
- //#region src/cartridge-services/react-router-config.ts
1290
- let isCliAvailable = null;
1291
- function checkReactRouterCli(projectDirectory) {
1292
- if (isCliAvailable !== null) return isCliAvailable;
1293
- try {
1294
- execSync$1("react-router --version", {
1295
- cwd: projectDirectory,
1296
- env: npmRunPathEnv(),
1297
- stdio: "pipe"
1298
- });
1299
- isCliAvailable = true;
1300
- } catch {
1301
- isCliAvailable = false;
1302
- }
1303
- return isCliAvailable;
1304
- }
1305
- /**
1306
- * Get the fully resolved routes from React Router by invoking its CLI.
1307
- * This ensures we get the exact same route resolution as React Router uses internally,
1308
- * including all presets, file-system routes, and custom route configurations.
1309
- * @param projectDirectory - The project root directory
1310
- * @returns Array of resolved route config entries
1311
- * @example
1312
- * const routes = getReactRouterRoutes('/path/to/project');
1313
- * // Returns the same structure as `react-router routes --json`
1314
- */
1315
- function getReactRouterRoutes(projectDirectory) {
1316
- if (!checkReactRouterCli(projectDirectory)) throw new Error("React Router CLI is not available. Please make sure @react-router/dev is installed and accessible.");
1317
- const tempFile = join(tmpdir(), `react-router-routes-${randomUUID()}.json`);
1318
- try {
1319
- execSync$1(`react-router routes --json > "${tempFile}"`, {
1320
- cwd: projectDirectory,
1321
- env: npmRunPathEnv(),
1322
- encoding: "utf-8",
1323
- stdio: [
1324
- "pipe",
1325
- "pipe",
1326
- "pipe"
1327
- ]
1328
- });
1329
- const output = readFileSync(tempFile, "utf-8");
1330
- return JSON.parse(output);
1331
- } catch (error$1) {
1332
- throw new Error(`Failed to get routes from React Router CLI: ${error$1.message}`);
1333
- } finally {
1334
- try {
1335
- if (existsSync(tempFile)) unlinkSync(tempFile);
1336
- } catch {}
1337
- }
1338
- }
1339
- /**
1340
- * Convert a file path to its corresponding route path using React Router's CLI.
1341
- * This ensures we get the exact same route resolution as React Router uses internally.
1342
- * @param filePath - Absolute path to the route file
1343
- * @param projectRoot - The project root directory
1344
- * @returns The route path (e.g., '/cart', '/product/:productId')
1345
- * @example
1346
- * const route = filePathToRoute('/path/to/project/src/routes/_app.cart.tsx', '/path/to/project');
1347
- * // Returns: '/cart'
1348
- */
1349
- function filePathToRoute(filePath, projectRoot) {
1350
- const filePathPosix = filePath.replace(/\\/g, "/");
1351
- const flatRoutes = flattenRoutes(getReactRouterRoutes(projectRoot));
1352
- for (const route of flatRoutes) {
1353
- const routeFilePosix = route.file.replace(/\\/g, "/");
1354
- if (filePathPosix.endsWith(routeFilePosix) || filePathPosix.endsWith(`/${routeFilePosix}`)) return route.path;
1355
- const routeFileNormalized = routeFilePosix.replace(/^\.\//, "");
1356
- if (filePathPosix.endsWith(routeFileNormalized) || filePathPosix.endsWith(`/${routeFileNormalized}`)) return route.path;
1357
- }
1358
- console.warn(`Warning: Could not find route for file: ${filePath}`);
1359
- return "/unknown";
1360
- }
1361
- /**
1362
- * Flatten a nested route tree into a flat array with computed paths.
1363
- * Each route will have its full path computed from parent paths.
1364
- * @param routes - The nested route config entries
1365
- * @param parentPath - The parent path prefix (used internally for recursion)
1366
- * @returns Flat array of routes with their full paths
1367
- */
1368
- function flattenRoutes(routes, parentPath = "") {
1369
- const result = [];
1370
- for (const route of routes) {
1371
- let fullPath;
1372
- if (route.index) fullPath = parentPath || "/";
1373
- else if (route.path) {
1374
- const pathSegment = route.path.startsWith("/") ? route.path : `/${route.path}`;
1375
- fullPath = parentPath ? `${parentPath}${pathSegment}`.replace(/\/+/g, "/") : pathSegment;
1376
- } else fullPath = parentPath || "/";
1377
- if (route.id) result.push({
1378
- id: route.id,
1379
- path: fullPath,
1380
- file: route.file,
1381
- index: route.index
1382
- });
1383
- if (route.children && route.children.length > 0) {
1384
- const childPath = route.path ? fullPath : parentPath;
1385
- result.push(...flattenRoutes(route.children, childPath));
1386
- }
1387
- }
1388
- return result;
1389
- }
1390
-
1391
- //#endregion
1392
- //#region src/cartridge-services/generate-cartridge.ts
1393
- const SKIP_DIRECTORIES = [
1394
- "build",
1395
- "dist",
1396
- "node_modules",
1397
- ".git",
1398
- ".next",
1399
- "coverage"
1400
- ];
1401
- const DEFAULT_COMPONENT_GROUP = "odyssey_base";
1402
- const ARCH_TYPE_HEADLESS = "headless";
1403
- const VALID_ATTRIBUTE_TYPES = [
1404
- "string",
1405
- "text",
1406
- "markup",
1407
- "integer",
1408
- "boolean",
1409
- "product",
1410
- "category",
1411
- "file",
1412
- "page",
1413
- "image",
1414
- "url",
1415
- "enum",
1416
- "custom",
1417
- "cms_record"
1418
- ];
1419
- const TYPE_MAPPING = {
1420
- String: "string",
1421
- string: "string",
1422
- Number: "integer",
1423
- number: "integer",
1424
- Boolean: "boolean",
1425
- boolean: "boolean",
1426
- Date: "string",
1427
- URL: "url",
1428
- CMSRecord: "cms_record"
1429
- };
1430
- function resolveAttributeType(decoratorType, tsMorphType, fieldName) {
1431
- if (decoratorType) {
1432
- if (!VALID_ATTRIBUTE_TYPES.includes(decoratorType)) {
1433
- console.error(`Error: Invalid attribute type '${decoratorType}' for field '${fieldName || "unknown"}'. Valid types are: ${VALID_ATTRIBUTE_TYPES.join(", ")}`);
1434
- process.exit(1);
1435
- }
1436
- return decoratorType;
1437
- }
1438
- if (tsMorphType && TYPE_MAPPING[tsMorphType]) return TYPE_MAPPING[tsMorphType];
1439
- return "string";
1440
- }
1441
- function toHumanReadableName(fieldName) {
1442
- return fieldName.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
1443
- }
1444
- function toCamelCaseFileName(name) {
1445
- if (!/[\s-]/.test(name)) return name;
1446
- return name.split(/[\s-]+/).map((word, index) => {
1447
- if (index === 0) return word.toLowerCase();
1448
- return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
1449
- }).join("");
1450
- }
1451
- function getTypeFromTsMorph(property, _sourceFile) {
1452
- try {
1453
- const typeNode = property.getTypeNode();
1454
- if (typeNode) return typeNode.getText().split("|")[0].split("&")[0].trim();
1455
- } catch {}
1456
- return "string";
1457
- }
1458
- function parseExpression(expression) {
1459
- if (Node.isStringLiteral(expression)) return expression.getLiteralValue();
1460
- else if (Node.isNumericLiteral(expression)) return expression.getLiteralValue();
1461
- else if (Node.isTrueLiteral(expression)) return true;
1462
- else if (Node.isFalseLiteral(expression)) return false;
1463
- else if (Node.isObjectLiteralExpression(expression)) return parseNestedObject(expression);
1464
- else if (Node.isArrayLiteralExpression(expression)) return parseArrayLiteral(expression);
1465
- else return expression.getText();
1466
- }
1467
- function parseNestedObject(objectLiteral) {
1468
- const result = {};
1469
- try {
1470
- const properties = objectLiteral.getProperties();
1471
- for (const property of properties) if (Node.isPropertyAssignment(property)) {
1472
- const name = property.getName();
1473
- const initializer = property.getInitializer();
1474
- if (initializer) result[name] = parseExpression(initializer);
1475
- }
1476
- } catch (error$1) {
1477
- console.warn(`Warning: Could not parse nested object: ${error$1.message}`);
1478
- return result;
1479
- }
1480
- return result;
1481
- }
1482
- function parseArrayLiteral(arrayLiteral) {
1483
- const result = [];
1484
- try {
1485
- const elements = arrayLiteral.getElements();
1486
- for (const element of elements) result.push(parseExpression(element));
1487
- } catch (error$1) {
1488
- console.warn(`Warning: Could not parse array literal: ${error$1.message}`);
1489
- }
1490
- return result;
1491
- }
1492
- function parseDecoratorArgs(decorator) {
1493
- const result = {};
1494
- try {
1495
- const args = decorator.getArguments();
1496
- if (args.length === 0) return result;
1497
- const firstArg = args[0];
1498
- if (Node.isObjectLiteralExpression(firstArg)) {
1499
- const properties = firstArg.getProperties();
1500
- for (const property of properties) if (Node.isPropertyAssignment(property)) {
1501
- const name = property.getName();
1502
- const initializer = property.getInitializer();
1503
- if (initializer) result[name] = parseExpression(initializer);
1504
- }
1505
- } else if (Node.isStringLiteral(firstArg)) {
1506
- result.id = parseExpression(firstArg);
1507
- if (args.length > 1) {
1508
- const secondArg = args[1];
1509
- if (Node.isObjectLiteralExpression(secondArg)) {
1510
- const properties = secondArg.getProperties();
1511
- for (const property of properties) if (Node.isPropertyAssignment(property)) {
1512
- const name = property.getName();
1513
- const initializer = property.getInitializer();
1514
- if (initializer) result[name] = parseExpression(initializer);
1515
- }
1516
- }
1517
- }
1518
- }
1519
- return result;
1520
- } catch (error$1) {
1521
- console.warn(`Warning: Could not parse decorator arguments: ${error$1.message}`);
1522
- return result;
1523
- }
1524
- }
1525
- function extractAttributesFromSource(sourceFile, className) {
1526
- const attributes = [];
1527
- try {
1528
- const classDeclaration = sourceFile.getClass(className);
1529
- if (!classDeclaration) return attributes;
1530
- const properties = classDeclaration.getProperties();
1531
- for (const property of properties) {
1532
- const attributeDecorator = property.getDecorator("AttributeDefinition");
1533
- if (!attributeDecorator) continue;
1534
- const fieldName = property.getName();
1535
- const config = parseDecoratorArgs(attributeDecorator);
1536
- const isRequired = !property.hasQuestionToken();
1537
- const inferredType = config.type || getTypeFromTsMorph(property, sourceFile);
1538
- const attribute = {
1539
- id: config.id || fieldName,
1540
- name: config.name || toHumanReadableName(fieldName),
1541
- type: resolveAttributeType(config.type, inferredType, fieldName),
1542
- required: config.required !== void 0 ? config.required : isRequired,
1543
- description: config.description || `Field: ${fieldName}`
1544
- };
1545
- if (config.values) attribute.values = config.values;
1546
- if (config.defaultValue !== void 0) attribute.default_value = config.defaultValue;
1547
- attributes.push(attribute);
1548
- }
1549
- } catch (error$1) {
1550
- console.warn(`Warning: Could not extract attributes from class ${className}: ${error$1.message}`);
1551
- }
1552
- return attributes;
1553
- }
1554
- function extractRegionDefinitionsFromSource(sourceFile, className) {
1555
- const regionDefinitions = [];
1556
- try {
1557
- const classDeclaration = sourceFile.getClass(className);
1558
- if (!classDeclaration) return regionDefinitions;
1559
- const classRegionDecorator = classDeclaration.getDecorator("RegionDefinition");
1560
- if (classRegionDecorator) {
1561
- const args = classRegionDecorator.getArguments();
1562
- if (args.length > 0) {
1563
- const firstArg = args[0];
1564
- if (Node.isArrayLiteralExpression(firstArg)) {
1565
- const elements = firstArg.getElements();
1566
- for (const element of elements) if (Node.isObjectLiteralExpression(element)) {
1567
- const regionConfig = parseDecoratorArgs({ getArguments: () => [element] });
1568
- const regionDefinition = {
1569
- id: regionConfig.id || "region",
1570
- name: regionConfig.name || "Region"
1571
- };
1572
- if (regionConfig.componentTypes) regionDefinition.component_types = regionConfig.componentTypes;
1573
- if (Array.isArray(regionConfig.componentTypeInclusions)) regionDefinition.component_type_inclusions = regionConfig.componentTypeInclusions.map((incl) => ({ type_id: incl }));
1574
- if (Array.isArray(regionConfig.componentTypeExclusions)) regionDefinition.component_type_exclusions = regionConfig.componentTypeExclusions.map((excl) => ({ type_id: excl }));
1575
- if (regionConfig.maxComponents !== void 0) regionDefinition.max_components = regionConfig.maxComponents;
1576
- if (regionConfig.minComponents !== void 0) regionDefinition.min_components = regionConfig.minComponents;
1577
- if (regionConfig.allowMultiple !== void 0) regionDefinition.allow_multiple = regionConfig.allowMultiple;
1578
- if (regionConfig.defaultComponentConstructors) regionDefinition.default_component_constructors = regionConfig.defaultComponentConstructors;
1579
- regionDefinitions.push(regionDefinition);
1580
- }
1581
- }
1582
- }
1583
- }
1584
- } catch (error$1) {
1585
- console.warn(`Warning: Could not extract region definitions from class ${className}: ${error$1.message}`);
1586
- }
1587
- return regionDefinitions;
1588
- }
1589
- async function processComponentFile(filePath, _projectRoot) {
1590
- try {
1591
- const content = await readFile(filePath, "utf-8");
1592
- const components = [];
1593
- if (!content.includes("@Component")) return components;
1594
- try {
1595
- const sourceFile = new Project({
1596
- useInMemoryFileSystem: true,
1597
- skipAddingFilesFromTsConfig: true
1598
- }).createSourceFile(filePath, content);
1599
- const classes = sourceFile.getClasses();
1600
- for (const classDeclaration of classes) {
1601
- const componentDecorator = classDeclaration.getDecorator("Component");
1602
- if (!componentDecorator) continue;
1603
- const className = classDeclaration.getName();
1604
- if (!className) continue;
1605
- const componentConfig = parseDecoratorArgs(componentDecorator);
1606
- const attributes = extractAttributesFromSource(sourceFile, className);
1607
- const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
1608
- const componentMetadata = {
1609
- typeId: componentConfig.id || className.toLowerCase(),
1610
- name: componentConfig.name || toHumanReadableName(className),
1611
- group: componentConfig.group || DEFAULT_COMPONENT_GROUP,
1612
- description: componentConfig.description || `Custom component: ${className}`,
1613
- regionDefinitions,
1614
- attributes
1615
- };
1616
- components.push(componentMetadata);
1617
- }
1618
- } catch (error$1) {
1619
- console.warn(`Warning: Could not process file ${filePath}:`, error$1.message);
1620
- }
1621
- return components;
1622
- } catch (error$1) {
1623
- console.warn(`Warning: Could not read file ${filePath}:`, error$1.message);
1624
- return [];
1625
- }
1626
- }
1627
- async function processPageTypeFile(filePath, projectRoot) {
1628
- try {
1629
- const content = await readFile(filePath, "utf-8");
1630
- const pageTypes = [];
1631
- if (!content.includes("@PageType")) return pageTypes;
1632
- try {
1633
- const sourceFile = new Project({
1634
- useInMemoryFileSystem: true,
1635
- skipAddingFilesFromTsConfig: true
1636
- }).createSourceFile(filePath, content);
1637
- const classes = sourceFile.getClasses();
1638
- for (const classDeclaration of classes) {
1639
- const pageTypeDecorator = classDeclaration.getDecorator("PageType");
1640
- if (!pageTypeDecorator) continue;
1641
- const className = classDeclaration.getName();
1642
- if (!className) continue;
1643
- const pageTypeConfig = parseDecoratorArgs(pageTypeDecorator);
1644
- const attributes = extractAttributesFromSource(sourceFile, className);
1645
- const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
1646
- const route = filePathToRoute(filePath, projectRoot);
1647
- const pageTypeMetadata = {
1648
- typeId: pageTypeConfig.id || className.toLowerCase(),
1649
- name: pageTypeConfig.name || toHumanReadableName(className),
1650
- description: pageTypeConfig.description || `Custom page type: ${className}`,
1651
- regionDefinitions,
1652
- supportedAspectTypes: pageTypeConfig.supportedAspectTypes || [],
1653
- attributes,
1654
- route
1655
- };
1656
- pageTypes.push(pageTypeMetadata);
1657
- }
1658
- } catch (error$1) {
1659
- console.warn(`Warning: Could not process file ${filePath}:`, error$1.message);
1660
- }
1661
- return pageTypes;
1662
- } catch (error$1) {
1663
- console.warn(`Warning: Could not read file ${filePath}:`, error$1.message);
1664
- return [];
1665
- }
1666
- }
1667
- async function processAspectFile(filePath, _projectRoot) {
1668
- try {
1669
- const content = await readFile(filePath, "utf-8");
1670
- const aspects = [];
1671
- if (!filePath.endsWith(".json") || !content.trim().startsWith("{")) return aspects;
1672
- if (!filePath.includes("/aspects/") && !filePath.includes("\\aspects\\")) return aspects;
1673
- try {
1674
- const aspectData = JSON.parse(content);
1675
- const fileName = basename(filePath, ".json");
1676
- if (!aspectData.name || !aspectData.attribute_definitions) return aspects;
1677
- const aspectMetadata = {
1678
- id: fileName,
1679
- name: aspectData.name,
1680
- description: aspectData.description || `Aspect type: ${aspectData.name}`,
1681
- attributeDefinitions: aspectData.attribute_definitions || [],
1682
- supportedObjectTypes: aspectData.supported_object_types || []
1683
- };
1684
- aspects.push(aspectMetadata);
1685
- } catch (parseError) {
1686
- console.warn(`Warning: Could not parse JSON in file ${filePath}:`, parseError.message);
1687
- }
1688
- return aspects;
1689
- } catch (error$1) {
1690
- console.warn(`Warning: Could not read file ${filePath}:`, error$1.message);
1691
- return [];
1692
- }
1693
- }
1694
- async function generateComponentCartridge(component, outputDir, dryRun = false) {
1695
- const fileName = toCamelCaseFileName(component.typeId);
1696
- const groupDir = join(outputDir, component.group);
1697
- const outputPath = join(groupDir, `${fileName}.json`);
1698
- if (!dryRun) {
1699
- try {
1700
- await mkdir(groupDir, { recursive: true });
1701
- } catch {}
1702
- const attributeDefinitionGroups = [{
1703
- id: component.typeId,
1704
- name: component.name,
1705
- description: component.description,
1706
- attribute_definitions: component.attributes
1707
- }];
1708
- const cartridgeData = {
1709
- name: component.name,
1710
- description: component.description,
1711
- group: component.group,
1712
- arch_type: ARCH_TYPE_HEADLESS,
1713
- region_definitions: component.regionDefinitions || [],
1714
- attribute_definition_groups: attributeDefinitionGroups
1715
- };
1716
- await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
1717
- }
1718
- const prefix = dryRun ? " - [DRY RUN]" : " -";
1719
- console.log(`${prefix} ${String(component.typeId)}: ${String(component.name)} (${String(component.attributes.length)} attributes) → ${fileName}.json`);
1720
- }
1721
- async function generatePageTypeCartridge(pageType, outputDir, dryRun = false) {
1722
- const fileName = toCamelCaseFileName(pageType.name);
1723
- const outputPath = join(outputDir, `${fileName}.json`);
1724
- if (!dryRun) {
1725
- const cartridgeData = {
1726
- name: pageType.name,
1727
- description: pageType.description,
1728
- arch_type: ARCH_TYPE_HEADLESS,
1729
- region_definitions: pageType.regionDefinitions || []
1730
- };
1731
- if (pageType.attributes && pageType.attributes.length > 0) cartridgeData.attribute_definition_groups = [{
1732
- id: pageType.typeId || fileName,
1733
- name: pageType.name,
1734
- description: pageType.description,
1735
- attribute_definitions: pageType.attributes
1736
- }];
1737
- if (pageType.supportedAspectTypes) cartridgeData.supported_aspect_types = pageType.supportedAspectTypes;
1738
- if (pageType.route) cartridgeData.route = pageType.route;
1739
- await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
1740
- }
1741
- const prefix = dryRun ? " - [DRY RUN]" : " -";
1742
- console.log(`${prefix} ${String(pageType.name)}: ${String(pageType.description)} (${String(pageType.attributes.length)} attributes) → ${fileName}.json`);
1743
- }
1744
- async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
1745
- const fileName = toCamelCaseFileName(aspect.id);
1746
- const outputPath = join(outputDir, `${fileName}.json`);
1747
- if (!dryRun) {
1748
- const cartridgeData = {
1749
- name: aspect.name,
1750
- description: aspect.description,
1751
- arch_type: ARCH_TYPE_HEADLESS,
1752
- attribute_definitions: aspect.attributeDefinitions || []
1753
- };
1754
- if (aspect.supportedObjectTypes) cartridgeData.supported_object_types = aspect.supportedObjectTypes;
1755
- await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
1756
- }
1757
- const prefix = dryRun ? " - [DRY RUN]" : " -";
1758
- console.log(`${prefix} ${String(aspect.name)}: ${String(aspect.description)} (${String(aspect.attributeDefinitions.length)} attributes) → ${fileName}.json`);
1759
- }
1760
- /**
1761
- * Runs ESLint with --fix on the specified directory to format JSON files.
1762
- * This ensures generated JSON files match the project's Prettier/ESLint configuration.
1763
- */
1764
- function lintGeneratedFiles(metadataDir, projectRoot) {
1765
- try {
1766
- console.log("🔧 Running ESLint --fix on generated JSON files...");
1767
- execSync$1(`npx eslint "${metadataDir}/**/*.json" --fix --no-error-on-unmatched-pattern`, {
1768
- cwd: projectRoot,
1769
- stdio: "pipe",
1770
- encoding: "utf-8"
1771
- });
1772
- console.log("✅ JSON files formatted successfully");
1773
- } catch (error$1) {
1774
- const execError = error$1;
1775
- if (execError.status === 2) {
1776
- const errMsg = execError.stderr || execError.stdout || "Unknown error";
1777
- console.warn(`⚠️ Warning: Could not run ESLint --fix: ${errMsg}`);
1778
- } else if (execError.stderr && execError.stderr.includes("error")) console.warn(`⚠️ Warning: Some linting issues could not be auto-fixed. Run ESLint manually to review.`);
1779
- else console.log("✅ JSON files formatted successfully");
1780
- }
1781
- }
1782
- async function generateMetadata(projectDirectory, metadataDirectory, options) {
1783
- try {
1784
- const filePaths = options?.filePaths;
1785
- const isIncrementalMode = filePaths && filePaths.length > 0;
1786
- const dryRun = options?.dryRun || false;
1787
- if (dryRun) console.log("🔍 [DRY RUN] Scanning for decorated components and page types...");
1788
- else if (isIncrementalMode) console.log(`🔍 Generating metadata for ${filePaths.length} specified file(s)...`);
1789
- else console.log("🔍 Generating metadata for decorated components and page types...");
1790
- const projectRoot = resolve(projectDirectory);
1791
- const srcDir = join(projectRoot, "src");
1792
- const metadataDir = resolve(metadataDirectory);
1793
- const componentsOutputDir = join(metadataDir, "components");
1794
- const pagesOutputDir = join(metadataDir, "pages");
1795
- const aspectsOutputDir = join(metadataDir, "aspects");
1796
- if (!dryRun) {
1797
- if (!isIncrementalMode) {
1798
- console.log("🗑️ Cleaning existing output directories...");
1799
- for (const outputDir of [
1800
- componentsOutputDir,
1801
- pagesOutputDir,
1802
- aspectsOutputDir
1803
- ]) try {
1804
- await rm(outputDir, {
1805
- recursive: true,
1806
- force: true
1807
- });
1808
- console.log(` - Deleted: ${outputDir}`);
1809
- } catch {
1810
- console.log(` - Directory not found (skipping): ${outputDir}`);
1811
- }
1812
- } else console.log("📝 Incremental mode: existing cartridge files will be preserved/overwritten");
1813
- console.log("📁 Creating output directories...");
1814
- for (const outputDir of [
1815
- componentsOutputDir,
1816
- pagesOutputDir,
1817
- aspectsOutputDir
1818
- ]) try {
1819
- await mkdir(outputDir, { recursive: true });
1820
- } catch (error$1) {
1821
- try {
1822
- await access(outputDir);
1823
- } catch {
1824
- console.error(`❌ Error: Failed to create output directory ${outputDir}: ${error$1.message}`);
1825
- process.exit(1);
1826
- }
1827
- }
1828
- } else if (isIncrementalMode) console.log(`📝 [DRY RUN] Would process ${filePaths.length} specific file(s)`);
1829
- else console.log("📝 [DRY RUN] Would clean and regenerate all metadata files");
1830
- let files = [];
1831
- if (isIncrementalMode && filePaths) {
1832
- files = filePaths.map((fp) => resolve(projectRoot, fp));
1833
- console.log(`📂 Processing ${files.length} specified file(s)...`);
1834
- } else {
1835
- const scanDirectory = async (dir) => {
1836
- const entries = await readdir(dir, { withFileTypes: true });
1837
- for (const entry of entries) {
1838
- const fullPath = join(dir, entry.name);
1839
- if (entry.isDirectory()) {
1840
- if (!SKIP_DIRECTORIES.includes(entry.name)) await scanDirectory(fullPath);
1841
- } else if (entry.isFile() && (extname$1(entry.name) === ".ts" || extname$1(entry.name) === ".tsx" || extname$1(entry.name) === ".json")) files.push(fullPath);
1842
- }
1843
- };
1844
- await scanDirectory(srcDir);
1845
- }
1846
- const allComponents = [];
1847
- const allPageTypes = [];
1848
- const allAspects = [];
1849
- for (const file of files) {
1850
- const components = await processComponentFile(file, projectRoot);
1851
- allComponents.push(...components);
1852
- const pageTypes = await processPageTypeFile(file, projectRoot);
1853
- allPageTypes.push(...pageTypes);
1854
- const aspects = await processAspectFile(file, projectRoot);
1855
- allAspects.push(...aspects);
1856
- }
1857
- if (allComponents.length === 0 && allPageTypes.length === 0 && allAspects.length === 0) {
1858
- console.log("⚠️ No decorated components, page types, or aspect files found.");
1859
- return {
1860
- componentsGenerated: 0,
1861
- pageTypesGenerated: 0,
1862
- aspectsGenerated: 0,
1863
- totalFiles: 0
1864
- };
1865
- }
1866
- if (allComponents.length > 0) {
1867
- console.log(`✅ Found ${allComponents.length} decorated component(s):`);
1868
- for (const component of allComponents) await generateComponentCartridge(component, componentsOutputDir, dryRun);
1869
- if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allComponents.length} component metadata file(s) in: ${componentsOutputDir}`);
1870
- else console.log(`📄 Generated ${allComponents.length} component metadata file(s) in: ${componentsOutputDir}`);
1871
- }
1872
- if (allPageTypes.length > 0) {
1873
- console.log(`✅ Found ${allPageTypes.length} decorated page type(s):`);
1874
- for (const pageType of allPageTypes) await generatePageTypeCartridge(pageType, pagesOutputDir, dryRun);
1875
- if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allPageTypes.length} page type metadata file(s) in: ${pagesOutputDir}`);
1876
- else console.log(`📄 Generated ${allPageTypes.length} page type metadata file(s) in: ${pagesOutputDir}`);
1877
- }
1878
- if (allAspects.length > 0) {
1879
- console.log(`✅ Found ${allAspects.length} decorated aspect(s):`);
1880
- for (const aspect of allAspects) await generateAspectCartridge(aspect, aspectsOutputDir, dryRun);
1881
- if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allAspects.length} aspect metadata file(s) in: ${aspectsOutputDir}`);
1882
- else console.log(`📄 Generated ${allAspects.length} aspect metadata file(s) in: ${aspectsOutputDir}`);
1883
- }
1884
- const shouldLintFix = options?.lintFix !== false;
1885
- if (!dryRun && shouldLintFix && (allComponents.length > 0 || allPageTypes.length > 0 || allAspects.length > 0)) lintGeneratedFiles(metadataDir, projectRoot);
1886
- return {
1887
- componentsGenerated: allComponents.length,
1888
- pageTypesGenerated: allPageTypes.length,
1889
- aspectsGenerated: allAspects.length,
1890
- totalFiles: allComponents.length + allPageTypes.length + allAspects.length
1891
- };
1892
- } catch (error$1) {
1893
- console.error("❌ Error:", error$1.message);
1894
- process.exit(1);
1895
- }
1896
- }
1897
-
1898
- //#endregion
1899
- //#region src/cartridge-services/types.ts
1900
- const WEBDAV_BASE = "/on/demandware.servlet/webdav/Sites";
1901
- const CARTRIDGES_PATH = "Cartridges";
1902
- const HTTP_METHODS = {
1903
- PUT: "PUT",
1904
- POST: "POST",
1905
- DELETE: "DELETE"
1906
- };
1907
- const CONTENT_TYPES = {
1908
- APPLICATION_ZIP: "application/zip",
1909
- APPLICATION_FORM_URLENCODED: "application/x-www-form-urlencoded",
1910
- APPLICATION_JSON: "application/json"
1911
- };
1912
- const WEBDAV_OPERATIONS = {
1913
- UNZIP: "UNZIP",
1914
- TARGET_CARTRIDGES: "cartridges"
1915
- };
1916
-
1917
- //#endregion
1918
- //#region src/cartridge-services/sfcc-client.ts
1919
- /**
1920
- * Create HTTP request options for WebDAV operations (file upload/download)
1921
- *
1922
- * @param instance - The Commerce Cloud instance hostname
1923
- * @param path - The WebDAV path (e.g., '/cartridges')
1924
- * @param basicAuth - Base64 encoded basic authentication credentials (required)
1925
- * @param method - HTTP method (PUT, DELETE, UNZIP, etc.)
1926
- * @param formData - Optional form data for the request
1927
- * @returns Configured HTTP request options for WebDAV operations
1928
- */
1929
- function getWebdavOptions(instance, path$1, basicAuth, method, formData) {
1930
- const endpoint = `${WEBDAV_BASE}/${path$1}`;
1931
- return {
1932
- baseUrl: `https://${instance}`,
1933
- uri: endpoint,
1934
- auth: { basic: basicAuth },
1935
- method,
1936
- ...formData && { form: formData }
1937
- };
1938
- }
1939
- /**
1940
- * Check if an HTTP response indicates an authentication error and throw if so
1941
- *
1942
- * @param response - The HTTP response to check
1943
- * @throws Error with authentication message if status code is 401
1944
- */
1945
- function checkAuthenticationError(response) {
1946
- if (response.statusCode === 401) throw new Error("Authentication failed. Please login again.");
1947
- }
1948
- /**
1949
- * Execute an HTTP request using the native fetch API with default SSL validation
1950
- *
1951
- * This function handles general HTTP requests and does not automatically set Content-Type headers.
1952
- * Callers must set the appropriate Content-Type header in opts.headers based on their body type
1953
- *
1954
- * @param opts - HTTP request configuration including URL, method, headers, and body
1955
- * @returns Promise resolving to an object containing the HTTP response and parsed body
1956
- * @throws Error if the HTTP request fails or cannot be completed
1957
- */
1958
- async function makeRequest(opts) {
1959
- const url = opts.uri;
1960
- const fetchOptions = {
1961
- ...opts,
1962
- headers: {
1963
- Authorization: `Basic ${opts.auth.basic}`,
1964
- ...opts.headers
1965
- }
1966
- };
1967
- if (opts.form) {
1968
- const formData = new URLSearchParams();
1969
- Object.entries(opts.form).forEach(([key, value]) => {
1970
- formData.append(key, String(value));
1971
- });
1972
- fetchOptions.body = formData;
1973
- fetchOptions.headers = {
1974
- ...fetchOptions.headers,
1975
- "Content-Type": CONTENT_TYPES.APPLICATION_FORM_URLENCODED
1976
- };
1977
- }
1978
- try {
1979
- const response = await fetch(url, fetchOptions);
1980
- const body = response.headers.get("content-type")?.includes(CONTENT_TYPES.APPLICATION_JSON) ? await response.json() : await response.text();
1981
- const headers = {};
1982
- response.headers.forEach((value, key) => {
1983
- headers[key] = value;
1984
- });
1985
- return {
1986
- response: {
1987
- statusCode: response.status,
1988
- statusMessage: response.statusText,
1989
- headers
1990
- },
1991
- body
1992
- };
1993
- } catch (error$1) {
1994
- throw new Error(`HTTP request failed: ${error$1 instanceof Error ? error$1.message : String(error$1)}`);
1995
- }
1996
- }
1997
-
1998
- //#endregion
1999
- //#region src/cartridge-services/validation.ts
2000
- /**
2001
- * Validation error class for cartridge service parameter validation
2002
- */
2003
- var ValidationError = class extends Error {
2004
- constructor(message) {
2005
- super(message);
2006
- this.name = "ValidationError";
2007
- }
2008
- };
2009
- /**
2010
- * Validate Commerce Cloud instance hostname
2011
- *
2012
- * @param instance - The instance hostname to validate
2013
- * @throws ValidationError if instance is invalid
2014
- */
2015
- function validateInstance(instance) {
2016
- if (!instance || typeof instance !== "string") throw new ValidationError("Instance parameter is required and must be a string");
2017
- if (instance.trim().length === 0) throw new ValidationError("Instance parameter cannot be empty");
2018
- if (!instance.includes(".")) throw new ValidationError("Parameter instance must be a valid domain name");
2019
- }
2020
- /**
2021
- * Validate cartridge file (must be a ZIP file)
2022
- *
2023
- * @param cartridgePath - The cartridge file path to validate
2024
- * @throws ValidationError if cartridge is invalid
2025
- */
2026
- function validateCartridgePath(cartridgePath) {
2027
- if (!cartridgePath || typeof cartridgePath !== "string") throw new ValidationError("cartridge parameter is required and must be a string");
2028
- if (cartridgePath.trim().length === 0) throw new ValidationError("cartridge parameter cannot be empty");
2029
- const ext = extname(cartridgePath).toLowerCase();
2030
- if (ext !== "") throw new ValidationError(`cartridge must be a directory, got: ${ext}`);
2031
- }
2032
- /**
2033
- * Validate Basic Auth credentials
2034
- *
2035
- * @param basicAuth - The base64 encoded basic auth credentials to validate
2036
- * @throws ValidationError if credentials are invalid
2037
- */
2038
- function validateBasicAuth(basicAuth) {
2039
- if (!basicAuth || typeof basicAuth !== "string") throw new ValidationError("Basic auth credentials parameter is required and must be a string");
2040
- if (basicAuth.trim().length === 0) throw new ValidationError("Basic auth credentials parameter cannot be empty");
2041
- if (basicAuth.length < 10) throw new ValidationError("Basic auth credentials appear to be too short to be valid");
2042
- }
2043
- /**
2044
- * Validate code version name
2045
- *
2046
- * @param version - The code version name to validate
2047
- * @throws ValidationError if version is invalid
2048
- */
2049
- function validateVersion(version$1) {
2050
- if (!version$1 || typeof version$1 !== "string") throw new ValidationError("Version parameter is required and must be a string");
2051
- if (version$1.trim().length === 0) throw new ValidationError("Version parameter cannot be empty");
2052
- if (!/^[a-zA-Z0-9._-]+$/.test(version$1)) throw new ValidationError("Version parameter contains invalid characters. Only alphanumeric, dots, hyphens, and underscores are allowed");
2053
- }
2054
- /**
2055
- * Validate WebDAV path
2056
- *
2057
- * @param webdavPath - The WebDAV path to validate
2058
- * @throws ValidationError if path is invalid
2059
- */
2060
- function validateWebdavPath(webdavPath) {
2061
- if (!webdavPath || typeof webdavPath !== "string") throw new ValidationError("WebDAV path parameter is required and must be a string");
2062
- if (!webdavPath.startsWith("/")) throw new ValidationError("WebDAV path must start with a forward slash");
2063
- }
2064
- /**
2065
- * Validate all parameters for deployCode function
2066
- *
2067
- * @param instance - Commerce Cloud instance hostname
2068
- * @param codeVersionName - Target code version name
2069
- * @param cartridgeDirectoryPath - Path to the source directory
2070
- * @param basicAuth - Base64 encoded basic auth credentials
2071
- * @param cartridgeWebDevPath - WebDAV path for cartridge deployment
2072
- * @throws ValidationError if any parameter is invalid
2073
- */
2074
- function validateDeployCodeParams(instance, codeVersionName, cartridgeDirectoryPath, basicAuth, cartridgeWebDevPath) {
2075
- validateInstance(instance);
2076
- validateVersion(codeVersionName);
2077
- validateCartridgePath(cartridgeDirectoryPath);
2078
- validateBasicAuth(basicAuth);
2079
- validateWebdavPath(cartridgeWebDevPath);
2080
- }
2081
-
2082
- //#endregion
2083
- //#region src/cartridge-services/deploy-cartridge.ts
2084
- /**
2085
- * Extract the filename (including extension) from a file path
2086
- *
2087
- * @param filePath - The full path to the file
2088
- * @returns The filename portion of the path (e.g., 'archive.zip' from '/path/to/archive.zip')
2089
- */
2090
- function getFilename(filePath) {
2091
- return path.basename(filePath);
2092
- }
2093
- /**
2094
- * Create a ZIP cartridge from a directory
2095
- *
2096
- * @param sourceDir - The directory to zip
2097
- * @param outputPath - The output ZIP file path (can be same as sourceDir)
2098
- * @returns Promise resolving when the ZIP file is created
2099
- */
2100
- async function zipCartridge(sourceDir, outputPath) {
2101
- const archive = archiver("zip", { zlib: { level: 9 } });
2102
- const output = fs$1.createWriteStream(outputPath);
2103
- archive.pipe(output);
2104
- archive.directory(sourceDir, false);
2105
- await archive.finalize();
2106
- }
2107
- /**
2108
- * Build the WebDAV endpoint URL for a file
2109
- *
2110
- * @param instance - The Commerce Cloud instance hostname
2111
- * @param path - The WebDAV path (e.g., 'Cartridges/local_metadata')
2112
- * @param file - The local file path (filename will be extracted)
2113
- * @returns The complete WebDAV endpoint URL
2114
- */
2115
- function buildWebdavEndpoint(instance, webdavPath, file) {
2116
- return `https://${instance}${WEBDAV_BASE}/${webdavPath}/${getFilename(file)}`;
2117
- }
2118
- /**
2119
- * Unzip an uploaded archive file on Commerce Cloud via WebDAV
2120
- *
2121
- * @param instance - The Commerce Cloud instance hostname
2122
- * @param path - The WebDAV path where the file was uploaded
2123
- * @param file - The local file path (used to determine the remote filename)
2124
- * @param basicAuth - Base64 encoded basic authentication credentials
2125
- * @returns Promise resolving to HTTP response and body from the unzip operation
2126
- */
2127
- async function unzip(instance, webdavPath, file, basicAuth) {
2128
- const endpoint = buildWebdavEndpoint(instance, webdavPath, file);
2129
- const opts = getWebdavOptions(instance, webdavPath, basicAuth, HTTP_METHODS.POST, {
2130
- method: WEBDAV_OPERATIONS.UNZIP,
2131
- target: WEBDAV_OPERATIONS.TARGET_CARTRIDGES
2132
- });
2133
- opts.uri = endpoint;
2134
- const result = await makeRequest(opts);
2135
- checkAuthenticationError(result.response);
2136
- return result;
2137
- }
2138
- /**
2139
- * Delete a file from Commerce Cloud via WebDAV
2140
- *
2141
- * @param instance - The Commerce Cloud instance hostname
2142
- * @param path - The WebDAV path where the file is located
2143
- * @param file - The local file path (used to determine the remote filename)
2144
- * @param basicAuth - Base64 encoded basic authentication credentials
2145
- * @returns Promise resolving to HTTP response and body from the delete operation
2146
- */
2147
- async function deleteFile(instance, webdavPath, file, basicAuth) {
2148
- const endpoint = buildWebdavEndpoint(instance, webdavPath, file);
2149
- const opts = getWebdavOptions(instance, webdavPath, basicAuth, HTTP_METHODS.DELETE);
2150
- opts.uri = endpoint;
2151
- const result = await makeRequest(opts);
2152
- checkAuthenticationError(result.response);
2153
- return result;
2154
- }
2155
- /**
2156
- * Upload a file to a specific cartridge version on Commerce Cloud via WebDAV (internal function)
2157
- *
2158
- * @param instance - The Commerce Cloud instance hostname
2159
- * @param codeVersionName - The target code version name
2160
- * @param filePath - The local file path to upload
2161
- * @param basicAuth - Base64 encoded basic authentication credentials
2162
- * @returns Promise resolving to HTTP response and body from the upload operation
2163
- */
2164
- async function postFile(instance, codeVersionName, filePath, basicAuth) {
2165
- const targetPath = `${CARTRIDGES_PATH}/${codeVersionName}`;
2166
- try {
2167
- const endpoint = buildWebdavEndpoint(instance, targetPath, filePath);
2168
- const opts = getWebdavOptions(instance, targetPath, basicAuth, HTTP_METHODS.PUT);
2169
- opts.uri = endpoint;
2170
- opts.body = fs$1.createReadStream(filePath);
2171
- opts.duplex = "half";
2172
- opts.headers = {
2173
- ...opts.headers,
2174
- "Content-Type": CONTENT_TYPES.APPLICATION_ZIP
2175
- };
2176
- const result = await makeRequest(opts);
2177
- checkAuthenticationError(result.response);
2178
- if (![
2179
- 200,
2180
- 201,
2181
- 204
2182
- ].includes(result.response.statusCode)) throw new Error(`Post file "${filePath}" failed: ${result.response.statusCode} (${result.response.statusMessage})`);
2183
- return result;
2184
- } catch (error$1) {
2185
- throw new Error(`Post file "${filePath}" failed: ${error$1 instanceof Error ? error$1.message : String(error$1)}`);
2186
- }
2187
- }
2188
- /**
2189
- * Deploy code to Commerce Cloud by uploading, unzipping, and cleaning up
2190
- *
2191
- * This function performs a complete code deployment workflow:
2192
- * 1. Uploads the archive file via WebDAV to the specified cartridge version
2193
- * 2. Unzips the archive on the server
2194
- * 3. Deletes the uploaded archive file
2195
- * 4. Returns the deployed version name
2196
- *
2197
- * @param instance - The Commerce Cloud instance hostname
2198
- * @param codeVersionName - The target code version name
2199
- * @param sourceDir - The local directory containing the source files to deploy
2200
- * @param basicAuth - Base64 encoded basic authentication credentials
2201
- * @returns Promise resolving to deployment result with the version name
2202
- * @throws Error if any step of the deployment process fails
2203
- */
2204
- async function deployCode(instance, codeVersionName, sourceDir, basicAuth) {
2205
- validateDeployCodeParams(instance, codeVersionName, sourceDir, basicAuth, `/${CARTRIDGES_PATH}/${codeVersionName}/cartridges`);
2206
- const tempZipPath = path.join(path.dirname(sourceDir), `metadata-${Date.now()}.zip`);
2207
- try {
2208
- await zipCartridge(sourceDir, tempZipPath);
2209
- const file = path.basename(tempZipPath);
2210
- await postFile(instance, codeVersionName, tempZipPath, basicAuth);
2211
- const unzipResult = await unzip(instance, `${CARTRIDGES_PATH}/${codeVersionName}`, file, basicAuth);
2212
- if (![
2213
- 200,
2214
- 201,
2215
- 202
2216
- ].includes(unzipResult.response.statusCode)) throw new Error(`Deploy code ${file} failed (unzip step): ${unzipResult.response.statusCode} (${unzipResult.response.statusMessage})`);
2217
- const deleteResult = await deleteFile(instance, `${CARTRIDGES_PATH}/${codeVersionName}`, file, basicAuth);
2218
- if (![200, 204].includes(deleteResult.response.statusCode)) throw new Error(`Delete ZIP file ${file} after deployment failed (deleteFile step): ${deleteResult.response.statusCode} (${deleteResult.response.statusMessage})`);
2219
- return { version: getFilename(file).replace(".zip", "") };
2220
- } catch (error$1) {
2221
- if (error$1 instanceof Error) throw error$1;
2222
- throw new Error(`Deploy code ${sourceDir} failed: ${String(error$1)}`);
2223
- } finally {
2224
- if (fs$1.existsSync(tempZipPath)) fs$1.unlinkSync(tempZipPath);
2225
- }
2226
- }
2227
-
2228
- //#endregion
2229
- //#region src/extensibility/path-util.ts
2230
- const FILE_EXTENSIONS = [
2231
- ".tsx",
2232
- ".ts",
2233
- ".d.ts"
2234
- ];
2235
- function isSupportedFileExtension(fileName) {
2236
- return FILE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
2237
- }
2238
-
2239
- //#endregion
2240
- //#region src/extensibility/trim-extensions.ts
2241
- const SINGLE_LINE_MARKER = "@sfdc-extension-line";
2242
- const BLOCK_MARKER_START = "@sfdc-extension-block-start";
2243
- const BLOCK_MARKER_END = "@sfdc-extension-block-end";
2244
- const FILE_MARKER = "@sfdc-extension-file";
2245
- let verbose = false;
2246
- function trimExtensions(directory, selectedExtensions, extensionConfig, verboseOverride = false) {
2247
- const startTime = Date.now();
2248
- verbose = verboseOverride ?? false;
2249
- const configuredExtensions = extensionConfig?.extensions || {};
2250
- const extensions = {};
2251
- Object.keys(configuredExtensions).forEach((pluginKey) => {
2252
- extensions[pluginKey] = Boolean(selectedExtensions?.[pluginKey]) || false;
2253
- });
2254
- if (Object.keys(extensions).length === 0) {
2255
- if (verbose) console.log("No plugins found, skipping trim");
2256
- return;
2257
- }
2258
- const processDirectory = (dir) => {
2259
- fs$1.readdirSync(dir).forEach((file) => {
2260
- const filePath = path.join(dir, file);
2261
- const stats = fs$1.statSync(filePath);
2262
- if (!filePath.includes("node_modules")) {
2263
- if (stats.isDirectory()) processDirectory(filePath);
2264
- else if (isSupportedFileExtension(file)) processFile(filePath, extensions);
2265
- }
2266
- });
2267
- };
2268
- processDirectory(directory);
2269
- if (extensionConfig?.extensions) {
2270
- deleteExtensionFolders(directory, extensions, extensionConfig);
2271
- updateExtensionConfig(directory, extensions);
2272
- }
2273
- const endTime = Date.now();
2274
- if (verbose) console.log(`Trim extensions took ${endTime - startTime}ms`);
2275
- }
2276
- /**
2277
- * Update the extension config file to only include the selected extensions.
2278
- * @param projectDirectory - The project directory
2279
- * @param extensionSelections - The selected extensions
2280
- */
2281
- function updateExtensionConfig(projectDirectory, extensionSelections) {
2282
- const extensionConfigPath = path.join(projectDirectory, "src", "extensions", "config.json");
2283
- const extensionConfig = JSON.parse(fs$1.readFileSync(extensionConfigPath, "utf8"));
2284
- Object.keys(extensionConfig.extensions).forEach((extensionKey) => {
2285
- if (!extensionSelections[extensionKey]) delete extensionConfig.extensions[extensionKey];
2286
- });
2287
- fs$1.writeFileSync(extensionConfigPath, JSON.stringify({ extensions: extensionConfig.extensions }, null, 4), "utf8");
2288
- }
2289
- /**
2290
- * Process a file to trim extension-specific code based on markers.
2291
- * @param filePath - The file path to process
2292
- * @param extensions - The extension selections
2293
- */
2294
- function processFile(filePath, extensions) {
2295
- const source = fs$1.readFileSync(filePath, "utf-8");
2296
- if (source.includes(FILE_MARKER)) {
2297
- const markerLine = source.split("\n").find((line) => line.includes(FILE_MARKER));
2298
- const extMatch = Object.keys(extensions).find((ext) => markerLine.includes(ext));
2299
- if (!extMatch) {
2300
- if (verbose) console.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
2301
- } else if (extensions[extMatch] === false) {
2302
- try {
2303
- fs$1.unlinkSync(filePath);
2304
- if (verbose) console.log(`Deleted file ${filePath}`);
2305
- } catch (e) {
2306
- const error$1 = e;
2307
- console.error(`Error deleting file ${filePath}: ${error$1.message}`);
2308
- throw e;
2309
- }
2310
- return;
2311
- }
2312
- }
2313
- const extKeys = Object.keys(extensions);
2314
- if (new RegExp(extKeys.join("|"), "g").test(source)) {
2315
- const lines = source.split("\n");
2316
- const newLines = [];
2317
- const blockMarkers = [];
2318
- let skippingBlock = false;
2319
- let i = 0;
2320
- while (i < lines.length) {
2321
- const line = lines[i];
2322
- if (line.includes(SINGLE_LINE_MARKER)) {
2323
- const matchingExtension = Object.keys(extensions).find((extension) => line.includes(extension));
2324
- if (matchingExtension && extensions[matchingExtension] === false) {
2325
- i += 2;
2326
- continue;
2327
- }
2328
- } else if (line.includes(BLOCK_MARKER_START)) {
2329
- const matchingExtension = Object.keys(extensions).find((extension) => line.includes(extension));
2330
- if (matchingExtension) {
2331
- blockMarkers.push({
2332
- extension: matchingExtension,
2333
- line: i
2334
- });
2335
- skippingBlock = extensions[matchingExtension] === false;
2336
- } else if (verbose) console.warn(`Warning: Unknown marker found in ${filePath} at line ${i}: \n${line}`);
2337
- } else if (line.includes(BLOCK_MARKER_END)) {
2338
- if (Object.keys(extensions).find((extension) => line.includes(extension))) {
2339
- const extension = Object.keys(extensions).find((p) => line.includes(p));
2340
- if (blockMarkers.length === 0) throw new Error(`Block marker mismatch in ${filePath}, encountered end marker ${extension} without a matching start marker at line ${i}:\n${lines[i]}`);
2341
- const startMarker = blockMarkers.pop();
2342
- if (!extension || startMarker.extension !== extension) throw new Error(`Block marker mismatch in ${filePath}, expected end marker for ${startMarker.extension} but got ${extension} at line ${i}:\n${lines[i]}`);
2343
- if (extensions[extension] === false) {
2344
- skippingBlock = false;
2345
- i++;
2346
- continue;
2347
- }
2348
- }
2349
- }
2350
- if (!skippingBlock) newLines.push(line);
2351
- i++;
2352
- }
2353
- if (blockMarkers.length > 0) throw new Error(`Unclosed end marker found in ${filePath}: ${blockMarkers[blockMarkers.length - 1].extension}`);
2354
- const newSource = newLines.join("\n");
2355
- if (newSource !== source) try {
2356
- fs$1.writeFileSync(filePath, newSource);
2357
- if (verbose) console.log(`Updated file ${filePath}`);
2358
- } catch (e) {
2359
- const error$1 = e;
2360
- console.error(`Error updating file ${filePath}: ${error$1.message}`);
2361
- throw e;
2362
- }
2363
- }
2364
- }
2365
- /**
2366
- * Delete extension folders for disabled extensions.
2367
- * @param projectRoot - The project root directory
2368
- * @param extensions - The extension selections
2369
- * @param extensionConfig - The extension configuration
2370
- */
2371
- function deleteExtensionFolders(projectRoot, extensions, extensionConfig) {
2372
- const extensionsDir = path.join(projectRoot, "src", "extensions");
2373
- if (!fs$1.existsSync(extensionsDir)) return;
2374
- const configuredExtensions = extensionConfig.extensions;
2375
- Object.keys(extensions).filter((ext) => extensions[ext] === false).forEach((extKey) => {
2376
- const extensionMeta = configuredExtensions[extKey];
2377
- if (extensionMeta?.folder) {
2378
- const extensionFolderPath = path.join(extensionsDir, extensionMeta.folder);
2379
- if (fs$1.existsSync(extensionFolderPath)) try {
2380
- fs$1.rmSync(extensionFolderPath, {
2381
- recursive: true,
2382
- force: true
2383
- });
2384
- if (verbose) console.log(`Deleted extension folder: ${extensionFolderPath}`);
2385
- } catch (err) {
2386
- const error$1 = err;
2387
- if (error$1.code === "EPERM") console.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
2388
- else console.error(`Error deleting ${extensionFolderPath}: ${error$1.message}`);
2389
- }
2390
- }
2391
- });
2392
- }
2393
-
2394
- //#endregion
2395
- //#region src/extensibility/dependency-utils.ts
2396
- /**
2397
- * Resolve full transitive dependency chain in topological order (dependencies first).
2398
- * Example: resolveDependencies('BOPIS', config) → ['Store Locator', 'BOPIS']
2399
- *
2400
- * @param extensionKey - The extension key to resolve dependencies for
2401
- * @param config - The extension configuration
2402
- * @returns Array of extension keys in topological order (dependencies first, then the extension itself)
2403
- */
2404
- function resolveDependencies(extensionKey, config) {
2405
- const visited = /* @__PURE__ */ new Set();
2406
- const result = [];
2407
- function visit(key) {
2408
- if (visited.has(key)) return;
2409
- visited.add(key);
2410
- const extension = config.extensions[key];
2411
- if (!extension) return;
2412
- const dependencies = extension.dependencies || [];
2413
- for (const dep of dependencies) visit(dep);
2414
- result.push(key);
2415
- }
2416
- visit(extensionKey);
2417
- return result;
2418
- }
2419
- /**
2420
- * Reverse lookup: find immediate extensions that depend on this one.
2421
- * Example: getDependents('Store Locator', config) → ['BOPIS']
2422
- *
2423
- * @param extensionKey - The extension key to find dependents for
2424
- * @param config - The extension configuration
2425
- * @returns Array of extension keys that directly depend on this extension
2426
- */
2427
- function getDependents(extensionKey, config) {
2428
- const dependents = [];
2429
- for (const [key, extension] of Object.entries(config.extensions)) if ((extension.dependencies || []).includes(extensionKey)) dependents.push(key);
2430
- return dependents;
2431
- }
2432
- /**
2433
- * Resolve full transitive dependent chain in reverse topological order (dependents first).
2434
- * Example: resolveDependents('Store Locator', config) → ['BOPIS', 'Store Locator']
2435
- *
2436
- * @param extensionKey - The extension key to resolve dependents for
2437
- * @param config - The extension configuration
2438
- * @returns Array of extension keys in reverse topological order (dependents first, then the extension itself)
2439
- */
2440
- function resolveDependents(extensionKey, config) {
2441
- const visited = /* @__PURE__ */ new Set();
2442
- const result = [];
2443
- function visit(key) {
2444
- if (visited.has(key)) return;
2445
- visited.add(key);
2446
- const dependents = getDependents(key, config);
2447
- for (const dep of dependents) visit(dep);
2448
- result.push(key);
2449
- }
2450
- visit(extensionKey);
2451
- return result;
2452
- }
2453
- /**
2454
- * Validate that no circular dependencies exist in the configuration.
2455
- * Throws a descriptive error if a cycle is found.
2456
- *
2457
- * @param config - The extension configuration to validate
2458
- * @throws Error if a circular dependency is detected
2459
- */
2460
- function validateNoCycles(config) {
2461
- const visiting = /* @__PURE__ */ new Set();
2462
- const visited = /* @__PURE__ */ new Set();
2463
- function visit(key, path$1) {
2464
- if (visited.has(key)) return;
2465
- if (visiting.has(key)) {
2466
- const cycleStart = path$1.indexOf(key);
2467
- const cyclePath = [...path$1.slice(cycleStart), key];
2468
- throw new Error(`Circular dependency detected: ${cyclePath.join(" -> ")}`);
2469
- }
2470
- visiting.add(key);
2471
- path$1.push(key);
2472
- const extension = config.extensions[key];
2473
- if (extension) {
2474
- const dependencies = extension.dependencies || [];
2475
- for (const dep of dependencies) visit(dep, path$1);
2476
- }
2477
- path$1.pop();
2478
- visiting.delete(key);
2479
- visited.add(key);
2480
- }
2481
- for (const key of Object.keys(config.extensions)) visit(key, []);
2482
- }
2483
- /**
2484
- * Filter resolved dependencies to only those not yet installed.
2485
- * Returns dependencies in topological order (install order).
2486
- *
2487
- * @param extensionKey - The extension key to check dependencies for
2488
- * @param installedExtensions - Array of already installed extension keys
2489
- * @param config - The extension configuration
2490
- * @returns Array of missing extension keys in topological order (install order)
2491
- */
2492
- function getMissingDependencies(extensionKey, installedExtensions, config) {
2493
- const allDependencies = resolveDependencies(extensionKey, config);
2494
- const installedSet = new Set(installedExtensions);
2495
- return allDependencies.filter((key) => !installedSet.has(key));
2496
- }
2497
- /**
2498
- * Resolve dependencies for multiple extensions, merging and deduplicating the results.
2499
- * Returns all dependencies in topological order.
2500
- *
2501
- * @param extensionKeys - Array of extension keys to resolve dependencies for
2502
- * @param config - The extension configuration
2503
- * @returns Array of all extension keys in topological order (dependencies first)
2504
- */
2505
- function resolveDependenciesForMultiple(extensionKeys, config) {
2506
- const allDeps = /* @__PURE__ */ new Set();
2507
- const result = [];
2508
- for (const key of extensionKeys) {
2509
- const deps = resolveDependencies(key, config);
2510
- for (const dep of deps) if (!allDeps.has(dep)) {
2511
- allDeps.add(dep);
2512
- result.push(dep);
2513
- }
2514
- }
2515
- return result;
2516
- }
2517
- /**
2518
- * Resolve dependents for multiple extensions, merging and deduplicating the results.
2519
- * Returns all dependents in reverse topological order (uninstall order).
2520
- *
2521
- * @param extensionKeys - Array of extension keys to resolve dependents for
2522
- * @param config - The extension configuration
2523
- * @returns Array of all extension keys in reverse topological order (dependents first)
2524
- */
2525
- function resolveDependentsForMultiple(extensionKeys, config) {
2526
- const allDeps = /* @__PURE__ */ new Set();
2527
- const result = [];
2528
- for (const key of extensionKeys) {
2529
- const deps = resolveDependents(key, config);
2530
- for (const dep of deps) if (!allDeps.has(dep)) {
2531
- allDeps.add(dep);
2532
- result.push(dep);
2533
- }
2534
- }
2535
- return result;
2536
- }
2537
-
2538
- //#endregion
2539
- //#region src/utils/local-dev-setup.ts
2540
- /**
2541
- * Prepares a cloned template for standalone use outside the monorepo.
2542
- * Prompts user for local package paths and replaces workspace:* dependencies with file: references.
2543
- */
2544
- async function prepareForLocalDev(options) {
2545
- const { projectDirectory, sourcePackagesDir } = options;
2546
- const packageJsonPath = path.join(projectDirectory, "package.json");
2547
- if (!fs.existsSync(packageJsonPath)) throw new Error(`package.json not found in ${projectDirectory}`);
2548
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
2549
- const workspaceDeps = [];
2550
- for (const depType of [
2551
- "dependencies",
2552
- "devDependencies",
2553
- "peerDependencies"
2554
- ]) {
2555
- const deps = packageJson[depType];
2556
- if (!deps) continue;
2557
- for (const [pkg, version$1] of Object.entries(deps)) if (typeof version$1 === "string" && version$1.startsWith("workspace:")) workspaceDeps.push({
2558
- pkg,
2559
- depType
2560
- });
2561
- }
2562
- if (workspaceDeps.length === 0) {
2563
- info("No workspace:* dependencies found. Project is ready for standalone use.");
2564
- return;
2565
- }
2566
- console.log("\n🔗 Found workspace dependencies that need to be linked to local packages:\n");
2567
- for (const { pkg } of workspaceDeps) console.log(` • ${pkg}`);
2568
- console.log("");
2569
- const defaultPaths = {};
2570
- if (sourcePackagesDir) {
2571
- defaultPaths["@salesforce/storefront-next-dev"] = path.join(sourcePackagesDir, "storefront-next-dev");
2572
- defaultPaths["@salesforce/storefront-next-runtime"] = path.join(sourcePackagesDir, "storefront-next-runtime");
2573
- }
2574
- const resolvedPaths = {};
2575
- for (const { pkg } of workspaceDeps) {
2576
- if (resolvedPaths[pkg]) continue;
2577
- const defaultPath = defaultPaths[pkg] || "";
2578
- const defaultExists = defaultPath && fs.existsSync(defaultPath);
2579
- const { localPath } = await prompts({
2580
- type: "text",
2581
- name: "localPath",
2582
- message: `📦 Path to ${pkg}:`,
2583
- initial: defaultExists ? defaultPath : "",
2584
- validate: (value) => {
2585
- if (!value) return "Path is required";
2586
- if (!fs.existsSync(value)) return `Directory not found: ${value}`;
2587
- if (!fs.existsSync(path.join(value, "package.json"))) return `No package.json found in: ${value}`;
2588
- return true;
2589
- }
2590
- });
2591
- if (!localPath) {
2592
- warn(`Skipping ${pkg} - no path provided`);
2593
- continue;
2594
- }
2595
- resolvedPaths[pkg] = localPath;
2596
- }
2597
- let modified = false;
2598
- for (const depType of [
2599
- "dependencies",
2600
- "devDependencies",
2601
- "peerDependencies"
2602
- ]) {
2603
- const deps = packageJson[depType];
2604
- if (!deps) continue;
2605
- for (const [pkg, version$1] of Object.entries(deps)) if (typeof version$1 === "string" && version$1.startsWith("workspace:")) {
2606
- const localPath = resolvedPaths[pkg];
2607
- if (localPath) {
2608
- const fileRef = `file:${localPath}`;
2609
- info(`Linked ${pkg} → ${fileRef}`);
2610
- deps[pkg] = fileRef;
2611
- modified = true;
2612
- } else {
2613
- warn(`Removing unresolved workspace dependency: ${pkg}`);
2614
- delete deps[pkg];
2615
- modified = true;
2616
- }
2617
- }
2618
- }
2619
- if (packageJson.volta?.extends) {
2620
- delete packageJson.volta.extends;
2621
- if (Object.keys(packageJson.volta).length === 0) delete packageJson.volta;
2622
- modified = true;
2623
- }
2624
- if (modified) {
2625
- fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 4)}\n`);
2626
- success("package.json updated with local package links");
2627
- patchViteConfigForLinkedPackages(projectDirectory, Object.keys(resolvedPaths));
2628
- }
2629
- }
2630
- /**
2631
- * Patches vite.config.ts to fix "You must render this element inside a <HydratedRouter>" errors
2632
- * that occur when using file: linked packages.
2633
- *
2634
- * The fix adds:
2635
- * 1. resolve.dedupe for react, react-dom, react-router (helps with non-linked duplicates)
2636
- * 2. ssr.noExternal for file-linked packages (key fix - bundles them so they use host's dependencies)
2637
- *
2638
- * When packages are in ssr.noExternal, Vite bundles them during SSR instead of externalizing.
2639
- * During bundling, their imports resolve through the host project's node_modules,
2640
- * ensuring all code uses the same react-router instance with the same context.
2641
- */
2642
- function patchViteConfigForLinkedPackages(projectDirectory, linkedPackages) {
2643
- const viteConfigPath = path.join(projectDirectory, "vite.config.ts");
2644
- if (!fs.existsSync(viteConfigPath)) {
2645
- warn("vite.config.ts not found, skipping patch for file-linked packages");
2646
- return;
2647
- }
2648
- if (linkedPackages.length === 0) return;
2649
- let viteConfig = fs.readFileSync(viteConfigPath, "utf8");
2650
- let modified = false;
2651
- if (!viteConfig.includes("dedupe:")) {
2652
- const resolveMatch = viteConfig.match(/resolve:\s*\{/);
2653
- if (resolveMatch && resolveMatch.index !== void 0) {
2654
- const insertPos = resolveMatch.index + resolveMatch[0].length;
2655
- viteConfig = viteConfig.slice(0, insertPos) + `
2656
- // Deduplicates packages to prevent context issues with file-linked packages
2657
- dedupe: ['react', 'react-dom', 'react-router'],` + viteConfig.slice(insertPos);
2658
- modified = true;
2659
- }
2660
- }
2661
- const packageList = linkedPackages.map((p) => `'${p}'`).join(", ");
2662
- if (/ssr:\s*\{[^}]*noExternal:/.test(viteConfig)) {
2663
- const noExternalArrayRegex = /noExternal:\s*\[([^\]]*)\]/;
2664
- const noExternalMatch = viteConfig.match(noExternalArrayRegex);
2665
- if (noExternalMatch) {
2666
- const existingPackages = noExternalMatch[1];
2667
- const packagesToAdd = linkedPackages.filter((p) => !existingPackages.includes(p));
2668
- if (packagesToAdd.length > 0) {
2669
- const newPackageList = packagesToAdd.map((p) => `'${p}'`).join(", ");
2670
- const newArray = existingPackages.trim() ? `[${existingPackages.trim()}, ${newPackageList}]` : `[${newPackageList}]`;
2671
- viteConfig = viteConfig.replace(noExternalArrayRegex, `noExternal: ${newArray}`);
2672
- modified = true;
2673
- }
2674
- }
2675
- } else {
2676
- const ssrMatch = viteConfig.match(/ssr:\s*\{/);
2677
- if (ssrMatch && ssrMatch.index !== void 0) {
2678
- const insertPos = ssrMatch.index + ssrMatch[0].length;
2679
- const noExternalBlock = `
2680
- // Bundle file-linked packages so they use host project's dependencies
2681
- // This prevents "You must render this element inside a <HydratedRouter>" errors
2682
- noExternal: [${packageList}],`;
2683
- viteConfig = viteConfig.slice(0, insertPos) + noExternalBlock + viteConfig.slice(insertPos);
2684
- modified = true;
2685
- } else {
2686
- const returnMatch = viteConfig.match(/return\s*\{/);
2687
- if (returnMatch && returnMatch.index !== void 0) {
2688
- const insertPos = returnMatch.index + returnMatch[0].length;
2689
- const ssrBlock = `
2690
- // SSR config for file-linked packages
2691
- ssr: {
2692
- // Bundle file-linked packages so they use host project's dependencies
2693
- // This prevents "You must render this element inside a <HydratedRouter>" errors
2694
- noExternal: [${packageList}],
2695
- target: 'node',
2696
- },`;
2697
- viteConfig = viteConfig.slice(0, insertPos) + ssrBlock + viteConfig.slice(insertPos);
2698
- modified = true;
2699
- }
2700
- }
2701
- }
2702
- if (modified) {
2703
- fs.writeFileSync(viteConfigPath, viteConfig);
2704
- success("vite.config.ts patched for file-linked packages (ssr.noExternal + resolve.dedupe)");
2705
- } else info("vite.config.ts already configured for file-linked packages");
2706
- }
2707
-
2708
- //#endregion
2709
- //#region src/create-storefront.ts
2710
- const DEFAULT_STOREFRONT = "sfcc-storefront";
2711
- const STOREFRONT_NEXT_GITHUB_URL = "https://github.com/SalesforceCommerceCloud/storefront-next-template";
2712
- const createStorefront = async (options = {}) => {
2713
- try {
2714
- execSync("git --version", { stdio: "ignore" });
2715
- } catch (e) {
2716
- error(`❌ git isn't installed or found in your PATH. Install git before running this command: ${String(e)}`);
2717
- process.exit(1);
2718
- }
2719
- let storefront = options.name;
2720
- if (!storefront) storefront = (await prompts({
2721
- type: "text",
2722
- name: "storefront",
2723
- message: "🏪 What would you like to name your storefront?\n",
2724
- initial: DEFAULT_STOREFRONT
2725
- })).storefront;
2726
- if (!storefront) {
2727
- error("Storefront name is required.");
2728
- process.exit(1);
2729
- }
2730
- console.log("\n");
2731
- let template = options.template;
2732
- if (!template) {
2733
- template = (await prompts({
2734
- type: "select",
2735
- name: "template",
2736
- message: "📄 Which template would you like to use for your storefront?\n",
2737
- choices: [{
2738
- title: "Salesforce B2C Commerce Retail Storefront",
2739
- value: STOREFRONT_NEXT_GITHUB_URL
2740
- }, {
2741
- title: "A different template (I will provide the Github URL)",
2742
- value: "custom"
2743
- }]
2744
- })).template;
2745
- console.log("\n");
2746
- if (template === "custom") {
2747
- const { githubUrl } = await prompts({
2748
- type: "text",
2749
- name: "githubUrl",
2750
- message: "🌐 What is the Github URL for your template?\n"
2751
- });
2752
- if (!githubUrl) {
2753
- error("Github URL is required.");
2754
- process.exit(1);
2755
- }
2756
- template = githubUrl;
2757
- }
2758
- }
2759
- if (!template) {
2760
- error("Template is required.");
2761
- process.exit(1);
2762
- }
2763
- execSync(`git clone --depth 1 ${template} ${storefront}`);
2764
- const gitDir = path.join(storefront, ".git");
2765
- if (fs.existsSync(gitDir)) fs.rmSync(gitDir, {
2766
- recursive: true,
2767
- force: true
2768
- });
2769
- if (template.startsWith("file://") || options.localPackagesDir) {
2770
- const templatePath = template.replace("file://", "");
2771
- const sourcePackagesDir = options.localPackagesDir || path.dirname(templatePath);
2772
- await prepareForLocalDev({
2773
- projectDirectory: storefront,
2774
- sourcePackagesDir
2775
- });
2776
- }
2777
- console.log("\n");
2778
- if (fs.existsSync(path.join(storefront, "src", "extensions", "config.json"))) {
2779
- const extensionConfigText = fs.readFileSync(path.join(storefront, "src", "extensions", "config.json"), "utf8");
2780
- const extensionConfig = JSON.parse(extensionConfigText);
2781
- if (extensionConfig.extensions) {
2782
- try {
2783
- validateNoCycles(extensionConfig);
2784
- } catch (e) {
2785
- error(`Extension configuration error: ${e.message}`);
2786
- process.exit(1);
2787
- }
2788
- const { selectedExtensions } = await prompts({
2789
- type: "multiselect",
2790
- name: "selectedExtensions",
2791
- message: "🔌 Which extension would you like to enable? (Use arrow keys to select, space to toggle, and enter to confirm.)\n",
2792
- choices: Object.keys(extensionConfig.extensions).map((extension) => ({
2793
- title: `${extensionConfig.extensions[extension].name} - ${extensionConfig.extensions[extension].description}`,
2794
- value: extension,
2795
- selected: extensionConfig.extensions[extension].defaultOn ?? true
2796
- })),
2797
- instructions: false
2798
- });
2799
- const resolvedExtensions = resolveDependenciesForMultiple(selectedExtensions, extensionConfig);
2800
- const selectedSet = new Set(selectedExtensions);
2801
- const autoAdded = resolvedExtensions.filter((ext) => !selectedSet.has(ext));
2802
- if (autoAdded.length > 0) for (const addedExt of autoAdded) {
2803
- const dependentExts = selectedExtensions.filter((selected) => {
2804
- return (extensionConfig.extensions[selected]?.dependencies || []).includes(addedExt) || resolvedExtensions.indexOf(addedExt) < resolvedExtensions.indexOf(selected);
2805
- });
2806
- if (dependentExts.length > 0) {
2807
- const addedName = extensionConfig.extensions[addedExt]?.name || addedExt;
2808
- warn(`${dependentExts.map((ext) => extensionConfig.extensions[ext]?.name || ext).join(", ")} requires ${addedName}. ${addedName} has been automatically added.`);
2809
- }
2810
- }
2811
- const enabledExtensions = Object.fromEntries(resolvedExtensions.map((ext) => [ext, true]));
2812
- trimExtensions(storefront, enabledExtensions, { extensions: extensionConfig.extensions }, options?.verbose || false);
2813
- }
2814
- }
2815
- const configMeta = JSON.parse(fs.readFileSync(path.join(storefront, "src", "config", "config-meta.json"), "utf8"));
2816
- const envDefaultPath = path.join(storefront, ".env.default");
2817
- let envDefaultValues = {};
2818
- if (fs.existsSync(envDefaultPath)) envDefaultValues = dotenv.parse(fs.readFileSync(envDefaultPath, "utf8"));
2819
- console.log("\n⚙️ We will now configure your storefront before it will be ready to run.\n");
2820
- const configOverrides = {};
2821
- for (const config of configMeta.configs) {
2822
- const answer = await prompts({
2823
- type: "text",
2824
- name: config.key,
2825
- message: `What is the value for ${config.name}? (default: ${envDefaultValues[config.key]})\n`,
2826
- initial: envDefaultValues[config.key] ?? ""
2827
- });
2828
- configOverrides[config.key] = answer[config.key];
2829
- }
2830
- generateEnvFile(storefront, configOverrides);
2831
- const BANNER = `
2832
- ╔══════════════════════════════════════════════════════════════════╗
2833
- ║ CONGRATULATIONS ║
2834
- ╚══════════════════════════════════════════════════════════════════╝
2835
-
2836
- 🎉 Congratulations! Your storefront is ready to use! 🎉
2837
- What's next:
2838
- - Navigate to the storefront directory: cd ${storefront}
2839
- - Install dependencies: pnpm install
2840
- - Build the storefront: pnpm run build
2841
- - Run the development server: pnpm run dev
2842
- `;
2843
- console.log(BANNER);
2844
- };
2845
-
2846
- //#endregion
2847
- //#region src/extensibility/manage-extensions.ts
2848
- const EXTENSIONS_DIR = ["src", "extensions"];
2849
- const CONFIG_PATH = [...EXTENSIONS_DIR, "config.json"];
2850
- const EXTENSION_FOLDERS = [
2851
- "components",
2852
- "locales",
2853
- "hooks",
2854
- "routes"
2855
- ];
2856
- /**
2857
- * Console log a message with a specific type
2858
- * @param message string
2859
- * @param type
2860
- */
2861
- const consoleLog = (message, type) => {
2862
- switch (type) {
2863
- case "error":
2864
- console.error(`❌ ${message}`);
2865
- break;
2866
- case "success":
2867
- console.log(`✅ ${message}`);
2868
- break;
2869
- default:
2870
- console.log(message);
2871
- break;
2872
- }
2873
- };
2874
- /**
2875
- * Get the path to the extension config file
2876
- */
2877
- const getExtensionConfigPath = (projectDirectory) => {
2878
- return path.join(projectDirectory, ...CONFIG_PATH);
2879
- };
2880
- /**
2881
- * Check if the project directory contains the extensions directory and config.json file
2882
- */
2883
- const getExtensionConfig = (projectDirectory) => {
2884
- const extensionConfigPath = getExtensionConfigPath(projectDirectory);
2885
- if (!fs.existsSync(extensionConfigPath)) {
2886
- consoleLog(`Extension config file not found: ${extensionConfigPath}. Are you running this command in the correct project directory?`, "error");
2887
- process.exit(1);
2888
- }
2889
- return JSON.parse(fs.readFileSync(extensionConfigPath, "utf8")).extensions;
2890
- };
2891
- /**
2892
- * Common function to get the extension selection from the user
2893
- * @param type 'multiselect' | 'select'
2894
- * @param extensionConfig Record<string, ExtensionMeta>
2895
- * @param message string
2896
- * @param installedExtensions string[]
2897
- * @param excludeExtensions string[] extensions to exclude from the list, so we can filter out extensions that are already installed
2898
- * @returns string[]
2899
- */
2900
- const getExtensionSelection = async (type, extensionConfig, message, installedExtensions, excludeExtensions = []) => {
2901
- consoleLog("\n", "info");
2902
- const { selectedExtensions } = await prompts({
2903
- type,
2904
- name: "selectedExtensions",
2905
- message,
2906
- choices: installedExtensions.filter((extensionKey) => !excludeExtensions.includes(extensionKey)).map((extensionKey) => ({
2907
- title: `${extensionConfig[extensionKey].name} - ${extensionConfig[extensionKey].description}`,
2908
- value: extensionKey
2909
- })),
2910
- instructions: false
2911
- });
2912
- return type === "multiselect" ? selectedExtensions : [selectedExtensions];
2913
- };
2914
- /**
2915
- * Handle the uninstallation of extensions
2916
- * @param extensionConfig Record<string, ExtensionMeta>
2917
- * @param options {
2918
- projectDirectory: string;
2919
- extensions?: string[];
2920
- verbose?: boolean;
2921
- }
2922
- * @returns void
2923
- */
2924
- const handleUninstall = async (extensionConfig, options) => {
2925
- let installedExtensions = Object.keys(extensionConfig);
2926
- if (installedExtensions.length === 0) {
2927
- consoleLog("\n You have not installed any extensions yet.", "error");
2928
- return;
2929
- }
2930
- const selectedExtensions = options.extensions ? options.extensions : await getExtensionSelection("multiselect", extensionConfig, "🔌 Which extensions would you like to uninstall?", installedExtensions);
2931
- if (selectedExtensions == null || selectedExtensions.length === 0) {
2932
- consoleLog("\n Please select at least one extension to uninstall.", "error");
2933
- return;
2934
- }
2935
- const allToUninstall = resolveDependentsForMultiple(selectedExtensions, { extensions: extensionConfig });
2936
- const installedSet = new Set(installedExtensions);
2937
- const extensionsToUninstall = allToUninstall.filter((key) => installedSet.has(key));
2938
- const selectedSet = new Set(selectedExtensions);
2939
- const additionalDependents = extensionsToUninstall.filter((key) => !selectedSet.has(key));
2940
- if (additionalDependents.length > 0) {
2941
- consoleLog("\n", "info");
2942
- consoleLog(`Uninstalling the selected extension(s) will also uninstall the following dependent extensions:`, "info");
2943
- additionalDependents.forEach((depKey) => {
2944
- const depExtension = extensionConfig[depKey];
2945
- const dependsOn = selectedExtensions.find((selKey) => {
2946
- return extensionConfig[selKey] && extensionConfig[depKey]?.dependencies?.includes(selKey);
2947
- });
2948
- const dependsOnName = dependsOn ? extensionConfig[dependsOn]?.name : "selected extension";
2949
- consoleLog(` • ${depExtension?.name || depKey} (depends on ${dependsOnName})`, "info");
2950
- });
2951
- consoleLog("\n", "info");
2952
- const { confirmUninstall } = await prompts({
2953
- type: "confirm",
2954
- name: "confirmUninstall",
2955
- message: `Uninstall all ${extensionsToUninstall.length} extensions?`,
2956
- initial: true
2957
- });
2958
- if (!confirmUninstall) {
2959
- consoleLog("Uninstallation aborted.", "info");
2960
- return;
2961
- }
2962
- }
2963
- extensionsToUninstall.forEach((ext) => {
2964
- if (extensionConfig[ext]?.folder) fs.rmSync(path.join(options.projectDirectory, ...EXTENSIONS_DIR, extensionConfig[ext].folder), {
2965
- recursive: true,
2966
- force: true
2967
- });
2968
- });
2969
- const extensionsToUninstallSet = new Set(extensionsToUninstall);
2970
- installedExtensions = installedExtensions.filter((ext) => !extensionsToUninstallSet.has(ext));
2971
- trimExtensions(options.projectDirectory, Object.fromEntries(installedExtensions.map((ext) => [ext, true])), { extensions: extensionConfig }, options.verbose ?? false);
2972
- consoleLog(" Extensions uninstalled.", "success");
2973
- };
2974
- /**
2975
- * Install a single extension (internal helper)
2976
- * @returns true if installation succeeded, false otherwise
2977
- */
2978
- const installSingleExtension = (extensionKey, srcExtensionConfig, extensionConfig, tmpDir, projectDirectory) => {
2979
- const extension = srcExtensionConfig[extensionKey];
2980
- const startTime = Date.now();
2981
- if (extension.folder) fs.copySync(path.join(tmpDir, ...EXTENSIONS_DIR, extension.folder), path.join(projectDirectory, ...EXTENSIONS_DIR, extension.folder));
2982
- if (extension.installationInstructions) {
2983
- console.log(`\n⏳ Installing ${extension.name}, this will take a few minutes...`);
2984
- try {
2985
- execSync(`cursor-agent -p --force 'Execute the steps specified in the installation instructions file: ${extension.installationInstructions}' --output-format text`, {
2986
- cwd: projectDirectory,
2987
- stdio: "inherit"
2988
- });
2989
- } catch (e) {
2990
- consoleLog(`Error installing ${extension.name}. ${e.message}`, "error");
2991
- return false;
2992
- }
2993
- }
2994
- extensionConfig[extensionKey] = extension;
2995
- fs.writeFileSync(getExtensionConfigPath(projectDirectory), JSON.stringify({ extensions: extensionConfig }, null, 4));
2996
- consoleLog(`${extension.name} was installed successfully. (${Date.now() - startTime}ms)`, "success");
2997
- return true;
2998
- };
2999
- /**
3000
- * Handle the installation of extensions
3001
- * @param extensionConfig
3002
- * @param options {
3003
- sourceGithubUrl?: string;
3004
- projectDirectory: string;
3005
- extensions?: string[];
3006
- verbose?: boolean;
3007
- }
3008
- * @returns
3009
- */
3010
- const handleInstall = async (extensionConfig, options) => {
3011
- const { sourceGitUrl } = await prompts({
3012
- type: "text",
3013
- name: "sourceGitUrl",
3014
- message: "🌐 What is the Git URL for the extensions project?",
3015
- initial: options.sourceGitUrl
3016
- });
3017
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `sfnext-extensions-${Date.now()}`));
3018
- execSync(`git clone ${sourceGitUrl} ${tmpDir}`);
3019
- const srcExtensionConfig = getExtensionConfig(tmpDir);
3020
- if (srcExtensionConfig == null || Object.keys(srcExtensionConfig).length === 0) {
3021
- consoleLog(`No extensions found in the source project, please check ${path.join(...CONFIG_PATH)} exists in ${sourceGitUrl} and contains at least one extension.`, "error");
3022
- return;
3023
- }
3024
- const selectedExtensions = options.extensions ? options.extensions : await getExtensionSelection("select", srcExtensionConfig, "🔌 Which extension would you like to install?", Object.keys(srcExtensionConfig), Object.keys(extensionConfig));
3025
- if (selectedExtensions == null || selectedExtensions.length !== 1 || selectedExtensions[0] == null) {
3026
- consoleLog("Please select exactly one extension to install.", "error");
3027
- return;
3028
- }
3029
- const extensionKey = selectedExtensions[0];
3030
- const extension = srcExtensionConfig[extensionKey];
3031
- if (Object.values(srcExtensionConfig).some((ext) => ext.installationInstructions)) try {
3032
- execSync("cursor-agent -v", { stdio: "ignore" });
3033
- } catch (e) {
3034
- consoleLog("This extension contains LLM instructions, please install cursor cli and try again. (https://cursor.com/docs/cli/overview)", "error");
3035
- return;
3036
- }
3037
- const srcConfig = { extensions: srcExtensionConfig };
3038
- const missingDeps = getMissingDependencies(extensionKey, Object.keys(extensionConfig), srcConfig);
3039
- const dependenciesToInstall = missingDeps.slice(0, -1);
3040
- let hasError = false;
3041
- try {
3042
- if (dependenciesToInstall.length > 0) {
3043
- consoleLog("\n", "info");
3044
- consoleLog(`Installing ${extension.name} requires the following dependencies:`, "info");
3045
- dependenciesToInstall.forEach((depKey) => {
3046
- const depExtension = srcExtensionConfig[depKey];
3047
- consoleLog(` • ${depExtension?.name || depKey} (not installed)`, "info");
3048
- });
3049
- consoleLog("\n", "info");
3050
- const estimatedMinutes = missingDeps.length * 5;
3051
- const { confirmInstall } = await prompts({
3052
- type: "confirm",
3053
- name: "confirmInstall",
3054
- message: `Install all ${missingDeps.length} extensions? (~${estimatedMinutes} minutes total)`,
3055
- initial: true
3056
- });
3057
- if (!confirmInstall) {
3058
- consoleLog("Installation aborted.", "info");
3059
- return;
3060
- }
3061
- }
3062
- for (const depKey of missingDeps) if (!installSingleExtension(depKey, srcExtensionConfig, extensionConfig, tmpDir, options.projectDirectory)) hasError = true;
3063
- } finally {
3064
- fs.rmSync(tmpDir, {
3065
- recursive: true,
3066
- force: true
3067
- });
3068
- }
3069
- const originalFiles = fs.readdirSync(path.join(options.projectDirectory, "src"), { recursive: true }).filter((file) => file.toString().endsWith(".original"));
3070
- if (originalFiles.length > 0) {
3071
- consoleLog("\n📄 The following files were modified. The original files are still available in the same location with the \".original\" extension.:", "info");
3072
- originalFiles.forEach((file) => {
3073
- consoleLog(`- ${file.toString().replace(".original", "")}`, "info");
3074
- });
3075
- }
3076
- if (!hasError) consoleLog("\n🚀 Installation completed successfully.", "info");
3077
- };
3078
- const manageExtensions = async (options) => {
3079
- if (options.install && options.uninstall) {
3080
- consoleLog("Please select either install or uninstall, not both.", "error");
3081
- return;
3082
- }
3083
- let operation = options.install ? "install" : options.uninstall ? "uninstall" : void 0;
3084
- const extensionConfig = getExtensionConfig(options.projectDirectory);
3085
- if (operation == null) operation = (await prompts({
3086
- type: "select",
3087
- name: "operation",
3088
- message: "🤔 What would you like to do?",
3089
- choices: [{
3090
- title: "Install extensions",
3091
- value: "install"
3092
- }, {
3093
- title: "Uninstall extensions",
3094
- value: "uninstall"
3095
- }]
3096
- })).operation;
3097
- if (operation === "uninstall") await handleUninstall(extensionConfig, options);
3098
- else await handleInstall(extensionConfig, options);
3099
- };
3100
- const getExtensionMarker = (val) => {
3101
- return `SFDC_EXT_${val.toUpperCase().replaceAll(" ", "_").replaceAll("-", "_")}`;
3102
- };
3103
- const getExtensionFolderName = (val) => {
3104
- return val.toLowerCase().replaceAll(" ", "-").trim();
3105
- };
3106
- const getExtensionNameSchema = (projectDirectory, extensionConfig) => {
3107
- return z.object({ name: z.string().regex(/^[a-zA-Z0-9 _-]+$/, { message: "Extension name can only contain alphanumeric characters, spaces, dashes, or underscores" }) }).superRefine((data, ctx) => {
3108
- if (extensionConfig[getExtensionMarker(data.name)]) ctx.addIssue({
3109
- code: z.ZodIssueCode.custom,
3110
- message: `Extension "${data.name}" already exists`
3111
- });
3112
- if (fs.existsSync(path.join(projectDirectory, ...EXTENSIONS_DIR, getExtensionFolderName(data.name)))) ctx.addIssue({
3113
- code: z.ZodIssueCode.custom,
3114
- message: `Extension directory ${getExtensionFolderName(data.name)} already exists`
3115
- });
3116
- });
3117
- };
3118
- const listExtensions = (options) => {
3119
- const extensionConfig = getExtensionConfig(options.projectDirectory);
3120
- consoleLog("The following extensions are installed:", "info");
3121
- Object.keys(extensionConfig).forEach((key) => {
3122
- consoleLog(`- ${extensionConfig[key].name}: ${extensionConfig[key].description}`, "info");
3123
- });
3124
- };
3125
- const createExtension = async (options) => {
3126
- const { projectDirectory, name, description } = options;
3127
- const extensionConfig = getExtensionConfig(projectDirectory);
3128
- let extensionName = name;
3129
- let extensionDescription = description;
3130
- if (extensionName == null || extensionName.trim() === "") extensionName = (await prompts({
3131
- type: "text",
3132
- name: "extensionName",
3133
- message: "What would you like to name the extension? (e.g., \"My Extension\")"
3134
- })).extensionName;
3135
- const result = getExtensionNameSchema(projectDirectory, extensionConfig).safeParse({ name: extensionName });
3136
- if (!result.success) {
3137
- const firstIssueMessage = result.error.issues?.[0]?.message;
3138
- consoleLog(firstIssueMessage, "error");
3139
- return;
3140
- }
3141
- if (extensionDescription == null || extensionDescription.trim() === "") extensionDescription = (await prompts({
3142
- type: "text",
3143
- name: "extensionDescription",
3144
- message: "How would you describe the extension?"
3145
- })).extensionDescription;
3146
- const folderName = getExtensionFolderName(extensionName);
3147
- const extensionFolderPath = path.join(projectDirectory, ...EXTENSIONS_DIR, folderName);
3148
- fs.mkdirSync(extensionFolderPath, { recursive: true });
3149
- EXTENSION_FOLDERS.forEach((folder) => {
3150
- fs.mkdirSync(path.join(extensionFolderPath, folder), { recursive: true });
3151
- });
3152
- fs.writeFileSync(path.join(extensionFolderPath, "README.md"), `# ${extensionName}\n\n${extensionDescription}`);
3153
- const marker = getExtensionMarker(extensionName);
3154
- extensionConfig[marker] = {
3155
- name: extensionName,
3156
- description: extensionDescription,
3157
- installationInstructions: "",
3158
- uninstallationInstructions: "",
3159
- folder: folderName,
3160
- dependencies: []
3161
- };
3162
- fs.writeFileSync(path.join(projectDirectory, ...CONFIG_PATH), JSON.stringify({ extensions: extensionConfig }, null, 4));
3163
- consoleLog(`Extension "${extensionName}" scaffolding was created successfully.`, "success");
3164
- };
3165
-
3166
- //#endregion
3167
- //#region src/cli.ts
3168
- const __dirname = dirname(fileURLToPath(import.meta.url));
3169
- function validateAndBuildPaths(options) {
3170
- if (!options.projectDirectory) {
3171
- error("--project-directory is required.");
3172
- process.exit(1);
3173
- }
3174
- if (!fs.existsSync(options.projectDirectory)) {
3175
- error(`Project directory doesn't exist: ${options.projectDirectory}`);
3176
- process.exit(1);
3177
- }
3178
- const cartridgeBaseDir = path.join(options.projectDirectory, CARTRIDGES_BASE_DIR);
3179
- const metadataDir = path.join(options.projectDirectory, CARTRIDGES_BASE_DIR, SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR);
3180
- return {
3181
- projectDirectory: options.projectDirectory,
3182
- cartridgeBaseDir,
3183
- metadataDir
3184
- };
3185
- }
3186
- /**
3187
- * Shared function to generate cartridge metadata
3188
- * Used by both the generate-cartridge command and the push command (when enabled)
3189
- */
3190
- async function runGenerateCartridge(projectDirectory) {
3191
- const { projectDirectory: validatedProjectDir, metadataDir } = validateAndBuildPaths({ projectDirectory });
3192
- if (!fs.existsSync(metadataDir)) {
3193
- info(`Creating metadata directory: ${metadataDir}`);
3194
- fs.mkdirSync(metadataDir, { recursive: true });
3195
- }
3196
- await generateMetadata(validatedProjectDir, metadataDir);
3197
- }
3198
- /**
3199
- * Shared function to deploy cartridge to Commerce Cloud
3200
- * Used by both the deploy-cartridge command and the push command (when enabled)
3201
- */
3202
- async function runDeployCartridge(projectDirectory) {
3203
- const dwJsonPath = path.join(__dirname, "..", "dw.json");
3204
- if (!fs.existsSync(dwJsonPath)) throw new Error(`The dw.json file not found in storefront-next-dev directory. Make sure dw.json exists at ${dwJsonPath}`);
3205
- const dwConfig = JSON.parse(fs.readFileSync(dwJsonPath, "utf8"));
3206
- const { cartridgeBaseDir, metadataDir } = validateAndBuildPaths({ projectDirectory });
3207
- if (!fs.existsSync(metadataDir)) throw new Error(`Metadata directory doesn't exist: ${metadataDir}. Run 'generate-cartridge' first.`);
3208
- if (!dwConfig.username || !dwConfig.password) throw new Error("Username and password are required in the dw.json file.");
3209
- const instance = dwConfig.hostname;
3210
- if (!instance) throw new Error("Instance is required. Add \"hostname\" to the dw.json file.");
3211
- const codeVersion = dwConfig["code-version"];
3212
- if (!codeVersion) throw new Error("Code version is required. Add \"code-version\" to the dw.json file.");
3213
- const credentials = `${dwConfig.username}:${dwConfig.password}`;
3214
- success(`Code deployed to version "${(await deployCode(instance, codeVersion, cartridgeBaseDir, Buffer.from(credentials).toString("base64"))).version}" successfully!`);
3215
- }
3216
- const program = new Command();
3217
- const DEFAULT_TEMPLATE_GIT_URL = process.env.DEFAULT_TEMPLATE_GIT_URL || "https://github.com/SalesforceCommerceCloud/storefront-next-template.git";
3218
- const handleCommandError = (label, err) => {
3219
- if (err instanceof Error) {
3220
- error(err.stack || err.message);
3221
- error(`${label} failed: ${err.message}`);
3222
- } else {
3223
- error(String(err));
3224
- error(`${label} failed`);
3225
- }
3226
- process.exit(1);
3227
- };
3228
- program.name("sfnext").description("Dev and build tools for Storefront Next.").version(version);
3229
- program.command("create-storefront").description("Create a storefront project.").option("-v --verbose", "Verbose mode").option("-n, --name <name>", "Name for the storefront (skips interactive prompt)").option("-t, --template <template>", "Template URL or path (e.g., file:///path/to/template or GitHub URL)").option("-l, --local-packages-dir <dir>", "Local monorepo packages directory for file:// templates (pre-fills dependency paths)").action(async (options) => {
3230
- try {
3231
- await createStorefront({
3232
- verbose: options.verbose,
3233
- name: options.name,
3234
- template: options.template,
3235
- localPackagesDir: options.localPackagesDir
3236
- });
3237
- } catch (err) {
3238
- handleCommandError("create-storefront", err);
3239
- }
3240
- });
3241
- program.command("prepare-local").description("Prepare a storefront project for local development with file-linked packages. Converts workspace:* dependencies to file: references and patches vite.config.ts.").option("-d, --project-directory <dir>", "Project directory to prepare", process.cwd()).option("-s, --source-packages-dir <dir>", "Source monorepo packages directory (for default path suggestions)").action(async (options) => {
3242
- try {
3243
- await prepareForLocalDev({
3244
- projectDirectory: options.projectDirectory,
3245
- sourcePackagesDir: options.sourcePackagesDir
3246
- });
3247
- process.exit(0);
3248
- } catch (err) {
3249
- handleCommandError("prepare-local", err);
3250
- }
3251
- });
3252
- program.command("push").description("Create and push bundle to Managed Runtime.").requiredOption("-d, --project-directory <dir>", "Project directory").option("-b, --build-directory <dir>", "Build directory to push (default: auto-detected)").option("-m, --message <message>", "Bundle message (default: git branch:commit)").option("-s, --project-slug <slug>", "Project slug - the unique identifier for your project on Managed Runtime (default: from .env MRT_PROJECT or package.json name.)").option("-t, --target <target>", "Deploy target environment (default: from .env MRT_TARGET).").option("-w, --wait", "Wait for deployment to complete.", false).option("--cloud-origin <origin>", "API origin", DEFAULT_CLOUD_ORIGIN).option("-c, --credentials-file <file>", "Credentials file location.").option("-u, --user <email>", "User email for Managed Runtime.").option("-k, --key <api-key>", "API key for Managed Runtime.").action(async (options) => {
3253
- try {
3254
- if (GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH) try {
3255
- info("Generating cartridge metadata before MRT push...");
3256
- await runGenerateCartridge(options.projectDirectory);
3257
- success("Cartridge metadata generated successfully!");
3258
- info("Deploying cartridge to Commerce Cloud...");
3259
- await runDeployCartridge(options.projectDirectory);
3260
- success("Cartridge deployed successfully!");
3261
- } catch (cartridgeError) {
3262
- error(`Warning: Failed to generate or deploy cartridge: ${cartridgeError.message}`);
3263
- }
3264
- await push({
3265
- projectDirectory: options.projectDirectory,
3266
- buildDirectory: options.buildDirectory,
3267
- message: options.message,
3268
- projectSlug: options.projectSlug,
3269
- target: options.target,
3270
- wait: options.wait,
3271
- cloudOrigin: options.cloudOrigin,
3272
- credentialsFile: options.credentialsFile,
3273
- user: options.user,
3274
- key: options.key
3275
- });
3276
- process.exit(0);
3277
- } catch (err) {
3278
- handleCommandError("Push", err);
3279
- }
3280
- });
3281
- program.command("dev").description("Start Vite development server with SSR.").option("-d, --project-directory <dir>", "Project directory (default: current directory).").option("-p, --port <port>", "Port number (default: 5173)", (val) => parseInt(val, 10)).action(async (options) => {
3282
- try {
3283
- await dev({
3284
- projectDirectory: options.projectDirectory,
3285
- port: options.port
3286
- });
3287
- } catch (err) {
3288
- handleCommandError("Dev", err);
3289
- }
3290
- });
3291
- program.command("preview").description("Start preview server with production build (auto-builds if needed).").option("-d, --project-directory <dir>", "Project directory (default: current directory).").option("-p, --port <port>", "Port number (default: 3000)", (val) => parseInt(val, 10)).action(async (options) => {
3292
- try {
3293
- await preview({
3294
- projectDirectory: options.projectDirectory,
3295
- port: options.port
3296
- });
3297
- } catch (err) {
3298
- handleCommandError("Serve", err);
3299
- }
3300
- });
3301
- program.command("create-instructions").description("Generate LLM instructions using prompt templating for installing and uninstalling Storefront Next feature extensions.").requiredOption("-d, --project-directory <dir>", "Project directory.").requiredOption("-c, --extension-config <config>", "Extension config JSON file location.").requiredOption("-e, --extension <extension>", "Extension marker value (e.g. SFDC_EXT_featureA).").option("-p, --template-repo <repo>", "Storefront template repo URL (default: https://github.com/SalesforceCommerceCloud/storefront-next-template.git)").option("-b, --branch <branch>", "Storefront template repo branch (default: main).").option("-f, --files <files...>", "Specific files to include (relative to project directory).").option("-o, --output-dir <dir>", "Output directory (default: ./instructions).").action((options) => {
3302
- try {
3303
- const baseDir = process.cwd();
3304
- const projectDirectory = path.resolve(baseDir, options.projectDirectory);
3305
- const extensionConfig = path.resolve(baseDir, options.extensionConfig);
3306
- const files = options.files ?? void 0;
3307
- generateInstructions(projectDirectory, options.extension, options.outputDir, options.templateRepo, options.branch, files, extensionConfig, `${__dirname}/extensibility/templates`);
3308
- process.exit(0);
3309
- } catch (err) {
3310
- handleCommandError("create-instructions", err);
3311
- }
3312
- });
3313
- const extensionsCommand = program.command("extensions").description("Manage features extensions for a storefront project.");
3314
- extensionsCommand.command("list").description("List all installed extensions.").option("-d, --project-directory <dir>", "Target project directory", process.cwd()).action((options) => {
3315
- try {
3316
- listExtensions(options);
3317
- } catch (err) {
3318
- handleCommandError("extensions list", err);
3319
- }
3320
- });
3321
- extensionsCommand.command("install").description("Install an extension.").option("-d, --project-directory <dir>", "Target project directory.", process.cwd()).option("-e, --extension <extension>", "Extension marker value (e.g. SFDC_EXT_STORE_LOCATOR).").option("-s, --source-git-url <url>", "Git URL of the source template project", DEFAULT_TEMPLATE_GIT_URL).option("-v, --verbose", "Verbose mode.").action(async (options) => {
3322
- try {
3323
- await manageExtensions({
3324
- projectDirectory: options.projectDirectory,
3325
- install: true,
3326
- extensions: options.extension ? [options.extension] : void 0,
3327
- sourceGitUrl: options.sourceGitUrl,
3328
- verbose: options.verbose
3329
- });
3330
- } catch (err) {
3331
- handleCommandError("extensions install", err);
3332
- }
3333
- });
3334
- extensionsCommand.command("remove").description("Remove one or more installed extensions.").option("-d, --project-directory <dir>", "Target project directory", process.cwd()).option("-e, --extensions <extensions>", "Comma-separated list of extension marker values (e.g. SFDC_EXT_STORE_LOCATOR,SFDC_EXT_INTERNAL_THEME_SWITCHER).").option("-v, --verbose", "Verbose mode.").action(async (options) => {
3335
- try {
3336
- await manageExtensions({
3337
- projectDirectory: options.projectDirectory,
3338
- uninstall: true,
3339
- extensions: options.extensions?.split(","),
3340
- verbose: options.verbose
3341
- });
3342
- } catch (err) {
3343
- handleCommandError("extensions remove", err);
3344
- }
3345
- });
3346
- extensionsCommand.command("create").description("Create an extension.").option("-p, --project-directory <projectDirectory>", "Target project directory", process.cwd()).option("-n, --name <name>", "Name of the extension to create, e.g., \"My Extension\".").option("-d, --description <description>", "Description of the extension.").action(async (options) => {
3347
- try {
3348
- await createExtension(options);
3349
- } catch (err) {
3350
- handleCommandError("extensions create", err);
3351
- }
3352
- });
3353
- program.command("create-bundle").description("Create a bundle from the build directory without pushing to Managed Runtime.").requiredOption("-d, --project-directory <dir>", "Project directory").option("-b, --build-directory <dir>", "Build directory to bundle (default: auto-detected)").option("-o, --output-directory <dir>", "Output directory for bundle files (default: .bundle)").option("-m, --message <message>", "Bundle message (default: git branch:commit)").option("-s, --project-slug <slug>", "Project slug - the unique identifier for your project on Managed Runtime (default: from .env MRT_PROJECT or package.json name.)").action(async (options) => {
3354
- try {
3355
- await createBundleCommand({
3356
- projectDirectory: options.projectDirectory,
3357
- buildDirectory: options.buildDirectory,
3358
- outputDirectory: options.outputDirectory,
3359
- message: options.message,
3360
- projectSlug: options.projectSlug
3361
- });
3362
- process.exit(0);
3363
- } catch (err) {
3364
- handleCommandError("create-bundle", err);
3365
- }
3366
- });
3367
- program.command("generate-cartridge").description("Generate component cartridge metadata from decorated components.").requiredOption("-d, --project-directory <dir>", "Project directory containing the source code.").action(async (options) => {
3368
- try {
3369
- await runGenerateCartridge(options.projectDirectory);
3370
- process.exit(0);
3371
- } catch (err) {
3372
- error(`Generate metadata failed: ${err.message}`);
3373
- process.exit(1);
3374
- }
3375
- });
3376
- program.command("deploy-cartridge").description("Deploy a cartridge to Commerce Cloud (zips and uploads the metadata directory).").requiredOption("-d, --project-directory <dir>", "Project directory containing the source code.").action(async (options) => {
3377
- try {
3378
- await runDeployCartridge(options.projectDirectory);
3379
- process.exit(0);
3380
- } catch (err) {
3381
- error(`Deploy failed: ${err.message}`);
3382
- process.exit(1);
3383
- }
3384
- });
3385
- process.on("unhandledRejection", (reason, promise) => {
3386
- error(`Unhandled Rejection at: ${String(promise)}, reason: ${String(reason)}`);
3387
- process.exit(1);
3388
- });
3389
- program.parse();
3390
- if (!process.argv.slice(2).length) program.outputHelp();
3391
-
3392
- //#endregion
3393
- export { };