npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sfdc-webapps/cli

v1.0.6

Published

CLI tool for applying feature patches to base apps

Readme

Feature Patch CLI

CLI tool for applying feature patches to apps in the webapps-templates monorepo.

Installation

Build the CLI from the monorepo root:

yarn build --workspace=@sfdc-webapps/cli

Usage

Apply patches

# Apply patches to a target directory (base app is used as reference only)
yarn apply-patches <feature-path> <app-path> <target-dir>

# Examples:
# Apply navigation-menu patches to my-app (using base-react-app as reference)
yarn apply-patches packages/feature-navigation-menu packages/base-react-app my-app

# Skip dependency installation
yarn apply-patches packages/feature-navigation-menu packages/base-react-app my-app -- --skip-dependency-changes

# Reset target directory to base app state before applying (preserves node_modules)
yarn apply-patches packages/feature-navigation-menu packages/base-react-app my-app -- --reset

Direct usage

# From monorepo root
node packages/cli/dist/index.js <feature-path> <app-path> <target-dir>

# With flags
node packages/cli/dist/index.js <feature-path> <app-path> my-app --skip-dependency-changes --reset

Create a new feature

Create a new feature package from the base-feature template:

# Create a new feature (will be prefixed with "feature-")
yarn new-feature <feature-name>

# 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

# From monorepo root
node packages/cli/dist/index.js new-feature <feature-name>

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-menufeature-nav-menu)

Examples:

  • navigationfeature-navigation
  • user-authfeature-user-auth
  • dashboard-v2feature-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 package

The created feature will have:

  • ✅ Correct package.json configuration
  • ✅ TypeScript configuration
  • ✅ Feature structure following conventions
  • ✅ Ready for customization and development

Next steps after creation

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

Options

  • <feature-path>: Path to the feature directory (can be relative to monorepo root or absolute). The feature must contain a feature.ts file.
  • <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.
  • <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.
  • --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

Example: Apply feature patches to create a new app

$ yarn apply-patches packages/feature-navigation-menu packages/base-react-app my-app

Applying 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:

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.

Feature Structure

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/<feature-name>/)
    │   └── 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/<feature-name>/
  • SFDX metadata files (like classes/, triggers/, objects/, etc.): Placed at root level in the output directory

This structure ensures proper organization for both digital experience applications and Salesforce metadata.

Feature Configuration

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/<webAppName>/src/routes.tsx)
  • packageJson: NPM dependencies to install in the target app
  • dependencies: Array of other features this feature depends on (applied first)

Basic Feature Example:

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/<webAppName>/src/routes.tsx'
  packageJson: {
    dependencies: {
      'react-router': '^7.10.1',
    },
  },
};

export default feature;

Feature with Dependencies:

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;

Custom Configuration:

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-menufeature-navigation-menu)
  • routeFilePath: Must be a path relative to templateDir. If not specified, defaults to digitalExperiences/webApplications/<webAppName>/src/routes.tsx

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.

Default Behavior (Enabled by Default)

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)

Using the Default Mapping

Simply organize your template files under webApp/ for web application code, and at the root level for SFDX metadata:

// 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

Disabling Path Mappings (Opt-Out)

Use full paths when you need precise control or for backwards compatibility:

// feature.ts
export default {
  pathMappings: {
    enabled: false  // Disable automatic mapping
  }
};
template/
└── digitalExperiences/    # Use full structure
    └── webApplications/
        └── <feature-name>/
            └── src/
                └── routes.tsx

Custom Path Mappings

Define custom mappings for non-standard structures:

// 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.tsxdigitalExperiences/webApplications/custom-app-name/src/app.tsx

Multiple Mappings

You can define multiple mappings in one feature:

export default {
  pathMappings: {
    mappings: [
      { from: 'webApp', to: 'digitalExperiences/webApplications/my-app' },
      { from: 'shared', to: 'digitalExperiences/shared-resources' }
    ]
  }
};

Mixed Path Formats

You can mix web application files, SFDX metadata, and full paths in the same feature:

template/
├── webApp/                    # Mapped to digitalExperiences/webApplications/<feature-name>/
│   └── 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/<feature-name>/
  • SFDX metadata (classes/, triggers/, objects/, lwc/, etc.) → Root level
  • Full paths (already containing digitalExperiences/) → Used as-is

Path Mapping Examples

Example 1: Default mapping (recommended)

export default {};  // That's it!

Example 2: Opt-out for backwards compatibility

export default {
  pathMappings: { enabled: false }
};

Example 3: Custom app name

export default {
  pathMappings: {
    mappings: [
      { from: 'webApp', to: 'digitalExperiences/webApplications/custom-name' }
    ]
  }
};

Example 4: Completely custom structure

export default {
  pathMappings: {
    mappings: [
      { from: 'src', to: 'app/sources' },
      { from: 'assets', to: 'public/static' }
    ]
  }
};

How Path Mappings Work

  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.

How Dependencies Work

  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

Dependency Resolution Order

When you apply a feature with dependencies:

Feature A depends on Feature B
Feature B depends on Feature C

