@salesforce/webapp-template-cli-experimental 1.3.3

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 (71) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +1464 -0
  3. package/dist/commands/apply-patches.d.ts +23 -0
  4. package/dist/commands/apply-patches.d.ts.map +1 -0
  5. package/dist/commands/apply-patches.js +650 -0
  6. package/dist/commands/apply-patches.js.map +1 -0
  7. package/dist/commands/new-app-feature.d.ts +7 -0
  8. package/dist/commands/new-app-feature.d.ts.map +1 -0
  9. package/dist/commands/new-app-feature.js +208 -0
  10. package/dist/commands/new-app-feature.js.map +1 -0
  11. package/dist/commands/new-app.d.ts +6 -0
  12. package/dist/commands/new-app.d.ts.map +1 -0
  13. package/dist/commands/new-app.js +7 -0
  14. package/dist/commands/new-app.js.map +1 -0
  15. package/dist/commands/watch-patches.d.ts +4 -0
  16. package/dist/commands/watch-patches.d.ts.map +1 -0
  17. package/dist/commands/watch-patches.js +128 -0
  18. package/dist/commands/watch-patches.js.map +1 -0
  19. package/dist/core/dependency-resolver.d.ts +40 -0
  20. package/dist/core/dependency-resolver.d.ts.map +1 -0
  21. package/dist/core/dependency-resolver.js +126 -0
  22. package/dist/core/dependency-resolver.js.map +1 -0
  23. package/dist/core/file-operations.d.ts +55 -0
  24. package/dist/core/file-operations.d.ts.map +1 -0
  25. package/dist/core/file-operations.js +350 -0
  26. package/dist/core/file-operations.js.map +1 -0
  27. package/dist/core/package-json-merger.d.ts +31 -0
  28. package/dist/core/package-json-merger.d.ts.map +1 -0
  29. package/dist/core/package-json-merger.js +149 -0
  30. package/dist/core/package-json-merger.js.map +1 -0
  31. package/dist/core/patch-loader.d.ts +18 -0
  32. package/dist/core/patch-loader.d.ts.map +1 -0
  33. package/dist/core/patch-loader.js +115 -0
  34. package/dist/core/patch-loader.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +95 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/types.d.ts +28 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +2 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/utils/debounce.d.ts +9 -0
  44. package/dist/utils/debounce.d.ts.map +1 -0
  45. package/dist/utils/debounce.js +20 -0
  46. package/dist/utils/debounce.js.map +1 -0
  47. package/dist/utils/import-merger.d.ts +17 -0
  48. package/dist/utils/import-merger.d.ts.map +1 -0
  49. package/dist/utils/import-merger.js +244 -0
  50. package/dist/utils/import-merger.js.map +1 -0
  51. package/dist/utils/logger.d.ts +6 -0
  52. package/dist/utils/logger.d.ts.map +1 -0
  53. package/dist/utils/logger.js +22 -0
  54. package/dist/utils/logger.js.map +1 -0
  55. package/dist/utils/path-mappings.d.ts +94 -0
  56. package/dist/utils/path-mappings.d.ts.map +1 -0
  57. package/dist/utils/path-mappings.js +139 -0
  58. package/dist/utils/path-mappings.js.map +1 -0
  59. package/dist/utils/paths.d.ts +61 -0
  60. package/dist/utils/paths.d.ts.map +1 -0
  61. package/dist/utils/paths.js +178 -0
  62. package/dist/utils/paths.js.map +1 -0
  63. package/dist/utils/route-merger.d.ts +107 -0
  64. package/dist/utils/route-merger.d.ts.map +1 -0
  65. package/dist/utils/route-merger.js +358 -0
  66. package/dist/utils/route-merger.js.map +1 -0
  67. package/dist/utils/validation.d.ts +35 -0
  68. package/dist/utils/validation.d.ts.map +1 -0
  69. package/dist/utils/validation.js +137 -0
  70. package/dist/utils/validation.js.map +1 -0
  71. package/package.json +44 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Apply feature patches to create a new app
