Skip to content

webeach/ecss-transformer

Repository files navigation

@ecss/transformer


@ecss/transformer

npm package build npm downloads

🇺🇸 English version | 🇷🇺 Русская версия

ECSS AST → CSS + JS + TypeScript declaration transformer.


📖 Documentation | 📋 Specification


💎 Features

  • 🔄 Single pass — CSS, JS bindings and .d.ts are all generated from one AST in one pass
  • 📦 Dual CJS/ESMrequire and import out of the box
  • 🎨 Flexible class names — configurable template with [name] and [hash:N] tokens
  • ⚙️ Config fileecss.config.json for project-wide defaults
  • 🧩 Framework-agnostic — supports React (className), Vue / Svelte / Solid (class) and both at once
  • 🏃 Runtime helpers — minimal _h + merge available via the ./runtime subpath
  • 📝 TypeScript — types included, overloads for both positional and named-object call styles

📦 Installation

npm install @ecss/transformer

or

pnpm add @ecss/transformer

or

yarn add @ecss/transformer

🚀 Quick start

import { parseEcss } from '@ecss/parser';
import { transform } from '@ecss/transformer';

const source = `
  @state-variant Theme {
    values: light, dark;
  }

  @state-def Button(--theme Theme: "light", --disabled boolean: false) {
    border-radius: 6px;

    @if (--disabled) {
      opacity: 0.4;
      cursor: not-allowed;
    }

    @if (--theme == "light") {
      background: #fff;
      color: #111;
    }
    @else {
      background: #1e1e1e;
      color: #f0f0f0;
    }
  }
`;

const ast = parseEcss(source);

const { css, js, dts } = transform(ast, {
  filePath: '/src/button.ecss',
});

console.log(css); // expanded CSS with attribute selectors
console.log(js); // ES module with Button(...) function
console.log(dts); // TypeScript declarations

🛠 API

transform(ast, config): TransformResult

The main entry point. Accepts an ECSS AST and config, returns all three artifacts in one pass.

import { transform } from '@ecss/transformer';

const { css, js, dts } = transform(ast, {
  filePath: '/src/button.ecss',
  classTemplate: '[name]-[hash:8]', // default: '[name]-[hash:6]'
  classAttribute: 'className', // 'className' | 'class' | 'both'
  runtimeImport: '@ecss/transformer/runtime', // default: 'virtual:ecss/runtime'
});

generateDts(ast, config): string

Generates only the .d.ts string without CSS or JS. Useful for producing sidecar declaration files next to .ecss sources.

import { generateDts } from '@ecss/transformer';

const dts = generateDts(ast, {
  filePath: '/src/button.ecss',
  classAttribute: 'class',
});

loadConfig(projectRoot): EcssConfig

Reads and parses ecss.config.json from the given directory. Returns an empty object when the file is absent or unreadable.

import { loadConfig } from '@ecss/transformer';

const config = loadConfig(process.cwd());

mergeConfig(fileConfig, explicit): EcssConfig

Merges file-level config with explicit per-call overrides. Explicit values take precedence; undefined values are ignored so that file defaults are preserved.

import { loadConfig, mergeConfig } from '@ecss/transformer';

const fileConfig = loadConfig(process.cwd());
const merged = mergeConfig(fileConfig, { classAttribute: 'class' });

⚙️ Configuration

TransformConfig

interface TransformConfig {
  filePath: string; // path to the .ecss file, required (used for hashing)
  classTemplate?: string; // class name template, default: '[name]-[hash:6]'
  classAttribute?: ClassAttribute; // 'className' | 'class' | 'both', default: 'className'
  runtimeImport?: string; // runtime import specifier, default: 'virtual:ecss/runtime'
}

DtsConfig

interface DtsConfig {
  filePath: string;
  classTemplate?: string;
  classAttribute?: ClassAttribute; // default: 'className'
}

EcssConfig (ecss.config.json)

interface EcssConfig {
  classAttribute?: ClassAttribute; // default: 'className'
  classTemplate?: string; // default: '[name]-[hash:6]'
  generateDeclarations?: boolean; // generate .ecss.d.ts alongside sources
}