Application order: C → B → A (dependencies first)

The CLI builds a complete dependency graph and applies features in topological order.

Example: Building on Navigation Features

// 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;
// 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

Nested Dependencies

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 Auth

Application order: Auth → Navigation → Dashboard → App

Circular Dependency Detection

The CLI detects and prevents circular dependencies:

// 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.

Main Feature Wins

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.tsx

When 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

Route Merging with Dependencies

Routes from all features are merged together:

Base App Routes:

export const routes = [
  {
    path: '/',
    children: [
      { index: true, element: <Home /> }
    ]
  }
];

navigation-menu Routes:

export const routes = [
  {
    path: '/',
    children: [
      { path: 'about', element: <About /> },
      { path: 'contact', element: <Contact /> }
    ]
  }
];

Final Merged Routes:

export const routes = [
  {
    path: '/',
    children: [
      { index: true, element: <Home /> },      // From base
      { path: 'about', element: <About /> },   // From navigation-menu
      { path: 'contact', element: <Contact /> } // From navigation-menu
    ]
  }
];

Routes accumulate across all features, preserving routes from base app and all dependencies.

Dependency Paths

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.

Diamond Dependencies

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 B

Dependency graph:
    Main
    /  \
   A    B
    \  /
     C

Resolution: Feature C is applied once (not duplicated).

Application order: C → A → B → Main

Watch Mode with Dependencies

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

Deleting Files and Directories

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.

Inheriting Files from Base App

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:

// 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:

// 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 processed

This 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:

// 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.tsx
  2. 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 app

If 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

// 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 <AppLayout>Dashboard using {routes.length} routes</AppLayout>;
}

// 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 <AppLayout>Dashboard using {routes.length} routes</AppLayout>;
}

// 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 ✓

Prepending and Appending to Base Files

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
/* 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:

/* 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
/* Feature's __prepend__index.tsx */
import { initializeFeature } from './feature-init';

initializeFeature();

Result in target app's index.tsx:

/* 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.css
  2. 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 app

If 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:

// 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

// 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.

How It Works

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 → Preserved
  3. 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 exist
  4. Recursion:

    • Applies same logic to nested children arrays

Example

Base App Routes:

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      {
        index: true,
        element: <Home />,
        handle: { showInNavigation: true, label: 'Home' }
      }
    ]
  }
]

Feature Routes (in template/digitalExperiences/webApplications/<feature-name>/src/routes.tsx):

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      {
        path: 'about',
        element: <About />,
        handle: { showInNavigation: true, label: 'About' }
      },
      {
        path: 'contact',
        element: <Contact />,
        handle: { showInNavigation: false }
      }
    ]
  }
]

Merged Result (after applying feature):

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,  // Uses feature's element
    children: [
      {
        index: true,
        element: <Home />,    // ✓ Preserved from base
        handle: { showInNavigation: true, label: 'Home' }
      },
      {
        path: 'about',
        element: <About />,   // ✓ Added from feature
        handle: { showInNavigation: true, label: 'About' }
      },
      {
        path: 'contact',
        element: <Contact />, // ✓ Added from feature
        handle: { showInNavigation: false }
      }
    ]
  }
]

Key Benefits

  • 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.

Deleting Routes

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__<route-name>' 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:

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      { path: 'new', element: <New /> }
    ]
  }
]

Feature Routes (deleting 'new'):

export const routes: RouteObject[] = [
  {
    path: '/',
    children: [
      {
        path: '__delete__new',
        element: <></>  // Element value is ignored for deletion markers
      }
    ]
  }
]

Merged Result:

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      { index: true, element: <Home /> },   // ✓ Preserved
      { path: 'about', element: <About /> } // ✓ 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.

Automatic Import Cleanup for Deleted Files

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:

import type { RouteObject } from "react-router";
import AppLayout from "./__inherit__appLayout";
import Home from ".";
import New from "./__delete__new";  // Import from deleted file

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      {
        index: true,
        element: <Home />,
        handle: { showInNavigation: true, label: 'Home' }
      },
      {
        path: '__delete__new',  // Delete the 'new' route
        element: <New />,
      }
    ]
  }
]

Merged Result:

import type { RouteObject } from "react-router";
import AppLayout from "./appLayout";
import Home from ".";
// ✓ Import from ./__delete__new automatically removed

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <AppLayout />,
    children: [
      {
        index: true,
        element: <Home />,
        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 needed

Testing

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

Running Tests

# Run all tests
npm test

# Run with UI
yarn test:ui

# Run E2E tests only
yarn test:e2e

# Run unit tests only
yarn test:unit

# Run with coverage
yarn test:coverage

Test Structure

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

E2E Tests

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

Unit Tests

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

Updating Gold Files

When intentionally changing CLI behavior, update gold files:

UPDATE_GOLD=1 npm test

⚠️ Warning: Only update gold files after verifying the new output is correct!

Creating New Tests

E2E Test Example:

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:

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

# Build the CLI
yarn build

# Run without building (development)
yarn dev -- <feature> <base-app>