@salesforce/storefront-next-dev 0.2.0-alpha.2 → 0.3.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/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +171 -50
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/create-bundle.js +12 -11
- package/dist/commands/create-instructions.js +7 -5
- package/dist/commands/create-storefront.js +18 -22
- package/dist/commands/deploy-cartridge.js +67 -26
- package/dist/commands/dev.js +6 -4
- package/dist/commands/extensions/create.js +2 -0
- package/dist/commands/extensions/install.js +3 -7
- package/dist/commands/extensions/list.js +2 -0
- package/dist/commands/extensions/remove.js +3 -7
- package/dist/commands/generate-cartridge.js +23 -2
- package/dist/commands/preview.js +15 -10
- package/dist/commands/push.js +25 -19
- package/dist/commands/validate-cartridge.js +51 -0
- package/dist/config.js +74 -47
- package/dist/configs/react-router.config.d.ts.map +1 -1
- package/dist/configs/react-router.config.js +36 -0
- package/dist/configs/react-router.config.js.map +1 -1
- package/dist/dependency-utils.js +14 -16
- package/dist/entry/server.d.ts.map +1 -1
- package/dist/entry/server.js +221 -11
- package/dist/entry/server.js.map +1 -1
- package/dist/generate-cartridge.js +106 -50
- package/dist/index.d.ts +127 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1147 -167
- package/dist/index.js.map +1 -1
- package/dist/local-dev-setup.js +13 -13
- package/dist/logger/index.d.ts +20 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +69 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger.js +79 -33
- package/dist/logger2.js +1 -0
- package/dist/manage-extensions.js +7 -13
- package/dist/mrt/ssr.mjs +60 -72
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +66 -78
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/react-router/Scripts.d.ts +1 -1
- package/dist/react-router/Scripts.d.ts.map +1 -1
- package/dist/react-router/Scripts.js +38 -2
- package/dist/react-router/Scripts.js.map +1 -1
- package/dist/server.js +296 -16
- package/dist/utils.js +4 -4
- package/dist/validate-cartridge.js +45 -0
- package/package.json +22 -5
package/dist/index.js
CHANGED
|
@@ -1,30 +1,106 @@
|
|
|
1
1
|
import path, { basename, extname, join, resolve } from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import path$1, { dirname, join as join$1, relative, resolve as resolve$1 } from "path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
6
|
import { parse } from "@babel/parser";
|
|
6
|
-
import { isJSXAttribute, isJSXElement, isJSXFragment, isJSXIdentifier, jsxClosingElement, jsxClosingFragment, jsxElement, jsxFragment, jsxIdentifier, jsxOpeningElement, jsxOpeningFragment, jsxText } from "@babel/types";
|
|
7
|
+
import { isArrayPattern, isClassDeclaration, isExportSpecifier, isFunctionDeclaration, isIdentifier, isJSXAttribute, isJSXElement, isJSXFragment, isJSXIdentifier, isMemberExpression, isObjectPattern, isObjectProperty, isRestElement, isVariableDeclaration, jsxClosingElement, jsxClosingFragment, jsxElement, jsxFragment, jsxIdentifier, jsxOpeningElement, jsxOpeningFragment, jsxText } from "@babel/types";
|
|
7
8
|
import { generate } from "@babel/generator";
|
|
8
9
|
import traverseModule from "@babel/traverse";
|
|
9
10
|
import fs$1, { existsSync, readFileSync, writeFileSync } from "fs";
|
|
10
11
|
import { glob } from "glob";
|
|
11
12
|
import { Node, Project, ts } from "ts-morph";
|
|
12
13
|
import fs$2, { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync } from "node:fs";
|
|
14
|
+
import { deadCodeElimination, findReferencedIdentifiers } from "babel-dead-code-elimination";
|
|
15
|
+
import httpProxy from "http-proxy";
|
|
16
|
+
import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
|
|
13
17
|
import express from "express";
|
|
14
18
|
import { createRequestHandler } from "@react-router/express";
|
|
15
19
|
import { pathToFileURL as pathToFileURL$1 } from "node:url";
|
|
16
20
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
17
|
-
import chalk from "chalk";
|
|
18
21
|
import compression from "compression";
|
|
19
22
|
import zlib from "node:zlib";
|
|
20
23
|
import morgan from "morgan";
|
|
21
24
|
import { minimatch } from "minimatch";
|
|
25
|
+
import { SpanStatusCode, context, trace } from "@opentelemetry/api";
|
|
26
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
27
|
+
import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
28
|
+
import { ExportResultCode, hrTimeToTimeStamp } from "@opentelemetry/core";
|
|
29
|
+
import { Resource } from "@opentelemetry/resources";
|
|
30
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
31
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
32
|
+
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
|
|
22
33
|
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
23
34
|
import { execSync } from "node:child_process";
|
|
24
35
|
import { tmpdir } from "node:os";
|
|
25
36
|
import { randomUUID } from "node:crypto";
|
|
26
37
|
import { npmRunPathEnv } from "npm-run-path";
|
|
27
38
|
|
|
39
|
+
//#region src/utils/logger.ts
|
|
40
|
+
const LEVEL_PRIORITY = {
|
|
41
|
+
error: 0,
|
|
42
|
+
warn: 1,
|
|
43
|
+
info: 2,
|
|
44
|
+
debug: 3
|
|
45
|
+
};
|
|
46
|
+
let overrideLevel;
|
|
47
|
+
/**
|
|
48
|
+
* Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.
|
|
49
|
+
* Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries
|
|
50
|
+
* (e.g. `DEBUG=express:*`).
|
|
51
|
+
*/
|
|
52
|
+
function debugEnablesSfnext() {
|
|
53
|
+
const raw = process.env.DEBUG?.trim();
|
|
54
|
+
if (!raw) return false;
|
|
55
|
+
const normalized = raw.toLowerCase();
|
|
56
|
+
if ([
|
|
57
|
+
"1",
|
|
58
|
+
"true",
|
|
59
|
+
"yes",
|
|
60
|
+
"on"
|
|
61
|
+
].includes(normalized)) return true;
|
|
62
|
+
return raw.split(",").some((token) => {
|
|
63
|
+
const value = token.trim();
|
|
64
|
+
return value === "*" || value === "sfnext" || value === "sfnext:*";
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function resolveLevel() {
|
|
68
|
+
if (overrideLevel) return overrideLevel;
|
|
69
|
+
const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;
|
|
70
|
+
if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel;
|
|
71
|
+
if (debugEnablesSfnext()) return "debug";
|
|
72
|
+
if (process.env.NODE_ENV === "production") return "warn";
|
|
73
|
+
return "info";
|
|
74
|
+
}
|
|
75
|
+
function shouldLog(level) {
|
|
76
|
+
return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];
|
|
77
|
+
}
|
|
78
|
+
const logger = {
|
|
79
|
+
error(msg, ...args) {
|
|
80
|
+
if (!shouldLog("error")) return;
|
|
81
|
+
console.error(chalk.red("[sfnext:error]"), msg, ...args);
|
|
82
|
+
},
|
|
83
|
+
warn(msg, ...args) {
|
|
84
|
+
if (!shouldLog("warn")) return;
|
|
85
|
+
console.warn(chalk.yellow("[sfnext:warn]"), msg, ...args);
|
|
86
|
+
},
|
|
87
|
+
info(msg, ...args) {
|
|
88
|
+
if (!shouldLog("info")) return;
|
|
89
|
+
console.log(chalk.cyan("[sfnext:info]"), msg, ...args);
|
|
90
|
+
},
|
|
91
|
+
debug(msg, ...args) {
|
|
92
|
+
if (!shouldLog("debug")) return;
|
|
93
|
+
console.log(chalk.gray("[sfnext:debug]"), msg, ...args);
|
|
94
|
+
},
|
|
95
|
+
setLevel(level) {
|
|
96
|
+
overrideLevel = level;
|
|
97
|
+
},
|
|
98
|
+
getLevel() {
|
|
99
|
+
return resolveLevel();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
28
104
|
//#region src/plugins/fixReactRouterManifestUrls.ts
|
|
29
105
|
function patchAssetsPaths(dir) {
|
|
30
106
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -35,7 +111,7 @@ function patchAssetsPaths(dir) {
|
|
|
35
111
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
36
112
|
if (content.includes("\"/assets/") || content.includes("'/assets/")) {
|
|
37
113
|
fs.writeFileSync(fullPath, content.replace(/["']\/assets\//g, "(window._BUNDLE_PATH || \"/\") + \"assets/"));
|
|
38
|
-
|
|
114
|
+
logger.debug(`patched /assets/ references in ${fullPath}`);
|
|
39
115
|
}
|
|
40
116
|
}
|
|
41
117
|
}
|
|
@@ -90,10 +166,41 @@ function getCommerceCloudApiUrl(shortCode, proxyHost) {
|
|
|
90
166
|
return proxyHost || `https://${shortCode}.api.commercecloud.salesforce.com`;
|
|
91
167
|
}
|
|
92
168
|
/**
|
|
169
|
+
* Get the configurable base path for the application.
|
|
170
|
+
* Reads from MRT_ENV_BASE_PATH environment variable.
|
|
171
|
+
*
|
|
172
|
+
* The base path is used for CDN routing to the correct MRT environment.
|
|
173
|
+
* It is prepended to all URLs: page routes, /mobify/bundle/ assets, and /mobify/proxy/api.
|
|
174
|
+
*
|
|
175
|
+
* Validation rules:
|
|
176
|
+
* - Must be a single path segment starting with '/'
|
|
177
|
+
* - Max 63 characters after the leading slash
|
|
178
|
+
* - Only URL-safe characters allowed
|
|
179
|
+
* - Returns empty string if not set
|
|
180
|
+
*
|
|
181
|
+
* @returns The sanitized base path (e.g., '/site-a' or '')
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* // No base path configured
|
|
185
|
+
* getBasePath() // → ''
|
|
186
|
+
*
|
|
187
|
+
* // With base path '/storefront'
|
|
188
|
+
* getBasePath() // → '/storefront'
|
|
189
|
+
*
|
|
190
|
+
* // Automatically sanitizes
|
|
191
|
+
* // MRT_ENV_BASE_PATH='storefront/' → '/storefront'
|
|
192
|
+
*/
|
|
193
|
+
function getBasePath() {
|
|
194
|
+
const basePath = process.env.MRT_ENV_BASE_PATH?.trim();
|
|
195
|
+
if (!basePath) return "";
|
|
196
|
+
if (!/^\/[a-zA-Z0-9_.+$~"'@:-]{1,63}$/.test(basePath)) throw new Error(`Invalid base path: "${basePath}". Base path must be a single segment starting with '/' (e.g., '/site-a'), contain only URL-safe characters, and be at most 63 characters after the leading slash.`);
|
|
197
|
+
return basePath;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
93
200
|
* Get the bundle path for static assets
|
|
94
201
|
*/
|
|
95
202
|
function getBundlePath(bundleId) {
|
|
96
|
-
return
|
|
203
|
+
return `${getBasePath()}/mobify/bundle/${bundleId}/client/`;
|
|
97
204
|
}
|
|
98
205
|
|
|
99
206
|
//#endregion
|
|
@@ -230,10 +337,16 @@ const managedRuntimeBundlePlugin = () => {
|
|
|
230
337
|
await fs.outputFile(loaderPath, "// This file is intentionally empty");
|
|
231
338
|
const prebuiltMrtEntryPath = path$1.resolve(__dirname$1, `./mrt/${mrtEntryFile}`);
|
|
232
339
|
await fs.copy(prebuiltMrtEntryPath, mrtEntryPath);
|
|
340
|
+
const prebuiltMrtEntryMapPath = `${prebuiltMrtEntryPath}.map`;
|
|
341
|
+
if (await fs.pathExists(prebuiltMrtEntryMapPath)) await fs.copy(prebuiltMrtEntryMapPath, `${mrtEntryPath}.map`);
|
|
233
342
|
const mrtDir = path$1.resolve(__dirname$1, "./mrt");
|
|
234
343
|
if (await fs.pathExists(mrtDir)) {
|
|
235
344
|
const files = await fs.readdir(mrtDir);
|
|
236
|
-
for (const file of files) if (file.startsWith("sfnext-server-") && file.endsWith(".mjs"))
|
|
345
|
+
for (const file of files) if (file.startsWith("sfnext-server-") && file.endsWith(".mjs")) {
|
|
346
|
+
await fs.copy(path$1.join(mrtDir, file), path$1.resolve(buildDirectory, file));
|
|
347
|
+
const mapFile = `${file}.map`;
|
|
348
|
+
if (files.includes(mapFile)) await fs.copy(path$1.join(mrtDir, mapFile), path$1.resolve(buildDirectory, mapFile));
|
|
349
|
+
}
|
|
237
350
|
}
|
|
238
351
|
const packageJsonPath = path$1.resolve(resolvedConfig.root, "package.json");
|
|
239
352
|
const buildPackageJsonPath = path$1.resolve(buildDirectory, "package.json");
|
|
@@ -246,9 +359,13 @@ const managedRuntimeBundlePlugin = () => {
|
|
|
246
359
|
apply: "build",
|
|
247
360
|
config({ mode }) {
|
|
248
361
|
return {
|
|
362
|
+
build: { rollupOptions: { onLog(level, log, defaultHandler) {
|
|
363
|
+
if (log.code === "SOURCEMAP_ERROR" && log.message.includes("resolve original location")) return;
|
|
364
|
+
defaultHandler(level, log);
|
|
365
|
+
} } },
|
|
249
366
|
environments: { ssr: { resolve: { noExternal: true } } },
|
|
250
367
|
experimental: { renderBuiltUrl(filename, { type }) {
|
|
251
|
-
if (mode !== "preview" && (type === "asset" || type === "public")) return { runtime: `(typeof window !== 'undefined' ? window._BUNDLE_PATH : ('/mobify/bundle/'+(process.env.BUNDLE_ID??'local')+'/client/')) + ${JSON.stringify(filename)}` };
|
|
368
|
+
if (mode !== "preview" && (type === "asset" || type === "public")) return { runtime: `(typeof window !== 'undefined' ? window._BUNDLE_PATH : ((process.env.MRT_ENV_BASE_PATH??'')+'/mobify/bundle/'+(process.env.BUNDLE_ID??'local')+'/client/')) + ${JSON.stringify(filename)}` };
|
|
252
369
|
} }
|
|
253
370
|
};
|
|
254
371
|
},
|
|
@@ -311,7 +428,7 @@ const patchReactRouterPlugin = () => {
|
|
|
311
428
|
|
|
312
429
|
//#endregion
|
|
313
430
|
//#region src/extensibility/target-utils.ts
|
|
314
|
-
const traverse = traverseModule.default || traverseModule;
|
|
431
|
+
const traverse$1 = traverseModule.default || traverseModule;
|
|
315
432
|
const TARGET_COMPONENT_TAG = "UITarget";
|
|
316
433
|
const TARGET_PROVIDERS_TAG = "TargetProviders";
|
|
317
434
|
const TARGET_ID_ATTRIBUTE = "targetId";
|
|
@@ -376,7 +493,7 @@ function runReplacementPass(ast, tagName, targetRegistry = null, contextProvider
|
|
|
376
493
|
if (replacedId) targetIdsReplaced.add(replacedId);
|
|
377
494
|
} else if (contextProviders) findAndReplaceProviders(pathToReplace, contextProviders);
|
|
378
495
|
};
|
|
379
|
-
traverse(ast, {
|
|
496
|
+
traverse$1(ast, {
|
|
380
497
|
VariableDeclaration(nodePath) {
|
|
381
498
|
const declarationPaths = nodePath.get("declarations");
|
|
382
499
|
const declarationsArray = Array.isArray(declarationPaths) ? declarationPaths : [declarationPaths];
|
|
@@ -429,7 +546,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
|
|
|
429
546
|
});
|
|
430
547
|
if (code.includes(TARGET_COMPONENT_TAG)) {
|
|
431
548
|
const replacementImportStatements = buildReplacementImportStatements(runReplacementPass(ast, TARGET_COMPONENT_TAG, targetRegistry, null), targetRegistry);
|
|
432
|
-
traverse(ast, { ImportDeclaration(nodePath) {
|
|
549
|
+
traverse$1(ast, { ImportDeclaration(nodePath) {
|
|
433
550
|
if (nodePath.node.source.value.includes("@/targets/ui-target")) nodePath.replaceWith(jsxText(replacementImportStatements));
|
|
434
551
|
} });
|
|
435
552
|
}
|
|
@@ -437,7 +554,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
|
|
|
437
554
|
const importStatements = /* @__PURE__ */ new Set();
|
|
438
555
|
for (const contextProvider of contextProviders) importStatements.add(`import ${contextProvider.componentName} from '@/${contextProvider.path.replace(".tsx", "")}';`);
|
|
439
556
|
const replacementImportStatements = Array.from(importStatements).join("\n");
|
|
440
|
-
traverse(ast, { ImportDeclaration(nodePath) {
|
|
557
|
+
traverse$1(ast, { ImportDeclaration(nodePath) {
|
|
441
558
|
if (nodePath.node.source.value.includes("@/targets/target-providers")) nodePath.replaceWith(jsxText(replacementImportStatements));
|
|
442
559
|
} });
|
|
443
560
|
runReplacementPass(ast, TARGET_PROVIDERS_TAG, null, contextProviders);
|
|
@@ -527,7 +644,7 @@ function transformTargetPlaceholderPlugin() {
|
|
|
527
644
|
};
|
|
528
645
|
return null;
|
|
529
646
|
} catch (err) {
|
|
530
|
-
|
|
647
|
+
logger.error(`UITarget replace ERROR in ${id}: ${err instanceof Error ? err.stack : String(err)}`);
|
|
531
648
|
throw err;
|
|
532
649
|
}
|
|
533
650
|
}
|
|
@@ -550,7 +667,7 @@ const watchConfigFilesPlugin = () => {
|
|
|
550
667
|
server.watcher.add(glob$1);
|
|
551
668
|
const onChange = (file) => {
|
|
552
669
|
if (file.endsWith("target-config.json")) {
|
|
553
|
-
|
|
670
|
+
logger.debug(`🔁 target-config.json changed: ${file}`);
|
|
554
671
|
server.restart();
|
|
555
672
|
}
|
|
556
673
|
};
|
|
@@ -630,12 +747,12 @@ function hasFallbackExport(sourceFile) {
|
|
|
630
747
|
/**
|
|
631
748
|
* Scans all files in the component directory for @Component decorators and extracts metadata using ts-morph
|
|
632
749
|
*/
|
|
633
|
-
async function scanComponents(project, projectRoot, componentPath, registryPath
|
|
750
|
+
async function scanComponents(project, projectRoot, componentPath, registryPath) {
|
|
634
751
|
const componentFiles = await glob(`${componentPath}/**/*.{ts,tsx}`, {
|
|
635
752
|
cwd: projectRoot,
|
|
636
753
|
absolute: true
|
|
637
754
|
});
|
|
638
|
-
|
|
755
|
+
logger.debug(`🔍 Scanning ${componentFiles.length} files in ${componentPath}...`);
|
|
639
756
|
const components = [];
|
|
640
757
|
const registryDir = dirname(resolve$1(projectRoot, registryPath));
|
|
641
758
|
for (const filePath of componentFiles) try {
|
|
@@ -660,19 +777,17 @@ async function scanComponents(project, projectRoot, componentPath, registryPath,
|
|
|
660
777
|
hasClientLoader: hasClientLoaderExport,
|
|
661
778
|
hasFallback
|
|
662
779
|
});
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
console.log(` ✅ Found component: ${componentInfo.id} → ${relativePath}${exportsText}`);
|
|
670
|
-
}
|
|
780
|
+
const exports = [];
|
|
781
|
+
if (hasLoaderExport) exports.push("loader");
|
|
782
|
+
if (hasClientLoaderExport) exports.push("clientLoader");
|
|
783
|
+
if (hasFallback) exports.push("fallback");
|
|
784
|
+
const exportsText = exports.length > 0 ? ` (with ${exports.join(", ")})` : "";
|
|
785
|
+
logger.debug(` ✅ Found component: ${componentInfo.id} → ${relativePath}${exportsText}`);
|
|
671
786
|
}
|
|
672
787
|
}
|
|
673
788
|
}
|
|
674
789
|
} catch (error) {
|
|
675
|
-
|
|
790
|
+
logger.warn(`⚠️ Could not process ${filePath}: ${error.message}`);
|
|
676
791
|
}
|
|
677
792
|
return components;
|
|
678
793
|
}
|
|
@@ -720,10 +835,10 @@ ${registrations}
|
|
|
720
835
|
/**
|
|
721
836
|
* Updates the registry.ts file with the generated code
|
|
722
837
|
*/
|
|
723
|
-
function updateRegistryFile(registryFilePath, generatedCode
|
|
838
|
+
function updateRegistryFile(registryFilePath, generatedCode) {
|
|
724
839
|
let existingContent;
|
|
725
840
|
if (!existsSync(registryFilePath)) {
|
|
726
|
-
|
|
841
|
+
logger.debug("📝 Creating new registry file...");
|
|
727
842
|
const basicRegistryContent = `import { ComponentRegistry } from '@/lib/component-registry';
|
|
728
843
|
|
|
729
844
|
// Create the component registry instance
|
|
@@ -748,7 +863,7 @@ export const registry = new ComponentRegistry();
|
|
|
748
863
|
const updatedContent = `${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}`;
|
|
749
864
|
try {
|
|
750
865
|
writeFileSync(registryFilePath, updatedContent, "utf-8");
|
|
751
|
-
|
|
866
|
+
logger.debug(`💾 Updated registry file: ${registryFilePath}`);
|
|
752
867
|
} catch (error) {
|
|
753
868
|
throw new Error(`Failed to write registry file: ${error.message}`);
|
|
754
869
|
}
|
|
@@ -777,10 +892,10 @@ export const registry = new ComponentRegistry();
|
|
|
777
892
|
* })
|
|
778
893
|
*/
|
|
779
894
|
const staticRegistryPlugin = (config = {}) => {
|
|
780
|
-
const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true
|
|
895
|
+
const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true } = config;
|
|
781
896
|
let projectRoot;
|
|
782
897
|
const runRegistryGeneration = async () => {
|
|
783
|
-
|
|
898
|
+
logger.debug("🚀 Starting static registry generation...");
|
|
784
899
|
const components = await scanComponents(new Project({ compilerOptions: {
|
|
785
900
|
target: ts.ScriptTarget.Latest,
|
|
786
901
|
module: ts.ModuleKind.ESNext,
|
|
@@ -788,12 +903,12 @@ const staticRegistryPlugin = (config = {}) => {
|
|
|
788
903
|
allowJs: true,
|
|
789
904
|
skipLibCheck: true,
|
|
790
905
|
noEmit: true
|
|
791
|
-
} }), projectRoot, componentPath, registryPath
|
|
792
|
-
|
|
906
|
+
} }), projectRoot, componentPath, registryPath);
|
|
907
|
+
logger.debug(`📦 Found ${components.length} components with @Component decorators`);
|
|
793
908
|
const generatedCode = generateRegistryCode(components, registryIdentifier);
|
|
794
909
|
const registryFilePath = resolve$1(projectRoot, registryPath);
|
|
795
|
-
updateRegistryFile(registryFilePath, generatedCode
|
|
796
|
-
|
|
910
|
+
updateRegistryFile(registryFilePath, generatedCode);
|
|
911
|
+
logger.debug("✅ Static registry generation complete!");
|
|
797
912
|
return registryFilePath;
|
|
798
913
|
};
|
|
799
914
|
return {
|
|
@@ -805,23 +920,23 @@ const staticRegistryPlugin = (config = {}) => {
|
|
|
805
920
|
try {
|
|
806
921
|
await runRegistryGeneration();
|
|
807
922
|
} catch (error) {
|
|
808
|
-
|
|
923
|
+
logger.error(`❌ Static registry generation failed: ${error.message}`);
|
|
809
924
|
if (failOnError) throw error;
|
|
810
|
-
|
|
925
|
+
logger.warn("⚠️ Continuing build without static registry...");
|
|
811
926
|
}
|
|
812
927
|
},
|
|
813
928
|
async handleHotUpdate({ file, server }) {
|
|
814
929
|
const normalizedComponentPath = componentPath.replace(/\\/g, "/");
|
|
815
930
|
const normalizedFile = file.replace(/\\/g, "/");
|
|
816
931
|
if (normalizedFile.includes(`/${normalizedComponentPath}/`) && (normalizedFile.endsWith(".ts") || normalizedFile.endsWith(".tsx"))) {
|
|
817
|
-
|
|
932
|
+
logger.debug(`🔄 Component file changed: ${file}, regenerating registry...`);
|
|
818
933
|
try {
|
|
819
934
|
const registryFilePath = await runRegistryGeneration();
|
|
820
935
|
const registryModule = server.moduleGraph.getModuleById(registryFilePath);
|
|
821
936
|
if (registryModule) await server.reloadModule(registryModule);
|
|
822
|
-
|
|
937
|
+
logger.debug("✅ Registry regenerated successfully!");
|
|
823
938
|
} catch (error) {
|
|
824
|
-
|
|
939
|
+
logger.error(`❌ Failed to regenerate registry: ${error.message}`);
|
|
825
940
|
}
|
|
826
941
|
return [];
|
|
827
942
|
}
|
|
@@ -834,19 +949,19 @@ const staticRegistryPlugin = (config = {}) => {
|
|
|
834
949
|
/**
|
|
835
950
|
* Load the engagement config from config.server.ts
|
|
836
951
|
*/
|
|
837
|
-
async function loadEngagementConfig(projectRoot, configPath
|
|
952
|
+
async function loadEngagementConfig(projectRoot, configPath) {
|
|
838
953
|
const absoluteConfigPath = resolve$1(projectRoot, configPath);
|
|
839
954
|
try {
|
|
840
955
|
const config = (await import(pathToFileURL(absoluteConfigPath).href)).default;
|
|
841
|
-
|
|
956
|
+
logger.debug(`📄 Loaded config from ${configPath}`);
|
|
842
957
|
const engagement = config?.app?.engagement;
|
|
843
958
|
if (!engagement) {
|
|
844
|
-
|
|
959
|
+
logger.debug(`⚠️ No engagement config found in ${configPath}`);
|
|
845
960
|
return null;
|
|
846
961
|
}
|
|
847
962
|
return engagement;
|
|
848
963
|
} catch (error) {
|
|
849
|
-
|
|
964
|
+
logger.warn(`⚠️ Could not load config from ${configPath}: ${error.message}`);
|
|
850
965
|
return null;
|
|
851
966
|
}
|
|
852
967
|
}
|
|
@@ -856,7 +971,7 @@ async function loadEngagementConfig(projectRoot, configPath, verbose$1) {
|
|
|
856
971
|
/**
|
|
857
972
|
* Extract all trackEvent calls from source files and return the event types found
|
|
858
973
|
*/
|
|
859
|
-
async function scanForInstrumentedEvents(projectRoot, scanPaths
|
|
974
|
+
async function scanForInstrumentedEvents(projectRoot, scanPaths) {
|
|
860
975
|
const instrumentedEvents = /* @__PURE__ */ new Set();
|
|
861
976
|
const trackEventPattern = /trackEvent\s*\([^,]+,[^,]+,[^,]+,\s*['"]([^'"]+)['"]/g;
|
|
862
977
|
const sendViewPagePattern = /sendViewPageEvent\s*\(/g;
|
|
@@ -869,29 +984,29 @@ async function scanForInstrumentedEvents(projectRoot, scanPaths, verbose$1) {
|
|
|
869
984
|
"**/*.spec.tsx",
|
|
870
985
|
"**/node_modules/**"
|
|
871
986
|
] });
|
|
872
|
-
|
|
987
|
+
logger.debug(`📂 Scanning ${files.length} files in ${scanPath}...`);
|
|
873
988
|
for (const file of files) try {
|
|
874
989
|
const content = readFileSync(file, "utf-8");
|
|
875
990
|
let match;
|
|
876
991
|
while ((match = trackEventPattern.exec(content)) !== null) {
|
|
877
992
|
const eventType = match[1];
|
|
878
993
|
instrumentedEvents.add(eventType);
|
|
879
|
-
|
|
994
|
+
logger.debug(` ✓ Found trackEvent('${eventType}') in ${file}`);
|
|
880
995
|
}
|
|
881
996
|
if (sendViewPagePattern.test(content)) {
|
|
882
997
|
instrumentedEvents.add("view_page");
|
|
883
|
-
|
|
998
|
+
logger.debug(` ✓ Found sendViewPageEvent() in ${file}`);
|
|
884
999
|
}
|
|
885
1000
|
while ((match = createEventPattern.exec(content)) !== null) {
|
|
886
1001
|
const eventType = match[1];
|
|
887
1002
|
instrumentedEvents.add(eventType);
|
|
888
|
-
|
|
1003
|
+
logger.debug(` ✓ Found createEvent('${eventType}') in ${file}`);
|
|
889
1004
|
}
|
|
890
1005
|
trackEventPattern.lastIndex = 0;
|
|
891
1006
|
sendViewPagePattern.lastIndex = 0;
|
|
892
1007
|
createEventPattern.lastIndex = 0;
|
|
893
1008
|
} catch (error) {
|
|
894
|
-
|
|
1009
|
+
logger.warn(`⚠️ Could not read ${file}: ${error.message}`);
|
|
895
1010
|
}
|
|
896
1011
|
}
|
|
897
1012
|
return instrumentedEvents;
|
|
@@ -935,7 +1050,7 @@ function extractEnabledEvents(engagement) {
|
|
|
935
1050
|
* })
|
|
936
1051
|
*/
|
|
937
1052
|
const eventInstrumentationValidatorPlugin = (config = {}) => {
|
|
938
|
-
const { configPath = "config.server.ts", scanPaths = ["src"], failOnMissing = false
|
|
1053
|
+
const { configPath = "config.server.ts", scanPaths = ["src"], failOnMissing = false } = config;
|
|
939
1054
|
let resolvedConfig;
|
|
940
1055
|
return {
|
|
941
1056
|
name: "storefrontnext:event-instrumentation-validator",
|
|
@@ -945,34 +1060,29 @@ const eventInstrumentationValidatorPlugin = (config = {}) => {
|
|
|
945
1060
|
},
|
|
946
1061
|
async buildStart() {
|
|
947
1062
|
const projectRoot = resolvedConfig.root;
|
|
948
|
-
|
|
949
|
-
const engagement = await loadEngagementConfig(projectRoot, configPath
|
|
1063
|
+
logger.debug("🔍 Validating event instrumentation...");
|
|
1064
|
+
const engagement = await loadEngagementConfig(projectRoot, configPath);
|
|
950
1065
|
if (!engagement) {
|
|
951
|
-
|
|
1066
|
+
logger.debug("ℹ️ Skipping validation - no engagement config found");
|
|
952
1067
|
return;
|
|
953
1068
|
}
|
|
954
1069
|
const adapterEvents = extractEnabledEvents(engagement);
|
|
955
1070
|
if (adapterEvents.size === 0) {
|
|
956
|
-
|
|
1071
|
+
logger.debug("ℹ️ No enabled adapters with event toggles found");
|
|
957
1072
|
return;
|
|
958
1073
|
}
|
|
959
|
-
const instrumentedEvents = await scanForInstrumentedEvents(projectRoot, scanPaths
|
|
960
|
-
|
|
961
|
-
console.log(`\n 🔎 Found ${instrumentedEvents.size} instrumented event types:`);
|
|
962
|
-
for (const event of instrumentedEvents) console.log(` - ${event}`);
|
|
963
|
-
}
|
|
1074
|
+
const instrumentedEvents = await scanForInstrumentedEvents(projectRoot, scanPaths);
|
|
1075
|
+
logger.debug(`🔎 Found ${instrumentedEvents.size} instrumented event types: ${[...instrumentedEvents].join(", ")}`);
|
|
964
1076
|
const missingInstrumentation = [];
|
|
965
1077
|
for (const [adapterName, enabledEvents] of adapterEvents) for (const eventType of enabledEvents) if (!instrumentedEvents.has(eventType)) missingInstrumentation.push({
|
|
966
1078
|
adapter: adapterName,
|
|
967
1079
|
event: eventType
|
|
968
1080
|
});
|
|
969
1081
|
if (missingInstrumentation.length === 0) {
|
|
970
|
-
|
|
1082
|
+
logger.debug("✅ All enabled events are instrumented");
|
|
971
1083
|
return;
|
|
972
1084
|
}
|
|
973
|
-
|
|
974
|
-
for (const { adapter, event } of missingInstrumentation) console.warn(` ⚠️ [event-instrumentation] ${adapter}.${event} is enabled but '${event}' is never instrumented`);
|
|
975
|
-
console.log("\n");
|
|
1085
|
+
for (const { adapter, event } of missingInstrumentation) logger.warn(`⚠️ ${adapter}.${event} is enabled but '${event}' is never instrumented`);
|
|
976
1086
|
if (failOnMissing) throw new Error(`[event-instrumentation] ${missingInstrumentation.length} event(s) are enabled but not instrumented. Either add instrumentation or disable the event toggles in config.server.ts.`);
|
|
977
1087
|
}
|
|
978
1088
|
};
|
|
@@ -1252,6 +1362,242 @@ const workspacePlugin = () => {
|
|
|
1252
1362
|
};
|
|
1253
1363
|
};
|
|
1254
1364
|
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/plugins/componentLoaders.ts
|
|
1367
|
+
const traverse = traverseModule.default || traverseModule;
|
|
1368
|
+
const generate$1 = generate.default || generate;
|
|
1369
|
+
/**
|
|
1370
|
+
* Names of exports to strip per environment.
|
|
1371
|
+
*
|
|
1372
|
+
* - `loader` is server-only → strip from the **client** build
|
|
1373
|
+
* - `clientLoader` is client-only → strip from the **server** build
|
|
1374
|
+
*/
|
|
1375
|
+
const STRIP_FROM_CLIENT = ["loader"];
|
|
1376
|
+
const STRIP_FROM_SERVER = ["clientLoader"];
|
|
1377
|
+
/**
|
|
1378
|
+
* Determines which export names should be stripped for a given Vite environment.
|
|
1379
|
+
*/
|
|
1380
|
+
function getExportsToStrip(environmentName) {
|
|
1381
|
+
if (environmentName === "client") return STRIP_FROM_CLIENT;
|
|
1382
|
+
if (environmentName === "ssr") return STRIP_FROM_SERVER;
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Returns `true` when the source code contains at least one of the given export names as a quick pre-check before
|
|
1387
|
+
* running the full AST transform.
|
|
1388
|
+
*/
|
|
1389
|
+
function hasExportCandidate(code, names) {
|
|
1390
|
+
return names.some((name) => code.includes(name));
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Checks whether the AST contains at least one class declaration decorated with `@Component(…)`.
|
|
1394
|
+
*/
|
|
1395
|
+
function hasComponentDecorator(ast) {
|
|
1396
|
+
let found = false;
|
|
1397
|
+
traverse(ast, { ClassDeclaration(path$2) {
|
|
1398
|
+
const decorators = path$2.node.decorators;
|
|
1399
|
+
if (!decorators) return;
|
|
1400
|
+
for (const decorator of decorators) if (decorator.expression.type === "CallExpression" && isIdentifier(decorator.expression.callee) && decorator.expression.callee.name === "Component") {
|
|
1401
|
+
found = true;
|
|
1402
|
+
path$2.stop();
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
} });
|
|
1406
|
+
return found;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Strips the specified named exports from the given source code using a
|
|
1410
|
+
* Babel AST transform.
|
|
1411
|
+
*
|
|
1412
|
+
* The transform handles the following patterns:
|
|
1413
|
+
*
|
|
1414
|
+
* 1. `export const loader = …;`
|
|
1415
|
+
* 2. `export function loader(…) {…}`
|
|
1416
|
+
* 3. `export class Loader {…}`
|
|
1417
|
+
* 4. `export { loader }` / `export { foo as loader }`
|
|
1418
|
+
* 5. `export { loader } from './loaders'`
|
|
1419
|
+
*
|
|
1420
|
+
* Destructured exports (`export const { loader } = …` or
|
|
1421
|
+
* `export const [loader] = …`) cannot be safely removed and will
|
|
1422
|
+
* throw an error if encountered (matching React Router behaviour).
|
|
1423
|
+
*
|
|
1424
|
+
* After removing an export, the transform also:
|
|
1425
|
+
* - Removes top-level property assignments to the stripped export
|
|
1426
|
+
* (e.g. `clientLoader.hydrate = true`)
|
|
1427
|
+
* - Removes any import declarations that become unused as a result
|
|
1428
|
+
*
|
|
1429
|
+
* @see {@link https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/vite/remove-exports.ts React Router remove-exports}
|
|
1430
|
+
* @returns The transformed source code, or `null` if nothing was changed.
|
|
1431
|
+
*/
|
|
1432
|
+
function stripExports(code, exportsToStrip, preParsedAst) {
|
|
1433
|
+
const ast = preParsedAst ?? parse(code, {
|
|
1434
|
+
sourceType: "module",
|
|
1435
|
+
plugins: [
|
|
1436
|
+
"typescript",
|
|
1437
|
+
"jsx",
|
|
1438
|
+
"decorators"
|
|
1439
|
+
]
|
|
1440
|
+
});
|
|
1441
|
+
let changed = false;
|
|
1442
|
+
const previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);
|
|
1443
|
+
const removedExportLocalNames = /* @__PURE__ */ new Set();
|
|
1444
|
+
traverse(ast, { ExportNamedDeclaration(path$2) {
|
|
1445
|
+
const { declaration, specifiers } = path$2.node;
|
|
1446
|
+
if (declaration && isVariableDeclaration(declaration)) {
|
|
1447
|
+
const remaining = declaration.declarations.filter((decl) => {
|
|
1448
|
+
if (isIdentifier(decl.id) && exportsToStrip.includes(decl.id.name)) {
|
|
1449
|
+
removedExportLocalNames.add(decl.id.name);
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
if (isArrayPattern(decl.id) || isObjectPattern(decl.id)) validateDestructuredExports(decl.id, exportsToStrip);
|
|
1453
|
+
return true;
|
|
1454
|
+
});
|
|
1455
|
+
if (remaining.length < declaration.declarations.length) {
|
|
1456
|
+
changed = true;
|
|
1457
|
+
if (remaining.length === 0) {
|
|
1458
|
+
removeLeadingEslintDisableComment(path$2);
|
|
1459
|
+
path$2.remove();
|
|
1460
|
+
} else declaration.declarations = remaining;
|
|
1461
|
+
}
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (declaration && isFunctionDeclaration(declaration)) {
|
|
1465
|
+
if (declaration.id && exportsToStrip.includes(declaration.id.name)) {
|
|
1466
|
+
changed = true;
|
|
1467
|
+
removedExportLocalNames.add(declaration.id.name);
|
|
1468
|
+
removeLeadingEslintDisableComment(path$2);
|
|
1469
|
+
path$2.remove();
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (declaration && isClassDeclaration(declaration)) {
|
|
1474
|
+
if (declaration.id && exportsToStrip.includes(declaration.id.name)) {
|
|
1475
|
+
changed = true;
|
|
1476
|
+
removedExportLocalNames.add(declaration.id.name);
|
|
1477
|
+
removeLeadingEslintDisableComment(path$2);
|
|
1478
|
+
path$2.remove();
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (specifiers.length > 0) {
|
|
1483
|
+
const remaining = specifiers.filter((spec) => {
|
|
1484
|
+
if (isExportSpecifier(spec)) {
|
|
1485
|
+
const exportedName = isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
|
|
1486
|
+
if (exportsToStrip.includes(exportedName)) {
|
|
1487
|
+
removedExportLocalNames.add(spec.local.name);
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return true;
|
|
1492
|
+
});
|
|
1493
|
+
if (remaining.length < specifiers.length) {
|
|
1494
|
+
changed = true;
|
|
1495
|
+
if (remaining.length === 0) {
|
|
1496
|
+
removeLeadingEslintDisableComment(path$2);
|
|
1497
|
+
path$2.remove();
|
|
1498
|
+
} else path$2.node.specifiers = remaining;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
} });
|
|
1502
|
+
if (changed) traverse(ast, { ExpressionStatement(path$2) {
|
|
1503
|
+
if (!path$2.parentPath?.isProgram()) return;
|
|
1504
|
+
if (path$2.node.expression.type === "AssignmentExpression") {
|
|
1505
|
+
const left = path$2.node.expression.left;
|
|
1506
|
+
if (isMemberExpression(left) && isIdentifier(left.object) && (exportsToStrip.includes(left.object.name) || removedExportLocalNames.has(left.object.name))) {
|
|
1507
|
+
removeLeadingEslintDisableComment(path$2);
|
|
1508
|
+
path$2.remove();
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
} });
|
|
1512
|
+
if (changed) deadCodeElimination(ast, previouslyReferencedIdentifiers);
|
|
1513
|
+
if (!changed) return null;
|
|
1514
|
+
return generate$1(ast, { retainLines: true }, code).code;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Validates that no destructured export patterns contain names that should
|
|
1518
|
+
* be stripped. Destructured exports cannot be safely removed, so we throw
|
|
1519
|
+
* an error instead (matching React Router behaviour).
|
|
1520
|
+
*/
|
|
1521
|
+
function validateDestructuredExports(id, exportsToStrip) {
|
|
1522
|
+
if (isArrayPattern(id)) for (const element of id.elements) {
|
|
1523
|
+
if (!element) continue;
|
|
1524
|
+
if (isIdentifier(element) && exportsToStrip.includes(element.name)) throw new Error(`Cannot remove destructured export "${element.name}"`);
|
|
1525
|
+
if (isRestElement(element) && isIdentifier(element.argument) && exportsToStrip.includes(element.argument.name)) throw new Error(`Cannot remove destructured export "${element.argument.name}"`);
|
|
1526
|
+
if (isArrayPattern(element) || isObjectPattern(element)) validateDestructuredExports(element, exportsToStrip);
|
|
1527
|
+
}
|
|
1528
|
+
if (isObjectPattern(id)) for (const property of id.properties) {
|
|
1529
|
+
if (!property) continue;
|
|
1530
|
+
if (isObjectProperty(property) && isIdentifier(property.key)) {
|
|
1531
|
+
if (isIdentifier(property.value) && exportsToStrip.includes(property.value.name)) throw new Error(`Cannot remove destructured export "${property.value.name}"`);
|
|
1532
|
+
if (isArrayPattern(property.value) || isObjectPattern(property.value)) validateDestructuredExports(property.value, exportsToStrip);
|
|
1533
|
+
}
|
|
1534
|
+
if (isRestElement(property) && isIdentifier(property.argument) && exportsToStrip.includes(property.argument.name)) throw new Error(`Cannot remove destructured export "${property.argument.name}"`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Removes a leading `// eslint-disable-next-line …` comment that sits on
|
|
1539
|
+
* the line immediately before the given path.
|
|
1540
|
+
*/
|
|
1541
|
+
function removeLeadingEslintDisableComment(path$2) {
|
|
1542
|
+
const leadingComments = path$2.node.leadingComments;
|
|
1543
|
+
if (!leadingComments || leadingComments.length === 0) return;
|
|
1544
|
+
const last = leadingComments[leadingComments.length - 1];
|
|
1545
|
+
if (last.type === "CommentLine" && last.value.includes("eslint-disable")) leadingComments.pop();
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Vite plugin that strips environment-specific loader exports from
|
|
1549
|
+
* component modules.
|
|
1550
|
+
*
|
|
1551
|
+
* Following the React Router convention:
|
|
1552
|
+
* - `export const loader` → server-only, stripped from the **client** bundle
|
|
1553
|
+
* - `export const clientLoader` → client-only, stripped from the **server** bundle
|
|
1554
|
+
*
|
|
1555
|
+
* This ensures that server-only code (e.g. API calls, database access) is
|
|
1556
|
+
* never included in the client bundle, and vice versa.
|
|
1557
|
+
*
|
|
1558
|
+
* The plugin only processes files that:
|
|
1559
|
+
* 1. Are under the configured `componentPath` directory
|
|
1560
|
+
* 2. Contain a `@Component` decorator (i.e. are Page Designer components)
|
|
1561
|
+
* 3. Are not test or story files
|
|
1562
|
+
*/
|
|
1563
|
+
function componentLoadersPlugin(config = {}) {
|
|
1564
|
+
const { componentPath = "src/components" } = config;
|
|
1565
|
+
let isTestMode = false;
|
|
1566
|
+
return {
|
|
1567
|
+
name: "storefrontnext:component-loaders",
|
|
1568
|
+
enforce: "pre",
|
|
1569
|
+
configResolved(resolvedConfig) {
|
|
1570
|
+
isTestMode = resolvedConfig.mode === "test";
|
|
1571
|
+
},
|
|
1572
|
+
transform(code, id) {
|
|
1573
|
+
if (isTestMode) return null;
|
|
1574
|
+
if (!id.includes(componentPath)) return null;
|
|
1575
|
+
if (!/\.[mc]?[jt]sx?$/.test(id)) return null;
|
|
1576
|
+
if (/\.(test|spec|stories)\.[jt]sx?$/.test(id)) return null;
|
|
1577
|
+
const environmentName = this.environment?.name;
|
|
1578
|
+
if (!environmentName) return null;
|
|
1579
|
+
const exportsToStrip = getExportsToStrip(environmentName);
|
|
1580
|
+
if (exportsToStrip.length === 0) return null;
|
|
1581
|
+
if (!hasExportCandidate(code, exportsToStrip)) return null;
|
|
1582
|
+
const ast = parse(code, {
|
|
1583
|
+
sourceType: "module",
|
|
1584
|
+
plugins: [
|
|
1585
|
+
"typescript",
|
|
1586
|
+
"jsx",
|
|
1587
|
+
"decorators"
|
|
1588
|
+
]
|
|
1589
|
+
});
|
|
1590
|
+
if (!hasComponentDecorator(ast)) return null;
|
|
1591
|
+
const transformed = stripExports(code, exportsToStrip, ast);
|
|
1592
|
+
if (!transformed) return null;
|
|
1593
|
+
return {
|
|
1594
|
+
code: transformed,
|
|
1595
|
+
map: null
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1255
1601
|
//#endregion
|
|
1256
1602
|
//#region src/storefront-next-targets.ts
|
|
1257
1603
|
/**
|
|
@@ -1276,13 +1622,11 @@ const workspacePlugin = () => {
|
|
|
1276
1622
|
function storefrontNextTargets(config = {}) {
|
|
1277
1623
|
const { readableChunkNames = false, staticRegistry = {
|
|
1278
1624
|
componentPath: "",
|
|
1279
|
-
registryPath: ""
|
|
1280
|
-
verbose: false
|
|
1625
|
+
registryPath: ""
|
|
1281
1626
|
}, eventInstrumentationValidator = {
|
|
1282
1627
|
configPath: "config.server.ts",
|
|
1283
1628
|
scanPaths: ["src"],
|
|
1284
|
-
failOnMissing: false
|
|
1285
|
-
verbose: false
|
|
1629
|
+
failOnMissing: false
|
|
1286
1630
|
} } = config;
|
|
1287
1631
|
const plugins = [
|
|
1288
1632
|
...process.env.SCAPI_PROXY_HOST ? [workspacePlugin()] : [],
|
|
@@ -1294,12 +1638,383 @@ function storefrontNextTargets(config = {}) {
|
|
|
1294
1638
|
watchConfigFilesPlugin(),
|
|
1295
1639
|
buildMiddlewareRegistryPlugin()
|
|
1296
1640
|
];
|
|
1297
|
-
if (staticRegistry?.componentPath && staticRegistry?.registryPath)
|
|
1641
|
+
if (staticRegistry?.componentPath && staticRegistry?.registryPath) {
|
|
1642
|
+
plugins.push(staticRegistryPlugin(staticRegistry));
|
|
1643
|
+
plugins.push(componentLoadersPlugin({ componentPath: staticRegistry.componentPath }));
|
|
1644
|
+
}
|
|
1298
1645
|
if (eventInstrumentationValidator !== false) plugins.push(eventInstrumentationValidatorPlugin(eventInstrumentationValidator));
|
|
1299
1646
|
if (readableChunkNames) plugins.push(readableChunkFileNamesPlugin());
|
|
1300
1647
|
return plugins;
|
|
1301
1648
|
}
|
|
1302
1649
|
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/plugins/hybridProxy.ts
|
|
1652
|
+
/**
|
|
1653
|
+
* Check if a request path should skip proxying (Vite internals, assets, etc.)
|
|
1654
|
+
*
|
|
1655
|
+
* @param pathname - URL pathname to check
|
|
1656
|
+
* @returns true if the request should NOT be proxied
|
|
1657
|
+
*/
|
|
1658
|
+
function shouldSkipProxy(pathname) {
|
|
1659
|
+
if (pathname.startsWith("/@")) return true;
|
|
1660
|
+
if (pathname.startsWith("/__")) return true;
|
|
1661
|
+
if (pathname.startsWith("/src/")) return true;
|
|
1662
|
+
if (pathname.startsWith("/node_modules/")) return true;
|
|
1663
|
+
if (pathname.endsWith(".data")) return true;
|
|
1664
|
+
if (pathname.startsWith("/mobify/")) return true;
|
|
1665
|
+
if (pathname.startsWith("/on/demandware.")) return false;
|
|
1666
|
+
if (/\.(js|jsx|ts|tsx|css|json|map|woff2?|ttf|svg|png|jpe?g|gif|webp|ico|mp4)$/i.test(pathname)) return true;
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Rewrite Set-Cookie header for localhost development.
|
|
1671
|
+
*
|
|
1672
|
+
* Rewrites SFCC Set-Cookie headers so they work on localhost during local development.
|
|
1673
|
+
*
|
|
1674
|
+
* **LOCAL DEVELOPMENT ONLY** — This function is part of the hybrid proxy Vite plugin
|
|
1675
|
+
* which only runs during `pnpm dev`. In production (MRT deployments), SFCC cookies
|
|
1676
|
+
* flow through the eCDN unmodified.
|
|
1677
|
+
*
|
|
1678
|
+
* Rewrites applied:
|
|
1679
|
+
* - **Domain**: `.salesforce.com` → `localhost` (browsers reject cross-domain cookies)
|
|
1680
|
+
*
|
|
1681
|
+
* Attributes intentionally preserved:
|
|
1682
|
+
* - **Secure**: Kept. Localhost is a secure context — browsers accept `Secure` cookies
|
|
1683
|
+
* on `http://localhost` (see https://w3c.github.io/webappsec-secure-contexts/).
|
|
1684
|
+
* - **SameSite**: Kept. `SameSite=None; Secure` is valid on localhost since `Secure`
|
|
1685
|
+
* is accepted. This keeps SFCC cookies transparent and in sync with Storefront Next
|
|
1686
|
+
* cookies, which is critical for hybrid auth session bridging.
|
|
1687
|
+
*
|
|
1688
|
+
* @param cookie - Original Set-Cookie header value from SFCC
|
|
1689
|
+
* @returns Rewritten cookie suitable for localhost
|
|
1690
|
+
*
|
|
1691
|
+
* @example
|
|
1692
|
+
* Input: "dwsid=abc123; Domain=.salesforce.com; Path=/; Secure; SameSite=None; HttpOnly"
|
|
1693
|
+
* Output: "dwsid=abc123; Domain=localhost; Path=/; Secure; SameSite=None; HttpOnly"
|
|
1694
|
+
*/
|
|
1695
|
+
function rewriteCookieForLocalhost(cookie) {
|
|
1696
|
+
let rewritten = cookie;
|
|
1697
|
+
rewritten = rewritten.replace(/Domain=[^;]+/gi, "Domain=localhost");
|
|
1698
|
+
if (!/Domain=/i.test(cookie)) rewritten = rewritten.replace(/^([^;]+)/, "$1; Domain=localhost");
|
|
1699
|
+
return rewritten.trim();
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Inline script injected into proxied HTML responses to intercept `document.cookie` writes.
|
|
1703
|
+
*
|
|
1704
|
+
* **Why this is needed (Layer 3 cookie rewriting):**
|
|
1705
|
+
*
|
|
1706
|
+
* The hybrid proxy rewrites Set-Cookie headers from SFCC responses (Layer 1), but after
|
|
1707
|
+
* the SFRA page fully loads, client-side JavaScript sets cookies via `document.cookie`.
|
|
1708
|
+
* These writes bypass the proxy entirely.
|
|
1709
|
+
*
|
|
1710
|
+
* SFRA's JS typically checks `window.location.protocol` to decide whether to add `Secure`.
|
|
1711
|
+
* On `http://localhost`, it sees `http:` and omits `Secure`, producing cookies like:
|
|
1712
|
+
*
|
|
1713
|
+
* document.cookie = "dwsid=abc; SameSite=None" // No Secure → browser rejects
|
|
1714
|
+
*
|
|
1715
|
+
* This interceptor patches `document.cookie` to:
|
|
1716
|
+
* 1. Rewrite `Domain=...` → `Domain=localhost`
|
|
1717
|
+
* 2. Ensure `Secure` is present (localhost is a secure context)
|
|
1718
|
+
* 3. If `SameSite=None` is present without `Secure`, add `Secure`
|
|
1719
|
+
*
|
|
1720
|
+
* This keeps client-side cookie writes consistent with the proxy's Layer 1 rewrites
|
|
1721
|
+
* and ensures hybrid auth cookies (dwsid, cc-*) stay in sync between Storefront Next
|
|
1722
|
+
* and SFRA.
|
|
1723
|
+
*
|
|
1724
|
+
* **LOCAL DEVELOPMENT ONLY** — This script is only injected by the Vite dev server proxy.
|
|
1725
|
+
*/
|
|
1726
|
+
const COOKIE_INTERCEPTOR_SCRIPT = `<script data-hybrid-proxy="cookie-interceptor">
|
|
1727
|
+
(function() {
|
|
1728
|
+
var desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
|
|
1729
|
+
if (!desc || !desc.set) return;
|
|
1730
|
+
Object.defineProperty(document, 'cookie', {
|
|
1731
|
+
get: function() { return desc.get.call(this); },
|
|
1732
|
+
set: function(val) {
|
|
1733
|
+
// Rewrite Domain to localhost
|
|
1734
|
+
val = val.replace(/Domain=[^;]+/gi, 'Domain=localhost');
|
|
1735
|
+
// Ensure Secure is present if SameSite=None (localhost is a secure context)
|
|
1736
|
+
if (/SameSite=None/i.test(val) && !/;\\s*Secure\\b/i.test(val)) {
|
|
1737
|
+
val += '; Secure';
|
|
1738
|
+
}
|
|
1739
|
+
desc.set.call(this, val);
|
|
1740
|
+
},
|
|
1741
|
+
configurable: true
|
|
1742
|
+
});
|
|
1743
|
+
})();
|
|
1744
|
+
<\/script>`;
|
|
1745
|
+
/**
|
|
1746
|
+
* Escape special regex characters in a string for use in `new RegExp()`.
|
|
1747
|
+
*/
|
|
1748
|
+
function escapeRegExp(str) {
|
|
1749
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Vite plugin for hybrid proxying between Storefront Next and legacy SFRA.
|
|
1753
|
+
*
|
|
1754
|
+
* Uses http-proxy to silently forward non-matching requests to SFCC without visible
|
|
1755
|
+
* redirects. Rewrites Set-Cookie headers, Location headers, and HTML/JSON response
|
|
1756
|
+
* bodies to keep all navigation within the localhost proxy.
|
|
1757
|
+
*
|
|
1758
|
+
* Routing decisions are delegated to the `routeMatcher` callback injected via options,
|
|
1759
|
+
* keeping the SDK free of template-specific routing logic.
|
|
1760
|
+
*
|
|
1761
|
+
* @param options - Plugin configuration
|
|
1762
|
+
* @returns Vite plugin
|
|
1763
|
+
*/
|
|
1764
|
+
function hybridProxyPlugin(options) {
|
|
1765
|
+
if (!options.enabled) {
|
|
1766
|
+
logger.debug("Hybrid proxy disabled (HYBRID_PROXY_ENABLED is not true)");
|
|
1767
|
+
return { name: "hybrid-proxy" };
|
|
1768
|
+
}
|
|
1769
|
+
if (!options.targetOrigin) {
|
|
1770
|
+
logger.warn("Hybrid proxy: no target origin configured (SFCC_ORIGIN required)");
|
|
1771
|
+
return { name: "hybrid-proxy" };
|
|
1772
|
+
}
|
|
1773
|
+
logger.info(`Hybrid proxy enabled → ${options.targetOrigin}`);
|
|
1774
|
+
logger.debug(`Hybrid proxy routing rules: ${options.routingRules.slice(0, 100)}...`);
|
|
1775
|
+
const locale = options.locale || "default";
|
|
1776
|
+
logger.debug(`Hybrid proxy path transformation: / → /s/${options.siteId}, /path → /s/${options.siteId}/${locale}/path`);
|
|
1777
|
+
const targetOriginPattern = new RegExp(escapeRegExp(options.targetOrigin), "g");
|
|
1778
|
+
const proxy = httpProxy.createProxyServer({
|
|
1779
|
+
changeOrigin: true,
|
|
1780
|
+
followRedirects: false,
|
|
1781
|
+
selfHandleResponse: true
|
|
1782
|
+
});
|
|
1783
|
+
proxy.on("proxyReq", (proxyReq, req) => {
|
|
1784
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
1785
|
+
const pathname = url.pathname;
|
|
1786
|
+
if (!pathname.startsWith("/s/") && !pathname.startsWith("/on/demandware.")) {
|
|
1787
|
+
const originalPath = proxyReq.path;
|
|
1788
|
+
/**
|
|
1789
|
+
* "/" maps to the SFRA/SiteGenesis site root — no locale in the path
|
|
1790
|
+
* This would simply proxy to SFCC hostname (eg.: https://zzrf-001.dx.commercecloud.salesforce.com/s/{siteId}/{locale}/) which is not a valid storefront URL.
|
|
1791
|
+
* We need to rewrite the path to /s/{siteId} so that it can be proxied to the correct SFCC URL.
|
|
1792
|
+
*/
|
|
1793
|
+
if (pathname === "/") proxyReq.path = `/s/${options.siteId}${url.search}`;
|
|
1794
|
+
else proxyReq.path = `/s/${options.siteId}/${locale}${pathname}${url.search}`;
|
|
1795
|
+
logger.debug(`Hybrid proxy path rewrite: ${originalPath} → ${proxyReq.path}`);
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
proxy.on("proxyRes", (proxyRes, req, res) => {
|
|
1799
|
+
const clientRes = res;
|
|
1800
|
+
const locationHeader = proxyRes.headers.location;
|
|
1801
|
+
const statusCode = proxyRes.statusCode || 200;
|
|
1802
|
+
if (statusCode >= 300 && statusCode < 400 && typeof locationHeader === "string" && /\/404\b/.test(locationHeader)) {
|
|
1803
|
+
logger.warn(`⚠️ SFCC returned a redirect to 404 for ${req.url}. This usually means your HYBRID_ROUTING_RULES are missing a pattern for this path. Stripping Set-Cookie headers to prevent session cookie corruption. Fix: add a matching pattern to HYBRID_ROUTING_RULES (e.g., "^${req.url?.split("?")[0]}.*")`);
|
|
1804
|
+
delete proxyRes.headers["set-cookie"];
|
|
1805
|
+
}
|
|
1806
|
+
const setCookieHeaders = proxyRes.headers["set-cookie"];
|
|
1807
|
+
if (setCookieHeaders && Array.isArray(setCookieHeaders)) proxyRes.headers["set-cookie"] = setCookieHeaders.map((cookie) => {
|
|
1808
|
+
const rewritten = rewriteCookieForLocalhost(cookie);
|
|
1809
|
+
logger.debug(`Hybrid proxy cookie rewrite: ${cookie.slice(0, 50)}... → ${rewritten.slice(0, 50)}...`);
|
|
1810
|
+
return rewritten;
|
|
1811
|
+
});
|
|
1812
|
+
if (locationHeader && typeof locationHeader === "string") try {
|
|
1813
|
+
const locationUrl = new URL(locationHeader, options.targetOrigin);
|
|
1814
|
+
if (locationUrl.origin === options.targetOrigin) {
|
|
1815
|
+
const localUrl = `http://${req.headers.host}${locationUrl.pathname}${locationUrl.search}${locationUrl.hash}`;
|
|
1816
|
+
proxyRes.headers.location = localUrl;
|
|
1817
|
+
logger.debug(`Hybrid proxy location rewrite: ${locationHeader} → ${localUrl}`);
|
|
1818
|
+
}
|
|
1819
|
+
} catch {
|
|
1820
|
+
logger.warn(`Hybrid proxy: invalid Location header: ${locationHeader}`);
|
|
1821
|
+
}
|
|
1822
|
+
const contentType = (proxyRes.headers["content-type"] || "").split(";")[0].trim();
|
|
1823
|
+
if (!(contentType === "text/html" || contentType === "application/json")) {
|
|
1824
|
+
clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
1825
|
+
proxyRes.pipe(clientRes);
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const chunks = [];
|
|
1829
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
1830
|
+
proxyRes.on("end", () => {
|
|
1831
|
+
let body = Buffer.concat(chunks);
|
|
1832
|
+
const encoding = proxyRes.headers["content-encoding"];
|
|
1833
|
+
if (encoding === "gzip") body = gunzipSync(body);
|
|
1834
|
+
else if (encoding === "br") body = brotliDecompressSync(body);
|
|
1835
|
+
else if (encoding === "deflate") body = inflateSync(body);
|
|
1836
|
+
const proxyOrigin = `http://${req.headers.host}`;
|
|
1837
|
+
let text = body.toString("utf8");
|
|
1838
|
+
targetOriginPattern.lastIndex = 0;
|
|
1839
|
+
text = text.replace(targetOriginPattern, proxyOrigin);
|
|
1840
|
+
if (contentType === "text/html") {
|
|
1841
|
+
const headIndex = text.indexOf("<head");
|
|
1842
|
+
if (headIndex !== -1) {
|
|
1843
|
+
const insertAfter = text.indexOf(">", headIndex);
|
|
1844
|
+
if (insertAfter !== -1) text = text.slice(0, insertAfter + 1) + COOKIE_INTERCEPTOR_SCRIPT + text.slice(insertAfter + 1);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const headers = { ...proxyRes.headers };
|
|
1848
|
+
delete headers["content-encoding"];
|
|
1849
|
+
delete headers["transfer-encoding"];
|
|
1850
|
+
headers["content-length"] = String(Buffer.byteLength(text, "utf8"));
|
|
1851
|
+
clientRes.writeHead(proxyRes.statusCode || 200, headers);
|
|
1852
|
+
clientRes.end(text);
|
|
1853
|
+
logger.debug(`Hybrid proxy rewrote ${contentType} body URLs for ${req.url}`);
|
|
1854
|
+
});
|
|
1855
|
+
});
|
|
1856
|
+
proxy.on("error", (err, req, res) => {
|
|
1857
|
+
logger.error(`Hybrid proxy error: ${err.message} ${req.url}`);
|
|
1858
|
+
if ("writeHead" in res && !res.headersSent) {
|
|
1859
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
1860
|
+
res.end("Bad Gateway: Failed to proxy to SFCC");
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
return {
|
|
1864
|
+
name: "hybrid-proxy",
|
|
1865
|
+
enforce: "pre",
|
|
1866
|
+
configureServer(server) {
|
|
1867
|
+
server.middlewares.use((req, res, next) => {
|
|
1868
|
+
const pathname = req.url?.split("?")[0] || "";
|
|
1869
|
+
if (shouldSkipProxy(pathname)) return next();
|
|
1870
|
+
const isSFCCPath = pathname.startsWith("/on/demandware.");
|
|
1871
|
+
let shouldRouteToNextApp = false;
|
|
1872
|
+
if (!isSFCCPath) {
|
|
1873
|
+
try {
|
|
1874
|
+
shouldRouteToNextApp = options.routeMatcher(pathname, options.routingRules);
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
logger.error(`Hybrid proxy error checking routing rules: ${String(error)}`);
|
|
1877
|
+
return next();
|
|
1878
|
+
}
|
|
1879
|
+
if (shouldRouteToNextApp) return next();
|
|
1880
|
+
}
|
|
1881
|
+
logger.debug(`Hybrid proxy: ${req.method} ${pathname} → ${options.targetOrigin}`);
|
|
1882
|
+
try {
|
|
1883
|
+
proxy.web(req, res, { target: options.targetOrigin });
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
logger.error(`Hybrid proxy failed to proxy request: ${String(error)}`);
|
|
1886
|
+
if (!res.headersSent) {
|
|
1887
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
1888
|
+
res.end("Bad Gateway: Failed to proxy to SFCC");
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
//#endregion
|
|
1897
|
+
//#region src/plugins/ecdnMatcher.ts
|
|
1898
|
+
/**
|
|
1899
|
+
* Cloudflare eCDN Routing Rule Matcher
|
|
1900
|
+
*
|
|
1901
|
+
* Parses Cloudflare-style routing expressions and tests pathnames against them.
|
|
1902
|
+
* This utility is environment-agnostic and works in both Node.js and browser contexts.
|
|
1903
|
+
*
|
|
1904
|
+
* @example
|
|
1905
|
+
* ```typescript
|
|
1906
|
+
* const rules = '(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/search.*")';
|
|
1907
|
+
* shouldRouteToNext('/', rules); // true - route to Storefront Next
|
|
1908
|
+
* shouldRouteToNext('/search', rules); // true - route to Storefront Next
|
|
1909
|
+
* shouldRouteToNext('/checkout', rules); // false - proxy to SFRA/legacy
|
|
1910
|
+
* ```
|
|
1911
|
+
*
|
|
1912
|
+
* Environment variables used:
|
|
1913
|
+
* - HYBRID_PROXY_ENABLED (optional) - Boolean flag to enable/disable hybrid proxy
|
|
1914
|
+
* - HYBRID_ROUTING_RULES (optional) - Cloudflare routing expression string
|
|
1915
|
+
* - SFCC_ORIGIN (optional) - Base URL for SFCC sandbox redirects
|
|
1916
|
+
*/
|
|
1917
|
+
const regexCache = /* @__PURE__ */ new Map();
|
|
1918
|
+
/**
|
|
1919
|
+
* Extracts regex patterns from a Cloudflare routing expression.
|
|
1920
|
+
*
|
|
1921
|
+
* Parses Cloudflare "matches" expressions like:
|
|
1922
|
+
* (http.request.uri.path matches "^/$" or http.request.uri.path matches "^/category.*")
|
|
1923
|
+
*
|
|
1924
|
+
* And extracts the regex patterns: ["^/$", "^/category.*"]
|
|
1925
|
+
*
|
|
1926
|
+
* @param expression - Cloudflare expression string
|
|
1927
|
+
* @returns Array of regex pattern strings
|
|
1928
|
+
*
|
|
1929
|
+
* @example
|
|
1930
|
+
* ```typescript
|
|
1931
|
+
* extractPatterns('(http.request.uri.path matches "^/$")');
|
|
1932
|
+
* // Returns: ["^/$"]
|
|
1933
|
+
*
|
|
1934
|
+
* extractPatterns('(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/search.*")');
|
|
1935
|
+
* // Returns: ["^/$", "^/search.*"]
|
|
1936
|
+
* ```
|
|
1937
|
+
*/
|
|
1938
|
+
function extractPatterns(expression) {
|
|
1939
|
+
if (!expression || typeof expression !== "string") return [];
|
|
1940
|
+
const regex = /http\.request\.uri\.path\s+matches\s+["']([^"']+)["']/gi;
|
|
1941
|
+
const patterns = [];
|
|
1942
|
+
let match;
|
|
1943
|
+
while ((match = regex.exec(expression)) !== null) patterns.push(match[1]);
|
|
1944
|
+
return patterns;
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Tests if a pathname matches any of the provided regex patterns (logical OR).
|
|
1948
|
+
* Uses caching to optimize repeated pattern compilations.
|
|
1949
|
+
*
|
|
1950
|
+
* @param pathname - URL pathname to test (e.g., "/search", "/category/shoes")
|
|
1951
|
+
* @param patterns - Array of regex pattern strings
|
|
1952
|
+
* @returns true if pathname matches any pattern, false otherwise
|
|
1953
|
+
*
|
|
1954
|
+
* @example
|
|
1955
|
+
* ```typescript
|
|
1956
|
+
* testPatterns('/category/shoes', ['^/category.*', '^/search.*']);
|
|
1957
|
+
* // Returns: true (matches first pattern)
|
|
1958
|
+
*
|
|
1959
|
+
* testPatterns('/checkout', ['^/category.*', '^/search.*']);
|
|
1960
|
+
* // Returns: false (matches no patterns)
|
|
1961
|
+
* ```
|
|
1962
|
+
*/
|
|
1963
|
+
function testPatterns(pathname, patterns) {
|
|
1964
|
+
if (!pathname || !patterns || patterns.length === 0) return false;
|
|
1965
|
+
for (const pattern of patterns) try {
|
|
1966
|
+
let regex = regexCache.get(pattern);
|
|
1967
|
+
if (!regex) {
|
|
1968
|
+
regex = new RegExp(pattern);
|
|
1969
|
+
regexCache.set(pattern, regex);
|
|
1970
|
+
}
|
|
1971
|
+
if (regex.test(pathname)) return true;
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
logger.warn(`Invalid regex pattern: ${pattern} ${String(error)}`);
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Main function: Determines if a pathname should route to Storefront Next
|
|
1980
|
+
* or be proxied/redirected to SFRA/legacy backend.
|
|
1981
|
+
*
|
|
1982
|
+
* @param pathname - URL pathname (e.g., "/search", "/checkout")
|
|
1983
|
+
* @param routingRules - Cloudflare routing expression string (optional)
|
|
1984
|
+
* @returns true if should route to Storefront Next, false if should proxy to SFRA
|
|
1985
|
+
*
|
|
1986
|
+
* @example
|
|
1987
|
+
* ```typescript
|
|
1988
|
+
* const rules = '(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/category.*")';
|
|
1989
|
+
*
|
|
1990
|
+
* shouldRouteToNext('/', rules); // true - route to Next
|
|
1991
|
+
* shouldRouteToNext('/category/mens', rules); // true - route to Next
|
|
1992
|
+
* shouldRouteToNext('/checkout', rules); // false - proxy to SFRA
|
|
1993
|
+
* shouldRouteToNext('/any-path', undefined); // true - no rules = default to Next
|
|
1994
|
+
* ```
|
|
1995
|
+
*/
|
|
1996
|
+
function shouldRouteToNext(pathname, routingRules) {
|
|
1997
|
+
if (!routingRules) return true;
|
|
1998
|
+
const patterns = extractPatterns(routingRules);
|
|
1999
|
+
if (patterns.length === 0) {
|
|
2000
|
+
logger.warn("No valid patterns found in routing rules");
|
|
2001
|
+
return true;
|
|
2002
|
+
}
|
|
2003
|
+
return testPatterns(pathname, patterns);
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Clears the regex cache. Useful for testing or when routing rules change.
|
|
2007
|
+
*
|
|
2008
|
+
* @example
|
|
2009
|
+
* ```typescript
|
|
2010
|
+
* clearCache();
|
|
2011
|
+
* // All cached regex patterns are removed
|
|
2012
|
+
* ```
|
|
2013
|
+
*/
|
|
2014
|
+
function clearCache() {
|
|
2015
|
+
regexCache.clear();
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1303
2018
|
//#endregion
|
|
1304
2019
|
//#region src/server/ts-import.ts
|
|
1305
2020
|
/**
|
|
@@ -1367,18 +2082,15 @@ function loadConfigFromEnv() {
|
|
|
1367
2082
|
const shortCode = process.env.PUBLIC__app__commerce__api__shortCode;
|
|
1368
2083
|
const organizationId = process.env.PUBLIC__app__commerce__api__organizationId;
|
|
1369
2084
|
const clientId = process.env.PUBLIC__app__commerce__api__clientId;
|
|
1370
|
-
const siteId = process.env.PUBLIC__app__commerce__api__siteId;
|
|
1371
2085
|
const proxy = process.env.PUBLIC__app__commerce__api__proxy || "/mobify/proxy/api";
|
|
1372
2086
|
const proxyHost = process.env.SCAPI_PROXY_HOST;
|
|
1373
2087
|
if (!shortCode && !proxyHost) throw new Error("Missing PUBLIC__app__commerce__api__shortCode environment variable.\nPlease set it in your .env file or environment.");
|
|
1374
2088
|
if (!organizationId) throw new Error("Missing PUBLIC__app__commerce__api__organizationId environment variable.\nPlease set it in your .env file or environment.");
|
|
1375
2089
|
if (!clientId) throw new Error("Missing PUBLIC__app__commerce__api__clientId environment variable.\nPlease set it in your .env file or environment.");
|
|
1376
|
-
if (!siteId) throw new Error("Missing PUBLIC__app__commerce__api__siteId environment variable.\nPlease set it in your .env file or environment.");
|
|
1377
2090
|
return { commerce: { api: {
|
|
1378
2091
|
shortCode: shortCode || "",
|
|
1379
2092
|
organizationId,
|
|
1380
2093
|
clientId,
|
|
1381
|
-
siteId,
|
|
1382
2094
|
proxy,
|
|
1383
2095
|
proxyHost
|
|
1384
2096
|
} } };
|
|
@@ -1404,17 +2116,20 @@ async function loadProjectConfig(projectDirectory) {
|
|
|
1404
2116
|
if (!api.shortCode && !proxyHost) throw new Error("Missing shortCode in config.server.ts commerce.api configuration");
|
|
1405
2117
|
if (!api.organizationId) throw new Error("Missing organizationId in config.server.ts commerce.api configuration");
|
|
1406
2118
|
if (!api.clientId) throw new Error("Missing clientId in config.server.ts commerce.api configuration");
|
|
1407
|
-
if (!api.siteId) throw new Error("Missing siteId in config.server.ts commerce.api configuration");
|
|
1408
2119
|
return { commerce: { api: {
|
|
1409
2120
|
shortCode: api.shortCode || "",
|
|
1410
2121
|
organizationId: api.organizationId,
|
|
1411
2122
|
clientId: api.clientId,
|
|
1412
|
-
siteId: api.siteId,
|
|
1413
2123
|
proxy: api.proxy || "/mobify/proxy/api",
|
|
1414
2124
|
proxyHost
|
|
1415
2125
|
} } };
|
|
1416
2126
|
}
|
|
1417
2127
|
|
|
2128
|
+
//#endregion
|
|
2129
|
+
//#region src/config.ts
|
|
2130
|
+
const SFNEXT_BASE_CARTRIDGE_NAME = "app_storefrontnext_base";
|
|
2131
|
+
const SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR = `${SFNEXT_BASE_CARTRIDGE_NAME}/cartridge/experience`;
|
|
2132
|
+
|
|
1418
2133
|
//#endregion
|
|
1419
2134
|
//#region src/server/middleware/proxy.ts
|
|
1420
2135
|
/**
|
|
@@ -1429,25 +2144,6 @@ function createCommerceProxyMiddleware(config) {
|
|
|
1429
2144
|
});
|
|
1430
2145
|
}
|
|
1431
2146
|
|
|
1432
|
-
//#endregion
|
|
1433
|
-
//#region src/utils/logger.ts
|
|
1434
|
-
/**
|
|
1435
|
-
* Logger utilities
|
|
1436
|
-
*/
|
|
1437
|
-
const colors = {
|
|
1438
|
-
warn: "yellow",
|
|
1439
|
-
error: "red",
|
|
1440
|
-
success: "cyan",
|
|
1441
|
-
info: "green",
|
|
1442
|
-
debug: "gray"
|
|
1443
|
-
};
|
|
1444
|
-
const fancyLog = (level, msg) => {
|
|
1445
|
-
const colorFn = chalk[colors[level]];
|
|
1446
|
-
console.log(`${colorFn(level)}: ${msg}`);
|
|
1447
|
-
};
|
|
1448
|
-
const info = (msg) => fancyLog("info", msg);
|
|
1449
|
-
const warn = (msg) => fancyLog("warn", msg);
|
|
1450
|
-
|
|
1451
2147
|
//#endregion
|
|
1452
2148
|
//#region src/server/middleware/static.ts
|
|
1453
2149
|
/**
|
|
@@ -1457,7 +2153,7 @@ const warn = (msg) => fancyLog("warn", msg);
|
|
|
1457
2153
|
function createStaticMiddleware(bundleId, projectDirectory) {
|
|
1458
2154
|
const bundlePath = getBundlePath(bundleId);
|
|
1459
2155
|
const clientBuildDir = path$1.join(projectDirectory, "build", "client");
|
|
1460
|
-
info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
|
|
2156
|
+
logger.info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
|
|
1461
2157
|
return express.static(clientBuildDir, { setHeaders: (res) => {
|
|
1462
2158
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1463
2159
|
res.setHeader("x-local-static-cache-control", "1");
|
|
@@ -1476,7 +2172,7 @@ function getCompressionLevel() {
|
|
|
1476
2172
|
if (raw == null || raw.trim() === "") return DEFAULT;
|
|
1477
2173
|
const level = Number(raw);
|
|
1478
2174
|
if (!(Number.isInteger(level) && level >= 0 && level <= 9)) {
|
|
1479
|
-
warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
|
|
2175
|
+
logger.warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
|
|
1480
2176
|
return DEFAULT;
|
|
1481
2177
|
}
|
|
1482
2178
|
return level;
|
|
@@ -1529,20 +2225,18 @@ function createLoggingMiddleware() {
|
|
|
1529
2225
|
});
|
|
1530
2226
|
morgan.token("method-colored", (req) => {
|
|
1531
2227
|
const method = req.method;
|
|
1532
|
-
const colors
|
|
2228
|
+
const colors = {
|
|
1533
2229
|
GET: chalk.green,
|
|
1534
2230
|
POST: chalk.blue,
|
|
1535
2231
|
PUT: chalk.yellow,
|
|
1536
2232
|
DELETE: chalk.red,
|
|
1537
2233
|
PATCH: chalk.magenta
|
|
1538
2234
|
};
|
|
1539
|
-
return (method && colors
|
|
2235
|
+
return (method && colors[method] || chalk.white)(method);
|
|
1540
2236
|
});
|
|
1541
2237
|
return morgan((tokens, req, res) => {
|
|
1542
2238
|
return [
|
|
1543
|
-
chalk.gray("["),
|
|
1544
2239
|
tokens["method-colored"](req, res),
|
|
1545
|
-
chalk.gray("]"),
|
|
1546
2240
|
tokens.url(req, res),
|
|
1547
2241
|
"-",
|
|
1548
2242
|
tokens["status-colored"](req, res),
|
|
@@ -1594,11 +2288,13 @@ function createHostHeaderMiddleware() {
|
|
|
1594
2288
|
*/
|
|
1595
2289
|
function patchReactRouterBuild(build, bundleId) {
|
|
1596
2290
|
const bundlePath = getBundlePath(bundleId);
|
|
2291
|
+
const basePath = getBasePath();
|
|
1597
2292
|
const patchedAssetsJson = JSON.stringify(build.assets).replace(/"\/assets\//g, `"${bundlePath}assets/`);
|
|
1598
2293
|
const newAssets = JSON.parse(patchedAssetsJson);
|
|
1599
2294
|
return Object.assign({}, build, {
|
|
1600
2295
|
publicPath: bundlePath,
|
|
1601
|
-
assets: newAssets
|
|
2296
|
+
assets: newAssets,
|
|
2297
|
+
...basePath && { basename: basePath }
|
|
1602
2298
|
});
|
|
1603
2299
|
}
|
|
1604
2300
|
|
|
@@ -1631,6 +2327,225 @@ const ServerModeFeatureMap = {
|
|
|
1631
2327
|
}
|
|
1632
2328
|
};
|
|
1633
2329
|
|
|
2330
|
+
//#endregion
|
|
2331
|
+
//#region src/otel/mrt-console-span-exporter.ts
|
|
2332
|
+
var MrtConsoleSpanExporter = class extends ConsoleSpanExporter {
|
|
2333
|
+
export(spans, resultCallback) {
|
|
2334
|
+
for (const span of spans) try {
|
|
2335
|
+
const ctx = span.spanContext();
|
|
2336
|
+
const spanData = {
|
|
2337
|
+
traceId: ctx.traceId,
|
|
2338
|
+
parentId: span.parentSpanId,
|
|
2339
|
+
name: span.name,
|
|
2340
|
+
id: ctx.spanId,
|
|
2341
|
+
kind: span.kind,
|
|
2342
|
+
timestamp: hrTimeToTimeStamp(span.startTime),
|
|
2343
|
+
duration: span.duration,
|
|
2344
|
+
attributes: span.attributes,
|
|
2345
|
+
status: span.status,
|
|
2346
|
+
events: span.events,
|
|
2347
|
+
links: span.links,
|
|
2348
|
+
start_time: span.startTime,
|
|
2349
|
+
end_time: span.endTime,
|
|
2350
|
+
forwardTrace: process.env.SFNEXT_OTEL_ENABLED === "true"
|
|
2351
|
+
};
|
|
2352
|
+
console.info(JSON.stringify(spanData));
|
|
2353
|
+
} catch {}
|
|
2354
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
|
|
2358
|
+
//#endregion
|
|
2359
|
+
//#region src/otel/setup.ts
|
|
2360
|
+
const SERVICE_NAME = "storefront-next";
|
|
2361
|
+
/**
|
|
2362
|
+
* Initializes OpenTelemetry and returns a Tracer from the provider directly.
|
|
2363
|
+
*
|
|
2364
|
+
* Returns the tracer via `provider.getTracer()` instead of the global
|
|
2365
|
+
* `trace.getTracer()` API. In the Vite SSR module runner, the built
|
|
2366
|
+
* dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node
|
|
2367
|
+
* resolve @opentelemetry/api to different module instances (different paths
|
|
2368
|
+
* through pnpm's strict node_modules). Each instance has its own
|
|
2369
|
+
* ProxyTracerProvider singleton, so `provider.register()` sets the delegate
|
|
2370
|
+
* on sdk-trace-node's API instance while our code's `trace.getTracer()`
|
|
2371
|
+
* reads from a separate API instance with no delegate — returning a tracer
|
|
2372
|
+
* backed by a bare BasicTracerProvider with NoopSpanProcessor.
|
|
2373
|
+
*
|
|
2374
|
+
* Getting the tracer directly from the provider bypasses the global registry
|
|
2375
|
+
* entirely, guaranteeing the tracer uses our configured span processors.
|
|
2376
|
+
*/
|
|
2377
|
+
let cachedTracer = null;
|
|
2378
|
+
const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
|
|
2379
|
+
function initTelemetry() {
|
|
2380
|
+
if (cachedTracer) return cachedTracer;
|
|
2381
|
+
try {
|
|
2382
|
+
const provider = new NodeTracerProvider({ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }) });
|
|
2383
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
|
|
2384
|
+
provider.register();
|
|
2385
|
+
if (!globalThis[UNDICI_REGISTERED_KEY]) {
|
|
2386
|
+
globalThis[UNDICI_REGISTERED_KEY] = true;
|
|
2387
|
+
registerInstrumentations({
|
|
2388
|
+
tracerProvider: provider,
|
|
2389
|
+
instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
|
|
2390
|
+
try {
|
|
2391
|
+
const method = request.method.toUpperCase();
|
|
2392
|
+
const url = `${request.origin}${request.path}`;
|
|
2393
|
+
span.updateName(`${method} ${url}`);
|
|
2394
|
+
} catch {}
|
|
2395
|
+
} })]
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
cachedTracer = provider.getTracer(SERVICE_NAME);
|
|
2399
|
+
return cachedTracer;
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
logger.error("[otel] Failed to initialize OpenTelemetry:", error);
|
|
2402
|
+
return null;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/otel/express/middleware.ts
|
|
2408
|
+
function createOtelExpressMiddleware() {
|
|
2409
|
+
const maybeTracer = initTelemetry();
|
|
2410
|
+
if (!maybeTracer) return (_req, _res, next) => next();
|
|
2411
|
+
const tracer = maybeTracer;
|
|
2412
|
+
return (req, res, next) => {
|
|
2413
|
+
try {
|
|
2414
|
+
const url = new URL(req.originalUrl || req.url, "http://localhost").pathname;
|
|
2415
|
+
const method = req.method;
|
|
2416
|
+
tracer.startActiveSpan(`[sfnext] server ${method} ${url}`, { attributes: {
|
|
2417
|
+
"http.request.method": method,
|
|
2418
|
+
"url.path": url
|
|
2419
|
+
} }, (serverSpan) => {
|
|
2420
|
+
try {
|
|
2421
|
+
const spanContext = trace.getSpan(context.active())?.spanContext();
|
|
2422
|
+
if (spanContext) {
|
|
2423
|
+
const flags = spanContext.traceFlags.toString(16).padStart(2, "0");
|
|
2424
|
+
const traceparent = `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
|
|
2425
|
+
res.setHeader("traceparent", traceparent);
|
|
2426
|
+
}
|
|
2427
|
+
} catch {}
|
|
2428
|
+
const serverCtx = context.active();
|
|
2429
|
+
const startTime = performance.now();
|
|
2430
|
+
let streamingSpan = null;
|
|
2431
|
+
let ttfbMs = 0;
|
|
2432
|
+
let ended = false;
|
|
2433
|
+
function recordTTFB() {
|
|
2434
|
+
if (streamingSpan) return;
|
|
2435
|
+
try {
|
|
2436
|
+
ttfbMs = Math.round(performance.now() - startTime);
|
|
2437
|
+
serverSpan.setAttribute("sfnext.ttfb_ms", ttfbMs);
|
|
2438
|
+
streamingSpan = tracer.startSpan(`[sfnext] response streaming ${method} ${url}`, { attributes: {
|
|
2439
|
+
"http.request.method": method,
|
|
2440
|
+
"url.path": url,
|
|
2441
|
+
"sfnext.ttfb_ms": ttfbMs
|
|
2442
|
+
} }, serverCtx);
|
|
2443
|
+
} catch {}
|
|
2444
|
+
}
|
|
2445
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
2446
|
+
res.writeHead = ((...args) => {
|
|
2447
|
+
recordTTFB();
|
|
2448
|
+
return origWriteHead(...args);
|
|
2449
|
+
});
|
|
2450
|
+
const origWrite = res.write.bind(res);
|
|
2451
|
+
res.write = ((...args) => {
|
|
2452
|
+
recordTTFB();
|
|
2453
|
+
return origWrite(...args);
|
|
2454
|
+
});
|
|
2455
|
+
function endSpans() {
|
|
2456
|
+
if (ended) return;
|
|
2457
|
+
ended = true;
|
|
2458
|
+
try {
|
|
2459
|
+
const totalMs = Math.round(performance.now() - startTime);
|
|
2460
|
+
const statusCode = res.statusCode;
|
|
2461
|
+
if (streamingSpan) {
|
|
2462
|
+
streamingSpan.setAttribute("http.streaming_duration_ms", totalMs - ttfbMs);
|
|
2463
|
+
streamingSpan.setAttribute("http.response.status_code", statusCode);
|
|
2464
|
+
if (statusCode >= 500) streamingSpan.setStatus({ code: SpanStatusCode.ERROR });
|
|
2465
|
+
streamingSpan.end();
|
|
2466
|
+
}
|
|
2467
|
+
serverSpan.setAttribute("http.response.status_code", statusCode);
|
|
2468
|
+
serverSpan.setAttribute("http.total_duration_ms", totalMs);
|
|
2469
|
+
if (statusCode >= 500) serverSpan.setStatus({ code: SpanStatusCode.ERROR });
|
|
2470
|
+
serverSpan.end();
|
|
2471
|
+
} catch {}
|
|
2472
|
+
}
|
|
2473
|
+
res.once("close", endSpans);
|
|
2474
|
+
res.once("finish", endSpans);
|
|
2475
|
+
next();
|
|
2476
|
+
});
|
|
2477
|
+
} catch {
|
|
2478
|
+
next();
|
|
2479
|
+
}
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
//#endregion
|
|
2484
|
+
//#region src/server/handlers/health-check.ts
|
|
2485
|
+
const DEFAULT_HEALTH_DESCRIPTION = "storefront-next-dev server health";
|
|
2486
|
+
const PACKAGE_JSON_NAME = "package.json";
|
|
2487
|
+
const RUNTIME_PACKAGE_NAME = "@salesforce/storefront-next-runtime";
|
|
2488
|
+
const DEV_PACKAGE_NAME = "@salesforce/storefront-next-dev";
|
|
2489
|
+
const BUILD_FOLDER_NAME = "build";
|
|
2490
|
+
const LOCAL_BUNDLE_ID = "local";
|
|
2491
|
+
const HEALTH_ENDPOINT_PATH = "/sfdc-health";
|
|
2492
|
+
/**
|
|
2493
|
+
* Reads a package.json file and returns selected metadata.
|
|
2494
|
+
*
|
|
2495
|
+
* @param path - Absolute path to a package.json file
|
|
2496
|
+
* @returns Parsed metadata, or null if missing/unreadable
|
|
2497
|
+
*
|
|
2498
|
+
* @example
|
|
2499
|
+
* ```ts
|
|
2500
|
+
* const metadata = readPackageMetadata('/app/package.json');
|
|
2501
|
+
* console.log(metadata?.version);
|
|
2502
|
+
* ```
|
|
2503
|
+
*/
|
|
2504
|
+
function readPackageMetadata(path$2) {
|
|
2505
|
+
if (!existsSync$1(path$2)) return null;
|
|
2506
|
+
try {
|
|
2507
|
+
return JSON.parse(readFileSync$1(path$2, "utf8"));
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
logger.debug(`Health check: failed to parse package.json at ${path$2}`, error);
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Creates an Express handler that returns Health+JSON for the project.
|
|
2515
|
+
*
|
|
2516
|
+
* @param options - Handler options
|
|
2517
|
+
* @returns Express request handler for the health endpoint
|
|
2518
|
+
*
|
|
2519
|
+
* @example
|
|
2520
|
+
* ```ts
|
|
2521
|
+
* app.get(HEALTH_ENDPOINT_PATH, createHealthCheckHandler({
|
|
2522
|
+
* projectDirectory: process.cwd(),
|
|
2523
|
+
* bundleId: LOCAL_BUNDLE_ID,
|
|
2524
|
+
* }));
|
|
2525
|
+
* ```
|
|
2526
|
+
*/
|
|
2527
|
+
function createHealthCheckHandler(options) {
|
|
2528
|
+
const { projectDirectory, bundleId } = options;
|
|
2529
|
+
const projectPackage = readPackageMetadata(bundleId === LOCAL_BUNDLE_ID ? resolve(projectDirectory, PACKAGE_JSON_NAME) : resolve(projectDirectory, BUILD_FOLDER_NAME, PACKAGE_JSON_NAME));
|
|
2530
|
+
const allDependencies = {
|
|
2531
|
+
...projectPackage?.dependencies,
|
|
2532
|
+
...projectPackage?.devDependencies
|
|
2533
|
+
};
|
|
2534
|
+
const devVersion = allDependencies?.[DEV_PACKAGE_NAME];
|
|
2535
|
+
const runtimeVersion = allDependencies?.[RUNTIME_PACKAGE_NAME];
|
|
2536
|
+
const notes = [devVersion ? `Built using ${DEV_PACKAGE_NAME}@${devVersion}.` : null, runtimeVersion ? `Running ${RUNTIME_PACKAGE_NAME}@${runtimeVersion}.` : null].filter(Boolean);
|
|
2537
|
+
return (_req, res) => {
|
|
2538
|
+
const healthResponse = {
|
|
2539
|
+
status: "pass",
|
|
2540
|
+
version: projectPackage?.version,
|
|
2541
|
+
bundleId,
|
|
2542
|
+
description: projectPackage?.description ?? DEFAULT_HEALTH_DESCRIPTION,
|
|
2543
|
+
notes: notes.length > 0 ? notes : void 0
|
|
2544
|
+
};
|
|
2545
|
+
res.status(200).type("application/health+json").json(healthResponse);
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
1634
2549
|
//#endregion
|
|
1635
2550
|
//#region src/server/index.ts
|
|
1636
2551
|
/** Relative path to the middleware registry TypeScript source (development). Must match appDirectory + server dir + filename used by buildMiddlewareRegistry plugin. */
|
|
@@ -1643,6 +2558,7 @@ const MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS = [
|
|
|
1643
2558
|
];
|
|
1644
2559
|
/** All paths to try when loading the built middlewares (base + extension). */
|
|
1645
2560
|
const RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS = ["bld/server/middleware-registry", "build/server/middleware-registry"].flatMap((base) => MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS.map((ext) => `${base}${ext}`));
|
|
2561
|
+
const DEFAULT_BUNDLE_ID = "local";
|
|
1646
2562
|
/**
|
|
1647
2563
|
* Create a unified Express server for development, preview, or production mode
|
|
1648
2564
|
*/
|
|
@@ -1651,9 +2567,14 @@ async function createServer(options) {
|
|
|
1651
2567
|
if (mode === "development" && !vite) throw new Error("Vite dev server instance is required for development mode");
|
|
1652
2568
|
if ((mode === "preview" || mode === "production") && !build) throw new Error("React Router server build is required for preview/production mode");
|
|
1653
2569
|
const config = providedConfig ?? loadConfigFromEnv();
|
|
1654
|
-
const bundleId = process.env.BUNDLE_ID ??
|
|
2570
|
+
const bundleId = process.env.BUNDLE_ID ?? DEFAULT_BUNDLE_ID;
|
|
1655
2571
|
const app = express();
|
|
1656
2572
|
app.disable("x-powered-by");
|
|
2573
|
+
if (process.env.SFNEXT_OTEL_ENABLED === "true") app.use(createOtelExpressMiddleware());
|
|
2574
|
+
app.get(HEALTH_ENDPOINT_PATH, createHealthCheckHandler({
|
|
2575
|
+
projectDirectory,
|
|
2576
|
+
bundleId
|
|
2577
|
+
}));
|
|
1657
2578
|
if (enableLogging) app.use(createLoggingMiddleware());
|
|
1658
2579
|
if (enableCompression && !streaming) app.use(createCompressionMiddleware());
|
|
1659
2580
|
if (enableStaticServing && build) {
|
|
@@ -1678,8 +2599,14 @@ async function createServer(options) {
|
|
|
1678
2599
|
});
|
|
1679
2600
|
if (mode === "development" && vite) app.use(vite.middlewares);
|
|
1680
2601
|
if (enableProxy) app.use(config.commerce.api.proxy, createCommerceProxyMiddleware(config));
|
|
2602
|
+
const basePath = getBasePath();
|
|
2603
|
+
if (basePath) app.use((req, res, next) => {
|
|
2604
|
+
if (req.path.startsWith(`${basePath}/`) || req.path === basePath) return next();
|
|
2605
|
+
if (req.path.startsWith("/mobify/")) return next();
|
|
2606
|
+
res.redirect(`${basePath}${req.originalUrl}`);
|
|
2607
|
+
});
|
|
1681
2608
|
app.use(createHostHeaderMiddleware());
|
|
1682
|
-
app.all("*", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
|
|
2609
|
+
app.all("*splat", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
|
|
1683
2610
|
return app;
|
|
1684
2611
|
}
|
|
1685
2612
|
/**
|
|
@@ -1707,9 +2634,10 @@ async function createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatch
|
|
|
1707
2634
|
} else if (build) {
|
|
1708
2635
|
let patchedBuild = build;
|
|
1709
2636
|
if (enableAssetUrlPatching) patchedBuild = patchReactRouterBuild(build, bundleId);
|
|
2637
|
+
const requestHandlerMode = process.env.NODE_OPTIONS?.includes("--enable-source-maps") ? "development" : process.env.NODE_ENV;
|
|
1710
2638
|
return createRequestHandler({
|
|
1711
2639
|
build: patchedBuild,
|
|
1712
|
-
mode:
|
|
2640
|
+
mode: requestHandlerMode
|
|
1713
2641
|
});
|
|
1714
2642
|
} else throw new Error("Invalid server configuration: no vite or build provided");
|
|
1715
2643
|
}
|
|
@@ -1731,17 +2659,15 @@ const SINGLE_LINE_MARKER = "@sfdc-extension-line";
|
|
|
1731
2659
|
const BLOCK_MARKER_START = "@sfdc-extension-block-start";
|
|
1732
2660
|
const BLOCK_MARKER_END = "@sfdc-extension-block-end";
|
|
1733
2661
|
const FILE_MARKER = "@sfdc-extension-file";
|
|
1734
|
-
|
|
1735
|
-
function trimExtensions(directory, selectedExtensions, extensionConfig, verboseOverride = false) {
|
|
2662
|
+
function trimExtensions(directory, selectedExtensions, extensionConfig) {
|
|
1736
2663
|
const startTime = Date.now();
|
|
1737
|
-
verbose = verboseOverride ?? false;
|
|
1738
2664
|
const configuredExtensions = extensionConfig?.extensions || {};
|
|
1739
2665
|
const extensions = {};
|
|
1740
2666
|
Object.keys(configuredExtensions).forEach((targetKey) => {
|
|
1741
2667
|
extensions[targetKey] = Boolean(selectedExtensions?.[targetKey]) || false;
|
|
1742
2668
|
});
|
|
1743
2669
|
if (Object.keys(extensions).length === 0) {
|
|
1744
|
-
|
|
2670
|
+
logger.debug("No targets found, skipping trim");
|
|
1745
2671
|
return;
|
|
1746
2672
|
}
|
|
1747
2673
|
const processDirectory = (dir) => {
|
|
@@ -1760,7 +2686,7 @@ function trimExtensions(directory, selectedExtensions, extensionConfig, verboseO
|
|
|
1760
2686
|
updateExtensionConfig(directory, extensions);
|
|
1761
2687
|
}
|
|
1762
2688
|
const endTime = Date.now();
|
|
1763
|
-
|
|
2689
|
+
logger.debug(`Trim extensions took ${endTime - startTime}ms`);
|
|
1764
2690
|
}
|
|
1765
2691
|
/**
|
|
1766
2692
|
* Update the extension config file to only include the selected extensions.
|
|
@@ -1785,15 +2711,14 @@ function processFile(filePath, extensions) {
|
|
|
1785
2711
|
if (source.includes(FILE_MARKER)) {
|
|
1786
2712
|
const markerLine = source.split("\n").find((line) => line.includes(FILE_MARKER));
|
|
1787
2713
|
const extMatch = Object.keys(extensions).find((ext) => markerLine.includes(ext));
|
|
1788
|
-
if (!extMatch) {
|
|
1789
|
-
|
|
1790
|
-
} else if (extensions[extMatch] === false) {
|
|
2714
|
+
if (!extMatch) logger.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
|
|
2715
|
+
else if (extensions[extMatch] === false) {
|
|
1791
2716
|
try {
|
|
1792
2717
|
fs$1.unlinkSync(filePath);
|
|
1793
|
-
|
|
2718
|
+
logger.debug(`Deleted file ${filePath}`);
|
|
1794
2719
|
} catch (e) {
|
|
1795
2720
|
const error = e;
|
|
1796
|
-
|
|
2721
|
+
logger.error(`Error deleting file ${filePath}: ${error.message}`);
|
|
1797
2722
|
throw e;
|
|
1798
2723
|
}
|
|
1799
2724
|
return;
|
|
@@ -1822,7 +2747,7 @@ function processFile(filePath, extensions) {
|
|
|
1822
2747
|
line: i
|
|
1823
2748
|
});
|
|
1824
2749
|
skippingBlock = extensions[matchingExtension] === false;
|
|
1825
|
-
} else
|
|
2750
|
+
} else logger.warn(`Unknown marker found in ${filePath} at line ${i}: \n${line}`);
|
|
1826
2751
|
} else if (line.includes(BLOCK_MARKER_END)) {
|
|
1827
2752
|
if (Object.keys(extensions).find((extension) => line.includes(extension))) {
|
|
1828
2753
|
const extension = Object.keys(extensions).find((p) => line.includes(p));
|
|
@@ -1843,10 +2768,10 @@ function processFile(filePath, extensions) {
|
|
|
1843
2768
|
const newSource = newLines.join("\n");
|
|
1844
2769
|
if (newSource !== source) try {
|
|
1845
2770
|
fs$1.writeFileSync(filePath, newSource);
|
|
1846
|
-
|
|
2771
|
+
logger.debug(`Updated file ${filePath}`);
|
|
1847
2772
|
} catch (e) {
|
|
1848
2773
|
const error = e;
|
|
1849
|
-
|
|
2774
|
+
logger.error(`Error updating file ${filePath}: ${error.message}`);
|
|
1850
2775
|
throw e;
|
|
1851
2776
|
}
|
|
1852
2777
|
}
|
|
@@ -1870,11 +2795,11 @@ function deleteExtensionFolders(projectRoot, extensions, extensionConfig) {
|
|
|
1870
2795
|
recursive: true,
|
|
1871
2796
|
force: true
|
|
1872
2797
|
});
|
|
1873
|
-
|
|
2798
|
+
logger.debug(`Deleted extension folder: ${extensionFolderPath}`);
|
|
1874
2799
|
} catch (err) {
|
|
1875
2800
|
const error = err;
|
|
1876
|
-
if (error.code === "EPERM")
|
|
1877
|
-
else
|
|
2801
|
+
if (error.code === "EPERM") logger.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
|
|
2802
|
+
else logger.error(`Error deleting ${extensionFolderPath}: ${error.message}`);
|
|
1878
2803
|
}
|
|
1879
2804
|
}
|
|
1880
2805
|
});
|
|
@@ -1950,7 +2875,7 @@ function filePathToRoute(filePath, projectRoot) {
|
|
|
1950
2875
|
const routeFileNormalized = routeFilePosix.replace(/^\.\//, "");
|
|
1951
2876
|
if (filePathPosix.endsWith(routeFileNormalized) || filePathPosix.endsWith(`/${routeFileNormalized}`)) return route.path;
|
|
1952
2877
|
}
|
|
1953
|
-
|
|
2878
|
+
logger.warn(`Could not find route for file: ${filePath}`);
|
|
1954
2879
|
return "/unknown";
|
|
1955
2880
|
}
|
|
1956
2881
|
/**
|
|
@@ -2025,7 +2950,7 @@ const TYPE_MAPPING = {
|
|
|
2025
2950
|
function resolveAttributeType(decoratorType, tsMorphType, fieldName) {
|
|
2026
2951
|
if (decoratorType) {
|
|
2027
2952
|
if (!VALID_ATTRIBUTE_TYPES.includes(decoratorType)) {
|
|
2028
|
-
|
|
2953
|
+
logger.error(`Invalid attribute type '${decoratorType}' for field '${fieldName || "unknown"}'. Valid types are: ${VALID_ATTRIBUTE_TYPES.join(", ")}`);
|
|
2029
2954
|
process.exit(1);
|
|
2030
2955
|
}
|
|
2031
2956
|
return decoratorType;
|
|
@@ -2050,6 +2975,30 @@ function getTypeFromTsMorph(property, _sourceFile) {
|
|
|
2050
2975
|
} catch {}
|
|
2051
2976
|
return "string";
|
|
2052
2977
|
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Resolve a variable's initializer expression from the same source file,
|
|
2980
|
+
* unwrapping `as const` type assertions.
|
|
2981
|
+
*/
|
|
2982
|
+
function resolveVariableInitializer(sourceFile, name) {
|
|
2983
|
+
const varDecl = sourceFile.getVariableDeclaration(name);
|
|
2984
|
+
if (!varDecl) return void 0;
|
|
2985
|
+
let initializer = varDecl.getInitializer();
|
|
2986
|
+
if (initializer && Node.isAsExpression(initializer)) initializer = initializer.getExpression();
|
|
2987
|
+
return initializer;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Check whether an AST node is a type that `parseExpression` can resolve to a
|
|
2991
|
+
* concrete JS value (as opposed to falling through to `getText()`).
|
|
2992
|
+
*/
|
|
2993
|
+
function isResolvableLiteral(node) {
|
|
2994
|
+
return Node.isStringLiteral(node) || Node.isNumericLiteral(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node) || Node.isObjectLiteralExpression(node) || Node.isArrayLiteralExpression(node);
|
|
2995
|
+
}
|
|
2996
|
+
var UnresolvedConstantReferenceError = class extends Error {
|
|
2997
|
+
constructor(reference) {
|
|
2998
|
+
super(`Cannot resolve constant reference '${reference}'. Ensure the variable is declared in the same file as a literal value.`);
|
|
2999
|
+
this.name = "UnresolvedConstantReferenceError";
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
2053
3002
|
function parseExpression(expression) {
|
|
2054
3003
|
if (Node.isStringLiteral(expression)) return expression.getLiteralValue();
|
|
2055
3004
|
else if (Node.isNumericLiteral(expression)) return expression.getLiteralValue();
|
|
@@ -2057,7 +3006,26 @@ function parseExpression(expression) {
|
|
|
2057
3006
|
else if (Node.isFalseLiteral(expression)) return false;
|
|
2058
3007
|
else if (Node.isObjectLiteralExpression(expression)) return parseNestedObject(expression);
|
|
2059
3008
|
else if (Node.isArrayLiteralExpression(expression)) return parseArrayLiteral(expression);
|
|
2060
|
-
else
|
|
3009
|
+
else if (Node.isPropertyAccessExpression(expression)) {
|
|
3010
|
+
const obj = expression.getExpression();
|
|
3011
|
+
const propName = expression.getName();
|
|
3012
|
+
if (Node.isIdentifier(obj)) {
|
|
3013
|
+
const resolved = resolveVariableInitializer(expression.getSourceFile(), obj.getText());
|
|
3014
|
+
if (resolved && Node.isObjectLiteralExpression(resolved)) {
|
|
3015
|
+
const prop = resolved.getProperty(propName);
|
|
3016
|
+
if (prop && Node.isPropertyAssignment(prop)) {
|
|
3017
|
+
const propInit = prop.getInitializer();
|
|
3018
|
+
if (propInit) return parseExpression(propInit);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
throw new UnresolvedConstantReferenceError(expression.getText());
|
|
3022
|
+
}
|
|
3023
|
+
return expression.getText();
|
|
3024
|
+
} else if (Node.isIdentifier(expression)) {
|
|
3025
|
+
const resolved = resolveVariableInitializer(expression.getSourceFile(), expression.getText());
|
|
3026
|
+
if (resolved && isResolvableLiteral(resolved)) return parseExpression(resolved);
|
|
3027
|
+
return expression.getText();
|
|
3028
|
+
} else return expression.getText();
|
|
2061
3029
|
}
|
|
2062
3030
|
function parseNestedObject(objectLiteral) {
|
|
2063
3031
|
const result = {};
|
|
@@ -2069,7 +3037,7 @@ function parseNestedObject(objectLiteral) {
|
|
|
2069
3037
|
if (initializer) result[name] = parseExpression(initializer);
|
|
2070
3038
|
}
|
|
2071
3039
|
} catch (error) {
|
|
2072
|
-
|
|
3040
|
+
logger.warn(`Could not parse nested object: ${error.message}`);
|
|
2073
3041
|
return result;
|
|
2074
3042
|
}
|
|
2075
3043
|
return result;
|
|
@@ -2080,7 +3048,7 @@ function parseArrayLiteral(arrayLiteral) {
|
|
|
2080
3048
|
const elements = arrayLiteral.getElements();
|
|
2081
3049
|
for (const element of elements) result.push(parseExpression(element));
|
|
2082
3050
|
} catch (error) {
|
|
2083
|
-
|
|
3051
|
+
logger.warn(`Could not parse array literal: ${error.message}`);
|
|
2084
3052
|
}
|
|
2085
3053
|
return result;
|
|
2086
3054
|
}
|
|
@@ -2113,7 +3081,8 @@ function parseDecoratorArgs(decorator) {
|
|
|
2113
3081
|
}
|
|
2114
3082
|
return result;
|
|
2115
3083
|
} catch (error) {
|
|
2116
|
-
|
|
3084
|
+
if (error instanceof UnresolvedConstantReferenceError) throw error;
|
|
3085
|
+
logger.warn(`Could not parse decorator arguments: ${error.message}`);
|
|
2117
3086
|
return result;
|
|
2118
3087
|
}
|
|
2119
3088
|
}
|
|
@@ -2142,11 +3111,15 @@ function extractAttributesFromSource(sourceFile, className) {
|
|
|
2142
3111
|
attributes.push(attribute);
|
|
2143
3112
|
}
|
|
2144
3113
|
} catch (error) {
|
|
2145
|
-
|
|
3114
|
+
if (error instanceof UnresolvedConstantReferenceError) throw error;
|
|
3115
|
+
logger.warn(`Could not extract attributes from class ${className}: ${error.message}`);
|
|
2146
3116
|
}
|
|
2147
3117
|
return attributes;
|
|
2148
3118
|
}
|
|
2149
|
-
function
|
|
3119
|
+
function normalizeComponentTypeId(typeId, defaultGroup) {
|
|
3120
|
+
return typeId.includes(".") ? typeId : `${defaultGroup}.${typeId}`;
|
|
3121
|
+
}
|
|
3122
|
+
function extractRegionDefinitionsFromSource(sourceFile, className, defaultComponentGroup = DEFAULT_COMPONENT_GROUP) {
|
|
2150
3123
|
const regionDefinitions = [];
|
|
2151
3124
|
try {
|
|
2152
3125
|
const classDeclaration = sourceFile.getClass(className);
|
|
@@ -2165,8 +3138,8 @@ function extractRegionDefinitionsFromSource(sourceFile, className) {
|
|
|
2165
3138
|
name: regionConfig.name || "Region"
|
|
2166
3139
|
};
|
|
2167
3140
|
if (regionConfig.componentTypes) regionDefinition.component_types = regionConfig.componentTypes;
|
|
2168
|
-
if (Array.isArray(regionConfig.componentTypeInclusions)) regionDefinition.component_type_inclusions = regionConfig.componentTypeInclusions.map((incl) => ({ type_id: incl }));
|
|
2169
|
-
if (Array.isArray(regionConfig.componentTypeExclusions)) regionDefinition.component_type_exclusions = regionConfig.componentTypeExclusions.map((excl) => ({ type_id: excl }));
|
|
3141
|
+
if (Array.isArray(regionConfig.componentTypeInclusions)) regionDefinition.component_type_inclusions = regionConfig.componentTypeInclusions.map((incl) => ({ type_id: normalizeComponentTypeId(String(incl), defaultComponentGroup) }));
|
|
3142
|
+
if (Array.isArray(regionConfig.componentTypeExclusions)) regionDefinition.component_type_exclusions = regionConfig.componentTypeExclusions.map((excl) => ({ type_id: normalizeComponentTypeId(String(excl), defaultComponentGroup) }));
|
|
2170
3143
|
if (regionConfig.maxComponents !== void 0) regionDefinition.max_components = regionConfig.maxComponents;
|
|
2171
3144
|
if (regionConfig.minComponents !== void 0) regionDefinition.min_components = regionConfig.minComponents;
|
|
2172
3145
|
if (regionConfig.allowMultiple !== void 0) regionDefinition.allow_multiple = regionConfig.allowMultiple;
|
|
@@ -2177,7 +3150,7 @@ function extractRegionDefinitionsFromSource(sourceFile, className) {
|
|
|
2177
3150
|
}
|
|
2178
3151
|
}
|
|
2179
3152
|
} catch (error) {
|
|
2180
|
-
|
|
3153
|
+
logger.warn(`Warning: Could not extract region definitions from class ${className}: ${error.message}`);
|
|
2181
3154
|
}
|
|
2182
3155
|
return regionDefinitions;
|
|
2183
3156
|
}
|
|
@@ -2198,12 +3171,13 @@ async function processComponentFile(filePath, _projectRoot) {
|
|
|
2198
3171
|
const className = classDeclaration.getName();
|
|
2199
3172
|
if (!className) continue;
|
|
2200
3173
|
const componentConfig = parseDecoratorArgs(componentDecorator);
|
|
3174
|
+
const componentGroup = String(componentConfig.group || DEFAULT_COMPONENT_GROUP);
|
|
2201
3175
|
const attributes = extractAttributesFromSource(sourceFile, className);
|
|
2202
|
-
const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
|
|
3176
|
+
const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className, componentGroup);
|
|
2203
3177
|
const componentMetadata = {
|
|
2204
3178
|
typeId: componentConfig.id || className.toLowerCase(),
|
|
2205
3179
|
name: componentConfig.name || toHumanReadableName(className),
|
|
2206
|
-
group:
|
|
3180
|
+
group: componentGroup,
|
|
2207
3181
|
description: componentConfig.description || `Custom component: ${className}`,
|
|
2208
3182
|
regionDefinitions,
|
|
2209
3183
|
attributes
|
|
@@ -2211,11 +3185,13 @@ async function processComponentFile(filePath, _projectRoot) {
|
|
|
2211
3185
|
components.push(componentMetadata);
|
|
2212
3186
|
}
|
|
2213
3187
|
} catch (error) {
|
|
2214
|
-
|
|
3188
|
+
if (error instanceof UnresolvedConstantReferenceError) throw error;
|
|
3189
|
+
logger.warn(`Could not process file ${filePath}:`, error.message);
|
|
2215
3190
|
}
|
|
2216
3191
|
return components;
|
|
2217
3192
|
} catch (error) {
|
|
2218
|
-
|
|
3193
|
+
if (error instanceof UnresolvedConstantReferenceError) throw error;
|
|
3194
|
+
logger.warn(`Could not read file ${filePath}:`, error.message);
|
|
2219
3195
|
return [];
|
|
2220
3196
|
}
|
|
2221
3197
|
}
|
|
@@ -2251,11 +3227,11 @@ async function processPageTypeFile(filePath, projectRoot) {
|
|
|
2251
3227
|
pageTypes.push(pageTypeMetadata);
|
|
2252
3228
|
}
|
|
2253
3229
|
} catch (error) {
|
|
2254
|
-
|
|
3230
|
+
logger.warn(`Could not process file ${filePath}:`, error.message);
|
|
2255
3231
|
}
|
|
2256
3232
|
return pageTypes;
|
|
2257
3233
|
} catch (error) {
|
|
2258
|
-
|
|
3234
|
+
logger.warn(`Could not read file ${filePath}:`, error.message);
|
|
2259
3235
|
return [];
|
|
2260
3236
|
}
|
|
2261
3237
|
}
|
|
@@ -2278,11 +3254,11 @@ async function processAspectFile(filePath, _projectRoot) {
|
|
|
2278
3254
|
};
|
|
2279
3255
|
aspects.push(aspectMetadata);
|
|
2280
3256
|
} catch (parseError) {
|
|
2281
|
-
|
|
3257
|
+
logger.warn(`Could not parse JSON in file ${filePath}:`, parseError.message);
|
|
2282
3258
|
}
|
|
2283
3259
|
return aspects;
|
|
2284
3260
|
} catch (error) {
|
|
2285
|
-
|
|
3261
|
+
logger.warn(`Could not read file ${filePath}:`, error.message);
|
|
2286
3262
|
return [];
|
|
2287
3263
|
}
|
|
2288
3264
|
}
|
|
@@ -2311,7 +3287,7 @@ async function generateComponentCartridge(component, outputDir, dryRun = false)
|
|
|
2311
3287
|
await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
|
|
2312
3288
|
}
|
|
2313
3289
|
const prefix = dryRun ? " - [DRY RUN]" : " -";
|
|
2314
|
-
|
|
3290
|
+
logger.debug(`${prefix} ${String(component.typeId)}: ${String(component.name)} (${String(component.attributes.length)} attributes) → ${fileName}.json`);
|
|
2315
3291
|
}
|
|
2316
3292
|
async function generatePageTypeCartridge(pageType, outputDir, dryRun = false) {
|
|
2317
3293
|
const fileName = toCamelCaseFileName(pageType.name);
|
|
@@ -2334,7 +3310,7 @@ async function generatePageTypeCartridge(pageType, outputDir, dryRun = false) {
|
|
|
2334
3310
|
await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
|
|
2335
3311
|
}
|
|
2336
3312
|
const prefix = dryRun ? " - [DRY RUN]" : " -";
|
|
2337
|
-
|
|
3313
|
+
logger.debug(`${prefix} ${String(pageType.name)}: ${String(pageType.description)} (${String(pageType.attributes.length)} attributes) → ${fileName}.json`);
|
|
2338
3314
|
}
|
|
2339
3315
|
async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
|
|
2340
3316
|
const fileName = toCamelCaseFileName(aspect.id);
|
|
@@ -2350,7 +3326,7 @@ async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
|
|
|
2350
3326
|
await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
|
|
2351
3327
|
}
|
|
2352
3328
|
const prefix = dryRun ? " - [DRY RUN]" : " -";
|
|
2353
|
-
|
|
3329
|
+
logger.debug(`${prefix} ${String(aspect.name)}: ${String(aspect.description)} (${String(aspect.attributeDefinitions.length)} attributes) → ${fileName}.json`);
|
|
2354
3330
|
}
|
|
2355
3331
|
/**
|
|
2356
3332
|
* Runs ESLint with --fix on the specified directory to format JSON files.
|
|
@@ -2358,20 +3334,20 @@ async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
|
|
|
2358
3334
|
*/
|
|
2359
3335
|
function lintGeneratedFiles(metadataDir, projectRoot) {
|
|
2360
3336
|
try {
|
|
2361
|
-
|
|
3337
|
+
logger.debug("🔧 Running ESLint --fix on generated JSON files...");
|
|
2362
3338
|
execSync(`npx eslint "${metadataDir}/**/*.json" --fix --no-error-on-unmatched-pattern`, {
|
|
2363
3339
|
cwd: projectRoot,
|
|
2364
3340
|
stdio: "pipe",
|
|
2365
3341
|
encoding: "utf-8"
|
|
2366
3342
|
});
|
|
2367
|
-
|
|
3343
|
+
logger.debug("✅ JSON files formatted successfully");
|
|
2368
3344
|
} catch (error) {
|
|
2369
3345
|
const execError = error;
|
|
2370
3346
|
if (execError.status === 2) {
|
|
2371
3347
|
const errMsg = execError.stderr || execError.stdout || "Unknown error";
|
|
2372
|
-
|
|
2373
|
-
} else if (execError.stderr && execError.stderr.includes("error"))
|
|
2374
|
-
else
|
|
3348
|
+
logger.warn(`⚠️ Could not run ESLint --fix: ${errMsg}`);
|
|
3349
|
+
} else if (execError.stderr && execError.stderr.includes("error")) logger.warn(`⚠️ Some linting issues could not be auto-fixed. Run ESLint manually to review.`);
|
|
3350
|
+
else logger.debug("✅ JSON files formatted successfully");
|
|
2375
3351
|
}
|
|
2376
3352
|
}
|
|
2377
3353
|
async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
@@ -2379,9 +3355,9 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2379
3355
|
const filePaths = options?.filePaths;
|
|
2380
3356
|
const isIncrementalMode = filePaths && filePaths.length > 0;
|
|
2381
3357
|
const dryRun = options?.dryRun || false;
|
|
2382
|
-
if (dryRun)
|
|
2383
|
-
else if (isIncrementalMode)
|
|
2384
|
-
else
|
|
3358
|
+
if (dryRun) logger.debug("🔍 [DRY RUN] Scanning for decorated components and page types...");
|
|
3359
|
+
else if (isIncrementalMode) logger.debug(`🔍 Generating metadata for ${filePaths.length} specified file(s)...`);
|
|
3360
|
+
else logger.debug("🔍 Generating metadata for decorated components and page types...");
|
|
2385
3361
|
const projectRoot = resolve(projectDirectory);
|
|
2386
3362
|
const srcDir = join(projectRoot, "src");
|
|
2387
3363
|
const metadataDir = resolve(metadataDirectory);
|
|
@@ -2390,7 +3366,7 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2390
3366
|
const aspectsOutputDir = join(metadataDir, "aspects");
|
|
2391
3367
|
if (!dryRun) {
|
|
2392
3368
|
if (!isIncrementalMode) {
|
|
2393
|
-
|
|
3369
|
+
logger.debug("🗑️ Cleaning existing output directories...");
|
|
2394
3370
|
for (const outputDir of [
|
|
2395
3371
|
componentsOutputDir,
|
|
2396
3372
|
pagesOutputDir,
|
|
@@ -2400,12 +3376,12 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2400
3376
|
recursive: true,
|
|
2401
3377
|
force: true
|
|
2402
3378
|
});
|
|
2403
|
-
|
|
3379
|
+
logger.debug(` - Deleted: ${outputDir}`);
|
|
2404
3380
|
} catch {
|
|
2405
|
-
|
|
3381
|
+
logger.debug(` - Directory not found (skipping): ${outputDir}`);
|
|
2406
3382
|
}
|
|
2407
|
-
} else
|
|
2408
|
-
|
|
3383
|
+
} else logger.debug("📝 Incremental mode: existing cartridge files will be preserved/overwritten");
|
|
3384
|
+
logger.debug("Creating output directories...");
|
|
2409
3385
|
for (const outputDir of [
|
|
2410
3386
|
componentsOutputDir,
|
|
2411
3387
|
pagesOutputDir,
|
|
@@ -2416,16 +3392,18 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2416
3392
|
try {
|
|
2417
3393
|
await access(outputDir);
|
|
2418
3394
|
} catch {
|
|
2419
|
-
|
|
3395
|
+
const err = error;
|
|
3396
|
+
logger.error(`❌ Failed to create output directory ${outputDir}: ${err.message}`);
|
|
2420
3397
|
process.exit(1);
|
|
3398
|
+
throw err;
|
|
2421
3399
|
}
|
|
2422
3400
|
}
|
|
2423
|
-
} else if (isIncrementalMode)
|
|
2424
|
-
else
|
|
3401
|
+
} else if (isIncrementalMode) logger.debug(`📝 [DRY RUN] Would process ${filePaths.length} specific file(s)`);
|
|
3402
|
+
else logger.debug("📝 [DRY RUN] Would clean and regenerate all metadata files");
|
|
2425
3403
|
let files = [];
|
|
2426
3404
|
if (isIncrementalMode && filePaths) {
|
|
2427
3405
|
files = filePaths.map((fp) => resolve(projectRoot, fp));
|
|
2428
|
-
|
|
3406
|
+
logger.debug(`📂 Processing ${files.length} specified file(s)...`);
|
|
2429
3407
|
} else {
|
|
2430
3408
|
const scanDirectory = async (dir) => {
|
|
2431
3409
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -2450,7 +3428,7 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2450
3428
|
allAspects.push(...aspects);
|
|
2451
3429
|
}
|
|
2452
3430
|
if (allComponents.length === 0 && allPageTypes.length === 0 && allAspects.length === 0) {
|
|
2453
|
-
|
|
3431
|
+
logger.info("⚠️ No decorated components, page types, or aspect files found.");
|
|
2454
3432
|
return {
|
|
2455
3433
|
componentsGenerated: 0,
|
|
2456
3434
|
pageTypesGenerated: 0,
|
|
@@ -2459,22 +3437,22 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2459
3437
|
};
|
|
2460
3438
|
}
|
|
2461
3439
|
if (allComponents.length > 0) {
|
|
2462
|
-
|
|
3440
|
+
logger.debug(`✅ Found ${allComponents.length} decorated component(s)`);
|
|
2463
3441
|
for (const component of allComponents) await generateComponentCartridge(component, componentsOutputDir, dryRun);
|
|
2464
|
-
if (dryRun)
|
|
2465
|
-
else
|
|
3442
|
+
if (dryRun) logger.info(`[DRY RUN] Would generate ${allComponents.length} component metadata file(s)`);
|
|
3443
|
+
else logger.info(`Generated ${allComponents.length} component metadata file(s)`);
|
|
2466
3444
|
}
|
|
2467
3445
|
if (allPageTypes.length > 0) {
|
|
2468
|
-
|
|
3446
|
+
logger.debug(`✅ Found ${allPageTypes.length} decorated page type(s)`);
|
|
2469
3447
|
for (const pageType of allPageTypes) await generatePageTypeCartridge(pageType, pagesOutputDir, dryRun);
|
|
2470
|
-
if (dryRun)
|
|
2471
|
-
else
|
|
3448
|
+
if (dryRun) logger.info(`[DRY RUN] Would generate ${allPageTypes.length} page type metadata file(s)`);
|
|
3449
|
+
else logger.info(`Generated ${allPageTypes.length} page type metadata file(s)`);
|
|
2472
3450
|
}
|
|
2473
3451
|
if (allAspects.length > 0) {
|
|
2474
|
-
|
|
3452
|
+
logger.debug(`✅ Found ${allAspects.length} decorated aspect(s)`);
|
|
2475
3453
|
for (const aspect of allAspects) await generateAspectCartridge(aspect, aspectsOutputDir, dryRun);
|
|
2476
|
-
if (dryRun)
|
|
2477
|
-
else
|
|
3454
|
+
if (dryRun) logger.info(`[DRY RUN] Would generate ${allAspects.length} aspect metadata file(s)`);
|
|
3455
|
+
else logger.info(`Generated ${allAspects.length} aspect metadata file(s)`);
|
|
2478
3456
|
}
|
|
2479
3457
|
const shouldLintFix = options?.lintFix !== false;
|
|
2480
3458
|
if (!dryRun && shouldLintFix && (allComponents.length > 0 || allPageTypes.length > 0 || allAspects.length > 0)) lintGeneratedFiles(metadataDir, projectRoot);
|
|
@@ -2485,11 +3463,13 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
2485
3463
|
totalFiles: allComponents.length + allPageTypes.length + allAspects.length
|
|
2486
3464
|
};
|
|
2487
3465
|
} catch (error) {
|
|
2488
|
-
|
|
3466
|
+
const err = error;
|
|
3467
|
+
logger.error("❌ Error:", err.message);
|
|
2489
3468
|
process.exit(1);
|
|
3469
|
+
throw err;
|
|
2490
3470
|
}
|
|
2491
3471
|
}
|
|
2492
3472
|
|
|
2493
3473
|
//#endregion
|
|
2494
|
-
export { createServer, storefrontNextTargets as default, generateMetadata, loadConfigFromEnv, loadProjectConfig, transformTargetPlaceholderPlugin, trimExtensions };
|
|
3474
|
+
export { clearCache, createServer, storefrontNextTargets as default, extractPatterns, generateMetadata, hybridProxyPlugin, loadConfigFromEnv, loadProjectConfig, shouldRouteToNext, testPatterns, transformTargetPlaceholderPlugin, trimExtensions };
|
|
2495
3475
|
//# sourceMappingURL=index.js.map
|