3
+ *
4
+ * @param featurePath - Path to the feature directory containing feature.ts
5
+ * @param appPath - Path to the base app directory (used as reference, remains unchanged)
6
+ * @param options - Configuration options
7
+ * @param options.targetDirName - Required. Target directory where feature will be applied
8
+ * @param options.skipDependencyChanges - Optional. Skip npm dependency installation
9
+ * @param options.reset - Optional. Reset target directory to base app state before applying (preserves node_modules)
10
+ *
11
+ * @example
12
+ * await applyPatchesCommand(
13
+ * 'packages/feature-navigation-menu',
14
+ * 'packages/base-react-app',
15
+ * { targetDirName: 'my-app' }
16
+ * );
17
+ */
18
+ export declare function applyPatchesCommand(featurePath: string, appPath: string, options: {
19
+ targetDirName: string;
20
+ skipDependencyChanges?: boolean;
21
+ reset?: boolean;
22
+ }): Promise<void>;
23
+ //# sourceMappingURL=apply-patches.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-patches.d.ts","sourceRoot":"","sources":["../../src/commands/apply-patches.ts"],"names":[],"mappings":"AAulBA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAClF,OAAO,CAAC,IAAI,CAAC,CAwLf"}
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import { randomBytes } from "crypto";
7
+ import { cpSync, existsSync, rmSync, readdirSync, mkdirSync, readFileSync } from "fs";
8
+ import { tmpdir } from "os";
9
+ import { join, relative, basename, dirname, isAbsolute } from "path";
10
+ import { resolveDependencies } from "../core/dependency-resolver.js";
11
+ import { applyFile, copySiteContainerTemplate, deleteFile, prependToFile, appendToFile, processWebApplicationMetaFile, } from "../core/file-operations.js";
12
+ import { mergePackageJson } from "../core/package-json-merger.js";
13
+ import * as logger from "../utils/logger.js";
14
+ import { getEffectivePathMappings, applyPathMappings } from "../utils/path-mappings.js";
15
+ import { getMonorepoRoot, getSfdxProjectTemplatePath, getSiteContainerTemplatePath, translatePathToBaseApp, translatePathToTargetApp, } from "../utils/paths.js";
16
+ import { validateAndResolveFeaturePath, validateAndResolveAppPath, getWebApplicationPath, } from "../utils/validation.js";
17
+ /**
18
+ * Recursively discover all files in a directory
19
+ */
20
+ function discoverFiles(dir, baseDir = dir) {
21
+ const files = [];
22
+ const entries = readdirSync(dir, { withFileTypes: true });
23
+ for (const entry of entries) {
24
+ const fullPath = join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ files.push(...discoverFiles(fullPath, baseDir));
27
+ }
28
+ else if (entry.isFile()) {
29
+ // Get relative path from baseDir
30
+ files.push(relative(baseDir, fullPath));
31
+ }
32
+ }
33
+ return files;
34
+ }
35
+ const WEBAPPLICATION_META_PATTERN = /^_webapplication\.webapplication-meta\.xml$/i;
36
+ /**
37
+ * Process the webapplication meta file in the target app dir after a base copy:
38
+ * replace <%= masterLabel %> with app name and rename _webapplication.*-meta.xml to <appName>.webapplication-meta.xml.
39
+ */
40
+ function processWebApplicationMetaInDir(targetAppDir, appName) {
41
+ if (!existsSync(targetAppDir))
42
+ return;
43
+ const entries = readdirSync(targetAppDir, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ if (entry.isFile() && WEBAPPLICATION_META_PATTERN.test(entry.name)) {
46
+ processWebApplicationMetaFile(join(targetAppDir, entry.name), appName);
47
+ return;
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Reset target directory to match base app state (excluding node_modules).
53
+ * This syncs the target directory with the base app before applying features.
54
+ */
55
+ function resetTargetToBaseApp(targetDir, baseAppPath, targetAppName) {
56
+ const targetAppDir = join(targetDir, "force-app", "main", "default", "webapplications", targetAppName);
57
+ const sourceWebAppPath = getWebApplicationPath(baseAppPath);
58
+ // Helper to check if path contains node_modules
59
+ const isNodeModulesPath = (path) => {
60
+ return path.split("/").includes("node_modules");
61
+ };
62
+ // Step 1: Remove files/directories from target that don't exist in base app
63
+ const removeExtraFiles = (targetPath, sourcePath) => {
64
+ if (!existsSync(targetPath))
65
+ return;
66
+ const entries = readdirSync(targetPath, { withFileTypes: true });
67
+ for (const entry of entries) {
68
+ const targetEntryPath = join(targetPath, entry.name);
69
+ const sourceEntryPath = join(sourcePath, entry.name);
70
+ const relativeFromTarget = relative(targetAppDir, targetEntryPath);
71
+ // Skip node_modules
72
+ if (isNodeModulesPath(relativeFromTarget)) {
73
+ continue;
74
+ }
75
+ if (!existsSync(sourceEntryPath)) {
76
+ // Doesn't exist in base app, remove it
77
+ rmSync(targetEntryPath, { recursive: true, force: true });
78
+ const suffix = entry.isDirectory() ? "/" : "";
79
+ logger.info(`Removed: ${relativeFromTarget}${suffix}`);
80
+ }
81
+ else if (entry.isDirectory()) {
82
+ // Recurse into directory
83
+ removeExtraFiles(targetEntryPath, sourceEntryPath);
84
+ }
85
+ }
86
+ };
87
+ // Step 2: Copy/update files from base app to target
88
+ const syncFiles = (sourcePath, targetPath) => {
89
+ if (!existsSync(sourcePath))
90
+ return;
91
+ const entries = readdirSync(sourcePath, { withFileTypes: true });
92
+ for (const entry of entries) {
93
+ const sourceEntryPath = join(sourcePath, entry.name);
94
+ const targetEntryPath = join(targetPath, entry.name);
95
+ const relativeFromTarget = relative(targetAppDir, targetEntryPath);
96
+ // Skip node_modules
97
+ if (isNodeModulesPath(relativeFromTarget)) {
98
+ continue;
99
+ }
100
+ if (entry.isDirectory()) {
101
+ // Ensure directory exists in target
102
+ if (!existsSync(targetEntryPath)) {
103
+ mkdirSync(targetEntryPath, { recursive: true });
104
+ }
105
+ // Recurse into directory
106
+ syncFiles(sourceEntryPath, targetEntryPath);
107
+ }
108
+ else if (entry.isFile()) {
109
+ // Copy file if it doesn't exist or is different
110
+ let shouldCopy = false;
111
+ if (!existsSync(targetEntryPath)) {
112
+ shouldCopy = true;
113
+ }
114
+ else {
115
+ // Compare file contents
116
+ const sourceContent = readFileSync(sourceEntryPath);
117
+ const targetContent = readFileSync(targetEntryPath);
118
+ shouldCopy = !sourceContent.equals(targetContent);
119
+ }
120
+ if (shouldCopy) {
121
+ cpSync(sourceEntryPath, targetEntryPath);
122
+ logger.info(`Synced: ${relativeFromTarget}`);
123
+ }
124
+ }
125
+ }
126
+ };
127
+ logger.info("Resetting target directory to base app state...");
128
+ removeExtraFiles(targetAppDir, sourceWebAppPath);
129
+ syncFiles(sourceWebAppPath, targetAppDir);
130
+ processWebApplicationMetaInDir(targetAppDir, targetAppName);
131
+ logger.success("Target directory reset complete");
132
+ }
133
+ const DELETE_PREFIX = "__delete__";
134
+ const INHERIT_PREFIX = "__inherit__";
135
+ const PREPEND_PREFIX = "__prepend__";
136
+ const APPEND_PREFIX = "__append__";
137
+ /**
138
+ * Check if a path component has the delete prefix and return the actual path
139
+ * Returns null if no delete prefix is found, otherwise returns the actual path
140
+ *
141
+ * Examples:
142
+ * - "src/__delete__routes.tsx" -> "src/routes.tsx"
143
+ * - "src/__delete__pages" -> "src/pages"
144
+ * - "__delete__src/routes.tsx" -> "src/routes.tsx"
145
+ * - "src/routes.tsx" -> null (no prefix)
146
+ */
147
+ function extractDeletePath(relativePath) {
148
+ const parts = relativePath.split("/");
149
+ let hasDeletePrefix = false;
150
+ const transformedParts = parts.map((part) => {
151
+ if (part.startsWith(DELETE_PREFIX)) {
152
+ hasDeletePrefix = true;
153
+ return part.substring(DELETE_PREFIX.length);
154
+ }
155
+ return part;
156
+ });
157
+ return hasDeletePrefix ? transformedParts.join("/") : null;
158
+ }
159
+ /**
160
+ * Check if a path component has the inherit prefix and return the actual path
161
+ * Returns null if no inherit prefix is found, otherwise returns the actual path
162
+ *
163
+ * Examples:
164
+ * - "src/__inherit__routes.tsx" -> "src/routes.tsx"
165
+ * - "__inherit__src/routes.tsx" -> "src/routes.tsx"
166
+ * - "src/routes.tsx" -> null (no prefix)
167
+ */
168
+ function extractInheritPath(relativePath) {
169
+ const parts = relativePath.split("/");
170
+ let hasInheritPrefix = false;
171
+ const transformedParts = parts.map((part) => {
172
+ if (part.startsWith(INHERIT_PREFIX)) {
173
+ hasInheritPrefix = true;
174
+ return part.substring(INHERIT_PREFIX.length);
175
+ }
176
+ return part;
177
+ });
178
+ return hasInheritPrefix ? transformedParts.join("/") : null;
179
+ }
180
+ /**
181
+ * Check if a path component has the prepend prefix and return the actual path
182
+ * Returns null if no prepend prefix is found, otherwise returns the actual path
183
+ *
184
+ * Examples:
185
+ * - "src/styles/__prepend__global.css" -> "src/styles/global.css"
186
+ * - "__prepend__file.txt" -> "file.txt"
187
+ * - "src/file.tsx" -> null (no prefix)
188
+ */
189
+ function extractPrependPath(relativePath) {
190
+ const parts = relativePath.split("/");
191
+ let hasPrependPrefix = false;
192
+ const transformedParts = parts.map((part) => {
193
+ if (part.startsWith(PREPEND_PREFIX)) {
194
+ hasPrependPrefix = true;
195
+ return part.substring(PREPEND_PREFIX.length);
196
+ }
197
+ return part;
198
+ });
199
+ return hasPrependPrefix ? transformedParts.join("/") : null;
200
+ }
201
+ /**
202
+ * Check if a path component has the append prefix and return the actual path
203
+ * Returns null if no append prefix is found, otherwise returns the actual path
204
+ *
205
+ * Examples:
206
+ * - "src/styles/__append__global.css" -> "src/styles/global.css"
207
+ * - "__append__file.txt" -> "file.txt"
208
+ * - "src/file.tsx" -> null (no prefix)
209
+ */
210
+ function extractAppendPath(relativePath) {
211
+ const parts = relativePath.split("/");
212
+ let hasAppendPrefix = false;
213
+ const transformedParts = parts.map((part) => {
214
+ if (part.startsWith(APPEND_PREFIX)) {
215
+ hasAppendPrefix = true;
216
+ return part.substring(APPEND_PREFIX.length);
217
+ }
218
+ return part;
219
+ });
220
+ return hasAppendPrefix ? transformedParts.join("/") : null;
221
+ }
222
+ /**
223
+ * Apply a single feature's files to the target directory
224
+ * This is the core application logic extracted for reuse
225
+ */
226
+ async function applySingleFeature(resolvedFeature, baseAppPath, targetDir, targetAppName) {
227
+ const { featureDir, config, featurePath } = resolvedFeature;
228
+ const { templateDir, routeFilePath, pathMappings } = config;
229
+ // Extract feature name for path translation
230
+ const featureName = basename(featurePath);
231
+ // Get effective path mappings for this feature
232
+ const effectiveMappings = getEffectivePathMappings(featureName, pathMappings);
233
+ // Discover and apply files from feature's template directory
234
+ const templatePath = join(featureDir, templateDir);
235
+ if (!existsSync(templatePath)) {
236
+ throw new Error(`Template directory '${templateDir}' does not exist in feature path '${featurePath}'`);
237
+ }
238
+ logger.heading(`\nApplying: ${featurePath}`);
239
+ logger.info("Discovering files...");
240
+ const discoveredFiles = discoverFiles(templatePath);
241
+ if (discoveredFiles.length === 0) {
242
+ throw new Error(`Feature '${featurePath}' has no files in ${templateDir} directory`);
243
+ }
244
+ logger.info(`Found ${discoveredFiles.length} file(s)`);
245
+ // Validate that there are no conflicting paths and that base files exist
246
+ logger.info("Validating paths...");
247
+ const normalizedPaths = new Map();
248
+ const deleteFiles = [];
249
+ const inheritFiles = [];
250
+ const prependFiles = [];
251
+ const appendFiles = [];
252
+ for (const relativePath of discoveredFiles) {
253
+ // Apply path mappings to transform template path
254
+ const mappedPath = applyPathMappings(relativePath, effectiveMappings);
255
+ const deleteTargetPath = extractDeletePath(mappedPath);
256
+ const inheritTargetPath = extractInheritPath(mappedPath);
257
+ const prependTargetPath = extractPrependPath(mappedPath);
258
+ const appendTargetPath = extractAppendPath(mappedPath);
259
+ let normalizedPath;
260
+ let type;
261
+ if (deleteTargetPath) {
262
+ normalizedPath = deleteTargetPath;
263
+ type = "delete";
264
+ deleteFiles.push(deleteTargetPath);
265
+ }
266
+ else if (inheritTargetPath) {
267
+ normalizedPath = inheritTargetPath;
268
+ type = "inherit";
269
+ inheritFiles.push(inheritTargetPath);
270
+ }
271
+ else if (prependTargetPath) {
272
+ normalizedPath = prependTargetPath;
273
+ type = "prepend";
274
+ prependFiles.push(prependTargetPath);
275
+ }
276
+ else if (appendTargetPath) {
277
+ normalizedPath = appendTargetPath;
278
+ type = "append";
279
+ appendFiles.push(appendTargetPath);
280
+ }
281
+ else {
282
+ normalizedPath = mappedPath;
283
+ type = "normal";
284
+ }
285
+ // Check for conflicts
286
+ if (normalizedPaths.has(normalizedPath)) {
287
+ const existing = normalizedPaths.get(normalizedPath);
288
+ logger.error("\nPath conflict detected!\n");
289
+ logger.info(`The following paths resolve to the same target file:`);
290
+ logger.info(` 1. ${existing.path} (${existing.type})`);
291
+ logger.info(` 2. ${relativePath} (${type})`);
292
+ logger.info(` → Both target: ${normalizedPath}\n`);
293
+ logger.warning("You cannot have multiple files targeting the same path.");
294
+ logger.info("Please remove one of these files from the template directory.\n");
295
+ throw new Error("Path conflict - cannot proceed");
296
+ }
297
+ normalizedPaths.set(normalizedPath, { path: relativePath, type });
298
+ }
299
+ // Validate that files marked for deletion exist
300
+ // Note: Files may exist in target (from dependencies) or will be inherited from base app
301
+ for (const deleteFile of deleteFiles) {
302
+ // Translate feature path to target app path
303
+ const targetPath = translatePathToTargetApp(deleteFile, featureName, targetAppName);
304
+ const targetFilePath = join(targetDir, targetPath);
305
+ // If file doesn't exist in target yet, check if it will be copied from base app during deletion
306
+ if (!existsSync(targetFilePath)) {
307
+ // Check if file exists in base app (will be inherited during delete operation)
308
+ const translatedPath = translatePathToBaseApp(deleteFile, featureName, baseAppPath);
309
+ const baseFilePath = join(baseAppPath, translatedPath);
310
+ if (!existsSync(baseFilePath)) {
311
+ logger.error(`\nValidation error: Cannot delete file that doesn't exist!\n`);
312
+ logger.info(`File marked for deletion: ${deleteFile}`);
313
+ logger.info(`Not found in target: ${targetFilePath}`);
314
+ logger.info(`Not found in base app: ${baseFilePath}`);
315
+ logger.warning(`The file must exist in target or base app to be deleted.\n`);
316
+ throw new Error("Delete validation failed - cannot proceed");
317
+ }
318
+ }
319
+ }
320
+ // Validate that files marked for inheritance exist in base app
321
+ for (const inheritFile of inheritFiles) {
322
+ // Translate feature path to target app path
323
+ const targetPath = translatePathToTargetApp(inheritFile, featureName, targetAppName);
324
+ const targetFilePath = join(targetDir, targetPath);
325
+ // If file doesn't exist in target yet, check if it will be copied from base app during inheritance
326
+ if (!existsSync(targetFilePath)) {
327
+ // Translate feature path to base app path (we inherit from base app)
328
+ const translatedPath = translatePathToBaseApp(inheritFile, featureName, baseAppPath);
329
+ const baseFilePath = join(baseAppPath, translatedPath);
330
+ if (!existsSync(baseFilePath)) {
331
+ logger.error(`\nValidation error: Cannot inherit file that doesn't exist!\n`);
332
+ logger.info(`File marked for inheritance: ${inheritFile}`);
333
+ logger.info(`Translated to: ${translatedPath}`);
334
+ logger.info(`Expected location in base app: ${baseFilePath}`);
335
+ logger.warning(`The file doesn't exist in the base app.\n`);
336
+ throw new Error("Inherit validation failed - cannot proceed");
337
+ }
338
+ }
339
+ }
340
+ // Validate that files marked for prepending exist in base app
341
+ for (const prependFile of prependFiles) {
342
+ // Translate feature path to base app path (we prepend to base app files)
343
+ const translatedPath = translatePathToBaseApp(prependFile, featureName, baseAppPath);
344
+ const baseFilePath = join(baseAppPath, translatedPath);
345
+ if (!existsSync(baseFilePath)) {
346
+ logger.error(`\nValidation error: Cannot prepend to file that doesn't exist!\n`);
347
+ logger.info(`File marked for prepending: ${prependFile}`);
348
+ logger.info(`Translated to: ${translatedPath}`);
349
+ logger.info(`Expected location in base app: ${baseFilePath}`);
350
+ logger.warning(`The file doesn't exist in the base app.\n`);
351
+ throw new Error("Prepend validation failed - cannot proceed");
352
+ }
353
+ }
354
+ // Validate that files marked for appending exist in base app
355
+ for (const appendFile of appendFiles) {
356
+ // Translate feature path to base app path (we append to base app files)
357
+ const translatedPath = translatePathToBaseApp(appendFile, featureName, baseAppPath);
358
+ const baseFilePath = join(baseAppPath, translatedPath);
359
+ if (!existsSync(baseFilePath)) {
360
+ logger.error(`\nValidation error: Cannot append to file that doesn't exist!\n`);
361
+ logger.info(`File marked for appending: ${appendFile}`);
362
+ logger.info(`Translated to: ${translatedPath}`);
363
+ logger.info(`Expected location in base app: ${baseFilePath}`);
364
+ logger.warning(`The file doesn't exist in the base app.\n`);
365
+ throw new Error("Append validation failed - cannot proceed");
366
+ }
367
+ }
368
+ logger.success("Paths validated");
369
+ // Apply each discovered file
370
+ for (const relativePath of discoveredFiles) {
371
+ // Apply path mappings to transform template path
372
+ const mappedPath = applyPathMappings(relativePath, effectiveMappings);
373
+ // Translate feature path to base app path (for finding base files)
374
+ const translatedPath = translatePathToBaseApp(mappedPath, featureName, baseAppPath);
375
+ // Translate feature path to target app path (for writing files)
376
+ const targetPath = translatePathToTargetApp(mappedPath, featureName, targetAppName);
377
+ // Check if this is a special operation
378
+ const deleteTargetPath = extractDeletePath(mappedPath);
379
+ const inheritTargetPath = extractInheritPath(mappedPath);
380
+ const prependTargetPath = extractPrependPath(mappedPath);
381
+ const appendTargetPath = extractAppendPath(mappedPath);
382
+ if (deleteTargetPath) {
383
+ // This is a delete operation - delete the target file/directory
384
+ const targetDeletePath = translatePathToTargetApp(deleteTargetPath, featureName, targetAppName);
385
+ const targetFilePath = join(targetDir, targetDeletePath);
386
+ await deleteFile(targetFilePath, targetDeletePath);
387
+ }
388
+ else if (inheritTargetPath) {
389
+ // This is an inherit operation - copy from base app if target doesn't have it
390
+ const targetInheritPath = translatePathToTargetApp(inheritTargetPath, featureName, targetAppName);
391
+ const targetFilePath = join(targetDir, targetInheritPath);
392
+ if (!existsSync(targetFilePath)) {
393
+ // Copy from base app to preserve inherited file in new target structure
394
+ const translatedInheritPath = translatePathToBaseApp(inheritTargetPath, featureName, baseAppPath);
395
+ const baseFilePath = join(baseAppPath, translatedInheritPath);
396
+ if (existsSync(baseFilePath)) {
397
+ const targetFileDir = dirname(targetFilePath);
398
+ if (!existsSync(targetFileDir)) {
399
+ mkdirSync(targetFileDir, { recursive: true });
400
+ }
401
+ cpSync(baseFilePath, targetFilePath);
402
+ logger.info(`Copied ${targetInheritPath} (inherited from base app)`);
403
+ }
404
+ else {
405
+ logger.info(`Skipped ${targetInheritPath} (file not in base app)`);
406
+ }
407
+ }
408
+ else {
409
+ logger.info(`Skipped ${targetInheritPath} (already exists in target)`);
410
+ }
411
+ }
412
+ else if (prependTargetPath) {
413
+ // This is a prepend operation - ensure target file exists first
414
+ const featureFilePath = join(templatePath, relativePath);
415
+ const targetPrependPath = translatePathToTargetApp(prependTargetPath, featureName, targetAppName);
416
+ const targetFilePath = join(targetDir, targetPrependPath);
417
+ // Copy from base app if target doesn't have the file yet
418
+ if (!existsSync(targetFilePath)) {
419
+ const translatedPrependPath = translatePathToBaseApp(prependTargetPath, featureName, baseAppPath);
420
+ const baseFilePath = join(baseAppPath, translatedPrependPath);
421
+ if (existsSync(baseFilePath)) {
422
+ const targetFileDir = dirname(targetFilePath);
423
+ if (!existsSync(targetFileDir)) {
424
+ mkdirSync(targetFileDir, { recursive: true });
425
+ }
426
+ cpSync(baseFilePath, targetFilePath);
427
+ }
428
+ }
429
+ await prependToFile(featureFilePath, targetFilePath, targetPrependPath);
430
+ }
431
+ else if (appendTargetPath) {
432
+ // This is an append operation - ensure target file exists first
433
+ const featureFilePath = join(templatePath, relativePath);
434
+ const targetAppendPath = translatePathToTargetApp(appendTargetPath, featureName, targetAppName);
435
+ const targetFilePath = join(targetDir, targetAppendPath);
436
+ // Copy from base app if target doesn't have the file yet
437
+ if (!existsSync(targetFilePath)) {
438
+ const translatedAppendPath = translatePathToBaseApp(appendTargetPath, featureName, baseAppPath);
439
+ const baseFilePath = join(baseAppPath, translatedAppendPath);
440
+ if (existsSync(baseFilePath)) {
441
+ const targetFileDir = dirname(targetFilePath);
442
+ if (!existsSync(targetFileDir)) {
443
+ mkdirSync(targetFileDir, { recursive: true });
444
+ }
445
+ cpSync(baseFilePath, targetFilePath);
446
+ }
447
+ }
448
+ await appendToFile(featureFilePath, targetFilePath, targetAppendPath);
449
+ }
450
+ else {
451
+ // Normal file operation - add/update/merge
452
+ const patchFilePath = join(templatePath, relativePath);
453
+ // Use target app path to ensure all features write to same location
454
+ const targetFilePath = join(targetDir, targetPath);
455
+ // Determine if this should be merged (only for the routes file)
456
+ // The routeFilePath from config includes the full path from template root
457
+ const shouldMerge = mappedPath === routeFilePath;
458
+ // For route merging with dependencies:
459
+ // - Use target file as base if it exists (accumulates routes from previous features)
460
+ // - Otherwise use base app file (first feature merges with base app)
461
+ // Create a temp copy of target to avoid ts-morph issues with same file as base and target
462
+ let baseFilePath = join(baseAppPath, translatedPath);
463
+ let tempFilePath;
464
+ if (shouldMerge && existsSync(targetFilePath)) {
465
+ // Create temp copy of target to use as base
466
+ tempFilePath = join(tmpdir(), `route-merge-${randomBytes(8).toString("hex")}.tsx`);
467
+ cpSync(targetFilePath, tempFilePath);
468
+ baseFilePath = tempFilePath;
469
+ }
470
+ try {
471
+ await applyFile(patchFilePath, baseFilePath, targetFilePath, targetPath, shouldMerge, targetAppName);
472
+ }
473
+ finally {
474
+ // Clean up temp file
475
+ if (tempFilePath && existsSync(tempFilePath)) {
476
+ rmSync(tempFilePath, { force: true });
477
+ }
478
+ }
479
+ }
480
+ }
481
+ }
482
+ /**
483
+ * Apply feature patches to create a new app
484
+ *
485
+ * @param featurePath - Path to the feature directory containing feature.ts
486
+ * @param appPath - Path to the base app directory (used as reference, remains unchanged)
487
+ * @param options - Configuration options
488
+ * @param options.targetDirName - Required. Target directory where feature will be applied
489
+ * @param options.skipDependencyChanges - Optional. Skip npm dependency installation
490
+ * @param options.reset - Optional. Reset target directory to base app state before applying (preserves node_modules)
491
+ *
492
+ * @example
493
+ * await applyPatchesCommand(
494
+ * 'packages/feature-navigation-menu',
495
+ * 'packages/base-react-app',
496
+ * { targetDirName: 'my-app' }
497
+ * );
498
+ */
499
+ export async function applyPatchesCommand(featurePath, appPath, options) {
500
+ try {
501
+ logger.heading(`Applying patches: ${featurePath} → ${options.targetDirName}`);
502
+ // Step 1: Validate inputs
503
+ logger.info("Validating paths...");
504
+ validateAndResolveFeaturePath(featurePath); // Fast-fail validation
505
+ const baseAppPath = validateAndResolveAppPath(appPath);
506
+ logger.success("Validation passed");
507
+ // Step 2: Prepare target directory
508
+ // Extract target app name from the main feature path for nested structure
509
+ const mainFeatureName = basename(featurePath);
510
+ const targetAppName = mainFeatureName;
511
+ // Resolve target directory - if absolute, use as-is; otherwise resolve relative to monorepo root
512
+ const resolvedTargetDir = isAbsolute(options.targetDirName)
513
+ ? options.targetDirName
514
+ : join(getMonorepoRoot(), options.targetDirName);
515
+ // Calculate target app directory path (SFDX structure so deploy has meta xml, webapplication.json, etc.)
516
+ const targetAppDir = join(resolvedTargetDir, "force-app", "main", "default", "webapplications", targetAppName);
517
+ if (existsSync(resolvedTargetDir)) {
518
+ if (options.reset) {
519
+ // Reset target to base app state (preserving node_modules)
520
+ resetTargetToBaseApp(resolvedTargetDir, baseAppPath, targetAppName);
521
+ }
522
+ }
523
+ else {
524
+ logger.info(`Creating target directory ${options.targetDirName}...`);
525
+ mkdirSync(resolvedTargetDir, { recursive: true });
526
+ logger.success("Target directory created");
527
+ // Copy SFDX project template base (config/, sfdx-project.json, .forceignore, etc.) into target
528
+ const sfdxTemplatePath = getSfdxProjectTemplatePath();
529
+ if (existsSync(sfdxTemplatePath)) {
530
+ logger.info(`Copying SFDX project template base to ${options.targetDirName}...`);
531
+ cpSync(sfdxTemplatePath, resolvedTargetDir, {
532
+ recursive: true,
533
+ filter: (src) => !src.includes("node_modules"),
534
+ });
535
+ logger.success("SFDX project template base copied");
536
+ }
537
+ // Copy base app to target directory with nested structure
538
+ logger.info(`Copying base app to ${options.targetDirName}...`);
539
+ const sourceWebAppPath = getWebApplicationPath(baseAppPath);
540
+ mkdirSync(dirname(targetAppDir), { recursive: true });
541
+ cpSync(sourceWebAppPath, targetAppDir, { recursive: true });
542
+ processWebApplicationMetaInDir(targetAppDir, targetAppName);
543
+ logger.success("Base app copied");
544
+ }
545
+ const targetDir = resolvedTargetDir;
546
+ // Step 3: Resolve dependencies
547
+ logger.heading("\nResolving Dependencies");
548
+ const { orderedFeatures } = await resolveDependencies(featurePath);
549
+ if (orderedFeatures.length > 1) {
550
+ logger.info(`Resolved dependency chain (${orderedFeatures.length} features):`);
551
+ for (let i = 0; i < orderedFeatures.length; i++) {
552
+ const { featurePath: path } = orderedFeatures[i];
553
+ const isMainFeature = i === orderedFeatures.length - 1;
554
+ const prefix = isMainFeature ? "└─" : "├─";
555
+ const label = isMainFeature ? " (main)" : " (dependency)";
556
+ logger.info(` ${prefix} ${path}${label}`);
557
+ }
558
+ logger.success("Dependency resolution complete");
559
+ }
560
+ else {
561
+ logger.info("No dependencies found");
562
+ }
563
+ // Step 4: Apply all features in dependency order
564
+ for (const resolvedFeature of orderedFeatures) {
565
+ await applySingleFeature(resolvedFeature, baseAppPath, targetDir, targetAppName);
566
+ }
567
+ // Step 4b: Copy site-container metadata from template if main feature has siteContainer
568
+ const mainFeature = orderedFeatures[orderedFeatures.length - 1];
569
+ if (mainFeature?.config.siteContainer) {
570
+ const siteContainerTemplatePath = getSiteContainerTemplatePath();
571
+ const targetForceAppDefault = join(targetDir, "force-app", "main", "default");
572
+ if (existsSync(siteContainerTemplatePath)) {
573
+ logger.info("Copying site-container metadata from template-site-container...");
574
+ copySiteContainerTemplate(siteContainerTemplatePath, targetForceAppDefault, targetAppName);
575
+ logger.success("Site-container metadata applied");
576
+ }
577
+ }
578
+ // Step 5: Aggregate and install all dependencies once
579
+ if (options.skipDependencyChanges) {
580
+ logger.info("\n⏭ Skipping dependency installation");
581
+ }
582
+ else {
583
+ // Aggregate all dependencies from all resolved features, checking for conflicts
584
+ const aggregatedDependencies = {};
585
+ const aggregatedDevDependencies = {};
586
+ // Collect all features from all resolved features
587
+ const allFeatures = orderedFeatures.flatMap((rf) => rf.config.features);
588
+ // First pass: collect all regular dependencies
589
+ for (const feature of allFeatures) {
590
+ if (feature.packageJson?.dependencies) {
591
+ for (const [name, version] of Object.entries(feature.packageJson.dependencies)) {
592
+ if (aggregatedDependencies[name] && aggregatedDependencies[name] !== version) {
593
+ throw new Error(`Dependency version conflict for '${name}': feature requires '${version}' but another feature requires '${aggregatedDependencies[name]}'`);
594
+ }
595
+ aggregatedDependencies[name] = version;
596
+ }
597
+ }
598
+ }
599
+ // Second pass: collect dev dependencies (skip if already in dependencies)
600
+ for (const feature of allFeatures) {
601
+ if (feature.packageJson?.devDependencies) {
602
+ for (const [name, version] of Object.entries(feature.packageJson.devDependencies)) {
603
+ // Skip if already in regular dependencies
604
+ if (aggregatedDependencies[name]) {
605
+ continue;
606
+ }
607
+ if (aggregatedDevDependencies[name] && aggregatedDevDependencies[name] !== version) {
608
+ throw new Error(`Dev dependency version conflict for '${name}': feature requires '${version}' but another feature requires '${aggregatedDevDependencies[name]}'`);
609
+ }
610
+ aggregatedDevDependencies[name] = version;
611
+ }
612
+ }
613
+ }
614
+ const hasAnyDependencies = Object.keys(aggregatedDependencies).length > 0 ||
615
+ Object.keys(aggregatedDevDependencies).length > 0;
616
+ if (hasAnyDependencies) {
617
+ logger.heading("\nInstalling Dependencies");
618
+ await mergePackageJson(targetDir, {
619
+ dependencies: Object.keys(aggregatedDependencies).length > 0 ? aggregatedDependencies : undefined,
620
+ devDependencies: Object.keys(aggregatedDevDependencies).length > 0
621
+ ? aggregatedDevDependencies
622
+ : undefined,
623
+ }, targetAppDir);
624
+ }
625
+ else {
626
+ logger.info("\n✓ No dependencies to install");
627
+ }
628
+ }
629
+ // Step 6: Success message
630
+ logger.heading("\n✓ Success");
631
+ logger.success(`Created: ${targetDir}`);
632
+ }
633
+ catch (err) {
634
+ // Only log error if it's not already been logged (check for our custom error messages)
635
+ const errorMessage = err instanceof Error ? err.message : String(err);
636
+ const skipLogging = [
637
+ "Path conflict - cannot proceed",
638
+ "Delete validation failed - cannot proceed",
639
+ "Inherit validation failed - cannot proceed",
640
+ "Prepend validation failed - cannot proceed",
641
+ "Append validation failed - cannot proceed",
642
+ "Route deletion failed - cannot proceed",
643
+ ].includes(errorMessage);
644
+ if (!skipLogging) {
645
+ logger.error(`Failed to apply feature: ${errorMessage}`);
646
+ }
647
+ throw err;
648
+ }
649
+ }
650
+ //# sourceMappingURL=apply-patches.js.map