Scope:
packages/watcher/src/framework/
The FrameworkAnalyzer interface is the extension point for adding new source-language support to the aesthetic-function watcher. Each analyzer understands one framework's file format and produces the same canonical outputs (IntentModel, AstIntentReport, AnchoredAstReport) that the rest of the pipeline consumes unchanged.
The watcher originally only understood React/TypeScript (.tsx, .jsx, .ts, .js). Adding Vue 3 required a pluggable dispatch layer so:
- A single hot-path in
watch.tshandles all file types without conditionals scattered everywhere. - New frameworks (Svelte, Solid, Astro) can be added by registering one module — no other files need changing.
- The React analyzer's existing behavior is unchanged — it was wrapped in the same interface.
// packages/watcher/src/framework/types.ts
interface FrameworkAnalyzer {
/** Unique analyzer ID (e.g., 'react', 'vue3'). */
readonly id: string;
/** File extensions this analyzer handles (e.g., ['.vue']). */
readonly extensions: ReadonlyArray<string>;
/** Parse file source → AstIntentReport (component structure, props, semantics). */
parseAst(code: string, filePath: string, opts?: AnalyzerOpts): AstIntentReport;
/** Parse file source → IntentModel (marker-driven Figma operations). */
parseIntent(code: string, filePath: string, opts?: AnalyzerOpts): IntentModel;
/** Anchor @figma markers to their nearest code element → AnchoredAstReport. */
anchorMarkers(code: string, filePath: string, opts?: AnalyzerOpts): AnchoredAstReport;
/** Quick check: does this file contain any @figma markers? */
hasMarkers(code: string): boolean;
}packages/watcher/src/framework/registry.ts maintains an extension → FrameworkAnalyzer map. The registry is initialized once at watcher startup via initializeDefaultAnalyzers().
import { initializeDefaultAnalyzers, resolveByPath } from './framework/index.js';
initializeDefaultAnalyzers(); // registers React + Vue 3
const analyzer = resolveByPath('src/App.vue'); // → Vue3FrameworkAnalyzer
const analyzer2 = resolveByPath('src/Button.tsx'); // → ReactFrameworkAnalyzer- Last registration wins for any given extension.
- Extension matching is case-insensitive (
.VUE→.vue). resolveByPathreturnsundefinedfor unsupported extensions — the watcher logs "Unsupported extension" and skips.
| Analyzer | ID | Extensions | Write-back |
|---|---|---|---|
ReactFrameworkAnalyzer |
react |
.tsx, .jsx, .ts, .js |
✅ Enabled (existing behavior) |
Vue3FrameworkAnalyzer |
vue3 |
.vue |
❌ Disabled (Phase 3 spike pending) |
- Create
packages/watcher/src/framework/<name>/index.ts. - Implement
FrameworkAnalyzer. - Add to
packages/watcher/src/framework/index.ts:
import { svelte3Analyzer } from './svelte3/index.js';
registerFrameworkAnalyzer(svelte3Analyzer);- Update the watcher glob in
watch.tsto include the new extension (e.g.,.svelte). - Add fixtures under
packages/watcher/src/__fixtures__/<name>/. - Add tests.
No other files need to change.
File change (.vue or .tsx etc.)
↓
resolveByPath(relativePath) → finds the right FrameworkAnalyzer
↓
analyzer.hasMarkers(content) → quick skip if no @figma markers
↓
analyzer.parseIntent(content, ...) → IntentModel
↓
[Vue write-back blocked] → if .vue, materialize() is skipped
↓
intentToFigmaOps(model) → FigmaOperation[]
↓
applyComponentMapResolution() → stable ID mapping
↓
sendOperationsToServer() → → Figma Plugin
Source file mutation (materialize()) is gated by extension:
- React files (
.tsx,.jsx): materialize() runs normally per existing behavior. - Vue files (
.vue): materialize() is always skipped with a warning until the Phase 3 round-trip spike passes. This prevents.vuesource corruption. af reconcile --write: writes JSON artifacts todesign-materializations/— not source files — and is safe for all extensions at all times.
See vue3-adapter.md for Vue-specific details.