@salesforce/ui-bundle-template-cli 1.117.2

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 (92) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +1613 -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 +804 -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 +215 -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 +134 -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 +19 -0
  28. package/dist/core/package-json-merger.d.ts.map +1 -0
  29. package/dist/core/package-json-merger.js +104 -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/nx-plugin/executors/apply-patches/executor.d.ts +20 -0
  40. package/dist/nx-plugin/executors/apply-patches/executor.d.ts.map +1 -0
  41. package/dist/nx-plugin/executors/apply-patches/executor.js +77 -0
  42. package/dist/nx-plugin/executors/apply-patches/executor.js.map +1 -0
  43. package/dist/nx-plugin/executors/build-dist-app/executor.d.ts +20 -0
  44. package/dist/nx-plugin/executors/build-dist-app/executor.d.ts.map +1 -0
  45. package/dist/nx-plugin/executors/build-dist-app/executor.js +88 -0
  46. package/dist/nx-plugin/executors/build-dist-app/executor.js.map +1 -0
  47. package/dist/nx-plugin/executors/dev-server/executor.d.ts +20 -0
  48. package/dist/nx-plugin/executors/dev-server/executor.d.ts.map +1 -0
  49. package/dist/nx-plugin/executors/dev-server/executor.js +59 -0
  50. package/dist/nx-plugin/executors/dev-server/executor.js.map +1 -0
  51. package/dist/nx-plugin/executors/utils.d.ts +9 -0
  52. package/dist/nx-plugin/executors/utils.d.ts.map +1 -0
  53. package/dist/nx-plugin/executors/utils.js +25 -0
  54. package/dist/nx-plugin/executors/utils.js.map +1 -0
  55. package/dist/nx-plugin/executors/watch-patches/executor.d.ts +20 -0
  56. package/dist/nx-plugin/executors/watch-patches/executor.d.ts.map +1 -0
  57. package/dist/nx-plugin/executors/watch-patches/executor.js +71 -0
  58. package/dist/nx-plugin/executors/watch-patches/executor.js.map +1 -0
  59. package/dist/types.d.ts +28 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/utils/debounce.d.ts +9 -0
  64. package/dist/utils/debounce.d.ts.map +1 -0
  65. package/dist/utils/debounce.js +20 -0
  66. package/dist/utils/debounce.js.map +1 -0
  67. package/dist/utils/import-merger.d.ts +17 -0
  68. package/dist/utils/import-merger.d.ts.map +1 -0
  69. package/dist/utils/import-merger.js +244 -0
  70. package/dist/utils/import-merger.js.map +1 -0
  71. package/dist/utils/logger.d.ts +7 -0
  72. package/dist/utils/logger.d.ts.map +1 -0
  73. package/dist/utils/logger.js +23 -0
  74. package/dist/utils/logger.js.map +1 -0
  75. package/dist/utils/path-mappings.d.ts +94 -0
  76. package/dist/utils/path-mappings.d.ts.map +1 -0
  77. package/dist/utils/path-mappings.js +139 -0
  78. package/dist/utils/path-mappings.js.map +1 -0
  79. package/dist/utils/paths.d.ts +61 -0
  80. package/dist/utils/paths.d.ts.map +1 -0
  81. package/dist/utils/paths.js +182 -0
  82. package/dist/utils/paths.js.map +1 -0
  83. package/dist/utils/route-merger.d.ts +107 -0
  84. package/dist/utils/route-merger.d.ts.map +1 -0
  85. package/dist/utils/route-merger.js +358 -0
  86. package/dist/utils/route-merger.js.map +1 -0
  87. package/dist/utils/validation.d.ts +35 -0
  88. package/dist/utils/validation.d.ts.map +1 -0
  89. package/dist/utils/validation.js +137 -0
  90. package/dist/utils/validation.js.map +1 -0
  91. package/executors.json +24 -0
  92. package/package.json +48 -0
