Tutorials

Hands-on guides that walk you through real-world layer-pack patterns. Each tutorial builds a complete, working example.

Tutorial: Reusable React Layer

What you will build A reusable layer (npm package) with React, babel, and webpack preconfigured. Then a consuming project that extends it, inheriting the entire build stack and adding its own components.

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.

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

my-react-layer/.layers.json
{
  "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:

my-react-layer/webpack.config.js
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:

my-react-layer/App/index.js
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 />);
my-react-layer/App/components/App.jsx
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>
  );
}
my-react-layer/App/components/Header.jsx
import React from 'react';

export default function Header() {
  return (
    <header>
      <nav>Base Layer Navigation</nav>
    </header>
  );
}

Step 5: Publish or Link the Layer

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

terminal
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
my-app/.layers.json
{
  "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:

my-app/App/components/Header.jsx
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>
  );
}
terminal
# 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

What you will build A project with a shared core layer and two separate build targets: a web frontend (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:

Directory structure
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

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

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

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

App/shared/utils.js
// 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, '');
}
App/index.server.js
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

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

What you will build A project that overrides a parent layer's Button component, wrapping it with additional functionality while preserving the original via $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):

@company/ui-layer/App/components/Button.jsx
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:

my-project/App/components/Button.jsx
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:

App/pages/Home.jsx
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:

$super resolution chain
// 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

What you will build An automatic routing system where adding a new file to 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:

Directory structure
App/
  pages/
    Home.jsx           # Route: /
    About.jsx          # Route: /about
    Contact.jsx        # Route: /contact
    blog/
      Index.jsx        # Route: /blog
      Post.jsx         # Route: /blog/post
App/pages/Home.jsx
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'
};
App/pages/About.jsx
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:

App/router.jsx
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:

App/router.jsx (with code splitting)
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/:

App/pages/Settings.jsx
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.

Tip: Express Route Handlers The same glob pattern works for API routes too. Use import handlers from 'App/api/handlers/(*.js)' to auto-discover all route handler files and register them with Express.