@salesforce/storefront-next-dev 1.3.2 → 1.4.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.
package/README.md CHANGED
@@ -149,18 +149,31 @@ Run `pnpm sfnext push --help` to see all available options:
149
149
 
150
150
  - `-b, --build-directory <dir>`: Build directory to push (default: auto-detected)
151
151
  - `-m, --message <message>`: Bundle message (default: git branch:commit)
152
- - `-p, --project <slug>`: Project slug - the unique identifier for your project on Managed Runtime
153
- - `-e, --environment <target>`: Deploy target environment on Managed Runtime
152
+ - `-p, --project <slug>` (alias `-s, --storefront`): Project slug on Managed Runtime (env: `MRT_PROJECT`)
153
+ - `-e, --environment <env>` (alias `-t, --target`): Environment to deploy to (env: `MRT_ENVIRONMENT`; `MRT_TARGET` also accepted)
154
154
  - `-w, --wait`: Wait for deployment to complete
155
- - `--cloud-origin <origin>`: API origin (default: https://cloud.mobify.com)
156
- - `--credentials-file <file>`: Credentials file location
157
- - `--api-key <api-key>`: MRT API key
155
+ - `--mrt-backend <legacy|scapi|auto>`: MRT API to use (env: `MRT_BACKEND`, default `legacy`). See [MRT backends](#mrt-backends).
156
+ - `--api-key <api-key>`: MRT API key (legacy backend; env: `MRT_API_KEY`)
157
+ - `-u, --cloud-origin <origin>` (alias `-o`): MRT API origin (legacy backend; default: https://cloud.mobify.com)
158
+ - `-c, --credentials-file <file>`: Credentials file location (legacy backend; default: `~/.mobify`)
158
159
 
159
- Backward compatibility:
160
+ `--project-slug` is still accepted as a deprecated alias for `--project`.
160
161
 
161
- - `--project-slug` is supported as a deprecated alias for `--project`
162
- - `--target` is supported as a deprecated alias for `--environment`
163
- - `MRT_PROJECT` and `MRT_TARGET` are supported as fallback env vars for `project` and `environment`
162
+ #### MRT backends
163
+
164
+ `push` and `config inspect` can talk to Managed Runtime through either API:
165
+
166
+ | `--mrt-backend` | Behavior |
167
+ | --- | --- |
168
+ | `legacy` (default) | MRT Cloud API with an MRT API key. |
169
+ | `scapi` | SCAPI with Account Manager OAuth. Errors if the prerequisites are missing. |
170
+ | `auto` | SCAPI when its prerequisites are configured, otherwise legacy. Falls back to legacy on safe pre-request errors. |
171
+
172
+ The SCAPI backend needs `SFCC_CLIENT_ID`, `SFCC_CLIENT_SECRET`, `SFCC_SHORTCODE`, and `SFCC_TENANT_ID`, and an API client with the `sfcc.storefront.deployments.rw` scope (`push`) or `sfcc.storefront.environments` (`config inspect`). See [MRT Backends](https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#mrt-backends).
173
+
174
+ #### Deploy cartridge
175
+
176
+ `deploy-cartridge` looks up and reloads the active code version through SCAPI (`sfcc.scripts` / `sfcc.scripts.rw` scopes) when `SFCC_SHORTCODE` and `SFCC_TENANT_ID` are set, and falls back to OCAPI otherwise. Force one with `--api-backend scapi|ocapi` (env: `SFCC_API_BACKEND`).
164
177
 
165
178
  #### Manage extensions
166
179
 
package/dist/bundle.js CHANGED
@@ -9,6 +9,20 @@ import archiver from "archiver";
9
9
  /**
10
10
  * Create a bundle from the build directory
11
11
  */
12
+ /**
13
+ * Dependencies recorded in bundle metadata: the project's declared dependencies
14
+ * plus the resolved versions of PWA Kit / Storefront Next packages.
15
+ */
16
+ const getBundleDependencies = (projectDirectory) => {
17
+ const { dependencies = {}, devDependencies = {} } = getProjectPkg(projectDirectory);
18
+ const dependencyTree = getProjectDependencyTree(projectDirectory);
19
+ const pwaKitDeps = dependencyTree ? getPwaKitDependencies(dependencyTree) : {};
20
+ return {
21
+ ...dependencies,
22
+ ...devDependencies,
23
+ ...pwaKitDeps
24
+ };
25
+ };
12
26
  const createBundle = async (options) => {
13
27
  const { message, ssr_parameters, ssr_only, ssr_shared, buildDirectory, projectDirectory, projectSlug } = options;
14
28
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "storefront-next-dev-push-"));
@@ -38,14 +52,7 @@ const createBundle = async (options) => {
38
52
  archive.on("error", reject);
39
53
  output.on("finish", () => {
40
54
  try {
41
- const { dependencies = {}, devDependencies = {} } = getProjectPkg(projectDirectory);
42
- const dependencyTree = getProjectDependencyTree(projectDirectory);
43
- const pwaKitDeps = dependencyTree ? getPwaKitDependencies(dependencyTree) : {};
44
- const bundle_metadata = { dependencies: {
45
- ...dependencies,
46
- ...devDependencies,
47
- ...pwaKitDeps
48
- } };
55
+ const bundle_metadata = { dependencies: getBundleDependencies(projectDirectory) };
49
56
  const data = fs.readFileSync(destination);
50
57
  const encoding = "base64";
51
58
  fs.rmSync(tmpDir, { recursive: true });
@@ -80,4 +87,4 @@ const createBundle = async (options) => {
80
87
  };
81
88
 
82
89
  //#endregion
83
- export { createBundle as t };
90
+ export { getBundleDependencies as n, createBundle as t };
@@ -19,6 +19,27 @@ const LEVEL_PRIORITY = {
19
19
  info: 2,
20
20
  debug: 3
21
21
  };
22
+ /**
23
+ * Type guard for a {@link LogLevel} name. Uses `Object.hasOwn` (not `in`) so
24
+ * inherited properties like `toString` don't falsely validate an env value.
25
+ */
26
+ function isLogLevel(value) {
27
+ return Object.hasOwn(LEVEL_PRIORITY, value);
28
+ }
29
+ /**
30
+ * Managed Runtime injects `MRT_LOG_LEVEL` as a numeric string from its own
31
+ * `LogLevel` enum (`0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=FATAL`), not a
32
+ * level name — so it must be mapped, not matched against {@link LEVEL_PRIORITY}'s
33
+ * keys. `TRACE` and `FATAL` clamp to the closest level this logger supports.
34
+ */
35
+ const MRT_NUMERIC_TO_LEVEL = {
36
+ "0": "debug",
37
+ "1": "debug",
38
+ "2": "info",
39
+ "3": "warn",
40
+ "4": "error",
41
+ "5": "error"
42
+ };
22
43
  let overrideLevel;
23
44
  /**
24
45
  * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.
@@ -42,8 +63,10 @@ function debugEnablesSfnext() {
42
63
  }
43
64
  function resolveLevel() {
44
65
  if (overrideLevel) return overrideLevel;
45
- const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;
46
- if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel;
66
+ const mrtLevel = process.env.MRT_LOG_LEVEL;
67
+ if (mrtLevel && mrtLevel in MRT_NUMERIC_TO_LEVEL) return MRT_NUMERIC_TO_LEVEL[mrtLevel];
68
+ const sfccLevel = process.env.SFCC_LOG_LEVEL;
69
+ if (sfccLevel && isLogLevel(sfccLevel)) return sfccLevel;
47
70
  if (debugEnablesSfnext()) return "debug";
48
71
  if (process.env.NODE_ENV === "production") return "warn";
49
72
  return "info";
@@ -79,6 +102,23 @@ const logger = {
79
102
  //#endregion
80
103
  //#region src/cartridge-services/react-router-config.ts
81
104
  let isCliAvailable = null;
105
+ function filePathEndsWithRoute(filePath, routeFile) {
106
+ const routeFileNormalized = routeFile.replace(/^\.\//, "");
107
+ return filePath === routeFileNormalized || filePath.endsWith(`/${routeFileNormalized}`);
108
+ }
109
+ /**
110
+ * Match the selected vertical route shadow to its canonical route module.
111
+ *
112
+ * `VERTICAL` is optional and selects the overlay directory reported by React Router,
113
+ * for example `furniture` for `verticals/furniture/routes/_app._index.tsx`.
114
+ */
115
+ function routeFileMatches(filePath, routeFile) {
116
+ if (filePathEndsWithRoute(filePath, routeFile)) return true;
117
+ const vertical = process.env.VERTICAL;
118
+ const verticalRoutePrefix = vertical ? `verticals/${vertical}/routes/` : void 0;
119
+ if (!verticalRoutePrefix || !routeFile.startsWith(verticalRoutePrefix)) return false;
120
+ return filePathEndsWithRoute(filePath, `routes/${routeFile.slice(verticalRoutePrefix.length)}`);
121
+ }
82
122
  function checkReactRouterCli(projectDirectory) {
83
123
  if (isCliAvailable !== null) return isCliAvailable;
84
124
  try {
@@ -132,7 +172,7 @@ function getReactRouterRoutes(projectDirectory) {
132
172
  * This ensures we get the exact same route resolution as React Router uses internally.
133
173
  * @param filePath - Absolute path to the route file
134
174
  * @param projectRoot - The project root directory
135
- * @returns The route path (e.g., '/cart', '/product/:productId')
175
+ * @returns The route path (e.g., '/cart', '/p/*')
136
176
  * @example
137
177
  * const route = filePathToRoute('/path/to/project/src/routes/_app.cart.tsx', '/path/to/project');
138
178
  * // Returns: '/cart'
@@ -140,10 +180,13 @@ function getReactRouterRoutes(projectDirectory) {
140
180
  function filePathToRoute(filePath, projectRoot) {
141
181
  const filePathPosix = filePath.replace(/\\/g, "/");
142
182
  const canonicalRoutes = flattenRoutes(getReactRouterRoutes(projectRoot)).filter((route) => !route.id.endsWith("--root-duplicate"));
143
- for (const route of canonicalRoutes) {
144
- const routeFilePosix = route.file.replace(/\\/g, "/");
145
- const routeFileNormalized = routeFilePosix.replace(/^\.\//, "");
146
- if (filePathPosix.endsWith(routeFilePosix) || filePathPosix.endsWith(`/${routeFilePosix}`) || filePathPosix.endsWith(routeFileNormalized) || filePathPosix.endsWith(`/${routeFileNormalized}`)) return route.path;
183
+ for (const route of canonicalRoutes) if (routeFileMatches(filePathPosix, route.file.replace(/\\/g, "/"))) {
184
+ const seoAliases = canonicalRoutes.filter((candidate) => candidate.id.startsWith(`${route.id}--seo-alias--`)).sort((left, right) => left.path.localeCompare(right.path));
185
+ if (seoAliases.length > 0) {
186
+ if (seoAliases.length > 1) logger.warn(`Multiple SEO route aliases match ${filePath}; using ${seoAliases[0].path} for Page Designer metadata.`);
187
+ return seoAliases[0].path;
188
+ }
189
+ return route.path;
147
190
  }
148
191
  logger.warn(`Could not find route for file: ${filePath}`);
149
192
  return "/unknown";