This document explains the comprehensive build constraints, transformations, and code management strategies in Glimmer VM. It serves as a reference for understanding how code is transformed from development to production and as a starting point for further analysis of the build system.
Glimmer VM uses several categories of code that have different constraints on where they can appear:
- Production Code - Ships to end users in production builds
- Development Code - Available in development builds for end users
- Local Development Code - Only for Glimmer VM developers, never ships
- Build-Time Code - Used during compilation but not at runtime
What it is: A de facto standard created by Vite for build-time environment variables.
Usage in Glimmer VM:
import.meta.env.DEV-truein development builds,falsein productionimport.meta.env.PROD-truein production builds,falsein developmentimport.meta.env.VM_LOCAL_DEV-falsein published builds,truein Vite dev server
Constraint: These references are replaced at build time with actual values. The string import.meta.env never appears in published builds.
What it is: A build-time flag for code that should only run during local Glimmer VM development.
Purpose: Enables expensive debugging features when working on the VM itself. These features never reach published packages (not even development builds).
Example Usage:
if (VM_LOCAL) {
// Expensive validation that helps VM developers
validateOpcodeSequence(opcodes);
}Constraint: Code blocks guarded by VM_LOCAL are completely removed from all published builds. The condition and its contents are stripped out.
What they are: Runtime type checking and validation functions from @glimmer/debug:
check(value, checker)- Validates a value against a type checkerexpect(value, message)- Asserts a condition is truthylocalAssert(condition, message)- Development-only assertionunwrap(value)- Unwraps optional values, throwing if null/undefined
Purpose: Catch bugs during Glimmer VM development by validating assumptions about types and state.
Example Usage:
import { check } from '@glimmer/debug';
import { CheckReference } from './-debug-strip';
let definition = check(stack.pop(), CheckReference);
let capturedArgs = check(stack.pop(), CheckCapturedArguments);Constraint: These function calls are stripped from ALL published builds (both development and production) using a Babel plugin during the build process.
What they are: Functions that create runtime type validators:
CheckInterface- Validates object shapeCheckOr- Union type validationCheckFunction- Function type validationCheckObject- Object/WeakMap key validation
Purpose: Define the type constraints used by check() calls.
Example Usage:
export const CheckReference: Checker<Reference> = CheckInterface({
[REFERENCE]: CheckFunction,
});
export const CheckArguments = CheckOr(CheckObject, CheckFunction);Constraint: These should never appear in published builds as they're only used by the stripped check() calls.
Three private packages contain development-only utilities:
- @glimmer/debug - Type checkers, validation utilities, debugging tools
- @glimmer/constants - VM opcodes, DOM constants (inlined during build)
- @glimmer/debug-util - Debug assertions, platform-specific logging
Constraint: These packages are never published to npm. Import statements for them should never appear in published builds - their contents are either inlined or stripped during compilation.
The build process uses a Babel plugin (@glimmer/local-debug-babel-plugin) that:
- Identifies imports from
@glimmer/debug - Tracks which debug functions are imported
- Strips or transforms the function calls:
check(value, checker)→valueexpect(...)→ removed entirelyCheckInterface(...)→() => truerecordStackSize()→ removed entirely
The Rollup replace plugin performs these build-time replacements:
Production builds:
import.meta.env.MODE→"production"import.meta.env.DEV→falseimport.meta.env.PROD→trueimport.meta.env.VM_LOCAL_DEV→false
Development builds:
import.meta.env.MODE→"development"import.meta.env.DEV→DEBUG(withimport { DEBUG } from '@glimmer/env'injected)import.meta.env.PROD→!DEBUGimport.meta.env.VM_LOCAL_DEV→false(becomestrueonly in Vite dev server)
The build system has specific rules for what gets inlined vs treated as external:
Always Inlined:
@glimmer/local-debug-flags@glimmer/constants@glimmer/debug@glimmer/debug-util- Relative imports (
.,/,#) - TypeScript helper library (
tslib)
Always External:
simple-html-tokenizerbabel-plugin-debug-macros- Other
@glimmer/*packages (to avoid duplication) @simple-dom/*packages@babel/*packages- Node.js built-ins (
node:*)
Every package produces multiple build artifacts:
-
Development Build (
dist/dev/)- Readable, formatted code
- Preserves comments
- No variable name mangling
- Includes source maps
-
Production Build (
dist/prod/)- Minified with Terser (3 passes)
- Aggressive optimizations
- Preserves
debuggerstatements (for{{debugger}}helper) - Includes source maps
-
Type Definitions (
dist/{dev,prod}/*.d.ts)- Generated from TypeScript source
- Rolled up into single files per entry point
-
CommonJS Build (optional,
*.cjs)- Only generated if package.json includes CommonJS exports
- Follows same dev/prod split
Glimmer VM uses a multi-tiered TypeScript configuration system:
tsconfig.base.json- Shared base configurationtsconfig.json- Development configuration (looser for better DX)tsconfig.dist.json- Distribution configuration (stricter for published code)
Packages can declare their strictness level in package.json:
{
"repo-meta": {
"strictness": "strict" | "loose"
}
}This affects which TypeScript compiler options are applied during type checking.
- Target: ES2022
- Module Resolution: "bundler" mode
- Isolated Modules: Required for build performance
- Exact Optional Properties: Enforced in distribution builds
- No Unchecked Indexed Access: Enforced in distribution builds
The build system uses Turbo for orchestration with these key relationships:
prepackmust complete before any builds- Type checking runs in parallel with builds
- Cache keys include TypeScript configs, source files, and lock files
pnpm repo:prepack- Build all packages via Turbo (recommended)pnpm repo:lint:types- Type check all packagespnpm clean- Clean build artifacts
Published Package Structure:
- Only
dist/directory is included in npm packages - Conditional exports for dev/prod builds
publintvalidates package structure before publishing
Export Configuration:
{
"exports": {
".": {
"development": "./dist/dev/index.js",
"default": "./dist/prod/index.js"
}
}
}Note: Private packages (@glimmer/debug, @glimmer/constants, @glimmer/debug-util, and all @glimmer-workspace/*) are never published to npm.
- Automated size tracking via GitHub Actions
- Compares dev/prod sizes against main branch
- Reports size changes in PR comments
- Uses
dustutility for accurate measurements
- Browser Tests: Puppeteer with specific Chrome flags
- Smoke Tests: 300s timeout (vs 30s for regular tests)
- BrowserStack: Cross-browser testing for releases
- Floating Dependencies: Special CI job tests against latest deps
- Type checking (
tsc) - Linting (
eslint) - Unit tests (QUnit/Vitest)
- Smoke tests
- Bundle size analysis
- Package structure validation (
publint)
- Transforms
import.meta.env.VM_LOCAL_DEV→truefor local development - Pre-bundles test dependencies for performance
- Custom extension resolution order
- Environment-aware rules (console vs non-console packages)
- Strictness based on package metadata
- Test-specific rules for QUnit
- Custom rules for Glimmer-specific patterns
Tools in bin/fixes/:
apply-eslint-suggestions.js- Apply ESLint auto-fixesapply-ts-codefixes.js- Apply TypeScript code fixesapply-suggestions.js- Apply both types of fixes
- Use debug assertions liberally - They help catch bugs and document assumptions
- Don't wrap debug code in conditions - The build process handles removal
- Import from the right place - Use
@glimmer/debugimports in VM code - Trust the build process - Write clear development code; the build makes it production-ready
- Respect package boundaries - Don't import from private packages in public ones
- Follow strictness levels - Adhere to the TypeScript strictness of your package
The Glimmer VM build system enables developers to write defensive, well-instrumented code during development while shipping minimal, performant code to production. Through multiple layers of transformations, validations, and constraints, it ensures debug code never reaches users while maintaining a fast and helpful development experience.