Tutorials
Hands-on guides that walk you through real-world layer-pack patterns. Each tutorial builds a complete, working example.
Tutorial: Reusable React Layer
Step 1: Create the Base Layer Package
Create a new npm package that will serve as your shared base layer. This package will contain webpack configuration, babel presets, and default components that all consuming projects inherit.
mkdir my-react-layer && cd my-react-layer
npm init -y
npm install layer-pack webpack webpack-cli babel-loader \
@babel/core @babel/preset-env @babel/preset-react \
react react-dom --save
Step 2: Define the Layer Configuration
Create a .layers.json that defines this package as a layer with a source root and webpack config:
{
"default": {
"rootFolder": "App",
"config": "./webpack.config.js",
"vars": {
"rootAlias": "App",
"devPort": 3000,
"targetDir": "dist"
}
}
}
Step 3: Create the Webpack Config
This config uses the layer-pack API so it works correctly when inherited by consuming projects:
const layerPack = require('layer-pack');
const cfg = layerPack.getConfig();
module.exports = [
{
mode: cfg.vars.production ? 'production' : 'development',
entry: {
App: cfg.vars.rootAlias
},
output: {
path: layerPack.getHeadRoot() + '/' + (cfg.vars.targetDir || 'dist'),
filename: '[name].js'
},
plugins: [
layerPack.plugin()
],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: layerPack.isFileExcluded(),
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
}
]
},
resolve: {
extensions: ['.js', '.jsx', '.json']
}
}
];
Step 4: Add Default Components
Provide default components in the layer's App/ directory. Consuming projects can override any of these:
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from 'App/components/App';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
import React from 'react';
import Header from 'App/components/Header';
export default function App() {
return (
<div className="app">
<Header />
<main>
<h1>Welcome</h1>
<p>This is the base layer app.</p>
</main>
</div>
);
}
import React from 'react';
export default function Header() {
return (
<header>
<nav>Base Layer Navigation</nav>
</header>
);
}
Step 5: Publish or Link the Layer
# For local development, use npm link
npm link
# Or publish to npm
npm publish
Step 6: Create a Consuming Project
Now create a project that extends your layer:
mkdir my-app && cd my-app
npm init -y
npm install layer-pack webpack webpack-cli --save-dev
npm link my-react-layer # or: npm install my-react-layer
npx lpack-setup # installs inherited dependencies
{
"default": {
"rootFolder": "App",
"extend": ["my-react-layer"],
"vars": {
"rootAlias": "App",
"project": "My Application"
}
}
}
Notice there is no config field — the webpack config is inherited from my-react-layer. The consuming project only needs to provide a .layers.json and optionally override files in App/.
Step 7: Override a Component
Override the Header component by creating the same file path in the head project:
import React from 'react';
// This file shadows my-react-layer/App/components/Header.jsx
// because the head project has higher priority
export default function Header() {
return (
<header>
<nav>My Custom Navigation</nav>
</header>
);
}
# Build the project — inherits everything from the base layer
npx lpack
The build uses the inherited webpack config, babel presets, and entry point from my-react-layer, but the Header component comes from the head project because head project files always take priority in namespace resolution.
Tutorial: Multi-Endpoint Architecture
www) and an API server (api), each built with its own profile and webpack config.
Step 1: Project Structure
Set up a project with shared code and separate entry points for each target:
my-fullstack-app/
.layers.json
webpack.config.www.js # Web frontend config
webpack.config.api.js # API server config
App/
index.js # Default/www entry point
index.server.js # API entry point
components/
Header.jsx
Footer.jsx
api/
routes.js
handlers/
users.js
posts.js
shared/
utils.js # Shared between www and api
constants.js
Step 2: Configure Profiles
{
"default": {
"rootFolder": "App",
"extend": ["lpack-react"],
"vars": {
"rootAlias": "App",
"project": "my-fullstack-app",
"devPort": 3000
}
},
"www": {
"basedOn": "default",
"config": "./webpack.config.www.js",
"vars": {
"targetDir": "dist/www",
"production": true
}
},
"api": {
"basedOn": "default",
"config": "./webpack.config.api.js",
"vars": {
"targetDir": "dist/api",
"externals": true
}
}
}
Step 3: Web Frontend Config
const layerPack = require('layer-pack');
const cfg = layerPack.getConfig();
module.exports = [
{
mode: cfg.vars.production ? 'production' : 'development',
target: 'web',
entry: {
App: cfg.vars.rootAlias // resolves to App/index.js
},
output: {
path: layerPack.getHeadRoot() + '/' + cfg.vars.targetDir,
filename: '[name].[contenthash:8].js',
publicPath: '/'
},
plugins: [ layerPack.plugin() ],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: layerPack.isFileExcluded(),
use: 'babel-loader'
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
resolve: {
extensions: ['.js', '.jsx', '.json']
},
devServer: {
port: cfg.vars.devPort
}
}
];
Step 4: API Server Config
const layerPack = require('layer-pack');
const cfg = layerPack.getConfig();
module.exports = [
{
mode: cfg.vars.production ? 'production' : 'development',
target: 'node',
entry: {
'App.server': cfg.vars.rootAlias + '/index.server'
},
output: {
path: layerPack.getHeadRoot() + '/' + cfg.vars.targetDir,
filename: '[name].js',
libraryTarget: 'commonjs2'
},
plugins: [ layerPack.plugin() ],
module: {
rules: [
{
test: /\.jsx?$/,
exclude: layerPack.isFileExcluded(),
use: 'babel-loader'
}
]
},
resolve: {
extensions: ['.js', '.jsx', '.json']
}
}
];
Step 5: Shared Code
Both the web frontend and API server can import shared code using the same namespace:
// This file is available to both www and api profiles
// via: import { formatDate } from 'App/shared/utils';
export function formatDate(date) {
return new Intl.DateTimeFormat('en-US').format(new Date(date));
}
export function slugify(text) {
return text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
}
import express from 'express';
import { formatDate } from 'App/shared/utils';
const app = express();
app.get('/api/status', (req, res) => {
res.json({
status: 'ok',
timestamp: formatDate(Date.now())
});
});
app.listen(8080, () => {
console.log('API server running on :8080');
});
Step 6: Build Both Targets
# Build the web frontend
npx lpack :www
# Build the API server
npx lpack :api
# Run the API server
node dist/api/App.server.js
# Dev server for the frontend
npx lpack-dev-server :www
Each profile builds independently with its own webpack config, target, and output directory, but they share the same source tree and layer chain.
Tutorial: Component Override with $super
$super.
The Scenario
Your team uses a shared UI layer (@company/ui-layer) that provides a Button component. Your project needs to add analytics tracking to every button click, but you still want the original Button's rendering and behavior.
Step 1: The Parent Layer's Button
Here is what the parent layer provides (you do not modify this):
import React from 'react';
export default function Button({ children, onClick, variant = 'primary', ...props }) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
{...props}
>
{children}
</button>
);
}
Step 2: Override with $super
Create the same file path in your project to override it. Use $super to import the parent's version:
import React, { useCallback } from 'react';
// Import the parent layer's Button via $super
import OriginalButton from '$super';
import { trackEvent } from 'App/analytics/tracker';
export default function Button({ children, onClick, trackingLabel, ...props }) {
const handleClick = useCallback((e) => {
// Add analytics tracking
if (trackingLabel) {
trackEvent('button_click', {
label: trackingLabel,
component: 'Button'
});
}
// Call the original onClick handler
if (onClick) onClick(e);
}, [onClick, trackingLabel]);
// Render the parent layer's Button with enhanced onClick
return (
<OriginalButton onClick={handleClick} {...props}>
{children}
</OriginalButton>
);
}
Step 3: Use It Transparently
Any code that imports App/components/Button gets your enhanced version automatically. The consumer does not know or care that it is an override:
import React from 'react';
import Button from 'App/components/Button';
export default function Home() {
return (
<div>
<h1>Welcome</h1>
{/* This uses your enhanced Button with analytics tracking */}
<Button
trackingLabel="hero-cta"
onClick={() => window.location = '/signup'}
>
Get Started
</Button>
</div>
);
}
How the Resolution Chain Works
Import: "App/components/Button"
│
├─ Check my-project/App/components/Button.jsx ← FOUND (head project wins)
│ └─ This file imports "$super"
│ └─ Resolves to: @company/ui-layer/App/components/Button.jsx
│
└─ Result: Consumer gets the enhanced Button,
which internally renders the original Button
Chaining Multiple Overrides
$super works through the entire layer chain. If you have three layers (A extends B extends C), and both A and B override the same file, each can use $super to access the next layer's version:
// Layer A (head) — App/components/Button.jsx
import ParentButton from '$super'; // gets Layer B's version
// Layer B (middle) — App/components/Button.jsx
import ParentButton from '$super'; // gets Layer C's version
// Layer C (base) — App/components/Button.jsx
// No $super — this is the bottom of the chain
Tutorial: Glob-Powered Auto-Routing
App/pages/ automatically registers it as a route — no manual imports or route config needed.
The Concept
Instead of manually importing every page component and wiring up routes, use glob imports to auto-discover all page files. The glob pattern captures the file path, which becomes the route path.
Step 1: Create Page Components
Create page components using a file-system convention. The file path determines the URL path:
App/
pages/
Home.jsx # Route: /
About.jsx # Route: /about
Contact.jsx # Route: /contact
blog/
Index.jsx # Route: /blog
Post.jsx # Route: /blog/post
import React from 'react';
export default function Home() {
return <h1>Home Page</h1>;
}
// Optional: export route metadata
export const routeMeta = {
path: '/',
exact: true,
title: 'Home'
};
import React from 'react';
export default function About() {
return <h1>About Us</h1>;
}
export const routeMeta = {
path: '/about',
title: 'About'
};
Step 2: Auto-Import All Pages with Glob
Use a glob import to auto-discover all page components. The captured group becomes the key:
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Glob import: auto-discover all .jsx files under App/pages/
// The (**/*.jsx) pattern matches recursively
// Each match becomes a key like "Home", "About", "blog/Index", "blog/Post"
import pages from 'App/pages/(**/*.jsx)';
function filePathToRoute(key) {
// Convert file path to URL path
// "Home" -> "/"
// "About" -> "/about"
// "blog/Index" -> "/blog"
// "blog/Post" -> "/blog/post"
let route = '/' + key
.replace(/\/Index$/, '') // Index files map to parent directory
.replace(/^Home$/, '') // Home maps to root
.toLowerCase();
return route || '/';
}
export default function AppRouter() {
return (
<BrowserRouter>
<Routes>
{Object.keys(pages).map(key => {
const PageComponent = pages[key].default || pages[key];
const meta = pages[key].routeMeta || {};
const path = meta.path || filePathToRoute(key);
return (
<Route
key={key}
path={path}
element={<PageComponent />}
/>
);
})}
</Routes>
</BrowserRouter>
);
}
Step 3: Use with Code Splitting
For larger apps, use the LazyReact codec to automatically code-split each page:
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// LazyReact codec wraps each import in React.lazy()
// Each page becomes a separate chunk, loaded on demand
import pages from 'App/pages/(**/*.jsx)?using=lazyReact';
function filePathToRoute(key) {
let route = '/' + key
.replace(/\/Index$/, '')
.replace(/^Home$/, '')
.toLowerCase();
return route || '/';
}
export default function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{Object.keys(pages).map(key => {
const LazyPage = pages[key];
const path = filePathToRoute(key);
return (
<Route
key={key}
path={path}
element={<LazyPage />}
/>
);
})}
</Routes>
</Suspense>
</BrowserRouter>
);
}
Step 4: Auto-Routing Across Layers
The most powerful aspect: glob imports scan all layer roots. If a parent layer provides default pages and your project adds more, they are all discovered automatically:
Glob: "App/pages/(**/*.jsx)" scans all layers:
my-project/App/pages/
Home.jsx ← Head project (highest priority)
Dashboard.jsx ← Only in head project
@company/ui-layer/App/pages/
Home.jsx ← Shadowed by head project's version
About.jsx ← Inherited (not in head project)
Contact.jsx ← Inherited (not in head project)
Result: { Home, Dashboard, About, Contact }
Head project's Home.jsx wins over the parent's.
Adding a New Route
To add a new route, simply create a new .jsx file in App/pages/:
import React from 'react';
export default function Settings() {
return <h1>Settings</h1>;
}
That is it. No router config to update, no import to add. The glob import picks it up automatically. In watch mode, the virtual module is regenerated when new files appear in the matched directories.
import handlers from 'App/api/handlers/(*.js)' to auto-discover all route handler files and register them with Express.