@@ -0,0 +1,804 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import { execSync } from "child_process";
7
+ import { randomBytes } from "crypto";
8
+ import { cpSync, existsSync, rmSync, readdirSync, renameSync, mkdirSync, readFileSync, writeFileSync, } from "fs";
9
+ import { tmpdir } from "os";
10
+ import { join, relative, basename, dirname, isAbsolute } from "path";
11
+ import { resolveDependencies } from "../core/dependency-resolver";
12
+ import { applyFile, copySiteContainerTemplate, deleteFile, prependToFile, appendToFile, processUIBundleMetaFile, } from "../core/file-operations";
13
+ import { mergePackageJson } from "../core/package-json-merger";
14
+ import * as logger from "../utils/logger";
15
+ import { getEffectivePathMappings, applyPathMappings } from "../utils/path-mappings";
16
+ import { getMonorepoRoot, getSfdxProjectTemplatePath, getSiteContainerTemplatePath, getFeaturesDir, translatePathToBaseApp, translatePathToTargetApp, } from "../utils/paths";
17
+ import { validateAndResolveFeaturePath, validateAndResolveAppPath, getUIBundlePath, } from "../utils/validation";
18
+ /**
19
+ * Recursively discover all files in a directory
20
+ */
21
+ function discoverFiles(dir, baseDir = dir) {
22
+ const files = [];
23
+ const entries = readdirSync(dir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ const fullPath = join(dir, entry.name);
26
+ if (entry.isDirectory()) {
27
+ files.push(...discoverFiles(fullPath, baseDir));
28
+ }
29
+ else if (entry.isFile()) {
30
+ // Get relative path from baseDir
31
+ files.push(relative(baseDir, fullPath));
32
+ }
33
+ }
34
+ return files;
35
+ }
36
+ const UIBUNDLE_META_PATTERN = /^_uibundle\.uibundle-meta\.xml$/i;
37
+ /**
38
+ * Rewrite workspace package dependencies in the dist app's package.json to use
39
+ * local file: paths so `npm install` resolves them without the npm registry.
40
+ * Only rewrites packages that exist as local workspace directories.
41
+ */
42
+ function rewriteWorkspaceDepsToLocalPaths(targetAppDir, monorepoRoot) {
43
+ const pkgJsonPath = join(targetAppDir, "package.json");
44
+ if (!existsSync(pkgJsonPath))
45
+ return;
46
+ const localPackageMap = {
47
+ "@salesforce/ui-bundle": join(monorepoRoot, "packages", "ui-bundle"),
48
+ "@salesforce/vite-plugin-ui-bundle": join(monorepoRoot, "packages", "vite-plugin-ui-bundle"),
49
+ "@salesforce/sdk-core": join(monorepoRoot, "packages", "sdk", "sdk-core"),
50
+ "@salesforce/sdk-data": join(monorepoRoot, "packages", "sdk", "sdk-data"),
51
+ "@salesforce/sdk-chat": join(monorepoRoot, "packages", "sdk", "sdk-chat"),
52
+ "@salesforce/sdk-view": join(monorepoRoot, "packages", "sdk", "sdk-view"),
53
+ "@salesforce/sdk-lightning": join(monorepoRoot, "packages", "sdk", "sdk-lightning"),
54
+ };
55
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
56
+ let changed = false;
57
+ for (const depType of ["dependencies", "devDependencies"]) {
58
+ const deps = pkgJson[depType];
59
+ if (!deps)
60
+ continue;
61
+ for (const [name, version] of Object.entries(deps)) {
62
+ if (typeof version === "string" && !version.startsWith("file:") && localPackageMap[name]) {
63
+ const localPath = localPackageMap[name];
64
+ if (existsSync(localPath)) {
65
+ deps[name] = `file:${relative(targetAppDir, localPath)}`;
66
+ changed = true;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ if (changed) {
72
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf-8");
73
+ logger.info("Rewrote workspace dependencies to local file: paths");
74
+ }
75
+ }
76
+ /**
77
+ * Process the UI bundle meta file in the target app dir after a base copy:
78
+ * replace <%= masterLabel %> with app name and rename _uibundle.*-meta.xml to <appName>.uibundle-meta.xml.
79
+ */
80
+ function processUIBundleMetaInDir(targetAppDir, appName) {
81
+ if (!existsSync(targetAppDir))
82
+ return;
83
+ const entries = readdirSync(targetAppDir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (entry.isFile() && UIBUNDLE_META_PATTERN.test(entry.name)) {
86
+ processUIBundleMetaFile(join(targetAppDir, entry.name), appName);
87
+ return;
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Reset target directory to match base app state (excluding node_modules).
93
+ * This syncs the target directory with the base app before applying features.
94
+ */
95
+ function resetTargetToBaseApp(targetDir, baseAppPath, targetAppName) {
96
+ const targetAppDir = join(targetDir, "force-app", "main", "default", "uiBundles", targetAppName);
97
+ const sourceUIBundlePath = getUIBundlePath(baseAppPath);
98
+ // Helper to check if path contains node_modules
99
+ const isNodeModulesPath = (path) => {
100
+ return path.split("/").includes("node_modules");
101
+ };
102
+ // Step 1: Remove files/directories from target that don't exist in base app
103
+ const removeExtraFiles = (targetPath, sourcePath) => {
104
+ if (!existsSync(targetPath))
105
+ return;
106
+ const entries = readdirSync(targetPath, { withFileTypes: true });
107
+ for (const entry of entries) {
108
+ const targetEntryPath = join(targetPath, entry.name);
109
+ const sourceEntryPath = join(sourcePath, entry.name);
110
+ const relativeFromTarget = relative(targetAppDir, targetEntryPath);
111
+ // Skip node_modules
112
+ if (isNodeModulesPath(relativeFromTarget)) {
113
+ continue;
114
+ }
115
+ if (!existsSync(sourceEntryPath)) {
116
+ // Doesn't exist in base app, remove it
117
+ rmSync(targetEntryPath, { recursive: true, force: true });
118
+ const suffix = entry.isDirectory() ? "/" : "";
119
+ logger.info(`Removed: ${relativeFromTarget}${suffix}`);
120
+ }
121
+ else if (entry.isDirectory()) {
122
+ // Recurse into directory
123
+ removeExtraFiles(targetEntryPath, sourceEntryPath);
124
+ }
125
+ }
126
+ };
127
+ // Step 2: Copy/update files from base app to target
128
+ const syncFiles = (sourcePath, targetPath) => {
129
+ if (!existsSync(sourcePath))
130
+ return;
131
+ const entries = readdirSync(sourcePath, { withFileTypes: true });
132
+ for (const entry of entries) {
133
+ const sourceEntryPath = join(sourcePath, entry.name);
134
+ const targetEntryPath = join(targetPath, entry.name);
135
+ const relativeFromTarget = relative(targetAppDir, targetEntryPath);
136
+ // Skip node_modules
137
+ if (isNodeModulesPath(relativeFromTarget)) {
138
+ continue;
139
+ }
140
+ if (entry.isDirectory()) {
141
+ // Ensure directory exists in target
142
+ if (!existsSync(targetEntryPath)) {
143
+ mkdirSync(targetEntryPath, { recursive: true });
144
+ }
145
+ // Recurse into directory
146
+ syncFiles(sourceEntryPath, targetEntryPath);
147
+ }
148
+ else if (entry.isFile()) {
149
+ // Copy file if it doesn't exist or is different
150
+ let shouldCopy = false;
151
+ if (!existsSync(targetEntryPath)) {
152
+ shouldCopy = true;
153
+ }
154
+ else {
155
+ // Compare file contents
156
+ const sourceContent = readFileSync(sourceEntryPath);
157
+ const targetContent = readFileSync(targetEntryPath);
158
+ shouldCopy = !sourceContent.equals(targetContent);
159
+ }
160
+ if (shouldCopy) {
161
+ cpSync(sourceEntryPath, targetEntryPath);
162
+ logger.info(`Synced: ${relativeFromTarget}`);
163
+ }
164
+ }
165
+ }
166
+ };
167
+ logger.info("Resetting target directory to base app state...");
168
+ removeExtraFiles(targetAppDir, sourceUIBundlePath);
169
+ syncFiles(sourceUIBundlePath, targetAppDir);
170
+ processUIBundleMetaInDir(targetAppDir, targetAppName);
171
+ logger.success("Target directory reset complete");
172
+ }
173
+ const DELETE_PREFIX = "__delete__";
174
+ const INHERIT_PREFIX = "__inherit__";
175
+ const PREPEND_PREFIX = "__prepend__";
176
+ const APPEND_PREFIX = "__append__";
177
+ /**
178
+ * Check if a path component has the delete prefix and return the actual path
179
+ * Returns null if no delete prefix is found, otherwise returns the actual path
180
+ *
181
+ * Examples:
182
+ * - "src/__delete__routes.tsx" -> "src/routes.tsx"
183
+ * - "src/__delete__pages" -> "src/pages"
184
+ * - "__delete__src/routes.tsx" -> "src/routes.tsx"
185
+ * - "src/routes.tsx" -> null (no prefix)
186
+ */
187
+ function extractDeletePath(relativePath) {
188
+ const parts = relativePath.split("/");
189
+ let hasDeletePrefix = false;
190
+ const transformedParts = parts.map((part) => {
191
+ if (part.startsWith(DELETE_PREFIX)) {
192
+ hasDeletePrefix = true;
193
+ return part.substring(DELETE_PREFIX.length);
194
+ }
195
+ return part;
196
+ });
197
+ return hasDeletePrefix ? transformedParts.join("/") : null;
198
+ }
199
+ /**
200
+ * Check if a path component has the inherit prefix and return the actual path
201
+ * Returns null if no inherit prefix is found, otherwise returns the actual path
202
+ *
203
+ * Examples:
204
+ * - "src/__inherit__routes.tsx" -> "src/routes.tsx"
205
+ * - "__inherit__src/routes.tsx" -> "src/routes.tsx"
206
+ * - "src/routes.tsx" -> null (no prefix)
207
+ */
208
+ function extractInheritPath(relativePath) {
209
+ const parts = relativePath.split("/");
210
+ let hasInheritPrefix = false;
211
+ const transformedParts = parts.map((part) => {
212
+ if (part.startsWith(INHERIT_PREFIX)) {
213
+ hasInheritPrefix = true;
214
+ return part.substring(INHERIT_PREFIX.length);
215
+ }
216
+ return part;
217
+ });
218
+ return hasInheritPrefix ? transformedParts.join("/") : null;
219
+ }
220
+ /**
221
+ * Check if a path component has the prepend prefix and return the actual path
222
+ * Returns null if no prepend prefix is found, otherwise returns the actual path
223
+ *
224
+ * Examples:
225
+ * - "src/styles/__prepend__global.css" -> "src/styles/global.css"
226
+ * - "__prepend__file.txt" -> "file.txt"
227
+ * - "src/file.tsx" -> null (no prefix)
228
+ */
229
+ function extractPrependPath(relativePath) {
230
+ const parts = relativePath.split("/");
231
+ let hasPrependPrefix = false;
232
+ const transformedParts = parts.map((part) => {
233
+ if (part.startsWith(PREPEND_PREFIX)) {
234
+ hasPrependPrefix = true;
235
+ return part.substring(PREPEND_PREFIX.length);
236
+ }
237
+ return part;
238
+ });
239
+ return hasPrependPrefix ? transformedParts.join("/") : null;
240
+ }
241
+ /**
242
+ * Check if a path component has the append prefix and return the actual path
243
+ * Returns null if no append prefix is found, otherwise returns the actual path
244
+ *
245
+ * Examples:
246
+ * - "src/styles/__append__global.css" -> "src/styles/global.css"
247
+ * - "__append__file.txt" -> "file.txt"
248
+ * - "src/file.tsx" -> null (no prefix)
249
+ */
250
+ function extractAppendPath(relativePath) {
251
+ const parts = relativePath.split("/");
252
+ let hasAppendPrefix = false;
253
+ const transformedParts = parts.map((part) => {
254
+ if (part.startsWith(APPEND_PREFIX)) {
255
+ hasAppendPrefix = true;
256
+ return part.substring(APPEND_PREFIX.length);
257
+ }
258
+ return part;
259
+ });
260
+ return hasAppendPrefix ? transformedParts.join("/") : null;
261
+ }
262
+ /**
263
+ * Apply a single patch's files to the target directory
264
+ * This is the core application logic extracted for reuse
265
+ */
266
+ async function applySinglePatch(resolvedPatch, baseAppPath, targetDir, targetAppName) {
267
+ const { patchDir, config, patchPath } = resolvedPatch;
268
+ const { templateDir, routeFilePath, pathMappings } = config;
269
+ // Extract feature name for path translation
270
+ const featureName = basename(patchPath);
271
+ // Get effective path mappings for this feature
272
+ const effectiveMappings = getEffectivePathMappings(featureName, pathMappings);
273
+ // Discover and apply files from feature's template directory
274
+ const templatePath = join(patchDir, templateDir);
275
+ if (!existsSync(templatePath)) {
276
+ throw new Error(`Template directory '${templateDir}' does not exist in feature path '${patchPath}'`);
277
+ }
278
+ logger.heading(`\nApplying: ${patchPath}`);
279
+ logger.info("Discovering files...");
280
+ const discoveredFiles = discoverFiles(templatePath);
281
+ if (discoveredFiles.length === 0) {
282
+ throw new Error(`Feature '${patchPath}' has no files in ${templateDir} directory`);
283
+ }
284
+ logger.info(`Found ${discoveredFiles.length} file(s)`);
285
+ // Validate that there are no conflicting paths and that base files exist
286
+ logger.info("Validating paths...");
287
+ const normalizedPaths = new Map();
288
+ const deleteFiles = [];
289
+ const inheritFiles = [];
290
+ const prependFiles = [];
291
+ const appendFiles = [];
292
+ for (const relativePath of discoveredFiles) {
293
+ // Apply path mappings to transform template path
294
+ const mappedPath = applyPathMappings(relativePath, effectiveMappings);
295
+ const deleteTargetPath = extractDeletePath(mappedPath);
296
+ const inheritTargetPath = extractInheritPath(mappedPath);
297
+ const prependTargetPath = extractPrependPath(mappedPath);
298
+ const appendTargetPath = extractAppendPath(mappedPath);
299
+ let normalizedPath;
300
+ let type;
301
+ if (deleteTargetPath) {
302
+ normalizedPath = deleteTargetPath;
303
+ type = "delete";
304
+ deleteFiles.push(deleteTargetPath);
305
+ }
306
+ else if (inheritTargetPath) {
307
+ normalizedPath = inheritTargetPath;
308
+ type = "inherit";
309
+ inheritFiles.push(inheritTargetPath);
310
+ }
311
+ else if (prependTargetPath) {
312
+ normalizedPath = prependTargetPath;
313
+ type = "prepend";
314
+ prependFiles.push(prependTargetPath);
315
+ }
316
+ else if (appendTargetPath) {
317
+ normalizedPath = appendTargetPath;
318
+ type = "append";
319
+ appendFiles.push(appendTargetPath);
320
+ }
321
+ else {
322
+ normalizedPath = mappedPath;
323
+ type = "normal";
324
+ }
325
+ // Check for conflicts
326
+ if (normalizedPaths.has(normalizedPath)) {
327
+ const existing = normalizedPaths.get(normalizedPath);
328
+ logger.error("\nPath conflict detected!\n");
329
+ logger.info(`The following paths resolve to the same target file:`);
330
+ logger.info(` 1. ${existing.path} (${existing.type})`);
331
+ logger.info(` 2. ${relativePath} (${type})`);
332
+ logger.info(` → Both target: ${normalizedPath}\n`);
333
+ logger.warning("You cannot have multiple files targeting the same path.");
334
+ logger.info("Please remove one of these files from the template directory.\n");
335
+ throw new Error("Path conflict - cannot proceed");
336
+ }
337
+ normalizedPaths.set(normalizedPath, { path: relativePath, type });
338
+ }
339
+ // Validate that files marked for deletion exist
340
+ // Note: Files may exist in target (from dependencies) or will be inherited from base app
341
+ for (const deleteFile of deleteFiles) {
342
+ // Translate feature path to target app path
343
+ const targetPath = translatePathToTargetApp(deleteFile, featureName, targetAppName);
344
+ const targetFilePath = join(targetDir, targetPath);
345
+ // If file doesn't exist in target yet, check if it will be copied from base app during deletion
346
+ if (!existsSync(targetFilePath)) {
347
+ // Check if file exists in base app (will be inherited during delete operation)
348
+ const translatedPath = translatePathToBaseApp(deleteFile, featureName, baseAppPath);
349
+ const baseFilePath = join(baseAppPath, translatedPath);
350
+ if (!existsSync(baseFilePath)) {
351
+ logger.error(`\nValidation error: Cannot delete file that doesn't exist!\n`);
352
+ logger.info(`File marked for deletion: ${deleteFile}`);
353
+ logger.info(`Not found in target: ${targetFilePath}`);
354
+ logger.info(`Not found in base app: ${baseFilePath}`);
355
+ logger.warning(`The file must exist in target or base app to be deleted.\n`);
356
+ throw new Error("Delete validation failed - cannot proceed");
357
+ }
358
+ }
359
+ }
360
+ // Validate that files marked for inheritance exist in base app
361
+ for (const inheritFile of inheritFiles) {
362
+ // Translate feature path to target app path
363
+ const targetPath = translatePathToTargetApp(inheritFile, featureName, targetAppName);
364
+ const targetFilePath = join(targetDir, targetPath);
365
+ // If file doesn't exist in target yet, check if it will be copied from base app during inheritance
366
+ if (!existsSync(targetFilePath)) {
367
+ // Translate feature path to base app path (we inherit from base app)
368
+ const translatedPath = translatePathToBaseApp(inheritFile, featureName, baseAppPath);
369
+ const baseFilePath = join(baseAppPath, translatedPath);
370
+ if (!existsSync(baseFilePath)) {
371
+ logger.error(`\nValidation error: Cannot inherit file that doesn't exist!\n`);
372
+ logger.info(`File marked for inheritance: ${inheritFile}`);
373
+ logger.info(`Translated to: ${translatedPath}`);
374
+ logger.info(`Expected location in base app: ${baseFilePath}`);
375
+ logger.warning(`The file doesn't exist in the base app.\n`);
376
+ throw new Error("Inherit validation failed - cannot proceed");
377
+ }
378
+ }
379
+ }
380
+ // Validate that files marked for prepending exist in base app
381
+ for (const prependFile of prependFiles) {
382
+ // Translate feature path to base app path (we prepend to base app files)
383
+ const translatedPath = translatePathToBaseApp(prependFile, featureName, baseAppPath);
384
+ const baseFilePath = join(baseAppPath, translatedPath);
385
+ if (!existsSync(baseFilePath)) {
386
+ logger.error(`\nValidation error: Cannot prepend to file that doesn't exist!\n`);
387
+ logger.info(`File marked for prepending: ${prependFile}`);
388
+ logger.info(`Translated to: ${translatedPath}`);
389
+ logger.info(`Expected location in base app: ${baseFilePath}`);
390
+ logger.warning(`The file doesn't exist in the base app.\n`);
391
+ throw new Error("Prepend validation failed - cannot proceed");
392
+ }
393
+ }
394
+ // Validate that files marked for appending exist in base app
395
+ for (const appendFile of appendFiles) {
396
+ // Translate feature path to base app path (we append to base app files)
397
+ const translatedPath = translatePathToBaseApp(appendFile, featureName, baseAppPath);
398
+ const baseFilePath = join(baseAppPath, translatedPath);
399
+ if (!existsSync(baseFilePath)) {
400
+ logger.error(`\nValidation error: Cannot append to file that doesn't exist!\n`);
401
+ logger.info(`File marked for appending: ${appendFile}`);
402
+ logger.info(`Translated to: ${translatedPath}`);
403
+ logger.info(`Expected location in base app: ${baseFilePath}`);
404
+ logger.warning(`The file doesn't exist in the base app.\n`);
405
+ throw new Error("Append validation failed - cannot proceed");
406
+ }
407
+ }
408
+ logger.success("Paths validated");
409
+ // Apply each discovered file
410
+ for (const relativePath of discoveredFiles) {
411
+ // Apply path mappings to transform template path
412
+ const mappedPath = applyPathMappings(relativePath, effectiveMappings);
413
+ // Translate feature path to base app path (for finding base files)
414
+ const translatedPath = translatePathToBaseApp(mappedPath, featureName, baseAppPath);
415
+ // Translate feature path to target app path (for writing files)
416
+ const targetPath = translatePathToTargetApp(mappedPath, featureName, targetAppName);
417
+ // Check if this is a special operation
418
+ const deleteTargetPath = extractDeletePath(mappedPath);
419
+ const inheritTargetPath = extractInheritPath(mappedPath);
420
+ const prependTargetPath = extractPrependPath(mappedPath);
421
+ const appendTargetPath = extractAppendPath(mappedPath);
422
+ if (deleteTargetPath) {
423
+ // This is a delete operation - delete the target file/directory
424
+ const targetDeletePath = translatePathToTargetApp(deleteTargetPath, featureName, targetAppName);
425
+ const targetFilePath = join(targetDir, targetDeletePath);
426
+ await deleteFile(targetFilePath, targetDeletePath);
427
+ }
428
+ else if (inheritTargetPath) {
429
+ // This is an inherit operation - copy from base app if target doesn't have it
430
+ const targetInheritPath = translatePathToTargetApp(inheritTargetPath, featureName, targetAppName);
431
+ const targetFilePath = join(targetDir, targetInheritPath);
432
+ if (!existsSync(targetFilePath)) {
433
+ // Copy from base app to preserve inherited file in new target structure
434
+ const translatedInheritPath = translatePathToBaseApp(inheritTargetPath, featureName, baseAppPath);
435
+ const baseFilePath = join(baseAppPath, translatedInheritPath);
436
+ if (existsSync(baseFilePath)) {
437
+ const targetFileDir = dirname(targetFilePath);
438
+ if (!existsSync(targetFileDir)) {
439
+ mkdirSync(targetFileDir, { recursive: true });
440
+ }
441
+ cpSync(baseFilePath, targetFilePath);
442
+ logger.info(`Copied ${targetInheritPath} (inherited from base app)`);
443
+ }
444
+ else {
445
+ logger.info(`Skipped ${targetInheritPath} (file not in base app)`);
446
+ }
447
+ }
448
+ else {
449
+ logger.info(`Skipped ${targetInheritPath} (already exists in target)`);
450
+ }
451
+ }
452
+ else if (prependTargetPath) {
453
+ // This is a prepend operation - ensure target file exists first
454
+ const patchSourceFilePath = join(templatePath, relativePath);
455
+ const targetPrependPath = translatePathToTargetApp(prependTargetPath, featureName, targetAppName);
456
+ const targetFilePath = join(targetDir, targetPrependPath);
457
+ // Copy from base app if target doesn't have the file yet
458
+ if (!existsSync(targetFilePath)) {
459
+ const translatedPrependPath = translatePathToBaseApp(prependTargetPath, featureName, baseAppPath);
460
+ const baseFilePath = join(baseAppPath, translatedPrependPath);
461
+ if (existsSync(baseFilePath)) {
462
+ const targetFileDir = dirname(targetFilePath);
463
+ if (!existsSync(targetFileDir)) {
464
+ mkdirSync(targetFileDir, { recursive: true });
465
+ }
466
+ cpSync(baseFilePath, targetFilePath);
467
+ }
468
+ }
469
+ await prependToFile(patchSourceFilePath, targetFilePath, targetPrependPath);
470
+ }
471
+ else if (appendTargetPath) {
472
+ // This is an append operation - ensure target file exists first
473
+ const patchSourceFilePath = join(templatePath, relativePath);
474
+ const targetAppendPath = translatePathToTargetApp(appendTargetPath, featureName, targetAppName);
475
+ const targetFilePath = join(targetDir, targetAppendPath);
476
+ // Copy from base app if target doesn't have the file yet
477
+ if (!existsSync(targetFilePath)) {
478
+ const translatedAppendPath = translatePathToBaseApp(appendTargetPath, featureName, baseAppPath);
479
+ const baseFilePath = join(baseAppPath, translatedAppendPath);
480
+ if (existsSync(baseFilePath)) {
481
+ const targetFileDir = dirname(targetFilePath);
482
+ if (!existsSync(targetFileDir)) {
483
+ mkdirSync(targetFileDir, { recursive: true });
484
+ }
485
+ cpSync(baseFilePath, targetFilePath);
486
+ }
487
+ }
488
+ await appendToFile(patchSourceFilePath, targetFilePath, targetAppendPath);
489
+ }
490
+ else {
491
+ // Normal file operation - add/update/merge
492
+ const patchFilePath = join(templatePath, relativePath);
493
+ // Use target app path to ensure all features write to same location
494
+ const targetFilePath = join(targetDir, targetPath);
495
+ // Determine if this should be merged (only for the routes file)
496
+ // The routeFilePath from config includes the full path from template root
497
+ const shouldMerge = mappedPath === routeFilePath;
498
+ // For route merging with dependencies:
499
+ // - Use target file as base if it exists (accumulates routes from previous features)
500
+ // - Otherwise use base app file (first feature merges with base app)
501
+ // Create a temp copy of target to avoid ts-morph issues with same file as base and target
502
+ let baseFilePath = join(baseAppPath, translatedPath);
503
+ let tempFilePath;
504
+ if (shouldMerge && existsSync(targetFilePath)) {
505
+ // Create temp copy of target to use as base
506
+ tempFilePath = join(tmpdir(), `route-merge-${randomBytes(8).toString("hex")}.tsx`);
507
+ cpSync(targetFilePath, tempFilePath);
508
+ baseFilePath = tempFilePath;
509
+ }
510
+ try {
511
+ await applyFile(patchFilePath, baseFilePath, targetFilePath, targetPath, shouldMerge, targetAppName);
512
+ }
513
+ finally {
514
+ // Clean up temp file
515
+ if (tempFilePath && existsSync(tempFilePath)) {
516
+ rmSync(tempFilePath, { force: true });
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ /**
523
+ * Recursively rename any files or directories that have leading/trailing whitespace.
524
+ * Windows rejects filenames with trailing spaces, so this prevents cross-platform issues
525
+ * when the output is published to npm and cloned on Windows.
526
+ */
527
+ function sanitizeFilenames(dir) {
528
+ const entries = readdirSync(dir, { withFileTypes: true });
529
+ for (const entry of entries) {
530
+ const trimmed = entry.name.trim();
531
+ const srcPath = join(dir, entry.name);
532
+ if (trimmed !== entry.name) {
533
+ const destPath = join(dir, trimmed);
534
+ if (trimmed.length === 0) {
535
+ rmSync(srcPath, { recursive: true, force: true });
536
+ logger.warn(`Removed file/dir with whitespace-only name in ${dir}`);
537
+ continue;
538
+ }
539
+ renameSync(srcPath, destPath);
540
+ logger.warn(`Renamed "${entry.name}" → "${trimmed}" (stripped whitespace)`);
541
+ if (entry.isDirectory()) {
542
+ sanitizeFilenames(destPath);
543
+ }
544
+ }
545
+ else if (entry.isDirectory()) {
546
+ sanitizeFilenames(srcPath);
547
+ }
548
+ }
549
+ }
550
+ /**
551
+ * Sync base template .a4drules/ and copy feature-specific markdown rules to the target directory.
552
+ * 1. Syncs base template .a4drules/ from the SFDX project template.
553
+ * 2. Copies feature rules from feature/rules/*.md to .a4drules/features/ (prefixed by feature name).
554
+ */
555
+ function copyFeatureRulesAndSyncA4drules(targetDir) {
556
+ const targetFeaturesDir = join(targetDir, ".a4drules", "features");
557
+ // Sync base template .a4drules/
558
+ const sfdxTemplatePath = getSfdxProjectTemplatePath();
559
+ const sfdxA4dRulesPath = join(sfdxTemplatePath, ".a4drules");
560
+ if (existsSync(sfdxA4dRulesPath)) {
561
+ const targetA4dRulesPath = join(targetDir, ".a4drules");
562
+ cpSync(sfdxA4dRulesPath, targetA4dRulesPath, { recursive: true });
563
+ sanitizeFilenames(targetA4dRulesPath);
564
+ logger.success("Synced base template .a4drules/");
565
+ }
566
+ const featuresDir = getFeaturesDir();
567
+ if (!existsSync(featuresDir)) {
568
+ return;
569
+ }
570
+ const featureFolders = readdirSync(featuresDir, { withFileTypes: true });
571
+ let copiedRules = 0;
572
+ for (const folder of featureFolders) {
573
+ if (!folder.isDirectory())
574
+ continue;
575
+ const featureName = folder.name.trim();
576
+ const patchDir = join(featuresDir, folder.name);
577
+ // Copy rules from feature/rules/ directory (prefixed by feature name to avoid overwrites)
578
+ const featureRulesDir = join(patchDir, "rules");
579
+ if (existsSync(featureRulesDir)) {
580
+ const ruleFiles = readdirSync(featureRulesDir, { withFileTypes: true });
581
+ for (const entry of ruleFiles) {
582
+ if (entry.isFile() && entry.name.endsWith(".md")) {
583
+ const sourceRulePath = join(featureRulesDir, entry.name);
584
+ const prefixedRuleName = `${featureName}-${entry.name.trim()}`;
585
+ const targetRulePath = join(targetFeaturesDir, prefixedRuleName);
586
+ // Create features directory if needed
587
+ if (!existsSync(targetFeaturesDir)) {
588
+ mkdirSync(targetFeaturesDir, { recursive: true });
589
+ }
590
+ cpSync(sourceRulePath, targetRulePath);
591
+ logger.info(`Copied rule: features/${prefixedRuleName}`);
592
+ copiedRules++;
593
+ }
594
+ }
595
+ }
596
+ }
597
+ if (copiedRules > 0) {
598
+ logger.success(`Copied ${copiedRules} rule(s) to .a4drules/features/`);
599
+ }
600
+ }
601
+ /**
602
+ * Apply feature patches to create a new app
603
+ *
604
+ * @param patchPath - Path to the feature directory containing patches.ts
605
+ * @param appPath - Path to the base app directory (used as reference, remains unchanged)
606
+ * @param options - Configuration options
607
+ * @param options.targetDirName - Required. Target directory where feature will be applied
608
+ * @param options.skipDependencyChanges - Optional. Skip npm dependency installation
609
+ * @param options.reset - Optional. Reset target directory to base app state before applying (preserves node_modules)
610
+ *
611
+ * @example
612
+ * await applyPatchesCommand(
613
+ * 'packages/feature-navigation-menu',
614
+ * 'packages/base-react-app',
615
+ * { targetDirName: 'my-app' }
616
+ * );
617
+ */
618
+ export async function applyPatchesCommand(patchPath, appPath, options) {
619
+ try {
620
+ logger.heading(`Applying patches: ${patchPath} → ${options.targetDirName}`);
621
+ // Step 1: Validate inputs
622
+ logger.info("Validating paths...");
623
+ validateAndResolveFeaturePath(patchPath); // Fast-fail validation
624
+ const baseAppPath = validateAndResolveAppPath(appPath);
625
+ logger.success("Validation passed");
626
+ // Step 2: Prepare target directory
627
+ // Extract target app name from the main feature path for nested structure
628
+ const mainFeatureName = basename(patchPath);
629
+ const targetAppName = mainFeatureName;
630
+ // Resolve target directory - if absolute, use as-is; otherwise resolve relative to monorepo root
631
+ const resolvedTargetDir = isAbsolute(options.targetDirName)
632
+ ? options.targetDirName
633
+ : join(getMonorepoRoot(), options.targetDirName);
634
+ // Calculate target app directory path (SFDX structure so deploy has meta xml, uibundle.json, etc.)
635
+ const targetAppDir = join(resolvedTargetDir, "force-app", "main", "default", "uiBundles", targetAppName);
636
+ if (existsSync(resolvedTargetDir)) {
637
+ if (options.reset) {
638
+ // Reset target to base app state (preserving node_modules)
639
+ resetTargetToBaseApp(resolvedTargetDir, baseAppPath, targetAppName);
640
+ }
641
+ }
642
+ else {
643
+ logger.info(`Creating target directory ${options.targetDirName}...`);
644
+ mkdirSync(resolvedTargetDir, { recursive: true });
645
+ logger.success("Target directory created");
646
+ // Copy SFDX project template base (config/, sfdx-project.json, .forceignore, etc.) into target
647
+ const sfdxTemplatePath = getSfdxProjectTemplatePath();
648
+ if (existsSync(sfdxTemplatePath)) {
649
+ logger.info(`Copying SFDX project template base to ${options.targetDirName}...`);
650
+ cpSync(sfdxTemplatePath, resolvedTargetDir, {
651
+ recursive: true,
652
+ filter: (src) => !src.includes("node_modules"),
653
+ });
654
+ logger.success("SFDX project template base copied");
655
+ }
656
+ // Copy base app to target directory with nested structure
657
+ logger.info(`Copying base app to ${options.targetDirName}...`);
658
+ const sourceUIBundlePath = getUIBundlePath(baseAppPath);
659
+ mkdirSync(dirname(targetAppDir), { recursive: true });
660
+ cpSync(sourceUIBundlePath, targetAppDir, {
661
+ recursive: true,
662
+ filter: (src) => !src.includes("node_modules"),
663
+ });
664
+ processUIBundleMetaInDir(targetAppDir, targetAppName);
665
+ logger.success("Base app copied");
666
+ }
667
+ const targetDir = resolvedTargetDir;
668
+ // Step 3: Resolve dependencies
669
+ logger.heading("\nResolving Dependencies");
670
+ const { orderedPatches } = await resolveDependencies(patchPath);
671
+ if (orderedPatches.length > 1) {
672
+ logger.info(`Resolved dependency chain (${orderedPatches.length} patches):`);
673
+ for (let i = 0; i < orderedPatches.length; i++) {
674
+ const { patchPath: path } = orderedPatches[i];
675
+ const isMainPatch = i === orderedPatches.length - 1;
676
+ const prefix = isMainPatch ? "└─" : "├─";
677
+ const label = isMainPatch ? " (main)" : " (dependency)";
678
+ logger.info(` ${prefix} ${path}${label}`);
679
+ }
680
+ logger.success("Dependency resolution complete");
681
+ }
682
+ else {
683
+ logger.info("No dependencies found");
684
+ }
685
+ // Step 4: Apply all patches in dependency order
686
+ for (const resolvedPatch of orderedPatches) {
687
+ await applySinglePatch(resolvedPatch, baseAppPath, targetDir, targetAppName);
688
+ }
689
+ // Step 4b: Copy site-container metadata from template if main patch has siteContainer
690
+ const mainPatch = orderedPatches[orderedPatches.length - 1];
691
+ if (mainPatch?.config.siteContainer) {
692
+ const siteContainerTemplatePath = getSiteContainerTemplatePath();
693
+ const targetForceAppDefault = join(targetDir, "force-app", "main", "default");
694
+ if (existsSync(siteContainerTemplatePath)) {
695
+ logger.info("Copying site-container metadata from template-site-container...");
696
+ copySiteContainerTemplate(siteContainerTemplatePath, targetForceAppDefault, targetAppName);
697
+ logger.success("Site-container metadata applied");
698
+ }
699
+ const metaFilePath = join(targetAppDir, `${targetAppName}.uibundle-meta.xml`);
700
+ if (existsSync(metaFilePath)) {
701
+ let content = readFileSync(metaFilePath, "utf-8");
702
+ if (!content.includes("<target>")) {
703
+ content = content.replace("</UIBundle>", " <target>Experience</target>\n</UIBundle>");
704
+ writeFileSync(metaFilePath, content, "utf-8");
705
+ logger.success("Added Experience target to UI Bundle metadata");
706
+ }
707
+ }
708
+ }
709
+ // Step 5: Aggregate and install all dependencies once
710
+ if (options.skipDependencyChanges) {
711
+ logger.info("\n⏭ Skipping dependency installation");
712
+ }
713
+ else {
714
+ // Aggregate all dependencies from all resolved patches, checking for conflicts
715
+ const aggregatedDependencies = {};
716
+ const aggregatedDevDependencies = {};
717
+ // Collect all patches from all resolved patches
718
+ const allPatches = orderedPatches.flatMap((rf) => rf.config.patches);
719
+ // First pass: collect all regular dependencies
720
+ for (const patch of allPatches) {
721
+ if (patch.packageJson?.dependencies) {
722
+ for (const [name, version] of Object.entries(patch.packageJson.dependencies)) {
723
+ if (aggregatedDependencies[name] && aggregatedDependencies[name] !== version) {
724
+ throw new Error(`Dependency version conflict for '${name}': patch requires '${version}' but another patch requires '${aggregatedDependencies[name]}'`);
725
+ }
726
+ aggregatedDependencies[name] = version;
727
+ }
728
+ }
729
+ }
730
+ // Second pass: collect dev dependencies (skip if already in dependencies)
731
+ for (const patch of allPatches) {
732
+ if (patch.packageJson?.devDependencies) {
733
+ for (const [name, version] of Object.entries(patch.packageJson.devDependencies)) {
734
+ // Skip if already in regular dependencies
735
+ if (aggregatedDependencies[name]) {
736
+ continue;
737
+ }
738
+ if (aggregatedDevDependencies[name] && aggregatedDevDependencies[name] !== version) {
739
+ throw new Error(`Dev dependency version conflict for '${name}': patch requires '${version}' but another patch requires '${aggregatedDevDependencies[name]}'`);
740
+ }
741
+ aggregatedDevDependencies[name] = version;
742
+ }
743
+ }
744
+ }
745
+ const hasAnyDependencies = Object.keys(aggregatedDependencies).length > 0 ||
746
+ Object.keys(aggregatedDevDependencies).length > 0;
747
+ if (hasAnyDependencies) {
748
+ logger.heading("\nMerging Dependencies");
749
+ await mergePackageJson(targetDir, {
750
+ dependencies: Object.keys(aggregatedDependencies).length > 0 ? aggregatedDependencies : undefined,
751
+ devDependencies: Object.keys(aggregatedDevDependencies).length > 0
752
+ ? aggregatedDevDependencies
753
+ : undefined,
754
+ }, targetAppDir);
755
+ }
756
+ else {
757
+ logger.info("\n✓ No dependencies to merge");
758
+ }
759
+ logger.heading("\nInstalling Dependencies");
760
+ rewriteWorkspaceDepsToLocalPaths(targetAppDir, getMonorepoRoot());
761
+ execSync("npm install", { cwd: targetAppDir, stdio: "inherit" });
762
+ }
763
+ // Step 6: Sync .a4drules and copy feature rules to target directory
764
+ logger.heading("\nSyncing .a4drules & Feature Rules");
765
+ copyFeatureRulesAndSyncA4drules(targetDir);
766
+ // Step 7: Run GraphQL codegen (only when schema.graphql exists — e.g. after graphql:schema)
767
+ // In CI or when schema is not checked in, skip so build succeeds.
768
+ const schemaPath = join(targetDir, "schema.graphql");
769
+ const schemaExists = existsSync(schemaPath);
770
+ logger.heading("\nRunning GraphQL Codegen");
771
+ if (schemaExists) {
772
+ try {
773
+ execSync("npm run graphql:codegen", { cwd: targetAppDir, stdio: "inherit" });
774
+ logger.success("GraphQL codegen complete");
775
+ }
776
+ catch {
777
+ logger.warning("GraphQL codegen failed — skipping");
778
+ }
779
+ }
780
+ else {
781
+ logger.info("schema.graphql not found — skipping codegen (run graphql:schema when needed)");
782
+ }
783
+ // Step 8: Success message
784
+ logger.heading("\n✓ Success");
785
+ logger.success(`Created: ${targetDir}`);
786
+ }
787
+ catch (err) {
788
+ // Only log error if it's not already been logged (check for our custom error messages)
789
+ const errorMessage = err instanceof Error ? err.message : String(err);
790
+ const skipLogging = [
791
+ "Path conflict - cannot proceed",
792
+ "Delete validation failed - cannot proceed",
793
+ "Inherit validation failed - cannot proceed",
794
+ "Prepend validation failed - cannot proceed",
795
+ "Append validation failed - cannot proceed",
796
+ "Route deletion failed - cannot proceed",
797
+ ].includes(errorMessage);
798
+ if (!skipLogging) {
799
+ logger.error(`Failed to apply feature: ${errorMessage}`);
800
+ }
801
+ throw err;
802
+ }
803
+ }
804
+ //# sourceMappingURL=apply-patches.js.map