diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d00a92007f..90011e2353 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: node-version: 22.13.0 repo-token: ${{ secrets.GITHUB_TOKEN }} - run: pnpm turbo prepack - - run: node ./bin/build-verify.mjs lint: name: Linting diff --git a/bin/build-verify.mjs b/bin/build-verify.mjs deleted file mode 100644 index 42b97d8209..0000000000 --- a/bin/build-verify.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { globby } from 'globby'; - -const currentDir = fileURLToPath(import.meta.url); -const FORBIDDEN = [ - /** - * import.meta.env is not a platform standard - */ - 'import.meta.env', - /** - * These variables are wrapped around code for this repo only - */ - 'VM_LOCAL', - /** - * These are for local VM debugging and development, and are not meant to make it to real code - */ - /[^.]check\(/u, - 'CheckInterface', - 'CheckOr', - 'CheckFunction', - 'CheckObject', - - '@glimmer/debug', - '@glimmer/constants', - '@glimmer/debug-util', -]; - -const IGNORED_DIRS = [`@glimmer/debug`, `@glimmer/constants`, `@glimmer/debug-util`]; - -let files = await globby(resolve(currentDir, '../../packages/**/dist/**/index.js'), { - ignore: ['node_modules', '**/node_modules'], -}); - -files = files.filter((file) => !IGNORED_DIRS.some((dir) => file.includes(dir))); - -let errors = []; - -console.log(`Found ${files.length} files to check...`); - -for (let filePath of files) { - console.log(`Checking ${filePath}...`); - let file = await readFile(filePath); - let content = file.toString(); - - for (let searchFor of FORBIDDEN) { - const match = typeof searchFor === 'string' ? content === searchFor : searchFor.test(content); - - if (match) { - errors.push({ filePath, found: searchFor }); - } - } -} - -if (errors.length > 0) { - console.error(errors); - throw new Error(`The forbidden texts were encountered in the above files`); -} - -console.info('No forbidden texts!'); diff --git a/guides/development/build-constraints.md b/guides/development/build-constraints.md new file mode 100644 index 0000000000..ef5ab5e075 --- /dev/null +++ b/guides/development/build-constraints.md @@ -0,0 +1,288 @@ +# Build Constraints and Transformations + +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. + +## Overview + +Glimmer VM uses several categories of code that have different constraints on where they can appear: + +1. **Production Code** - Ships to end users in production builds +2. **Development Code** - Available in development builds for end users +3. **Local Development Code** - Only for Glimmer VM developers, never ships +4. **Build-Time Code** - Used during compilation but not at runtime + +## Code Categories and Constraints + +### 1. import.meta.env + +**What it is**: A de facto standard created by Vite for build-time environment variables. + +**Usage in Glimmer VM**: +- `import.meta.env.DEV` - `true` in development builds, `false` in production +- `import.meta.env.PROD` - `true` in production builds, `false` in development +- `import.meta.env.VM_LOCAL_DEV` - `false` in published builds, `true` in Vite dev server + +**Constraint**: These references are replaced at build time with actual values. The string `import.meta.env` never appears in published builds. + +### 2. VM_LOCAL Flag + +**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**: +```typescript +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. + +### 3. Debug Assertion Functions + +**What they are**: Runtime type checking and validation functions from `@glimmer/debug`: + +- `check(value, checker)` - Validates a value against a type checker +- `expect(value, message)` - Asserts a condition is truthy +- `localAssert(condition, message)` - Development-only assertion +- `unwrap(value)` - Unwraps optional values, throwing if null/undefined + +**Purpose**: Catch bugs during Glimmer VM development by validating assumptions about types and state. + +**Example Usage**: +```typescript +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. + +### 4. Type Checker Functions + +**What they are**: Functions that create runtime type validators: + +- `CheckInterface` - Validates object shape +- `CheckOr` - Union type validation +- `CheckFunction` - Function type validation +- `CheckObject` - Object/WeakMap key validation + +**Purpose**: Define the type constraints used by `check()` calls. + +**Example Usage**: +```typescript +export const CheckReference: Checker = 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. + +### 5. Debug-Only Packages + +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. + +## Build Process and Transformations + +### Debug Code Stripping + +The build process uses a Babel plugin (`@glimmer/local-debug-babel-plugin`) that: + +1. Identifies imports from `@glimmer/debug` +2. Tracks which debug functions are imported +3. Strips or transforms the function calls: + - `check(value, checker)` → `value` + - `expect(...)` → removed entirely + - `CheckInterface(...)` → `() => true` + - `recordStackSize()` → removed entirely + +### Environment Variable Replacements + +The Rollup replace plugin performs these build-time replacements: + +**Production builds:** +- `import.meta.env.MODE` → `"production"` +- `import.meta.env.DEV` → `false` +- `import.meta.env.PROD` → `true` +- `import.meta.env.VM_LOCAL_DEV` → `false` + +**Development builds:** +- `import.meta.env.MODE` → `"development"` +- `import.meta.env.DEV` → `DEBUG` (with `import { DEBUG } from '@glimmer/env'` injected) +- `import.meta.env.PROD` → `!DEBUG` +- `import.meta.env.VM_LOCAL_DEV` → `false` (becomes `true` only in Vite dev server) + +### Module Resolution and Bundling + +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:** +- `@handlebars/parser` +- `simple-html-tokenizer` +- `babel-plugin-debug-macros` +- Other `@glimmer/*` packages (to avoid duplication) +- `@simple-dom/*` packages +- `@babel/*` packages +- Node.js built-ins (`node:*`) + +### Build Output Structure + +Every package produces multiple build artifacts: + +1. **Development Build** (`dist/dev/`) + - Readable, formatted code + - Preserves comments + - No variable name mangling + - Includes source maps + +2. **Production Build** (`dist/prod/`) + - Minified with Terser (3 passes) + - Aggressive optimizations + - Preserves `debugger` statements (for `{{debugger}}` helper) + - Includes source maps + +3. **Type Definitions** (`dist/{dev,prod}/*.d.ts`) + - Generated from TypeScript source + - Rolled up into single files per entry point + +4. **CommonJS Build** (optional, `*.cjs`) + - Only generated if package.json includes CommonJS exports + - Follows same dev/prod split + +## TypeScript Configuration and Strictness + +Glimmer VM uses a multi-tiered TypeScript configuration system: + +### Configuration Files +- `tsconfig.base.json` - Shared base configuration +- `tsconfig.json` - Development configuration (looser for better DX) +- `tsconfig.dist.json` - Distribution configuration (stricter for published code) + +### Per-Package Strictness Levels + +Packages can declare their strictness level in `package.json`: +```json +{ + "repo-meta": { + "strictness": "strict" | "loose" + } +} +``` + +This affects which TypeScript compiler options are applied during type checking. + +### Key Compiler Constraints +- **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 + +## Build Orchestration + +### Turbo Pipeline + +The build system uses Turbo for orchestration with these key relationships: +- `prepack` must complete before any builds +- Type checking runs in parallel with builds +- Cache keys include TypeScript configs, source files, and lock files + +### Build Commands +- `pnpm build:control` - Build all packages using Rollup +- `pnpm repo:prepack` - Prepare packages for publishing +- `pnpm repo:lint:types` - Type check all packages + +### Package Publishing + +**Published Package Structure**: +- Only `dist/` directory is included in npm packages +- Conditional exports for dev/prod builds +- `publint` validates package structure before publishing + +**Export Configuration**: +```json +{ + "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. + +## Continuous Integration Constraints + +### Bundle Size Monitoring +- Automated size tracking via GitHub Actions +- Compares dev/prod sizes against main branch +- Reports size changes in PR comments +- Uses `dust` utility for accurate measurements + +### Test Environment Constraints +- **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 + +### Validation Steps +1. Type checking (`tsc`) +2. Linting (`eslint`) +3. Unit tests (QUnit/Vitest) +4. Smoke tests +5. Bundle size analysis +6. Package structure validation (`publint`) + +## Development Environment + +### Vite Development Server +- Transforms `import.meta.env.VM_LOCAL_DEV` → `true` for local development +- Pre-bundles test dependencies for performance +- Custom extension resolution order + +### ESLint Configuration +- Environment-aware rules (console vs non-console packages) +- Strictness based on package metadata +- Test-specific rules for QUnit +- Custom rules for Glimmer-specific patterns + +### Automated Code Fixes +Tools in `bin/fixes/`: +- `apply-eslint-suggestions.js` - Apply ESLint auto-fixes +- `apply-ts-codefixes.js` - Apply TypeScript code fixes +- `apply-suggestions.js` - Apply both types of fixes + +## Guidelines for Developers + +1. **Use debug assertions liberally** - They help catch bugs and document assumptions +2. **Don't wrap debug code in conditions** - The build process handles removal +3. **Import from the right place** - Use `@glimmer/debug` imports in VM code +4. **Trust the build process** - Write clear development code; the build makes it production-ready +5. **Respect package boundaries** - Don't import from private packages in public ones +6. **Follow strictness levels** - Adhere to the TypeScript strictness of your package + +## Summary + +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. \ No newline at end of file diff --git a/guides/development/debug-assertions.md b/guides/development/debug-assertions.md new file mode 100644 index 0000000000..275f1eb047 --- /dev/null +++ b/guides/development/debug-assertions.md @@ -0,0 +1,62 @@ +# Debug Assertions in Glimmer VM + +## Overview + +Glimmer VM uses debug assertion functions to validate assumptions and catch errors during development. These functions are essential for maintaining code quality but must never appear in published packages. + +## Debug Functions + +The following functions from `@glimmer/debug` are for local development only: + +- `check(value, checker)` - Validates a value against a type checker (e.g., `CheckReference`, `CheckString`) +- `expect(value, message)` - Throws with message if value is falsy +- `localAssert(condition, message)` - Throws with message if condition is false +- `unwrap(value)` - Returns value if truthy, throws if null/undefined +- `recordStackSize()` - Records stack size for debugging (completely removed in builds) + +## Usage Example + +```typescript +import { check } from '@glimmer/debug'; +import { CheckReference, CheckCapturedArguments } from './-debug-strip'; + +// In your VM opcode implementation: +let definition = check(stack.pop(), CheckReference); +let capturedArgs = check(stack.pop(), CheckCapturedArguments); + +// Type checkers validate specific shapes: +// CheckReference - validates the value is a Glimmer Reference +// CheckCapturedArguments - validates captured arguments structure +``` + +## Build Process + +These debug calls are automatically stripped from all builds using a Babel plugin. The transformation works as follows: + +### Source Code (what you write): +```typescript +let value = check(stack.pop(), CheckReference); +``` + +### Published Build (what gets shipped): +```typescript +let value = stack.pop(); +``` + +## Important Notes + +1. **Write debug assertions freely** - They help catch bugs during development +2. **Don't wrap in conditions** - The build process handles removal automatically +3. **Never in published code** - Debug stripping ensures this +4. **For VM developers only** - These are not part of the public API + +## How It Works + +The build system automatically removes all debug assertions: + +1. The Babel plugin (`@glimmer/local-debug-babel-plugin`) runs during build +2. It identifies all imports from `@glimmer/debug` +3. It strips or transforms the function calls appropriately +4. Both development and production builds have debug code removed + +This ensures that while developers can use these assertions freely during development, end users never pay the cost of these checks in any published builds. \ No newline at end of file diff --git a/packages/@glimmer-workspace/build/lib/config.js b/packages/@glimmer-workspace/build/lib/config.js index 5523b5c6b1..add0beecb1 100644 --- a/packages/@glimmer-workspace/build/lib/config.js +++ b/packages/@glimmer-workspace/build/lib/config.js @@ -17,6 +17,26 @@ const { ModuleKind, ModuleResolutionKind, ScriptTarget } = ts; const { default: nodeResolve } = await import('@rollup/plugin-node-resolve'); const { default: postcss } = await import('rollup-plugin-postcss'); const { default: nodePolyfills } = await import('rollup-plugin-polyfill-node'); +const { babel } = await import('@rollup/plugin-babel'); +const stripDebugPlugin = await import('@glimmer/local-debug-babel-plugin'); + +/** + * Create a Rollup plugin that strips debug calls from builds + * @returns {RollupPlugin} + */ +function stripGlimmerDebug() { + return babel({ + babelHelpers: 'bundled', + plugins: [stripDebugPlugin.default], + // Only process JavaScript files (TypeScript already transpiled by SWC) + include: ['packages/@glimmer/**/*.js'], + // Skip .d.ts files + exclude: ['**/*.d.ts'], + // Don't use any config files + configFile: false, + babelrc: false, + }); +} /** * @import { PartialCompilerOptions } from "@rollup/plugin-typescript"; @@ -328,6 +348,8 @@ export class Package { }, }, }), + // Strip debug calls in all builds - they're only for local development + stripGlimmerDebug(), ], }) ); @@ -399,6 +421,8 @@ export class Package { ]), postcss(), typescript(this.#package, env), + // Strip debug calls in all builds - they're only for local development + stripGlimmerDebug(), ], }) ), diff --git a/packages/@glimmer-workspace/build/package.json b/packages/@glimmer-workspace/build/package.json index 12d5f4630e..a729805afd 100644 --- a/packages/@glimmer-workspace/build/package.json +++ b/packages/@glimmer-workspace/build/package.json @@ -17,6 +17,7 @@ "scripts": {}, "dependencies": { "@glimmer/local-debug-babel-plugin": "workspace:*", + "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f395f7eb0..fca0af542e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -412,6 +412,9 @@ importers: '@glimmer/local-debug-babel-plugin': specifier: workspace:* version: link:../../@glimmer/local-debug-babel-plugin + '@rollup/plugin-babel': + specifier: ^6.0.4 + version: 6.0.4(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@4.34.8) '@rollup/plugin-commonjs': specifier: ^28.0.2 version: 28.0.2(rollup@4.34.8) @@ -3160,6 +3163,19 @@ packages: engines: {node: '>=18'} hasBin: true + '@rollup/plugin-babel@6.0.4': + resolution: {integrity: sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + rollup: + optional: true + '@rollup/plugin-commonjs@28.0.2': resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -12085,6 +12101,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@rollup/plugin-babel@6.0.4(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@4.34.8)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@rollup/pluginutils': 5.1.4(rollup@4.34.8) + optionalDependencies: + '@types/babel__core': 7.20.5 + rollup: 4.34.8 + transitivePeerDependencies: + - supports-color + '@rollup/plugin-commonjs@28.0.2(rollup@4.34.8)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.34.8) diff --git a/repo-metadata/lib/update.ts b/repo-metadata/lib/update.ts index 2b43994a21..89526d2b96 100644 --- a/repo-metadata/lib/update.ts +++ b/repo-metadata/lib/update.ts @@ -48,7 +48,6 @@ try { const packages: WorkspacePackage[] = pnpmPackages.map((pkg) => { // Read the actual package.json to get all fields including repo-meta const packageJsonPath = join(pkg.path, 'package.json'); - if (!existsSync(packageJsonPath)) { console.error(chalk.red(`Missing package.json at ${packageJsonPath}`)); throw new Error(`Missing package.json at ${packageJsonPath}`); diff --git a/repo-metadata/metadata.json b/repo-metadata/metadata.json index 5c6960fb65..6b777ef9cc 100644 --- a/repo-metadata/metadata.json +++ b/repo-metadata/metadata.json @@ -27,7 +27,6 @@ "env": ["node", "console"] }, "entryPoints": { - "./build-verify.mjs": [[["default"], "./build-verify.mjs"]], "./clean.mjs": [[["default"], "./clean.mjs"]], "./opcodes/opcodes.schema.json": [[["default"], "./opcodes/opcodes.schema.json"]], "./opcodes.json": [[["default"], "./opcodes.json"]], diff --git a/rollup.config.mjs b/rollup.config.mjs deleted file mode 100644 index e69de29bb2..0000000000