@@ -84,6 +84,15 @@ See [CLI/Core Architecture](#clicore-architecture) section for details.
8484 - ` formAssociated: true ` → Use ` @AttachInternals() ` decorator instead (auto-sets ` formAssociated: true ` )
8585 - To use ` @AttachInternals ` without form association: ` @AttachInternals({ formAssociated: false }) `
8686 - Run ` stencil migrate --dry-run ` to preview automatic migration, or ` stencil migrate ` to apply changes
87+ - ** ` buildDist ` and ` buildDocs ` config options removed.** Use ` skipInDev ` on individual output targets for granular control:
88+ - ` dist ` : ` skipInDev: false ` (default) - always builds in both dev and prod
89+ - ` dist-custom-elements ` : ` skipInDev: true ` (default) - skips in dev mode, builds in prod
90+ - ` dist-hydrate-script ` : ` skipInDev: true ` (default, unless ` devServer.ssr ` is enabled)
91+ - ` docs-* ` targets: ` skipInDev: true ` (default) - skips in dev mode, builds in prod
92+ - ` custom ` output targets: ` skipInDev: true ` (default) - skips in dev mode, builds in prod
93+ - All outputs ALWAYS run in production mode regardless of ` skipInDev ` setting
94+ - Run ` stencil migrate ` to update your config (removes deprecated options)
95+ - ** ` --esm ` CLI flag removed.** Configure ` skipInDev ` on output targets instead.
8796
8897### 8. 🏷️ Release Management: Changesets
8998** Status:** 📋 Planned
@@ -356,6 +365,197 @@ Simplified the version/build identification system for v5:
356365</details >
357366
358367
368+ ---
369+
370+ ## 🚀 Compilation Performance: Watch Mode Fast Path
371+
372+ ** Status:** 📋 Planned
373+
374+ ### Current Architecture
375+
376+ ```
377+ ┌─────────────────────────────────────────────────────────────────────────┐
378+ │ File Change → IncrementalCompiler.rebuild() → Full build() │
379+ │ └─ runTsProgram (all changed files) │
380+ │ └─ generateOutputTargets (rolldown compiles) │
381+ │ └─ writeBuild │
382+ └─────────────────────────────────────────────────────────────────────────┘
383+ ```
384+
385+ ** Problem:** Even a single-file change triggers the full pipeline.
386+
387+ ### Solution: Leverage ` transpileModule ` for Watch Mode
388+
389+ The existing ` transpileModule() ` function (` src/compiler/transpile/transpile-module.ts ` ) already does single-file compilation with all necessary transforms. We can use it for a "fast path" in watch mode.
390+
391+ #### How ` transpileModule ` Works Today
392+
393+ 1 . Creates a fresh ` ts.Program ` for each call
394+ 2 . Runs ` convertDecoratorsToStatic ` (extracts component metadata)
395+ 3 . Runs output-target transforms (` lazyComponentTransform ` or ` nativeComponentTransform ` )
396+ 4 . Handles inheritance via ` extraFiles ` parameter
397+ 5 . Returns: JS code, sourcemap, ` moduleFile ` with component metadata
398+
399+ #### Proposed Enhancement: Add Shared Context
400+
401+ ``` typescript
402+ // Enhanced transpileModule signature
403+ export const transpileModule = (
404+ config : d .ValidatedConfig ,
405+ input : string ,
406+ transformOpts : d .TransformOptions ,
407+ context ? : {
408+ // Reuse existing Program/TypeChecker from IncrementalCompiler
409+ program? : ts .Program ;
410+ typeChecker? : ts .TypeChecker ;
411+ // Update existing moduleMap instead of creating fresh
412+ compilerCtx? : d .CompilerCtx ;
413+ // Access to component list for cross-component transforms
414+ buildCtx? : d .BuildCtx ;
415+ },
416+ ): d .TranspileModuleResults => {
417+ // If context provided, reuse it; otherwise create fresh (current behavior)
418+ const compilerCtx = context ?.compilerCtx ?? new CompilerContext ();
419+ const buildCtx = context ?.buildCtx ?? new BuildContext (config , compilerCtx );
420+ const typeChecker = context ?.typeChecker ?? program .getTypeChecker ();
421+ // ...
422+ }
423+ ```
424+
425+ #### Watch Mode Fast Path
426+
427+ ```
428+ ┌────────────────────────────────────────────────────────────────────┐
429+ │ File Change Detected │
430+ ├────────────────────────────────────────────────────────────────────┤
431+ │ 1. Quick check: is it a component file? │
432+ │ ├─ NO (plain .ts) → Skip to step 5 │
433+ │ └─ YES → Continue... │
434+ │ │
435+ │ 2. transpileModule(source, opts, { │
436+ │ program: incrementalCompiler.getProgram(), │
437+ │ typeChecker: incrementalCompiler.getProgram().getTypeChecker(),
438+ │ compilerCtx, │
439+ │ buildCtx, │
440+ │ }) │
441+ │ └─ Reuses existing TypeChecker (no fresh Program creation) │
442+ │ └─ Updates existing moduleMap entry │
443+ │ │
444+ │ 3. Compare old vs new component metadata: │
445+ │ - API changed (props/events/methods)? → Full rebuild │
446+ │ - JSDoc changed && docsOutputTargets.length > 0? → Regen docs │
447+ │ - Neither? → HOT SWAP only │
448+ │ │
449+ │ 4. If docs need regen: outputDocs() only (skip bundling) │
450+ │ │
451+ │ 5. Hot-swap module in dev server (skip rolldown entirely) │
452+ └────────────────────────────────────────────────────────────────────┘
453+ ```
454+
455+ ### Implementation Plan
456+
457+ #### Phase 1: Add ` context ` Parameter to ` transpileModule `
458+
459+ Allow reusing existing ` Program ` /` TypeChecker ` /` CompilerCtx ` from the watch build:
460+
461+ ``` typescript
462+ // In watch-build.ts
463+ const results = transpileModule (config , source , transformOpts , {
464+ program: incrementalCompiler .getProgram (),
465+ typeChecker: incrementalCompiler .getProgram ()?.getTypeChecker (),
466+ compilerCtx ,
467+ buildCtx ,
468+ });
469+ ```
470+
471+ ** Benefits:**
472+ - No fresh ` ts.Program ` creation per file (expensive)
473+ - Shared type information for decorator resolution
474+ - Updates existing ` moduleMap ` in-place
475+
476+ #### Phase 2: Implement Change Detection
477+
478+ ``` typescript
479+ // In watch-build.ts, after transpileModule completes:
480+ const oldMeta = compilerCtx .moduleMap .get (filePath )?.cmps [0 ];
481+ const newMeta = results .moduleFile ?.cmps [0 ];
482+
483+ // Check if public API changed
484+ const apiChanged =
485+ JSON .stringify (oldMeta ?.properties ) !== JSON .stringify (newMeta ?.properties ) ||
486+ JSON .stringify (oldMeta ?.events ) !== JSON .stringify (newMeta ?.events ) ||
487+ JSON .stringify (oldMeta ?.methods ) !== JSON .stringify (newMeta ?.methods );
488+
489+ // Check if JSDoc changed (only matters if docs output targets exist)
490+ const hasDocsTargets = config .outputTargets .some (isOutputTargetDocs );
491+ const jsDocChanged = hasDocsTargets && (
492+ JSON .stringify (oldMeta ?.docs ) !== JSON .stringify (newMeta ?.docs ) ||
493+ JSON .stringify (oldMeta ?.docsTags ) !== JSON .stringify (newMeta ?.docsTags )
494+ );
495+
496+ if (apiChanged ) {
497+ // API changed - need full incremental rebuild (types, bundling, etc.)
498+ await triggerRebuild ();
499+ } else if (jsDocChanged ) {
500+ // Only docs changed - regenerate docs, hot-swap module
501+ compilerCtx .moduleMap .set (filePath , results .moduleFile );
502+ await outputDocs (config , compilerCtx , buildCtx );
503+ devServer .hotSwapModule (filePath , results .code );
504+ } else {
505+ // Internal change only - just hot-swap the module
506+ compilerCtx .moduleMap .set (filePath , results .moduleFile );
507+ devServer .hotSwapModule (filePath , results .code );
508+ }
509+ ```
510+
511+ #### Phase 3: Non-Component Files Fast Path
512+
513+ For plain ` .ts ` files (utilities, services, etc.), we don't need any Stencil transforms:
514+
515+ ``` typescript
516+ const isComponent = filePath .match (/ \. tsx? $ / ) &&
517+ compilerCtx .moduleMap .get (filePath )?.cmps ?.length > 0 ;
518+
519+ if (! isComponent ) {
520+ // Plain TS file - just re-emit via TypeScript
521+ // No decorator extraction, no component transforms needed
522+ const result = ts .transpileModule (source , { compilerOptions });
523+ devServer .hotSwapModule (filePath , result .outputText );
524+ return ;
525+ }
526+ ```
527+
528+ ### Change Detection Matrix
529+
530+ | Change Type | API Changed? | JSDoc Changed? | Action |
531+ | -------------| --------------| ----------------| --------|
532+ | Internal logic only | ❌ | ❌ | Hot-swap module |
533+ | JSDoc comment updated | ❌ | ✅ | Regen docs + hot-swap |
534+ | New ` @Prop() ` added | ✅ | - | Full rebuild |
535+ | Prop type changed | ✅ | - | Full rebuild |
536+ | Component renamed | ✅ | - | Full rebuild |
537+ | Style change | ❌ | ❌ | Existing CSS path |
538+
539+ ### What Triggers Full Rebuild
540+
541+ - Component API changes (props, events, methods, states)
542+ - New component added
543+ - Component deleted
544+ - Component tag name changed
545+ - Inheritance chain changes
546+
547+ ### Expected Impact
548+
549+ | Change Type | Current | With Fast Path |
550+ | -------------| ---------| ----------------|
551+ | Internal logic change | ~ 500ms-1s | ** < 50ms** |
552+ | JSDoc change (with docs targets) | ~ 500ms-1s | ** < 100ms** |
553+ | Style change | ~ 200ms | ~ 200ms (unchanged) |
554+ | API change (new prop) | ~ 500ms-1s | ~ 500ms-1s (unchanged) |
555+ | New component | ~ 500ms-1s | ~ 500ms-1s (unchanged) |
556+
557+ ** ~ 80% of dev changes are internal logic** → massive improvement for typical workflow.
558+
359559---
360560
361561## ⚠️ Notes for Future Agents
@@ -374,4 +574,4 @@ In individual packages or from root. pnpm workspaces handle dependency ordering
374574
375575---
376576
377- * Last updated: 2026-04-04 *
577+ * Last updated: 2026-04-13 *
0 commit comments