@salesforce/storefront-next-dev 0.4.1 → 1.0.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.
@@ -0,0 +1,225 @@
1
+ import { i as isBuiltInClientKey, r as deriveClientKey } from "../../schema-utils.js";
2
+ import { OAuthCommand } from "@salesforce/b2c-tooling-sdk/cli";
3
+ import { createScapiSchemasClient, toOrganizationId } from "@salesforce/b2c-tooling-sdk/clients";
4
+
5
+ //#region src/commands/scapi/available.ts
6
+ /**
7
+ * Survey SCAPI schemas on the active tenant to plan override testing. Lists:
8
+ * - Built-in shopper APIs (always available platform-side; overridable by sfnext scapi)
9
+ * along with how many `c_*` custom attributes each currently has on this tenant.
10
+ * - Tenant-registered custom SCAPI APIs (from the Schemas API endpoint).
11
+ *
12
+ * Authentication reuses the same pipeline as `sfnext scapi add`.
13
+ */
14
+ const BUILT_IN_APIS = [
15
+ {
16
+ apiFamily: "product",
17
+ apiName: "shopper-availability",
18
+ apiVersion: "v1",
19
+ clientKey: "shopperAvailability"
20
+ },
21
+ {
22
+ apiFamily: "checkout",
23
+ apiName: "shopper-baskets",
24
+ apiVersion: "v1",
25
+ clientKey: "shopperBasketsV1"
26
+ },
27
+ {
28
+ apiFamily: "checkout",
29
+ apiName: "shopper-baskets",
30
+ apiVersion: "v2",
31
+ clientKey: "shopperBasketsV2"
32
+ },
33
+ {
34
+ apiFamily: "configuration",
35
+ apiName: "shopper-configurations",
36
+ apiVersion: "v1",
37
+ clientKey: "shopperConfigurations"
38
+ },
39
+ {
40
+ apiFamily: "shopper",
41
+ apiName: "shopper-consents",
42
+ apiVersion: "v1",
43
+ clientKey: "shopperConsents"
44
+ },
45
+ {
46
+ apiFamily: "shopper",
47
+ apiName: "shopper-context",
48
+ apiVersion: "v1",
49
+ clientKey: "shopperContext"
50
+ },
51
+ {
52
+ apiFamily: "customer",
53
+ apiName: "shopper-customers",
54
+ apiVersion: "v1",
55
+ clientKey: "shopperCustomers"
56
+ },
57
+ {
58
+ apiFamily: "experience",
59
+ apiName: "shopper-experience",
60
+ apiVersion: "v1",
61
+ clientKey: "shopperExperience"
62
+ },
63
+ {
64
+ apiFamily: "pricing",
65
+ apiName: "shopper-gift-certificates",
66
+ apiVersion: "v1",
67
+ clientKey: "shopperGiftCertificates"
68
+ },
69
+ {
70
+ apiFamily: "shopper",
71
+ apiName: "auth",
72
+ apiVersion: "v1",
73
+ clientKey: "shopperLogin"
74
+ },
75
+ {
76
+ apiFamily: "checkout",
77
+ apiName: "shopper-orders",
78
+ apiVersion: "v1",
79
+ clientKey: "shopperOrders"
80
+ },
81
+ {
82
+ apiFamily: "checkout",
83
+ apiName: "shopper-payments",
84
+ apiVersion: "v1",
85
+ clientKey: "shopperPayments"
86
+ },
87
+ {
88
+ apiFamily: "product",
89
+ apiName: "shopper-products",
90
+ apiVersion: "v1",
91
+ clientKey: "shopperProducts"
92
+ },
93
+ {
94
+ apiFamily: "pricing",
95
+ apiName: "shopper-promotions",
96
+ apiVersion: "v1",
97
+ clientKey: "shopperPromotions"
98
+ },
99
+ {
100
+ apiFamily: "search",
101
+ apiName: "shopper-search",
102
+ apiVersion: "v1",
103
+ clientKey: "shopperSearch"
104
+ },
105
+ {
106
+ apiFamily: "site",
107
+ apiName: "shopper-seo",
108
+ apiVersion: "v1",
109
+ clientKey: "shopperSeo"
110
+ },
111
+ {
112
+ apiFamily: "store",
113
+ apiName: "shopper-stores",
114
+ apiVersion: "v1",
115
+ clientKey: "shopperStores"
116
+ }
117
+ ];
118
+ var Available = class extends OAuthCommand {
119
+ static description = "Survey SCAPI schemas available on the active tenant — built-in (overridable) and custom — and report the number of c_* custom attributes each currently has.";
120
+ static examples = ["<%= config.bin %> <%= command.id %>"];
121
+ async run() {
122
+ const { shortCode, tenantId } = this.resolvedConfig.values;
123
+ if (!shortCode) this.error("SCAPI short code required. Provide --short-code, set SFCC_SHORTCODE, or configure short-code in dw.json.");
124
+ if (!tenantId) this.error("tenant-id is required. Provide via --tenant-id flag, SFCC_TENANT_ID env var, or in dw.json.");
125
+ const organizationId = toOrganizationId(tenantId);
126
+ const oauthStrategy = this.getOAuthStrategy();
127
+ const client = createScapiSchemasClient({
128
+ shortCode,
129
+ tenantId
130
+ }, oauthStrategy);
131
+ this.log(`Probing built-in shopper APIs on tenant ${tenantId} for c_* custom attributes...\n`);
132
+ const builtInResults = [];
133
+ for (const api of BUILT_IN_APIS) try {
134
+ const { data, error, response } = await client.GET("/organizations/{organizationId}/schemas/{apiFamily}/{apiName}/{apiVersion}", { params: {
135
+ path: {
136
+ organizationId,
137
+ apiFamily: api.apiFamily,
138
+ apiName: api.apiName,
139
+ apiVersion: api.apiVersion
140
+ },
141
+ query: { expand: "custom_properties" }
142
+ } });
143
+ if (error || !data) {
144
+ builtInResults.push({
145
+ api,
146
+ cAttrCount: 0,
147
+ schemasWithC: [],
148
+ error: `${response.status}`
149
+ });
150
+ continue;
151
+ }
152
+ const schemas = data.components?.schemas ?? {};
153
+ let total = 0;
154
+ const interestingSchemas = [];
155
+ for (const [schemaName, schema] of Object.entries(schemas)) {
156
+ const props = schema?.properties ?? {};
157
+ const cProps = Object.keys(props).filter((k) => k.startsWith("c_"));
158
+ if (cProps.length > 0) {
159
+ total += cProps.length;
160
+ interestingSchemas.push(`${schemaName} (+${cProps.length})`);
161
+ }
162
+ }
163
+ builtInResults.push({
164
+ api,
165
+ cAttrCount: total,
166
+ schemasWithC: interestingSchemas
167
+ });
168
+ } catch (e) {
169
+ builtInResults.push({
170
+ api,
171
+ cAttrCount: 0,
172
+ schemasWithC: [],
173
+ error: e instanceof Error ? e.message : "unknown"
174
+ });
175
+ }
176
+ builtInResults.sort((a, b) => b.cAttrCount - a.cAttrCount);
177
+ const interesting = builtInResults.filter((r) => r.cAttrCount > 0);
178
+ const empty = builtInResults.filter((r) => r.cAttrCount === 0 && !r.error);
179
+ const errored = builtInResults.filter((r) => r.error);
180
+ this.log(`Built-in shopper APIs (${BUILT_IN_APIS.length}):`);
181
+ if (interesting.length > 0) {
182
+ this.log(`\n Interesting (c_* attributes present) — high-value override targets:\n`);
183
+ for (const r of interesting) {
184
+ this.log(` ${r.api.clientKey} (${r.cAttrCount} c_* attrs across ${r.schemasWithC.length} schemas)`);
185
+ this.log(` sfnext scapi add ${r.api.apiFamily} ${r.api.apiName} ${r.api.apiVersion}`);
186
+ if (r.schemasWithC.length <= 8) this.log(` schemas: ${r.schemasWithC.join(", ")}`);
187
+ }
188
+ }
189
+ if (empty.length > 0) {
190
+ this.log(`\n No c_* attributes (overridable but no tenant customizations to surface):`);
191
+ for (const r of empty) this.log(` ${r.api.clientKey}`);
192
+ }
193
+ if (errored.length > 0) {
194
+ this.log(`\n Errored:`);
195
+ for (const r of errored) this.log(` ${r.api.clientKey} — ${r.error}`);
196
+ }
197
+ this.log(`\n\nQuerying tenant for custom SCAPI APIs...`);
198
+ const { data: listData, error: listError, response: listResp } = await client.GET("/organizations/{organizationId}/schemas", { params: { path: { organizationId } } });
199
+ if (listError || !listData) {
200
+ this.warn(`Schemas listing failed (${listResp.status}). Custom APIs may exist but cannot be enumerated.`);
201
+ return;
202
+ }
203
+ const listed = listData.schemas ?? [];
204
+ const customApis = [];
205
+ for (const s of listed) {
206
+ if (!s.apiName) continue;
207
+ const clientKey = deriveClientKey(s.apiName);
208
+ if (!isBuiltInClientKey(clientKey)) customApis.push({
209
+ ...s,
210
+ clientKey
211
+ });
212
+ }
213
+ if (customApis.length === 0) this.log(" No custom SCAPI APIs registered on this tenant.");
214
+ else {
215
+ this.log(` Custom APIs (${customApis.length}):\n`);
216
+ for (const s of customApis) {
217
+ this.log(` ${s.apiFamily}/${s.apiName}/${s.apiVersion} (clientKey: ${s.clientKey})`);
218
+ this.log(` sfnext scapi add ${s.apiFamily} ${s.apiName} ${s.apiVersion}`);
219
+ }
220
+ }
221
+ }
222
+ };
223
+
224
+ //#endregion
225
+ export { Available as default };
@@ -1,14 +1,17 @@
1
1
  import { r as commonFlags } from "../../flags.js";
