@salesforce/storefront-next-dev 1.2.0 → 1.3.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/index.js CHANGED
@@ -1,17 +1,18 @@
1
- import path, { resolve } from "node:path";
1
+ import path, { relative, resolve } from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import chalk from "chalk";
4
4
  import { createRequire } from "module";
5
- import path$1, { basename, dirname, join, relative, resolve as resolve$1 } from "path";
5
+ import path$1, { basename, dirname, join, relative as relative$1, resolve as resolve$1 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { parse } from "@babel/parser";
8
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";
9
9
  import { generate } from "@babel/generator";
10
10
  import _traverse from "@babel/traverse";
11
- import { existsSync, readFileSync, writeFileSync } from "fs";
11
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
12
12
  import { glob } from "glob";
13
13
  import { Node, Project, ts } from "ts-morph";
14
14
  import { spawnSync } from "child_process";
15
+ import { brotliCompressSync, constants, gzipSync } from "node:zlib";
15
16
  import fs$1, { existsSync as existsSync$1, readFileSync as readFileSync$1 } from "node:fs";
16
17
  import { deadCodeElimination, findReferencedIdentifiers } from "babel-dead-code-elimination";
17
18
  import httpProxy from "http-proxy";
@@ -872,6 +873,165 @@ function resolveBiomeBin(filePath) {
872
873
  return null;
873
874
  }
874
875
 
876
+ //#endregion
877
+ //#region src/plugins/pageDesignerPreloadManifest.ts
878
+ const PAGE_DESIGNER_PRELOAD_MANIFEST_ID = "virtual:storefront-next/page-designer-preload-manifest";
879
+ const RESOLVED_PAGE_DESIGNER_PRELOAD_MANIFEST_ID = `\0${PAGE_DESIGNER_PRELOAD_MANIFEST_ID}`;
880
+ function correlateComponentRecords(source, viteManifest, bundle, projectRoot) {
881
+ const directMatches = Object.entries(viteManifest).filter(([key, record]) => normalizeSourceId(projectRoot, key) === source || normalizeSourceId(projectRoot, record.src ?? "") === source);
882
+ if (directMatches.length > 0) return directMatches;
883
+ const containingFiles = new Set(Object.values(bundle).filter((output) => output.type === "chunk" && Object.keys(output.modules ?? {}).some((moduleId) => normalizeSourceId(projectRoot, moduleId) === source)).map((chunk) => chunk.fileName));
884
+ return Object.entries(viteManifest).filter(([, record]) => containingFiles.has(record.file)).map(([key, record]) => [key, {
885
+ ...record,
886
+ isDynamicEntry: !record.isEntry
887
+ }]);
888
+ }
889
+ function assertViteManifestStructure(value) {
890
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid standard Vite manifest: expected a top-level object");
891
+ for (const [key, record] of Object.entries(value)) {
892
+ if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error(`Invalid standard Vite manifest: record "${key}" must be an object`);
893
+ if (!("file" in record) || typeof record.file !== "string" || record.file.length === 0) throw new Error(`Invalid standard Vite manifest: record "${key}" must have a non-empty string "file"`);
894
+ }
895
+ }
896
+ function integerInRange(value, path$2, min, max, fallback) {
897
+ if (value === void 0) return fallback;
898
+ if (!Number.isInteger(value) || value < min || value > max) throw new Error(`${path$2} must be an integer between ${min} and ${max}`);
899
+ return value;
900
+ }
901
+ function normalizePageDesignerPreloadManifestConfig(config) {
902
+ const options = typeof config === "object" ? config : {};
903
+ return {
904
+ path: options.path ?? "page-designer-preload-manifest.json",
905
+ requiredTypeIds: [...new Set(options.requiredTypeIds ?? [])].sort(),
906
+ compression: {
907
+ brotli: { quality: integerInRange(options.compression?.brotli?.quality, "preloadManifest.compression.brotli.quality", 0, 11, 9) },
908
+ gzip: { level: integerInRange(options.compression?.gzip?.level, "preloadManifest.compression.gzip.level", 0, 9, 6) }
909
+ }
910
+ };
911
+ }
912
+ function createEmptyPageDesignerPreloadManifest(config) {
913
+ return {
914
+ version: 1,
915
+ compression: config.compression,
916
+ resources: [],
917
+ components: {}
918
+ };
919
+ }
920
+ function normalizeSourceId(projectRoot, id) {
921
+ const withoutQuery = id.split("?", 1)[0].replace(/\\/g, "/");
922
+ const normalizedRoot = projectRoot.replace(/\\/g, "/").replace(/\/$/, "");
923
+ if (withoutQuery === normalizedRoot) return "";
924
+ if (withoutQuery.startsWith(`${normalizedRoot}/`)) return withoutQuery.slice(normalizedRoot.length + 1);
925
+ return withoutQuery.startsWith("/") ? relative(projectRoot, withoutQuery).replace(/\\/g, "/") : withoutQuery;
926
+ }
927
+ function outputBytes(output) {
928
+ if (output.type === "chunk") return Buffer.from(output.code);
929
+ return Buffer.isBuffer(output.source) ? output.source : Buffer.from(output.source);
930
+ }
931
+ function describeResource(file, kind, bundle, config) {
932
+ const output = bundle[file];
933
+ if (!output) throw new Error(`Vite manifest references missing emitted resource "${file}"`);
934
+ const bytes = outputBytes(output);
935
+ return {
936
+ file,
937
+ kind,
938
+ bytes: bytes.byteLength,
939
+ estimatedBrotliBytes: brotliCompressSync(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: config.compression.brotli.quality } }).byteLength,
940
+ estimatedGzipBytes: gzipSync(bytes, { level: config.compression.gzip.level }).byteLength
941
+ };
942
+ }
943
+ function assertUniqueTypeIds(components) {
944
+ const seen = /* @__PURE__ */ new Map();
945
+ for (const component of components) {
946
+ const previous = seen.get(component.id);
947
+ if (previous) throw new Error(`Duplicate Page Designer typeId "${component.id}" found in "${previous}" and "${component.filePath}"`);
948
+ seen.set(component.id, component.filePath);
949
+ }
950
+ }
951
+ function buildPageDesignerPreloadManifest(components, viteManifest, bundle, projectRoot, config, warn) {
952
+ assertUniqueTypeIds(components);
953
+ const required = new Set(config.requiredTypeIds);
954
+ const componentFiles = {};
955
+ const resourceKinds = /* @__PURE__ */ new Map();
956
+ const addResource = (file, kind) => {
957
+ const existingKind = resourceKinds.get(file);
958
+ if (existingKind && existingKind !== kind) throw new Error(`Emitted resource "${file}" was classified as both ${existingKind} and ${kind}`);
959
+ resourceKinds.set(file, kind);
960
+ };
961
+ for (const component of [...components].sort((a, b) => a.id.localeCompare(b.id))) {
962
+ const source = normalizeSourceId(projectRoot, component.filePath);
963
+ const matches = correlateComponentRecords(source, viteManifest, bundle, projectRoot);
964
+ if (matches.length > 1) throw new Error(`Ambiguous Vite manifest correlation for Page Designer typeId "${component.id}" (${source}): ${matches.map(([key]) => key).join(", ")}`);
965
+ if (matches.length === 0) {
966
+ const message = `No Vite manifest record found for Page Designer typeId "${component.id}" (${source})`;
967
+ if (required.has(component.id)) throw new Error(`${message}; it is listed in preloadManifest.requiredTypeIds`);
968
+ warn(message);
969
+ continue;
970
+ }
971
+ const [, entryRecord] = matches[0];
972
+ if (!entryRecord.isDynamicEntry) {
973
+ componentFiles[component.id] = {};
974
+ continue;
975
+ }
976
+ const visited = /* @__PURE__ */ new Set();
977
+ const scripts = /* @__PURE__ */ new Map();
978
+ const styles = /* @__PURE__ */ new Set();
979
+ const visit = (record, isComponentEntry) => {
980
+ if (visited.has(record.file)) return;
981
+ visited.add(record.file);
982
+ scripts.set(record.file, isComponentEntry ? "entry" : "dependency");
983
+ for (const importedKey of record.imports ?? []) {
984
+ const imported = viteManifest[importedKey];
985
+ if (!imported) throw new Error(`Vite manifest record "${record.file}" imports missing record "${importedKey}"`);
986
+ visit(imported, false);
987
+ }
988
+ for (const css of record.css ?? []) styles.add(css);
989
+ };
990
+ visit(entryRecord, true);
991
+ const styleFiles = [...styles];
992
+ const entryFiles = [...scripts].filter(([, role]) => role === "entry").map(([file]) => file).sort();
993
+ const dependencyFiles = [...scripts].filter(([, role]) => role === "dependency").map(([file]) => file).sort();
994
+ for (const file of styleFiles) addResource(file, "style");
995
+ for (const file of [...entryFiles, ...dependencyFiles]) addResource(file, "module");
996
+ componentFiles[component.id] = {
997
+ ...styleFiles.length > 0 ? { styles: styleFiles } : {},
998
+ entries: entryFiles,
999
+ ...dependencyFiles.length > 0 ? { dependencies: dependencyFiles } : {}
1000
+ };
1001
+ }
1002
+ for (const typeId of required) if (!(typeId in componentFiles)) throw new Error(`Required Page Designer typeId "${typeId}" was not discovered by the static registry scan`);
1003
+ const resourceFiles = [...resourceKinds.keys()].sort();
1004
+ const resourceIndices = Object.fromEntries(resourceFiles.map((file, index) => [file, index]));
1005
+ const toIndices = (files) => files.map((file) => resourceIndices[file]);
1006
+ return {
1007
+ version: 1,
1008
+ compression: config.compression,
1009
+ resources: resourceFiles.map((file) => describeResource(file, resourceKinds.get(file), bundle, config)),
1010
+ components: Object.fromEntries(Object.entries(componentFiles).map(([typeId, files]) => [typeId, {
1011
+ ...files.styles ? { styles: toIndices(files.styles) } : {},
1012
+ ...files.entries ? { entries: toIndices(files.entries) } : {},
1013
+ ...files.dependencies ? { dependencies: toIndices(files.dependencies) } : {}
1014
+ }]))
1015
+ };
1016
+ }
1017
+ function parseViteManifestAsset(bundle) {
1018
+ const candidates = Object.values(bundle).filter((output) => output.type === "asset" && /(^|\/)manifest\.json$/.test(output.fileName));
1019
+ if (candidates.length !== 1) throw new Error(`Expected exactly one standard Vite manifest asset, found ${candidates.length}`);
1020
+ let parsed;
1021
+ try {
1022
+ parsed = JSON.parse(String(candidates[0].source));
1023
+ } catch (error) {
1024
+ throw new Error(`Could not parse the standard Vite manifest: ${error.message}`);
1025
+ }
1026
+ assertViteManifestStructure(parsed);
1027
+ return parsed;
1028
+ }
1029
+ function validateEmbeddedPageDesignerPreloadManifest(value) {
1030
+ if (!value || typeof value !== "object" || value.version !== 1) throw new Error("Page Designer preload manifest is missing, malformed, or uses an unsupported version");
1031
+ const candidate = value;
1032
+ if (!candidate.compression || !Array.isArray(candidate.resources) || !candidate.components || typeof candidate.components !== "object") throw new Error("Page Designer preload manifest is incomplete");
1033
+ }
1034
+
875
1035
  //#endregion
