openpencil/packages/emit
Danila Poyarkov e17f74991b
feat(storybook): generate Radix UI components for React stories (#971)
* refactor(dom-css): share the generated component's markup and bindings

The component model now holds the element tree every framework writes: each layer's behaviour part, design tag, state class, and attributes, and the root's typed bindings (the two-way value, disabled, and each variant prop as a data attribute). The Vue generator only maps parts to Reka components and writes each binding its way, so another framework is a mapping table and a printer.

* feat(emit): print TSX modules with namespaced tags and spreads

jsx.printModule prints an es template filled with JSX as one TSX module, quoting every string the same way. Dotted tags such as Switch.Root print as JSX member expressions, and jsx.spread passes an object on as attributes. fill now fills holes inside the annotation it keeps on a filled identifier, which left $Props unfilled in a parameter's type.

* feat(storybook): generate Radix UI components for React stories

With --framework react, a component set whose behaviour is a Button, Switch, Checkbox, Toggle, or Collapsible exports as a component on the radix-ui package with its state styles in a CSS module. Its props extend the Radix root's, so checked, defaultChecked, onCheckedChange, and disabled come from Radix, and each other variant property is a prop set as a data attribute. Its stories use Radix's uncontrolled value so a play function can operate it. The browser spec now runs the same Switch and Collapsible checks for Vue and React.

* refactor(storybook): let generators say how stories import their component

The stories printer named each framework to know its Storybook package, how to import the component, and which prop sets its value. A generator now returns its files with the import path, whether the export is named, and the value's arg, and the Storybook packages come from the one table static stories use, so the printer is framework-neutral and exportStorybook looks generators up instead of branching.

* fix(storybook): keep a caller's class on React roots and run stories' play functions

The generated class came after the spread props, so a className passed to a generated React component was dropped. The root now joins its classes with the caller's. The browser spec mounts each framework's Default story and runs its exported play function with Storybook's own within and userEvent, checking the switch ends up on, and checks a caller's class stays on the root. emit shares one isNode guard.
2026-10-08 10:13:52 +00:00
..
scripts refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
src feat(storybook): generate Radix UI components for React stories (#971) 2026-10-08 10:13:52 +00:00
tests feat(storybook): generate Radix UI components for React stories (#971) 2026-10-08 10:13:52 +00:00
package.json feat(storybook): generate Reka UI components for Vue stories (#969) 2026-10-08 10:13:52 +00:00
README.md feat(storybook): generate Radix UI components for React stories (#971) 2026-10-08 10:13:52 +00:00
tsconfig.json refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
tsdown.config.ts refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00

@open-pencil/emit

How OpenPencil's exporters emit source code. Build JavaScript, TypeScript, and JSX as ESTree nodes and print them with esrap, instead of concatenating strings.

import { jsx } from '@open-pencil/emit'

const card = jsx.element(
  'Card',
  [jsx.attribute('title', jsx.stringValue('Fish & chips'))],
  [jsx.text('Hello')],
  0,
  true
)
jsx.printJSX(card) // <Card title={"Fish & chips"}>Hello</Card>

jsx.stringValue and jsx.text keep a value as plain JSX only when JSX reads it back unchanged; anything with quotes, entities, braces, angle brackets, backslashes, or line breaks becomes a string literal. jsx.printModule prints a whole TSX module, such as an es template filled with JSX, quoting every string the same way; dotted tags such as Switch.Root print as members of a namespace, and jsx.spread passes an object on as attributes.

es parses TypeScript templates, fills $name placeholders, and prints modules:

import { es } from '@open-pencil/emit'

const module = es.fill(es.parseModule("export const title = '$title'"), {
  $title: es.string('Checkout')
})
es.printModule(module) // export const title = 'Checkout';

vue builds templates and single-file components. Text and attribute values are escaped as HTML with entities, and text also writes { as an entity, since Vue reads {{ as an interpolation before it decodes entities:

import { es, vue } from '@open-pencil/emit'

const template = vue.element('SwitchRoot', [vue.attribute('class', 'switch'), vue.model(es.identifier('checked'))])
vue.printTemplate(template) // <SwitchRoot class="switch" v-model="checked" />