@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.
- package/README.md +45 -36
- package/bin/run.js +12 -0
- package/dist/bundle.js +83 -0
- package/dist/cartridge-services/index.d.ts +2 -26
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +3 -336
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/create-bundle.js +107 -0
- package/dist/commands/create-instructions.js +174 -0
- package/dist/commands/create-storefront.js +210 -0
- package/dist/commands/deploy-cartridge.js +52 -0
- package/dist/commands/dev.js +122 -0
- package/dist/commands/extensions/create.js +38 -0
- package/dist/commands/extensions/install.js +44 -0
- package/dist/commands/extensions/list.js +21 -0
- package/dist/commands/extensions/remove.js +38 -0
- package/dist/commands/generate-cartridge.js +35 -0
- package/dist/commands/prepare-local.js +30 -0
- package/dist/commands/preview.js +101 -0
- package/dist/commands/push.js +139 -0
- package/dist/config.js +87 -0
- package/dist/configs/react-router.config.js +3 -1
- package/dist/configs/react-router.config.js.map +1 -1
- package/dist/dependency-utils.js +314 -0
- package/dist/entry/client.d.ts +1 -0
- package/dist/entry/client.js +28 -0
- package/dist/entry/client.js.map +1 -0
- package/dist/entry/server.d.ts +15 -0
- package/dist/entry/server.d.ts.map +1 -0
- package/dist/entry/server.js +35 -0
- package/dist/entry/server.js.map +1 -0
- package/dist/flags.js +11 -0
- package/dist/generate-cartridge.js +620 -0
- package/dist/hooks/init.js +47 -0
- package/dist/index.d.ts +9 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +413 -621
- package/dist/index.js.map +1 -1
- package/dist/local-dev-setup.js +176 -0
- package/dist/logger.js +105 -0
- package/dist/manage-extensions.js +329 -0
- package/dist/mrt/ssr.mjs +3 -3
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +4 -4
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/server.js +425 -0
- package/dist/utils.js +126 -0
- package/package.json +44 -9
- package/dist/cli.js +0 -3393
- /package/{LICENSE.txt → LICENSE} +0 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { c as warn, r as info } from "./logger.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { createRequestHandler } from "@react-router/express";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
10
|
+
import compression from "compression";
|
|
11
|
+
import zlib from "node:zlib";
|
|
12
|
+
import morgan from "morgan";
|
|
13
|
+
import { minimatch } from "minimatch";
|
|
14
|
+
|
|
15
|
+
//#region src/server/ts-import.ts
|
|
16
|
+
/**
|
|
17
|
+
* Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
|
|
18
|
+
*
|
|
19
|
+
* @param tsconfigPath - Path to tsconfig.json
|
|
20
|
+
* @param projectDirectory - Project root directory for resolving relative paths
|
|
21
|
+
* @returns Record of alias mappings for jiti
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
|
|
25
|
+
* // Returns: { "@/": "/absolute/path/to/src/" }
|
|
26
|
+
*/
|
|
27
|
+
function parseTsconfigPaths(tsconfigPath, projectDirectory) {
|
|
28
|
+
const alias = {};
|
|
29
|
+
if (!existsSync(tsconfigPath)) return alias;
|
|
30
|
+
try {
|
|
31
|
+
const tsconfigContent = readFileSync(tsconfigPath, "utf-8");
|
|
32
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
33
|
+
const paths = tsconfig.compilerOptions?.paths;
|
|
34
|
+
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
35
|
+
if (paths) {
|
|
36
|
+
for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
|
|
37
|
+
const aliasKey = key.replace(/\/\*$/, "/");
|
|
38
|
+
alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
const sortedAlias = {};
|
|
43
|
+
Object.keys(alias).sort((a, b) => b.length - a.length).forEach((key) => {
|
|
44
|
+
sortedAlias[key] = alias[key];
|
|
45
|
+
});
|
|
46
|
+
return sortedAlias;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Import a TypeScript file using jiti with proper path alias resolution.
|
|
50
|
+
* This is a cross-platform alternative to tsx that works on Windows.
|
|
51
|
+
*
|
|
52
|
+
* @param filePath - Absolute path to the TypeScript file to import
|
|
53
|
+
* @param options - Import options including project directory
|
|
54
|
+
* @returns The imported module
|
|
55
|
+
*/
|
|
56
|
+
async function importTypescript(filePath, options) {
|
|
57
|
+
const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
|
|
58
|
+
const { createJiti } = await import("jiti");
|
|
59
|
+
const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
|
|
60
|
+
return createJiti(import.meta.url, {
|
|
61
|
+
fsCache: false,
|
|
62
|
+
interopDefault: true,
|
|
63
|
+
alias
|
|
64
|
+
}).import(filePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/server/config.ts
|
|
69
|
+
/**
|
|
70
|
+
* This is a temporary function before we move the config implementation from
|
|
71
|
+
* template-retail-rsc-app to the SDK.
|
|
72
|
+
*
|
|
73
|
+
* @ TODO: Remove this function after we move the config implementation from
|
|
74
|
+
* template-retail-rsc-app to the SDK.
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
function loadConfigFromEnv() {
|
|
78
|
+
const shortCode = process.env.PUBLIC__app__commerce__api__shortCode;
|
|
79
|
+
const organizationId = process.env.PUBLIC__app__commerce__api__organizationId;
|
|
80
|
+
const clientId = process.env.PUBLIC__app__commerce__api__clientId;
|
|
81
|
+
const siteId = process.env.PUBLIC__app__commerce__api__siteId;
|
|
82
|
+
const proxy = process.env.PUBLIC__app__commerce__api__proxy || "/mobify/proxy/api";
|
|
83
|
+
const proxyHost = process.env.SCAPI_PROXY_HOST;
|
|
84
|
+
if (!shortCode && !proxyHost) throw new Error("Missing PUBLIC__app__commerce__api__shortCode environment variable.\nPlease set it in your .env file or environment.");
|
|
85
|
+
if (!organizationId) throw new Error("Missing PUBLIC__app__commerce__api__organizationId environment variable.\nPlease set it in your .env file or environment.");
|
|
86
|
+
if (!clientId) throw new Error("Missing PUBLIC__app__commerce__api__clientId environment variable.\nPlease set it in your .env file or environment.");
|
|
87
|
+
if (!siteId) throw new Error("Missing PUBLIC__app__commerce__api__siteId environment variable.\nPlease set it in your .env file or environment.");
|
|
88
|
+
return { commerce: { api: {
|
|
89
|
+
shortCode: shortCode || "",
|
|
90
|
+
organizationId,
|
|
91
|
+
clientId,
|
|
92
|
+
siteId,
|
|
93
|
+
proxy,
|
|
94
|
+
proxyHost
|
|
95
|
+
} } };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Load storefront-next project configuration from config.server.ts.
|
|
99
|
+
* Requires projectDirectory to be provided.
|
|
100
|
+
*
|
|
101
|
+
* @param projectDirectory - Project directory to load config.server.ts from
|
|
102
|
+
* @throws Error if config.server.ts is not found or invalid
|
|
103
|
+
*/
|
|
104
|
+
async function loadProjectConfig(projectDirectory) {
|
|
105
|
+
const configPath = resolve(projectDirectory, "config.server.ts");
|
|
106
|
+
const tsconfigPath = resolve(projectDirectory, "tsconfig.json");
|
|
107
|
+
if (!existsSync(configPath)) throw new Error(`config.server.ts not found at ${configPath}.\nPlease ensure config.server.ts exists in your project root.`);
|
|
108
|
+
const config = (await importTypescript(configPath, {
|
|
109
|
+
projectDirectory,
|
|
110
|
+
tsconfigPath
|
|
111
|
+
})).default;
|
|
112
|
+
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.");
|
|
113
|
+
const api = config.app.commerce.api;
|
|
114
|
+
const proxyHost = process.env.SCAPI_PROXY_HOST;
|
|
115
|
+
if (!api.shortCode && !proxyHost) throw new Error("Missing shortCode in config.server.ts commerce.api configuration");
|
|
116
|
+
if (!api.organizationId) throw new Error("Missing organizationId in config.server.ts commerce.api configuration");
|
|
117
|
+
if (!api.clientId) throw new Error("Missing clientId in config.server.ts commerce.api configuration");
|
|
118
|
+
if (!api.siteId) throw new Error("Missing siteId in config.server.ts commerce.api configuration");
|
|
119
|
+
return { commerce: { api: {
|
|
120
|
+
shortCode: api.shortCode || "",
|
|
121
|
+
organizationId: api.organizationId,
|
|
122
|
+
clientId: api.clientId,
|
|
123
|
+
siteId: api.siteId,
|
|
124
|
+
proxy: api.proxy || "/mobify/proxy/api",
|
|
125
|
+
proxyHost
|
|
126
|
+
} } };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/utils/paths.ts
|
|
131
|
+
/**
|
|
132
|
+
* Get the Commerce Cloud API URL from a short code
|
|
133
|
+
*/
|
|
134
|
+
function getCommerceCloudApiUrl(shortCode, proxyHost) {
|
|
135
|
+
return proxyHost || `https://${shortCode}.api.commercecloud.salesforce.com`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the bundle path for static assets
|
|
139
|
+
*/
|
|
140
|
+
function getBundlePath(bundleId) {
|
|
141
|
+
return `/mobify/bundle/${bundleId}/client/`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/server/middleware/proxy.ts
|
|
146
|
+
/**
|
|
147
|
+
* Create proxy middleware for Commerce Cloud API
|
|
148
|
+
* Proxies requests from /mobify/proxy/api to the Commerce Cloud API
|
|
149
|
+
*/
|
|
150
|
+
function createCommerceProxyMiddleware(config) {
|
|
151
|
+
return createProxyMiddleware({
|
|
152
|
+
target: getCommerceCloudApiUrl(config.commerce.api.shortCode, config.commerce.api.proxyHost),
|
|
153
|
+
changeOrigin: true,
|
|
154
|
+
secure: !config.commerce.api.proxyHost
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/server/middleware/static.ts
|
|
160
|
+
/**
|
|
161
|
+
* Create static file serving middleware for client assets
|
|
162
|
+
* Serves files from build/client at /mobify/bundle/{BUNDLE_ID}/client/
|
|
163
|
+
*/
|
|
164
|
+
function createStaticMiddleware(bundleId, projectDirectory) {
|
|
165
|
+
const bundlePath = getBundlePath(bundleId);
|
|
166
|
+
const clientBuildDir = path.join(projectDirectory, "build", "client");
|
|
167
|
+
info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
|
|
168
|
+
return express.static(clientBuildDir, { setHeaders: (res) => {
|
|
169
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
170
|
+
res.setHeader("x-local-static-cache-control", "1");
|
|
171
|
+
} });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/server/middleware/compression.ts
|
|
176
|
+
/**
|
|
177
|
+
* Parse and validate COMPRESSION_LEVEL environment variable
|
|
178
|
+
* @returns Valid compression level (0-9) or default compression level
|
|
179
|
+
*/
|
|
180
|
+
function getCompressionLevel() {
|
|
181
|
+
const raw = process.env.COMPRESSION_LEVEL;
|
|
182
|
+
const DEFAULT = zlib.constants.Z_DEFAULT_COMPRESSION;
|
|
183
|
+
if (raw == null || raw.trim() === "") return DEFAULT;
|
|
184
|
+
const level = Number(raw);
|
|
185
|
+
if (!(Number.isInteger(level) && level >= 0 && level <= 9)) {
|
|
186
|
+
warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
|
|
187
|
+
return DEFAULT;
|
|
188
|
+
}
|
|
189
|
+
return level;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create compression middleware for gzip/brotli compression
|
|
193
|
+
* Used in preview mode to optimize response sizes
|
|
194
|
+
*/
|
|
195
|
+
function createCompressionMiddleware() {
|
|
196
|
+
return compression({
|
|
197
|
+
filter: (req, res) => {
|
|
198
|
+
if (req.headers["x-no-compression"]) return false;
|
|
199
|
+
return compression.filter(req, res);
|
|
200
|
+
},
|
|
201
|
+
level: getCompressionLevel()
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/server/middleware/logging.ts
|
|
207
|
+
/**
|
|
208
|
+
* Patterns for URLs to skip logging (static assets and Vite internals)
|
|
209
|
+
*/
|
|
210
|
+
const SKIP_PATTERNS = [
|
|
211
|
+
"/@vite/**",
|
|
212
|
+
"/@id/**",
|
|
213
|
+
"/@fs/**",
|
|
214
|
+
"/@react-router/**",
|
|
215
|
+
"/src/**",
|
|
216
|
+
"/node_modules/**",
|
|
217
|
+
"**/*.js",
|
|
218
|
+
"**/*.css",
|
|
219
|
+
"**/*.ts",
|
|
220
|
+
"**/*.tsx",
|
|
221
|
+
"**/*.js.map",
|
|
222
|
+
"**/*.css.map"
|
|
223
|
+
];
|
|
224
|
+
/**
|
|
225
|
+
* Create request logging middleware
|
|
226
|
+
* Used in dev and preview modes for request visibility
|
|
227
|
+
*/
|
|
228
|
+
function createLoggingMiddleware() {
|
|
229
|
+
morgan.token("status-colored", (req, res) => {
|
|
230
|
+
const status = res.statusCode;
|
|
231
|
+
let color = chalk.green;
|
|
232
|
+
if (status >= 500) color = chalk.red;
|
|
233
|
+
else if (status >= 400) color = chalk.yellow;
|
|
234
|
+
else if (status >= 300) color = chalk.cyan;
|
|
235
|
+
return color(String(status));
|
|
236
|
+
});
|
|
237
|
+
morgan.token("method-colored", (req) => {
|
|
238
|
+
const method = req.method;
|
|
239
|
+
const colors = {
|
|
240
|
+
GET: chalk.green,
|
|
241
|
+
POST: chalk.blue,
|
|
242
|
+
PUT: chalk.yellow,
|
|
243
|
+
DELETE: chalk.red,
|
|
244
|
+
PATCH: chalk.magenta
|
|
245
|
+
};
|
|
246
|
+
return (method && colors[method] || chalk.white)(method);
|
|
247
|
+
});
|
|
248
|
+
return morgan((tokens, req, res) => {
|
|
249
|
+
return [
|
|
250
|
+
chalk.gray("["),
|
|
251
|
+
tokens["method-colored"](req, res),
|
|
252
|
+
chalk.gray("]"),
|
|
253
|
+
tokens.url(req, res),
|
|
254
|
+
"-",
|
|
255
|
+
tokens["status-colored"](req, res),
|
|
256
|
+
chalk.gray(`(${tokens["response-time"](req, res)}ms)`)
|
|
257
|
+
].join(" ");
|
|
258
|
+
}, { skip: (req) => {
|
|
259
|
+
return SKIP_PATTERNS.some((pattern) => minimatch(req.url, pattern, { dot: true }));
|
|
260
|
+
} });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/server/middleware/host-header.ts
|
|
265
|
+
/**
|
|
266
|
+
* Normalizes the X-Forwarded-Host header to support React Router's CSRF validation features.
|
|
267
|
+
*
|
|
268
|
+
* NOTE: This middleware performs header manipulation as a temporary, internal
|
|
269
|
+
* solution for MRT/Lambda environments. It may be updated or removed if React Router
|
|
270
|
+
* introduces a first-class configuration for validating against forwarded headers.
|
|
271
|
+
*
|
|
272
|
+
* React Router v7.12+ uses the X-Forwarded-Host header (preferring it over Host)
|
|
273
|
+
* to validate request origins for security. In Managed Runtime (MRT) with a vanity
|
|
274
|
+
* domain, the eCDN automatically sets the X-Forwarded-Host to the vanity domain.
|
|
275
|
+
* React Router handles cases where this header contains multiple comma-separated
|
|
276
|
+
* values by prioritizing the first entry.
|
|
277
|
+
*
|
|
278
|
+
* This middleware ensures that X-Forwarded-Host is always present by falling back
|
|
279
|
+
* to a configured public domain if the header is missing (e.g., local development).
|
|
280
|
+
* By only modifying X-Forwarded-Host, we provide a consistent environment for
|
|
281
|
+
* React Router's security checks without modifying the internal 'Host' header,
|
|
282
|
+
* which is required for environment-specific routing logic (e.g., Hybrid Proxy).
|
|
283
|
+
*
|
|
284
|
+
* Priority order:
|
|
285
|
+
* 1. X-Forwarded-Host: Automatically set by eCDN for vanity domains.
|
|
286
|
+
* 2. EXTERNAL_DOMAIN_NAME: Fallback environment variable for the public domain
|
|
287
|
+
* used when no forwarded headers are present (e.g., local development).
|
|
288
|
+
*/
|
|
289
|
+
function createHostHeaderMiddleware() {
|
|
290
|
+
return (req, _res, next) => {
|
|
291
|
+
if (!req.get("x-forwarded-host") && process.env.EXTERNAL_DOMAIN_NAME) req.headers["x-forwarded-host"] = process.env.EXTERNAL_DOMAIN_NAME;
|
|
292
|
+
next();
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/server/utils.ts
|
|
298
|
+
/**
|
|
299
|
+
* Patch React Router build to rewrite asset URLs with the correct bundle path
|
|
300
|
+
* This is needed because the build output uses /assets/ but we preview at /mobify/bundle/{BUNDLE_ID}/client/assets/
|
|
301
|
+
*/
|
|
302
|
+
function patchReactRouterBuild(build, bundleId) {
|
|
303
|
+
const bundlePath = getBundlePath(bundleId);
|
|
304
|
+
const patchedAssetsJson = JSON.stringify(build.assets).replace(/"\/assets\//g, `"${bundlePath}assets/`);
|
|
305
|
+
const newAssets = JSON.parse(patchedAssetsJson);
|
|
306
|
+
return Object.assign({}, build, {
|
|
307
|
+
publicPath: bundlePath,
|
|
308
|
+
assets: newAssets
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/server/modes.ts
|
|
314
|
+
/**
|
|
315
|
+
* Default feature configuration for each server mode
|
|
316
|
+
*/
|
|
317
|
+
const ServerModeFeatureMap = {
|
|
318
|
+
development: {
|
|
319
|
+
enableProxy: true,
|
|
320
|
+
enableStaticServing: false,
|
|
321
|
+
enableCompression: false,
|
|
322
|
+
enableLogging: true,
|
|
323
|
+
enableAssetUrlPatching: false
|
|
324
|
+
},
|
|
325
|
+
preview: {
|
|
326
|
+
enableProxy: true,
|
|
327
|
+
enableStaticServing: true,
|
|
328
|
+
enableCompression: true,
|
|
329
|
+
enableLogging: true,
|
|
330
|
+
enableAssetUrlPatching: true
|
|
331
|
+
},
|
|
332
|
+
production: {
|
|
333
|
+
enableProxy: false,
|
|
334
|
+
enableStaticServing: false,
|
|
335
|
+
enableCompression: true,
|
|
336
|
+
enableLogging: true,
|
|
337
|
+
enableAssetUrlPatching: true
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/server/index.ts
|
|
343
|
+
/** Relative path to the middleware registry TypeScript source (development). Must match appDirectory + server dir + filename used by buildMiddlewareRegistry plugin. */
|
|
344
|
+
const RELATIVE_MIDDLEWARE_REGISTRY_SOURCE = "src/server/middleware-registry.ts";
|
|
345
|
+
/** Extensions to try for the built middlewares module (ESM first, then CJS for backwards compatibility). */
|
|
346
|
+
const MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS = [
|
|
347
|
+
".mjs",
|
|
348
|
+
".js",
|
|
349
|
+
".cjs"
|
|
350
|
+
];
|
|
351
|
+
/** All paths to try when loading the built middlewares (base + extension). */
|
|
352
|
+
const RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS = ["bld/server/middleware-registry", "build/server/middleware-registry"].flatMap((base) => MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS.map((ext) => `${base}${ext}`));
|
|
353
|
+
/**
|
|
354
|
+
* Create a unified Express server for development, preview, or production mode
|
|
355
|
+
*/
|
|
356
|
+
async function createServer(options) {
|
|
357
|
+
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;
|
|
358
|
+
if (mode === "development" && !vite) throw new Error("Vite dev server instance is required for development mode");
|
|
359
|
+
if ((mode === "preview" || mode === "production") && !build) throw new Error("React Router server build is required for preview/production mode");
|
|
360
|
+
const config = providedConfig ?? loadConfigFromEnv();
|
|
361
|
+
const bundleId = process.env.BUNDLE_ID ?? "local";
|
|
362
|
+
const app = express();
|
|
363
|
+
app.disable("x-powered-by");
|
|
364
|
+
if (enableLogging) app.use(createLoggingMiddleware());
|
|
365
|
+
if (enableCompression && !streaming) app.use(createCompressionMiddleware());
|
|
366
|
+
if (enableStaticServing && build) {
|
|
367
|
+
const bundlePath = getBundlePath(bundleId);
|
|
368
|
+
app.use(bundlePath, createStaticMiddleware(bundleId, projectDirectory));
|
|
369
|
+
}
|
|
370
|
+
let registry = null;
|
|
371
|
+
if (mode === "development") {
|
|
372
|
+
const middlewareRegistryPath = resolve(projectDirectory, RELATIVE_MIDDLEWARE_REGISTRY_SOURCE);
|
|
373
|
+
if (existsSync(middlewareRegistryPath)) registry = await importTypescript(middlewareRegistryPath, { projectDirectory });
|
|
374
|
+
} else {
|
|
375
|
+
const possiblePaths = RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS.map((p) => resolve(projectDirectory, p));
|
|
376
|
+
let builtRegistryPath = null;
|
|
377
|
+
for (const path$1 of possiblePaths) if (existsSync(path$1)) {
|
|
378
|
+
builtRegistryPath = path$1;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
if (builtRegistryPath) registry = await import(pathToFileURL(builtRegistryPath).href);
|
|
382
|
+
}
|
|
383
|
+
if (registry?.customMiddlewares && Array.isArray(registry.customMiddlewares)) registry.customMiddlewares.forEach((entry) => {
|
|
384
|
+
app.use(entry.handler);
|
|
385
|
+
});
|
|
386
|
+
if (mode === "development" && vite) app.use(vite.middlewares);
|
|
387
|
+
if (enableProxy) app.use(config.commerce.api.proxy, createCommerceProxyMiddleware(config));
|
|
388
|
+
app.use(createHostHeaderMiddleware());
|
|
389
|
+
app.all("*", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
|
|
390
|
+
return app;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Create the SSR request handler based on mode
|
|
394
|
+
*/
|
|
395
|
+
async function createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching) {
|
|
396
|
+
if (mode === "development" && vite) {
|
|
397
|
+
const { isRunnableDevEnvironment } = await import("vite");
|
|
398
|
+
return async (req, res, next) => {
|
|
399
|
+
try {
|
|
400
|
+
const ssrEnvironment = vite.environments.ssr;
|
|
401
|
+
if (!isRunnableDevEnvironment(ssrEnvironment)) {
|
|
402
|
+
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"));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
await createRequestHandler({
|
|
406
|
+
build: await ssrEnvironment.runner.import("virtual:react-router/server-build"),
|
|
407
|
+
mode: process.env.NODE_ENV
|
|
408
|
+
})(req, res, next);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
vite.ssrFixStacktrace(error);
|
|
411
|
+
next(error);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
} else if (build) {
|
|
415
|
+
let patchedBuild = build;
|
|
416
|
+
if (enableAssetUrlPatching) patchedBuild = patchReactRouterBuild(build, bundleId);
|
|
417
|
+
return createRequestHandler({
|
|
418
|
+
build: patchedBuild,
|
|
419
|
+
mode: process.env.NODE_ENV
|
|
420
|
+
});
|
|
421
|
+
} else throw new Error("Invalid server configuration: no vite or build provided");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
//#endregion
|
|
425
|
+
export { getCommerceCloudApiUrl as n, loadProjectConfig as r, createServer as t };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { c as warn, t as debug } from "./logger.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
|
|
8
|
+
//#region src/utils.ts
|
|
9
|
+
const getDefaultBuildDir = (targetDir) => path.join(targetDir, "build");
|
|
10
|
+
const NODE_ENV = process.env.NODE_ENV || "development";
|
|
11
|
+
/**
|
|
12
|
+
* Get project package.json
|
|
13
|
+
*/
|
|
14
|
+
const getProjectPkg = (projectDir) => {
|
|
15
|
+
const packagePath = path.join(projectDir, "package.json");
|
|
16
|
+
try {
|
|
17
|
+
return fs.readJSONSync(packagePath);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(`Could not read project package at "${packagePath}"`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Load .env file from project directory
|
|
24
|
+
*/
|
|
25
|
+
const loadEnvFile = (projectDir) => {
|
|
26
|
+
const envPath = path.join(projectDir, ".env");
|
|
27
|
+
if (fs.existsSync(envPath)) dotenv.config({ path: envPath });
|
|
28
|
+
else warn("No .env file found");
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get MRT configuration with priority logic: .env -> package.json -> defaults
|
|
32
|
+
*/
|
|
33
|
+
const getMrtConfig = (projectDir) => {
|
|
34
|
+
loadEnvFile(projectDir);
|
|
35
|
+
const pkg = getProjectPkg(projectDir);
|
|
36
|
+
const defaultMrtProject = process.env.MRT_PROJECT ?? pkg.name;
|
|
37
|
+
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.");
|
|
38
|
+
const defaultMrtTarget = process.env.MRT_TARGET ?? void 0;
|
|
39
|
+
debug("MRT configuration resolved", {
|
|
40
|
+
projectDir,
|
|
41
|
+
envMrtProject: process.env.MRT_PROJECT,
|
|
42
|
+
envMrtTarget: process.env.MRT_TARGET,
|
|
43
|
+
packageName: pkg.name,
|
|
44
|
+
resolvedProject: defaultMrtProject,
|
|
45
|
+
resolvedTarget: defaultMrtTarget
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
defaultMrtProject,
|
|
49
|
+
defaultMrtTarget
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Get project dependency tree (simplified version)
|
|
54
|
+
*/
|
|
55
|
+
const getProjectDependencyTree = (projectDir) => {
|
|
56
|
+
try {
|
|
57
|
+
const tmpFile = path.join(os.tmpdir(), `npm-ls-${Date.now()}.json`);
|
|
58
|
+
execSync(`npm ls --all --json > ${tmpFile}`, {
|
|
59
|
+
stdio: "ignore",
|
|
60
|
+
cwd: projectDir
|
|
61
|
+
});
|
|
62
|
+
const data = fs.readJSONSync(tmpFile);
|
|
63
|
+
fs.unlinkSync(tmpFile);
|
|
64
|
+
return data;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Get PWA Kit dependencies from dependency tree
|
|
71
|
+
*/
|
|
72
|
+
const getPwaKitDependencies = (dependencyTree) => {
|
|
73
|
+
if (!dependencyTree) return {};
|
|
74
|
+
const pwaKitDependencies = ["@salesforce/storefront-next-dev"];
|
|
75
|
+
const result = {};
|
|
76
|
+
const searchDeps = (tree) => {
|
|
77
|
+
if (tree.dependencies) for (const [name, dep] of Object.entries(tree.dependencies)) {
|
|
78
|
+
if (pwaKitDependencies.includes(name)) result[name] = dep.version || "unknown";
|
|
79
|
+
if (dep.dependencies) searchDeps({ dependencies: dep.dependencies });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
searchDeps(dependencyTree);
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Get default commit message from git
|
|
87
|
+
*/
|
|
88
|
+
const getDefaultMessage = (projectDir) => {
|
|
89
|
+
try {
|
|
90
|
+
return `${execSync("git rev-parse --abbrev-ref HEAD", {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
cwd: projectDir
|
|
93
|
+
}).trim()}: ${execSync("git rev-parse --short HEAD", {
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
cwd: projectDir
|
|
96
|
+
}).trim()}`;
|
|
97
|
+
} catch {
|
|
98
|
+
debug("Using default bundle message as no message was provided and not in a Git repo.");
|
|
99
|
+
return "PWA Kit Bundle";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Given a project directory and a record of config overrides, generate a new .env file with the overrides based on the .env.default file.
|
|
104
|
+
* @param projectDir
|
|
105
|
+
* @param configOverrides
|
|
106
|
+
*/
|
|
107
|
+
const generateEnvFile = (projectDir, configOverrides) => {
|
|
108
|
+
const envDefaultPath = path.join(projectDir, ".env.default");
|
|
109
|
+
const envPath = path.join(projectDir, ".env");
|
|
110
|
+
if (!fs.existsSync(envDefaultPath)) {
|
|
111
|
+
console.warn(`${envDefaultPath} not found`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const envOutputLines = fs.readFileSync(envDefaultPath, "utf8").split("\n").map((line) => {
|
|
115
|
+
if (!line || line.trim().startsWith("#")) return line;
|
|
116
|
+
const eqIndex = line.indexOf("=");
|
|
117
|
+
if (eqIndex === -1) return line;
|
|
118
|
+
const key = line.slice(0, eqIndex);
|
|
119
|
+
const originalValue = line.slice(eqIndex + 1);
|
|
120
|
+
return `${key}=${(Object.prototype.hasOwnProperty.call(configOverrides, key) ? configOverrides[key] : void 0) ?? originalValue}`;
|
|
121
|
+
});
|
|
122
|
+
fs.writeFileSync(envPath, envOutputLines.join("\n"));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { getProjectDependencyTree as a, loadEnvFile as c, getMrtConfig as i, getDefaultBuildDir as n, getProjectPkg as o, getDefaultMessage as r, getPwaKitDependencies as s, generateEnvFile as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/storefront-next-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
4
|
"description": "Dev and build tools for SFCC Storefront Next",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,10 +42,41 @@
|
|
|
42
42
|
"types": "./dist/cartridge-services/index.d.ts",
|
|
43
43
|
"default": "./dist/cartridge-services/index.js"
|
|
44
44
|
}
|
|
45
|
+
},
|
|
46
|
+
"./entry/server": {
|
|
47
|
+
"import": {
|
|
48
|
+
"types": "./dist/entry/server.d.ts",
|
|
49
|
+
"default": "./dist/entry/server.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"./entry/client": {
|
|
53
|
+
"import": {
|
|
54
|
+
"types": "./dist/entry/client.d.ts",
|
|
55
|
+
"default": "./dist/entry/client.js"
|
|
56
|
+
}
|
|
45
57
|
}
|
|
46
58
|
},
|
|
47
59
|
"bin": {
|
|
48
|
-
"sfnext": "./
|
|
60
|
+
"sfnext": "./bin/run.js"
|
|
61
|
+
},
|
|
62
|
+
"oclif": {
|
|
63
|
+
"bin": "sfnext",
|
|
64
|
+
"dirname": "sfnext",
|
|
65
|
+
"commands": "./dist/commands",
|
|
66
|
+
"topicSeparator": " ",
|
|
67
|
+
"hooks": {
|
|
68
|
+
"init": "./dist/hooks/init"
|
|
69
|
+
},
|
|
70
|
+
"plugins": [
|
|
71
|
+
"@oclif/plugin-help",
|
|
72
|
+
"@oclif/plugin-plugins",
|
|
73
|
+
"@oclif/plugin-not-found"
|
|
74
|
+
],
|
|
75
|
+
"topics": {
|
|
76
|
+
"extensions": {
|
|
77
|
+
"description": "Manage storefront extensions"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
49
80
|
},
|
|
50
81
|
"files": [
|
|
51
82
|
"dist"
|
|
@@ -70,6 +101,7 @@
|
|
|
70
101
|
"vite": ">=7.0.0"
|
|
71
102
|
},
|
|
72
103
|
"devDependencies": {
|
|
104
|
+
"@oclif/test": "^4.1.15",
|
|
73
105
|
"@react-router/dev": "7.12.0",
|
|
74
106
|
"@serverless/event-mocks": "1.1.1",
|
|
75
107
|
"@types/archiver": "^6.0.2",
|
|
@@ -81,7 +113,6 @@
|
|
|
81
113
|
"@types/express": "4.17.23",
|
|
82
114
|
"@types/fs-extra": "^11.0.4",
|
|
83
115
|
"@types/glob": "^8.1.0",
|
|
84
|
-
"@types/handlebars": "^4.1.0",
|
|
85
116
|
"@types/jest": "30.0.0",
|
|
86
117
|
"@types/minimatch": "^5.1.2",
|
|
87
118
|
"@types/morgan": "^1.9.9",
|
|
@@ -107,16 +138,20 @@
|
|
|
107
138
|
},
|
|
108
139
|
"dependencies": {
|
|
109
140
|
"tsdown": "^0.15.4",
|
|
141
|
+
"@babel/generator": "7.28.5",
|
|
110
142
|
"@babel/parser": "^7.28.4",
|
|
111
143
|
"@babel/traverse": "^7.28.4",
|
|
112
144
|
"@babel/types": "7.28.4",
|
|
113
|
-
"@babel/generator": "7.28.5",
|
|
114
145
|
"@codegenie/serverless-express": "4.17.0",
|
|
115
146
|
"@h4ad/serverless-adapter": "4.4.0",
|
|
147
|
+
"@oclif/core": "^4.2.10",
|
|
148
|
+
"@oclif/plugin-help": "^6.2.36",
|
|
149
|
+
"@oclif/plugin-not-found": "^3.2.73",
|
|
150
|
+
"@oclif/plugin-plugins": "^5",
|
|
116
151
|
"@react-router/express": "7.12.0",
|
|
152
|
+
"@salesforce/b2c-tooling-sdk": "^0.5.4",
|
|
117
153
|
"archiver": "^7.0.1",
|
|
118
154
|
"chalk": "^5.3.0",
|
|
119
|
-
"commander": "^12.1.0",
|
|
120
155
|
"compressible": "2.0.18",
|
|
121
156
|
"compression": "^1.7.4",
|
|
122
157
|
"dotenv": "^16.4.7",
|
|
@@ -150,9 +185,9 @@
|
|
|
150
185
|
"lint": "eslint src --ext .ts,.tsx",
|
|
151
186
|
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
152
187
|
"clean": "shx rm -rf dist",
|
|
153
|
-
"push": "
|
|
154
|
-
"create-bundle": "
|
|
155
|
-
"generate-cartridge": "
|
|
156
|
-
"deploy-cartridge": "
|
|
188
|
+
"push": "./bin/run.js push",
|
|
189
|
+
"create-bundle": "./bin/run.js create-bundle",
|
|
190
|
+
"generate-cartridge": "./bin/run.js generate-cartridge",
|
|
191
|
+
"deploy-cartridge": "./bin/run.js deploy-cartridge"
|
|
157
192
|
}
|
|
158
193
|
}
|