Vyriy Webpack Config

@vyriy/webpack-config is a shared Webpack configuration package for Vyriy projects.

The goal is to keep Webpack setup small and reusable across different runtime targets:

  • TypeScript API server;
  • React SSR server;
  • React CSR browser application.

Each project keeps only the local entry point and output settings. The common Webpack behavior lives in @vyriy/webpack-config.

This article shows three small examples.

Part 1: TypeScript API

The first example builds a small TypeScript API server.

It uses:

  • @vyriy/webpack-config for the shared Webpack SSR/server build;
  • @vyriy/typescript-config for the shared TypeScript baseline;
  • @vyriy/handler for API handler wrapping;
  • @vyriy/server for running the handler locally;
  • @vyriy/path for clean output paths.

Installation

yarn add @vyriy/path @vyriy/typescript-config @vyriy/webpack-config webpack webpack-cli @vyriy/handler @vyriy/server

TypeScript config

Create tsconfig.json:

{
  "extends": "@vyriy/typescript-config/index.json",
  "include": ["*.ts"]
}

API handler

Create handler.ts:

import { api } from '@vyriy/handler';

export const handler = api(async (event) => ({
  statusCode: 200,
  body: JSON.stringify({
    path: event.path,
  }),
}));

The handler returns a simple JSON response with the request path.

Local server

Create server.ts:

import { server } from '@vyriy/server';
import { handler } from './handler';

server(handler);

This file connects the handler to the local server runtime.

Webpack config

Create webpack.config.mjs:

import 'webpack';

import { path } from '@vyriy/path';
import { ssr } from '@vyriy/webpack-config';

export default ssr('./server', {
  path: path('dist', 'api'),
  filename: 'index.js',
  library: { type: 'commonjs2' },
  clean: true,
});

The ssr helper is used here because this bundle targets Node.js, not the browser.

The output file will be:

dist/api/index.js

Build API bundle

Run Webpack in production mode:

NODE_ENV=production npx webpack --mode production

Output:

asset index.js 7.64 KiB [emitted] [minimized] (name: main)
orphan modules 25.6 KiB [orphan] 65 modules
./server.ts + 52 modules 22.7 KiB [not cacheable] [built] [code generated]
webpack 5.106.2 compiled successfully in 560 ms

Run API server

Run the bundled server:

node dist/api

Output:

http://127.0.0.1:3000/
http://192.168.86.34:3000/

Open the local URL in a browser or call it with curl.

Response:

{ "path": "/" }

This confirms that the TypeScript API was bundled and started successfully.


Part 2: React SSR

The second example builds a React SSR server.

It uses the same server-side Webpack config, but the handler renders a React component to an HTML string.

Installation

yarn add @vyriy/path @vyriy/typescript-config @vyriy/webpack-config webpack webpack-cli @vyriy/handler @vyriy/server react react-dom @types/react @types/react-dom

TypeScript config

Create tsconfig.json:

{
  "extends": "@vyriy/typescript-config/index.json",
  "include": ["*.ts", "*.tsx"]
}

This time the project includes both TypeScript and TSX files.

React component

Create app.tsx:

export const App = () => <div>App</div>;

SSR handler

Create handler.tsx:

import { renderToString } from 'react-dom/server';
import { api } from '@vyriy/handler';
import { App } from './app';

export const handler = api(async (event) => ({
  statusCode: 200,
  body: JSON.stringify({
    html: renderToString(<App />),
  }),
}));

The handler renders <App /> with renderToString and returns the generated HTML inside a JSON response.

Local server

Create server.ts:

import { server } from '@vyriy/server';
import { handler } from './handler';

server(handler);

Webpack config

Create webpack.config.mjs:

import 'webpack';

import { path } from '@vyriy/path';
import { ssr } from '@vyriy/webpack-config';

export default ssr('./server', {
  path: path('dist', 'api'),
  filename: 'index.js',
  library: { type: 'commonjs2' },
  clean: true,
});

This is almost the same as the API example. The main difference is the application code: now the server bundle includes React SSR logic.

Build SSR bundle

Run Webpack:

NODE_ENV=production npx webpack --mode production

Output:

