@salesforce/storefront-next-dev 0.1.1 → 0.2.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.
Files changed (50) hide show
  1. package/README.md +45 -36
  2. package/bin/run.js +12 -0
  3. package/dist/bundle.js +83 -0
  4. package/dist/cartridge-services/index.d.ts +2 -26
  5. package/dist/cartridge-services/index.d.ts.map +1 -1
  6. package/dist/cartridge-services/index.js +3 -336
  7. package/dist/cartridge-services/index.js.map +1 -1
  8. package/dist/commands/create-bundle.js +107 -0
  9. package/dist/commands/create-instructions.js +174 -0
  10. package/dist/commands/create-storefront.js +210 -0
  11. package/dist/commands/deploy-cartridge.js +52 -0
  12. package/dist/commands/dev.js +122 -0
  13. package/dist/commands/extensions/create.js +38 -0
  14. package/dist/commands/extensions/install.js +44 -0
  15. package/dist/commands/extensions/list.js +21 -0
  16. package/dist/commands/extensions/remove.js +38 -0
  17. package/dist/commands/generate-cartridge.js +35 -0
  18. package/dist/commands/prepare-local.js +30 -0
  19. package/dist/commands/preview.js +101 -0
  20. package/dist/commands/push.js +139 -0
  21. package/dist/config.js +87 -0
  22. package/dist/configs/react-router.config.js +3 -1
  23. package/dist/configs/react-router.config.js.map +1 -1
  24. package/dist/dependency-utils.js +314 -0
  25. package/dist/entry/client.d.ts +1 -0
  26. package/dist/entry/client.js +28 -0
  27. package/dist/entry/client.js.map +1 -0
  28. package/dist/entry/server.d.ts +15 -0
  29. package/dist/entry/server.d.ts.map +1 -0
  30. package/dist/entry/server.js +35 -0
  31. package/dist/entry/server.js.map +1 -0
  32. package/dist/flags.js +11 -0
  33. package/dist/generate-cartridge.js +620 -0
  34. package/dist/hooks/init.js +47 -0
  35. package/dist/index.d.ts +9 -29
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +413 -621
  38. package/dist/index.js.map +1 -1
  39. package/dist/local-dev-setup.js +176 -0
  40. package/dist/logger.js +105 -0
  41. package/dist/manage-extensions.js +329 -0
  42. package/dist/mrt/ssr.mjs +21 -21
  43. package/dist/mrt/ssr.mjs.map +1 -1
  44. package/dist/mrt/streamingHandler.mjs +28 -28
  45. package/dist/mrt/streamingHandler.mjs.map +1 -1
  46. package/dist/server.js +425 -0
  47. package/dist/utils.js +126 -0
  48. package/package.json +44 -9
  49. package/dist/cli.js +0 -3393
  50. /package/{LICENSE.txt → LICENSE} +0 -0
