@asd14/vblocks
Zero-config React builds, as a static site or a single HTML file
✂️ Zero config
Needs only src/index.tsx and src/index.html
🔋 Maximal support
SWC, PostCSS, CSS Modules, MDX, React Refresh, Dotenv
📦 Several apps, one install
Apps share one package.json and node_modules/. A new app reuses the
installed packages
🤖 Optimized for AI agents
The agent's context holds only the app: no Webpack, PostCSS or TypeScript config
to write, read or fix
[!IMPORTANT]
VBlocks needs Node^22.22.3,^24.15.0or>=26.0.
Table of contents
Quick Start
-
Scaffold a minimal React project
npx @asd14/vblocks init my-vblocks-app cd my-vblocks-app npm installmy-vblocks-app/ ├─ src/ │ ├─ index.tsx │ ├─ index.html │ └─ index.css ├─ .gitignore ├─ tsconfig.json └─ package.json # react, react-dom and the vblocks scripts -
Start a development server
npx vblocks serve -
Build
npx vblocks build # production bundle in dist/ npx vblocks build --single-file # one dist/index.html, every asset inlined
Several apps in one project
Several apps, eg. demos or MVP sketches, can share one package.json and one
node_modules/:
-
Add the plumbing
npx @asd14/vblocks init sketches --only plumbingsketches/ ├─ .gitignore └─ package.json -
Add an app
npx @asd14/vblocks init sketches/flow-diagram --template xyflow --only app cd sketches npm installsketches/ + ├─ flow-diagram/ + │ ├─ src/ # the files of the xyflow template + │ ├─ postcss.config.js + │ └─ tsconfig.json ├─ .gitignore ! └─ package.json # + @xyflow/react, tailwindcss, @tailwindcss/postcss, cssnano -
Start a development server for one app
npx vblocks serve --prefix flow-diagram
Vocabulary
| Term | Means |
|---|---|
| Plumbing | The project files init writes: package.json, .gitignore |
| Project folder | The folder with package.json |
| App folder | The folder with src/index.tsx and src/index.html |
| Bundle | The files build writes to dist/ |
How to
Use environment variables
-
Add a
.envfile to your app folderAPI_URL=https://api.example.com -
Read the variable in your code
const apiUrl = process.env["API_URL"]
Webpack replaces each process.env["NAME"] with the value of NAME, and
undefined when .env does not set it. Webpack leaves out of the bundle every
variable your code does not read.
[!WARNING]
Never expose a secret, eg. an API key, throughprocess.env. Anyone who loads the page can read it.
[!NOTE]
The client types declareprocess.envand nothing else from Node, because Webpack replaces nothing else.
Stop using ../../../ everywhere
VBlocks resolves @self to the src/ of your app folder:
// Instead of: import { Component } from "../../../components"
import { Component } from "@self/components"
[!NOTE]
VBlocks compiles TypeScript with SWC and does not typecheck. Runtscyourself. Yourtsconfig.jsonneeds the same alias as Webpack.
Thetsconfig.jsonthatvblocks initwrites already has it. Otherwise, add:{ "compilerOptions": { + "paths": { "@self/*": ["./src/*"] } } }
Use CSS Modules
-
Name the file
*.module.css/* src/button.module.css */ .primary-button { color: tomato; } -
Import it as an object
/* src/button.tsx */ import styles from "./button.module.css" export const Button = () => <button className={styles.primaryButton} />
- camelCase keys -
.primary-buttonbecomesstyles.primaryButton - Class names - a bare hash in
productionmode,[local]__[hash]indevelopmentmode
[!NOTE]
The client types declare.css, image and font imports.
Replace a default config
Each config in your app folder replaces the default of the same name:
webpack.config.jspostcss.config.jspostcss-preset-env, which compiles nesting,oklch()andcolor-mix()for older browsers and adds vendor prefixescssnanoinproduction
.swcrc- browser targets
> 0.25%, not dead
- browser targets
-
Eject the default into your app folder
cd flow-diagram npx vblocks eject swc > .swcrc -
Update the ejected file
[!NOTE]
VBlocks does not merge your config with the default, with two exceptions:
.swcrc -> env.targets: VBlocks adds the default when yours is missing.swcrc -> jsc.transform.react.refresh: VBlocks adds the React Refresh transform whenserveruns indevelopmentmode
Style with Tailwind
VBlocks does not include Tailwind. Add it through a postcss.config.js in your
app folder:
[!NOTE]
Thexyflowandxyflow-tabstemplates ofvblocks inithave this setup.
-
Install Tailwind, the Tailwind plugin for PostCSS and cssnano, which the ejected config imports
npm install --save-dev tailwindcss @tailwindcss/postcss cssnano -
Eject the default PostCSS config into your app folder
npx vblocks eject postcss > postcss.config.js -
Replace
postcss-preset-envwith the Tailwind plugin+import tailwindcss from "@tailwindcss/postcss" import cssnano from "cssnano" -import postcssPresetEnv from "postcss-preset-env" export default ({ mode }) => ({ plugins: [ - postcssPresetEnv({ stage: 2 }), + tailwindcss({ optimize: mode === "production" }), ...(mode === "production" ? [cssnano({ preset: "default" })] : []), ], })
[!IMPORTANT]
Tailwind 4 compiles nesting and adds vendor prefixes itself. With both plugins,postcss-preset-envrewrites Tailwind's@layerrules into:not(#\#)selectors. The CSS file grows.
-
Add the Tailwind import to
src/index.css@import "tailwindcss" source(".");source(".")limits Tailwind's class scan to the folder of the CSS file,src/.
[!NOTE]
Withoutsource("."), Tailwind scans the current folder. With--prefix, you run VBlocks from the project folder, which holds every app. Each app's CSS then also gets the classes of the other apps.
Add VBlocks to an existing project
-
Install VBlocks
npm install --save-dev @asd14/vblocks # needs react and react-dom ^18 or ^19 -
Add the client types and the
@selfalias to yourtsconfig.json{ "compilerOptions": { + "types": ["@asd14/vblocks/client"], + "paths": { "@self/*": ["./src/*"] } } } -
Ensure
index.tsxandindex.htmlare insrc/
[!NOTE]
VBlocks buildsdist/index.htmlfromsrc/index.htmland adds the script and stylesheet tags to it. Drop the tag that loads your entry, eg. Vite's<script type="module" src="/src/main.tsx"></script>.
-
Start a development server
npx vblocks serve
References
Commands
Each command prints its own help with --help, eg. npx vblocks serve --help.
vblocks init
Write a template's plumbing, app, or both into <directory>.
vblocks init <directory>
[--template <plain|xyflow|xyflow-tabs>]
[--only <app|plumbing>]
<directory>- The folder to write into. It must be missing or empty
--template(default:plain)- The template to write, see Templates
--only(default: both)- Write one part of the template
plumbing:package.json,.gitignoreapp:src/,tsconfig.jsonand the template's configs.initadds the template's dependencies to thepackage.jsonof the project folder, the parent of<directory>. Without apackage.jsonthere,initfails
vblocks serve
Start the development server.
- Runs at
http://localhost:8080, or the next free port - Listens on
localhost, so another device on your network, eg. a phone, cannot open the page - Opens the page in the browser
vblocks serve
[--mode <development|production>]
[--prefix <directory>]
[--analyze]
[--log-level <error|warn|info|verbose>]
[-- <webpack option>...]
--mode(default:development)- The Webpack mode
- React Refresh runs only in
development
--prefix(default: the current folder)- The app folder, which must exist
--analyze- Run the bundle analyzer, on a free port
--log-level(default:warn)- How much Webpack and its development server print
-- <webpack option>...- Any
webpack serveoption servepasses each one to Webpack unchanged- eg.
-- --host 0.0.0.0, to listen on the network
- Any
vblocks build
Build the app into a bundle in the dist/ of the app folder.
By default, build:
- Writes JS and CSS files with content-hashed names
- Writes a
reactchunk, and avendorschunk when your other dependencies total over 20 KB (10 KB indevelopment) - Inlines
jpg,gif,pngandsvgfiles up to 10 KB - Writes larger images to
dist/with content-hashed names - Writes
woff,woff2,ttfandeotfiles todist/fonts/ - Copies
src/public/favicon.svgandsrc/public/robots.txtof your app, or the defaults of VBlocks
vblocks build
[--mode <development|production>]
[--prefix <directory>]
[--single-file|--single]
[--log-level <error|warn|info|verbose>]
[-- <webpack option>...]
--mode(default:production)- The Webpack mode
- In
production,buildminifies JS, HTML and, with the default PostCSS config, CSS
--prefix(default: the current folder)- The app folder, which must exist
--single-file|--single- Write one standalone
dist/index.html, every asset inlined - A browser opens the file from disk, with no server and no network
- The bundle has no
robots.txt - Keep the route in the URL hash
- On
file://,history.pushStateto another path throws
- On
- Write one standalone
--log-level(default:warn)- How much Webpack prints
-- <webpack option>...- Any
webpack buildoption buildpasses each one to Webpack unchanged- eg.
-- --devtool source-map, to write source maps
- Any
vblocks eject
Print a default config to stdout.
vblocks eject <webpack|postcss|swc>
<webpack|postcss|swc>- The config to print
Run it in your app folder and save the output there, to replace the default:
npx vblocks eject webpack > webpack.config.js
npx vblocks eject postcss > postcss.config.js
npx vblocks eject swc > .swcrc
An ejected config loads its plugins and loaders from your project. Install them
in the project folder, eg. postcss-preset-env and cssnano for the PostCSS
config.
Templates
All templates scaffold src/index.html, src/index.css and tsconfig.json.
plain
"Hello World!" in src/index.tsx.
- Adds no packages
xyflow
One diagram, and a details panel for the selected node or edge.
- Adds
@xyflow/react,tailwindcss,@tailwindcss/postcssandcssnanoto thepackage.jsonof the project folder - Writes a
postcss.config.jsthat configures Tailwind
src/
├── index.tsx # the layout: header, diagram, details panel, the selected id
├── diagram.tsx # the nodes and edges, placed and sized in grid cells
├── details.tsx # the panel for the selected node or edge
└── utils/grid.ts # CELL_SIZE and toPixels
xyflow-tabs
One tab per diagram, each with its own details panel.
- Adds the same packages and
postcss.config.jsasxyflow
src/
├── index.tsx # the layout: header, tab bar, details panel, the selected id
├── pages/
│ ├── build/
│ │ ├── diagram.tsx # the nodes and edges of one tab, in grid cells
│ │ └── details.tsx # the panel for its selected node or edge
│ └── serve/ # the same two files
├── ui/zoom-level.tsx # the zoom level, and a Reset to 100%
└── utils/grid.ts # CELL_SIZE and toPixels
License
MIT