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.

.layers.json
{
  "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.
Profile Aliases A profile value can be a plain string instead of an object, creating an alias. In the example above, "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):

.layers.json (with template vars)
{
  "default": {
    "rootFolder": "App",
    "vars": {
      "project": "my-app",
      "outputPath": "<% projectPath %>/dist"
    }
  }
}

Available built-in template variables:

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:

  1. Start at the head project's .layers.json
  2. For each entry in extend, locate the layer (npm package or local path)
  3. Load that layer's .layers.json and resolve the matching profile (using basedOn if needed)
  4. Recursively walk that layer's extend list
  5. De-duplicate: if the same layer appears multiple times, keep the last occurrence (deepest position)
  6. 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:

Merge order (conceptual)
// 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:

  1. The CLI argument: lpack :profileName
  2. The __LPACK_PROFILE__ environment variable
  3. 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:

.layers.json
{
  "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

App/index.js
// 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

  1. The plugin detects the glob pattern (parenthesized portion) in the import path
  2. It scans all layer roots for files matching App/components/**/*.jsx
  3. Files from the head project take priority over parent layers (same relative path = override)
  4. A virtual JavaScript module is generated that imports and re-exports all matched files
  5. The virtual module is injected via webpack-virtual-modules
  6. 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:

Glob import codecs
// 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:

App/styles/main.scss
// 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:

  1. Determines which layer the current file belongs to
  2. Looks for the same relative path in the next layer down the chain
  3. Resolves the import to that parent layer's file
your-project/App/components/Button.jsx (head project)
// 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.

Note: $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

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

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

terminal
# 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:

  1. Parses the :profile argument and sets __LPACK_PROFILE__
  2. Sets __LPACK_HEAD__ to the current working directory
  3. Spawns webpack using the layer-pack proxy config (etc/wp/webpack.config.js)
  4. 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:

terminal
# 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:

terminal
# 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:

terminal
# 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:

terminal
npx lpack-init

Plugin API

The layer-pack Node.js API is used in webpack configuration files to integrate layer-pack with your build:

webpack.config.js
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".

API usage
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.

API usage
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.

API usage
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.

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

API usage
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:

API usage
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:

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:

webpack.config.js (head project)
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:

Using vars in webpack config
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

Quick Setup

terminal
npm install lpack-react --save-dev
npx lpack-setup
.layers.json
{
  "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