@enclosurejs/stamp
v1.1.0
Published
Compile-time build metadata for Enclosure apps
Readme
@enclosurejs/stamp — Compile-time build metadata
[!IMPORTANT] This package collects git, version, and environment information at build time and injects it as a frozen object available at runtime. One import — all build context. Zero runtime dependencies. Works with any backend.
The Problem
Every application needs build metadata — git commit, branch, version, build mode — for about screens, error reports, telemetry, and diagnostics. Collecting this by hand in every repo means duplicated scripts, inconsistent formats, and fragile shell commands scattered across configs.
@enclosurejs/stamp solves this with a single collectStamp() call that reads package.json, queries git, and snapshots the environment into a typed, frozen object. A Vite plugin (stampPlugin) wraps it as virtual:stamp for zero-config frontend access. Non-Vite bundlers call collectStamp() directly in their plugin/script layer.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Build Time │
│ │
│ ┌──────────────┐ ┌───────────┐ │
│ │ package.json │ │ git repo │ │
│ └──────┬───────┘ └─────┬─────┘ │
│ │ │ │
│ └────────┬────────┘ │
│ ▼ │
│ collectStamp(options) │
│ │ │
│ ┌────────┴─────────┐ │
│ ▼ ▼ │
│ stampPlugin() Direct use in │
│ (Vite only) Node/CI scripts │
│ │ │
│ virtual:stamp │
└─────────┼────────────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ Runtime │
│ │
│ import stamp from 'virtual:stamp' │
│ stamp.version // "1.2.3" │
│ stamp.branch // "main" │
│ stamp.revision // "a1b2c3d4e5f6..." │
│ stamp.dirty // false │
│ stamp.tag // "v1.2.3-5-gabc1234" │
│ stamp.remark // "Pilot v1.2.3 [main@a1b2c3d] │
│ production" │
│ stamp.extra.target // "electron" (user-supplied) │
└──────────────────────────────────────────────────────────────┘Dependency rule: @enclosurejs/stamp imports nothing from @enclosurejs/core or any platform package — it is a standalone build-time utility that works everywhere Node.js runs.
Quick Start
1. Install
pnpm add @enclosurejs/stamp2a. Vite setup (Tauri, Electron, Web, Capacitor)
// vite.config.ts
import { stampPlugin } from '@enclosurejs/stamp/vite';
export default defineConfig({
plugins: [stampPlugin({ app: 'pilot', product: 'Enclosure Pilot' })],
});// src/env.d.ts (or any .d.ts included in tsconfig)
/// <reference types="@enclosurejs/stamp/virtual-stamp" />// app code
import stamp from 'virtual:stamp';
console.log(stamp.remark);
// "Enclosure Pilot v1.0.0 [main@a1b2c3d] production"
// or with uncommitted changes:
// "Enclosure Pilot v1.0.0 [main@a1b2c3d] development (dirty)"
if (stamp.dev) {
console.log(`Branch: ${stamp.branch}, Commit: ${stamp.shortRevision}`);
if (stamp.dirty) console.warn('Build from dirty working tree!');
}2b. Direct use (Node scripts, CI, non-Vite bundlers)
import { collectStamp } from '@enclosurejs/stamp';
const stamp = collectStamp({ app: 'pilot', product: 'Pilot' });
// Use stamp.version, stamp.revision, etc. in your build pipelineFor webpack or esbuild, call collectStamp() in your plugin/define config and inject the result via DefinePlugin or define.
How It Works
Data Collection
collectStamp() gathers metadata from three sources:
- package.json —
name,version,description(read fromoptions.packageRoot→options.root→cwd). Fields are validated as strings; non-string values are ignored. - Git —
branch,revision(full SHA),shortRevision(7 chars),commitDate(ISO 8601),dirty(uncommitted changes),tag(git describe). Collected viagit log,git status --porcelain, andgit describe --tags --alwayswith a fallbackrev-parsefor detached HEAD. - Mode — resolved as
options.mode→NODE_ENV→"development".DateprovidesbuiltAtandyear.
All git commands use execFileSync (no shell interpolation) and are wrapped in try/catch — fields become "unknown" when git is unavailable (CI containers, shallow clones, non-repo directories). No crashes, no warnings.
Vite Integration
stampPlugin() hooks into Vite's configResolved phase:
- Passes
config.modeasoptions.modeandconfig.rootasoptions.root(user-supplied options take priority) - Calls
collectStamp()— noprocess.envmutation - Serializes the result as a JSON object
- Serves it via
resolveId/loadas the virtual modulevirtual:stamp
The plugin runs at enforce: 'pre' so stamp data is available to all downstream plugins.
Extra Fields
User-supplied extra fields are namespaced under stamp.extra:
const stamp = collectStamp({
app: 'pilot',
extra: { target: 'electron', channel: 'beta' },
});
stamp.extra.target; // "electron"
stamp.extra.channel; // "beta"
stamp.version; // always the real version — core fields are separateFreeze Guarantee
The returned Stamp is deeply frozen via Object.freeze — both the stamp object and stamp.extra are immutable at runtime, not just at the type level.
API
Core (@enclosurejs/stamp)
| Export | Kind | Purpose |
| -------------- | -------- | --------------------------------------------------------------- |
| collectStamp | function | Snapshot git, package.json, and environment into a frozen Stamp |
| Stamp | type | Frozen, deeply-immutable build metadata |
| StampOptions | type | Configuration for collectStamp |
Vite plugin (@enclosurejs/stamp/vite)
| Export | Kind | Purpose |
| ------------- | -------- | --------------------------------------------------- |
| stampPlugin | function | Vite plugin exposing virtual:stamp virtual module |
StampOptions
| Field | Type | Required | Description |
| ------------- | ------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| app | string | Yes | Short application name (e.g. "pilot") |
| product | string | No | Human-readable name. Falls back to package.json name, then app |
| root | string | No | Directory for git lookup. Falls back to cwd or Vite root |
| packageRoot | string | No | Directory for package.json lookup. Falls back to root. Use when version lives elsewhere |
| mode | string | No | Build mode. Falls back to NODE_ENV, then "development" |
| extra | Record<string, unknown> | No | User fields available at stamp.extra |
Stamp
| Field | Type | Source |
| --------------- | ------------------------- | ---------------------------------------------------------------------- |
| app | string | Options app |
| product | string | Options product → package.json name → app |
| name | string | package.json name → app |
| version | string | package.json version → "0.0.0" |
| description | string | package.json description → "" |
| mode | string | Options mode → NODE_ENV → "development" |
| dev | boolean | mode !== "production" |
| prod | boolean | mode === "production" |
| branch | string | Git branch or "unknown" |
| revision | string | Git SHA (40 hex chars) or "unknown" |
| shortRevision | string | First 7 chars of revision or "unknown" |
| commitDate | string | ISO 8601 commit date or "unknown" |
| dirty | boolean | true when working tree has uncommitted changes |
| tag | string | git describe --tags --always (e.g. "v1.2.3-5-gabc") or "unknown" |
| builtAt | string | ISO 8601 build timestamp |
| year | number | Build year (for copyright lines) |
| remark | string | "Product vX.Y.Z [branch@hash] mode" + " (dirty)" if dirty |
| extra | Record<string, unknown> | User-supplied fields from options |
Configuration
The only required field is app. Everything else is auto-detected:
// Minimal
collectStamp({ app: 'myapp' });
// Full control
collectStamp({
app: 'studio',
product: 'Creative Studio',
root: '/path/to/project',
mode: 'production',
extra: { target: 'electron', locale: 'en' },
});
// Monorepo — git from root, version from workspace package
collectStamp({
app: 'studio',
root: '/path/to/monorepo',
packageRoot: '/path/to/monorepo/packages/studio',
});
// Vite — minimal
stampPlugin({ app: 'myapp' });
// Vite — full control
stampPlugin({
app: 'studio',
product: 'Creative Studio',
root: '/path/to/project',
extra: { target: 'tauri', channel: 'beta' },
});Backend Compatibility Matrix
@enclosurejs/stamp is a compile-time utility — it runs in Node.js during your build, not at runtime. It has no dependency on @enclosurejs/core, BackendAdapter, or any platform package:
| Backend | How to use | Plugin needed |
| -------------------- | ----------------------------------- | ------------- |
| Tauri (Vite) | stampPlugin() in vite.config.ts | Built-in |
| Electron (Vite) | stampPlugin() in vite.config.ts | Built-in |
| Capacitor (Vite) | stampPlugin() in vite.config.ts | Built-in |
| Web (Vite) | stampPlugin() in vite.config.ts | Built-in |
| Electron (webpack) | collectStamp() + DefinePlugin | Manual |
| Electron (esbuild) | collectStamp() + define option | Manual |
| Node.js (no bundler) | collectStamp() directly | None |
| CI scripts | collectStamp() directly | None |
Types Exported
Types other packages depend on:
| Type | Used by |
| -------------- | ------------------------------------------------- |
| Stamp | Any code that reads build metadata |
| StampOptions | Build configs, CI scripts, bundler plugin configs |
Entrypoint separation prevents Vite type pollution:
| Import path | Contains | Requires Vite types |
| ----------------------- | -------------- | ------------------- |
| @enclosurejs/stamp | collectStamp | No |
| @enclosurejs/stamp/vite | stampPlugin | Yes (peer dep) |
Safety
Graceful Degradation
- All git commands (
log,rev-parse,status --porcelain,describe --tags) useexecFileSync(no shell) wrapped intry/catch— no crashes outside a repo or in shallow clones - Missing or corrupted
package.json→ sensible defaults ("0.0.0","",appas name) - Non-string fields in
package.json(name: 42,version: [1,0]) are ignored — treated as missing - Missing
NODE_ENVand nomodeoption → defaults to"development"
Immutability
- The returned
StampisObject.freeze'd — mutation throws in strict mode, silently fails otherwise stamp.extrais also frozen — deeply immutable at runtime, not just at the type level
Environment Safety
stampPlugindoes not mutateprocess.env— mode is passed as a parameter tocollectStampextrafields are namespaced understamp.extra— cannot collide with core fields
Dependency Safety
- Zero runtime dependencies — only Node.js builtins (
child_process,fs,path) at build time - Vite is an optional peer dependency — only needed by
@enclosurejs/stamp/vite - Main entrypoint (
@enclosurejs/stamp) does not import Vite — safe for pure Node.js consumers
Benchmarks
Not applicable. @enclosurejs/stamp is a build-time utility — it runs once during compilation via collectStamp() or stampPlugin(). There is no runtime hot path to benchmark. Execution time is dominated by a single execFileSync('git', ...) call (typically < 50ms) and a single readFileSync for package.json.
Bundle Size
| Output | File | Size |
| ------------ | -------------- | -------- |
| Runtime (JS) | collect.js | 3.08 KB |
| | vite.js | 3.69 KB |
| | index.js | 3.07 KB |
| Types (DTS) | collect.d.ts | 2.99 KB |
| | vite.d.ts | 0.63 KB |
| | index.d.ts | 0.06 KB |
| Total JS | | 9.84 KB |
| Total | | 13.52 KB |
index.js is a barrel that re-exports from collect.js — consumers import either @enclosurejs/stamp or @enclosurejs/stamp/collect. vite.js is only loaded by Vite builds. Zero runtime dependencies — only Node.js builtins at build time.
Quality
| Metric | Value |
| --------------------- | ------------------------------------------------------------------ |
| Unit tests | 51 (all pass) |
| Test files | 3 (collect.test.ts, collect-git.test.ts, vite.test.ts) |
| Source files | 4 (index.ts, collect.ts, vite.ts, virtual-stamp.d.ts) |
| External dependencies | 0 (devDependencies only: tsup, vite) |
| Peer dependencies | vite >=5.0.0 (optional) |
| Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |
Quality Layers
Layer 1: STATIC ANALYSIS (every commit)
tsc --noEmit strict mode, zero errors
eslint ESLint 9 flat config, zero warnings
prettier --check formatting
Layer 2: UNIT TESTS (every commit)
51 tests collectStamp + stampPlugin, edge cases, safety
v8 coverage statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%
Layer 3: BENCHMARKS
N/A build-time utility, no runtime path to benchmark
Layer 4: PACKAGE HEALTH
0 external deps pure TypeScript + Node.js builtins
tsup build ESM + DTS output, separate entrypointsFile Structure
packages/stamp/
├── src/
│ ├── index.ts Barrel: collectStamp, Stamp, StampOptions
│ ├── collect.ts Core collector (git + package.json + env)
│ ├── vite.ts Vite plugin (virtual:stamp)
│ ├── virtual-stamp.d.ts Type declaration for virtual module
│ └── __tests__/
│ ├── collect.test.ts 36 tests — fields, fallbacks, modes, extra, freeze, validation, dirty, tag, packageRoot
│ ├── collect-git.test.ts 2 tests — detached HEAD, sparse git log (mocked execFileSync)
│ └── vite.test.ts 13 tests — plugin identity, resolve, load, mode, root, env safety
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── README.mdLicense
MIT
