@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
package/README.md ADDED
@@ -0,0 +1,1613 @@
1
+ # Feature Patch CLI
2
+
3
+ CLI tool for applying feature patches to apps in the ui-bundle-templates monorepo.
4
+
5
+ ## Installation
6
+
7
+ Build the CLI from the monorepo root:
8
+
9
+ ```bash
10
+ npm run build --workspace=@salesforce/ui-bundle-template-cli
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Apply patches
16
+
17
+ ```bash
18
+ # Apply patches to a target directory (base app is used as reference only)
19
+ npm run apply-patches -- <feature-path> <app-path> <target-dir>
20
+
21
+ # Examples:
22
+ # Apply a feature (e.g. chart) to my-app (using base-react-app as reference)
23
+ npm run apply-patches -- packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app my-app
24
+
25
+ # Skip dependency installation
26
+ npm run apply-patches -- packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app my-app --skip-dependency-changes
27
+
28
+ # Reset target directory to base app state before applying (preserves node_modules)
29
+ npm run apply-patches -- packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app my-app --reset
30
+ ```
31
+
32
+ ### Direct usage
33
+
34
+ ```bash
35
+ # From monorepo root
36
+ node packages/cli/dist/index.js <feature-path> <app-path> <target-dir>
37
+
38
+ # With flags
39
+ node packages/cli/dist/index.js <feature-path> <app-path> my-app --skip-dependency-changes --reset
40
+ ```
41
+
42
+ ### Create a new feature
43
+
44
+ Create a new feature package from the base-feature template:
45
+
46
+ ```bash
47
+ # Create a new feature (will be prefixed with "feature-")
48
+ npm run new-feature -- <feature-name>
49
+
50
+ # Examples:
51
+ npm run new-feature -- navigation # Creates packages/feature/feature-navigation
52
+ npm run new-feature -- user-dashboard # Creates packages/feature/feature-user-dashboard
53
+ npm run new-feature -- api-client # Creates packages/feature/feature-api-client
54
+ ```
55
+
56
+ #### Direct usage
57
+
58
+ ```bash
59
+ # From monorepo root
60
+ node packages/cli/dist/index.js new-feature <feature-name>
61
+ ```
62
+
63
+ #### Feature Name Requirements
64
+
65
+ - **Format**: Must be in kebab-case (lowercase with hyphens)
66
+ - **Characters**: Only alphanumeric characters and hyphens allowed
67
+ - **Cannot**: Start or end with hyphens, have consecutive hyphens
68
+ - **Reserved**: Cannot be "base" or "cli"
69
+ - **Auto-prefix**: Automatically prefixed with "feature-" (e.g., `nav-menu` → `feature-nav-menu`)
70
+
71
+ Examples:
72
+
73
+ - ✅ `navigation` → `feature-navigation`
74
+ - ✅ `user-auth` → `feature-user-auth`
75
+ - ✅ `dashboard-v2` → `feature-dashboard-v2`
76
+ - ❌ `Navigation` (uppercase)
77
+ - ❌ `user_auth` (underscore)
78
+ - ❌ `--menu` (starts with hyphen)
79
+ - ❌ `nav--menu` (consecutive hyphens)
80
+
81
+ #### What the command does
82
+
83
+ The `new-feature` command:
84
+
85
+ 1. **Validates** the feature name format
86
+ 2. **Checks** that the feature doesn't already exist
87
+ 3. **Copies** the template-feature template to `packages/feature/feature-{name}/`
88
+ 4. **Renames** the template directory from `template-feature` to `feature-{name}`
89
+ 5. **Updates** `package.json` with the new feature name
90
+ 6. **Updates** `tsconfig.app.json` with the new directory path
91
+ 7. **Creates** a ready-to-use feature package
92
+
93
+ The created feature will have:
94
+
95
+ - ✅ Correct `package.json` configuration
96
+ - ✅ TypeScript configuration
97
+ - ✅ Feature structure following conventions
98
+ - ✅ Ready for customization and development
99
+
100
+ #### Next steps after creation
101
+
102
+ ```bash
103
+ cd packages/feature/feature-{name}
104
+ # 1. Customize template files in template/ directory
105
+ # 2. Update patches.ts with feature configuration
106
+ # 3. Test with: npm run apply-patches -- packages/feature/feature-{name} packages/base-app/base-react-app test-app
107
+ ```
108
+
109
+ ### Options
110
+
111
+ - `<feature-path>`: Path to the feature directory (can be relative to monorepo root or absolute). The feature must contain a patches.ts file.
112
+ - `<app-path>`: Path to the base app directory (can be relative to monorepo root or absolute). Used as a reference for file inheritance and validation. The base app remains unchanged.
113
+ - `<target-dir>`: **Required**. Target directory where the feature will be applied. Can be a relative or absolute path. The CLI will create this directory and apply all features to it.
114
+ - `--skip-dependency-changes`: Skip installing dependencies from package.json. Only file changes will be applied.
115
+ - `--reset`: Reset target directory to base app state before applying patches. Syncs the target with the base app by removing extra files, updating changed files, and adding new files (preserves node_modules).
116
+
117
+ ## What it does
118
+
119
+ The CLI tool:
120
+
121
+ 1. **Validates** that the specified feature path exists and contains a patches.ts file, and the base app path is valid
122
+ 2. **Prepares target directory**: Creates the target directory (or resets it to base app state if `--reset` flag is used, preserving node_modules)
123
+ 3. **Resolves dependencies**: Recursively resolves all feature dependencies, detecting circular dependencies and building an ordered list where dependencies are applied before dependent features
124
+ 4. **Loads** the feature definitions from each `patches.ts` file in dependency order
125
+ 5. **Discovers files** from each feature's template directory (defaults to `template`, configurable via `templateDir` in patches.ts)
126
+ 6. **Validates paths**: For each feature, ensures:
127
+ - No conflicting paths exist (e.g., both `routes.tsx` and `__delete__routes.tsx`, or `__prepend__global.css` and `__append__global.css`)
128
+ - Files marked with `__delete__`, `__inherit__`, `__prepend__`, or `__append__` exist in the base app
129
+ 7. **Applies file changes** from each feature's template directory in dependency order:
130
+ - **Delete operations**: Removes files/directories marked with `__delete__` prefix from the target app
131
+ - **Inherit operations**: Skips files marked with `__inherit__` (inherited from base app)
132
+ - **Prepend operations**: Adds content from feature file before the base file's content (for files marked with `__prepend__`)
133
+ - **Append operations**: Adds content from feature file after the base file's content (for files marked with `__append__`)
134
+ - **Import path fixing**: Automatically removes `__inherit__` prefix from import statements in JS/TS files
135
+ - **File changes**: Copies each file from the template directory to the target
136
+ - **Route merging**: Intelligently merges route files (`routes.tsx`), accumulating routes from all features
137
+ 8. **Aggregates and installs dependencies**: Collects all NPM dependencies from all features and installs them in a single `npm` command (unless `--skip-dependency-changes` is used). Detects and errors on version conflicts.
138
+
139
+ ## Examples
140
+
141
+ ### Example: Apply feature patches to create a new app
142
+
143
+ ```bash
144
+ $ npm run apply-patches -- packages/template/feature/feature-react-authentication packages/template/base-app/base-react-app my-app
145
+
146
+ Applying patches: packages/template/feature/feature-react-authentication → my-app
147
+ ℹ Validating paths...
148
+ ✓ Validation passed
149
+ ℹ Creating target directory my-app...
150
+ ✓ Target directory created
151
+
152
+ Resolving Dependencies
153
+ ℹ No dependencies to resolve
154
+
155
+
156
+ Applying: packages/template/feature/feature-react-authentication
157
+ ℹ Discovering files...
158
+ ℹ Found 14 file(s)
159
+ ℹ Validating paths...
160
+ ✓ Paths validated
161
+ ✓ Added uiBundles/feature-react-authentication/src/...
162
+ ...
163
+
164
+ Installing dependencies
165
+ ℹ Installing dependencies...
166
+ [npm output]
167
+ ✓ Dependencies installed
168
+
169
+ ✓ Success
170
+ ✓ Created: /path/to/monorepo/my-app
171
+ ```
172
+
173
+ ## Creating Features
174
+
175
+ **Quick Start:** Use the CLI to create a new feature from the template:
176
+
177
+ ```bash
178
+ npm run new-feature -- your-feature-name
179
+ ```
180
+
181
+ This creates a new feature at `packages/feature/feature-your-feature-name/` with all the necessary configuration files. Then customize the template files in the `template/` directory.
182
+
183
+ For manual setup or advanced configuration, see below...
184
+
185
+ ---
186
+
187
+ Features are defined in your feature's `patches.ts` file and must be exported as a **default export**. The default export can be either a single feature object or an array of features. Import types from `packages/cli/src/types.js`.
188
+
189
+ ### Feature Structure
190
+
191
+ ```
192
+ packages/feature/feature-my-feature/
193
+ ├── patches.ts # Feature configuration
194
+ ├── package.json # NPM dependencies for development
195
+ └── template/ # Template files (default directory name)
196
+ ├── uiBundle/ # Web application files (mapped to uiBundles/<feature-name>/)
197
+ │ └── src/
198
+ │ ├── routes.tsx
199
+ │ ├── component1.tsx
200
+ │ └── component2.tsx
201
+ └── classes/ # SFDX metadata (placed at root level in dist)
202
+ └── MyClass.cls
203
+ ```
204
+
205
+ **Note**: The CLI handles two types of files differently:
206
+
207
+ - **UI Bundle files** (under `uiBundle/`): Automatically mapped to `uiBundles/<feature-name>/`
208
+ - **SFDX metadata files** (like `classes/`, `triggers/`, `objects/`, etc.): Placed at root level in the output directory
209
+
210
+ This structure ensures proper organization for both digital experience applications and Salesforce metadata.
211
+
212
+ ### Feature Configuration
213
+
214
+ The feature configuration file specifies:
215
+
216
+ - `templateDir`: Directory containing template files (defaults to `template`)
217
+ - `uiBundleName`: Name of the UI Bundle (defaults to the feature name extracted from the directory). This is used for constructing the default route path.
218
+ - `routeFilePath`: Path to the routes file for merging (defaults to `uiBundles/<uiBundleName>/src/routes.tsx`)
219
+ - `packageJson`: NPM dependencies to install in the target app
220
+ - `dependencies`: Array of other features this feature depends on (applied first)
221
+
222
+ ### Basic Feature Example:
223
+
224
+ ```typescript
225
+ import type { Patch } from "../cli/src/types.js";
226
+
227
+ const patch: Patch = {
228
+ // All fields are optional with sensible defaults
229
+ templateDir: "template", // Optional, defaults to 'template'
230
+ uiBundleName: "my-feature", // Optional, defaults to feature directory name
231
+ // routeFilePath defaults to 'uiBundles/<uiBundleName>/src/routes.tsx'
232
+ packageJson: {
233
+ dependencies: {
234
+ "react-router": "^7.10.1",
235
+ },
236
+ },
237
+ };
238
+
239
+ export default patch;
240
+ ```
241
+
242
+ ### Feature with Dependencies:
243
+
244
+ ```typescript
245
+ import type { Patch } from "../cli/src/types.js";
246
+
247
+ const patch: Patch = {
248
+ // This feature depends on shadcn UI feature
249
+ // shadcn will be applied first, then this feature
250
+ dependencies: ["packages/template/feature/feature-react-authentication"],
251
+ packageJson: {
252
+ dependencies: {
253
+ "some-package": "^1.0.0",
254
+ },
255
+ },
256
+ };
257
+
258
+ export default patch;
259
+ ```
260
+
261
+ ### Custom Configuration:
262
+
263
+ ```typescript
264
+ import type { Patch } from "../cli/src/types.js";
265
+
266
+ const patch: Patch = {
267
+ templateDir: "src", // Use 'src' instead of 'template'
268
+ uiBundleName: "custom-app-name", // Override default app name
269
+ routeFilePath: "custom/path/to/routes.tsx", // Custom route file path
270
+ packageJson: {
271
+ dependencies: {
272
+ "react-router": "^7.10.1",
273
+ },
274
+ },
275
+ };
276
+
277
+ export default patch;
278
+ ```
279
+
280
+ **Notes:**
281
+
282
+ - `templateDir`: All files in this directory will be discovered and applied to the target app
283
+ - `uiBundleName`: Used to construct the default route path and organize files. Defaults to the feature directory name (e.g., `feature-react-authentication` → `feature-react-authentication`)
284
+ - `routeFilePath`: Must be a path relative to `templateDir`. If not specified, defaults to `uiBundles/<uiBundleName>/src/routes.tsx`
285
+
286
+ ## Path Mappings
287
+
288
+ Path mappings allow features to use simplified directory structures that are automatically transformed to the full Salesforce Digital Experience structure. This makes feature templates easier to create and maintain by removing repetitive nested directory paths.
289
+
290
+ ### Default Behavior (Enabled by Default)
291
+
292
+ By default, all features automatically get the `uiBundle` mapping, which transforms UI Bundle files into the proper nested structure. For example, in `feature-react-authentication`:
293
+
294
+ ```
295
+ template/uiBundle/src/app.tsx → dist/uiBundles/feature-react-authentication/src/app.tsx
296
+ ```
297
+
298
+ This simplifies feature templates by removing the repetitive nested directory structure.
299
+
300
+ **Important**: Only files under `uiBundle/` get the nested structure. SFDX metadata types (like `classes/`, `triggers/`, `objects/`, `lwc/`, etc.) are placed at root level:
301
+
302
+ ```
303
+ feature-react-authentication/template/
304
+ ├── uiBundle/
305
+ │ └── src/
306
+ │ └── app.tsx → dist/uiBundles/feature-react-authentication/src/app.tsx
307
+ └── classes/
308
+ └── MyClass.cls → dist/classes/MyClass.cls (root level)
309
+ ```
310
+
311
+ ### Using the Default Mapping
312
+
313
+ Simply organize your template files under `uiBundle/` for UI Bundle code, and at the root level for SFDX metadata:
314
+
315
+ ```typescript
316
+ // patches.ts - No configuration needed
317
+ export default {};
318
+ ```
319
+
320
+ ```
321
+ template/
322
+ ├── uiBundle/ # Web application files (automatically mapped)
323
+ │ └── src/
324
+ │ ├── routes.tsx
325
+ │ ├── app.tsx
326
+ │ └── components/
327
+ │ └── Header.tsx
328
+ └── classes/ # SFDX metadata (placed at root level)
329
+ └── MyClass.cls
330
+ ```
331
+
332
+ **Result**:
333
+
334
+ - Web app files go to `dist/uiBundles/feature-name/src/`
335
+ - SFDX metadata stays at root: `dist/classes/MyClass.cls`
336
+
337
+ ### Disabling Path Mappings (Opt-Out)
338
+
339
+ Use full paths when you need precise control or for backwards compatibility:
340
+
341
+ ```typescript
342
+ // patches.ts
343
+ export default {
344
+ pathMappings: {
345
+ enabled: false, // Disable automatic mapping
346
+ },
347
+ };
348
+ ```
349
+
350
+ ```
351
+ template/
352
+ └── digitalExperiences/ # Use full structure
353
+ └── uiBundles/
354
+ └── <feature-name>/
355
+ └── src/
356
+ └── routes.tsx
357
+ ```
358
+
359
+ ### Custom Path Mappings
360
+
361
+ Define custom mappings for non-standard structures:
362
+
363
+ ```typescript
364
+ // patches.ts
365
+ export default {
366
+ pathMappings: {
367
+ mappings: [
368
+ {
369
+ from: "web",
370
+ to: "uiBundles/custom-app-name",
371
+ },
372
+ ],
373
+ },
374
+ };
375
+ ```
376
+
377
+ ```
378
+ template/
379
+ └── web/ # Custom prefix
380
+ └── src/
381
+ └── app.tsx
382
+ ```
383
+
384
+ **Result**: Maps `web/src/app.tsx` → `uiBundles/custom-app-name/src/app.tsx`
385
+
386
+ ### Multiple Mappings
387
+
388
+ You can define multiple mappings in one feature:
389
+
390
+ ```typescript
391
+ export default {
392
+ pathMappings: {
393
+ mappings: [
394
+ { from: "uiBundle", to: "uiBundles/my-app" },
395
+ { from: "shared", to: "digitalExperiences/shared-resources" },
396
+ ],
397
+ },
398
+ };
399
+ ```
400
+
401
+ ### Mixed Path Formats
402
+
403
+ You can mix UI Bundle files, SFDX metadata, and full paths in the same feature:
404
+
405
+ ```
406
+ template/
407
+ ├── uiBundle/ # Mapped to uiBundles/<feature-name>/
408
+ │ └── src/
409
+ │ └── app.tsx
410
+ ├── classes/ # SFDX metadata (placed at root)
411
+ │ └── MyClass.cls
412
+ ├── triggers/ # SFDX metadata (placed at root)
413
+ │ └── MyTrigger.trigger
414
+ └── digitalExperiences/ # Full paths (passed through)
415
+ └── siteAssets/
416
+ └── logo.png
417
+ ```
418
+
419
+ All formats work together seamlessly:
420
+
421
+ - `uiBundle/` files → `uiBundles/<feature-name>/`
422
+ - SFDX metadata (`classes/`, `triggers/`, `objects/`, `lwc/`, etc.) → Root level
423
+ - Full paths (already containing `digitalExperiences/`) → Used as-is
424
+
425
+ ### Path Mapping Examples
426
+
427
+ **Example 1: Default mapping (recommended)**
428
+
429
+ ```typescript
430
+ export default {}; // That's it!
431
+ ```
432
+
433
+ **Example 2: Opt-out for backwards compatibility**
434
+
435
+ ```typescript
436
+ export default {
437
+ pathMappings: { enabled: false },
438
+ };
439
+ ```
440
+
441
+ **Example 3: Custom app name**
442
+
443
+ ```typescript
444
+ export default {
445
+ pathMappings: {
446
+ mappings: [{ from: "uiBundle", to: "uiBundles/custom-name" }],
447
+ },
448
+ };
449
+ ```
450
+
451
+ **Example 4: Completely custom structure**
452
+
453
+ ```typescript
454
+ export default {
455
+ pathMappings: {
456
+ mappings: [
457
+ { from: "src", to: "app/sources" },
458
+ { from: "assets", to: "public/static" },
459
+ ],
460
+ },
461
+ };
462
+ ```
463
+
464
+ ### How Path Mappings Work
465
+
466
+ 1. **Discovery**: CLI discovers all files in your template directory
467
+ 2. **Mapping**: Each file path is checked against mapping rules (first match wins)
468
+ 3. **Transformation**: Matching prefix is replaced with target prefix
469
+ 4. **Pass-Through**: Paths that don't match any mapping are used as-is
470
+ 5. **Application**: Transformed paths are used for file operations
471
+
472
+ This ensures:
473
+
474
+ - ✅ Backwards compatibility (old features still work)
475
+ - ✅ Simplified templates (new features are easier to create)
476
+ - ✅ Flexibility (custom mappings for special cases)
477
+ - ✅ No breaking changes (opt-in for existing features, opt-out available)
478
+
479
+ ## Feature Dependencies
480
+
481
+ Features can depend on other features. Dependencies are automatically resolved and applied in the correct order.
482
+
483
+ ### How Dependencies Work
484
+
485
+ 1. **Declaration**: Specify dependencies in your `patches.ts` file
486
+ 2. **Resolution**: CLI recursively resolves all dependencies (including nested dependencies)
487
+ 3. **Ordering**: Dependencies are always applied before the feature that depends on them
488
+ 4. **Circular Detection**: CLI detects and prevents circular dependencies
489
+ 5. **File Layering**: Files from dependencies can be overridden by dependent features (main feature wins)
490
+ 6. **Route Accumulation**: Routes from all features are merged together
491
+
492
+ ### Dependency Resolution Order
493
+
494
+ When you apply a feature with dependencies:
495
+
496
+ ```
497
+ Feature A depends on Feature B
498
+ Feature B depends on Feature C
499
+
500
+ Application order: C → B → A (dependencies first)
501
+ ```
502
+
503
+ The CLI builds a complete dependency graph and applies features in topological order.
504
+
505
+ ### Example: Building on Shared UI Features
506
+
507
+ ```typescript
508
+ // packages/template/feature/feature-react-authentication/patches.ts
509
+ import type { Patch } from "../../../cli/src/types.js";
510
+
511
+ const patch: Patch = {
512
+ // Shadcn UI has no dependencies
513
+ };
514
+
515
+ export default patch;
516
+ ```
517
+
518
+ ```typescript
519
+ // packages/template/feature/feature-admin-dashboard/patches.ts
520
+ import type { Patch } from "../../../cli/src/types.js";
521
+
522
+ const patch: Patch = {
523
+ // Admin dashboard builds on top of shadcn UI
524
+ dependencies: ["packages/template/feature/feature-react-authentication"],
525
+ };
526
+
527
+ export default patch;
528
+ ```
529
+
530
+ When you apply `feature-admin-dashboard`:
531
+
532
+ 1. CLI resolves `feature-react-authentication` as a dependency
533
+ 2. Applies `feature-react-authentication` first
534
+ 3. Applies `feature-admin-dashboard` second (dashboard files and routes)
535
+ 4. Result: App has both shared UI and admin dashboard
536
+
537
+ ### Nested Dependencies
538
+
539
+ Dependencies can have their own dependencies. The CLI resolves them recursively:
540
+
541
+ ```
542
+ Feature App depends on Feature Dashboard
543
+ Feature Dashboard depends on Feature Navigation
544
+ Feature Navigation depends on Feature Auth
545
+
546
+ Application order: Auth → Navigation → Dashboard → App
547
+ ```
548
+
549
+ ### Circular Dependency Detection
550
+
551
+ The CLI detects and prevents circular dependencies:
552
+
553
+ ```typescript
554
+ // Feature A depends on B
555
+ // Feature B depends on C
556
+ // Feature C depends on A
557
+
558
+ // CLI will error:
559
+ // "Circular dependency detected:
560
+ // packages/feature-a → packages/feature-b → packages/feature-c → packages/feature-a"
561
+ ```
562
+
563
+ **Fix**: Remove one of the dependencies to break the cycle.
564
+
565
+ ### Main Feature Wins
566
+
567
+ When multiple features modify the same file:
568
+
569
+ - **Dependencies applied first**: Their files are written to the target
570
+ - **Main feature applied last**: Its files overwrite dependency files
571
+ - **Result**: Main feature can customize/override dependency behavior
572
+
573
+ Example:
574
+
575
+ ```
576
+ Features can provide e.g. src/appLayout.tsx
577
+ feature-custom-app also provides: src/appLayout.tsx
578
+
579
+ When applying feature-custom-app:
580
+ 1. shadcn's appLayout.tsx is applied
581
+ 2. custom-app's appLayout.tsx overwrites it
582
+ 3. Final result: custom-app's version is used
583
+ ```
584
+
585
+ ### Route Merging with Dependencies
586
+
587
+ Routes from all features are merged together:
588
+
589
+ **Base App Routes:**
590
+
591
+ ```typescript
592
+ export const routes = [
593
+ {
594
+ path: '/',
595
+ children: [
596
+ { index: true, element: <Home /> }
597
+ ]
598
+ }
599
+ ];
600
+ ```
601
+
602
+ **navigation-menu Routes:**
603
+
604
+ ```typescript
605
+ export const routes = [
606
+ {
607
+ path: '/',
608
+ children: [
609
+ { path: 'about', element: <About /> },
610
+ { path: 'contact', element: <Contact /> }
611
+ ]
612
+ }
613
+ ];
614
+ ```
615
+
616
+ **Final Merged Routes:**
617
+
618
+ ```typescript
619
+ export const routes = [
620
+ {
621
+ path: '/',
622
+ children: [
623
+ { index: true, element: <Home /> }, // From base
624
+ { path: 'about', element: <About /> }, // From navigation-menu
625
+ { path: 'contact', element: <Contact /> } // From navigation-menu
626
+ ]
627
+ }
628
+ ];
629
+ ```
630
+
631
+ Routes accumulate across all features, preserving routes from base app and all dependencies.
632
+
633
+ ### Dependency Paths
634
+
635
+ Dependency paths can be:
636
+
637
+ - **Relative to monorepo root**: `'packages/template/feature/feature-react-authentication'`
638
+ - **Absolute paths**: `'/absolute/path/to/feature'`
639
+
640
+ The CLI normalizes and resolves all paths consistently.
641
+
642
+ ### Diamond Dependencies
643
+
644
+ When multiple features depend on the same feature:
645
+
646
+ ```
647
+ Feature A depends on Feature C
648
+ Feature B depends on Feature C
649
+ Feature Main depends on A and B
650
+
651
+ Dependency graph:
652
+ Main
653
+ / \
654
+ A B
655
+ \ /
656
+ C
657
+ ```
658
+
659
+ **Resolution**: Feature C is applied once (not duplicated).
660
+
661
+ **Application order**: `C → A → B → Main`
662
+
663
+ ### Watch Mode with Dependencies
664
+
665
+ When using watch mode, dependencies are:
666
+
667
+ - Applied once on initial startup with reset to ensure clean state
668
+ - Re-applied when template files change (always resets to base app state, preserving node_modules)
669
+ - Not watched for changes (only the main feature is watched)
670
+
671
+ This is efficient for development: edit your main feature while keeping dependencies stable. The reset behavior ensures stale files are removed while preserving built artifacts in node_modules.
672
+
673
+ ## File Application
674
+
675
+ The CLI discovers all files in your feature's template directory and applies them to the target app:
676
+
677
+ 1. **Standard Files**: Copied directly to the target, creating directories as needed
678
+ 2. **Delete Markers**: Files/directories prefixed with `__delete__` mark files for deletion from the target app
679
+ 3. **Inherit Markers**: Files prefixed with `__inherit__` are kept in the feature for type safety but not copied (inherited from base app)
680
+ 4. **Prepend Markers**: Files prefixed with `__prepend__` add their content before the base file's content
681
+ 5. **Append Markers**: Files prefixed with `__append__` add their content after the base file's content
682
+ 6. **Route Files** (`routes.tsx`): Intelligently merged with existing routes to preserve base app routes
683
+
684
+ ### Deleting Files and Directories
685
+
686
+ Features can delete files or directories from the target app by using the `__delete__` prefix in the template directory.
687
+
688
+ #### How It Works
689
+
690
+ 1. Create a file with the `__delete__` prefix in your feature's template directory
691
+ 2. The prefix can appear anywhere in the path
692
+ 3. When the feature is applied, the corresponding file/directory will be deleted from the target app
693
+ 4. The content of the delete marker file is ignored (can be empty or contain comments)
694
+
695
+ #### Examples
696
+
697
+ **Delete a single file:**
698
+
699
+ ```
700
+ template/
701
+ └── src/
702
+ └── __delete__routes.tsx # Deletes src/routes.tsx from target app
703
+ ```
704
+
705
+ **Delete a directory:**
706
+
707
+ ```
708
+ template/
709
+ └── src/
710
+ └── __delete__pages/ # Deletes src/pages/ directory from target app
711
+ └── .gitkeep # Placeholder file (content ignored)
712
+ ```
713
+
714
+ **Delete from nested path:**
715
+
716
+ ```
717
+ template/
718
+ └── src/
719
+ └── components/
720
+ └── __delete__Footer.tsx # Deletes src/components/Footer.tsx
721
+ ```
722
+
723
+ **Delete a parent directory:**
724
+
725
+ ```
726
+ template/
727
+ └── __delete__src/
728
+ └── legacy/ # Deletes src/legacy/ directory from target app
729
+ └── .gitkeep
730
+ ```
731
+
732
+ #### Validation
733
+
734
+ The CLI validates that you don't have conflicting paths:
735
+
736
+ - ❌ **Invalid**: Having both `routes.tsx` and `__delete__routes.tsx` in the same template
737
+ - ✅ **Valid**: Having only `__delete__routes.tsx` (to delete) or only `routes.tsx` (to add/update)
738
+
739
+ If a conflict is detected, the CLI will throw an error:
740
+
741
+ ```
742
+ Path conflict detected: "src/routes.tsx" appears multiple times in the template.
743
+ This can happen when both a file and its __delete__ marker exist.
744
+ Please remove one of them.
745
+ ```
746
+
747
+ #### Use Cases
748
+
749
+ - **Remove obsolete files**: Delete deprecated components or utilities that the feature replaces
750
+ - **Clean up after refactoring**: Remove files that are no longer needed with the new feature
751
+ - **Remove base app scaffolding**: Delete placeholder files from the base app that the feature supersedes
752
+
753
+ For example, the `vibe-coding-starter` feature deletes the base app's `routes.tsx` because it provides a single-page app in `index.tsx` instead.
754
+
755
+ ### Inheriting Files from Base App
756
+
757
+ Features can maintain type-safe references to base app files without copying them using the `__inherit__` prefix. This is useful when your feature code needs to import and reference files from the base app while keeping TypeScript/IDE support in your feature directory.
758
+
759
+ #### How It Works
760
+
761
+ 1. Create a file with the `__inherit__` prefix in your feature's template directory
762
+ 2. Copy the base app file's contents to the `__inherit__` file (for type checking and IDE support)
763
+ 3. When the feature is applied, the file is NOT copied to the target (the base app's file is used)
764
+ 4. The feature gets type safety and autocomplete for the inherited file
765
+
766
+ #### Why Use `__inherit__`?
767
+
768
+ **Problem**: You want to import a base app file in your feature code, but:
769
+
770
+ - If you don't have the file in your feature, TypeScript shows errors and IDE autocomplete doesn't work
771
+ - If you copy the file to your feature, it will overwrite the base app file when applied
772
+
773
+ **Solution**: Use `__inherit__` to keep the file in your feature for development, but skip it during application.
774
+
775
+ #### Automatic Import Path Fixing
776
+
777
+ **Important**: The CLI automatically fixes import paths in your feature files!
778
+
779
+ When you import from `__inherit__` files in your feature code:
780
+
781
+ ```typescript
782
+ // In your feature's template
783
+ import { routes } from "./__inherit__routes";
784
+ import AppLayout from "./__inherit__appLayout";
785
+ ```
786
+
787
+ The CLI automatically removes the `__inherit__` prefix when applying the feature:
788
+
789
+ ```typescript
790
+ // In the target app after applying
791
+ import { routes } from "./routes";
792
+ import AppLayout from "./appLayout";
793
+ ```
794
+
795
+ **Supported patterns:**
796
+
797
+ - `import ... from './__inherit__file'` → `import ... from './file'`
798
+ - `export ... from './__inherit__file'` → `export ... from './file'`
799
+ - `require('./__inherit__file')` → `require('./file')`
800
+ - `import('./__inherit__file')` → `import('./file')`
801
+ - Works with single quotes (`'`), double quotes (`"`), and backticks (`` ` ``)
802
+ - Works with relative paths (`../`, `./`)
803
+
804
+ **Only processes JavaScript/TypeScript files:**
805
+
806
+ - `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`
807
+ - Other files (`.md`, `.json`, etc.) are not processed
808
+
809
+ This means you can freely import from `__inherit__` files in your feature code, and the imports will "just work" when the feature is applied!
810
+
811
+ #### Examples
812
+
813
+ **Inherit routes for type safety:**
814
+
815
+ ```typescript
816
+ // Feature structure
817
+ template/
818
+ └── src/
819
+ ├── __inherit__routes.tsx // Copy of base app routes for types
820
+ └── index.tsx // Can import routes safely
821
+
822
+ // In your feature's index.tsx
823
+ import { routes } from './routes'; // TypeScript works!
824
+
825
+ // When applied:
826
+ // - routes.tsx from base app is used (not overwritten)
827
+ // - Your index.tsx can still import it
828
+ ```
829
+
830
+ **Inherit shared layout:**
831
+
832
+ ```
833
+ template/
834
+ └── src/
835
+ ├── __inherit__appLayout.tsx # Copy from base for types
836
+ └── pages/
837
+ └── MyPage.tsx # Can import appLayout safely
838
+ ```
839
+
840
+ #### Validation
841
+
842
+ The CLI validates `__inherit__` files:
843
+
844
+ 1. **No conflicts**: You cannot have both `routes.tsx` and `__inherit__routes.tsx` in the same template
845
+ - ❌ **Invalid**: Both `routes.tsx` and `__inherit__routes.tsx`
846
+ - ✅ **Valid**: Only `__inherit__routes.tsx`
847
+
848
+ 2. **Base file must exist**: The file must exist in the base app
849
+ - ❌ **Invalid**: `__inherit__nonexistent.tsx` when file doesn't exist in base app
850
+ - ✅ **Valid**: `__inherit__routes.tsx` when `routes.tsx` exists in base app
851
+
852
+ If validation fails, you'll see clear error messages:
853
+
854
+ ```
855
+ Validation error: Cannot inherit file that doesn't exist!
856
+
857
+ File marked for inheritance: src/routes.tsx
858
+ Expected location in base app: /path/to/base/src/routes.tsx
859
+ The file doesn't exist in the base app.
860
+ ```
861
+
862
+ #### Use Cases
863
+
864
+ - **Type-safe imports**: Import base app files in your feature with full TypeScript support
865
+ - **Shared layouts**: Reference base app layouts without overwriting them
866
+ - **Route definitions**: Import base routes to extend or reference them
867
+ - **Shared utilities**: Reference base app utility functions with autocomplete
868
+
869
+ #### Complete Example
870
+
871
+ ```typescript
872
+ // Base app has: src/routes.tsx, src/appLayout.tsx
873
+
874
+ // Feature template structure:
875
+ template/
876
+ └── src/
877
+ ├── __inherit__routes.tsx # Copy from base (for types only)
878
+ ├── __inherit__appLayout.tsx # Copy from base (for types only)
879
+ └── pages/
880
+ └── Dashboard.tsx # New page that imports both
881
+
882
+ // In template/src/pages/Dashboard.tsx (during development):
883
+ import { routes } from '../__inherit__routes'; // Import from __inherit__ file
884
+ import AppLayout from '../__inherit__appLayout'; // TypeScript works!
885
+
886
+ export default function Dashboard() {
887
+ return <AppLayout>Dashboard using {routes.length} routes</AppLayout>;
888
+ }
889
+
890
+ // When applied, Dashboard.tsx is automatically transformed to:
891
+ import { routes } from '../routes'; // __inherit__ removed!
892
+ import AppLayout from '../appLayout'; // __inherit__ removed!
893
+
894
+ export default function Dashboard() {
895
+ return <AppLayout>Dashboard using {routes.length} routes</AppLayout>;
896
+ }
897
+
898
+ // Final result in target app:
899
+ // - src/routes.tsx: inherited from base (not overwritten) ✓
900
+ // - src/appLayout.tsx: inherited from base (not overwritten) ✓
901
+ // - src/pages/Dashboard.tsx: added from feature with fixed imports ✓
902
+ ```
903
+
904
+ ### Prepending and Appending to Base Files
905
+
906
+ Features can add content to the beginning (`__prepend__`) or end (`__append__`) of existing base app files. This is useful for adding styles, imports, or configuration to base files without completely replacing them.
907
+
908
+ #### How It Works
909
+
910
+ 1. **Prepend**: Content from the feature file is added **before** the base file's content
911
+ 2. **Append**: Content from the feature file is added **after** the base file's content
912
+ 3. The prefix can appear anywhere in the path
913
+ 4. Import paths with `__inherit__` are automatically fixed in the content
914
+
915
+ #### Examples
916
+
917
+ **Append CSS to global styles:**
918
+
919
+ ```
920
+ template/
921
+ └── src/
922
+ └── styles/
923
+ └── __append__global.css # Adds content after base global.css
924
+ ```
925
+
926
+ ```css
927
+ /* Feature's __append__global.css */
928
+ :root {
929
+ --feature-color: #066afe;
930
+ --feature-background: #ffffff;
931
+ }
932
+
933
+ .feature-specific {
934
+ color: var(--feature-color);
935
+ }
936
+ ```
937
+
938
+ **Result in target app's `global.css`:**
939
+
940
+ ```css
941
+ /* Base app's existing content */
942
+ @import "tailwindcss";
943
+
944
+ body {
945
+ margin: 0;
946
+ }
947
+
948
+ /* Content appended from feature */
949
+ :root {
950
+ --feature-color: #066afe;
951
+ --feature-background: #ffffff;
952
+ }
953
+
954
+ .feature-specific {
955
+ color: var(--feature-color);
956
+ }
957
+ ```
958
+
959
+ **Prepend imports to a TypeScript file:**
960
+
961
+ ```
962
+ template/
963
+ └── src/
964
+ └── __prepend__index.tsx # Adds imports before base index.tsx
965
+ ```
966
+
967
+ ```typescript
968
+ /* Feature's __prepend__index.tsx */
969
+ import { initializeFeature } from "./feature-init";
970
+
971
+ initializeFeature();
972
+ ```
973
+
974
+ **Result in target app's `index.tsx`:**
975
+
976
+ ```typescript
977
+ /* Content prepended from feature */
978
+ import { initializeFeature } from "./feature-init";
979
+
980
+ initializeFeature();
981
+
982
+ /* Base app's existing content */
983
+ import React from "react";
984
+ import ReactDOM from "react-dom";
985
+ // ... rest of base file
986
+ ```
987
+
988
+ #### Validation
989
+
990
+ The CLI validates prepend/append operations:
991
+
992
+ 1. **No conflicts**: You cannot target the same file with multiple operations
993
+ - ❌ **Invalid**: Both `__prepend__global.css` and `__append__global.css`
994
+ - ❌ **Invalid**: Both `global.css` and `__append__global.css`
995
+ - ✅ **Valid**: Only `__append__global.css`
996
+
997
+ 2. **Base file must exist**: The target file must exist in the base app
998
+ - ❌ **Invalid**: `__append__nonexistent.css` when file doesn't exist
999
+ - ✅ **Valid**: `__append__global.css` when `global.css` exists in base app
1000
+
1001
+ If validation fails, you'll see clear error messages:
1002
+
1003
+ ```
1004
+ Path conflict detected!
1005
+
1006
+ The following paths resolve to the same target file:
1007
+ 1. src/styles/__append__global.css (append)
1008
+ 2. src/styles/__prepend__global.css (prepend)
1009
+ → Both target: src/styles/global.css
1010
+
1011
+ You cannot have multiple files targeting the same path.
1012
+ ```
1013
+
1014
+ #### Automatic Import Fixing
1015
+
1016
+ When prepending or appending TypeScript/JavaScript files, import paths with `__inherit__` are automatically fixed:
1017
+
1018
+ ```typescript
1019
+ // In feature's __prepend__index.tsx
1020
+ import { routes } from "./__inherit__routes";
1021
+
1022
+ // After prepending to target app
1023
+ import { routes } from "./routes"; // __inherit__ removed!
1024
+ ```
1025
+
1026
+ This works the same as regular file operations (see "Automatic Import Path Fixing" above).
1027
+
1028
+ #### Use Cases
1029
+
1030
+ **Prepend:**
1031
+
1032
+ - Add initialization code at the start of entry files
1033
+ - Add imports before existing code
1034
+ - Add type declarations or interfaces
1035
+
1036
+ **Append:**
1037
+
1038
+ - Add CSS variables and styles to global stylesheets
1039
+ - Add routes or configuration entries
1040
+ - Extend existing files with additional functionality
1041
+ - Add utility functions or helpers
1042
+
1043
+ #### Complete Example
1044
+
1045
+ ```typescript
1046
+ // Base app has: src/styles/global.css
1047
+ // with Tailwind configuration
1048
+
1049
+ // Feature adds Salesforce Design Tokens by appending
1050
+
1051
+ // Feature structure:
1052
+ template/
1053
+ └── src/
1054
+ └── styles/
1055
+ └── __append__global.css
1056
+
1057
+ // Feature's __append__global.css:
1058
+ :root {
1059
+ /* Salesforce Design Tokens */
1060
+ --electric-blue-50: #066afe;
1061
+ --constant-white: #ffffff;
1062
+ }
1063
+
1064
+ button {
1065
+ background-color: var(--electric-blue-50);
1066
+ color: var(--constant-white);
1067
+ }
1068
+
1069
+ // Final result in target app's global.css:
1070
+ @import "tailwindcss"; // ← Base content
1071
+
1072
+ body {
1073
+ @apply antialiased; // ← Base content
1074
+ }
1075
+
1076
+ :root {
1077
+ /* Salesforce Design Tokens */
1078
+ --electric-blue-50: #066afe; // ← Appended content
1079
+ --constant-white: #ffffff; // ← Appended content
1080
+ }
1081
+
1082
+ button {
1083
+ background-color: var(--electric-blue-50); // ← Appended content
1084
+ color: var(--constant-white); // ← Appended content
1085
+ }
1086
+ ```
1087
+
1088
+ ## Route Merging Strategy
1089
+
1090
+ The `merge` change type with `routes` strategy intelligently combines route definitions from features with base app routes.
1091
+
1092
+ ### How It Works
1093
+
1094
+ **Replace-Matching with Deep Children Merge:**
1095
+
1096
+ 1. **Top-Level Routes:**
1097
+ - Routes with the **same path** → Merge their children arrays
1098
+ - Routes with **different paths** → Add feature route to result
1099
+
1100
+ 2. **Children Array Merging** (when parent paths match):
1101
+ - Index routes (`index: true`) → Feature replaces base if both exist
1102
+ - Named routes (`path: 'about'`) → Feature replaces base if paths match
1103
+ - **New routes** → Added to children array
1104
+ - **Base routes not in feature** → Preserved
1105
+
1106
+ 3. **Route Deletion:**
1107
+ - Routes with path starting with `__delete__` → Remove matching route from result
1108
+ - Example: `path: '__delete__new'` removes the route with `path: 'new'`
1109
+ - Throws error if route to delete doesn't exist
1110
+
1111
+ 4. **Recursion:**
1112
+ - Applies same logic to nested children arrays
1113
+
1114
+ ### Example
1115
+
1116
+ **Base App Routes**:
1117
+
1118
+ ```typescript
1119
+ export const routes: RouteObject[] = [
1120
+ {
1121
+ path: '/',
1122
+ element: <AppLayout />,
1123
+ children: [
1124
+ {
1125
+ index: true,
1126
+ element: <Home />,
1127
+ handle: { showInNavigation: true, label: 'Home' }
1128
+ }
1129
+ ]
1130
+ }
1131
+ ]
1132
+ ```
1133
+
1134
+ **Feature Routes** (in `template/uiBundles/<feature-name>/src/routes.tsx`):
1135
+
1136
+ ```typescript
1137
+ export const routes: RouteObject[] = [
1138
+ {
1139
+ path: '/',
1140
+ element: <AppLayout />,
1141
+ children: [
1142
+ {
1143
+ path: 'about',
1144
+ element: <About />,
1145
+ handle: { showInNavigation: true, label: 'About' }
1146
+ },
1147
+ {
1148
+ path: 'contact',
1149
+ element: <Contact />,
1150
+ handle: { showInNavigation: false }
1151
+ }
1152
+ ]
1153
+ }
1154
+ ]
1155
+ ```
1156
+
1157
+ **Merged Result** (after applying feature):
1158
+
1159
+ ```typescript
1160
+ export const routes: RouteObject[] = [
1161
+ {
1162
+ path: '/',
1163
+ element: <AppLayout />, // Uses feature's element
1164
+ children: [
1165
+ {
1166
+ index: true,
1167
+ element: <Home />, // ✓ Preserved from base
1168
+ handle: { showInNavigation: true, label: 'Home' }
1169
+ },
1170
+ {
1171
+ path: 'about',
1172
+ element: <About />, // ✓ Added from feature
1173
+ handle: { showInNavigation: true, label: 'About' }
1174
+ },
1175
+ {
1176
+ path: 'contact',
1177
+ element: <Contact />, // ✓ Added from feature
1178
+ handle: { showInNavigation: false }
1179
+ }
1180
+ ]
1181
+ }
1182
+ ]
1183
+ ```
1184
+
1185
+ ### Key Benefits
1186
+
1187
+ - **Preserves existing routes**: Base app's Home route stays intact
1188
+ - **Adds new routes**: Feature routes (About, Contact) are added
1189
+ - **No duplication**: Routes with matching paths are replaced, not duplicated
1190
+ - **Deep merging**: Works with nested route structures
1191
+ - **Multiple features**: Apply multiple route-adding features sequentially
1192
+
1193
+ Route merging happens automatically for `routes.tsx` files.
1194
+
1195
+ ### Deleting Routes
1196
+
1197
+ Features can delete routes from the base app or previously applied features by using the `__delete__` prefix in the route path. This is useful when a feature needs to remove routes that were added by dependencies or the base app.
1198
+
1199
+ #### How It Works
1200
+
1201
+ 1. Add a route with `path: '__delete__<route-name>'` in your feature's routes file
1202
+ 2. The route with the matching path (without the prefix) will be removed during merging
1203
+ 3. If the route doesn't exist, an error will be thrown
1204
+
1205
+ #### Example
1206
+
1207
+ **Base/Previous Routes:**
1208
+
1209
+ ```typescript
1210
+ export const routes: RouteObject[] = [
1211
+ {
1212
+ path: '/',
1213
+ element: <AppLayout />,
1214
+ children: [
1215
+ { index: true, element: <Home /> },
1216
+ { path: 'about', element: <About /> },
1217
+ { path: 'new', element: <New /> }
1218
+ ]
1219
+ }
1220
+ ]
1221
+ ```
1222
+
1223
+ **Feature Routes (deleting 'new'):**
1224
+
1225
+ ```typescript
1226
+ export const routes: RouteObject[] = [
1227
+ {
1228
+ path: '/',
1229
+ children: [
1230
+ {
1231
+ path: '__delete__new',
1232
+ element: <></> // Element value is ignored for deletion markers
1233
+ }
1234
+ ]
1235
+ }
1236
+ ]
1237
+ ```
1238
+
1239
+ **Merged Result:**
1240
+
1241
+ ```typescript
1242
+ export const routes: RouteObject[] = [
1243
+ {
1244
+ path: '/',
1245
+ element: <AppLayout />,
1246
+ children: [
1247
+ { index: true, element: <Home /> }, // ✓ Preserved
1248
+ { path: 'about', element: <About /> } // ✓ Preserved
1249
+ // 'new' route deleted ✓
1250
+ ]
1251
+ }
1252
+ ]
1253
+ ```
1254
+
1255
+ #### Validation
1256
+
1257
+ - **Error if route doesn't exist**: The CLI will throw an error if you attempt to delete a route that doesn't exist in the current routes
1258
+ - This prevents silent failures and ensures routes are being deleted as expected
1259
+
1260
+ #### Use Cases
1261
+
1262
+ - **Remove dependency routes**: Delete routes added by feature dependencies that aren't needed
1263
+ - **Clean up base routes**: Remove placeholder or example routes from the base app
1264
+ - **Override parent features**: Child features can remove routes added by parent features they depend on
1265
+
1266
+ For example, a feature that provides a single-page app might delete all routes from the base app to start fresh.
1267
+
1268
+ ### Automatic Import Cleanup for Deleted Files
1269
+
1270
+ When using route deletion (or file deletion with `__delete__` prefix), imports from deleted files are automatically removed during route merging. This prevents broken import references in the final merged code.
1271
+
1272
+ #### How It Works
1273
+
1274
+ 1. During route merging, after all imports are merged from the feature file
1275
+ 2. The import merger scans all import statements in the target file
1276
+ 3. Any imports with `__delete__` in the module specifier are automatically removed
1277
+ 4. This ensures deleted components don't leave broken import references
1278
+
1279
+ #### Example
1280
+
1281
+ **Feature Routes File:**
1282
+
1283
+ ```typescript
1284
+ import type { RouteObject } from "react-router";
1285
+ import AppLayout from "./__inherit__appLayout";
1286
+ import Home from ".";
1287
+ import New from "./__delete__new"; // Import from deleted file
1288
+
1289
+ export const routes: RouteObject[] = [
1290
+ {
1291
+ path: '/',
1292
+ element: <AppLayout />,
1293
+ children: [
1294
+ {
1295
+ index: true,
1296
+ element: <Home />,
1297
+ handle: { showInNavigation: true, label: 'Home' }
1298
+ },
1299
+ {
1300
+ path: '__delete__new', // Delete the 'new' route
1301
+ element: <New />,
1302
+ }
1303
+ ]
1304
+ }
1305
+ ]
1306
+ ```
1307
+
1308
+ **Merged Result:**
1309
+
1310
+ ```typescript
1311
+ import type { RouteObject } from "react-router";
1312
+ import AppLayout from "./appLayout";
1313
+ import Home from ".";
1314
+ // ✓ Import from ./__delete__new automatically removed
1315
+
1316
+ export const routes: RouteObject[] = [
1317
+ {
1318
+ path: '/',
1319
+ element: <AppLayout />,
1320
+ children: [
1321
+ {
1322
+ index: true,
1323
+ element: <Home />,
1324
+ handle: { showInNavigation: true, label: 'Home' }
1325
+ }
1326
+ // ✓ 'new' route deleted
1327
+ ]
1328
+ }
1329
+ ]
1330
+ ```
1331
+
1332
+ #### Key Benefits
1333
+
1334
+ - **No broken imports**: Automatically removes imports from deleted files
1335
+ - **Clean merged code**: Final output doesn't reference non-existent files
1336
+ - **Works with file deletion**: Applies to any file marked with `__delete__` prefix
1337
+ - **Seamless integration**: Happens automatically during route merging, no manual cleanup needed
1338
+
1339
+ ## Testing
1340
+
1341
+ The CLI has a comprehensive test suite using Vitest, including E2E tests with gold files and unit tests for critical utilities.
1342
+
1343
+ **Test Coverage:**
1344
+
1345
+ - ✅ 12 E2E test scenarios covering all major CLI workflows
1346
+ - ✅ 46+ unit tests covering route merging, import merging, and file operations
1347
+ - ✅ Gold file comparison for E2E validation
1348
+ - ✅ No skipped tests - all functionality fully tested
1349
+
1350
+ ### Running Tests
1351
+
1352
+ ```bash
1353
+ # Run all tests
1354
+ npm test
1355
+
1356
+ # Run with UI
1357
+ npm run test:ui
1358
+
1359
+ # Run E2E tests only
1360
+ npm run test:e2e
1361
+
1362
+ # Run unit tests only
1363
+ npm run test:unit
1364
+
1365
+ # Run with coverage
1366
+ npm run test:coverage
1367
+ ```
1368
+
1369
+ ### Test Structure
1370
+
1371
+ ```
1372
+ packages/cli/
1373
+ ├── test/
1374
+ │ ├── e2e/ # End-to-end tests
1375
+ │ │ ├── fixtures/ # Test fixtures (base apps, features)
1376
+ │ │ ├── gold/ # Expected outputs for E2E tests
1377
+ │ │ └── apply-patches.spec.ts
1378
+ │ ├── unit/ # Unit tests
1379
+ │ │ ├── route-merger.spec.ts
1380
+ │ │ ├── import-merger.spec.ts
1381
+ │ │ └── file-operations.spec.ts
1382
+ │ └── helpers/ # Test utilities
1383
+ │ ├── compare-directories.ts
1384
+ │ ├── create-temp-dir.ts
1385
+ │ └── fixtures.ts
1386
+ ```
1387
+
1388
+ ### E2E Tests
1389
+
1390
+ E2E tests verify complete CLI workflows using gold files (expected outputs). Tests cover:
1391
+
1392
+ - Simple feature application (adding routes/files)
1393
+ - File deletion with `__delete__` prefix
1394
+ - Route deletion and import cleanup
1395
+ - Feature dependency resolution
1396
+ - Complex operations (`__inherit__`, `__prepend__`, `__append__`)
1397
+ - Error handling and validation
1398
+
1399
+ ### Unit Tests
1400
+
1401
+ Unit tests focus on individual modules:
1402
+
1403
+ - **route-merger**: Route merging logic and deletion
1404
+ - **import-merger**: Import statement merging, deduplication, type imports, and formatting
1405
+ - **file-operations**: File deletion, prepending, and appending
1406
+
1407
+ ### Updating Gold Files
1408
+
1409
+ When intentionally changing CLI behavior, update gold files:
1410
+
1411
+ ```bash
1412
+ UPDATE_GOLD=1 npm test
1413
+ ```
1414
+
1415
+ ⚠️ **Warning**: Only update gold files after verifying the new output is correct!
1416
+
1417
+ ### Creating New Tests
1418
+
1419
+ **E2E Test Example**:
1420
+
1421
+ ```typescript
1422
+ it("should apply a simple feature correctly", async () => {
1423
+ const outputDir = copyFixture("base-app", join(tempDir, "output"));
1424
+ const featurePath = getFixturePath("feature-simple");
1425
+
1426
+ await applyPatchesCommand(featurePath, outputDir, {
1427
+ skipDependencyChanges: true,
1428
+ });
1429
+
1430
+ const goldDir = getGoldPath("simple-apply");
1431
+ const differences = compareOrUpdate(outputDir, goldDir);
1432
+
1433
+ expect(differences).toEqual([]);
1434
+ });
1435
+ ```
1436
+
1437
+ **Unit Test Example**:
1438
+
1439
+ ```typescript
1440
+ it("should merge routes correctly", () => {
1441
+ const project = new Project({ useInMemoryFileSystem: true });
1442
+
1443
+ const targetFile = project.createSourceFile("target.tsx", `...`);
1444
+ const featureFile = project.createSourceFile("feature.tsx", `...`);
1445
+
1446
+ const result = mergeRoutes("feature.tsx", "target.tsx", project);
1447
+
1448
+ expect(result).toContain("expected content");
1449
+ });
1450
+ ```
1451
+
1452
+ ## Nx Plugin Executors
1453
+
1454
+ This package provides Nx executors for integrating the template CLI with Nx workspaces. The executors automate applying patches and building feature apps within the monorepo.
1455
+
1456
+ ### Available Executors
1457
+
1458
+ #### `apply-patches`
1459
+
1460
+ Applies template patches to create feature apps. This executor wraps the `apply-patches` CLI command and automatically determines paths based on the Nx project configuration.
1461
+
1462
+ **Usage in `package.json`:**
1463
+
1464
+ ```json
1465
+ {
1466
+ "nx": {
1467
+ "targets": {
1468
+ "build": {
1469
+ "executor": "@salesforce/ui-bundle-template-cli:apply-patches"
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ ```
1475
+
1476
+ **Options:**
1477
+
1478
+ - `baseAppPath` (string, optional): Path to the base app directory (relative to workspace root)
1479
+ - Default: `"packages/template/base-app/base-react-app"`
1480
+ - `reset` (boolean, optional): Reset target directory to base app state before applying patches
1481
+ - Default: `true`
1482
+ - `skipDependencyChanges` (boolean, optional): Skip installing dependencies from package.json
1483
+ - Default: `false`
1484
+ - `outputDir` (string, optional): Output directory name (relative to project root)
1485
+ - Default: `"dist"`
1486
+
1487
+ **Example with options:**
1488
+
1489
+ ```json
1490
+ {
1491
+ "nx": {
1492
+ "targets": {
1493
+ "build": {
1494
+ "executor": "@salesforce/ui-bundle-template-cli:apply-patches",
1495
+ "options": {
1496
+ "reset": true,
1497
+ "skipDependencyChanges": false,
1498
+ "outputDir": "dist"
1499
+ }
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+ ```
1505
+
1506
+ **Workspace-level configuration (`nx.json`):**
1507
+
1508
+ You can set default options for all projects in `nx.json`:
1509
+
1510
+ ```json
1511
+ {
1512
+ "targetDefaults": {
1513
+ "@salesforce/ui-bundle-template-cli:apply-patches": {
1514
+ "cache": true,
1515
+ "inputs": [
1516
+ "{projectRoot}/src/**/*",
1517
+ "{projectRoot}/patches.ts",
1518
+ "{projectRoot}/package.json",
1519
+ "{workspaceRoot}/packages/template/base-app/base-react-app/**/*"
1520
+ ],
1521
+ "outputs": ["{projectRoot}/dist"],
1522
+ "options": {
1523
+ "reset": true,
1524
+ "skipDependencyChanges": false
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ ```
1530
+
1531
+ #### `build-dist-app`
1532
+
1533
+ Builds the application in the dist folder by running `npm ci` (or `npm install` when no `package-lock.json` exists) and `npm run build`. This executor is useful for building feature apps after patches have been applied.
1534
+
1535
+ **Usage in `package.json`:**
1536
+
1537
+ ```json
1538
+ {
1539
+ "nx": {
1540
+ "targets": {
1541
+ "test": {
1542
+ "executor": "@salesforce/ui-bundle-template-cli:build-dist-app"
1543
+ }
1544
+ }
1545
+ }
1546
+ }
1547
+ ```
1548
+
1549
+ **Options:**
1550
+
1551
+ This executor has no configurable options. It automatically determines the build path based on the project structure.
1552
+
1553
+ **Workspace-level configuration (`nx.json`):**
1554
+
1555
+ ```json
1556
+ {
1557
+ "targetDefaults": {
1558
+ "@salesforce/ui-bundle-template-cli:build-dist-app": {
1559
+ "cache": true,
1560
+ "inputs": ["{projectRoot}/dist/**/*"],
1561
+ "outputs": ["{projectRoot}/dist"],
1562
+ "dependsOn": ["build"]
1563
+ }
1564
+ }
1565
+ }
1566
+ ```
1567
+
1568
+ ### Running Executors
1569
+
1570
+ Once configured, you can run the executors using Nx:
1571
+
1572
+ ```bash
1573
+ # Apply patches to a feature project
1574
+ nx build feature-react-global-search
1575
+
1576
+ # Build the dist app
1577
+ nx test feature-react-global-search
1578
+
1579
+ # Run both in sequence (test depends on build)
1580
+ nx run-many -t build test
1581
+ ```
1582
+
1583
+ ### How It Works
1584
+
1585
+ 1. **apply-patches executor**:
1586
+ - Reads project configuration from Nx workspace
1587
+ - Determines feature path and base app path
1588
+ - Calls the CLI's `applyPatchesCommand` function
1589
+ - Creates the dist directory with merged feature and base app files
1590
+
1591
+ 2. **build-dist-app executor**:
1592
+ - Locates the built application in `dist/force-app/main/default/uiBundles/<project-name>/`
1593
+ - Runs `npm ci` to install dependencies (or `npm install` when no `package-lock.json` exists)
1594
+ - Runs `npm run build` to build the application
1595
+ - Suitable for CI/CD pipelines and automated testing
1596
+
1597
+ ### Benefits
1598
+
1599
+ - **Nx Caching**: Both executors support Nx caching for faster builds
1600
+ - **Dependency Graph**: Nx understands dependencies between features and builds them in correct order
1601
+ - **Workspace Integration**: Seamlessly integrates with Nx workspace commands
1602
+ - **Type Safety**: Full TypeScript support with schema validation
1603
+ - **Incremental Builds**: Only rebuilds projects when inputs change
1604
+
1605
+ ## Development
1606
+
1607
+ ```bash
1608
+ # Build the CLI
1609
+ npm run build
1610
+
1611
+ # Run without building (development)
1612
+ npm run dev -- <feature> <base-app>
1613
+ ```