@salesforce/storefront-next-dev 0.3.1-alpha.1 → 0.4.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +12 -12
  2. package/bin/run.js +19 -9
  3. package/dist/cartridge-services/index.js +9 -1
  4. package/dist/cartridge-services/index.js.map +1 -1
  5. package/dist/commands/config/inspect.js +191 -0
  6. package/dist/commands/create-bundle.js +1 -1
  7. package/dist/commands/create-storefront.js +3 -3
  8. package/dist/commands/dev.js +1 -3
  9. package/dist/commands/extensions/install.js +1 -1
  10. package/dist/commands/extensions/list.js +1 -1
  11. package/dist/commands/extensions/remove.js +1 -1
  12. package/dist/commands/generate-cartridge.js +1 -1
  13. package/dist/commands/locales/aggregate-extensions.js +181 -0
  14. package/dist/commands/prepare-local.js +1 -1
  15. package/dist/commands/preview.js +1 -3
  16. package/dist/commands/scapi/add.js +298 -0
  17. package/dist/commands/scapi/list.js +35 -0
  18. package/dist/commands/scapi/remove.js +56 -0
  19. package/dist/commands/validate-cartridge.js +1 -1
  20. package/dist/configs/react-router.config.js +2 -1
  21. package/dist/configs/react-router.config.js.map +1 -1
  22. package/dist/data-store/local-provider.d.ts +42 -0
  23. package/dist/data-store/local-provider.d.ts.map +1 -0
  24. package/dist/data-store/local-provider.js +66 -0
  25. package/dist/data-store/local-provider.js.map +1 -0
  26. package/dist/flags.js +5 -3
  27. package/dist/generate-cartridge.js +9 -1
  28. package/dist/generate-custom-clients.js +73 -0
  29. package/dist/hooks/init.js +29 -4
  30. package/dist/index.d.ts +46 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +260 -42
  33. package/dist/index.js.map +1 -1
  34. package/dist/logger.js +1 -1
  35. package/dist/mrt/ssr.mjs +51 -51
  36. package/dist/mrt/ssr.mjs.map +1 -1
  37. package/dist/mrt/streamingHandler.mjs +57 -57
  38. package/dist/mrt/streamingHandler.mjs.map +1 -1
  39. package/dist/schema-utils.js +64 -0
  40. package/dist/utils.js +3 -11
  41. package/package.json +19 -3