@@ -0,0 +1,176 @@
1
+ import { c as warn, r as info, s as success } from "./logger.js";
2
+ import path from "path";
3
+ import fs from "fs-extra";
4
+ import prompts from "prompts";
5
+
6
+ //#region src/utils/local-dev-setup.ts
7
+ /**
8
+ * Prepares a cloned template for standalone use outside the monorepo.
9
+ * Prompts user for local package paths and replaces workspace:* dependencies with file: references.
10
+ */
11
+ async function prepareForLocalDev(options) {
12
+ const { projectDirectory, sourcePackagesDir } = options;
13
+ const packageJsonPath = path.join(projectDirectory, "package.json");
14
+ if (!fs.existsSync(packageJsonPath)) throw new Error(`package.json not found in ${projectDirectory}`);
15
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
16
+ const workspaceDeps = [];
17
+ for (const depType of [
18
+ "dependencies",
19
+ "devDependencies",
20
+ "peerDependencies"
21
+ ]) {
22
+ const deps = packageJson[depType];
23
+ if (!deps) continue;
24
+ for (const [pkg, version] of Object.entries(deps)) if (typeof version === "string" && version.startsWith("workspace:")) workspaceDeps.push({
25
+ pkg,
26
+ depType
27
+ });
28
+ }
29
+ if (workspaceDeps.length === 0) {
30
+ info("No workspace:* dependencies found. Project is ready for standalone use.");
31
+ return;
32
+ }
33
+ console.log("\nšŸ”— Found workspace dependencies that need to be linked to local packages:\n");
34
+ for (const { pkg } of workspaceDeps) console.log(` • ${pkg}`);
35
+ console.log("");
36
+ const defaultPaths = {};
37
+ if (sourcePackagesDir) {
38
+ defaultPaths["@salesforce/storefront-next-dev"] = path.join(sourcePackagesDir, "storefront-next-dev");
39
+ defaultPaths["@salesforce/storefront-next-runtime"] = path.join(sourcePackagesDir, "storefront-next-runtime");
40
+ }
41
+ const resolvedPaths = {};
42
+ for (const { pkg } of workspaceDeps) {
43
+ if (resolvedPaths[pkg]) continue;
44
+ const defaultPath = defaultPaths[pkg] || "";
45
+ const defaultExists = defaultPath && fs.existsSync(defaultPath);
46
+ const { localPath } = await prompts({
47
+ type: "text",
48
+ name: "localPath",
49
+ message: `šŸ“¦ Path to ${pkg}:`,
50
+ initial: defaultExists ? defaultPath : "",
51
+ validate: (value) => {
52
+ if (!value) return "Path is required";
53
+ if (!fs.existsSync(value)) return `Directory not found: ${value}`;
54
+ if (!fs.existsSync(path.join(value, "package.json"))) return `No package.json found in: ${value}`;
55
+ return true;
56
+ }
57
+ });
58
+ if (!localPath) {
59
+ warn(`Skipping ${pkg} - no path provided`);
60
+ continue;
61
+ }
62
+ resolvedPaths[pkg] = localPath;
63
+ }
64
+ let modified = false;
65
+ for (const depType of [
66
+ "dependencies",
67
+ "devDependencies",
68
+ "peerDependencies"
69
+ ]) {
70
+ const deps = packageJson[depType];
71
+ if (!deps) continue;
72
+ for (const [pkg, version] of Object.entries(deps)) if (typeof version === "string" && version.startsWith("workspace:")) {
73
+ const localPath = resolvedPaths[pkg];
74
+ if (localPath) {
75
+ const fileRef = `file:${localPath}`;
76
+ info(`Linked ${pkg} → ${fileRef}`);
77
+ deps[pkg] = fileRef;
78
+ modified = true;
79
+ } else {
80
+ warn(`Removing unresolved workspace dependency: ${pkg}`);
81
+ delete deps[pkg];
82
+ modified = true;
83
+ }
84
+ }
85
+ }
86
+ if (packageJson.volta?.extends) {
87
+ delete packageJson.volta.extends;
88
+ if (Object.keys(packageJson.volta).length === 0) delete packageJson.volta;
89
+ modified = true;
90
+ }
91
+ if (modified) {
92
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 4)}\n`);
93
+ success("package.json updated with local package links");
94
+ patchViteConfigForLinkedPackages(projectDirectory, Object.keys(resolvedPaths));
95
+ }
96
+ }
97
+ /**
98
+ * Patches vite.config.ts to fix "You must render this element inside a <HydratedRouter>" errors
99
+ * that occur when using file: linked packages.
100
+ *
101
+ * The fix adds:
102
+ * 1. resolve.dedupe for react, react-dom, react-router (helps with non-linked duplicates)
103
+ * 2. ssr.noExternal for file-linked packages (key fix - bundles them so they use host's dependencies)
104
+ *
105
+ * When packages are in ssr.noExternal, Vite bundles them during SSR instead of externalizing.
106
+ * During bundling, their imports resolve through the host project's node_modules,
107
+ * ensuring all code uses the same react-router instance with the same context.
108
+ */
109
+ function patchViteConfigForLinkedPackages(projectDirectory, linkedPackages) {
110
+ const viteConfigPath = path.join(projectDirectory, "vite.config.ts");
111
+ if (!fs.existsSync(viteConfigPath)) {
112
+ warn("vite.config.ts not found, skipping patch for file-linked packages");
113
+ return;
114
+ }
115
+ if (linkedPackages.length === 0) return;
116
+ let viteConfig = fs.readFileSync(viteConfigPath, "utf8");
117
+ let modified = false;
118
+ if (!viteConfig.includes("dedupe:")) {
119
+ const resolveMatch = viteConfig.match(/resolve:\s*\{/);
120
+ if (resolveMatch && resolveMatch.index !== void 0) {
121
+ const insertPos = resolveMatch.index + resolveMatch[0].length;
122
+ viteConfig = viteConfig.slice(0, insertPos) + `
123
+ // Deduplicates packages to prevent context issues with file-linked packages
124
+ dedupe: ['react', 'react-dom', 'react-router'],` + viteConfig.slice(insertPos);
125
+ modified = true;
126
+ }
127
+ }
128
+ const packageList = linkedPackages.map((p) => `'${p}'`).join(", ");
129
+ if (/ssr:\s*\{[^}]*noExternal:/.test(viteConfig)) {
130
+ const noExternalArrayRegex = /noExternal:\s*\[([^\]]*)\]/;
131
+ const noExternalMatch = viteConfig.match(noExternalArrayRegex);
132
+ if (noExternalMatch) {
133
+ const existingPackages = noExternalMatch[1];
134
+ const packagesToAdd = linkedPackages.filter((p) => !existingPackages.includes(p));
135
+ if (packagesToAdd.length > 0) {
136
+ const newPackageList = packagesToAdd.map((p) => `'${p}'`).join(", ");
137
+ const newArray = existingPackages.trim() ? `[${existingPackages.trim()}, ${newPackageList}]` : `[${newPackageList}]`;
138
+ viteConfig = viteConfig.replace(noExternalArrayRegex, `noExternal: ${newArray}`);
139
+ modified = true;
140
+ }
141
+ }
142
+ } else {
143
+ const ssrMatch = viteConfig.match(/ssr:\s*\{/);
144
+ if (ssrMatch && ssrMatch.index !== void 0) {
145
+ const insertPos = ssrMatch.index + ssrMatch[0].length;
146
+ const noExternalBlock = `
147
+ // Bundle file-linked packages so they use host project's dependencies
148
+ // This prevents "You must render this element inside a <HydratedRouter>" errors
149
+ noExternal: [${packageList}],`;
150
+ viteConfig = viteConfig.slice(0, insertPos) + noExternalBlock + viteConfig.slice(insertPos);
151
+ modified = true;
152
+ } else {
153
+ const returnMatch = viteConfig.match(/return\s*\{/);
154
+ if (returnMatch && returnMatch.index !== void 0) {
155
+ const insertPos = returnMatch.index + returnMatch[0].length;
156
+ const ssrBlock = `
157
+ // SSR config for file-linked packages
158
+ ssr: {
159
+ // Bundle file-linked packages so they use host project's dependencies
160
+ // This prevents "You must render this element inside a <HydratedRouter>" errors
161
+ noExternal: [${packageList}],
162
+ target: 'node',
163
+ },`;
164
+ viteConfig = viteConfig.slice(0, insertPos) + ssrBlock + viteConfig.slice(insertPos);
165
+ modified = true;
166
+ }
167
+ }
168
+ }
169
+ if (modified) {
170
+ fs.writeFileSync(viteConfigPath, viteConfig);
171
+ success("vite.config.ts patched for file-linked packages (ssr.noExternal + resolve.dedupe)");
172
+ } else info("vite.config.ts already configured for file-linked packages");
173
+ }
174
+
175
+ //#endregion
176
+ export { prepareForLocalDev as t };
package/dist/logger.js ADDED
@@ -0,0 +1,105 @@
1
+ import os from "os";
2
+ import chalk from "chalk";
3
+ import { createRequire } from "module";
4
+
5
+ //#region package.json
6
+ var version = "0.2.0-alpha.1";
7
+
8
+ //#endregion
9
+ //#region src/utils/logger.ts
10
+ /**
11
+ * Get the local network IPv4 address
12
+ */
13
+ function getNetworkAddress() {
14
+ const interfaces = os.networkInterfaces();
15
+ for (const name of Object.keys(interfaces)) {
16
+ const iface = interfaces[name];
17
+ if (!iface) continue;
18
+ for (const alias of iface) if (alias.family === "IPv4" && !alias.internal) return alias.address;
19
+ }
20
+ }
21
+ /**
22
+ * Get the version of a package from the project's package.json
23
+ */
24
+ function getPackageVersion(packageName, projectDir) {
25
+ try {
26
+ const require = createRequire(import.meta.url);
27
+ return require(require.resolve(`${packageName}/package.json`, { paths: [projectDir] })).version;
28
+ } catch {
29
+ return "unknown";
30
+ }
31
+ }
32
+ /**
33
+ * Logger utilities
34
+ */
35
+ const colors = {
36
+ warn: "yellow",
37
+ error: "red",
38
+ success: "cyan",
39
+ info: "green",
40
+ debug: "gray"
41
+ };
42
+ const fancyLog = (level, msg) => {
43
+ const colorFn = chalk[colors[level]];
44
+ console.log(`${colorFn(level)}: ${msg}`);
45
+ };
46
+ const info = (msg) => fancyLog("info", msg);
47
+ const success = (msg) => fancyLog("success", msg);
48
+ const warn = (msg) => fancyLog("warn", msg);
49
+ const error = (msg) => fancyLog("error", msg);
50
+ const debug = (msg, data) => {
51
+ if (process.env.DEBUG || process.env.NODE_ENV !== "production") {
52
+ fancyLog("debug", msg);
53
+ if (data) console.log(data);
54
+ }
55
+ };
56
+ /**
57
+ * Print the server information banner with URLs and versions
58
+ */
59
+ function printServerInfo(mode, port, startTime, projectDir) {
60
+ const elapsed = Date.now() - startTime;
61
+ const sfnextVersion = version;
62
+ const reactVersion = getPackageVersion("react", projectDir);
63
+ const reactRouterVersion = getPackageVersion("react-router", projectDir);
64
+ const modeLabel = mode === "development" ? "Development Mode" : "Preview Mode";
65
+ console.log();
66
+ console.log(` ${chalk.cyan.bold("⚔ SFCC Storefront Next")} ${chalk.dim(`v${sfnextVersion}`)}`);
67
+ console.log(` ${chalk.green.bold(modeLabel)}`);
68
+ console.log();
69
+ console.log(` ${chalk.dim("react")} ${chalk.green(`v${reactVersion}`)} ${chalk.dim("│")} ${chalk.dim("react-router")} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim("│")} ${chalk.green(`ready in ${elapsed}ms`)}`);
70
+ console.log();
71
+ }
72
+ /**
73
+ * Print server configuration details (proxy, static, etc.)
74
+ */
75
+ function printServerConfig(config) {
76
+ const { port, enableProxy, enableStaticServing, enableCompression, proxyPath, proxyHost, shortCode, organizationId, clientId, siteId } = config;
77
+ console.log(` ${chalk.bold("Environment Configuration:")}`);
78
+ if (enableProxy && proxyPath && proxyHost && shortCode) {
79
+ console.log(` ${chalk.green("āœ“")} ${chalk.bold("Proxy:")} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim("→")} ${chalk.cyan(proxyHost)}`);
80
+ console.log(` ${chalk.dim("Short Code: ")} ${chalk.dim(shortCode)}`);
81
+ if (organizationId) console.log(` ${chalk.dim("Organization ID:")} ${chalk.dim(organizationId)}`);
82
+ if (clientId) console.log(` ${chalk.dim("Client ID: ")} ${chalk.dim(clientId)}`);
83
+ if (siteId) console.log(` ${chalk.dim("Site ID: ")} ${chalk.dim(siteId)}`);
84
+ } else console.log(` ${chalk.gray("ā—‹")} ${chalk.bold("Proxy: ")} ${chalk.dim("disabled")}`);
85
+ if (enableStaticServing) console.log(` ${chalk.green("āœ“")} ${chalk.bold("Static: ")} ${chalk.dim("enabled")}`);
86
+ if (enableCompression) console.log(` ${chalk.green("āœ“")} ${chalk.bold("Compression: ")} ${chalk.dim("enabled")}`);
87
+ const localUrl = `http://localhost:${port}`;
88
+ const networkAddress = process.env.SHOW_NETWORK === "true" ? getNetworkAddress() : void 0;
89
+ const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : void 0;
90
+ console.log();
91
+ console.log(` ${chalk.green("āžœ")} ${chalk.bold("Local: ")} ${chalk.cyan(localUrl)}`);
92
+ if (networkUrl) console.log(` ${chalk.green("āžœ")} ${chalk.bold("Network:")} ${chalk.cyan(networkUrl)}`);
93
+ console.log();
94
+ console.log(` ${chalk.dim("Press")} ${chalk.bold("Ctrl+C")} ${chalk.dim("to stop the server")}`);
95
+ console.log();
96
+ }
97
+ /**
98
+ * Print shutdown message
99
+ */
100
+ function printShutdownMessage() {
101
+ console.log(`\n ${chalk.yellow("⚔")} ${chalk.dim("Server shutting down...")}\n`);
102
+ }
103
+
104
+ //#endregion
105
+ export { printServerInfo as a, warn as c, printServerConfig as i, error as n, printShutdownMessage as o, info as r, success as s, debug as t };
@@ -0,0 +1,329 @@
1
+ import { a as trimExtensions, r as resolveDependentsForMultiple, t as getMissingDependencies } from "./dependency-utils.js";
2
+ import { execSync } from "child_process";
3
+ import os from "os";
4
+ import path from "path";
5
+ import fs from "fs-extra";
6
+ import prompts from "prompts";
7
+ import { z } from "zod";
8
+
9
+ //#region src/extensibility/manage-extensions.ts
10
+ const EXTENSIONS_DIR = ["src", "extensions"];
11
+ const CONFIG_PATH = [...EXTENSIONS_DIR, "config.json"];
12
+ const EXTENSION_FOLDERS = [
13
+ "components",
14
+ "locales",
15
+ "hooks",
16
+ "routes"
17
+ ];
18
+ /**
19
+ * Console log a message with a specific type
20
+ * @param message string
21
+ * @param type
22
+ */
23
+ const consoleLog = (message, type) => {
24
+ switch (type) {
25
+ case "error":
26
+ console.error(`āŒ ${message}`);
27
+ break;
28
+ case "success":
29
+ console.log(`āœ… ${message}`);
30
+ break;
31
+ default:
32
+ console.log(message);
33
+ break;
34
+ }
35
+ };
36
+ /**
37
+ * Get the path to the extension config file
38
+ */
39
+ const getExtensionConfigPath = (projectDirectory) => {
40
+ return path.join(projectDirectory, ...CONFIG_PATH);
41
+ };
42
+ /**
43
+ * Check if the project directory contains the extensions directory and config.json file
44
+ */
45
+ const getExtensionConfig = (projectDirectory) => {
46
+ const extensionConfigPath = getExtensionConfigPath(projectDirectory);
47
+ if (!fs.existsSync(extensionConfigPath)) {
48
+ consoleLog(`Extension config file not found: ${extensionConfigPath}. Are you running this command in the correct project directory?`, "error");
49
+ process.exit(1);
50
+ }
51
+ return JSON.parse(fs.readFileSync(extensionConfigPath, "utf8")).extensions;
52
+ };
53
+ /**
54
+ * Common function to get the extension selection from the user
55
+ * @param type 'multiselect' | 'select'
56
+ * @param extensionConfig Record<string, ExtensionMeta>
57
+ * @param message string
58
+ * @param installedExtensions string[]
59
+ * @param excludeExtensions string[] extensions to exclude from the list, so we can filter out extensions that are already installed
60
+ * @returns string[]
61
+ */
62
+ const getExtensionSelection = async (type, extensionConfig, message, installedExtensions, excludeExtensions = []) => {
63
+ consoleLog("\n", "info");
64
+ const { selectedExtensions } = await prompts({
65
+ type,
66
+ name: "selectedExtensions",
67
+ message,
68
+ choices: installedExtensions.filter((extensionKey) => !excludeExtensions.includes(extensionKey)).map((extensionKey) => ({
69
+ title: `${extensionConfig[extensionKey].name} - ${extensionConfig[extensionKey].description}`,
70
+ value: extensionKey
71
+ })),
72
+ instructions: false
73
+ });
74
+ return type === "multiselect" ? selectedExtensions : [selectedExtensions];
75
+ };
76
+ /**
77
+ * Handle the uninstallation of extensions
78
+ * @param extensionConfig Record<string, ExtensionMeta>
79
+ * @param options {
80
+ projectDirectory: string;
81
+ extensions?: string[];
82
+ verbose?: boolean;
83
+ }
84
+ * @returns void
85
+ */
86
+ const handleUninstall = async (extensionConfig, options) => {
87
+ let installedExtensions = Object.keys(extensionConfig);
88
+ if (installedExtensions.length === 0) {
89
+ consoleLog("\n You have not installed any extensions yet.", "error");
90
+ return;
91
+ }
92
+ const selectedExtensions = options.extensions ? options.extensions : await getExtensionSelection("multiselect", extensionConfig, "šŸ”Œ Which extensions would you like to uninstall?", installedExtensions);
93
+ if (selectedExtensions == null || selectedExtensions.length === 0) {
94
+ consoleLog("\n Please select at least one extension to uninstall.", "error");
95
+ return;
96
+ }
97
+ const allToUninstall = resolveDependentsForMultiple(selectedExtensions, { extensions: extensionConfig });
98
+ const installedSet = new Set(installedExtensions);
99
+ const extensionsToUninstall = allToUninstall.filter((key) => installedSet.has(key));
100
+ const selectedSet = new Set(selectedExtensions);
101
+ const additionalDependents = extensionsToUninstall.filter((key) => !selectedSet.has(key));
102
+ if (additionalDependents.length > 0) {
103
+ consoleLog("\n", "info");
104
+ consoleLog(`Uninstalling the selected extension(s) will also uninstall the following dependent extensions:`, "info");
105
+ additionalDependents.forEach((depKey) => {
106
+ const depExtension = extensionConfig[depKey];
107
+ const dependsOn = selectedExtensions.find((selKey) => {
108
+ return extensionConfig[selKey] && extensionConfig[depKey]?.dependencies?.includes(selKey);
109
+ });
110
+ const dependsOnName = dependsOn ? extensionConfig[dependsOn]?.name : "selected extension";
111
+ consoleLog(` • ${depExtension?.name || depKey} (depends on ${dependsOnName})`, "info");
112
+ });
113
+ consoleLog("\n", "info");
114
+ const { confirmUninstall } = await prompts({
115
+ type: "confirm",
116
+ name: "confirmUninstall",
117
+ message: `Uninstall all ${extensionsToUninstall.length} extensions?`,
118
+ initial: true
119
+ });
120
+ if (!confirmUninstall) {
121
+ consoleLog("Uninstallation aborted.", "info");
122
+ return;
123
+ }
124
+ }
125
+ extensionsToUninstall.forEach((ext) => {
126
+ if (extensionConfig[ext]?.folder) fs.rmSync(path.join(options.projectDirectory, ...EXTENSIONS_DIR, extensionConfig[ext].folder), {
127
+ recursive: true,
128
+ force: true
129
+ });
130
+ });
131
+ const extensionsToUninstallSet = new Set(extensionsToUninstall);
132
+ installedExtensions = installedExtensions.filter((ext) => !extensionsToUninstallSet.has(ext));
133
+ trimExtensions(options.projectDirectory, Object.fromEntries(installedExtensions.map((ext) => [ext, true])), { extensions: extensionConfig }, options.verbose ?? false);
134
+ consoleLog(" Extensions uninstalled.", "success");
135
+ };
136
+ /**
137
+ * Install a single extension (internal helper)
138
+ * @returns true if installation succeeded, false otherwise
139
+ */
140
+ const installSingleExtension = (extensionKey, srcExtensionConfig, extensionConfig, tmpDir, projectDirectory) => {
141
+ const extension = srcExtensionConfig[extensionKey];
142
+ const startTime = Date.now();
143
+ if (extension.folder) fs.copySync(path.join(tmpDir, ...EXTENSIONS_DIR, extension.folder), path.join(projectDirectory, ...EXTENSIONS_DIR, extension.folder));
144
+ if (extension.installationInstructions) {
145
+ console.log(`\nā³ Installing ${extension.name}, this will take a few minutes...`);
146
+ try {
147
+ execSync(`cursor-agent -p --force 'Execute the steps specified in the installation instructions file: ${extension.installationInstructions}' --output-format text`, {
148
+ cwd: projectDirectory,
149
+ stdio: "inherit"
150
+ });
151
+ } catch (e) {
152
+ consoleLog(`Error installing ${extension.name}. ${e.message}`, "error");
153
+ return false;
154
+ }
155
+ }
156
+ extensionConfig[extensionKey] = extension;
157
+ fs.writeFileSync(getExtensionConfigPath(projectDirectory), JSON.stringify({ extensions: extensionConfig }, null, 4));
158
+ consoleLog(`${extension.name} was installed successfully. (${Date.now() - startTime}ms)`, "success");
159
+ return true;
160
+ };
161
+ /**
162
+ * Handle the installation of extensions
163
+ * @param extensionConfig
164
+ * @param options {
165
+ sourceGithubUrl?: string;
166
+ projectDirectory: string;
167
+ extensions?: string[];
168
+ verbose?: boolean;
169
+ }
170
+ * @returns
171
+ */
172
+ const handleInstall = async (extensionConfig, options) => {
173
+ const { sourceGitUrl } = await prompts({
174
+ type: "text",
175
+ name: "sourceGitUrl",
176
+ message: "🌐 What is the Git URL for the extensions project?",
177
+ initial: options.sourceGitUrl
178
+ });
179
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `sfnext-extensions-${Date.now()}`));
180
+ execSync(`git clone ${sourceGitUrl} ${tmpDir}`);
181
+ const srcExtensionConfig = getExtensionConfig(tmpDir);
182
+ if (srcExtensionConfig == null || Object.keys(srcExtensionConfig).length === 0) {
183
+ consoleLog(`No extensions found in the source project, please check ${path.join(...CONFIG_PATH)} exists in ${sourceGitUrl} and contains at least one extension.`, "error");
184
+ return;
185
+ }
186
+ const selectedExtensions = options.extensions ? options.extensions : await getExtensionSelection("select", srcExtensionConfig, "šŸ”Œ Which extension would you like to install?", Object.keys(srcExtensionConfig), Object.keys(extensionConfig));
187
+ if (selectedExtensions == null || selectedExtensions.length !== 1 || selectedExtensions[0] == null) {
188
+ consoleLog("Please select exactly one extension to install.", "error");
189
+ return;
190
+ }
191
+ const extensionKey = selectedExtensions[0];
192
+ const extension = srcExtensionConfig[extensionKey];
193
+ if (Object.values(srcExtensionConfig).some((ext) => ext.installationInstructions)) try {
194
+ execSync("cursor-agent -v", { stdio: "ignore" });
195
+ } catch (e) {
196
+ consoleLog("This extension contains LLM instructions, please install cursor cli and try again. (https://cursor.com/docs/cli/overview)", "error");
197
+ return;
198
+ }
199
+ const srcConfig = { extensions: srcExtensionConfig };
200
+ const missingDeps = getMissingDependencies(extensionKey, Object.keys(extensionConfig), srcConfig);
201
+ const dependenciesToInstall = missingDeps.slice(0, -1);
202
+ let hasError = false;
203
+ try {
204
+ if (dependenciesToInstall.length > 0) {
205
+ consoleLog("\n", "info");
206
+ consoleLog(`Installing ${extension.name} requires the following dependencies:`, "info");
207
+ dependenciesToInstall.forEach((depKey) => {
208
+ const depExtension = srcExtensionConfig[depKey];
209
+ consoleLog(` • ${depExtension?.name || depKey} (not installed)`, "info");
210
+ });
211
+ consoleLog("\n", "info");
212
+ const estimatedMinutes = missingDeps.length * 5;
213
+ const { confirmInstall } = await prompts({
214
+ type: "confirm",
215
+ name: "confirmInstall",
216
+ message: `Install all ${missingDeps.length} extensions? (~${estimatedMinutes} minutes total)`,
217
+ initial: true
218
+ });
219
+ if (!confirmInstall) {
220
+ consoleLog("Installation aborted.", "info");
221
+ return;
222
+ }
223
+ }
224
+ for (const depKey of missingDeps) if (!installSingleExtension(depKey, srcExtensionConfig, extensionConfig, tmpDir, options.projectDirectory)) hasError = true;
225
+ } finally {
226
+ fs.rmSync(tmpDir, {
227
+ recursive: true,
228
+ force: true
229
+ });
230
+ }
231
+ const originalFiles = fs.readdirSync(path.join(options.projectDirectory, "src"), { recursive: true }).filter((file) => file.toString().endsWith(".original"));
232
+ if (originalFiles.length > 0) {
233
+ consoleLog("\nšŸ“„ The following files were modified. The original files are still available in the same location with the \".original\" extension.:", "info");
234
+ originalFiles.forEach((file) => {
235
+ consoleLog(`- ${file.toString().replace(".original", "")}`, "info");
236
+ });
237
+ }
238
+ if (!hasError) consoleLog("\nšŸš€ Installation completed successfully.", "info");
239
+ };
240
+ const manageExtensions = async (options) => {
241
+ if (options.install && options.uninstall) {
242
+ consoleLog("Please select either install or uninstall, not both.", "error");
243
+ return;
244
+ }
245
+ let operation = options.install ? "install" : options.uninstall ? "uninstall" : void 0;
246
+ const extensionConfig = getExtensionConfig(options.projectDirectory);
247
+ if (operation == null) operation = (await prompts({
248
+ type: "select",
249
+ name: "operation",
250
+ message: "šŸ¤” What would you like to do?",
251
+ choices: [{
252
+ title: "Install extensions",
253
+ value: "install"
254
+ }, {
255
+ title: "Uninstall extensions",
256
+ value: "uninstall"
257
+ }]
258
+ })).operation;
259
+ if (operation === "uninstall") await handleUninstall(extensionConfig, options);
260
+ else await handleInstall(extensionConfig, options);
261
+ };
262
+ const getExtensionMarker = (val) => {
263
+ return `SFDC_EXT_${val.toUpperCase().replaceAll(" ", "_").replaceAll("-", "_")}`;
264
+ };
265
+ const getExtensionFolderName = (val) => {
266
+ return val.toLowerCase().replaceAll(" ", "-").trim();
267
+ };
268
+ const getExtensionNameSchema = (projectDirectory, extensionConfig) => {
269
+ return z.object({ name: z.string().regex(/^[a-zA-Z0-9 _-]+$/, { message: "Extension name can only contain alphanumeric characters, spaces, dashes, or underscores" }) }).superRefine((data, ctx) => {
270
+ if (extensionConfig[getExtensionMarker(data.name)]) ctx.addIssue({
271
+ code: z.ZodIssueCode.custom,
272
+ message: `Extension "${data.name}" already exists`
273
+ });
274
+ if (fs.existsSync(path.join(projectDirectory, ...EXTENSIONS_DIR, getExtensionFolderName(data.name)))) ctx.addIssue({
275
+ code: z.ZodIssueCode.custom,
276
+ message: `Extension directory ${getExtensionFolderName(data.name)} already exists`
277
+ });
278
+ });
279
+ };
280
+ const listExtensions = (options) => {
281
+ const extensionConfig = getExtensionConfig(options.projectDirectory);
282
+ consoleLog("The following extensions are installed:", "info");
283
+ Object.keys(extensionConfig).forEach((key) => {
284
+ consoleLog(`- ${extensionConfig[key].name}: ${extensionConfig[key].description}`, "info");
285
+ });
286
+ };
287
+ const createExtension = async (options) => {
288
+ const { projectDirectory, name, description } = options;
289
+ const extensionConfig = getExtensionConfig(projectDirectory);
290
+ let extensionName = name;
291
+ let extensionDescription = description;
292
+ if (extensionName == null || extensionName.trim() === "") extensionName = (await prompts({
293
+ type: "text",
294
+ name: "extensionName",
295
+ message: "What would you like to name the extension? (e.g., \"My Extension\")"
296
+ })).extensionName;
297
+ const result = getExtensionNameSchema(projectDirectory, extensionConfig).safeParse({ name: extensionName });
298
+ if (!result.success) {
299
+ const firstIssueMessage = result.error.issues?.[0]?.message;
300
+ consoleLog(firstIssueMessage, "error");
301
+ return;
302
+ }
303
+ if (extensionDescription == null || extensionDescription.trim() === "") extensionDescription = (await prompts({
304
+ type: "text",
305
+ name: "extensionDescription",
306
+ message: "How would you describe the extension?"
307
+ })).extensionDescription;
308
+ const folderName = getExtensionFolderName(extensionName);
309
+ const extensionFolderPath = path.join(projectDirectory, ...EXTENSIONS_DIR, folderName);
310
+ fs.mkdirSync(extensionFolderPath, { recursive: true });
311
+ EXTENSION_FOLDERS.forEach((folder) => {
312
+ fs.mkdirSync(path.join(extensionFolderPath, folder), { recursive: true });
313
+ });
314
+ fs.writeFileSync(path.join(extensionFolderPath, "README.md"), `# ${extensionName}\n\n${extensionDescription}`);
315
+ const marker = getExtensionMarker(extensionName);
316
+ extensionConfig[marker] = {
317
+ name: extensionName,
318
+ description: extensionDescription,
319
+ installationInstructions: "",
320
+ uninstallationInstructions: "",
321
+ folder: folderName,
322
+ dependencies: []
323
+ };
324
+ fs.writeFileSync(path.join(projectDirectory, ...CONFIG_PATH), JSON.stringify({ extensions: extensionConfig }, null, 4));
325
+ consoleLog(`Extension "${extensionName}" scaffolding was created successfully.`, "success");
326
+ };
327
+
328
+ //#endregion
329
+ export { listExtensions as n, manageExtensions as r, createExtension as t };