876
1036
  //#region src/plugins/staticRegistry.ts
877
1037
  const DEFAULT_COMPONENT_GROUP = "storefrontnext_base";
@@ -958,7 +1118,7 @@ async function scanComponents(project, projectRoot, componentPath, registryPath)
958
1118
  for (const decorator of decorators) if (decorator.getName() === "Component") {
959
1119
  const componentInfo = extractComponentInfo(decorator);
960
1120
  if (componentInfo) {
961
- let relativePath = relative(registryDir, filePath).replace(/\\/g, "/").replace(/\.(ts|tsx)$/, "");
1121
+ let relativePath = relative$1(registryDir, filePath).replace(/\\/g, "/").replace(/\.(ts|tsx)$/, "");
962
1122
  if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
963
1123
  const hasLoaderExport = hasNamedExport(sourceFile, "loader");
964
1124
  const hasClientLoaderExport = hasNamedExport(sourceFile, "clientLoader");
@@ -1000,6 +1160,14 @@ function generateRegistryCode(components, registryIdentifier = "registry") {
1000
1160
  export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
1001
1161
  // No components found with @Component decorators
1002
1162
  }
1163
+
1164
+ /** Load selected component modules and register their concrete exports before SSR. */
1165
+ export async function loadAndRegisterRegistryComponents(
1166
+ typeIds: Iterable<string>,
1167
+ targetRegistry = ${registryIdentifier}
1168
+ ): Promise<void> {
1169
+ await Promise.all([...new Set(typeIds)].map((id) => targetRegistry.loadAndRegister(id)));
1170
+ }
1003
1171
  `;
1004
1172
  const registrations = sorted.map(({ id, relativePath, hasLoader, hasClientLoader, hasFallback }) => {
1005
1173
  if (hasLoader || hasClientLoader || hasFallback) {
@@ -1022,6 +1190,18 @@ export function initializeRegistry(targetRegistry = ${registryIdentifier}): void
1022
1190
  export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
1023
1191
  ${registrations}
1024
1192
  }
1193
+
1194
+ /**
1195
+ * Load selected component modules and register their concrete exports before SSR.
1196
+ * This keeps the component boundary out of the initial Suspense shell while
1197
+ * preserving nested data-loading Suspense boundaries and their fallbacks.
1198
+ */
1199
+ export async function loadAndRegisterRegistryComponents(
1200
+ typeIds: Iterable<string>,
1201
+ targetRegistry = ${registryIdentifier}
1202
+ ): Promise<void> {
1203
+ await Promise.all([...new Set(typeIds)].map((id) => targetRegistry.loadAndRegister(id)));
1204
+ }
1025
1205
  `;
1026
1206
  }
1027
1207
  /**
@@ -1089,11 +1269,16 @@ export const registry = new ComponentRegistry();
1089
1269
  * })
1090
1270
  */
1091
1271
  const staticRegistryPlugin = (config = {}) => {
1092
- const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true } = config;
1272
+ const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true, preloadManifest = false } = config;
1093
1273
  let projectRoot;
