constraints.md
3,182 tokens · o200k_base · 13,214 bytes
Source excerpt starting at line 1.# Constraints & Gotchas Critical constraints and common pitfalls when developing in the Astro monorepo. ## Node.js API Restrictions **MOST IMPORTANT CONSTRAINT** ### The Rule (Conservative Approach) **Location**: `/CONTRIBUTING.md:84-98` Code in `packages/astro` has restricted Node.js API usage because Astro runs in multiple runtimes (Node.js, Cloudflare Workers, Deno, edge runtimes). **Safe decision rule:** ```1. In Vite plugin implementations → Node.js APIs allowed2. In /runtime/ folders → Node.js APIs FORBIDDEN3. Everywhere else → Avoid Node.js APIs, use @astrojs/internal-helpers``` **Reality**: The codebase is messy. Not all code follows clear boundaries. The safest approach is to avoid Node.js APIs unless you're in a Vite plugin. ### Where Node.js APIs Are ABSOLUTELY FORBIDDEN **Pattern** (enforced by Biome linter): 1. Any file in a `runtime/` folder: `**/packages/astro/src/**/runtime/**/*.ts`2. Any file with `runtime` in its name: `**/packages/astro/src/**/*runtime*.ts` **Examples of forbidden locations**: - `packages/astro/src/runtime/server/` → FORBIDDEN (runtime folder)- `packages/astro/src/runtime/client/` → FORBIDDEN (runtime folder)- `packages/astro/src/vite-plugin-astro/runtime.ts` → FORBIDDEN (runtime in name)- `packages/astro/src/core/render/runtime-utils.ts` → FORBIDDEN (runtime in name) **Forbidden APIs**: ```typescript// FORBIDDEN in runtime/ folders or *runtime*.ts filesimport fs from 'node:fs';import path from 'node:path';import { Buffer } from 'node:buffer';import process from 'node:process';import crypto from 'node:crypto';// etc.``` **Enforcement**: Biome linter rule `noNodejsModules` prevents this at lint time. ### Where Node.js APIs Are SAFE **Vite Plugin Implementations**: Node.js APIs are safe in Vite plugins **Locations**: - `packages/astro/src/vite-plugin-*/` → Safe for Node.js APIs- `packages/astro/src/cli/` → Safe for Node.js APIs- Test files → Safe for Node.js APIs **Mixed/Unclear locations**: - `packages/astro/src/core/` → Mixed (contains both build-time and runtime code) **Safe approach for core/**: ```typescript// AVOID in core/ unless in Vite pluginimport fs from 'node:fs'; // PREFER cross-platform utilitiesimport { fileURLToPath } from '@astrojs/internal-helpers/path';``` ### Special Case: Vite Plugins **CAN use Node.js APIs**: In the plugin implementation itself ```typescript// ALLOWEDexport function myVitePlugin() { return { name: 'my-plugin', load: { filter: { id: /\.astro$/, }, async handler(id) { // Can use Node.js APIs here const fs = await import('node:fs/promises'); const content = await fs.readFile(id, 'utf-8'); return { code: content }; }, }, };}``` **CANNOT use Node.js APIs**: In virtual modules returned by plugins ```typescript// FORBIDDENexport function myVitePlugin() { return { name: 'my-plugin', load: { filter: { id: new RegExp(`^\\0virtual:my-module$`), }, handler() { // This code runs in runtime/server context return { code: ` import fs from 'node:fs'; // FORBIDDEN export const data = fs.readFileSync('/data.json'); `, }; }, }, };}``` ### Critical: NonRunnableDevEnvironment **Important**: Some adapters (like Cloudflare) set Vite's `NonRunnableDevEnvironment`, which imposes limitations on what Vite plugins can do. **Limitation**: Vite plugins execute, but certain operations are restricted. For example, you cannot use `runner.import()` in this environment. **Implication**: While Vite plugins still run their hooks (transform, load, etc.), they may not have access to all runtime capabilities. Code must work with these limitations. **Rule**: Write code that doesn't depend on full Vite runtime capabilities. This is why runtime-agnostic code is essential. ### Why This Matters **Multi-runtime support**: Astro code must run in: - Node.js (traditional hosting)- Cloudflare Workers (V8 isolates)- Deno (alternative runtime)- Edge runtimes (limited APIs) **Implication**: Most code must be platform-agnostic. **Reality**: The codebase doesn't have perfect separation yet. When in doubt, avoid Node.js APIs and use cross-platform utilities from `@astrojs/internal-helpers`. ### How to Handle This #### Pattern 1: Use Cross-Platform Utilities Use `@astrojs/internal-helpers` instead of Node.js APIs: ```typescript// BAD: Direct Node.js APIimport { resolve } from 'node:path';const fullPath = resolve('./config.json'); // GOOD: Cross-platform utilityimport { fileURLToPath } from '@astrojs/internal-helpers/path';const fullPath = fileURLToPath(new URL('./config.json', import.meta.url));``` #### Pattern 2: Vite Plugin for File Operations If you need file operations, do them in a Vite plugin: ```typescript// GOOD: In Vite plugin implementationexport function myVitePlugin() { return { name: 'my-plugin', load: { filter: { id: /\.config\.js$/, }, async handler(id) { // Safe to use Node.js APIs here const fs = await import('node:fs/promises'); const content = await fs.readFile(id, 'utf-8'); return { code: content }; }, }, };}``` #### Pattern 3: Build-Time Data Generation Generate data at build time via Vite plugin, embed in virtual module: ```typescript// Vite plugin - safe to use Node.js APIsconst VIRTUAL_MODULE_ID = 'virtual:my-config';const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID; export function myVitePlugin() { return { name: 'my-plugin', resolveId: { filter: { id: new RegExp(`^${VIRTUAL_MODULE_ID}$`), }, handler() { return RESOLVED_VIRTUAL_MODULE_ID; }, }, load: { filter: { id: new RegExp(`^${RESOLVED_VIRTUAL_MODULE_ID}$`), }, async handler() { // Read at build time (safe here) const fs = await import('node:fs/promises'); const config = JSON.parse(await fs.readFile('./config.json', 'utf-8')); // Embed in generated code (no Node.js APIs in output) return { code: `export default ${JSON.stringify(config)}`, }; }, }, };}``` ### Detection **Error message**: If you violate this, you'll typically see: - `ReferenceError: fs is not defined`- `ReferenceError: process is not defined`- Build succeeds but runtime fails in edge environments **Prevention**: Code review, test in multiple environments ## Test Isolation Requirements **Every test fixture MUST have a unique `outDir`** to avoid cache pollution between tests. **Why**: Build artifacts are cached and shared via ESM between test runs. **Reference**: See [testing.md](testing.md) for detailed explanation, examples, and detection strategies. ## Circular Dependencies ### The Problem TypeScript circular dependencies can cause: - Undefined exports at runtime- Type resolution issues- Build failures ### Where It Happens Most commonly in type imports. ### The Solution **Types are centralized**: `packages/astro/src/types/` ```typescript// BAD: Import type from implementationimport type { AstroConfig } from '../config/index.js'; // GOOD: Import type from types/import type { AstroConfig } from '../types/public.js';``` ### Prevention - Import types from `types/` directory- Avoid importing implementation files for types- Use `import type` (not `import`) for type-only imports ## Virtual Module Conventions **Constraint**: New virtual modules must use `virtual:astro:*` prefix (not `@astro-page:*` which is legacy). **Why**: Standard convention, avoids conflicts, follows Rollup/Vite patterns. **Reference**: See [architecture.md](architecture.md) for virtual module registry, implementation patterns, and detailed conventions. ## Build vs Runtime Boundaries ### The Separation Code must respect execution boundaries: | Context | Location | When Runs | Node APIs || -------------- | ----------------- | ----------------- | --------- || Build | `core/` | `astro build` | Yes || Dev | `core/` | `astro dev` setup | Yes || Runtime Server | `runtime/server/` | SSR rendering | No || Runtime Client | `runtime/client/` | Browser | No | ### Common Violation ```typescript// BAD: Runtime code importing build code// In runtime/server/render.tsimport { buildSomething } from '../../core/build/utils.js';``` **Why bad**: Runtime shouldn't depend on build-time code. Creates coupling and increases bundle size. ### Correct Pattern ```typescript// GOOD: Data flows from build to runtime via manifest// Build time (core/build/)const manifest = { routes: processedRoutes, config: runtimeConfig,}; // Runtime (runtime/server/)import { manifest } from 'virtual:astro:manifest';``` ## Package Boundaries ### Workspace Dependencies Test fixtures and examples MUST use workspace dependencies: ```json// GOOD: fixture package.json{ "dependencies": { "astro": "workspace:*", "@astrojs/react": "workspace:*" }}``` ```json// BAD: specific versions{ "dependencies": { "astro": "^4.0.0", "@astrojs/react": "^3.0.0" }}``` ### Why - Links to local packages during development- Automatically uses latest local code- Prevents version mismatches ### Catalog Dependencies For external packages, use catalog when available: ```json{ "dependencies": { "astro": "workspace:*", "react": "catalog:", "react-dom": "catalog:" }}``` **Catalog location**: Root `package.json` ## Changeset Requirements ### When Required **Required for**: - All packages in `packages/`- Any user-facing changes **NOT required for**: - Examples in `examples/`- Test fixtures- Documentation only changes- Internal refactors with no API changes ### Prerelease Mode **Current state**: Repository in prerelease mode (check `.changeset/config.json`) ```json{ "baseBranch": "origin/next"}``` **Implication**: Changes go to `next` release, not `latest` ### Creating Changeset ```bashpnpm exec changeset``` Select packages and describe changes. ## Performance Constraints ### Build Cache **Shared cache**: Build artifacts cached in `.astro/` and `dist/` **Implication**: - Tests must use unique `outDir`- Manual cache clear may be needed- Cache can speed up but also cause issues ### Content Layer Concurrency **Location**: `packages/astro/src/content/content-layer.ts` **Pattern**: Uses PQueue with concurrency: 1 ```typescriptconst queue = new PQueue({ concurrency: 1 });``` **Implication**: Loaders run sequentially, not in parallel **Reason**: Prevents race conditions in data store ### Memory Constraints **Large sites**: May hit memory limits during build **Mitigation**: ```bashnode --max-old-space-size=4096 node_modules/.bin/astro build``` ## Debugging Constraints ### Cannot Use Interactive Commands **Forbidden in CI**: ```bash# BAD: Interactivegit rebase -igit add -i``` **Reason**: No TTY in CI environment ### Log Levels **Production**: Don't log verbosely by default ```typescript// BAD: Always logsconsole.log('Processing file:', file); // GOOD: Use logger with levelslogger.debug('Processing file:', file);``` ## Common Gotchas ### 1. File Paths in Tests ```typescript// BAD: Relative to cwdconst fixture = await loadFixture({ root: 'fixtures/my-test/',}); // GOOD: Relative to test fileconst fixture = await loadFixture({ root: './fixtures/my-test/',});``` ### 2. Async/Await in Hooks ```typescript// BAD: Forgetting awaitbefore(async () => { fixture.build(); // Missing await}); // GOOD: Proper awaitbefore(async () => { await fixture.build();});``` ### 3. Server Cleanup ```typescript// BAD: Server not stoppeddescribe('dev', () => { before(async () => { devServer = await fixture.startDevServer(); }); // Missing after() hook}); // GOOD: Always cleanupdescribe('dev', () => { before(async () => { devServer = await fixture.startDevServer(); }); after(async () => { await devServer.stop(); });});``` ### 4. Type Imports ```typescript// BAD: Runtime import for typesimport { AstroConfig } from 'astro'; // GOOD: Type-only importimport type { AstroConfig } from 'astro';``` ### 5. Module Resolution ```typescript// BAD: May fail in different contextsimport helper from '../utils'; // GOOD: Explicit extensionimport helper from '../utils.js';``` ## Constraint Checklist Before submitting code, verify: - [ ] No Node.js APIs in `runtime/` code- [ ] Tests have unique `outDir`- [ ] No circular dependencies- [ ] Virtual modules follow naming conventions- [ ] Workspace dependencies in fixtures- [ ] Changeset created (if needed)- [ ] Servers cleaned up in tests- [ ] Type imports use `import type`- [ ] File extensions explicit (`.js`) ## Further Reading - CONTRIBUTING.md: Node.js API restrictions (lines 84-98)- CONTRIBUTING.md: Test isolation (lines 203-214)- architecture.md: Execution contexts- testing.md: Test patterns
Referenced from SKILL.md
Source excerpt starting at line 15.| -------------------------- | -------------------------------------------------------------------- | ---------------------------------- || Adding a core feature | [architecture.md](architecture.md), [constraints.md](constraints.md) | [testing.md](testing.md) || Fixing a bug | [debugging.md](debugging.md) | [architecture.md](architecture.md) || Writing/fixing tests | [testing.md](testing.md) | [constraints.md](constraints.md) || Creating an integration | Explore `packages/integrations/` for examples | [testing.md](testing.md) |
Source excerpt starting at line 20.| Understanding architecture | [architecture.md](architecture.md) | - || Dealing with errors | [debugging.md](debugging.md), [constraints.md](constraints.md) | [testing.md](testing.md) || Understanding constraints | [constraints.md](constraints.md) | [architecture.md](architecture.md) |
Source excerpt starting at line 28.1. **Prefer Unit Tests**: Write unit-testable code by default. Use integration tests only when necessary → [testing.md](testing.md)2. **Node.js API Restrictions**: Cannot use Node.js APIs in `runtime/` code → [constraints.md](constraints.md)3. **Test Isolation**: Must set unique `outDir` for each integration test → [testing.md](testing.md)