Skip to content

Latest commit

 

History

History
184 lines (149 loc) · 9.17 KB

File metadata and controls

184 lines (149 loc) · 9.17 KB

Agent Context for tscss

Project Overview

tscss is a CSS macro/transform system built on top of lightningcss. It lets users execute TypeScript functions inside CSS files to generate declarations, selectors, animations, and more at build time.

npm org: @subwaytime/tscss
Workspace packages: @tscss/core, @tscss/directives, @tscss/helpers, @tscss/vite

Architecture

Input CSS → lightningcss parse → AST visitor → process directives → serialize → Output CSS
                                  ↓
                            processRule (style rules)
                            processAtFunctionsInTokens (value functions)
                            collectRootRules (spawn directives)
  • Parser: lightningcss (fast, spec-compliant CSS parser)
  • Visitor: Rule.style for at-rules inside selectors, Rule.unknown for top-level at-rules
  • Metadata: WeakMap-based store (meta.ts) tags handlers with type info

Project Structure

packages/
  core/                          # @tscss/core — transform engine
    constant.ts                  # global TSCSS constant for error prefixes
    index.ts                     # exports: transformCSS, inlineCSS, generateId, defineHelper, types
    transform.ts                 # main pipeline: preprocess → lightningcss → collect root rules
    types.ts                     # Handler, FunctionHandlers, HandlerResult
    utils/
      defineHelper.ts            # defineHelper() factory — creates directives with alias/schema support
      meta.ts                    # WeakMap metadata store (setMeta, getMeta)
      processRule.ts             # AST visitor for style/unknown/nested-declarations rules
      processDeclarationsBlock.ts # value function replacement inside declarations
      processNestedDeclarations.ts # thin wrapper for nested declarations
      helpers.ts                 # token utilities: argsToStrings, getArgsFromPrelude, normalizeValue
      processAtFunctions.ts      # token-level @name(...) handler inside declaration values
      tokenUtils.ts              # serializeTokenOrValue, resolveVarTokens, tokensToString
      parseRule.ts               # parseRule() and parseValueToTokens() via lightningcss
      id.ts                      # generateId() via nanoid
      inlineCSS.ts               # recursive @import inlining with cycle detection

  directives/                    # @tscss/directives — directive factories
    index.ts                     # exports: defineHelper, cssVar, declaration, selector, media, keyframes, fontFace
    cssVar.ts                    # custom properties (with typed @property support, random names)
    declaration.ts               # fixed property wrapper
    selector.ts                  # selector variant generator (&:hover)
    media.ts                     # @media query wrapper
    keyframes.ts                 # @keyframes animations (spawn-based)
    fontFace.ts                  # @font-face + utility class (spawn-based)

  helpers/                       # @tscss/helpers — pre-built directives
    index.ts                     # single barrel: exports all directives
    background/                  # one file per helper
      bg.ts, checkerboard.ts, dotPattern.ts, grid.ts,
      horizontalLines.ts, paper.ts, verticalLines.ts
    border/
      perfectBorder.ts
    colors/                      # one file per helper
      black.ts, complement.ts, darken.ts, grayscale.ts, lighten.ts,
      mix.ts, opacity.ts, saturate.ts, tint.ts, white.ts
    math/                        # one file per helper
      abs.ts, lerp.ts, negate.ts, ratio.ts
    layouts/                     # one file per helper
      autoGrid.ts
    transforms/                  # one file per helper
      pxToRem.ts, remToPx.ts,
      toHsl.ts, toHwb.ts, toOklab.ts, toOklch.ts, toRgb.ts
    unified/                     # dispatch helpers
      bg.ts, format.ts, mod.ts
    types/                       # TypeScript type utilities (Px, Rem, Color, etc.)

  vite/                          # @tscss/vite — Vite plugin
    index.ts                     # tscss plugin with inlineCSS, loadFromDir, HMR

Directive Design

Every directive is created via defineHelper(options):

export interface DefineHelperOptions {
  type?: string;                // 'var' | 'declaration' | 'selector' | 'media' | 'function'
  property?: string;            // auto-wrap bare at-rules with this property
  name?: string;                // custom property name (for type='var')
  inline?: boolean;             // extract to :root when false (for type='var')
  spawn?: () => string;         // generates root-level CSS (keyframes, fontFace, @property)
  typedProperties?: TypedPropertyConfig[]; // typed @property rules for custom properties
  alias?: string | string[];    // alternate at-rule names for this directive
  schema?: TSchema;             // valibot schema for runtime argument validation
  vars?: (args, ctx) => string[]; // custom property declarations (e.g. --name: value;)
  value: (args, ctx) => string | string[]; // main handler function
}