asset index.js 213 KiB [emitted] [minimized] (name: main)
orphan modules 25.9 KiB [orphan] 66 modules
built modules 557 KiB [built]
  modules by path ./ 557 KiB
    modules by path ./node_modules/react-dom/ 516 KiB
      modules by path ./node_modules/react-dom/cjs/*.js 514 KiB 3 modules
      modules by path ./node_modules/react-dom/*.js 1.98 KiB 2 modules
    modules by path ./node_modules/react/ 18.2 KiB
      modules by path ./node_modules/react/*.js 396 bytes 2 modules
      modules by path ./node_modules/react/cjs/*.js 17.8 KiB 2 modules
    ./server.ts + 53 modules 23 KiB [not cacheable] [built] [code generated]
  external "util" 42 bytes [built] [code generated]
  external "crypto" 42 bytes [built] [code generated]
  external "async_hooks" 42 bytes [built] [code generated]
  external "stream" 42 bytes [built] [code generated]
webpack 5.106.2 compiled successfully in 1141 ms

Run SSR server

Run the bundled server:

node dist/api

Output:

http://127.0.0.1:3000/
http://192.168.86.34:3000/

Response:

{ "html": "<div>App</div>" }

This confirms that the React component was rendered on the server.


Part 3: React CSR

The third example builds a browser-side React application.

It uses:

  • csr from @vyriy/webpack-config;
  • @vyriy/browserslist-config for browser targets;
  • @vyriy/html to create a minimal HTML template;
  • html-webpack-plugin to generate index.html.

Installation

yarn add @vyriy/path @vyriy/typescript-config @vyriy/webpack-config webpack webpack-cli react react-dom @types/react @types/react-dom @vyriy/browserslist-config @vyriy/html html-webpack-plugin

Browserslist config

Create .browserslistrc:

[development]
extends @vyriy/browserslist-config

[ssr]
extends @vyriy/browserslist-config

[production]
extends @vyriy/browserslist-config

[modern]
extends @vyriy/browserslist-config

This keeps browser targets shared and consistent across Vyriy projects.

TypeScript config

Create tsconfig.json:

{
  "extends": "@vyriy/typescript-config/index.json",
  "include": ["*.ts", "*.tsx"]
}

React component

Create app.tsx:

export const App = () => <div>App</div>;

Browser entry point

Create index.tsx:

import { createRoot } from 'react-dom/client';
import { App } from './app';

createRoot(document.getElementById('root')!).render(<App />);

This is the client-side entry point. It mounts the React application into the HTML element with id="root".

Webpack config

Create webpack.config.mjs:

import 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';

import { csr } from '@vyriy/webpack-config';
import { path } from '@vyriy/path';
import { html } from '@vyriy/html';

export default csr(
  './index',
  {
    path: path('dist'),
    filename: 'index.js',
    clean: true,
  },
  (config) => {
    return {
      ...config,
      plugins: [
        ...(config.plugins ?? []),
        new HtmlWebpackPlugin({
          templateContent: html({
            body: '<div id="root"></div>',
          }),
          publicPath: '/',
          hash: true,
          inject: 'body',
          minify: {
            removeComments: true,
            collapseWhitespace: true,
            removeAttributeQuotes: false,
            minifyJS: true,
            minifyCSS: true,
          },
        }),
      ],
    };
  },
);

The csr helper creates a browser-oriented Webpack config.

The third argument is a small extension function. It receives the generated config and returns an updated version with HtmlWebpackPlugin added.

This keeps the default shared config reusable, while still allowing the local project to add its own plugins.

Build CSR bundle

Run Webpack:

NODE_ENV=production npx webpack --mode production

Output:

asset index.html 160 bytes [emitted]
orphan modules 122 bytes [orphan] 1 module
modules by path ./node_modules/ 561 KiB
  modules by path ./node_modules/react-dom/ 533 KiB
    modules by path ./node_modules/react-dom/*.js 2.67 KiB 2 modules
    modules by path ./node_modules/react-dom/cjs/*.js 530 KiB 2 modules
  modules by path ./node_modules/react/ 18.2 KiB
    modules by path ./node_modules/react/*.js 396 bytes 2 modules
    modules by path ./node_modules/react/cjs/*.js 17.8 KiB 2 modules
  modules by path ./node_modules/scheduler/ 10.1 KiB
    ./node_modules/scheduler/index.js 194 bytes [built] [code generated]
    ./node_modules/scheduler/cjs/scheduler.production.js 9.94 KiB [built] [code generated]
./index.tsx + 1 modules 326 bytes [built] [code generated]
webpack 5.106.2 compiled successfully in 1388 ms

Check output files

List the dist directory:

ls dist

Output:

index.html
index.js

The CSR build produces static files:

dist/index.html
dist/index.js

These files can be served by any static file server or deployed to S3, CloudFront, GitHub Pages, GitLab Pages, Nginx, or another static hosting target.

Why shared Webpack config?

Webpack can be powerful, but local configuration often becomes noisy.

A small project should not need to copy a large Webpack setup just to build a TypeScript server, an SSR handler, or a browser React entry point.

With @vyriy/webpack-config, the local project keeps only the important details:

entry point
output directory
output filename
runtime target
small local extensions

The shared package owns the common behavior.

This makes the setup easier to reuse across Vyriy packages:

api server      -> ssr('./server', ...)
react ssr       -> ssr('./server', ...)
react csr       -> csr('./index', ...)

The result is a calm Webpack setup: explicit where it matters, shared where repetition would create noise.

Documentation

See the Webpack config API documentation.