CLI tool for applying feature patches to base apps
npm install @sfdc-webapps/cliCLI tool for applying feature patches to apps in the webapps-templates monorepo.
Build the CLI from the monorepo root:
``bash`
yarn build --workspace=@sfdc-webapps/cli
`bashApply patches to a target directory (base app is used as reference only)
yarn apply-patches
$3
`bash
From monorepo root
node packages/cli/dist/index.js With flags
node packages/cli/dist/index.js my-app --skip-dependency-changes --reset
`$3
Create a new feature package from the base-feature template:
`bash
Create a new feature (will be prefixed with "feature-")
yarn new-feature Examples:
yarn new-feature navigation # Creates packages/feature-navigation
yarn new-feature user-dashboard # Creates packages/feature-user-dashboard
yarn new-feature api-client # Creates packages/feature-api-client
`#### Direct usage
`bash
From monorepo root
node packages/cli/dist/index.js new-feature
`#### Feature Name Requirements
- Format: Must be in kebab-case (lowercase with hyphens)
- Characters: Only alphanumeric characters and hyphens allowed
- Cannot: Start or end with hyphens, have consecutive hyphens
- Reserved: Cannot be "base" or "cli"
- Auto-prefix: Automatically prefixed with "feature-" (e.g.,
nav-menu → feature-nav-menu)Examples:
- ✅
navigation → feature-navigation
- ✅ user-auth → feature-user-auth
- ✅ dashboard-v2 → feature-dashboard-v2
- ❌ Navigation (uppercase)
- ❌ user_auth (underscore)
- ❌ --menu (starts with hyphen)
- ❌ nav--menu (consecutive hyphens)#### What the command does
The
new-feature command:1. Validates the feature name format
2. Checks that the feature doesn't already exist
3. Copies the base-feature template to
packages/feature-{name}/
4. Renames the template directory from base-feature to {name}
5. Updates package.json with the new feature name
6. Updates tsconfig.app.json with the new directory path
7. Creates a ready-to-use feature packageThe created feature will have:
- ✅ Correct
package.json configuration
- ✅ TypeScript configuration
- ✅ Feature structure following conventions
- ✅ Ready for customization and development#### Next steps after creation
`bash
cd packages/feature-{name}
1. Customize template files in template/ directory
2. Update feature.ts with feature configuration
3. Test with: yarn apply-patches packages/feature-{name} packages/base-react-app test-app
`$3
-
: Path to the feature directory (can be relative to monorepo root or absolute). The feature must contain a feature.ts file.
- : 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.
- : 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.
- --skip-dependency-changes: Skip installing dependencies from package.json. Only file changes will be applied.
- --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).What it does
The CLI tool:
1. Validates that the specified feature path exists and contains a feature.ts file, and the base app path is valid
2. Prepares target directory: Creates the target directory (or resets it to base app state if
--reset flag is used, preserving node_modules)
3. Resolves dependencies: Recursively resolves all feature dependencies, detecting circular dependencies and building an ordered list where dependencies are applied before dependent features
4. Loads the feature definitions from each feature.ts file in dependency order
5. Discovers files from each feature's template directory (defaults to template, configurable via templateDir in feature.ts)
6. Validates paths: For each feature, ensures:
- No conflicting paths exist (e.g., both routes.tsx and __delete__routes.tsx, or __prepend__global.css and __append__global.css)
- Files marked with __delete__, __inherit__, __prepend__, or __append__ exist in the base app
7. Applies file changes from each feature's template directory in dependency order:
- Delete operations: Removes files/directories marked with __delete__ prefix from the target app
- Inherit operations: Skips files marked with __inherit__ (inherited from base app)
- Prepend operations: Adds content from feature file before the base file's content (for files marked with __prepend__)
- Append operations: Adds content from feature file after the base file's content (for files marked with __append__)
- Import path fixing: Automatically removes __inherit__ prefix from import statements in JS/TS files
- File changes: Copies each file from the template directory to the target
- Route merging: Intelligently merges route files (routes.tsx), accumulating routes from all features
8. Aggregates and installs dependencies: Collects all NPM dependencies from all features and installs them in a single yarn command (unless --skip-dependency-changes is used). Detects and errors on version conflicts.Examples
$3
`bash
$ yarn apply-patches packages/feature-navigation-menu packages/base-react-app my-appApplying patches: packages/feature-navigation-menu → my-app
ℹ Validating paths...
✓ Validation passed
ℹ Creating target directory my-app...
✓ Target directory created
Resolving Dependencies
ℹ No dependencies to resolve
Applying: packages/feature-navigation-menu
ℹ Discovering files...
ℹ Found 6 file(s)
ℹ Validating paths...
✓ Paths validated
✓ Added digitalExperiences/webApplications/feature-navigation-menu/src/navigationMenu.tsx
✓ Added digitalExperiences/webApplications/feature-navigation-menu/src/appLayout.tsx
✓ Added digitalExperiences/webApplications/feature-navigation-menu/src/routes.tsx
✓ Added digitalExperiences/webApplications/feature-navigation-menu/src/router-utils.tsx
✓ Added digitalExperiences/webApplications/feature-navigation-menu/src/about.tsx
✓ Added digitalExperiences/webApplications/feature-navigation-menu/src/new.tsx
Installing dependencies
ℹ Installing dependencies...
[yarn output]
✓ Dependencies installed
✓ Success
✓ Created: /path/to/monorepo/my-app
`Creating Features
Quick Start: Use the CLI to create a new feature from the template:
`bash
yarn new-feature your-feature-name
`This creates a new feature at
packages/feature-your-feature-name/ with all the necessary configuration files. Then customize the template files in the template/ directory.For manual setup or advanced configuration, see below...
---
Features are defined in your feature's
feature.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.$3
`
packages/feature-my-feature/
├── feature.ts # Feature configuration
├── package.json # NPM dependencies for development
└── template/ # Template files (default directory name)
├── webApp/ # Web application files (mapped to digitalExperiences/webApplications//)
│ └── src/
│ ├── routes.tsx
│ ├── component1.tsx
│ └── component2.tsx
└── classes/ # SFDX metadata (placed at root level in dist)
└── MyClass.cls
`Note: The CLI handles two types of files differently:
- Web Application files (under
webApp/): Automatically mapped to digitalExperiences/webApplications/
- SFDX metadata files (like classes/, triggers/, objects/, etc.): Placed at root level in the output directoryThis structure ensures proper organization for both digital experience applications and Salesforce metadata.
$3
The feature configuration file specifies:
-
templateDir: Directory containing template files (defaults to template)
- webAppName: Name of the web application (defaults to the feature name extracted from the directory). This is used for constructing the default route path.
- routeFilePath: Path to the routes file for merging (defaults to digitalExperiences/webApplications/)
- packageJson: NPM dependencies to install in the target app
- dependencies: Array of other features this feature depends on (applied first)$3
`typescript
import type { Feature } from '../cli/src/types.js';const feature: Feature = {
// All fields are optional with sensible defaults
templateDir: 'template', // Optional, defaults to 'template'
webAppName: 'my-feature', // Optional, defaults to feature directory name
// routeFilePath defaults to 'digitalExperiences/webApplications//src/routes.tsx'
packageJson: {
dependencies: {
'react-router': '^7.10.1',
},
},
};
export default feature;
`$3
`typescript
import type { Feature } from '../cli/src/types.js';const feature: Feature = {
// This feature depends on navigation-menu feature
// navigation-menu will be applied first, then this feature
dependencies: ['packages/feature-navigation-menu'],
packageJson: {
dependencies: {
'some-package': '^1.0.0',
},
},
};
export default feature;
`$3
`typescript
import type { Feature } from '../cli/src/types.js';const feature: Feature = {
templateDir: 'src', // Use 'src' instead of 'template'
webAppName: 'custom-app-name', // Override default app name
routeFilePath: 'custom/path/to/routes.tsx', // Custom route file path
packageJson: {
dependencies: {
'react-router': '^7.10.1',
},
},
};
export default feature;
`Notes:
-
templateDir: All files in this directory will be discovered and applied to the target app
- webAppName: Used to construct the default route path and organize files. Defaults to the feature directory name (e.g., feature-navigation-menu → feature-navigation-menu)
- routeFilePath: Must be a path relative to templateDir. If not specified, defaults to digitalExperiences/webApplications/Path Mappings
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.
$3
By default, all features automatically get the
webApp mapping, which transforms web application files into the proper nested structure. For example, in feature-navigation-menu:`
template/webApp/src/app.tsx → dist/digitalExperiences/webApplications/feature-navigation-menu/src/app.tsx
`This simplifies feature templates by removing the repetitive nested directory structure.
Important: Only files under
webApp/ get the nested structure. SFDX metadata types (like classes/, triggers/, objects/, lwc/, etc.) are placed at root level:`
feature-navigation-menu/template/
├── webApp/
│ └── src/
│ └── app.tsx → dist/digitalExperiences/webApplications/feature-navigation-menu/src/app.tsx
└── classes/
└── NavMenu.cls → dist/classes/NavMenu.cls (root level)
`$3
Simply organize your template files under
webApp/ for web application code, and at the root level for SFDX metadata:`typescript
// feature.ts - No configuration needed
export default {};
``
template/
├── webApp/ # Web application files (automatically mapped)
│ └── src/
│ ├── routes.tsx
│ ├── app.tsx
│ └── components/
│ └── Header.tsx
└── classes/ # SFDX metadata (placed at root level)
└── MyClass.cls
`Result:
- Web app files go to
dist/digitalExperiences/webApplications/feature-name/src/
- SFDX metadata stays at root: dist/classes/MyClass.cls$3
Use full paths when you need precise control or for backwards compatibility:
`typescript
// feature.ts
export default {
pathMappings: {
enabled: false // Disable automatic mapping
}
};
``
template/
└── digitalExperiences/ # Use full structure
└── webApplications/
└── /
└── src/
└── routes.tsx
`$3
Define custom mappings for non-standard structures:
`typescript
// feature.ts
export default {
pathMappings: {
mappings: [
{
from: 'web',
to: 'digitalExperiences/webApplications/custom-app-name'
}
]
}
};
``
template/
└── web/ # Custom prefix
└── src/
└── app.tsx
`Result: Maps
web/src/app.tsx → digitalExperiences/webApplications/custom-app-name/src/app.tsx$3
You can define multiple mappings in one feature:
`typescript
export default {
pathMappings: {
mappings: [
{ from: 'webApp', to: 'digitalExperiences/webApplications/my-app' },
{ from: 'shared', to: 'digitalExperiences/shared-resources' }
]
}
};
`$3
You can mix web application files, SFDX metadata, and full paths in the same feature:
`
template/
├── webApp/ # Mapped to digitalExperiences/webApplications//
│ └── src/
│ └── app.tsx
├── classes/ # SFDX metadata (placed at root)
│ └── MyClass.cls
├── triggers/ # SFDX metadata (placed at root)
│ └── MyTrigger.trigger
└── digitalExperiences/ # Full paths (passed through)
└── siteAssets/
└── logo.png
`All formats work together seamlessly:
-
webApp/ files → digitalExperiences/webApplications/
- SFDX metadata (classes/, triggers/, objects/, lwc/, etc.) → Root level
- Full paths (already containing digitalExperiences/) → Used as-is$3
Example 1: Default mapping (recommended)
`typescript
export default {}; // That's it!
`Example 2: Opt-out for backwards compatibility
`typescript
export default {
pathMappings: { enabled: false }
};
`Example 3: Custom app name
`typescript
export default {
pathMappings: {
mappings: [
{ from: 'webApp', to: 'digitalExperiences/webApplications/custom-name' }
]
}
};
`Example 4: Completely custom structure
`typescript
export default {
pathMappings: {
mappings: [
{ from: 'src', to: 'app/sources' },
{ from: 'assets', to: 'public/static' }
]
}
};
`$3
1. Discovery: CLI discovers all files in your template directory
2. Mapping: Each file path is checked against mapping rules (first match wins)
3. Transformation: Matching prefix is replaced with target prefix
4. Pass-Through: Paths that don't match any mapping are used as-is
5. Application: Transformed paths are used for file operations
This ensures:
- ✅ Backwards compatibility (old features still work)
- ✅ Simplified templates (new features are easier to create)
- ✅ Flexibility (custom mappings for special cases)
- ✅ No breaking changes (opt-in for existing features, opt-out available)
Feature Dependencies
Features can depend on other features. Dependencies are automatically resolved and applied in the correct order.
$3
1. Declaration: Specify dependencies in your
feature.ts file
2. Resolution: CLI recursively resolves all dependencies (including nested dependencies)
3. Ordering: Dependencies are always applied before the feature that depends on them
4. Circular Detection: CLI detects and prevents circular dependencies
5. File Layering: Files from dependencies can be overridden by dependent features (main feature wins)
6. Route Accumulation: Routes from all features are merged together$3
When you apply a feature with dependencies:
`
Feature A depends on Feature B
Feature B depends on Feature CApplication order: C → B → A (dependencies first)
`The CLI builds a complete dependency graph and applies features in topological order.
$3
`typescript
// packages/feature-navigation-menu/feature.ts
import type { Feature } from '../cli/src/types.js';const feature: Feature = {
// Navigation menu has no dependencies
};
export default feature;
``typescript
// packages/feature-admin-dashboard/feature.ts
import type { Feature } from '../cli/src/types.js';const feature: Feature = {
// Admin dashboard builds on top of navigation menu
dependencies: ['packages/feature-navigation-menu'],
};
export default feature;
`When you apply
feature-admin-dashboard:
1. CLI resolves feature-navigation-menu as a dependency
2. Applies feature-navigation-menu first (navigation menu files and routes)
3. Applies feature-admin-dashboard second (dashboard files and routes)
4. Result: App has both navigation menu and admin dashboard$3
Dependencies can have their own dependencies. The CLI resolves them recursively:
`
Feature App depends on Feature Dashboard
Feature Dashboard depends on Feature Navigation
Feature Navigation depends on Feature AuthApplication order: Auth → Navigation → Dashboard → App
`$3
The CLI detects and prevents circular dependencies:
`typescript
// Feature A depends on B
// Feature B depends on C
// Feature C depends on A// CLI will error:
// "Circular dependency detected:
// packages/feature-a → packages/feature-b → packages/feature-c → packages/feature-a"
`Fix: Remove one of the dependencies to break the cycle.
$3
When multiple features modify the same file:
- Dependencies applied first: Their files are written to the target
- Main feature applied last: Its files overwrite dependency files
- Result: Main feature can customize/override dependency behavior
Example:
`
feature-navigation-menu provides: src/appLayout.tsx
feature-custom-app also provides: src/appLayout.tsxWhen applying feature-custom-app:
1. navigation-menu's appLayout.tsx is applied
2. custom-app's appLayout.tsx overwrites it
3. Final result: custom-app's version is used
`$3
Routes from all features are merged together:
Base App Routes:
`typescript
export const routes = [
{
path: '/',
children: [
{ index: true, element: }
]
}
];
`navigation-menu Routes:
`typescript
export const routes = [
{
path: '/',
children: [
{ path: 'about', element: },
{ path: 'contact', element: }
]
}
];
`Final Merged Routes:
`typescript
export const routes = [
{
path: '/',
children: [
{ index: true, element: }, // From base
{ path: 'about', element: }, // From navigation-menu
{ path: 'contact', element: } // From navigation-menu
]
}
];
`Routes accumulate across all features, preserving routes from base app and all dependencies.
$3
Dependency paths can be:
- Relative to monorepo root:
'packages/feature-navigation-menu'
- Absolute paths: '/absolute/path/to/feature'The CLI normalizes and resolves all paths consistently.
$3
When multiple features depend on the same feature:
`
Feature A depends on Feature C
Feature B depends on Feature C
Feature Main depends on A and BDependency graph:
Main
/ \
A B
\ /
C
`Resolution: Feature C is applied once (not duplicated).
Application order:
C → A → B → Main$3
When using watch mode, dependencies are:
- Applied once on initial startup with reset to ensure clean state
- Re-applied when template files change (always resets to base app state, preserving node_modules)
- Not watched for changes (only the main feature is watched)
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.
File Application
The CLI discovers all files in your feature's template directory and applies them to the target app:
1. Standard Files: Copied directly to the target, creating directories as needed
2. Delete Markers: Files/directories prefixed with
__delete__ mark files for deletion from the target app
3. Inherit Markers: Files prefixed with __inherit__ are kept in the feature for type safety but not copied (inherited from base app)
4. Prepend Markers: Files prefixed with __prepend__ add their content before the base file's content
5. Append Markers: Files prefixed with __append__ add their content after the base file's content
6. Route Files (routes.tsx): Intelligently merged with existing routes to preserve base app routes$3
Features can delete files or directories from the target app by using the
__delete__ prefix in the template directory.#### How It Works
1. Create a file with the
__delete__ prefix in your feature's template directory
2. The prefix can appear anywhere in the path
3. When the feature is applied, the corresponding file/directory will be deleted from the target app
4. The content of the delete marker file is ignored (can be empty or contain comments)#### Examples
Delete a single file:
`
template/
└── src/
└── __delete__routes.tsx # Deletes src/routes.tsx from target app
`Delete a directory:
`
template/
└── src/
└── __delete__pages/ # Deletes src/pages/ directory from target app
└── .gitkeep # Placeholder file (content ignored)
`Delete from nested path:
`
template/
└── src/
└── components/
└── __delete__Footer.tsx # Deletes src/components/Footer.tsx
`Delete a parent directory:
`
template/
└── __delete__src/
└── legacy/ # Deletes src/legacy/ directory from target app
└── .gitkeep
`#### Validation
The CLI validates that you don't have conflicting paths:
- ❌ Invalid: Having both
routes.tsx and __delete__routes.tsx in the same template
- ✅ Valid: Having only __delete__routes.tsx (to delete) or only routes.tsx (to add/update)If a conflict is detected, the CLI will throw an error:
`
Path conflict detected: "src/routes.tsx" appears multiple times in the template.
This can happen when both a file and its __delete__ marker exist.
Please remove one of them.
`#### Use Cases
- Remove obsolete files: Delete deprecated components or utilities that the feature replaces
- Clean up after refactoring: Remove files that are no longer needed with the new feature
- Remove base app scaffolding: Delete placeholder files from the base app that the feature supersedes
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.$3
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.#### How It Works
1. Create a file with the
__inherit__ prefix in your feature's template directory
2. Copy the base app file's contents to the __inherit__ file (for type checking and IDE support)
3. When the feature is applied, the file is NOT copied to the target (the base app's file is used)
4. The feature gets type safety and autocomplete for the inherited file#### Why Use
__inherit__?Problem: You want to import a base app file in your feature code, but:
- If you don't have the file in your feature, TypeScript shows errors and IDE autocomplete doesn't work
- If you copy the file to your feature, it will overwrite the base app file when applied
Solution: Use
__inherit__ to keep the file in your feature for development, but skip it during application.#### Automatic Import Path Fixing
Important: The CLI automatically fixes import paths in your feature files!
When you import from
__inherit__ files in your feature code:
`typescript
// In your feature's template
import { routes } from './__inherit__routes';
import AppLayout from './__inherit__appLayout';
`The CLI automatically removes the
__inherit__ prefix when applying the feature:
`typescript
// In the target app after applying
import { routes } from './routes';
import AppLayout from './appLayout';
`Supported patterns:
-
import ... from './__inherit__file' → import ... from './file'
- export ... from './__inherit__file' → export ... from './file'
- require('./__inherit__file') → require('./file')
- import('./__inherit__file') → import('./file')
- Works with single quotes ('), double quotes ("), and backticks ( `)
- Works with relative paths (../, ./)Only processes JavaScript/TypeScript files:
-
.js, .jsx, .ts, .tsx, .mjs, .cjs
- Other files (.md, .json, etc.) are not processedThis means you can freely import from
__inherit__ files in your feature code, and the imports will "just work" when the feature is applied!#### Examples
Inherit routes for type safety:
`typescript
// Feature structure
template/
└── src/
├── __inherit__routes.tsx // Copy of base app routes for types
└── index.tsx // Can import routes safely// In your feature's index.tsx
import { routes } from './routes'; // TypeScript works!
// When applied:
// - routes.tsx from base app is used (not overwritten)
// - Your index.tsx can still import it
`Inherit shared layout:
`
template/
└── src/
├── __inherit__appLayout.tsx # Copy from base for types
└── pages/
└── MyPage.tsx # Can import appLayout safely
`#### Validation
The CLI validates
__inherit__ files:1. No conflicts: You cannot have both
routes.tsx and __inherit__routes.tsx in the same template
- ❌ Invalid: Both routes.tsx and __inherit__routes.tsx
- ✅ Valid: Only __inherit__routes.tsx2. Base file must exist: The file must exist in the base app
- ❌ Invalid:
__inherit__nonexistent.tsx when file doesn't exist in base app
- ✅ Valid: __inherit__routes.tsx when routes.tsx exists in base appIf validation fails, you'll see clear error messages:
`
Validation error: Cannot inherit file that doesn't exist!File marked for inheritance: src/routes.tsx
Expected location in base app: /path/to/base/src/routes.tsx
The file doesn't exist in the base app.
`#### Use Cases
- Type-safe imports: Import base app files in your feature with full TypeScript support
- Shared layouts: Reference base app layouts without overwriting them
- Route definitions: Import base routes to extend or reference them
- Shared utilities: Reference base app utility functions with autocomplete
#### Complete Example
`typescript
// Base app has: src/routes.tsx, src/appLayout.tsx// Feature template structure:
template/
└── src/
├── __inherit__routes.tsx # Copy from base (for types only)
├── __inherit__appLayout.tsx # Copy from base (for types only)
└── pages/
└── Dashboard.tsx # New page that imports both
// In template/src/pages/Dashboard.tsx (during development):
import { routes } from '../__inherit__routes'; // Import from __inherit__ file
import AppLayout from '../__inherit__appLayout'; // TypeScript works!
export default function Dashboard() {
return Dashboard using {routes.length} routes ;
}
// When applied, Dashboard.tsx is automatically transformed to:
import { routes } from '../routes'; // __inherit__ removed!
import AppLayout from '../appLayout'; // __inherit__ removed!
export default function Dashboard() {
return Dashboard using {routes.length} routes ;
}
// Final result in target app:
// - src/routes.tsx: inherited from base (not overwritten) ✓
// - src/appLayout.tsx: inherited from base (not overwritten) ✓
// - src/pages/Dashboard.tsx: added from feature with fixed imports ✓
`$3
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.#### How It Works
1. Prepend: Content from the feature file is added before the base file's content
2. Append: Content from the feature file is added after the base file's content
3. The prefix can appear anywhere in the path
4. Import paths with
__inherit__ are automatically fixed in the content#### Examples
Append CSS to global styles:
`
template/
└── src/
└── styles/
└── __append__global.css # Adds content after base global.css
``css
/ Feature's __append__global.css /
:root {
--feature-color: #066afe;
--feature-background: #ffffff;
}.feature-specific {
color: var(--feature-color);
}
`Result in target app's
global.css:
`css
/ Base app's existing content /
@import "tailwindcss";body {
margin: 0;
}
/ Content appended from feature /
:root {
--feature-color: #066afe;
--feature-background: #ffffff;
}
.feature-specific {
color: var(--feature-color);
}
`Prepend imports to a TypeScript file:
`
template/
└── src/
└── __prepend__index.tsx # Adds imports before base index.tsx
``typescript
/ Feature's __prepend__index.tsx /
import { initializeFeature } from './feature-init';initializeFeature();
`Result in target app's
index.tsx:
`typescript
/ Content prepended from feature /
import { initializeFeature } from './feature-init';initializeFeature();
/ Base app's existing content /
import React from 'react';
import ReactDOM from 'react-dom';
// ... rest of base file
`#### Validation
The CLI validates prepend/append operations:
1. No conflicts: You cannot target the same file with multiple operations
- ❌ Invalid: Both
__prepend__global.css and __append__global.css
- ❌ Invalid: Both global.css and __append__global.css
- ✅ Valid: Only __append__global.css2. Base file must exist: The target file must exist in the base app
- ❌ Invalid:
__append__nonexistent.css when file doesn't exist
- ✅ Valid: __append__global.css when global.css exists in base appIf validation fails, you'll see clear error messages:
`
Path conflict detected!The following paths resolve to the same target file:
1. src/styles/__append__global.css (append)
2. src/styles/__prepend__global.css (prepend)
→ Both target: src/styles/global.css
You cannot have multiple files targeting the same path.
`#### Automatic Import Fixing
When prepending or appending TypeScript/JavaScript files, import paths with
__inherit__ are automatically fixed:`typescript
// In feature's __prepend__index.tsx
import { routes } from './__inherit__routes';// After prepending to target app
import { routes } from './routes'; // __inherit__ removed!
`This works the same as regular file operations (see "Automatic Import Path Fixing" above).
#### Use Cases
Prepend:
- Add initialization code at the start of entry files
- Add imports before existing code
- Add type declarations or interfaces
Append:
- Add CSS variables and styles to global stylesheets
- Add routes or configuration entries
- Extend existing files with additional functionality
- Add utility functions or helpers
#### Complete Example
`typescript
// Base app has: src/styles/global.css
// with Tailwind configuration// Feature adds Salesforce Design Tokens by appending
// Feature structure:
template/
└── src/
└── styles/
└── __append__global.css
// Feature's __append__global.css:
:root {
/ Salesforce Design Tokens /
--electric-blue-50: #066afe;
--constant-white: #ffffff;
}
button {
background-color: var(--electric-blue-50);
color: var(--constant-white);
}
// Final result in target app's global.css:
@import "tailwindcss"; // ← Base content
body {
@apply antialiased; // ← Base content
}
:root {
/ Salesforce Design Tokens /
--electric-blue-50: #066afe; // ← Appended content
--constant-white: #ffffff; // ← Appended content
}
button {
background-color: var(--electric-blue-50); // ← Appended content
color: var(--constant-white); // ← Appended content
}
`Route Merging Strategy
The
merge change type with routes strategy intelligently combines route definitions from features with base app routes.$3
Replace-Matching with Deep Children Merge:
1. Top-Level Routes:
- Routes with the same path → Merge their children arrays
- Routes with different paths → Add feature route to result
2. Children Array Merging (when parent paths match):
- Index routes (
index: true) → Feature replaces base if both exist
- Named routes (path: 'about') → Feature replaces base if paths match
- New routes → Added to children array
- Base routes not in feature → Preserved3. Route Deletion:
- Routes with path starting with
__delete__ → Remove matching route from result
- Example: path: '__delete__new' removes the route with path: 'new'
- Throws error if route to delete doesn't exist4. Recursion:
- Applies same logic to nested children arrays
$3
Base App Routes:
`typescript
export const routes: RouteObject[] = [
{
path: '/',
element: ,
children: [
{
index: true,
element: ,
handle: { showInNavigation: true, label: 'Home' }
}
]
}
]
`Feature Routes (in
template/digitalExperiences/webApplications/):
`typescript
export const routes: RouteObject[] = [
{
path: '/',
element: ,
children: [
{
path: 'about',
element: ,
handle: { showInNavigation: true, label: 'About' }
},
{
path: 'contact',
element: ,
handle: { showInNavigation: false }
}
]
}
]
`Merged Result (after applying feature):
`typescript
export const routes: RouteObject[] = [
{
path: '/',
element: , // Uses feature's element
children: [
{
index: true,
element: , // ✓ Preserved from base
handle: { showInNavigation: true, label: 'Home' }
},
{
path: 'about',
element: , // ✓ Added from feature
handle: { showInNavigation: true, label: 'About' }
},
{
path: 'contact',
element: , // ✓ Added from feature
handle: { showInNavigation: false }
}
]
}
]
`$3
- Preserves existing routes: Base app's Home route stays intact
- Adds new routes: Feature routes (About, Contact) are added
- No duplication: Routes with matching paths are replaced, not duplicated
- Deep merging: Works with nested route structures
- Multiple features: Apply multiple route-adding features sequentially
Route merging happens automatically for
routes.tsx files.$3
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.#### How It Works
1. Add a route with
path: '__delete__ in your feature's routes file
2. The route with the matching path (without the prefix) will be removed during merging
3. If the route doesn't exist, an error will be thrown#### Example
Base/Previous Routes:
`typescript
export const routes: RouteObject[] = [
{
path: '/',
element: ,
children: [
{ index: true, element: },
{ path: 'about', element: },
{ path: 'new', element: }
]
}
]
`Feature Routes (deleting 'new'):
`typescript
export const routes: RouteObject[] = [
{
path: '/',
children: [
{
path: '__delete__new',
element: <>> // Element value is ignored for deletion markers
}
]
}
]
`Merged Result:
`typescript
export const routes: RouteObject[] = [
{
path: '/',
element: ,
children: [
{ index: true, element: }, // ✓ Preserved
{ path: 'about', element: } // ✓ Preserved
// 'new' route deleted ✓
]
}
]
`#### Validation
- 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
- This prevents silent failures and ensures routes are being deleted as expected
#### Use Cases
- Remove dependency routes: Delete routes added by feature dependencies that aren't needed
- Clean up base routes: Remove placeholder or example routes from the base app
- Override parent features: Child features can remove routes added by parent features they depend on
For example, a feature that provides a single-page app might delete all routes from the base app to start fresh.
$3
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.#### How It Works
1. During route merging, after all imports are merged from the feature file
2. The import merger scans all import statements in the target file
3. Any imports with
__delete__ in the module specifier are automatically removed
4. This ensures deleted components don't leave broken import references#### Example
Feature Routes File:
`typescript
import type { RouteObject } from "react-router";
import AppLayout from "./__inherit__appLayout";
import Home from ".";
import New from "./__delete__new"; // Import from deleted fileexport const routes: RouteObject[] = [
{
path: '/',
element: ,
children: [
{
index: true,
element: ,
handle: { showInNavigation: true, label: 'Home' }
},
{
path: '__delete__new', // Delete the 'new' route
element: ,
}
]
}
]
`Merged Result:
`typescript
import type { RouteObject } from "react-router";
import AppLayout from "./appLayout";
import Home from ".";
// ✓ Import from ./__delete__new automatically removedexport const routes: RouteObject[] = [
{
path: '/',
element: ,
children: [
{
index: true,
element: ,
handle: { showInNavigation: true, label: 'Home' }
}
// ✓ 'new' route deleted
]
}
]
`#### Key Benefits
- No broken imports: Automatically removes imports from deleted files
- Clean merged code: Final output doesn't reference non-existent files
- Works with file deletion: Applies to any file marked with
__delete__ prefix
- Seamless integration: Happens automatically during route merging, no manual cleanup neededTesting
The CLI has a comprehensive test suite using Vitest, including E2E tests with gold files and unit tests for critical utilities.
Test Coverage:
- ✅ 12 E2E test scenarios covering all major CLI workflows
- ✅ 46+ unit tests covering route merging, import merging, and file operations
- ✅ Gold file comparison for E2E validation
- ✅ No skipped tests - all functionality fully tested
$3
`bash
Run all tests
npm testRun with UI
yarn test:uiRun E2E tests only
yarn test:e2eRun unit tests only
yarn test:unitRun with coverage
yarn test:coverage
`$3
`
packages/cli/
├── test/
│ ├── e2e/ # End-to-end tests
│ │ ├── fixtures/ # Test fixtures (base apps, features)
│ │ ├── gold/ # Expected outputs for E2E tests
│ │ └── apply-patches.spec.ts
│ ├── unit/ # Unit tests
│ │ ├── route-merger.spec.ts
│ │ ├── import-merger.spec.ts
│ │ └── file-operations.spec.ts
│ └── helpers/ # Test utilities
│ ├── compare-directories.ts
│ ├── create-temp-dir.ts
│ └── fixtures.ts
`$3
E2E tests verify complete CLI workflows using gold files (expected outputs). Tests cover:
- Simple feature application (adding routes/files)
- File deletion with
__delete__ prefix
- Route deletion and import cleanup
- Feature dependency resolution
- Complex operations (__inherit__, __prepend__, __append__)
- Error handling and validation$3
Unit tests focus on individual modules:
- route-merger: Route merging logic and deletion
- import-merger: Import statement merging, deduplication, type imports, and formatting
- file-operations: File deletion, prepending, and appending
$3
When intentionally changing CLI behavior, update gold files:
`bash
UPDATE_GOLD=1 npm test
`⚠️ Warning: Only update gold files after verifying the new output is correct!
$3
E2E Test Example:
`typescript
it('should apply a simple feature correctly', async () => {
const outputDir = copyFixture('base-app', join(tempDir, 'output'));
const featurePath = getFixturePath('feature-simple'); await applyPatchesCommand(featurePath, outputDir, {
skipDependencyChanges: true
});
const goldDir = getGoldPath('simple-apply');
const differences = compareOrUpdate(outputDir, goldDir);
expect(differences).toEqual([]);
});
`Unit Test Example:
`typescript
it('should merge routes correctly', () => {
const project = new Project({ useInMemoryFileSystem: true }); const targetFile = project.createSourceFile('target.tsx',
...);
const featureFile = project.createSourceFile('feature.tsx', ...); const result = mergeRoutes('feature.tsx', 'target.tsx', project);
expect(result).toContain('expected content');
});
`Development
`bash
Build the CLI
yarn buildRun without building (development)
yarn dev --
``