Usage Modes

  1. Bare at-rule inside selector: @paper(50px); → auto-wraps with property
  2. Value function in declaration: color: @mix(red, blue, 50); → replaces inline
  3. Spawn for root-level CSS: @fadeIn;@keyframes fadeIn { ... } at top level

Composition

Directives expose .value for string-level composition:

const v = verticalLines.value(['20px']);  // returns raw string

Metadata System

meta.ts uses a WeakMap<Handler, HandlerMeta> to store type info:

interface HandlerMeta {
  type: string;        // 'var' | 'declaration' | 'selector' | 'media' | 'function'
  property?: string;   // for declaration auto-wrap
  name?: string;       // custom property name
  inline?: boolean;    // extract to :root
  typedProperties?: TypedPropertyConfig[]; // typed @property rules
  aliases?: string[];  // expanded alternate names
}

processRule.ts reads meta to decide how to handle each directive:

  • type === 'selector' → generates new style rule with modified selector
  • type === 'declaration' → wraps result with property: value;
  • type === 'var' with vars → emits vars declarations (or extracts to :root)
  • type === 'var' without vars → falls back to manual --name: value; wrapping
  • type === 'media' → wraps parent rule in @media query
  • handler.spawn → skips inline, CSS collected by collectRootRules
  • type === 'declaration' with vars + inline: false → extracts vars to :root

Build System

  • Bundler: tsdown (Rolldown-powered, fast TypeScript bundler)
  • Config: tsdown.config.ts at root — builds all 4 packages into ./dist/
  • Format: ESM only (no CJS)
  • Workspace: Bun workspaces (packages/*)

Testing

  • Runner: vitest (via bun test which runs vitest)
  • Files:
    • tests/core.test.ts — defineHelper, bare args, schema validation, blocked aliases
    • tests/directives.test.ts — cssVar, declaration, selector, media, keyframes, fontFace
    • tests/helpers/ — background, color, transform, border, layout, unified, integration tests (one file per category)
    • tests/vite.test.ts — loadFromDir and inlineCSS tests
  • Current: 60+ tests (all passing)

Key Decisions

  1. No separate codegen.ts — CSS string generation is handled by lightningcss serialization after AST mutation.
  2. WeakMap metadata — avoids polluting handler functions with properties.
  3. spawn over root: truespawn is a direct property on the handler, not stored in meta. Keeps root CSS generation co-located with the directive factory via closure.
  4. String-level helpers are directives — all helpers in @tscss/helpers are directives (no separate string utilities). Composition uses .value.
  5. Bare argument syntax — quotes are optional: @mix(red, blue, 50) works the same as @mix('red', 'blue', '50').
  6. ESM-only — no CJS builds. All consumers are assumed to be modern ESM (Vite, Bun, Node ESM).
  7. One file per helper — helpers are organized in category folders (background/, colors/, etc.) with no per-category barrel files.
  8. generateId(4) for custom properties — nanoid-generated suffixes avoid collisions better than incrementing counters.
  9. BLOCKED_ALIASES — reserved aliases can be blocked from user-defined directives. var is auto-injected by defineHelper when type === 'var', bypassing the block. This prevents any user directive from claiming var while keeping the array clean.

Adding a New Directive

  1. Create file in packages/directives/ (e.g., myDirective.ts)
  2. Export factory function that returns defineHelper({ ... })
  3. Add export to packages/directives/index.ts
  4. Add to packages/vite/index.ts re-exports
  5. Add tests in the appropriate tests/*.test.ts file

Adding a New Helper

  1. Create file in the appropriate packages/helpers/<category>/ folder
  2. Use defineHelper({ schema: v.tuple([...]), value: (...) }) for runtime validation
  3. Add export to packages/helpers/index.ts
  4. Add tests in tests/helpers.test.ts

Files to Avoid Editing

  • packages/helpers/layouts/ — reserved for user (empty)
  • packages/helpers/transitions/ — reserved for user (empty)