@shulyugin/emmet-compiler
v0.2.0
Published
Emmet-based DSL React/Angular/CSS compiler with token-validated Tailwind
Maintainers
Readme
emmet-compiler
View-layer compiler: takes a concise Emmet-style markup tree and emits a React (.tsx), Angular, or React Native (.tsx) component against a YAML component registry.
Install
npm install -D @shulyugin/emmet-compiler
# or use directly
npx @shulyugin/emmet-compiler --helpRequires Node.js 20+.
CLI
# Compile a component to React (→ LoginForm.react.tsx)
emmet-compiler compile LoginForm.emmet -s components.yml --markup react
# …or to Angular (→ LoginForm.angular.ts)
emmet-compiler compile LoginForm.emmet -s components.yml --markup angular
# …or React Native (→ LoginForm.react-native.tsx)
emmet-compiler compile LoginForm.emmet -s components.yml --markup react-native
# Bulk compile with an output directory
emmet-compiler compile 'src/**/*.emmet' -s components.yml -o dist
# Watch mode
emmet-compiler watch 'src/**/*.emmet' -s components.yml -o dist
# Lint against the schema (unknown components, bad props, etc.)
emmet-compiler lint 'src/**/*.emmet' -s components.yml
# Generate a Zod validation schema from the YAML registry
emmet-compiler generate-schema components.yml -o src/generated-schema.ts
# Decompile existing React back into EmmetSpec (→ LoginForm.react.emmet)
emmet-compiler decompile src/LoginForm.tsx -s components.yml
emmet-compiler decompile src/LoginForm.tsx -s components.yml --json # emmet + lossy reportdecompile is the inverse of the React target: it parses a .tsx file with the TypeScript compiler API and emits EmmetSpec — schema components vs raw tags, className → .classes, literal props → (props), JSX text → {text}, Array.from({length: N}) → *N. Anything the DSL cannot express (spread props, ternaries, .map loops, template literals, non-literal expressions, components missing from the schema) is never dropped silently: it is emitted as # TODO: unrepresentable: <reason> lines and returned as a structured lossy list in --json. That makes decompile a bypass detector — hand-written or agent-written UI that never went through the compiler can be pulled back under the schema and linted like everything else — and the entry point for mining an existing codebase into design-system memory. Structural round-trip (compile → decompile → compile) is covered by tests.
Output filenames are <input>.<markup>.<ext> — LoginForm.emmet → LoginForm.react.tsx / LoginForm.angular.ts / LoginForm.react-native.tsx. Override the directory with -o.
Every compile run ends with a violations receipt — what the schema rejected on the way to a clean file, aggregated by diagnostic code:
Compiled 1 file(s), 0 error(s)
4 violations blocked (UNKNOWN_PROP: 1, UNKNOWN_COMPONENT: 1, UX_MYSTERY_MEAT_NAV: 1, SEO_MISSING_H1: 1)compile --json emits the same as a single object ({"compiled":1,"errors":0,"violations":{"total":4,"byCode":{...}}}) for CI dashboards.
The React Native target emits View, Text, Pressable, and StyleSheet.create instead of HTML and className. Its initial Tailwind mapping covers flex layout, gap, spacing, standard and rounded-[Npx] radii, semantic colors, and opacity suffixes such as text-white/60. Semantic values come from a generated copy of Lectella's shipped dark theme. Override only the tokens your app changes with nativeColorTokens:
compileComponent(file, schema, {
markup: 'react-native',
nativeColorTokens: {
accent: '#123456',
'muted-foreground': '#abcdef',
},
});The override is merged with the generated default before StyleSheet.create is emitted. After changing Lectella's .dark theme, regenerate that default with npm run generate:lectella-native-theme; use node scripts/generate-lectella-native-theme.mjs --check in verification. nativeColorResolver remains available when generated styles must reference a runtime token dictionary. Unmapped utilities are listed in the compile report without failing the compile. Imports under @lectella/ui/ are mapped to @lectella/ui-native/ by default; override the prefix table with --native-import-map '@lectella/ui/=@app/native-ui/'.
Run emmet-compiler <command> --help for all options.
DSL
A .emmet file is a markup tree. No directives, no metadata, no props — just the view. The component name is derived from the filename.
.flex.min-h-screen.items-center.justify-center.p-6
.w-full.max-w-sm.flex.flex-col.gap-4.rounded-md.border.p-4
h1.text-xl.font-semibold{Welcome back}
p.text-sm.text-muted-foreground{Sign in to your account}
.flex.flex-col.gap-2
Label(htmlFor=email){Email}
Input(type=email,[email protected])
Button.w-full{Log in}Indentation builds the tree; Name is a component (resolved via the schema), tag is a raw HTML element, .class adds a Tailwind class, (prop=value) sets a prop, {text} sets the child text.
Full expression grammar and schema reference: schemas/README.md.
Schema example
__config:
from: "@/components/ui/*"
Button(variant? {default|outline|ghost}, size? {sm|md|lg}):
Input(type? {text|email|password|number}, placeholder? {string}):
Label(htmlFor {string}):Programmatic API
import { parseMarkupFile, compileComponent, loadSchema } from 'emmet-compiler';
const schema = loadSchema('components.yml');
const file = parseMarkupFile(source, 'LoginForm');
const tsx = compileComponent(file, schema);See dist/index.d.ts for all exports (parser, schema loader, validator, compiler, adapters).
Bundled schemas
schemas/ ships with ready-to-use YAML registries:
| File | Purpose |
|---|---|
| shadcn.yml | shadcn/ui components (~100) |
| icons.yml | Lucide icon whitelist with Icon prefix |
| recharts.yml | Recharts charting components |
Compose them via @import ./node_modules/emmet-compiler/schemas/shadcn.yml from your project schema.
License
MIT