2
- import { r as readAllSchemaMetadata } from "../../schema-utils.js";
2
+ import { a as readAllSchemaMetadata, i as isBuiltInClientKey } from "../../schema-utils.js";
3
3
  import { Command } from "@oclif/core";
4
4
  import { join } from "node:path";
5
5
 
6
6
  //#region src/commands/scapi/list.ts
7
+ function entryKind(entry) {
8
+ return entry.kind ?? (isBuiltInClientKey(entry.clientKey) ? "override" : "custom");
9
+ }
7
10
  /**
8
- * List registered custom SCAPI clients.
11
+ * List registered SCAPI client overrides and custom APIs.
9
12
  */
10
13
  var List = class List extends Command {
11
- static description = "List registered custom SCAPI clients";
14
+ static description = "List registered SCAPI client overrides and custom APIs";
12
15
  static examples = ["<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> -d ./my-project"];
13
16
  static flags = { ...commonFlags };
14
17
  async run() {
@@ -16,18 +19,28 @@ var List = class List extends Command {
16
19
  const projectDir = flags["project-directory"];
17
20
  const entries = readAllSchemaMetadata(join(projectDir, "src", "scapi", "schemas"));
18
21
  if (entries.length === 0) {
19
- this.log("No custom SCAPI clients registered.");
22
+ this.log("No SCAPI client overrides or custom APIs registered.");
20
23
  this.log("Use `sfnext scapi add` to add one.");
21
24
  return;
22
25
  }
23
- this.log(`\nRegistered SCAPI clients (${entries.length}):\n`);
24
- for (const { clientKey, basePath, supportsLocale, schemaName } of entries) {
25
- this.log(` ${clientKey}`);
26
- this.log(` Schema: schemas/${schemaName}.yaml`);
27
- this.log(` Base: ${basePath}`);
28
- this.log(` Locale: ${supportsLocale ? "yes" : "no"}`);
29
- this.log("");
26
+ const overrides = entries.filter((e) => entryKind(e) === "override");
27
+ const customs = entries.filter((e) => entryKind(e) === "custom");
28
+ this.log(`\nRegistered SCAPI clients (${entries.length}):`);
29
+ if (overrides.length > 0) {
30
+ this.log(`\nOverrides (${overrides.length}):\n`);
31
+ for (const e of overrides) this.printEntry(e);
30
32
  }
33
+ if (customs.length > 0) {
34
+ this.log(`\nCustom APIs (${customs.length}):\n`);
35
+ for (const e of customs) this.printEntry(e);
36
+ }
37
+ }
38
+ printEntry({ clientKey, basePath, supportsLocale, schemaName }) {
39
+ this.log(` ${clientKey}`);
40
+ this.log(` Schema: schemas/${schemaName}.yaml`);
41
+ this.log(` Base: ${basePath}`);
42
+ this.log(` Locale: ${supportsLocale ? "yes" : "no"}`);
43
+ this.log("");
31
44
  }
32
45
  };
33
46
 
@@ -1,5 +1,5 @@
1
1
  import { r as commonFlags } from "../../flags.js";
2
- import { r as readAllSchemaMetadata } from "../../schema-utils.js";
2
+ import { a as readAllSchemaMetadata } from "../../schema-utils.js";
3
3
  import { t as generateCustomClientsFile } from "../../generate-custom-clients.js";
4
4
  import { Args, Command } from "@oclif/core";
5
5
  import { existsSync, unlinkSync } from "node:fs";
@@ -42,12 +42,18 @@ var Remove = class Remove extends Command {
42
42
  if (existsSync(metaPath)) unlinkSync(metaPath);
43
43
  const typesPath = join(generatedDir, `${schemaName}.ts`);
44
44
  const opsPath = join(generatedDir, `${schemaName}.operations.ts`);
45
- for (const filePath of [typesPath, opsPath]) if (existsSync(filePath)) {
45
+ const namespacePath = join(generatedDir, `${schemaName}.namespace.ts`);
46
+ for (const filePath of [
47
+ typesPath,
48
+ opsPath,
49
+ namespacePath
50
+ ]) if (existsSync(filePath)) {
46
51
  unlinkSync(filePath);
47
52
  this.log(`Removed ${relative(projectDir, filePath)}`);
48
53
  }
49
54
  generateCustomClientsFile(scapiDir);
50
55
  this.log(`Updated ${relative(projectDir, join(scapiDir, "custom-clients.ts"))}`);
56
+ this.log(`Updated ${relative(projectDir, join(scapiDir, "index.ts"))}`);
51
57
  this.log(`\nRemoved client "${clientKey}".`);
52
58
  }
53
59
  };
@@ -6,6 +6,13 @@ import { Preset } from "@react-router/dev/config";
6
6
  * Storefront Next preset for React Router configuration.
7
7
  * This preset enforces standard configuration for SFCC Storefront Next applications.
8
8
  * Users cannot override these values - they will be validated and an error will be thrown if modified.
9
+ *
10
+ * Environment variables:
11
+ * - `SFW_FALCON_INSTANCE` — (Optional) The Falcon instance identifier (e.g., `aws-dev2-uswest2`).
12
+ * When set together with `SFW_FUNCTIONAL_DOMAIN`, adds workspace proxy domains to
13
+ * `allowedActionOrigins` for CSRF protection on form actions.
14
+ * - `SFW_FUNCTIONAL_DOMAIN` — (Optional) The functional domain name (e.g., `cvw-dataplane-test`).
15
+ * Required alongside `SFW_FALCON_INSTANCE` to construct workspace origin patterns.
9
16
  */
10
17
  declare function storefrontNextPreset(): Preset;
11
18
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"react-router.config.d.ts","names":[],"sources":["../../src/configs/react-router.config.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAuBgB,oBAAA,CAAA,GAAwB"}
1
+ {"version":3,"file":"react-router.config.d.ts","names":[],"sources":["../../src/configs/react-router.config.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;iBA8BgB,oBAAA,CAAA,GAAwB"}
@@ -37,9 +37,19 @@ function getBasePath() {
37
37
  * Storefront Next preset for React Router configuration.
38
38
  * This preset enforces standard configuration for SFCC Storefront Next applications.
39
39
  * Users cannot override these values - they will be validated and an error will be thrown if modified.
40
+ *
41
+ * Environment variables:
42
+ * - `SFW_FALCON_INSTANCE` — (Optional) The Falcon instance identifier (e.g., `aws-dev2-uswest2`).
43
+ * When set together with `SFW_FUNCTIONAL_DOMAIN`, adds workspace proxy domains to
44
+ * `allowedActionOrigins` for CSRF protection on form actions.
45
+ * - `SFW_FUNCTIONAL_DOMAIN` — (Optional) The functional domain name (e.g., `cvw-dataplane-test`).
46
+ * Required alongside `SFW_FALCON_INSTANCE` to construct workspace origin patterns.
40
47
  */
41
48
  function storefrontNextPreset() {
42
49
  const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;
50
+ const sfwFunctionalDomain = process.env.SFW_FUNCTIONAL_DOMAIN;
51
+ if (sfwFalconInstance && !sfwFunctionalDomain) console.warn("[storefront-next] SFW_FALCON_INSTANCE is set but SFW_FUNCTIONAL_DOMAIN is not. allowedActionOrigins will not include workspace domains. Set both env vars to enable CSRF protection for workspace proxy origins.");
52
+ if (sfwFunctionalDomain && !sfwFalconInstance) console.warn("[storefront-next] SFW_FUNCTIONAL_DOMAIN is set but SFW_FALCON_INSTANCE is not. allowedActionOrigins will not include workspace domains. Set both env vars to enable CSRF protection for workspace proxy origins.");
43
53
  const presetConfig = {
44
54
  appDirectory: "./src",
45
55
  buildDirectory: "build",
@@ -52,7 +62,7 @@ function storefrontNextPreset() {
52
62
  unstable_optimizeDeps: true
53
63
  },
54
64
  basename: getBasePath() || "/",
55
- ...sfwFalconInstance && { allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`] }
65
+ ...sfwFalconInstance && sfwFunctionalDomain && { allowedActionOrigins: [`*.dataplane.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`, `*.platform.a.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`] }
56
66
  };
57
67
  return {
58
68
  name: "storefront-next-preset",
@@ -65,6 +75,7 @@ function storefrontNextPreset() {
65
75
  if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) errors.push(`future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`);
66
76
  if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) errors.push(`future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`);
67
77
  if (reactRouterConfig.basename !== presetConfig.basename) errors.push(`basename: expected ${presetConfig.basename}, got ${reactRouterConfig.basename}`);
78
+ if (presetConfig.allowedActionOrigins && JSON.stringify(reactRouterConfig.allowedActionOrigins) !== JSON.stringify(presetConfig.allowedActionOrigins)) errors.push(`allowedActionOrigins: expected ${JSON.stringify(presetConfig.allowedActionOrigins)}, got ${JSON.stringify(reactRouterConfig.allowedActionOrigins)}`);
68
79
  if (errors.length > 0) throw new Error(`Storefront Next preset configuration was overridden. The following values must not be modified:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
69
80
  }
70
81
  };
@@ -1 +1 @@
1
- {"version":3,"file":"react-router.config.js","names":["errors: string[]"],"sources":["../../src/utils/paths.ts","../../src/configs/react-router.config.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Normalize a file path to use forward slashes.\n * On Windows, Node APIs return backslash-separated paths, but ESM import\n * specifiers and Vite module IDs require forward slashes.\n */\nexport function toPosixPath(filePath: string): string {\n return filePath.replace(/\\\\/g, '/');\n}\n\n/**\n * Get the Commerce Cloud API URL from a short code\n */\nexport function getCommerceCloudApiUrl(shortCode: string, proxyHost?: string): string {\n return proxyHost || `https://${shortCode}.api.commercecloud.salesforce.com`;\n}\n\n/**\n * Get the configurable base path for the application.\n * Reads from MRT_ENV_BASE_PATH environment variable.\n *\n * The base path is used for CDN routing to the correct MRT environment.\n * It is prepended to all URLs: page routes, /mobify/bundle/ assets, and /mobify/proxy/api.\n *\n * Validation rules:\n * - Must be a single path segment starting with '/'\n * - Max 63 characters after the leading slash\n * - Only URL-safe characters allowed\n * - Returns empty string if not set\n *\n * @returns The sanitized base path (e.g., '/site-a' or '')\n *\n * @example\n * // No base path configured\n * getBasePath() // → ''\n *\n * // With base path '/storefront'\n * getBasePath() // → '/storefront'\n *\n * // Automatically sanitizes\n * // MRT_ENV_BASE_PATH='storefront/' → '/storefront'\n */\nexport function getBasePath(): string {\n const basePath = process.env.MRT_ENV_BASE_PATH?.trim();\n\n // Return empty string if not set or empty\n if (!basePath) {\n return '';\n }\n\n // Base path prefix must be a single path segment starting with '/', max 63 chars,\n // using only URL-safe characters (alphanumeric, hyphens, underscores, dots, and other safe symbols)\n // This aligns with the regex used by MRT\n if (!/^\\/[a-zA-Z0-9_.+$~\"'@:-]{1,63}$/.test(basePath)) {\n throw new Error(\n `Invalid base path: \"${basePath}\". ` +\n \"Base path must be a single segment starting with '/' (e.g., '/site-a'), \" +\n 'contain only URL-safe characters, and be at most 63 characters after the leading slash.'\n );\n }\n\n return basePath;\n}\n\n/**\n * Get the bundle path for static assets\n */\nexport function getBundlePath(bundleId: string): string {\n const basePath = getBasePath();\n return `${basePath}/mobify/bundle/${bundleId}/client/`;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { Preset } from '@react-router/dev/config';\nimport { getBasePath } from '../utils/paths';\n\n/**\n * Storefront Next preset for React Router configuration.\n * This preset enforces standard configuration for SFCC Storefront Next applications.\n * Users cannot override these values - they will be validated and an error will be thrown if modified.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n\n // Read base path from env var for basename configuration\n // This sets the base path for all React Router routes (e.g., '/site-a')\n // In dev: reads from .env at build time\n // In production: baked into the build, but can be overridden at runtime via patchReactRouterBuild\n const basePath = getBasePath();\n\n const presetConfig = {\n appDirectory: './src',\n buildDirectory: 'build',\n routeDiscovery: { mode: 'initial' as const },\n serverModuleFormat: 'cjs' as const,\n ssr: true,\n future: {\n v8_middleware: true,\n v8_viteEnvironmentApi: true,\n unstable_optimizeDeps: true,\n },\n // Set basename from base path for CDN routing\n // When set, all routes are served under this base path (e.g., /site-a/category/womens)\n // React Router automatically handles Link, navigate, .data requests, and redirects\n basename: basePath || '/',\n // Allow workspace proxy domain for CSRF protection on form actions\n ...(sfwFalconInstance && {\n allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`],\n }),\n };\n\n return {\n name: 'storefront-next-preset',\n reactRouterConfig: () => presetConfig,\n reactRouterConfigResolved: ({ reactRouterConfig }) => {\n // Validate that critical config values have not been overridden\n // Note: We don't validate appDirectory and buildDirectory because they get resolved\n // to absolute paths and we can't reliably determine the correct absolute path\n const errors: string[] = [];\n\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n errors.push(\n `routeDiscovery.mode: expected \"${presetConfig.routeDiscovery.mode}\", got \"${reactRouterConfig.routeDiscovery?.mode}\"`\n );\n }\n\n if (reactRouterConfig.serverModuleFormat !== presetConfig.serverModuleFormat) {\n errors.push(\n `serverModuleFormat: expected \"${presetConfig.serverModuleFormat}\", got \"${reactRouterConfig.serverModuleFormat}\"`\n );\n }\n\n if (reactRouterConfig.ssr !== presetConfig.ssr) {\n errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);\n }\n\n if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) {\n errors.push(\n `future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`\n );\n }\n\n if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) {\n errors.push(\n `future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`\n );\n }\n\n if (reactRouterConfig.basename !== presetConfig.basename) {\n errors.push(`basename: expected ${presetConfig.basename}, got ${reactRouterConfig.basename}`);\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Storefront Next preset configuration was overridden. The following values must not be modified:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,cAAsB;CAClC,MAAM,WAAW,QAAQ,IAAI,mBAAmB,MAAM;AAGtD,KAAI,CAAC,SACD,QAAO;AAMX,KAAI,CAAC,kCAAkC,KAAK,SAAS,CACjD,OAAM,IAAI,MACN,uBAAuB,SAAS,oKAGnC;AAGL,QAAO;;;;;;;;;;ACpDX,SAAgB,uBAA+B;CAC3C,MAAM,oBAAoB,QAAQ,IAAI;CAQtC,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GACvB,uBAAuB;GAC1B;EAID,UAhBa,aAAa,IAgBJ;EAEtB,GAAI,qBAAqB,EACrB,sBAAsB,CAAC,kCAAkC,kBAAkB,cAAc,EAC5F;EACJ;AAED,QAAO;EACH,MAAM;EACN,yBAAyB;EACzB,4BAA4B,EAAE,wBAAwB;GAIlD,MAAMA,SAAmB,EAAE;AAE3B,OAAI,kBAAkB,gBAAgB,SAAS,aAAa,eAAe,KACvE,QAAO,KACH,kCAAkC,aAAa,eAAe,KAAK,UAAU,kBAAkB,gBAAgB,KAAK,GACvH;AAGL,OAAI,kBAAkB,uBAAuB,aAAa,mBACtD,QAAO,KACH,iCAAiC,aAAa,mBAAmB,UAAU,kBAAkB,mBAAmB,GACnH;AAGL,OAAI,kBAAkB,QAAQ,aAAa,IACvC,QAAO,KAAK,iBAAiB,aAAa,IAAI,QAAQ,kBAAkB,MAAM;AAGlF,OAAI,kBAAkB,QAAQ,kBAAkB,aAAa,OAAO,cAChE,QAAO,KACH,kCAAkC,aAAa,OAAO,cAAc,QAAQ,kBAAkB,QAAQ,gBACzG;AAGL,OAAI,kBAAkB,QAAQ,0BAA0B,aAAa,OAAO,sBACxE,QAAO,KACH,0CAA0C,aAAa,OAAO,sBAAsB,QAAQ,kBAAkB,QAAQ,wBACzH;AAGL,OAAI,kBAAkB,aAAa,aAAa,SAC5C,QAAO,KAAK,sBAAsB,aAAa,SAAS,QAAQ,kBAAkB,WAAW;AAGjG,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
1
+ {"version":3,"file":"react-router.config.js","names":["errors: string[]"],"sources":["../../src/utils/paths.ts","../../src/configs/react-router.config.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Normalize a file path to use forward slashes.\n * On Windows, Node APIs return backslash-separated paths, but ESM import\n * specifiers and Vite module IDs require forward slashes.\n */\nexport function toPosixPath(filePath: string): string {\n return filePath.replace(/\\\\/g, '/');\n}\n\n/**\n * Get the Commerce Cloud API URL from a short code\n */\nexport function getCommerceCloudApiUrl(shortCode: string, proxyHost?: string): string {\n return proxyHost || `https://${shortCode}.api.commercecloud.salesforce.com`;\n}\n\n/**\n * Get the configurable base path for the application.\n * Reads from MRT_ENV_BASE_PATH environment variable.\n *\n * The base path is used for CDN routing to the correct MRT environment.\n * It is prepended to all URLs: page routes, /mobify/bundle/ assets, and /mobify/proxy/api.\n *\n * Validation rules:\n * - Must be a single path segment starting with '/'\n * - Max 63 characters after the leading slash\n * - Only URL-safe characters allowed\n * - Returns empty string if not set\n *\n * @returns The sanitized base path (e.g., '/site-a' or '')\n *\n * @example\n * // No base path configured\n * getBasePath() // → ''\n *\n * // With base path '/storefront'\n * getBasePath() // → '/storefront'\n *\n * // Automatically sanitizes\n * // MRT_ENV_BASE_PATH='storefront/' → '/storefront'\n */\nexport function getBasePath(): string {\n const basePath = process.env.MRT_ENV_BASE_PATH?.trim();\n\n // Return empty string if not set or empty\n if (!basePath) {\n return '';\n }\n\n // Base path prefix must be a single path segment starting with '/', max 63 chars,\n // using only URL-safe characters (alphanumeric, hyphens, underscores, dots, and other safe symbols)\n // This aligns with the regex used by MRT\n if (!/^\\/[a-zA-Z0-9_.+$~\"'@:-]{1,63}$/.test(basePath)) {\n throw new Error(\n `Invalid base path: \"${basePath}\". ` +\n \"Base path must be a single segment starting with '/' (e.g., '/site-a'), \" +\n 'contain only URL-safe characters, and be at most 63 characters after the leading slash.'\n );\n }\n\n return basePath;\n}\n\n/**\n * Get the bundle path for static assets\n */\nexport function getBundlePath(bundleId: string): string {\n const basePath = getBasePath();\n return `${basePath}/mobify/bundle/${bundleId}/client/`;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { Preset } from '@react-router/dev/config';\nimport { getBasePath } from '../utils/paths';\n\n/**\n * Storefront Next preset for React Router configuration.\n * This preset enforces standard configuration for SFCC Storefront Next applications.\n * Users cannot override these values - they will be validated and an error will be thrown if modified.\n *\n * Environment variables:\n * - `SFW_FALCON_INSTANCE` — (Optional) The Falcon instance identifier (e.g., `aws-dev2-uswest2`).\n * When set together with `SFW_FUNCTIONAL_DOMAIN`, adds workspace proxy domains to\n * `allowedActionOrigins` for CSRF protection on form actions.\n * - `SFW_FUNCTIONAL_DOMAIN` — (Optional) The functional domain name (e.g., `cvw-dataplane-test`).\n * Required alongside `SFW_FALCON_INSTANCE` to construct workspace origin patterns.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n const sfwFunctionalDomain = process.env.SFW_FUNCTIONAL_DOMAIN;\n\n if (sfwFalconInstance && !sfwFunctionalDomain) {\n console.warn(\n '[storefront-next] SFW_FALCON_INSTANCE is set but SFW_FUNCTIONAL_DOMAIN is not. ' +\n 'allowedActionOrigins will not include workspace domains. ' +\n 'Set both env vars to enable CSRF protection for workspace proxy origins.'\n );\n }\n\n if (sfwFunctionalDomain && !sfwFalconInstance) {\n console.warn(\n '[storefront-next] SFW_FUNCTIONAL_DOMAIN is set but SFW_FALCON_INSTANCE is not. ' +\n 'allowedActionOrigins will not include workspace domains. ' +\n 'Set both env vars to enable CSRF protection for workspace proxy origins.'\n );\n }\n\n // Read base path from env var for basename configuration\n // This sets the base path for all React Router routes (e.g., '/site-a')\n // In dev: reads from .env at build time\n // In production: baked into the build, but can be overridden at runtime via patchReactRouterBuild\n const basePath = getBasePath();\n\n const presetConfig = {\n appDirectory: './src',\n buildDirectory: 'build',\n routeDiscovery: { mode: 'initial' as const },\n serverModuleFormat: 'cjs' as const,\n ssr: true,\n future: {\n v8_middleware: true,\n v8_viteEnvironmentApi: true,\n unstable_optimizeDeps: true,\n },\n // Set basename from base path for CDN routing\n // When set, all routes are served under this base path (e.g., /site-a/category/womens)\n // React Router automatically handles Link, navigate, .data requests, and redirects\n basename: basePath || '/',\n // Allow workspace proxy domains for CSRF protection on form actions\n // Both the legacy port-based dataplane format and the Pomerium reverse proxy format are supported\n ...(sfwFalconInstance &&\n sfwFunctionalDomain && {\n allowedActionOrigins: [\n `*.dataplane.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`,\n `*.platform.a.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`,\n ],\n }),\n };\n\n return {\n name: 'storefront-next-preset',\n reactRouterConfig: () => presetConfig,\n reactRouterConfigResolved: ({ reactRouterConfig }) => {\n // Validate that critical config values have not been overridden\n // Note: We don't validate appDirectory and buildDirectory because they get resolved\n // to absolute paths and we can't reliably determine the correct absolute path\n const errors: string[] = [];\n\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n errors.push(\n `routeDiscovery.mode: expected \"${presetConfig.routeDiscovery.mode}\", got \"${reactRouterConfig.routeDiscovery?.mode}\"`\n );\n }\n\n if (reactRouterConfig.serverModuleFormat !== presetConfig.serverModuleFormat) {\n errors.push(\n `serverModuleFormat: expected \"${presetConfig.serverModuleFormat}\", got \"${reactRouterConfig.serverModuleFormat}\"`\n );\n }\n\n if (reactRouterConfig.ssr !== presetConfig.ssr) {\n errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);\n }\n\n if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) {\n errors.push(\n `future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`\n );\n }\n\n if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) {\n errors.push(\n `future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`\n );\n }\n\n if (reactRouterConfig.basename !== presetConfig.basename) {\n errors.push(`basename: expected ${presetConfig.basename}, got ${reactRouterConfig.basename}`);\n }\n\n // Only validate allowedActionOrigins when the preset sets it (workspace env).\n // In prod builds (no workspace env vars), customers are free to configure their own origins.\n if (\n presetConfig.allowedActionOrigins &&\n JSON.stringify(reactRouterConfig.allowedActionOrigins) !==\n JSON.stringify(presetConfig.allowedActionOrigins)\n ) {\n errors.push(\n `allowedActionOrigins: expected ${JSON.stringify(presetConfig.allowedActionOrigins)}, got ${JSON.stringify(reactRouterConfig.allowedActionOrigins)}`\n );\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Storefront Next preset configuration was overridden. The following values must not be modified:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,cAAsB;CAClC,MAAM,WAAW,QAAQ,IAAI,mBAAmB,MAAM;AAGtD,KAAI,CAAC,SACD,QAAO;AAMX,KAAI,CAAC,kCAAkC,KAAK,SAAS,CACjD,OAAM,IAAI,MACN,uBAAuB,SAAS,oKAGnC;AAGL,QAAO;;;;;;;;;;;;;;;;;AC7CX,SAAgB,uBAA+B;CAC3C,MAAM,oBAAoB,QAAQ,IAAI;CACtC,MAAM,sBAAsB,QAAQ,IAAI;AAExC,KAAI,qBAAqB,CAAC,oBACtB,SAAQ,KACJ,mNAGH;AAGL,KAAI,uBAAuB,CAAC,kBACxB,SAAQ,KACJ,mNAGH;CASL,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GACvB,uBAAuB;GAC1B;EAID,UAhBa,aAAa,IAgBJ;EAGtB,GAAI,qBACA,uBAAuB,EACnB,sBAAsB,CAClB,eAAe,oBAAoB,GAAG,kBAAkB,eACxD,gBAAgB,oBAAoB,GAAG,kBAAkB,cAC5D,EACJ;EACR;AAED,QAAO;EACH,MAAM;EACN,yBAAyB;EACzB,4BAA4B,EAAE,wBAAwB;GAIlD,MAAMA,SAAmB,EAAE;AAE3B,OAAI,kBAAkB,gBAAgB,SAAS,aAAa,eAAe,KACvE,QAAO,KACH,kCAAkC,aAAa,eAAe,KAAK,UAAU,kBAAkB,gBAAgB,KAAK,GACvH;AAGL,OAAI,kBAAkB,uBAAuB,aAAa,mBACtD,QAAO,KACH,iCAAiC,aAAa,mBAAmB,UAAU,kBAAkB,mBAAmB,GACnH;AAGL,OAAI,kBAAkB,QAAQ,aAAa,IACvC,QAAO,KAAK,iBAAiB,aAAa,IAAI,QAAQ,kBAAkB,MAAM;AAGlF,OAAI,kBAAkB,QAAQ,kBAAkB,aAAa,OAAO,cAChE,QAAO,KACH,kCAAkC,aAAa,OAAO,cAAc,QAAQ,kBAAkB,QAAQ,gBACzG;AAGL,OAAI,kBAAkB,QAAQ,0BAA0B,aAAa,OAAO,sBACxE,QAAO,KACH,0CAA0C,aAAa,OAAO,sBAAsB,QAAQ,kBAAkB,QAAQ,wBACzH;AAGL,OAAI,kBAAkB,aAAa,aAAa,SAC5C,QAAO,KAAK,sBAAsB,aAAa,SAAS,QAAQ,kBAAkB,WAAW;AAKjG,OACI,aAAa,wBACb,KAAK,UAAU,kBAAkB,qBAAqB,KAClD,KAAK,UAAU,aAAa,qBAAqB,CAErD,QAAO,KACH,kCAAkC,KAAK,UAAU,aAAa,qBAAqB,CAAC,QAAQ,KAAK,UAAU,kBAAkB,qBAAqB,GACrJ;AAGL,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
@@ -309,6 +309,7 @@ function extractAttributesFromSource(sourceFile, className) {
309
309
  };
310
310
  if (config.values) attribute.values = config.values;
311
311
  if (config.defaultValue !== void 0) attribute.default_value = config.defaultValue;
312
+ if (config.editorDefinition !== void 0) attribute.editor_definition = config.editorDefinition;
312
313
  attributes.push(attribute);
313
314
  }
314
315
  } catch (error) {
@@ -1,5 +1,5 @@
1
- import { r as readAllSchemaMetadata } from "./schema-utils.js";
2
- import { writeFileSync } from "node:fs";
1
+ import { a as readAllSchemaMetadata, i as isBuiltInClientKey } from "./schema-utils.js";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
5
  //#region src/scapi/generate-custom-clients.ts
@@ -19,12 +19,40 @@ const COPYRIGHT_HEADER = `/**
19
19
  * limitations under the License.
20
20
  */`;
21
21
  /**
22
- * Generate the custom-clients.ts file in the template project.
22
+ * Mapping of every built-in client key to its runtime namespace name.
23
+ * Camel→Pascal would suffice for all current entries, but keeping the map explicit
24
+ * prevents surprises if a future client uses a name that doesn't round-trip cleanly.
23
25
  *
24
- * @param scapiDir - Absolute path to the src/scapi/ directory in the template project
26
+ * Keep in sync with `BUILT_IN_CLIENT_KEYS` in schema-utils.ts and the namespaces in
27
+ * `packages/storefront-next-runtime/src/scapi-client/types.ts`.
25
28
  */
26
- function generateCustomClientsFile(scapiDir) {
27
- const entries = readAllSchemaMetadata(join(scapiDir, "schemas"));
29
+ const BUILT_IN_NAMESPACE_BY_KEY = {
30
+ shopperAvailability: "ShopperAvailability",
31
+ shopperBasketsV1: "ShopperBasketsV1",
32
+ shopperBasketsV2: "ShopperBasketsV2",
33
+ shopperConfigurations: "ShopperConfigurations",
34
+ shopperConsents: "ShopperConsents",
35
+ shopperContext: "ShopperContext",
36
+ shopperCustomers: "ShopperCustomers",
37
+ shopperExperience: "ShopperExperience",
38
+ shopperGiftCertificates: "ShopperGiftCertificates",
39
+ shopperLogin: "ShopperLogin",
40
+ shopperOrders: "ShopperOrders",
41
+ shopperPayments: "ShopperPayments",
42
+ shopperProducts: "ShopperProducts",
43
+ shopperPromotions: "ShopperPromotions",
44
+ shopperSearch: "ShopperSearch",
45
+ shopperSeo: "ShopperSeo",
46
+ shopperStores: "ShopperStores"
47
+ };
48
+ function isOverride(entry) {
49
+ return (entry.kind ?? (isBuiltInClientKey(entry.clientKey) ? "override" : "custom")) === "override";
50
+ }
51
+ /**
52
+ * Write `src/scapi/custom-clients.ts` from the schema entries.
53
+ * Always writes — empty registry produces a no-op file.
54
+ */
55
+ function writeCustomClientsFile(scapiDir, entries) {
28
56
  const customClientsPath = join(scapiDir, "custom-clients.ts");
29
57
  if (entries.length === 0) {
30
58
  writeFileSync(customClientsPath, `${COPYRIGHT_HEADER}
@@ -34,7 +62,7 @@ function generateCustomClientsFile(scapiDir) {
34
62
  *
35
63
  * Use \`sfnext scapi add --schema <file>\` to add a client from a local schema file.
36
64
  * Use \`sfnext scapi add <family> <name> <version>\` to pull from the SCAPI Schemas API.
37
- * Use \`sfnext scapi list\` to see registered custom clients.
65
+ * Use \`sfnext scapi list\` to see registered clients.
38
66
  */
39
67
  export { type Clients as AppClients } from '@salesforce/storefront-next-runtime/scapi';
40
68
 
@@ -68,6 +96,98 @@ ${clientEntries.join("\n")}
68
96
  ] as const;
69
97
  `, "utf-8");
70
98
  }
99
+ /**
100
+ * Write a per-override namespace wrapper that mirrors the shape of the runtime's
101
+ * namespaces in `packages/storefront-next-runtime/src/scapi-client/types.ts`.
102
+ * Only emitted when the entry overrides a built-in client.
103
+ */
104
+ function writeOverrideNamespaceFile(scapiDir, entry) {
105
+ const namespaceName = BUILT_IN_NAMESPACE_BY_KEY[entry.clientKey];
106
+ if (!namespaceName) return;
107
+ const generatedDir = join(scapiDir, "generated");
108
+ mkdirSync(generatedDir, { recursive: true });
109
+ writeFileSync(join(generatedDir, `${entry.schemaName}.namespace.ts`), `${COPYRIGHT_HEADER}
110
+
111
+ /* eslint-disable @typescript-eslint/no-namespace, import/no-namespace */
112
+ /**
113
+ * Override namespace for \`${namespaceName}\` — generated by \`sfnext scapi\`, do not edit manually.
114
+ *
115
+ * Mirrors the shape of the runtime SDK's namespace so consumers of \`@/scapi\` see the
116
+ * same \`endpoints\`, \`schemas\`, and \`operations\` keys, but resolved against the local
117
+ * schema (e.g., with \`c_*\` custom attributes typed).
118
+ */
119
+ import type * as Source from './${entry.schemaName}';
120
+
121
+ export namespace ${namespaceName} {
122
+ export type endpoints = Source.paths;
123
+ export type schemas = Source.components['schemas'];
124
+ export type operations = Source.operations;
125
+ }
126
+ `, "utf-8");
127
+ }
128
+ /**
129
+ * Write `src/scapi/index.ts` — the template-local SCAPI barrel.
130
+ *
131
+ * Always wildcards the runtime SCAPI surface so future SDK additions flow through
132
+ * automatically. Overridden namespaces are re-exported explicitly below the wildcard;
133
+ * ECMAScript module semantics make a named re-export shadow a same-named wildcard
134
+ * re-export, so consumers of `@/scapi` see the local generated namespace for overridden
135
+ * clients and the runtime version for everything else.
136
+ */
137
+ function writeBarrelFile(scapiDir, entries) {
138
+ const indexPath = join(scapiDir, "index.ts");
139
+ const overrides = entries.filter(isOverride);
140
+ if (overrides.length === 0) {
141
+ writeFileSync(indexPath, `${COPYRIGHT_HEADER}
142
+
143
+ /**
144
+ * Template-local SCAPI barrel — generated by \`sfnext scapi\`, do not edit manually.
145
+ *
146
+ * Template code should import SCAPI types and clients from \`@/scapi\` (this file) so
147
+ * that any overrides registered via \`sfnext scapi add\` resolve to local schemas. With
148
+ * no overrides registered this is a transparent re-export of the runtime SDK.
149
+ */
150
+ export * from '@salesforce/storefront-next-runtime/scapi';
151
+ export { customClients, type AppClients } from './custom-clients';
152
+ `, "utf-8");
153
+ return;
154
+ }
155
+ const overriddenNamespaces = overrides.map((e) => BUILT_IN_NAMESPACE_BY_KEY[e.clientKey]).filter((n) => typeof n === "string");
156
+ const overrideExports = overrides.map((e) => {
157
+ const ns = BUILT_IN_NAMESPACE_BY_KEY[e.clientKey];
158
+ return ns ? `export type { ${ns} } from './generated/${e.schemaName}.namespace';` : "";
159
+ }).filter(Boolean).join("\n");
160
+ writeFileSync(indexPath, `${COPYRIGHT_HEADER}
161
+
162
+ /**
163
+ * Template-local SCAPI barrel — generated by \`sfnext scapi\`, do not edit manually.
164
+ *
165
+ * Wildcard re-exports the runtime SCAPI surface; the explicit named re-exports below
166
+ * shadow the wildcard so each active override resolves to its locally-generated schema.
167
+ *
168
+ * Active overrides: ${overriddenNamespaces.join(", ")}
169
+ */
170
+ export * from '@salesforce/storefront-next-runtime/scapi';
171
+
172
+ // Overridden namespaces — resolved against local schemas
173
+ ${overrideExports}
174
+
175
+ // Custom client registry consumed by api-clients.server.ts
176
+ export { customClients, type AppClients } from './custom-clients';
177
+ `, "utf-8");
178
+ }
179
+ /**
180
+ * Generate the template's SCAPI surface (`custom-clients.ts`, `index.ts`, and any
181
+ * per-override namespace wrappers) from `.meta.json` sidecars in `schemas/`.
182
+ *
183
+ * @param scapiDir - Absolute path to the src/scapi/ directory in the template project
184
+ */
185
+ function generateCustomClientsFile(scapiDir) {
186
+ const entries = readAllSchemaMetadata(join(scapiDir, "schemas"));
187
+ writeCustomClientsFile(scapiDir, entries);
188
+ writeBarrelFile(scapiDir, entries);
189
+ for (const entry of entries) if (isOverride(entry)) writeOverrideNamespaceFile(scapiDir, entry);
190
+ }
71
191
 
72
192
  //#endregion
73
193
  export { generateCustomClientsFile as t };
@@ -48,6 +48,7 @@ async function initializePlugins() {
48
48
  * @env {string} [MRT_TARGET] - Target environment for MRT deployments
49
49
  */
50
50
  const hook = async function(opts) {
51
+ if (!(this.config.bin === "sfnext" || opts.id === "sfnext" || (opts.id?.startsWith("sfnext:") ?? false))) return;
51
52
  const args = opts.argv ?? [];
52
53
  let projectDir = process.cwd();
53
54
  for (let i = 0; i < args.length; i++) {