Place ecss.config.json in your project root to set defaults for all .ecss files:

{
  "classAttribute": "class",
  "classTemplate": "[name]-[hash:8]",
  "generateDeclarations": true
}

Class name template

The classTemplate string supports two tokens:

Token Description
[name] The @state-def identifier (e.g. Button)
[hash] First 6 characters of the SHA-256 digest of filePath + name
[hash:N] First N characters of the hash

Example: "[name]-[hash:8]" for Button produces something like Button-a1b2c3d4.


📐 Output format

CSS

Every @state-def is expanded into flat CSS rules with data-e-<hash>-<param> attribute selectors:

.Button-a1b2c3 {
  border-radius: 6px;
}

.Button-a1b2c3[data-e-a1b2c3-disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

.Button-a1b2c3[data-e-a1b2c3-theme='light'] {
  background: #fff;
  color: #111;
}

.Button-a1b2c3[data-e-a1b2c3-theme='dark'] {
  background: #1e1e1e;
  color: #f0f0f0;
}

JS

An ES module with named state functions and the merge helper:

import { _h, merge } from 'virtual:ecss/runtime';

const Button = _h(
  'Button-a1b2c3',
  [
    ['theme', 'data-e-a1b2c3-theme', 'v', 'light'],
    ['disabled', 'data-e-a1b2c3-disabled', 'b', false],
  ],
  ['className'],
);

export default { Button, merge };

.d.ts

TypeScript declarations with overloads for both positional and named-object call styles:

type Theme = 'light' | 'dark';

interface ButtonResult {
  className: string;
  'data-e-a1b2c3-theme': string;
  'data-e-a1b2c3-disabled'?: '';
}

interface ButtonParams {
  theme?: Theme;
  disabled?: boolean;
}

interface EcssStyles {
  Button: {
    (theme?: Theme, disabled?: boolean): ButtonResult;
    (params: ButtonParams): ButtonResult;
  };
  merge: (
    ...results: Record<string, string | undefined>[]
  ) => Record<string, string | undefined>;
}

declare const styles: EcssStyles;
export default styles;

🏃 Runtime (@ecss/transformer/runtime)

A minimal runtime for computing element attributes on the client. Normally consumed via the virtual:ecss/runtime virtual module provided by @ecss/vite-plugin, but can also be imported directly.

_h(className, params, classFields)

Creates a state function for a single @state-def. The returned function can be called positionally or with a named object:

import { _h } from '@ecss/transformer/runtime';

const Button = _h(
  'Button-a1b2c3',
  [
    ['theme', 'data-e-a1b2c3-theme', 'v', 'light'],
    ['disabled', 'data-e-a1b2c3-disabled', 'b', false],
  ],
  ['className'],
);

Button('dark', true);
// → { className: 'Button-a1b2c3', 'data-e-a1b2c3-theme': 'dark', 'data-e-a1b2c3-disabled': '' }

Button({ theme: 'dark' });
// → { className: 'Button-a1b2c3', 'data-e-a1b2c3-theme': 'dark' }

merge(...results)

Merges multiple state function results into one object. class / className values are concatenated with a space; all other attributes are overwritten by the last non-undefined value.

import { merge } from '@ecss/transformer/runtime';

const attrs = merge(Button('dark'), Icon({ size: 'sm' }));
// → { className: 'Button-a1b2c3 Icon-def456', 'data-e-def456-size': 'sm', ... }

🔧 Development

Build:

pnpm build    # production
pnpm dev      # watch mode

Tests:

pnpm test
pnpm test:watch

Type check:

pnpm typecheck

Lint and format:

pnpm lint         # oxlint
pnpm lint:fix     # oxlint --fix
pnpm fmt          # oxfmt
pnpm fmt:check    # oxfmt --check

👨‍💻 Author

Developed and maintained by Ruslan Martynov.

Found a bug or have a suggestion? Open an issue or submit a pull request.


📄 License

Distributed under the MIT License.

About

ECSS transformer — converts ECSS AST to CSS, JS bindings and TypeScript declarations.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors