@blupp/foundation
v0.5.0
Published
Centrally configured typography, layout, native actions and form controls for HTML, React and Astro
Readme
Blupp Foundation
A centrally configured design system for HTML, React, and Astro. One typed blupp.config.ts produces ordinary CSS, an optional Tailwind v4 theme bridge, and project-local component bindings. No runtime theme provider is needed.
Version 0.5.0 is licensed under MIT. Components are H1–H6, Text (p), Container (div), Stack (div), Button (button), Link (a), Label (label), Input (input), Textarea (textarea), Select (select), Checkbox and Radio (native inputs), Card (div), Badge (span), Divider (hr), Grid and Row (div), and Image (img). Variant styling never changes their semantic tags.
Astro quickstart
The npm package is @blupp/foundation. In Astro pages, @design/H1.astro is a project-local alias for a generated component with your configured variant types.
Use Node 22.12+ and the minimal Astro example. It includes Astro, TypeScript, @astrojs/check, a hero variant, plain CSS, and both root and nested pages. Inside the example directory:
npm install @blupp/foundation
npm run generate
npm run devThe install command adds Foundation and installs the example's declared dependencies. For local package development, use the packed-package workflow instead. Open the URL printed by Astro to see a centered “Welcome” heading. Generated files are absent on a clean clone: generate prepares editor types, while dev also generates before starting Astro. No install hook is needed.
Stop dev with Ctrl+C before running npm run check, npm run build, or standalone npm run generate: the dev supervisor owns the generated-output lock for its lifetime. After creating and committing the consumer's lockfile, clean CI can run npm ci followed by npm run build. If testing a local tarball, keep it available at its locked location.
Set up an existing Astro project
Install Foundation in your Astro application, then preview and apply initialization:
npm install @blupp/foundation
npm install -D typescript @astrojs/check
npx @blupp/foundation init --dry-run
npx @blupp/foundation init
npm run buildRun inside the application package containing package.json. Astro, TypeScript, @astrojs/check, and Foundation must be declared and installed; the installed Foundation version must match the CLI. Init does not install dependencies or change lockfiles. npm exec -- blupp init also runs the application's installed executable. For local package development, install the packed tarball from its absolute path first.
Init creates the Astro hero config when absent, adds the generated-component alias to local tsconfig.json, updates recognized stock Astro scripts, connects generated styles to one selected global CSS file and shared layout, adds precise output/recovery ignores, and generates artifacts. Existing configuration is preserved and validated under both locks. The result names the configured layout: pages must use it to receive the stylesheet. Rerunning unchanged setup adds no duplicate imports, aliases, scripts, or ignores.
Choose paths explicitly when discovery is ambiguous:
npm exec -- blupp init --config ./config/design.ts --out-dir ./src/generated/design --alias @ui --css ./src/styles/global.css --layout ./src/layouts/Base.astro --dry-run--config, --out-dir, --css, and --layout must stay inside the application and cannot traverse symlinks or overlap generated/authored/recovery paths. --alias accepts a prefix without a wildcard or trailing slash. Custom choices feed every script, alias, import, and ignore rule. Supported layout edits use parsed Astro frontmatter and direct CSS imports; ordinary CSS is supported, and a direct unconditional tailwindcss import adds the v4 bridge immediately after Foundation styles. When adding a stylesheet import, non-CSS side-effect imports require inspecting indirect wiring and adding the intended direct CSS import manually first; already-connected layouts preserve those imports. Bound component imports are preserved and do not prove the absence of indirect CSS. Indirect or advanced CSS wiring may require a manual bridge/import adjustment.
--dry-run prints before/after patches, relevant lifecycle hooks, conflicts, and deferred config validation; it performs no generation, config evaluation, writes, or lock creation. Apply runs without a prompt in terminals and CI. Exit codes are 0 for a valid preview/success, 1 for validation/write/generation errors, and 2 for conflicting files or unresolved selections. Custom scripts, conflicting aliases, inherited alias maps that a local map would replace, malformed inputs, and ignore negations need manual resolution before any source edits. Existing relevant pre/post hooks remain active and are reported.
On source-write failure, init restores only files it wrote whose bytes still match, preserving newer editor changes and retaining .blupp-init.blupp-backup/ with recovery instructions when needed. An interruption leaves ownership/recovery evidence that blocks fresh apply; inspect owner.json, confirm its process and descendants have stopped, then compare recovery record.json digests and numbered originals before manually resolving partial changes. Locks never expire automatically. If generation fails after source setup, init reports configuration applied; generation failed and prints the matching blupp build retry command. Trusted config side effects cannot be rolled back.
The manual setup below remains available for advanced applications.
Install @blupp/foundation, and install typescript and @astrojs/check if missing. Use an Astro version supported by the package peer range (currently ^7.3.6); the example pins the development versions used by this repository. Copy blupp.config.ts to your app root. It selects only adapters: ['astro'] and defines H1.variants.hero.
Add this alias to the existing compilerOptions.paths in tsconfig.json, preserving other settings:
{
"compilerOptions": {
"paths": {
"@design/*": ["./src/generated/blupp/astro/*"]
}
}
}Astro supports TypeScript import aliases. If @design/* is already used, choose another prefix and use it consistently in component imports. This alias belongs to your application; it is not an npm package or a Foundation export.
Add the generate, dev, check, and build scripts shown under Generate and integrate. Then create your global stylesheet:
/* src/styles.css */
@import "./generated/blupp/styles.css";
.text-center { text-align: center; }Import this stylesheet once in a shared layout, as in Layout.astro. Pages using that layout can import components identically at any directory depth:
---
import H1 from '@design/H1.astro';
import Layout from '../layouts/Layout.astro';
---
<Layout title="Welcome">
<H1 variant="hero" class="text-center" id="intro">Welcome</H1>
</Layout>Ignore src/generated/, .astro/, and the sibling *.blupp-lock, *.blupp-backup/, and *.blupp-stage-*/ paths in your app's .gitignore; merge the example entries into existing rules. Run npm run generate, then npm run dev. Use the Astro editor extension for .astro completion and diagnostics. Adding a variant to the config while dev runs regenerates its CSS and types and restarts Astro; an invalid edit stops the app until corrected. Invalid initial configuration exits, so correct it and start dev again. npm run check rejects nonexistent variants.
Tailwind is optional. If your app already compiles Tailwind v4, use the Tailwind import sequence instead of the plain stylesheet above; its text-center utility replaces the local rule.
Custom output paths and the package rename
When using --out-dir, update the alias target, global CSS imports, ignore entries, and every blupp build, blupp run, and blupp dev invocation together. For example, --out-dir ./src/generated/foundation needs @design/* mapped to ./src/generated/foundation/astro/* and the stylesheet to import ./generated/foundation/styles.css.
When moving an existing local consumer from @bluppco/foundation or @blupp/system, remove that dependency and install @blupp/foundation. Update package imports, including blupp.config.ts, to the new name. Output created by either previous package has a different ownership marker and is deliberately rejected. Stop the old dev process and choose a fresh dedicated output directory using the steps above, then regenerate. Keep old output intact until you have checked the new build; do not edit ownership markers to bypass validation. The executable remains blupp and configured component usage remains @design/H1.astro.
Develop this package
npm ci
npm run check
npm packnpm run check is the authoritative verification command: it builds the current source, runs unit/CLI tests, then runs packed-consumer and browser tests. npm test builds and runs just the unit/CLI tests. For a faster repeat against an existing build, use npm run test:unit; both that command and npm run test:integration consume dist, so run npm run build first whenever source changes.
Integration tests install packed consumer fixtures and therefore need npm registry access. Browser tests use installed Google Chrome on macOS; set BLUPP_CHROME_PATH to a Chrome/Chromium executable elsewhere, or run npx playwright install chromium to provide Playwright’s bundled Chromium.
Install the resulting tarball into a consumer to test local changes:
npm install /absolute/path/to/blupp-foundation-0.5.0.tgz
npm exec -- blupp init --dry-run
npm exec -- blupp initUse the actual filename reported by npm pack when the version changes. For the included example, run npm install ../../blupp-foundation-0.5.0.tgz inside examples/astro, then npm run generate. Keep the tarball at its locked location for subsequent npm ci runs. Package exports are explicit: @blupp/foundation/config, /core, /compiler, /react, and /astro/*.astro for the native component files. There is no root barrel. React and Astro are optional peers; install only the frameworks your application uses. HTML consumers need neither framework. Compiler dependencies stay out of browser import graphs.
The implementation targets Node 22.12+ and modern browsers supporting CSS cascade layers. Development versions are pinned in package-lock.json: React 19.3, Astro 7.3, TypeScript 6.0, Tailwind 4.3. Node 26 is used in this workspace. On POSIX the CLI supervises the child's process group, including descendants that stay in that group. Commands must remain in the foreground; processes that escape into a new session or group are outside this guarantee. Windows confirms only the immediate child's exit and has not been qualified for descendant cleanup. The package is ESM only.
Configure a project
// blupp.config.ts
import { defineConfig, token } from '@blupp/foundation/config';
export default defineConfig({
adapters: ['react', 'astro'],
tokens: {
colors: { brand: '#2563eb' },
fonts: { heading: 'Inter, sans-serif' },
},
components: {
H1: {
base: { fontFamily: token('fonts.heading'), fontSize: '2.5rem' },
at: { md: { fontSize: '3rem' } },
variants: {
hero: {
base: { fontSize: '3.5rem', textWrap: 'balance' },
at: { lg: { fontSize: '4rem' } },
},
},
},
},
});Use adapters: [] or omit it for HTML, ['react'] for React, and ['astro'] for Astro. Configuring a selected adapter requires its peer to be installed. See the complete example.
Tokens have categories colors, fonts, fontSizes, spacing, and radii. Token names, breakpoint names, and variant names begin with a lowercase letter and contain lowercase letters, digits, and single hyphens. Default breakpoints are sm: 40rem, md: 48rem, lg: 64rem, xl: 80rem; overrides merge by name, and all widths must remain positive, finite, and unique.
Declarations use camelCase CSS properties and string values, including explicit units. token('colors.brand') checks a reference at build time; a token alias can reference another token only in its category. Custom CSS properties must use --app-*. There are no raw selectors, callbacks, deletion markers, arbitrary at-rules, or !important. Invalid configuration reports its source file and configuration path. CSS validation checks structure and property names, not browser support or accessibility.
Partial overrides preserve omitted defaults. At each viewport width, component base and applicable component breakpoints are merged first; variant base and applicable variant breakpoints then take precedence. A variant base size therefore overrides a component responsive size until the variant changes it. Each rendered element uses exactly one complete recipe class.
CSS retains declaration groups in that precedence order, with package defaults before authored declarations at every base, breakpoint, variant, and state location. A later font shorthand therefore overrides earlier font longhands, and later breakpoint longhands can refine that shorthand. Within each declaration group, properties are emitted alphabetically by their config keys; overlapping shorthand/longhand declarations in the same group follow that order regardless of object insertion order.
Interaction states
Every component and named variant accepts states.hover, states.focusVisible, states.active, states.invalid, and states.disabled. Each contains base and optional breakpoint at declarations:
components: {
Button: {
variants: {
primary: {
states: {
hover: { base: { backgroundColor: '#1d4ed8' }, at: { md: { paddingInline: '2rem' } } },
focusVisible: { base: { outlineOffset: '5px' } },
},
},
},
},
}State styles follow ordinary responsive styles, in fixed order: hover, focusVisible, active, invalid, disabled. Each state contains only its own accumulated declarations, so simultaneous states compose: an active background preserves a focus outline. Component state declarations override ordinary variant styles; variant state declarations then refine the component state, including responsive declarations. :where(...) keeps the specificity equal to one class, preserving utility and inline overrides. Hover and active exclude native :disabled, including descendants of disabled fieldsets. disabled matches native disabled controls, not aria-disabled. State objects cannot contain nested states, variants, or selector strings.
Button supplies a neutral base and explicit primary, secondary, ghost, and destructive variants. Omit variant for the neutral base; use generated facades for built-in or custom variants. Buttons render native button, default to type="button", and forward form attributes, explicit submit/reset types, and boolean disabled. Native disabled controls cannot activate; plain HTML callers supply their own type.
Link renders native a and requires a non-null href in types and at runtime (BLUPP_LINK_HREF). An explicit empty href is valid; target, rel, download, and the framework's native href types pass through. There is no Link disabled prop or navigation interception. Use Button for actions and Link for navigation. Both ship visible focus styling; preserve a visible indicator when customizing focus.
Default action colors use the brand, brand-hover, brand-active, surface, subtle, subtle-hover, subtle-active, border, destructive, destructive-hover, and destructive-active color tokens. Changing brand alone does not derive hover/active shades; configure those tokens or state declarations explicitly.
Form controls
Label, Input, Textarea, Select, Checkbox, and Radio preserve native HTML behavior. Use generated bindings for configured variants. Checkbox and Radio fix their native type and exclude a caller-supplied type from props; Input accepts native input types. Input, Checkbox, and Radio are void elements. React uses htmlFor, defaultValue/defaultChecked or controlled value/checked plus handlers; Astro uses for, native attributes, textarea content, and selected on options. Select accepts native options and optgroups. Native disabled and disabled fieldsets control focus, activation, and inclusion in form data. React object and callback refs expose the underlying native element, including HTMLInputElement for Input, Checkbox, and Radio.
Use the built-in Text variants help and error with explicit IDs. Applications own validation timing, messages, announcements, and label/description associations:
import { Input, Label, Text } from './generated/blupp/react';
export function EmailField({ error }: { error?: string }) {
return <>
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" required
aria-invalid={Boolean(error)}
aria-describedby={`email-help${error ? ' email-error' : ''}`} />
<Text id="email-help" variant="help">Use your work email.</Text>
{error && <Text id="email-error" variant="error">{error}</Text>}
</>;
}In Astro, import the corresponding @design/*.astro components and use <Label for="email">. Give each form instance unique IDs. Group radios with a native fieldset and legend, and give related radios the same name. Components do not generate IDs, wrappers, validation state, or alert roles.
The invalid recipe state matches :where([aria-invalid="true"], :user-invalid). Explicit application errors show immediately; native constraint errors use the browser's interaction timing, keeping untouched required fields neutral. aria-invalid="false" does not suppress native :user-invalid. This styling does not set validity or block submission: applications use native constraints and/or their own validation. See MDN's native interaction state.
Inputs, textareas, and selects share borders, spacing, typography, focus outlines, disabled opacity, and invalid border colors. Checkboxes and radios preserve native appearance and use an invalid ring separate from the focus outline. Configure colors.control-border, colors.muted, colors.brand, and colors.destructive, or override each component's recipes. Disabled is emitted last; simultaneous states retain unrelated declarations. Preserve a visible focus indicator when customizing.
Try the React form for inline validation and a local success message, or open /form/ in the Astro example for inline validation and native GET submission to the guide. Both exercise all controls, help/error text, radio grouping, disabled fields, and reset. The Astro form keeps native browser validation when JavaScript is unavailable; its component adapters themselves need no hydration.
Card, Badge, and Divider
These components share the central recipe configuration, generated variant types, and native attribute forwarding used by the rest of the system. They add no runtime state or dependencies.
Generated component styles honor the native hidden attribute even when a recipe sets display. The until-found value is excluded from this hiding rule to retain browser reveal behavior where supported. Utility classes and inline styles can still override component styles.
Cardis a padded surface with a border and rounded corners. It renders adiv; compose headings, content, and actions inside it. It does not add click behavior or a landmark role.Badgerenders aspanfor short labels such as “Draft”. Its neutral style wraps long text within the available width. It does not add a button role or live announcements.Dividerrenders a horizontalhrwith native separator semantics. It has no children and no default margin; useStackor the surrounding layout for spacing. Usearia-hidden="true"for a purely decorative rule.
import { Badge, Card, Divider, H2, Stack, Text } from './generated/blupp/react';
export function Summary() {
return <Card>
<Stack>
<H2>Contact preferences</H2>
<div><Badge>Draft</Badge></div>
<Text>Choose how we should contact you.</Text>
<Divider />
<Text>Your preferences can be changed at any time.</Text>
</Stack>
</Card>;
}In Astro, import each component from @design/Card.astro, @design/Badge.astro, or @design/Divider.astro and use the same markup. For plain HTML, apply blupp-card, blupp-badge, and blupp-divider to div, span, and hr respectively.
All three start with a base recipe and support project-defined variants and breakpoints:
components: {
Card: { at: { md: { padding: '2rem' } } },
Badge: { variants: { emphasis: { base: { color: token('colors.surface'), backgroundColor: token('colors.brand') } } } },
Divider: { variants: { strong: { base: { borderBlockStartColor: token('colors.control-border') } } } },
}Use <Badge variant="emphasis">Featured</Badge> through generated bindings. Direct package adapters accept only the base recipe, as with existing components. Card uses the spacing.gutter, radii.default, colors.surface, colors.text, and colors.border tokens; Badge uses the shared body font and neutral colors; Divider uses colors.border.
Grid, Row, and Image
Grid and Row render native div elements. Grid starts with one shrinkable column; Row arranges children horizontally, wraps when needed, and centers them on the cross axis. Both use spacing.stack for their gap. Configure columns, alignment, spacing, and breakpoints through recipes; there are no additional layout props or built-in variants. Stack keeps its vertical layout.
For example, add these recipes to blupp.config.ts:
components: {
Grid: { variants: { cards: { at: {
md: { gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' },
lg: { gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' },
} } } },
Row: { variants: { actions: { base: { justifyContent: 'space-between' } } } },
Image: { variants: { card: { base: {
width: '100%', aspectRatio: '3 / 2', objectFit: 'cover',
borderRadius: token('radii.default'),
} } } },
}Then compose the generated components:
import { Badge, Card, Grid, H2, Image, Link, Row, Stack, Text } from './generated/blupp/react';
export function ProjectCard() {
return <Grid variant="cards">
<Card><Stack>
<Image variant="card" src="/workspace.jpg"
alt="Sunlit workspace with a wooden desk" width={960} height={640} />
<Row><Badge>Featured</Badge></Row>
<H2>Workspace</H2>
<Text>A quiet place for focused work.</Text>
<Row variant="actions"><Link href="/workspace/">View workspace</Link></Row>
</Stack></Card>
</Grid>;
}In Astro use @design/Grid.astro, @design/Row.astro, and @design/Image.astro; plain HTML uses blupp-grid, blupp-row, and blupp-image on div, div, and img. Recipe classes such as blupp-grid--cards also work directly in HTML. Direct package adapters remain base-only.
Image renders a native, void img. An explicit string alt is required by both adapters at compile time and runtime (BLUPP_IMAGE_ALT); use alt="" for decorative images. Plain HTML callers must provide their own alt text. Children and raw inner HTML are excluded from React Image props, and Astro bindings forward no slot. React refs expose the native HTMLImageElement.
The default image is a block with max-width: 100% and height: auto: it shrinks to fit while retaining its proportions. Full-width upscaling, cropping, aspect ratio, and radius are opt-in recipes, as above. Supply native width and height when known to reserve space; include descriptive text and meaningful link labels around image content.
Native attributes remain caller-owned: React uses srcSet, sizes, loading, decoding, and fetchPriority; Astro uses srcset, sizes, loading, decoding, and fetchpriority. Loading and decoding are not forced. src retains its native optionality for responsive-source or picture usage. Foundation does not fetch images, infer dimensions, generate sources, or optimize files. With an Astro imported image, pass its .src, .width, and .height explicitly. For a framework's optimized image component, apply a generated Image recipe class to that component's image output and let the framework own its asset pipeline.
The React example includes a responsive image-card gallery; the Astro example exposes it at /gallery/. Both use self-contained SVG illustrations, one/two/three-column layouts, badges, descriptions, and functioning links to the contact form. Their form actions use Row as well.
Generate and integrate
npx blupp build
npx blupp dev -- astro dev --ignore-lockThe default config is ./blupp.config.ts; output is ./src/generated/blupp. Override these with --config PATH and --out-dir PATH. Production scripts should generate first, then run checks/builds through the verifier:
{
"scripts": {
"generate": "blupp build",
"dev": "blupp dev -- astro dev --ignore-lock",
"check": "blupp build && blupp run -- astro check",
"build": "blupp build && blupp run -- astro check && blupp run -- astro build"
}
}Astro's --ignore-lock keeps it in the foreground when invoked by a coding agent and disables Astro's separate dev-server lock. Blupp retains its own exclusive output lock and owns startup, restarts, and shutdown. Stop this server with Ctrl+C in the supervising terminal; avoid Astro's --background and --force flags in this script. See Astro's CLI reference.
React substitutes its normal framework commands after --. Add generated output and its sibling *.blupp-lock, *.blupp-backup/, *.blupp-stage-*/ paths to your application's .gitignore. Regenerate them in clean CI with the same locked package version.
Generated artifacts include:
styles.css: complete tokens and recipes, usable directly by HTML.tailwind.css: optional@theme inlinebridge exposing namespaced utilities such astext-blupp-brand,font-blupp-heading, andgap-blupp-stack.manifest.json: version, generation ID, static dependency digests, and artifact digests.react.tsand/orastro/*.astro: native typed facades for selected adapters, plus variant metadata/types.
For Tailwind applications, import these once into the global stylesheet:
@import "tailwindcss";
@import "./generated/blupp/styles.css";
@import "./generated/blupp/tailwind.css";Your application owns the Tailwind compiler and optional Preflight reset. Blupp emits no reset and requires no source scanning for its component recipes. Its bridge preserves existing Tailwind token names and breakpoints. Normal utility declarations override component declarations through cascade layers; inline styles override normal stylesheet declarations. Unlayered rules and important declarations still obey native CSS precedence.
Without Tailwind, import only styles.css, or copy it into your public assets and link it from HTML. Do not link the optional Tailwind bridge directly in the browser.
Use the components
import { H1, Text, Stack, Button, Link } from './generated/blupp/react';
export function Intro() {
return <Stack>
<H1 variant="hero" className="text-center" id="intro">Welcome</H1>
<Text style={{ textWrap: 'balance' }}>Start here.</Text>
<Button variant="primary" onClick={() => alert('Saved')}>Save</Button>
<Link href="/guide/">Read the guide</Link>
</Stack>;
}React forwards native attributes, children, and correctly typed native ref props, and consumes variant. Both direct adapters and generated bindings support object refs and callback refs, including React 19 callback cleanup. It adds no hooks, browser APIs, or use client directive. Local className and object style are intentional exceptions to central configuration.
For DOM access in a client component, pass a ref to the native control. A custom date picker can use the same Input and Button refs for focus management; the application owns picker behavior and accessible associations.
import { useRef } from 'react';
import { Button, Input, Label } from './generated/blupp/react';
export function DateOfBirthField({ id }: { id: string }) {
const inputRef = useRef<HTMLInputElement>(null);
return <>
<Label htmlFor={id}>Date of birth</Label>
<Input ref={inputRef} id={id} name="dob" type="date" />
<Button onClick={() => inputRef.current?.focus()}>Focus date of birth</Button>
</>;
}Use a unique id per field instance. React 19 passes refs as props; no forwardRef wrapper is needed. In a server-component application, the component using hooks and event handlers belongs inside its client boundary.
---
import H1 from '@design/H1.astro';
---
<H1 variant="hero" class="text-center" id="intro">Welcome</H1>The Astro example uses the alias from the quickstart and assumes global CSS is loaded. Astro forwards native attributes, incoming classes/styles, scoped-style attributes, and the default slot through both facade and native wrapper. It adds no hydration or scripts. Use astro check for facade prop checking. Both adapters reject invalid dynamic variants at runtime; generated types reject nonexistent variants during framework checks. Direct package adapters such as @blupp/foundation/astro/H1.astro expose only the base recipe; use generated facades for configured variants.
<link rel="stylesheet" href="/blupp.css">
<h1 class="blupp-h1--hero">Welcome</h1>
<button class="blupp-button--primary" type="button">Save</button>
<a class="blupp-link" href="/guide/">Read the guide</a>
<p class="blupp-text">Start here.</p>Build lifecycle and recovery
The output directory must be empty or Blupp-owned; unrelated files and symlinks are refused. An exclusive directory lock covers build, run, and the entire dev child lifetime. Concurrent invocations fail immediately. Locks never expire automatically: a paused supervisor can still own live descendants. After a forced termination, inspect the sibling lock directory’s owner.json, verify that its process and descendants have stopped, and manually remove that specific lock directory before retrying. Normal shutdown releases it. Generation stages and validates complete artifacts, publishes a verified backup, writes an incomplete marker, replaces owned files, and clears the marker after verification. An interrupted commit is restored from its verified backup by the next build or dev; a first interrupted build can safely regenerate its owned artifacts.
run refuses incomplete, missing, tampered, or stale output before starting the child. Dev builds first, watches static config dependencies, stops the application before regeneration, and restarts after valid output. Invalid edits keep it stopped until corrected. Signals close the watcher and terminate the supervised child scope. The lock is released only after shutdown is confirmed. Calling framework commands directly bypasses those guarantees.
Shutdown allows the immediate child up to three seconds after TERM, then sends KILL and waits up to three seconds to confirm exit. POSIX confirmation also requires the process group to be absent; signal delivery alone is insufficient. Darwin may briefly retry a post-exit permission error, but persistent errors and confirmation timeouts fail with BLUPP_LOCK_RETAINED. The CLI leaves the lock and its original owner.json intact, reports the failure, and does not restart the application. Inspect the owner and its descendants, stop any survivors, and only then manually remove that specific lock directory. Locks are never automatically expired or removed after uncertain cleanup.
Configuration is trusted synchronous project code executed at build time. Use static imports; dynamic imports are rejected. Config evaluation should be pure. Changes to environment inputs require restarting dev; fonts are not downloaded automatically.
Programmatic compiler
import { compileConfig, buildProject, verifyOutput } from '@blupp/foundation/compiler';
const { files, manifest } = compileConfig({ components: { H1: { base: { fontSize: '2rem' } } } });
await buildProject({ configPath: './blupp.config.ts', outDir: './src/generated/blupp' });
await verifyOutput('./src/generated/blupp', './blupp.config.ts');compileConfig is a pure emission API; it neither checks installed framework peers nor writes files. buildProject loads trusted config, checks selected peers, acquires the lock, and commits output. Use CLI run when verification must remain protected throughout a child process lifetime.
