@salesforce/storefront-next-dev 1.0.1 → 1.1.0-alpha.1
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 +4 -0
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/config/aggregate-extensions.js +279 -0
- package/dist/commands/create-storefront.js +37 -6
- package/dist/commands/locales/aggregate-extensions.js +3 -2
- package/dist/commands/push.js +2 -0
- package/dist/constants.js +29 -0
- package/dist/dependency-utils.js +56 -4
- package/dist/entry/server.d.ts +1 -1
- package/dist/entry/server.js +68 -23
- package/dist/entry/server.js.map +1 -1
- package/dist/generate-cartridge.js +4 -0
- package/dist/index.d.ts +58 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +255 -123
- package/dist/index.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/manage-extensions.js +1 -1
- package/dist/mrt/ssr.mjs +53 -53
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +59 -59
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/server.js +79 -28
- package/package.json +13 -11
package/dist/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import path from "node:path";
|
|
1
|
+
import path, { resolve } from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import path$1, { dirname, join, relative, resolve as resolve$1 } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
6
7
|
import { parse } from "@babel/parser";
|
|
7
|
-
import {
|
|
8
|
+
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";
|
|
8
9
|
import { generate } from "@babel/generator";
|
|
9
10
|
import traverseModule from "@babel/traverse";
|
|
10
11
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
11
12
|
import { glob } from "glob";
|
|
12
13
|
import { Node, Project, ts } from "ts-morph";
|
|
13
|
-
import fs$1 from "node:fs";
|
|
14
|
+
import fs$1, { existsSync as existsSync$1, readFileSync as readFileSync$1 } from "node:fs";
|
|
14
15
|
import { deadCodeElimination, findReferencedIdentifiers } from "babel-dead-code-elimination";
|
|
15
16
|
import httpProxy from "http-proxy";
|
|
16
17
|
import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
|
|
18
|
+
import { extractPrefixParamValues, stripPathPrefix } from "@salesforce/storefront-next-runtime/site-context";
|
|
17
19
|
|
|
18
20
|
//#region src/utils/logger.ts
|
|
19
21
|
const LEVEL_PRIORITY = {
|
|
@@ -367,7 +369,7 @@ const patchReactRouterPlugin = () => {
|
|
|
367
369
|
|
|
368
370
|
//#endregion
|
|
369
371
|
//#region src/extensibility/target-utils.ts
|
|
370
|
-
const traverse$
|
|
372
|
+
const traverse$1 = traverseModule.default || traverseModule;
|
|
371
373
|
const TARGET_COMPONENT_TAG = "UITarget";
|
|
372
374
|
const TARGET_PROVIDERS_TAG = "UITargetProviders";
|
|
373
375
|
const TARGET_ID_ATTRIBUTE = "targetId";
|
|
@@ -442,7 +444,7 @@ function runReplacementPass(ast, tagName, targetRegistry = null, contextProvider
|
|
|
442
444
|
if (replacedId) targetIdsReplaced.add(replacedId);
|
|
443
445
|
} else if (contextProviders) findAndReplaceProviders(pathToReplace, contextProviders);
|
|
444
446
|
};
|
|
445
|
-
traverse$
|
|
447
|
+
traverse$1(ast, {
|
|
446
448
|
VariableDeclaration(nodePath) {
|
|
447
449
|
const declarationPaths = nodePath.get("declarations");
|
|
448
450
|
const declarationsArray = Array.isArray(declarationPaths) ? declarationPaths : [declarationPaths];
|
|
@@ -495,7 +497,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
|
|
|
495
497
|
});
|
|
496
498
|
if (TARGET_COMPONENT_JSX_RE.test(code)) {
|
|
497
499
|
const replacementImportStatements = buildReplacementImportStatements(runReplacementPass(ast, TARGET_COMPONENT_TAG, targetRegistry, null), targetRegistry);
|
|
498
|
-
traverse$
|
|
500
|
+
traverse$1(ast, { ImportDeclaration(nodePath) {
|
|
499
501
|
if (nodePath.node.source.value === "@/targets/ui-target") nodePath.replaceWith(jsxText(replacementImportStatements));
|
|
500
502
|
} });
|
|
501
503
|
}
|
|
@@ -503,7 +505,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
|
|
|
503
505
|
const importStatements = /* @__PURE__ */ new Set();
|
|
504
506
|
for (const contextProvider of contextProviders) importStatements.add(`import ${contextProvider.componentName} from '@/${contextProvider.path.replace(".tsx", "")}';`);
|
|
505
507
|
const replacementImportStatements = Array.from(importStatements).join("\n");
|
|
506
|
-
traverse$
|
|
508
|
+
traverse$1(ast, { ImportDeclaration(nodePath) {
|
|
507
509
|
if (nodePath.node.source.value === "@/targets/ui-target-providers") nodePath.replaceWith(jsxText(replacementImportStatements));
|
|
508
510
|
} });
|
|
509
511
|
runReplacementPass(ast, TARGET_PROVIDERS_TAG, null, contextProviders);
|
|
@@ -605,6 +607,44 @@ function buildTargetRegistry(rootDir, options = {}) {
|
|
|
605
607
|
actionHookRegistry
|
|
606
608
|
};
|
|
607
609
|
}
|
|
610
|
+
const TARGET_ID_PATTERN = /<UITarget[\s][^>]*targetId=["']([^"']+)["']/g;
|
|
611
|
+
/**
|
|
612
|
+
* Recursively collect all UITarget IDs declared in template source files.
|
|
613
|
+
* Excludes extension directories so only "real" UITarget placements are counted.
|
|
614
|
+
*/
|
|
615
|
+
function collectUITargetIds(sourceDir) {
|
|
616
|
+
const result = /* @__PURE__ */ new Set();
|
|
617
|
+
function walk(dir) {
|
|
618
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
619
|
+
for (const entry of entries) {
|
|
620
|
+
const fullPath = path$1.join(dir, entry.name);
|
|
621
|
+
if (entry.isDirectory()) {
|
|
622
|
+
if (entry.name === "extensions") continue;
|
|
623
|
+
walk(fullPath);
|
|
624
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name) && !/\.test\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
625
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
626
|
+
TARGET_ID_PATTERN.lastIndex = 0;
|
|
627
|
+
let match;
|
|
628
|
+
while ((match = TARGET_ID_PATTERN.exec(content)) !== null) result.add(match[1]);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
walk(sourceDir);
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Validate that all targetIds in the component registry correspond to
|
|
637
|
+
* UITarget declarations in the template source. Returns orphaned entries.
|
|
638
|
+
*/
|
|
639
|
+
function validateTargetRegistry(componentRegistry, declaredTargetIds) {
|
|
640
|
+
const orphaned = [];
|
|
641
|
+
for (const targetId in componentRegistry) if (!declaredTargetIds.has(targetId)) for (const entry of componentRegistry[targetId]) orphaned.push({
|
|
642
|
+
targetId,
|
|
643
|
+
extension: entry.namespace,
|
|
644
|
+
componentPath: entry.path
|
|
645
|
+
});
|
|
646
|
+
return orphaned;
|
|
647
|
+
}
|
|
608
648
|
|
|
609
649
|
//#endregion
|
|
610
650
|
//#region src/plugins/transformTargets.ts
|
|
@@ -622,9 +662,14 @@ function transformTargetPlaceholderPlugin() {
|
|
|
622
662
|
},
|
|
623
663
|
buildStart() {
|
|
624
664
|
({componentRegistry, contextProviders} = buildTargetRegistry(sourceDir, { isProduction }));
|
|
665
|
+
const declaredTargetIds = collectUITargetIds(sourceDir);
|
|
666
|
+
const orphaned = validateTargetRegistry(componentRegistry, declaredTargetIds);
|
|
667
|
+
if (orphaned.length > 0) {
|
|
668
|
+
const lines = orphaned.map((o) => ` • "${o.targetId}" (extension: ${o.extension}, component: ${o.componentPath})`);
|
|
669
|
+
throw new Error(`[storefront-next] ${orphaned.length} extension component(s) target UITarget IDs that do not exist in the template:\n${lines.join("\n")}\n\nEither add a <UITarget targetId="..."> to the template or remove/disable the component in target-config.json.`);
|
|
670
|
+
}
|
|
625
671
|
},
|
|
626
672
|
transform(code, id) {
|
|
627
|
-
if (process.env.VITE_UI_TARGET_DEV_MODE === "true") return null;
|
|
628
673
|
try {
|
|
629
674
|
const transformedCode = transformTargets(code, componentRegistry, contextProviders);
|
|
630
675
|
if (transformedCode) return {
|
|
@@ -833,7 +878,7 @@ async function scanComponents(project, projectRoot, componentPath, registryPath)
|
|
|
833
878
|
});
|
|
834
879
|
logger.debug(`🔍 Scanning ${componentFiles.length} files in ${componentPath}...`);
|
|
835
880
|
const components = [];
|
|
836
|
-
const registryDir = dirname(resolve(projectRoot, registryPath));
|
|
881
|
+
const registryDir = dirname(resolve$1(projectRoot, registryPath));
|
|
837
882
|
for (const filePath of componentFiles) try {
|
|
838
883
|
const content = readFileSync(filePath, "utf-8");
|
|
839
884
|
const sourceFile = project.createSourceFile(filePath, content, { overwrite: true });
|
|
@@ -876,11 +921,10 @@ async function scanComponents(project, projectRoot, componentPath, registryPath)
|
|
|
876
921
|
function generateRegistryCode(components, registryIdentifier = "registry") {
|
|
877
922
|
const sorted = [...components].sort((a, b) => a.id.localeCompare(b.id) || a.relativePath.localeCompare(b.relativePath));
|
|
878
923
|
if (sorted.length === 0) return `
|
|
879
|
-
/* eslint-disable */
|
|
880
924
|
/**
|
|
881
925
|
* Initialize the static component registry.
|
|
882
926
|
* This function is auto-generated by the staticRegistry Vite plugin.
|
|
883
|
-
*
|
|
927
|
+
*
|
|
884
928
|
* DO NOT EDIT THIS FUNCTION MANUALLY - it will be overwritten on next build.
|
|
885
929
|
*/
|
|
886
930
|
export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
|
|
@@ -897,13 +941,12 @@ export function initializeRegistry(targetRegistry = ${registryIdentifier}): void
|
|
|
897
941
|
} else return ` targetRegistry.registerImporter('${id}', () => import('${relativePath}'));`;
|
|
898
942
|
}).join("\n");
|
|
899
943
|
return `
|
|
900
|
-
/* eslint-disable */
|
|
901
944
|
/**
|
|
902
945
|
* Initialize the static component registry.
|
|
903
946
|
* This function is auto-generated by the staticRegistry Vite plugin.
|
|
904
|
-
*
|
|
947
|
+
*
|
|
905
948
|
* DO NOT EDIT THIS FUNCTION MANUALLY - it will be overwritten on next build.
|
|
906
|
-
*
|
|
949
|
+
*
|
|
907
950
|
* Components registered: ${sorted.map((c) => c.id).join(", ")}
|
|
908
951
|
*/
|
|
909
952
|
export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
|
|
@@ -912,9 +955,42 @@ ${registrations}
|
|
|
912
955
|
`;
|
|
913
956
|
}
|
|
914
957
|
/**
|
|
958
|
+
* Formats registry file content with the project's own Prettier so the written file
|
|
959
|
+
* matches what the project's formatter would produce.
|
|
960
|
+
*
|
|
961
|
+
* Without this, a standalone `prettier --write` (pre-commit hook, format-on-save) rewrites
|
|
962
|
+
* the generated file on every commit: the generator emits one `registerImporter` call per
|
|
963
|
+
* line and cannot anticipate an arbitrary `printWidth`, so Prettier re-wraps long lines and
|
|
964
|
+
* the regenerate -> format -> regenerate loop never settles.
|
|
965
|
+
*
|
|
966
|
+
* Prettier is resolved from the registry file's own location — i.e. the consuming project's
|
|
967
|
+
* `node_modules`, not the SDK bundle (which keeps `node_modules` external). It is the project's
|
|
968
|
+
* Prettier version and config whose output must be matched. Returns the content unchanged when
|
|
969
|
+
* Prettier is absent or fails, so registry generation never breaks a build over formatting.
|
|
970
|
+
*/
|
|
971
|
+
let warnedNoPrettier = false;
|
|
972
|
+
async function formatWithProjectPrettier(content, registryFilePath) {
|
|
973
|
+
try {
|
|
974
|
+
const prettier = createRequire(registryFilePath)("prettier");
|
|
975
|
+
const config = await prettier.resolveConfig(registryFilePath, { editorconfig: true });
|
|
976
|
+
return await prettier.format(content, {
|
|
977
|
+
...config,
|
|
978
|
+
filepath: registryFilePath
|
|
979
|
+
});
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (error.code === "MODULE_NOT_FOUND") {
|
|
982
|
+
if (!warnedNoPrettier) {
|
|
983
|
+
logger.warn("⚠️ Prettier not found in the project; static registry will be written unformatted.");
|
|
984
|
+
warnedNoPrettier = true;
|
|
985
|
+
}
|
|
986
|
+
} else logger.warn(`⚠️ Skipping Prettier formatting for registry file: ${error.message}`);
|
|
987
|
+
return content;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
915
991
|
* Updates the registry.ts file with the generated code
|
|
916
992
|
*/
|
|
917
|
-
function updateRegistryFile(registryFilePath, generatedCode) {
|
|
993
|
+
async function updateRegistryFile(registryFilePath, generatedCode) {
|
|
918
994
|
let existingContent;
|
|
919
995
|
if (!existsSync(registryFilePath)) {
|
|
920
996
|
logger.debug("📝 Creating new registry file...");
|
|
@@ -939,7 +1015,7 @@ export const registry = new ComponentRegistry();
|
|
|
939
1015
|
const startIndex = existingContent.indexOf(startMarker);
|
|
940
1016
|
const endIndex = existingContent.indexOf(endMarker);
|
|
941
1017
|
if (startIndex === -1 || endIndex === -1) throw new Error(`Registry file ${registryFilePath} is missing static registry markers. Please add "${startMarker}" and "${endMarker}" markers to define the generated content area.`);
|
|
942
|
-
const updatedContent = `${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}
|
|
1018
|
+
const updatedContent = await formatWithProjectPrettier(`${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}`, registryFilePath);
|
|
943
1019
|
if (updatedContent === existingContent) {
|
|
944
1020
|
logger.debug(`⏭️ Registry unchanged, skipping write: ${registryFilePath}`);
|
|
945
1021
|
return false;
|
|
@@ -990,8 +1066,8 @@ const staticRegistryPlugin = (config = {}) => {
|
|
|
990
1066
|
} }), projectRoot, componentPath, registryPath);
|
|
991
1067
|
logger.debug(`📦 Found ${components.length} components with @Component decorators`);
|
|
992
1068
|
const generatedCode = generateRegistryCode(components, registryIdentifier);
|
|
993
|
-
const registryFilePath = resolve(projectRoot, registryPath);
|
|
994
|
-
const changed = updateRegistryFile(registryFilePath, generatedCode);
|
|
1069
|
+
const registryFilePath = resolve$1(projectRoot, registryPath);
|
|
1070
|
+
const changed = await updateRegistryFile(registryFilePath, generatedCode);
|
|
995
1071
|
logger.debug("✅ Static registry generation complete!");
|
|
996
1072
|
return {
|
|
997
1073
|
registryFilePath,
|
|
@@ -1033,15 +1109,68 @@ const staticRegistryPlugin = (config = {}) => {
|
|
|
1033
1109
|
};
|
|
1034
1110
|
};
|
|
1035
1111
|
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region src/server/ts-import.ts
|
|
1114
|
+
/**
|
|
1115
|
+
* Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
|
|
1116
|
+
*
|
|
1117
|
+
* @param tsconfigPath - Path to tsconfig.json
|
|
1118
|
+
* @param projectDirectory - Project root directory for resolving relative paths
|
|
1119
|
+
* @returns Record of alias mappings for jiti
|
|
1120
|
+
*
|
|
1121
|
+
* @example
|
|
1122
|
+
* // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
|
|
1123
|
+
* // Returns: { "@/": "/absolute/path/to/src/" }
|
|
1124
|
+
*/
|
|
1125
|
+
function parseTsconfigPaths(tsconfigPath, projectDirectory) {
|
|
1126
|
+
const alias = {};
|
|
1127
|
+
if (!existsSync$1(tsconfigPath)) return alias;
|
|
1128
|
+
try {
|
|
1129
|
+
const tsconfigContent = readFileSync$1(tsconfigPath, "utf-8");
|
|
1130
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
1131
|
+
const paths = tsconfig.compilerOptions?.paths;
|
|
1132
|
+
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
1133
|
+
if (paths) {
|
|
1134
|
+
for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
|
|
1135
|
+
const aliasKey = key.replace(/\/\*$/, "/");
|
|
1136
|
+
alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} catch {}
|
|
1140
|
+
const sortedAlias = {};
|
|
1141
|
+
Object.keys(alias).sort((a, b) => b.length - a.length).forEach((key) => {
|
|
1142
|
+
sortedAlias[key] = alias[key];
|
|
1143
|
+
});
|
|
1144
|
+
return sortedAlias;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Import a TypeScript file using jiti with proper path alias resolution.
|
|
1148
|
+
* This is a cross-platform alternative to tsx that works on Windows.
|
|
1149
|
+
*
|
|
1150
|
+
* @param filePath - Absolute path to the TypeScript file to import
|
|
1151
|
+
* @param options - Import options including project directory
|
|
1152
|
+
* @returns The imported module
|
|
1153
|
+
*/
|
|
1154
|
+
async function importTypescript(filePath, options) {
|
|
1155
|
+
const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
|
|
1156
|
+
const { createJiti } = await import("jiti");
|
|
1157
|
+
const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
|
|
1158
|
+
return createJiti(import.meta.url, {
|
|
1159
|
+
fsCache: false,
|
|
1160
|
+
interopDefault: true,
|
|
1161
|
+
alias
|
|
1162
|
+
}).import(filePath);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1036
1165
|
//#endregion
|
|
1037
1166
|
//#region src/plugins/configLoader.ts
|
|
1038
1167
|
/**
|
|
1039
1168
|
* Load the engagement config from config.server.ts
|
|
1040
1169
|
*/
|
|
1041
1170
|
async function loadEngagementConfig(projectRoot, configPath) {
|
|
1042
|
-
const absoluteConfigPath = resolve(projectRoot, configPath);
|
|
1171
|
+
const absoluteConfigPath = resolve$1(projectRoot, configPath);
|
|
1043
1172
|
try {
|
|
1044
|
-
const config = (await
|
|
1173
|
+
const config = (await importTypescript(absoluteConfigPath, { projectDirectory: projectRoot })).default;
|
|
1045
1174
|
logger.debug(`📄 Loaded config from ${configPath}`);
|
|
1046
1175
|
const engagement = config?.app?.engagement;
|
|
1047
1176
|
if (!engagement) {
|
|
@@ -1066,7 +1195,7 @@ async function scanForInstrumentedEvents(projectRoot, scanPaths) {
|
|
|
1066
1195
|
const sendViewPagePattern = /sendViewPageEvent\s*\(/g;
|
|
1067
1196
|
const createEventPattern = /createEvent\s*\(\s*['"]([^'"]+)['"]/g;
|
|
1068
1197
|
for (const scanPath of scanPaths) {
|
|
1069
|
-
const files = await glob(join(resolve(projectRoot, scanPath), "**/*.{ts,tsx}"), { ignore: [
|
|
1198
|
+
const files = await glob(join(resolve$1(projectRoot, scanPath), "**/*.{ts,tsx}"), { ignore: [
|
|
1070
1199
|
"**/*.test.ts",
|
|
1071
1200
|
"**/*.test.tsx",
|
|
1072
1201
|
"**/*.spec.ts",
|
|
@@ -1218,17 +1347,17 @@ const buildMiddlewareRegistryPlugin = () => {
|
|
|
1218
1347
|
configResolved(config) {
|
|
1219
1348
|
resolvedConfig = config;
|
|
1220
1349
|
const rr = config.__reactRouterPluginContext?.reactRouterConfig ?? {};
|
|
1221
|
-
buildDirectory = rr.buildDirectory ?? resolve(config.root, "build");
|
|
1350
|
+
buildDirectory = rr.buildDirectory ?? resolve$1(config.root, "build");
|
|
1222
1351
|
appDirectory = rr.appDirectory ?? "src";
|
|
1223
1352
|
},
|
|
1224
1353
|
buildApp: {
|
|
1225
1354
|
order: "post",
|
|
1226
1355
|
handler: async () => {
|
|
1227
1356
|
const projectRoot = resolvedConfig.root;
|
|
1228
|
-
const middlewareRegistryPath = resolve(projectRoot, appDirectory, SERVER_OUT_SUBDIR, MIDDLEWARE_REGISTRY_SOURCE_FILE);
|
|
1357
|
+
const middlewareRegistryPath = resolve$1(projectRoot, appDirectory, SERVER_OUT_SUBDIR, MIDDLEWARE_REGISTRY_SOURCE_FILE);
|
|
1229
1358
|
if (!existsSync(middlewareRegistryPath)) return;
|
|
1230
1359
|
const { build } = await import("tsdown");
|
|
1231
|
-
const serverOutDir = resolve(projectRoot, buildDirectory, SERVER_OUT_SUBDIR);
|
|
1360
|
+
const serverOutDir = resolve$1(projectRoot, buildDirectory, SERVER_OUT_SUBDIR);
|
|
1232
1361
|
await build({
|
|
1233
1362
|
cwd: projectRoot,
|
|
1234
1363
|
entry: { [MIDDLEWARE_REGISTRY_SOURCE_FILE.replace(/\.ts$/, "")]: middlewareRegistryPath },
|
|
@@ -1308,7 +1437,7 @@ export * from ${importPath};
|
|
|
1308
1437
|
export default _composed.default;
|
|
1309
1438
|
export const handleDataRequest = _composed.handleDataRequest;
|
|
1310
1439
|
export const handleError = _composed.handleError;
|
|
1311
|
-
export const
|
|
1440
|
+
export const instrumentations = _composed.instrumentations;
|
|
1312
1441
|
export const streamTimeout = _composed.streamTimeout;
|
|
1313
1442
|
`.trim();
|
|
1314
1443
|
}
|
|
@@ -1453,7 +1582,7 @@ const workspacePlugin = () => {
|
|
|
1453
1582
|
|
|
1454
1583
|
//#endregion
|
|
1455
1584
|
//#region src/plugins/componentLoaders.ts
|
|
1456
|
-
const traverse
|
|
1585
|
+
const traverse = traverseModule.default || traverseModule;
|
|
1457
1586
|
const generate$1 = generate.default || generate;
|
|
1458
1587
|
/**
|
|
1459
1588
|
* Names of exports to strip per environment.
|
|
@@ -1483,7 +1612,7 @@ function hasExportCandidate(code, names) {
|
|
|
1483
1612
|
*/
|
|
1484
1613
|
function hasComponentDecorator(ast) {
|
|
1485
1614
|
let found = false;
|
|
1486
|
-
traverse
|
|
1615
|
+
traverse(ast, { ClassDeclaration(path$2) {
|
|
1487
1616
|
const decorators = path$2.node.decorators;
|
|
1488
1617
|
if (!decorators) return;
|
|
1489
1618
|
for (const decorator of decorators) if (decorator.expression.type === "CallExpression" && isIdentifier(decorator.expression.callee) && decorator.expression.callee.name === "Component") {
|
|
@@ -1530,7 +1659,7 @@ function stripExports(code, exportsToStrip, preParsedAst) {
|
|
|
1530
1659
|
let changed = false;
|
|
1531
1660
|
const previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);
|
|
1532
1661
|
const removedExportLocalNames = /* @__PURE__ */ new Set();
|
|
1533
|
-
traverse
|
|
1662
|
+
traverse(ast, { ExportNamedDeclaration(path$2) {
|
|
1534
1663
|
const { declaration, specifiers } = path$2.node;
|
|
1535
1664
|
if (declaration && isVariableDeclaration(declaration)) {
|
|
1536
1665
|
const remaining = declaration.declarations.filter((decl) => {
|
|
@@ -1588,7 +1717,7 @@ function stripExports(code, exportsToStrip, preParsedAst) {
|
|
|
1588
1717
|
}
|
|
1589
1718
|
}
|
|
1590
1719
|
} });
|
|
1591
|
-
if (changed) traverse
|
|
1720
|
+
if (changed) traverse(ast, { ExpressionStatement(path$2) {
|
|
1592
1721
|
if (!path$2.parentPath?.isProgram()) return;
|
|
1593
1722
|
if (path$2.node.expression.type === "AssignmentExpression") {
|
|
1594
1723
|
const left = path$2.node.expression.left;
|
|
@@ -1907,93 +2036,19 @@ function storefrontNextTargets(config = {}) {
|
|
|
1907
2036
|
|
|
1908
2037
|
//#endregion
|
|
1909
2038
|
//#region src/plugins/uiTargetDevMode.ts
|
|
1910
|
-
const traverse = traverseModule.default || traverseModule;
|
|
1911
|
-
const UI_TARGET_JSX_RE = /<UITarget[\s/>]/;
|
|
1912
2039
|
/**
|
|
1913
|
-
*
|
|
2040
|
+
* @deprecated This plugin has been removed. It was an internal-only debugging aid
|
|
2041
|
+
* that could cause build failures when extensions were trimmed. The export is kept
|
|
2042
|
+
* as a no-op for backward compatibility and will be removed in v2.
|
|
1914
2043
|
*
|
|
1915
|
-
*
|
|
1916
|
-
*
|
|
2044
|
+
* **Migration:** Remove any `uiTargetDevMode()` plugin from your `vite.config.ts`,
|
|
2045
|
+
* delete `vite-plugins/ui-target-dev-mode.ts` if it exists, and remove the plugin
|
|
2046
|
+
* from the Vite plugins array.
|
|
1917
2047
|
*
|
|
1918
|
-
* @
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
* </UITarget>
|
|
1923
|
-
*
|
|
1924
|
-
* // Transformed in DEV mode:
|
|
1925
|
-
* <UITargetDevMarker
|
|
1926
|
-
* targetId="pdp.loyalty.badge"
|
|
1927
|
-
* __file__="/src/components/product.tsx"
|
|
1928
|
-
* __hasChildren__={true}
|
|
1929
|
-
* >
|
|
1930
|
-
* <Widget />
|
|
1931
|
-
* </UITargetDevMarker>
|
|
1932
|
-
*/
|
|
1933
|
-
function uiTargetDevModePlugin(config = {}) {
|
|
1934
|
-
if (process.env.NODE_ENV === "production") return { name: "storefront-next:ui-target-dev-mode-noop" };
|
|
1935
|
-
if (!(config.enabled ?? process.env.VITE_UI_TARGET_DEV_MODE === "true")) return { name: "storefront-next:ui-target-dev-mode-disabled" };
|
|
1936
|
-
logger.info("🎯 UITarget Dev Mode enabled");
|
|
1937
|
-
if (config.filterCategory) logger.info(` Filtering to category: ${config.filterCategory} (build-time)`);
|
|
1938
|
-
return {
|
|
1939
|
-
name: "storefront-next:ui-target-dev-mode",
|
|
1940
|
-
enforce: "pre",
|
|
1941
|
-
transform(code, id) {
|
|
1942
|
-
if (!id.match(/\.(tsx|jsx)$/)) return null;
|
|
1943
|
-
if (id.includes("node_modules")) return null;
|
|
1944
|
-
if (id.includes("/targets/ui-target.tsx")) return null;
|
|
1945
|
-
if (!UI_TARGET_JSX_RE.test(code) || !code.includes("from '@/targets/ui-target'")) return null;
|
|
1946
|
-
try {
|
|
1947
|
-
const ast = parse(code, {
|
|
1948
|
-
sourceType: "module",
|
|
1949
|
-
plugins: [
|
|
1950
|
-
"typescript",
|
|
1951
|
-
"jsx",
|
|
1952
|
-
"decorators-legacy"
|
|
1953
|
-
]
|
|
1954
|
-
});
|
|
1955
|
-
let hasTransforms = false;
|
|
1956
|
-
const targetIds = [];
|
|
1957
|
-
traverse(ast, { JSXElement(path$2) {
|
|
1958
|
-
const openingElement = path$2.node.openingElement;
|
|
1959
|
-
if (!isJSXIdentifier(openingElement.name) || openingElement.name.name !== "UITarget") return;
|
|
1960
|
-
const targetIdAttr = openingElement.attributes.find((attr) => isJSXAttribute(attr) && isJSXIdentifier(attr.name) && attr.name.name === "targetId");
|
|
1961
|
-
if (!targetIdAttr) {
|
|
1962
|
-
logger.warn(`UITarget without targetId in ${id}`);
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const targetId = isStringLiteral(targetIdAttr.value) ? targetIdAttr.value.value : null;
|
|
1966
|
-
if (!targetId) {
|
|
1967
|
-
logger.warn(`UITarget with non-string targetId in ${id}`);
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
if (config.filterCategory && !targetId.startsWith(`${config.filterCategory}.`)) return;
|
|
1971
|
-
const hasChildren = path$2.node.children.length > 0;
|
|
1972
|
-
openingElement.name = jsxIdentifier("UITargetDevMarker");
|
|
1973
|
-
if (path$2.node.closingElement) path$2.node.closingElement.name = jsxIdentifier("UITargetDevMarker");
|
|
1974
|
-
const hint = config.hintMap?.[targetId];
|
|
1975
|
-
openingElement.attributes.push(jsxAttribute(jsxIdentifier("__file__"), stringLiteral(id)), jsxAttribute(jsxIdentifier("__hasChildren__"), jsxExpressionContainer(booleanLiteral(hasChildren))), ...hint ? [jsxAttribute(jsxIdentifier("__hint__"), stringLiteral(hint))] : []);
|
|
1976
|
-
hasTransforms = true;
|
|
1977
|
-
targetIds.push(targetId);
|
|
1978
|
-
} });
|
|
1979
|
-
if (!hasTransforms) return null;
|
|
1980
|
-
const devMarkerImport = importDeclaration([importSpecifier(identifier("UITargetDevMarker"), identifier("UITargetDevMarker"))], stringLiteral("@/lib/ui-target-dev-mode/marker"));
|
|
1981
|
-
ast.program.body.unshift(devMarkerImport);
|
|
1982
|
-
const output = generate(ast, {
|
|
1983
|
-
retainLines: true,
|
|
1984
|
-
compact: false
|
|
1985
|
-
}, code);
|
|
1986
|
-
logger.debug(`Transformed ${targetIds.length} targets in ${id.split("/").pop()}`);
|
|
1987
|
-
return {
|
|
1988
|
-
code: output.code,
|
|
1989
|
-
map: output.map
|
|
1990
|
-
};
|
|
1991
|
-
} catch (error) {
|
|
1992
|
-
logger.error(`Failed to transform UITarget in ${id}:`, error);
|
|
1993
|
-
return null;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
};
|
|
2048
|
+
* @returns A no-op Vite plugin
|
|
2049
|
+
*/
|
|
2050
|
+
function uiTargetDevModePlugin(_config) {
|
|
2051
|
+
return { name: "storefront-next:ui-target-dev-mode-deprecated-noop" };
|
|
1997
2052
|
}
|
|
1998
2053
|
|
|
1999
2054
|
//#endregion
|
|
@@ -2098,6 +2153,82 @@ function escapeRegExp(str) {
|
|
|
2098
2153
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2099
2154
|
}
|
|
2100
2155
|
/**
|
|
2156
|
+
* Resolve a captured prefix segment to its canonical-cased alias, case-insensitively.
|
|
2157
|
+
* Returns the matching entry from `aliases` (preserving the catalog's casing) so the value
|
|
2158
|
+
* forwarded to SFRA is always the expected canonical form, or `undefined` when the segment
|
|
2159
|
+
* isn't a known site/locale identifier.
|
|
2160
|
+
*/
|
|
2161
|
+
function matchAlias(segment, aliases) {
|
|
2162
|
+
if (segment === void 0) return void 0;
|
|
2163
|
+
const lower = segment.toLowerCase();
|
|
2164
|
+
return aliases.find((alias) => alias.toLowerCase() === lower);
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Rewrite an incoming proxy pathname to the SFRA `/s/{siteId}/{locale}/…` shape.
|
|
2168
|
+
*
|
|
2169
|
+
* This is the OOTB path transform. It is `url.prefix`-aware: when `urlPrefix` is set
|
|
2170
|
+
* it strips the configured prefix to recover the bare functional path AND reuses the
|
|
2171
|
+
* site/locale the path already carried — so a prefixed request like `/uk/cart` becomes
|
|
2172
|
+
* `/s/{siteId}/uk/cart` rather than the double-stacked `/s/{siteId}/{locale}/uk/cart`.
|
|
2173
|
+
*
|
|
2174
|
+
* It handles every standard prefix shape uniformly:
|
|
2175
|
+
* - no prefix (`''`/`/`): `/cart` → `/s/{site}/{locale}/cart`
|
|
2176
|
+
* - `/:localeId`: `/uk/cart` → `/s/{site}/uk/cart`
|
|
2177
|
+
* - `/:siteId/:localeId`: `/global/uk/cart` → `/s/global/uk/cart`
|
|
2178
|
+
*
|
|
2179
|
+
* Disambiguation: a captured prefix segment is only treated as a site/locale when it is
|
|
2180
|
+
* a known identifier (`siteAliases`/`localeAliases`). This stops a bare path like `/cart`
|
|
2181
|
+
* under a `/:localeId` prefix — which is structurally indistinguishable from a locale
|
|
2182
|
+
* home `/uk` — from being mis-stripped (here `cart` isn't a known locale, so the whole
|
|
2183
|
+
* path is treated as the bare functional path and decorated with the `defaultSiteId`/
|
|
2184
|
+
* `defaultLocale` fallbacks). When a prefix segment IS a known site/locale, that value is
|
|
2185
|
+
* passed straight through as the SFRA path segment; SFRA resolves both the BM site-path
|
|
2186
|
+
* alias (`uk`) and the canonical id (`en-GB`).
|
|
2187
|
+
*
|
|
2188
|
+
* Paths already in SFCC shape (`/s/…`, `/on/demandware.…`) are returned unchanged — the
|
|
2189
|
+
* caller is expected to skip them, but this stays defensive.
|
|
2190
|
+
*
|
|
2191
|
+
* Pure function (query string handled by the caller) so it's unit-testable in isolation.
|
|
2192
|
+
*
|
|
2193
|
+
* @param pathname - Incoming pathname on the proxy (no query string).
|
|
2194
|
+
* @param opts.defaultSiteId - Site to use when the path doesn't carry one.
|
|
2195
|
+
* @param opts.defaultLocale - Locale to use when the path doesn't carry one.
|
|
2196
|
+
* @param opts.urlPrefix - The storefront's `url.prefix` (e.g. `'/:siteId/:localeId'`).
|
|
2197
|
+
* @param opts.localeAliases - Known locale ids + aliases.
|
|
2198
|
+
* @param opts.siteAliases - Known site ids + aliases.
|
|
2199
|
+
*/
|
|
2200
|
+
function rewriteToSfraPath(pathname, opts) {
|
|
2201
|
+
const { defaultSiteId, defaultLocale, urlPrefix, localeAliases = [], siteAliases = [] } = opts;
|
|
2202
|
+
if (pathname.startsWith("/s/") || pathname.startsWith("/on/demandware.")) return pathname;
|
|
2203
|
+
let site = defaultSiteId;
|
|
2204
|
+
let locale = defaultLocale;
|
|
2205
|
+
let barePath = pathname;
|
|
2206
|
+
let localeFromPath = false;
|
|
2207
|
+
if (urlPrefix && urlPrefix !== "/") {
|
|
2208
|
+
const captured = extractPrefixParamValues({
|
|
2209
|
+
pathname,
|
|
2210
|
+
prefix: urlPrefix
|
|
2211
|
+
});
|
|
2212
|
+
const matchedSite = "siteId" in captured ? matchAlias(captured.siteId, siteAliases) : void 0;
|
|
2213
|
+
const matchedLocale = "localeId" in captured ? matchAlias(captured.localeId, localeAliases) : void 0;
|
|
2214
|
+
const siteOk = !("siteId" in captured) || matchedSite !== void 0;
|
|
2215
|
+
const localeOk = !("localeId" in captured) || matchedLocale !== void 0;
|
|
2216
|
+
if (Object.keys(captured).length > 0 && siteOk && localeOk) {
|
|
2217
|
+
if (matchedSite) site = matchedSite;
|
|
2218
|
+
if (matchedLocale) {
|
|
2219
|
+
locale = matchedLocale;
|
|
2220
|
+
localeFromPath = true;
|
|
2221
|
+
}
|
|
2222
|
+
barePath = stripPathPrefix({
|
|
2223
|
+
pathname,
|
|
2224
|
+
prefix: urlPrefix
|
|
2225
|
+
}) || "/";
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (barePath === "/") return localeFromPath ? `/s/${site}/${locale}/` : `/s/${site}`;
|
|
2229
|
+
return `/s/${site}/${locale}${barePath}`;
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2101
2232
|
* Rewrite an SFCC Location header so the user stays on the proxy origin AND so
|
|
2102
2233
|
* any query params from the original request survive an SFCC redirect.
|
|
2103
2234
|
*
|
|
@@ -2173,7 +2304,8 @@ function hybridProxyPlugin(options) {
|
|
|
2173
2304
|
logger.debug(`Hybrid proxy routing rules: ${options.routingRules.slice(0, 100)}...`);
|
|
2174
2305
|
const locale = options.locale || "default";
|
|
2175
2306
|
const defaultSiteId = options.defaultSiteId;
|
|
2176
|
-
|
|
2307
|
+
const { urlPrefix, localeAliases, siteAliases, rewritePath } = options;
|
|
2308
|
+
logger.debug(urlPrefix ? `Hybrid proxy path transformation: url.prefix=${urlPrefix} → /s/${defaultSiteId}/{locale}/{bare path}` : `Hybrid proxy path transformation: / → /s/${defaultSiteId}, /path → /s/${defaultSiteId}/${locale}/path`);
|
|
2177
2309
|
const targetOriginPattern = new RegExp(escapeRegExp(options.targetOrigin), "g");
|
|
2178
2310
|
const proxy = httpProxy.createProxyServer({
|
|
2179
2311
|
changeOrigin: true,
|
|
@@ -2185,13 +2317,13 @@ function hybridProxyPlugin(options) {
|
|
|
2185
2317
|
const pathname = url.pathname;
|
|
2186
2318
|
if (!pathname.startsWith("/s/") && !pathname.startsWith("/on/demandware.")) {
|
|
2187
2319
|
const originalPath = proxyReq.path;
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2320
|
+
proxyReq.path = `${rewritePath?.(pathname) ?? null ?? rewriteToSfraPath(pathname, {
|
|
2321
|
+
defaultSiteId,
|
|
2322
|
+
defaultLocale: locale,
|
|
2323
|
+
urlPrefix,
|
|
2324
|
+
localeAliases,
|
|
2325
|
+
siteAliases
|
|
2326
|
+
})}${url.search}`;
|
|
2195
2327
|
logger.debug(`Hybrid proxy path rewrite: ${originalPath} → ${proxyReq.path}`);
|
|
2196
2328
|
}
|
|
2197
2329
|
});
|