@@ -0,0 +1,191 @@
1
+ import { r as commonFlags } from "../../flags.js";
2
+ import { ux } from "@oclif/core";
3
+ import fs from "fs-extra";
4
+ import chalk from "chalk";
5
+ import { parseEnv } from "node:util";
6
+ import { join, resolve } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { MrtCommand } from "@salesforce/b2c-tooling-sdk/cli";
9
+ import { listEnvVars } from "@salesforce/b2c-tooling-sdk/operations/mrt";
10
+
11
+ //#region src/utils/objects.ts
12
+ /**
13
+ * Recursively flattens a nested object into dot-notation key-value pairs.
14
+ * Arrays and primitives are treated as leaf values (not further traversed).
15
+ *
16
+ * @param obj - The object to flatten
17
+ * @param prefix - Dot-notation prefix for nested keys
18
+ */
19
+ function flattenObject(obj, prefix = "") {
20
+ const entries = [];
21
+ for (const [k, v] of Object.entries(obj)) {
22
+ const key = prefix ? `${prefix}.${k}` : k;
23
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) entries.push(...flattenObject(v, key));
24
+ else entries.push({
25
+ key,
26
+ value: v
27
+ });
28
+ }
29
+ return entries;
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/commands/config/inspect-utils.ts
34
+ /**
35
+ * Converts a PUBLIC__-prefixed env key to dot-notation config path.
36
+ * Example: PUBLIC__app__commerce__api__clientId → app.commerce.api.clientId
37
+ */
38
+ function envKeyToConfigPath(envKey) {
39
+ return envKey.replace(/^PUBLIC__/, "").replace(/__/g, ".");
40
+ }
41
+ /**
42
+ * Converts a dot-notation config path to a PUBLIC__-prefixed env key.
43
+ * Example: app.commerce.api.clientId → PUBLIC__app__commerce__api__clientId
44
+ */
45
+ function configPathToEnvKey(configPath) {
46
+ return `PUBLIC__${configPath.replace(/\./g, "__")}`;
47
+ }
48
+ /**
49
+ * Determines the source of each flattened config path.
50
+ * Returns ".env" when there is a corresponding PUBLIC__-prefixed key in envKeys;
51
+ * returns "config" otherwise.
52
+ *
53
+ * @param flattenedKeys - Array of dot-notation config paths
54
+ * @param envKeys - Set of PUBLIC__-prefixed keys found in .env
55
+ */
56
+ function getValueSources(flattenedKeys, envKeys) {
57
+ const sources = /* @__PURE__ */ new Map();
58
+ for (const configPath of flattenedKeys) sources.set(configPath, envKeys.has(configPathToEnvKey(configPath)) ? ".env" : "config");
59
+ return sources;
60
+ }
61
+ /**
62
+ * Formats the full inspection output as a string.
63
+ */
64
+ function formatInspectOutput({ flatConfig, sources, localVars, mrtVars }) {
65
+ const lines = [];
66
+ const maxKeyLen = (keys) => keys.reduce((max, k) => Math.max(max, k.length), 0);
67
+ const envOverrides = flatConfig.filter((e) => sources.get(e.key) === ".env").sort((a, b) => a.key.localeCompare(b.key));
68
+ const totalCount = flatConfig.length;
69
+ if (totalCount > 0) lines.push(` Config: config.server.ts (${totalCount} values, ${envOverrides.length} overridden by .env)`);
70
+ else lines.push(chalk.dim(" (no config loaded)"));
71
+ lines.push("");
72
+ lines.push(chalk.bold("=== .env Overrides ==="));
73
+ lines.push(chalk.dim(" Config paths overridden by PUBLIC__ env vars in .env."));
74
+ lines.push("");
75
+ if (envOverrides.length === 0) lines.push(chalk.dim(" (no .env overrides)"));
76
+ else {
77
+ const padLen = maxKeyLen(envOverrides.map((e) => e.key));
78
+ for (const { key, value } of envOverrides) {
79
+ const tag = mrtVars !== null && !mrtVars.has(configPathToEnvKey(key)) ? ` ${chalk.yellow("[local only]")}` : "";
80
+ lines.push(` ${key.padEnd(padLen)} = ${JSON.stringify(value)}${tag}`);
81
+ }
82
+ }
83
+ lines.push("");
84
+ if (mrtVars !== null) {
85
+ lines.push(chalk.bold("=== MRT Overrides ==="));
86
+ lines.push(chalk.dim(" Config paths overridden by PUBLIC__ env vars in MRT. Values are masked by MRT."));
87
+ lines.push("");
88
+ const mrtPublicEntries = [...mrtVars.entries()].filter(([key]) => key.startsWith("PUBLIC__")).map(([key, value]) => [
89
+ key,
90
+ value,
91
+ envKeyToConfigPath(key)
92
+ ]).sort(([, , configPath1], [, , configPath2]) => configPath1.localeCompare(configPath2));
93
+ if (mrtPublicEntries.length === 0) lines.push(chalk.dim(" (no MRT config overrides)"));
94
+ else {
95
+ const padLen = maxKeyLen(mrtPublicEntries.map(([, , configPath]) => configPath));
96
+ for (const [key, value, configPath] of mrtPublicEntries) {
97
+ const tag = !localVars.has(key) ? ` ${chalk.cyan("[MRT only]")}` : "";
98
+ lines.push(` ${configPath.padEnd(padLen)} = ${value}${tag}`);
99
+ }
100
+ }
101
+ lines.push("");
102
+ }
103
+ if (envOverrides.length > 0) {
104
+ lines.push(chalk.dim(" Tip: Run `pnpm config:push-env` to sync local .env overrides to MRT."));
105
+ lines.push("");
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/commands/config/inspect.ts
112
+ /**
113
+ * Show which config.server.ts values are overridden by .env or MRT.
114
+ * When MRT is configured, each override is marked [local only] or [MRT only] as applicable.
115
+ *
116
+ * Environment variables read:
117
+ * MRT_PROJECT (optional) - MRT project slug, overridden by --project flag
118
+ * MRT_TARGET (optional) - MRT target environment, overridden by --environment flag
119
+ */
120
+ var ConfigInspect = class ConfigInspect extends MrtCommand {
121
+ static description = "Show which config.server.ts values are overridden by .env or MRT";
122
+ static examples = [
123
+ "<%= config.bin %> <%= command.id %>",
124
+ "<%= config.bin %> <%= command.id %> --project my-project --environment staging",
125
+ "<%= config.bin %> <%= command.id %> -d /path/to/my-storefront"
126
+ ];
127
+ static flags = {
128
+ ...MrtCommand.baseFlags,
129
+ ...commonFlags
130
+ };
131
+ operations = {
132
+ readEnvFile: (projectDirectory) => {
133
+ const envPath = join(projectDirectory, ".env");
134
+ try {
135
+ return parseEnv(fs.readFileSync(envPath, "utf8"));
136
+ } catch (err) {
137
+ if (err.code === "ENOENT") return {};
138
+ throw err;
139
+ }
140
+ },
141
+ loadConfig: async (projectDirectory) => {
142
+ return (await import(pathToFileURL(join(projectDirectory, "node_modules/@salesforce/storefront-next-runtime/dist/config-load.js")).href)).loadConfig();
143
+ },
144
+ listEnvVars
145
+ };
146
+ async run() {
147
+ const { flags, raw } = await this.parse(ConfigInspect);
148
+ const projectDirectory = resolve(flags["project-directory"]);
149
+ const explicitFlags = new Set(raw.filter((t) => t.type === "flag").map((t) => t.flag));
150
+ const rawEnvVars = this.operations.readEnvFile(projectDirectory);
151
+ if (Object.keys(rawEnvVars).length === 0) this.warn(`No .env file found in ${projectDirectory}. Showing config.server.ts defaults only.`);
152
+ let config = {};
153
+ const originalCwd = process.cwd();
154
+ try {
155
+ process.chdir(projectDirectory);
156
+ config = await this.operations.loadConfig(projectDirectory);
157
+ } catch (err) {
158
+ this.warn(`Could not load storefront config: ${err.message}`);
159
+ } finally {
160
+ process.chdir(originalCwd);
161
+ }
162
+ const flatConfig = flattenObject(config);
163
+ const envKeys = new Set(Object.keys(rawEnvVars).filter((k) => k.startsWith("PUBLIC__")));
164
+ const sources = getValueSources(flatConfig.map((e) => e.key), envKeys);
165
+ const project = (explicitFlags.has("project") ? flags.project : void 0) || rawEnvVars.MRT_PROJECT || this.resolvedConfig.values.mrtProject;
166
+ const environment = (explicitFlags.has("environment") ? flags.environment : void 0) || rawEnvVars.MRT_TARGET || this.resolvedConfig.values.mrtEnvironment;
167
+ let mrtVars = null;
168
+ if (project && environment) try {
169
+ this.requireMrtCredentials();
170
+ const { variables } = await this.operations.listEnvVars({
171
+ projectSlug: project,
172
+ environment,
173
+ origin: this.resolvedConfig.values.mrtOrigin
174
+ }, this.getMrtAuth());
175
+ mrtVars = new Map(variables.map((v) => [v.name, v.value]));
176
+ } catch (err) {
177
+ this.warn(`Could not fetch MRT env vars for ${project}/${environment}: ${err.message}`);
178
+ }
179
+ else ux.stdout("ℹ MRT project/environment not configured. Skipping MRT comparison.\n Use --project and --environment flags or set MRT_PROJECT/MRT_TARGET.\n");
180
+ const output = formatInspectOutput({
181
+ flatConfig,
182
+ sources,
183
+ localVars: new Map(Object.entries(rawEnvVars)),
184
+ mrtVars
185
+ });
186
+ ux.stdout(output);
187
+ }
188
+ };
189
+
190
+ //#endregion
191
+ export { ConfigInspect as default };
@@ -2,7 +2,7 @@ import { t as logger } from "../logger.js";
2
2
  import "../logger2.js";
3
3
  import { i as getMrtConfig, n as getDefaultBuildDir, r as getDefaultMessage } from "../utils.js";
4
4
  import { a as buildMrtConfig } from "../config.js";
5
- import { t as commonFlags } from "../flags.js";
5
+ import { r as commonFlags } from "../flags.js";
6
6
  import { t as createBundle } from "../bundle.js";
7
7
  import { Command, Flags } from "@oclif/core";
8
8
  import path from "path";
@@ -7,8 +7,8 @@ import { Command, Flags } from "@oclif/core";
7
7
  import { execFileSync, execSync } from "child_process";
8
8
  import path from "path";
9
9
  import fs from "fs-extra";
10
- import dotenv from "dotenv";
11
10
  import prompts from "prompts";
11
+ import { parseEnv } from "node:util";
12
12
 
13
13
  //#region src/create-storefront.ts
14
14
  const DEFAULT_STOREFRONT = "sfcc-storefront";
@@ -156,7 +156,7 @@ const createStorefront = async (options = {}) => {
156
156
  const configMeta = JSON.parse(fs.readFileSync(configMetaPath, "utf8"));
157
157
  const envDefaultPath = path.join(outputPath, ".env.default");
158
158
  let envDefaultValues = {};
159
- if (fs.existsSync(envDefaultPath)) envDefaultValues = dotenv.parse(fs.readFileSync(envDefaultPath, "utf8"));
159
+ if (fs.existsSync(envDefaultPath)) envDefaultValues = parseEnv(fs.readFileSync(envDefaultPath, "utf8"));
160
160
  logger.info("\n⚙️ We will now configure your storefront before it will be ready to run.\n");
161
161
  const configOverrides = {};
162
162
  for (const config of configMeta.configs) if (options.defaults) configOverrides[config.key] = envDefaultValues[config.key] ?? "";
@@ -164,7 +164,7 @@ const createStorefront = async (options = {}) => {
164
164
  const answer = await prompts({
165
165
  type: "text",
166
166
  name: config.key,
167
- message: `What is the value for ${config.name}? (default: ${envDefaultValues[config.key]})\n`,
167
+ message: `What is the value for ${config.name}? (default: ${envDefaultValues[config.key] ?? ""})\n`,
168
168
  initial: envDefaultValues[config.key] ?? ""
169
169
  });
170
170
  configOverrides[config.key] = answer[config.key];
@@ -1,9 +1,8 @@
1
1
  import { i as printShutdownMessage, n as printServerConfig, r as printServerInfo } from "../logger.js";
2
2
  import "../logger2.js";
3
- import { c as loadEnvFile } from "../utils.js";
4
3
  import { i as loadProjectConfig, n as initBasePathEnv, r as getCommerceCloudApiUrl, t as createServer$2 } from "../server.js";
5
4
  import "../config.js";
6
- import { t as commonFlags } from "../flags.js";
5
+ import { r as commonFlags } from "../flags.js";
7
6
  import { Command, Flags } from "@oclif/core";
8
7
  import path from "path";
9
8
  import { createServer } from "node:http";
@@ -49,7 +48,6 @@ async function dev(options = {}) {
49
48
  const projectDir = path.resolve(options.projectDirectory || process.cwd());
50
49
  const port = options.port || 5173;
51
50
  process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
52
- loadEnvFile(projectDir);
53
51
  await initBasePathEnv(projectDir);
54
52
  process.env.EXTERNAL_DOMAIN_NAME = process.env.EXTERNAL_DOMAIN_NAME ?? `localhost:${port}`;
55
53
  const config = await loadProjectConfig(projectDir);
@@ -1,7 +1,7 @@
1
1
  import "../../logger.js";
2
2
  import "../../logger2.js";
3
3
  import "../../dependency-utils.js";
4
- import { t as commonFlags } from "../../flags.js";
4
+ import { r as commonFlags } from "../../flags.js";
5
5
  import { r as manageExtensions } from "../../manage-extensions.js";
6
6
  import { Command, Flags } from "@oclif/core";
7
7
 
@@ -1,7 +1,7 @@
1
1
  import "../../logger.js";
2
2
  import "../../logger2.js";
3
3
  import "../../dependency-utils.js";
4
- import { t as commonFlags } from "../../flags.js";
4
+ import { r as commonFlags } from "../../flags.js";
5
5
  import { n as listExtensions } from "../../manage-extensions.js";
6
6
  import { Command } from "@oclif/core";
7
7
 
@@ -1,7 +1,7 @@
1
1
  import "../../logger.js";
2
2
  import "../../logger2.js";
3
3
  import "../../dependency-utils.js";
4
- import { t as commonFlags } from "../../flags.js";
4
+ import { r as commonFlags } from "../../flags.js";
5
5
  import { r as manageExtensions } from "../../manage-extensions.js";
6
6
  import { Command, Flags } from "@oclif/core";
7
7
 
@@ -1,7 +1,7 @@
1
1
  import "../logger.js";
2
2
  import "../logger2.js";
3
3
  import { i as SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR, t as CARTRIDGES_BASE_DIR } from "../config.js";
4
- import { t as commonFlags } from "../flags.js";
4
+ import { r as commonFlags } from "../flags.js";
5
5
  import { t as generateMetadata } from "../generate-cartridge.js";
6
6
  import { t as validateCartridgeMetadata } from "../validate-cartridge.js";
7
7
  import { Command } from "@oclif/core";
@@ -0,0 +1,181 @@
1
+ import { t as logger } from "../../logger.js";
2
+ import "../../logger2.js";
3
+ import { r as commonFlags } from "../../flags.js";
4
+ import { Command, Flags } from "@oclif/core";
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { mkdir, readdir, writeFile } from "fs/promises";
8
+
9
+ //#region src/i18n/aggregate-extension-locales.ts
10
+ /** Apache License 2.0 header text for generated files. Inlined to avoid path resolution issues in standalone projects. */
11
+ const APACHE_LICENSE_HEADER = [
12
+ `Copyright ${(/* @__PURE__ */ new Date()).getFullYear()} Salesforce, Inc.`,
13
+ "",
14
+ "Licensed under the Apache License, Version 2.0 (the \"License\");",
15
+ "you may not use this file except in compliance with the License.",
16
+ "You may obtain a copy of the License at",
17
+ "",
18
+ " http://www.apache.org/licenses/LICENSE-2.0",
19
+ "",
20
+ "Unless required by applicable law or agreed to in writing, software",
21
+ "distributed under the License is distributed on an \"AS IS\" BASIS,",
22
+ "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
23
+ "See the License for the specific language governing permissions and",
24
+ "limitations under the License."
25
+ ].join("\n");
26
+ function getDefaultDirs(projectDirectory) {
27
+ const srcDir = join(projectDirectory, "src");
28
+ return {
29
+ SRC_DIR: srcDir,
30
+ EXTENSIONS_DIR: join(srcDir, "extensions"),
31
+ OUTPUT_DIR: join(srcDir, "extensions", "locales")
32
+ };
33
+ }
34
+ /** Convert kebab-case to PascalCase (e.g. `store-locator` → `StoreLocator`). */
35
+ function toPascalCase(str) {
36
+ return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
37
+ }
38
+ /** Convert kebab-case to camelCase for variable names (e.g. `store-locator` → `storeLocator`). */
39
+ function toCamelCase(str) {
40
+ const pascal = toPascalCase(str);
41
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
42
+ }
43
+ /** Scan main app and extension directories to find all available locale codes. */
44
+ async function discoverLocales(dirs) {
45
+ const { SRC_DIR, EXTENSIONS_DIR } = dirs;
46
+ const locales = /* @__PURE__ */ new Set();
47
+ const mainLocalesPath = join(SRC_DIR, "locales");
48
+ if (existsSync(mainLocalesPath)) try {
49
+ const mainLocaleEntries = await readdir(mainLocalesPath, { withFileTypes: true });
50
+ for (const entry of mainLocaleEntries) if (entry.isDirectory()) locales.add(entry.name);
51
+ } catch (error) {
52
+ if (error.code !== "ENOENT") throw error;
53
+ }
54
+ try {
55
+ const extensions = await readdir(EXTENSIONS_DIR, { withFileTypes: true });
56
+ for (const extension of extensions) {
57
+ if (!extension.isDirectory() || extension.name === "locales") continue;
58
+ const localesPath = join(EXTENSIONS_DIR, extension.name, "locales");
59
+ if (!existsSync(localesPath)) continue;
60
+ const localeEntries = await readdir(localesPath, { withFileTypes: true });
61
+ for (const localeEntry of localeEntries) if (localeEntry.isDirectory()) locales.add(localeEntry.name);
62
+ }
63
+ } catch (error) {
64
+ if (error.code !== "ENOENT") throw error;
65
+ }
66
+ return locales;
67
+ }
68
+ /** Find all extensions (NOT main app) that have a `translations.json` for the given locale. */
69
+ async function findExtensionsWithLocale(locale, extensionsDir) {
70
+ const extensions = [];
71
+ try {
72
+ const extensionEntries = await readdir(extensionsDir, { withFileTypes: true });
73
+ for (const entry of extensionEntries) {
74
+ if (!entry.isDirectory() || entry.name === "locales") continue;
75
+ if (existsSync(join(extensionsDir, entry.name, "locales", locale, "translations.json"))) extensions.push({
76
+ name: entry.name,
77
+ path: `@/extensions/${entry.name}/locales/${locale}/translations.json`
78
+ });
79
+ }
80
+ } catch (error) {
81
+ if (error.code !== "ENOENT") throw error;
82
+ }
83
+ return extensions.sort((a, b) => a.name.localeCompare(b.name));
84
+ }
85
+ /** Generate the locale index file content that re-exports extension translations under `extPascalCase` namespaces. */
86
+ function generateLocaleFile(extensions) {
87
+ const header = `${`/**\n${APACHE_LICENSE_HEADER.split("\n").map((line) => line ? ` * ${line}` : " *").join("\n")}\n */`}
88
+
89
+ // NOTE: This file is auto-generated. Do not edit manually.
90
+ // Run 'pnpm locales:aggregate-extensions' to regenerate this file.
91
+
92
+ `;
93
+ if (extensions.length === 0) return `${header}// No extension translations found for this locale\nexport default {};\n`;
94
+ return `${header}${extensions.map((ext) => {
95
+ return `import ${`${toCamelCase(ext.name)}Translations`} from '${ext.path}';`;
96
+ }).join("\n")}
97
+
98
+ // Namespace is based on the following convention: extPascalCase, and it's the pascal case of the folder name (e.g. store-locator -> extStoreLocator)
99
+ export default {
100
+ ${extensions.map((ext) => {
101
+ return ` ${`ext${toPascalCase(ext.name)}`}: ${`${toCamelCase(ext.name)}Translations`},`;
102
+ }).join("\n")}
103
+ };
104
+ `;
105
+ }
106
+ /**
107
+ * Generate aggregation files for extension translations only.
108
+ * Main app translations in `/src/locales/` are NOT aggregated — only per-extension `translations.json` files.
109
+ */
110
+ async function aggregateExtensionLocales(options = {}) {
111
+ const { projectDirectory = process.cwd(), silent = false } = options;
112
+ const dirs = options.dirs ?? getDefaultDirs(projectDirectory);
113
+ const { OUTPUT_DIR, EXTENSIONS_DIR } = dirs;
114
+ const log = (message, ...args) => {
115
+ if (!silent) logger.debug(message, ...args);
116
+ };
117
+ try {
118
+ log("🔍 Scanning for extension translation files...");
119
+ const locales = await discoverLocales(dirs);
120
+ if (locales.size === 0) {
121
+ log("📝 No locales found in extensions. Nothing to generate.");
122
+ return {
123
+ generated: 0,
124
+ locales: []
125
+ };
126
+ }
127
+ log(`📝 Found ${locales.size} locale(s): ${Array.from(locales).join(", ")}`);
128
+ await mkdir(OUTPUT_DIR, { recursive: true });
129
+ const results = [];
130
+ for (const locale of locales) {
131
+ const extensions = await findExtensionsWithLocale(locale, EXTENSIONS_DIR);
132
+ const content = generateLocaleFile(extensions);
133
+ const outputPath = join(OUTPUT_DIR, locale);
134
+ await mkdir(outputPath, { recursive: true });
135
+ const filePath = join(outputPath, "index.ts");
136
+ await writeFile(filePath, content, "utf8");
137
+ log(`✅ Generated: src/extensions/locales/${locale}/index.ts (${extensions.length} extension(s))`);
138
+ results.push({
139
+ locale,
140
+ extensionCount: extensions.length,
141
+ filePath
142
+ });
143
+ }
144
+ log("✨ Extension locale generation complete!");
145
+ return {
146
+ generated: results.length,
147
+ locales: results
148
+ };
149
+ } catch (error) {
150
+ if (!silent) logger.error(`❌ Error generating extension locales: ${String(error)}`);
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ //#endregion
156
+ //#region src/commands/locales/aggregate-extensions.ts
157
+ var AggregateExtensions = class AggregateExtensions extends Command {
158
+ static description = "Aggregate extension translation files into per-locale barrel files";
159
+ static examples = [
160
+ "<%= config.bin %> <%= command.id %>",
161
+ "<%= config.bin %> <%= command.id %> -d ./my-project",
162
+ "<%= config.bin %> <%= command.id %> --silent"
163
+ ];
164
+ static flags = {
165
+ ...commonFlags,
166
+ silent: Flags.boolean({
167
+ description: "Suppress output",
168
+ default: false
169
+ })
170
+ };
171
+ async run() {
172
+ const { flags } = await this.parse(AggregateExtensions);
173
+ await aggregateExtensionLocales({
174
+ projectDirectory: flags["project-directory"],
175
+ silent: flags.silent
176
+ });
177
+ }
178
+ };
179
+
180
+ //#endregion
181
+ export { AggregateExtensions as default };
@@ -1,6 +1,6 @@
1
1
  import "../logger.js";
2
2
  import { t as prepareForLocalDev } from "../local-dev-setup.js";
3
- import { t as commonFlags } from "../flags.js";
3
+ import { r as commonFlags } from "../flags.js";
4
4
  import { Command, Flags } from "@oclif/core";
5
5
 
6
6
  //#region src/commands/prepare-local.ts
@@ -1,9 +1,8 @@
1
1
  import { i as printShutdownMessage, n as printServerConfig, r as printServerInfo, t as logger } from "../logger.js";
2
2
  import "../logger2.js";
3
- import { c as loadEnvFile } from "../utils.js";
4
3
  import { i as loadProjectConfig, n as initBasePathEnv, r as getCommerceCloudApiUrl, t as createServer } from "../server.js";
5
4
  import "../config.js";
6
- import { t as commonFlags } from "../flags.js";
5
+ import { r as commonFlags } from "../flags.js";
7
6
  import { Command, Flags } from "@oclif/core";
8
7
  import { execSync } from "child_process";
9
8
  import path from "path";
@@ -22,7 +21,6 @@ async function preview(options = {}) {
22
21
  const port = options.port || 3e3;
23
22
  process.env.NODE_ENV = process.env.NODE_ENV ?? "production";
24
23
  process.env.EXTERNAL_DOMAIN_NAME = process.env.EXTERNAL_DOMAIN_NAME ?? `localhost:${port}`;
25
- loadEnvFile(projectDir);
26
24
  await initBasePathEnv(projectDir);
27
25
  const buildPath = path.join(projectDir, "build", "server", "index.js");
28
26
  if (!fs.existsSync(buildPath)) {