1274
+ let isProductionBuild = false;
1275
+ let components = [];
1276
+ let embeddedManifest;
1277
+ let intermediateManifestPath;
1278
+ const normalizedPreloadConfig = preloadManifest ? normalizePageDesignerPreloadManifestConfig(preloadManifest) : void 0;
1094
1279
  const runRegistryGeneration = async () => {
1095
1280
  logger.debug("🚀 Starting static registry generation...");
1096
- const components = await scanComponents(new Project({ compilerOptions: {
1281
+ components = await scanComponents(new Project({ compilerOptions: {
1097
1282
  target: ts.ScriptTarget.Latest,
1098
1283
  module: ts.ModuleKind.ESNext,
1099
1284
  jsx: ts.JsxEmit.ReactJSX,
@@ -1113,9 +1298,25 @@ const staticRegistryPlugin = (config = {}) => {
1113
1298
  };
1114
1299
  return {
1115
1300
  name: "storefrontnext:static-registry",
1301
+ config(_userConfig, env) {
1302
+ isProductionBuild = env.command === "build" && env.mode === "production";
1303
+ },
1304
+ configEnvironment(name) {
1305
+ if (normalizedPreloadConfig && name === "client") return { build: { manifest: true } };
1306
+ },
1116
1307
  configResolved(resolvedConfig) {
1117
1308
  projectRoot = resolvedConfig.root;
1118
1309
  },
1310
+ resolveId(id) {
1311
+ if (normalizedPreloadConfig && id === PAGE_DESIGNER_PRELOAD_MANIFEST_ID) return RESOLVED_PAGE_DESIGNER_PRELOAD_MANIFEST_ID;
1312
+ },
1313
+ load(id) {
1314
+ if (!normalizedPreloadConfig || id !== RESOLVED_PAGE_DESIGNER_PRELOAD_MANIFEST_ID) return;
1315
+ if (!isProductionBuild) return `export default ${JSON.stringify(createEmptyPageDesignerPreloadManifest(normalizedPreloadConfig))};`;
1316
+ if (this.environment?.name === "client") return `export default ${JSON.stringify(createEmptyPageDesignerPreloadManifest(normalizedPreloadConfig))};`;
1317
+ validateEmbeddedPageDesignerPreloadManifest(embeddedManifest);
1318
+ return `export default ${JSON.stringify(embeddedManifest)};`;
1319
+ },
1119
1320
  async buildStart() {
1120
1321
  try {
1121
1322
  await runRegistryGeneration();
@@ -1125,6 +1326,23 @@ const staticRegistryPlugin = (config = {}) => {
1125
1326
  logger.warn("⚠️ Continuing build without static registry...");
1126
1327
  }
1127
1328
  },
1329
+ writeBundle(options, bundle) {
1330
+ if (!normalizedPreloadConfig) return;
1331
+ if (this.environment?.name !== "client") return;
1332
+ const viteManifest = parseViteManifestAsset(bundle);
1333
+ embeddedManifest = buildPageDesignerPreloadManifest(components, viteManifest, bundle, projectRoot, normalizedPreloadConfig, (message) => this.warn(message));
1334
+ validateEmbeddedPageDesignerPreloadManifest(embeddedManifest);
1335
+ intermediateManifestPath = resolve$1(options.dir ?? resolve$1(projectRoot, "dist"), normalizedPreloadConfig.path);
1336
+ mkdirSync(dirname(intermediateManifestPath), { recursive: true });
1337
+ writeFileSync(intermediateManifestPath, `${JSON.stringify(embeddedManifest, null, 2)}\n`, "utf8");
1338
+ },
1339
+ buildApp: normalizedPreloadConfig ? {
1340
+ order: "post",
1341
+ handler() {
1342
+ if (intermediateManifestPath && existsSync(intermediateManifestPath)) unlinkSync(intermediateManifestPath);
1343
+ return Promise.resolve();
1344
+ }
1345
+ } : void 0,
1128
1346
  async handleHotUpdate({ file, server }) {
1129
1347
  const normalizedComponentPath = componentPath.replace(/\\/g, "/");
1130
1348
  const normalizedFile = file.replace(/\\/g, "/");
@@ -1546,10 +1764,10 @@ function platformEntryPlugin() {
1546
1764
  const appDir = appDirectory;
1547
1765
  const watcher = server.watcher;
1548
1766
  const checkEntryChange = (filePath) => {
1549
- const relative$1 = path.relative(appDir, filePath);
1550
- const basename$1 = path.basename(relative$1, path.extname(relative$1));
1551
- if (path.dirname(relative$1) !== "." || basename$1 !== "entry.server" && basename$1 !== "entry.client") return;
1552
- const ext = path.extname(relative$1);
1767
+ const relative$2 = path.relative(appDir, filePath);
1768
+ const basename$1 = path.basename(relative$2, path.extname(relative$2));
1769
+ if (path.dirname(relative$2) !== "." || basename$1 !== "entry.server" && basename$1 !== "entry.client") return;
1770
+ const ext = path.extname(relative$2);
1553
1771
  if (!ENTRY_EXTENSIONS.includes(ext)) return;
1554
1772
  const nowHasServer = findUserEntry(appDir, "entry.server") !== void 0;
1555
1773
  const nowHasClient = findUserEntry(appDir, "entry.client") !== void 0;