Documentation
Complete reference for layer-pack configuration, features, and API.
.layers.json Format
The .layers.json file (or .layers.js for dynamic config) is the heart of every layer-pack project. It sits in the root of each layer (npm package or project) and defines profiles, inheritance chains, source roots, and build variables.
{
"default": {
"rootFolder": "App",
"libsPath": ["./libs"],
"extend": ["some-parent-layer"],
"config": "./webpack.config.js",
"vars": {
"rootAlias": "App",
"devPort": 3000,
"project": "my-app",
"production": false,
"targetDir": "dist/default"
},
"scripts": {
"start": "node dist/api/App.server.js"
},
"templates": {
"indexHtml": "./App/index.html.tpl"
}
},
"api": {
"basedOn": "default",
"config": "./webpack.config.api.js",
"vars": {
"externals": true,
"targetDir": "dist/api"
}
},
"dev": "default"
}
Profile Fields Reference
| Field | Type | Description |
|---|---|---|
rootFolder |
string | Relative path to the source root directory (default: "./App"). This directory is exposed as the namespace root for imports like App/components/Foo. |
extend |
string[] | Ordered list of parent layers to inherit from. Each entry is an npm package name or a relative/absolute path to a directory containing its own .layers.json. |
basedOn |
string | Name of another profile in the same file to inherit from. All settings from the base profile are inherited; this profile's settings override them. |
libsPath |
string[] | Additional directories to search when resolving extend entries. Useful for monorepos where layers live in a local ./libs directory rather than node_modules. |
config |
string | Relative path to the webpack config file for this profile. If omitted, the config is inherited from the first parent layer that provides one. |
vars |
object | Template variables. These are merged across the layer chain (parent first, head wins). Available in webpack configs via layerPack.getConfig().vars and in config files via <% varName %> mustache syntax. |
scripts |
object | Named scripts that can be referenced by consuming layers. Inherited and overridable like vars. |
templates |
object | Named template file paths. Inherited and overridable. Templates support mustache-style <% %> variable substitution with vars. |
"dev": "default" means building with lpack :dev is equivalent to lpack :default.
Variable Template Syntax
Values in .layers.json support mustache-style template variables using <% %> delimiters (not the standard {{ }} to avoid conflicts with JSON):
{
"default": {
"rootFolder": "App",
"vars": {
"project": "my-app",
"outputPath": "<% projectPath %>/dist"
}
}
}
Available built-in template variables:
<% projectPath %>— Absolute path to the head project root<% packagePath %>— Absolute path to the current layer's package root<% packageConfig %>— The layer's package.json contents
Layer Inheritance
Layer inheritance is the core concept of layer-pack. It works similarly to class inheritance in object-oriented programming: a child layer extends one or more parent layers, inheriting their source files, webpack configs, and build variables while being able to override any part.
How the Extend Chain Works
When layer-pack resolves a profile, it performs a depth-first traversal of the extend lists across all layers:
- Start at the head project's
.layers.json - For each entry in
extend, locate the layer (npm package or local path) - Load that layer's
.layers.jsonand resolve the matching profile (usingbasedOnif needed) - Recursively walk that layer's
extendlist - De-duplicate: if the same layer appears multiple times, keep the last occurrence (deepest position)
- Build all
all*arrays with head project at index 0 (highest priority)
Resolution Order
The resulting configuration has several all* arrays, all ordered head-first:
| Array | Contents |
|---|---|
allRoots |
Absolute paths to all layer rootFolder directories. Used for namespace resolution — first match wins. |
allModulePath |
All node_modules paths across the layer chain. Ensures dependencies from parent layers resolve correctly. |
allWebpackCfg |
Paths to all webpack config files in the chain. The head project's config (if any) is at index 0. |
allModuleRoots |
Absolute paths to all layer package roots. |
allCfg |
Raw .layers.json profile objects for each layer in the chain. |
Vars Merging
Variables are merged with deep-extend: parent layer values are applied first, then each child layer's values override. The head project always has the final say:
// Final vars = deepMerge(
// deepest-ancestor.vars,
// ...
// parent-layer.vars,
// head-project.vars // wins
// )
Multi-Profile Builds
Profiles let a single project produce multiple distinct builds from the same codebase. Each profile can have its own webpack config, layer chain, and variables.
Profile Selection
The active profile is determined by:
- The CLI argument:
lpack :profileName - The
__LPACK_PROFILE__environment variable - Falls back to
"default"
basedOn Inheritance
The basedOn field provides profile-level inheritance within the same .layers.json. All fields from the base profile are inherited; the current profile's fields override:
{
"default": {
"rootFolder": "App",
"extend": ["lpack-react"],
"vars": {
"rootAlias": "App",
"devPort": 3000
}
},
"www": {
"basedOn": "default",
"vars": {
"production": true,
"targetDir": "dist/www"
}
},
"api": {
"basedOn": "default",
"config": "./webpack.config.api.js",
"vars": {
"externals": true,
"targetDir": "dist/api"
}
}
}
Resulting vars for www: { rootAlias: "App", devPort: 3000, production: true, targetDir: "dist/www" }
Cross-Layer Profile Resolution
When extending a parent layer, layer-pack looks for the same profile name in the parent's .layers.json. If the parent does not define that profile, it falls back to the profile specified by basedOn, then to "default".
Environment Variables
The lpack CLI sets these environment variables before spawning webpack:
| Variable | Description |
|---|---|
__LPACK_PROFILE__ |
The active profile name (e.g., "www", "api") |
__LPACK_HEAD__ |
Absolute path to the head project root |
__LPACK_VARS_OVERRIDE__ |
JSON string of vars overrides (set manually or by profile commands.vars) |
Glob Imports
Glob imports let you import entire directory trees with a single import statement. layer-pack intercepts the import at build time, scans all layer roots for matching files, and generates a virtual JavaScript (or SCSS) module that re-exports everything.
Basic Syntax
// Import all .jsx files under App/components/ (recursively)
import components from 'App/components/(**/*.jsx)';
// Import all .js files in App/routes/ (one level only)
import routes from 'App/routes/(*.js)';
// The imported value is an object keyed by capture group match
// e.g., { "Header/Header": HeaderComponent, "Footer/Footer": FooterComponent }
How It Works
- The plugin detects the glob pattern (parenthesized portion) in the import path
- It scans all layer roots for files matching
App/components/**/*.jsx - Files from the head project take priority over parent layers (same relative path = override)
- A virtual JavaScript module is generated that imports and re-exports all matched files
- The virtual module is injected via
webpack-virtual-modules - During watch mode, changes to matched directories trigger regeneration of the virtual module
Codecs (Import Strategies)
By default, glob imports use eager, synchronous imports. You can change this with the ?using= query parameter:
// Default: eager synchronous imports
import pages from 'App/pages/(**/*.jsx)';
// Lazy React: wraps each import in React.lazy() for code splitting
import pages from 'App/pages/(**/*.jsx)?using=lazyReact';
// Suspense React: wraps each in React.lazy() with Suspense boundaries
import pages from 'App/pages/(**/*.jsx)?using=SuspenseReact';
// React Loadable: wraps each in react-loadable for SSR-compatible code splitting
import pages from 'App/pages/(**/*.jsx)?using=ReactLoadable';
| Codec | Behavior |
|---|---|
default |
Synchronous eager imports. All modules are bundled and available immediately. |
LazyReact |
Each matched file is wrapped in React.lazy(() => import(...)) for automatic code splitting. |
SuspenseReact |
Like LazyReact, but also wraps each component in a <Suspense> boundary with a fallback. |
ReactLoadable |
Uses react-loadable for SSR-compatible dynamic imports with loading states. |
SCSS Glob Imports
Glob imports also work for SCSS files. When the import path has a .scss extension, layer-pack generates a virtual SCSS file with @import statements:
// Import all component styles
@import 'App/components/(**/*.scss)';
$super Imports
The $super keyword lets you access the parent layer's version of the current file, much like calling super() in a class. This is the mechanism for overriding components while still being able to use the original.
How $super Works
When layer-pack encounters import something from '$super', it:
- Determines which layer the current file belongs to
- Looks for the same relative path in the next layer down the chain
- Resolves the import to that parent layer's file
// Import the parent layer's Button component
import ParentButton from '$super';
// Extend it with additional functionality
export default function Button(props) {
return (
<div className="custom-wrapper">
<ParentButton {...props} variant="enhanced" />
</div>
);
}
In this example, the head project's App/components/Button.jsx overrides the parent layer's version (because head-project files have the highest priority in namespace resolution). But it can still import the parent's original via $super.
$super only works as a bare import — it resolves to the same file path one layer down.
You cannot use it as a prefix (e.g. $super/App/config/theme will not work).
To access a specific parent file, override it at the same path and use bare $super inside.
Use Cases
- Component wrapping: Override a UI component to add analytics, styling, or behavior, while rendering the parent's original
- Configuration extension: Import the parent layer's config and merge in additional settings
- Middleware injection: Wrap parent middleware with logging, auth, or other cross-cutting concerns
- Gradual migration: Override components one by one, delegating to the parent version for functionality you have not migrated yet
Namespace Resolution
When you import from App/components/Foo, layer-pack resolves this through all layer roots in priority order. It checks each layer's rootFolder directory for the file, and the first match wins.
Resolution Algorithm
// Given import: "App/components/Header"
// And layer chain: [your-project, lpack-react, base-layer]
//
// layer-pack checks in order:
// 1. your-project/App/components/Header.jsx ← found? use it
// 2. lpack-react/App/components/Header.jsx ← found? use it
// 3. base-layer/App/components/Header.jsx ← found? use it
// 4. Not found → webpack error
//
// Head project always wins. Parent layers provide fallbacks.
How Aliases Are Created
The plugin hooks into webpack's enhanced-resolve to intercept module resolution. When a request starts with a known namespace (like App, matching the rootAlias var), the plugin rewrites the request to check each layer root directory in order.
This means you do not need to configure webpack aliases manually. layer-pack handles all namespace resolution dynamically based on the allRoots array.
Cross-Layer File Precedence
The precedence rule is simple: head project wins. If the head project provides App/components/Header.jsx, it shadows the same file from all parent layers. Parent layers provide fallback implementations that the head project can optionally override.
CLI Reference
lpack
Build the project using webpack with layer-aware module resolution.
# Build with the default profile
npx lpack
# Build with a named profile
npx lpack :www
# Build with watch mode
npx lpack :www --watch
# Production build
npx lpack :www --mode production
# Pass additional webpack flags
npx lpack :www --env goal=production
The lpack command:
- Parses the
:profileargument and sets__LPACK_PROFILE__ - Sets
__LPACK_HEAD__to the current working directory - Spawns webpack using the layer-pack proxy config (
etc/wp/webpack.config.js) - The proxy config calls
layerPack.getSuperWebpackCfg()to load the correct config from the layer chain
lpack-dev-server
Start webpack-dev-server with HMR support and layer-aware resolution:
# Start dev server for default profile
npx lpack-dev-server
# Start dev server for a specific profile
npx lpack-dev-server :www
# Custom port
npx lpack-dev-server :www --port 8080
lpack-setup
Walk the entire layer inheritance chain and install missing dependencies from parent layers:
# Install all inherited dependencies
npx lpack-setup
This command is essential after adding a new extend entry. Parent layers may depend on babel presets, webpack loaders, or other packages that your project does not have yet.
lpack-run
Run a Node.js script with layer-pack's module resolution paths active. This ensures require('App/...') works at runtime:
# Run a Node script with layer-aware resolution
npx lpack-run ./scripts/migrate.js
# Run with a specific profile
npx lpack-run :api ./dist/api/App.server.js
lpack-init
Scaffold a new layer-pack project from a template:
npx lpack-init
Plugin API
The layer-pack Node.js API is used in webpack configuration files to integrate layer-pack with your build:
const layerPack = require('layer-pack');
layerPack.getConfig(profile?)
Returns the fully resolved configuration object for a profile. Defaults to the active profile (__LPACK_PROFILE__ env var) or "default".
const cfg = layerPack.getConfig();
// Access merged vars
console.log(cfg.vars.rootAlias); // "App"
console.log(cfg.vars.devPort); // 3000
console.log(cfg.vars.targetDir); // "dist/www"
// Access layer chain info
console.log(cfg.allRoots); // ["/path/to/project/App", "/path/to/parent/App"]
console.log(cfg.allModulePath); // All node_modules paths
console.log(cfg.allWebpackCfg); // All webpack config paths
layerPack.getAllConfigs(dir?, reset?)
Returns a map of all profile configurations. Each key is a profile ID, each value is a resolved config object.
const allCfg = layerPack.getAllConfigs();
// { default: {...}, www: {...}, api: {...} }
layerPack.plugin(cfg?, profile?)
Creates and returns the webpack plugin instance for a profile. This is a singleton — calling it multiple times with the same profile returns the same instance.
module.exports = [
{
plugins: [
layerPack.plugin() // Uses the active profile
]
}
];
layerPack.getSuperWebpackCfg(profile?, head?)
Load and return the resolved webpack configuration array from the layer chain. If head is false (default), it loads the parent layer's config (skipping the head project's config). This is used by the CLI proxy to provide the base config that head projects can extend.
// Typically used by the CLI proxy, not directly
const wpCfg = layerPack.getSuperWebpackCfg('www');
// Returns the webpack config array for the www profile
layerPack.isFileExcluded(profile?)
Returns a function suitable for webpack's module.rules[].exclude option. The function returns true for any file path outside the layer chain's root directories, preventing unnecessary transpilation of third-party code:
module.exports = [{
module: {
rules: [{
test: /\.jsx?$/,
exclude: layerPack.isFileExcluded(),
use: 'babel-loader'
}]
}
}];
layerPack.getHeadRoot(profile?)
Returns the absolute path to the head project's root directory:
const headRoot = layerPack.getHeadRoot();
// "/home/user/projects/my-app"
module.exports = [{
output: {
path: headRoot + '/dist/'
}
}];
Webpack Integration
How the Plugin Hooks into Webpack
The layer-pack plugin integrates with webpack at several levels:
- Resolver hooks (enhanced-resolve): Intercepts module resolution to handle namespace aliases (
App/...),$superimports, and glob patterns. The plugin taps into theresolvestep to rewrite import paths before webpack's default resolution. - Virtual modules (webpack-virtual-modules): Glob imports generate virtual JavaScript or SCSS files that are injected into the compilation via the
webpack-virtual-modulesplugin. These files exist only in memory. - Loader resolution: Ensures webpack loaders (babel-loader, sass-loader, etc.) are resolved correctly even when they are installed in parent layer packages, not the head project.
- Externals handling: When
vars.externalsis set, the plugin configures webpack externals for Node.js builds (API servers), excluding node_modules from the bundle. - DefinePlugin integration: The
vars.DefinePluginCfgobject is passed to webpack'sDefinePluginfor compile-time constants. - Watch mode: During development, the plugin monitors glob-matched directories and regenerates virtual modules when files are added or removed.
Custom Webpack Config with Inherited Base
A common pattern is to provide a custom webpack config that extends the inherited one. Since the lpack CLI uses the proxy config to load the parent's webpack config, your project's config can import and extend it:
const layerPack = require('layer-pack');
const lPackCfg = layerPack.getConfig();
// When the head project has its own config, the CLI proxy
// loads the parent layer's config via getSuperWebpackCfg().
// Your config is then loaded on top.
module.exports = [
{
entry: { App: lPackCfg.vars.rootAlias },
output: {
path: layerPack.getHeadRoot() + '/dist/',
filename: '[name].js'
},
plugins: [ layerPack.plugin() ],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: layerPack.isFileExcluded(),
use: 'babel-loader'
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
}
];
Vars in Webpack Configs
All merged vars from the layer chain are available via layerPack.getConfig().vars. Common patterns:
const layerPack = require('layer-pack');
const cfg = layerPack.getConfig();
module.exports = [{
mode: cfg.vars.production ? 'production' : 'development',
output: {
path: layerPack.getHeadRoot() + '/' + cfg.vars.targetDir,
publicPath: cfg.vars.publicPath || '/'
},
devServer: {
port: cfg.vars.devPort || 3000
}
}];
lpack-react
lpack-react is a ready-made inheritable layer that provides a complete, production-ready frontend stack. Instead of configuring webpack, babel, sass, and express from scratch, you extend lpack-react and get everything pre-configured.
What lpack-react Provides
- React 18 with JSX/TSX support via babel
- Webpack 5 configuration with optimized defaults
- Sass/SCSS support with CSS modules
- Express server for SSR (server-side rendering)
- Hot Module Replacement for fast development
- Babel presets for modern JavaScript and React
- Asset handling (images, fonts, etc.)
Quick Setup
npm install lpack-react --save-dev
npx lpack-setup
{
"default": {
"rootFolder": "App",
"extend": ["lpack-react"],
"vars": {
"rootAlias": "App",
"project": "my-react-app",
"devPort": 3000,
"targetDir": "dist/www"
}
}
}
Available Vars
lpack-react exposes several vars that you can override in your project:
| Variable | Default | Description |
|---|---|---|
rootAlias |
"App" |
The import namespace alias for the source root |
devPort |
3000 |
Port for the webpack dev server |
targetDir |
"dist" |
Output directory for built files |
production |
false |
Enable production mode optimizations |
externals |
false |
Externalize node_modules (for server builds) |
project |
— | Project name used in output and page title |
webpackPatch |
— | Object merged into every webpack config via webpack-merge |
DefinePluginCfg |
— | Object passed to webpack DefinePlugin for compile-time constants |