refactor(cli): replace DOM command with import and HTML export

This commit is contained in:
Danila Poyarkov 2026-07-04 13:34:24 +03:00
parent 13ef7ac941
commit 8b1c452cc3
11 changed files with 217 additions and 152 deletions

View file

@ -57,7 +57,7 @@ The app editor session (`src/app/editor/session/create.ts`) is a Vue wrapper aro
- `bun run test:unit` — engine/unit tests
- `bun run test` — Playwright E2E and visual regression tests
- `bun run tauri dev` — desktop app with hot reload
- `bun open-pencil --help` — list CLI commands. Common commands include `info`, `tree`, `find`, `node`, `pages`, `variables`, `export`, `convert`, `lint`, `query`, `selection`, `formats`, `analyze ...`, and `eval` for Figma Plugin API scripting.
- `bun open-pencil --help` — list CLI commands. Common commands include `info`, `tree`, `find`, `node`, `pages`, `variables`, `export`, `import`, `convert`, `lint`, `query`, `selection`, `formats`, `analyze ...`, and `eval` for Figma Plugin API scripting.
## Releases & CI

View file

@ -6,7 +6,7 @@
- Add Figma-style page management in the Pages panel, including rename/delete actions and drag-and-drop page reordering.
- Add DOM/CSS import and authoring support so HTML, CSS, Tailwind, and JSX can be converted into editable OpenPencil documents from the app, CLI, and SDK.
- Add Tailwind class serialization for DOM/CSS HTML export in the SDK.
- Add Tailwind class serialization for DOM/CSS HTML export in the SDK and CLI.
- Add richer Design JSX authoring for components, variables, structured fills, gradients, shadows, and blur effects.
- Add overlap analysis for finding layout collisions and overflowing children from the CLI, AI tools, and MCP.
- Add saved per-node export settings for repeat exports.

View file

@ -79,15 +79,16 @@ openpencil export design.fig # PNG
openpencil export design.fig -f jpg -s 2 -q 90 # JPG at 2x, quality 90
openpencil export design.fig -f fig --page "Page 1" # Export a page as .fig
openpencil export design.fig -f jsx --style tailwind # Tailwind JSX
openpencil export design.fig -f html --style tailwind # Tailwind HTML
openpencil convert design.pen output.fig # Convert between document formats
openpencil dom page.html --css styles.css -o page.fig # HTML/CSS → editable .fig
openpencil import page.html --css styles.css -o page.fig # HTML/CSS → editable .fig
```
DOM/CSS input flows through `@open-pencil/dom-css`, so HTML, authored CSS, and Tailwind utility CSS can become editable OpenPencil layers:
```sh
openpencil dom card.html --css card.css -o card.fig
openpencil dom card.html --tailwind "flex flex-col gap-3 w-80 p-6 rounded-xl bg-white" -o card.fig
openpencil import card.html --css card.css -o card.fig
openpencil import card.html --tailwind "flex flex-col gap-3 w-80 p-6 rounded-xl bg-white" -o card.fig
```
```html

View file

@ -5,6 +5,11 @@ import { defineCommand } from 'citty'
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
import type { RasterExportFormat } from '@open-pencil/core/io'
import {
sceneGraphToDesignDocument,
serializeHTML,
type SerializeHTMLOptions
} from '@open-pencil/dom-css'
import { isAppMode, requireFile, rpc } from '#cli/app-client'
import { appTargetOptions, appTargetRpcArgs } from '#cli/app-target'
@ -13,8 +18,9 @@ import { loadDocument } from '#cli/headless'
const io = new IORegistry(BUILTIN_IO_FORMATS)
const RASTER_FORMATS = ['PNG', 'JPG', 'WEBP']
const ALL_FORMATS = new Set([...RASTER_FORMATS, 'SVG', 'PDF', 'JSX', 'FIG'])
const ALL_FORMATS = new Set([...RASTER_FORMATS, 'SVG', 'PDF', 'JSX', 'FIG', 'HTML'])
const JSX_STYLES = new Set(['openpencil', 'tailwind'])
const HTML_STYLES = new Set(['inline', 'tailwind'])
interface ExportArgs {
file?: string
@ -69,7 +75,7 @@ async function exportViaApp(format: string, args: ExportArgs) {
return
}
if (format === 'JSX' || format === 'FIG') {
if (format === 'JSX' || format === 'HTML' || format === 'FIG') {
printError(`${format} export is only available in file mode right now.`)
process.exit(1)
}
@ -94,6 +100,23 @@ function targetLabel(pageName?: string, nodeId?: string): string {
return pageName ? `page "${pageName}"` : 'first page'
}
type FileExportTarget = { scope: 'node'; nodeId: string } | { scope: 'page'; pageId: string }
async function exportHTMLFromFile(
args: ExportArgs,
graph: Awaited<ReturnType<typeof loadDocument>>,
target: FileExportTarget,
defaultName: string
) {
const document = sceneGraphToDesignDocument(graph, {
rootId: target.scope === 'page' ? target.pageId : target.nodeId
})
const html = serializeHTML(document, { style: args.style as SerializeHTMLOptions['style'] })
const output = resolve(args.output ?? exportFileName(defaultName, 'html'))
await writeAndLog(output, html)
console.log(ok(`Target: ${targetLabel(args.page, args.node)}`))
}
async function exportFromFile(format: string, args: ExportArgs) {
const file = requireFile(args.file)
const graph = await loadDocument(file)
@ -130,6 +153,11 @@ async function exportFromFile(format: string, args: ExportArgs) {
let options:
| { format?: string; scale?: number; quality?: number; renderThumbnail?: boolean }
| undefined
if (format === 'HTML') {
await exportHTMLFromFile(args, graph, target, defaultName)
return
}
if (format === 'JSX') {
options = { format: args.style }
} else if (format === 'FIG') {
@ -156,7 +184,7 @@ async function exportFromFile(format: string, args: ExportArgs) {
}
export default defineCommand({
meta: { description: 'Export a document to PNG, JPG, WEBP, SVG, PDF, JSX, or .fig' },
meta: { description: 'Export a document to PNG, JPG, WEBP, SVG, PDF, JSX, HTML, or .fig' },
args: {
file: {
type: 'positional',
@ -172,7 +200,7 @@ export default defineCommand({
format: {
type: 'string',
alias: 'f',
description: 'Export format: png, jpg, webp, svg, pdf, jsx, fig (default: png)',
description: 'Export format: png, jpg, webp, svg, pdf, jsx, html, fig (default: png)',
default: 'png'
},
scale: { type: 'string', alias: 's', description: 'Export scale (default: 1)', default: '1' },
@ -194,7 +222,8 @@ export default defineCommand({
},
style: {
type: 'string',
description: 'JSX style: openpencil, tailwind (default: openpencil)',
description:
'Code style for JSX/HTML: openpencil or tailwind for JSX; inline or tailwind for HTML (default: openpencil)',
default: 'openpencil'
},
thumbnail: { type: 'boolean', description: 'Export page thumbnail instead of full render' },
@ -203,9 +232,11 @@ export default defineCommand({
...appTargetOptions
},
async run({ args }) {
const format = args.format.toUpperCase() as RasterExportFormat | 'SVG' | 'JSX' | 'FIG'
const format = args.format.toUpperCase() as RasterExportFormat | 'SVG' | 'JSX' | 'FIG' | 'HTML'
if (!ALL_FORMATS.has(format)) {
printError(`Invalid format "${args.format}". Use png, jpg, webp, svg, pdf, jsx, or fig.`)
printError(
`Invalid format "${args.format}". Use png, jpg, webp, svg, pdf, jsx, html, or fig.`
)
process.exit(1)
}
@ -214,10 +245,20 @@ export default defineCommand({
process.exit(1)
}
const normalizedArgs = {
...args,
style: format === 'HTML' && args.style === 'openpencil' ? 'inline' : args.style
}
if (format === 'HTML' && !HTML_STYLES.has(normalizedArgs.style)) {
printError(`Invalid HTML style "${args.style}". Use inline or tailwind.`)
process.exit(1)
}
if (isAppMode(args.file)) {
await exportViaApp(format, args)
await exportViaApp(format, normalizedArgs)
} else {
await exportFromFile(format, args)
await exportFromFile(format, normalizedArgs)
}
}
})

View file

@ -7,21 +7,18 @@ import {
createHeadlessCSSRuntime,
htmlToDesignDocument,
htmlToSceneGraph,
serializeHTML,
tailwindHTMLToDesignDocument,
tailwindHTMLToSceneGraph,
type DesignDocument,
type SerializeHTMLOptions
type DesignDocument
} from '@open-pencil/dom-css'
import { requireFile } from '#cli/app-client'
import { fmtList, ok, printError } from '#cli/format'
const io = new IORegistry(BUILTIN_IO_FORMATS)
const OUTPUT_FORMATS = new Set(['fig', 'html', 'json'])
const HTML_STYLES = new Set(['inline', 'tailwind'])
const OUTPUT_FORMATS = new Set(['fig', 'json'])
interface DomArgs {
interface ImportArgs {
file?: string
output?: string
format: string
@ -30,7 +27,6 @@ interface DomArgs {
tailwind?: string
tailwindFile?: string
pageName: string
htmlStyle: SerializeHTMLOptions['style']
json?: boolean
}
@ -43,14 +39,14 @@ async function readTextFile(path: string): Promise<string> {
return Bun.file(requireFile(path)).text()
}
async function cssTextForArgs(args: DomArgs): Promise<string | undefined> {
async function cssTextForArgs(args: ImportArgs): Promise<string | undefined> {
const cssParts = []
if (args.css) cssParts.push(await readTextFile(args.css))
if (args.cssText) cssParts.push(args.cssText)
return cssParts.length > 0 ? cssParts.join('\n') : undefined
}
async function tailwindCandidatesForArgs(args: DomArgs): Promise<string[] | undefined> {
async function tailwindCandidatesForArgs(args: ImportArgs): Promise<string[] | undefined> {
const parts = []
if (args.tailwind) parts.push(args.tailwind)
if (args.tailwindFile) parts.push(await readTextFile(args.tailwindFile))
@ -64,7 +60,7 @@ function childCount(document: DesignDocument): number {
return document.children.length
}
async function convertDom(args: DomArgs) {
async function importHTML(args: ImportArgs) {
const file = requireFile(args.file)
const html = await readTextFile(file)
const runtime = createHeadlessCSSRuntime()
@ -87,7 +83,7 @@ async function convertDom(args: DomArgs) {
}
async function writeOutput(
args: DomArgs,
args: ImportArgs,
document: DesignDocument,
graph: Awaited<ReturnType<typeof htmlToSceneGraph>>
) {
@ -99,18 +95,13 @@ async function writeOutput(
return output
}
if (format === 'html') {
await Bun.write(output, serializeHTML(document, { style: args.htmlStyle }))
return output
}
const result = await io.writeDocument('fig', graph)
await Bun.write(output, result.data as Uint8Array)
return output
}
export default defineCommand({
meta: { description: 'Convert HTML/CSS/Tailwind into an OpenPencil document' },
meta: { description: 'Import HTML/CSS/Tailwind into an OpenPencil document' },
args: {
file: {
type: 'positional',
@ -126,7 +117,7 @@ export default defineCommand({
format: {
type: 'string',
alias: 'f',
description: 'Output format: fig, html, json (default: fig)',
description: 'Output format: fig or json (default: fig)',
default: 'fig'
},
css: {
@ -154,11 +145,6 @@ export default defineCommand({
description: 'Scene graph page name (default: DOM/CSS)',
default: 'DOM/CSS'
},
htmlStyle: {
type: 'string',
description: 'HTML style output: inline or tailwind (default: inline)',
default: 'inline'
},
json: {
type: 'boolean',
description: 'Print a machine-readable summary to stdout'
@ -167,19 +153,12 @@ export default defineCommand({
async run({ args }) {
const format = args.format.toLowerCase()
if (!OUTPUT_FORMATS.has(format)) {
printError(`Invalid format "${args.format}". Use fig, html, or json.`)
printError(`Invalid format "${args.format}". Use fig or json.`)
process.exit(1)
}
const htmlStyleArg = args.htmlStyle.toLowerCase()
if (!HTML_STYLES.has(htmlStyleArg)) {
printError(`Invalid htmlStyle "${args.htmlStyle}". Use inline or tailwind.`)
process.exit(1)
}
const htmlStyle = htmlStyleArg as SerializeHTMLOptions['style']
const normalizedArgs = { ...args, htmlStyle }
const { document, graph } = await convertDom(normalizedArgs)
const output = await writeOutput(normalizedArgs, document, graph)
const { document, graph } = await importHTML(args)
const output = await writeOutput(args, document, graph)
const pages = graph.getPages()
const summary = {
input: requireFile(args.file),
@ -198,7 +177,7 @@ export default defineCommand({
console.log(
fmtList([
{
header: 'DOM/CSS conversion',
header: 'HTML/CSS import',
details: summary
}
])

View file

@ -4,11 +4,11 @@ import { defineCommand, runMain } from 'citty'
import analyze from './commands/analyze'
import convert from './commands/convert'
import documents from './commands/documents'
import dom from './commands/dom'
import evalCmd from './commands/eval'
import exportCmd from './commands/export'
import find from './commands/find'
import formats from './commands/formats'
import importCmd from './commands/import'
import info from './commands/info'
import lint from './commands/lint'
import node from './commands/node'
@ -30,9 +30,9 @@ const main = defineCommand({
analyze,
convert,
documents,
dom,
eval: evalCmd,
export: exportCmd,
import: importCmd,
find,
formats,
info,

View file

@ -1,11 +1,11 @@
---
title: Exporting
description: Export document content to PNG, JPG, WEBP, SVG, `.fig`, or JSX, and convert between document formats.
description: Export document content to PNG, JPG, WEBP, SVG, `.fig`, JSX, or HTML, and convert between document formats.
---
# Exporting
Export designs from the terminal — raster images, vectors, `.fig` subsets, or JSX code.
Export designs from the terminal — raster images, vectors, `.fig` subsets, JSX code, or HTML.
## Image Export
@ -16,11 +16,12 @@ openpencil export design.fig -f webp -s 3 # WEBP at 3×
openpencil export design.fig -f svg # SVG vector
openpencil export design.fig -f fig --page "Page 1" # export one page as .fig
openpencil export design.fig -f fig --node 1:23 # export one node as .fig
openpencil export design.fig -f html --style tailwind # export HTML with Tailwind classes
```
Options:
- `-f` — format: `png`, `jpg`, `webp`, `svg`, `jsx`
- `-f` — format: `png`, `jpg`, `webp`, `svg`, `jsx`, `html`, `fig`
- `-s` — scale: `1``4`
- `-q` — quality: `0``100` (JPG/WEBP only)
- `-o` — output path
@ -46,6 +47,17 @@ Output:
Also supports `--style openpencil` for the native JSX format (see [JSX Renderer](../jsx-renderer)).
## HTML Export
Export as HTML with inline styles by default, or Tailwind utility classes:
```sh
openpencil export design.fig -f html
openpencil export design.fig -f html --style tailwind
```
HTML export is available in file mode.
## Thumbnails
```sh

View file

@ -90,7 +90,7 @@ openpencil variables [file] [options]
## export
Export to PNG, JPG, WEBP, SVG, or JSX.
Export to PNG, JPG, WEBP, SVG, JSX, HTML, or `.fig`.
```sh
openpencil export [file] [options]
@ -98,28 +98,28 @@ openpencil export [file] [options]
| Option | Alias | Description |
|--------|-------|-------------|
| `--format` | `-f` | `png` (default), `jpg`, `webp`, `svg`, `jsx` |
| `--format` | `-f` | `png` (default), `jpg`, `webp`, `svg`, `jsx`, `html`, `fig` |
| `--output` | `-o` | Output file path (default: `<name>.<format>`) |
| `--scale` | `-s` | Export scale (default: 1) |
| `--quality` | `-q` | Quality 0100, JPG/WEBP only (default: 90) |
| `--page` | | Page name (default: first page) |
| `--node` | | Node ID to export (default: all top-level nodes) |
| `--style` | | JSX style: `openpencil` (default), `tailwind` |
| `--style` | | JSX style: `openpencil` (default), `tailwind`; HTML style: `inline`, `tailwind` |
| `--thumbnail` | | Export page thumbnail instead of full render |
| `--width` | | Thumbnail width (default: 1920) |
| `--height` | | Thumbnail height (default: 1080) |
## dom
## import
Convert HTML/CSS/Tailwind into an editable OpenPencil document.
Import HTML/CSS/Tailwind into an editable OpenPencil document.
```sh
openpencil dom page.html [options]
openpencil import page.html [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--format` | `-f` | Output format: `fig` (default), `html`, `json` |
| `--format` | `-f` | Output format: `fig` (default), `json` |
| `--output` | `-o` | Output file path (default: `<name>.<format>`) |
| `--css` | | CSS file to apply before conversion |
| `--css-text` | | Inline CSS text to apply before conversion |
@ -131,8 +131,8 @@ openpencil dom page.html [options]
Examples:
```sh
openpencil dom card.html --css card.css -o card.fig
openpencil dom card.html --tailwind "flex flex-col gap-3 w-80 p-6 rounded-xl bg-white" -o card.fig
openpencil import card.html --css card.css -o card.fig
openpencil import card.html --tailwind "flex flex-col gap-3 w-80 p-6 rounded-xl bg-white" -o card.fig
```
## eval

View file

@ -0,0 +1,84 @@
import { expect, setDefaultTimeout, test } from 'bun:test'
import { mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
import { runOpenPencilCLI } from '#tests/helpers/cli'
import { createRect, firstPageId, makeSceneGraph } from '#tests/helpers/scene'
setDefaultTimeout(30_000)
const io = new IORegistry(BUILTIN_IO_FORMATS)
async function createFigFixture() {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-export-cli-'))
const figPath = join(dir, 'card.fig')
const graph = makeSceneGraph('Export Page')
const rect = createRect(graph, firstPageId(graph), {
name: 'Export Card',
x: 0,
y: 0,
width: 160,
height: 80
})
rect.layoutMode = 'HORIZONTAL'
rect.itemSpacing = 8
rect.paddingLeft = 16
rect.paddingRight = 16
rect.paddingTop = 16
rect.paddingBottom = 16
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 } }]
const result = await io.writeDocument('fig', graph)
await Bun.write(figPath, result.data as Uint8Array)
return { dir, figPath }
}
test('export CLI writes HTML with inline styles by default', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('data-open-pencil-node-id')
expect(html).toContain('style=')
expect(html).toContain('display: flex')
})
test('export CLI can write HTML styles as Tailwind classes', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card-tailwind.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--style',
'tailwind',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('data-open-pencil-node-id')
expect(html).toContain('class="')
expect(html).toContain('flex')
expect(html).not.toContain('style=')
})

View file

@ -6,33 +6,12 @@ import { join } from 'node:path'
import { parseFigFile } from '@open-pencil/core/io'
import type { SceneNode } from '@open-pencil/scene-graph'
import { cliSourcePath } from '#tests/helpers/paths'
import { runOpenPencilCLI } from '#tests/helpers/cli'
setDefaultTimeout(30_000)
const CLI = cliSourcePath('index.ts')
interface CommandResult {
stdout: string
stderr: string
exitCode: number
}
async function run(args: string[]): Promise<CommandResult> {
const proc = Bun.spawn(['bun', CLI, ...args], {
stdout: 'pipe',
stderr: 'pipe'
})
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text()
])
const exitCode = await proc.exited
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
}
async function createFixture() {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-dom-cli-'))
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-import-cli-'))
const htmlPath = join(dir, 'card.html')
const cssPath = join(dir, 'card.css')
@ -73,12 +52,12 @@ function findNode(nodes: Iterable<SceneNode>, name: string): SceneNode | undefin
}
}
test('dom CLI writes DesignDOM JSON output', async () => {
test('import CLI writes DesignDOM JSON output', async () => {
const { htmlPath, cssPath, dir } = await createFixture()
const output = join(dir, 'card.json')
const { stdout, stderr, exitCode } = await run([
'dom',
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
'import',
htmlPath,
'--css',
cssPath,
@ -101,8 +80,8 @@ test('dom CLI writes DesignDOM JSON output', async () => {
expect(document.children[0].computedStyle.width).toBe('240px')
})
test('dom CLI reads embedded HTML styles without a sidecar CSS file', async () => {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-dom-cli-embedded-'))
test('import CLI reads embedded HTML styles without a sidecar CSS file', async () => {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-import-cli-embedded-'))
const htmlPath = join(dir, 'embedded.html')
const output = join(dir, 'embedded.json')
@ -119,8 +98,8 @@ test('dom CLI reads embedded HTML styles without a sidecar CSS file', async () =
</html>`
)
const { stdout, stderr, exitCode } = await run([
'dom',
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
'import',
htmlPath,
'--format',
'json',
@ -139,65 +118,12 @@ test('dom CLI reads embedded HTML styles without a sidecar CSS file', async () =
expect(document.children[0].computedStyle.gap).toBe('10px')
})
test('dom CLI writes serialized HTML output', async () => {
const { htmlPath, cssPath, dir } = await createFixture()
const output = join(dir, 'card.out.html')
const { stderr, exitCode } = await run([
'dom',
htmlPath,
'--css',
cssPath,
'--format',
'html',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('DOM/CSS card')
expect(html).toContain('class="card"')
})
test('dom CLI can write HTML styles as Tailwind classes', async () => {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-dom-cli-html-tailwind-'))
const htmlPath = join(dir, 'card.html')
const output = join(dir, 'card.out.html')
await Bun.write(
htmlPath,
'<section class="card" style="display:flex;padding:16px;gap:8px;background-color:white">Tailwind HTML</section>'
)
const { stderr, exitCode } = await run([
'dom',
htmlPath,
'--format',
'html',
'--htmlStyle',
'tailwind',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('Tailwind HTML')
expect(html).toContain('class="card flex p-4 gap-2 bg-white"')
expect(html).not.toContain('style=')
})
test('dom CLI writes a .fig that core IO can import', async () => {
test('import CLI writes a .fig that core IO can import', async () => {
const { htmlPath, cssPath, dir } = await createFixture()
const output = join(dir, 'card.fig')
const { stdout, stderr, exitCode } = await run([
'dom',
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
'import',
htmlPath,
'--css',
cssPath,
@ -221,8 +147,8 @@ test('dom CLI writes a .fig that core IO can import', async () => {
expect(title?.type).toBe('TEXT')
})
test('dom CLI compiles Tailwind candidates before import', async () => {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-dom-cli-tailwind-'))
test('import CLI compiles Tailwind candidates before import', async () => {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-import-cli-tailwind-'))
const htmlPath = join(dir, 'tailwind.html')
const output = join(dir, 'tailwind.json')
const classes = ['flex', 'flex-col', 'gap-2', 'w-60', 'p-6', 'rounded-xl', 'bg-white']
@ -232,8 +158,8 @@ test('dom CLI compiles Tailwind candidates before import', async () => {
`<article class="${classes.join(' ')}"><h1 class="text-2xl">Tailwind card</h1></article>`
)
const { stdout, stderr, exitCode } = await run([
'dom',
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
'import',
htmlPath,
'--tailwind',
classes.join(' '),

22
tests/helpers/cli.ts Normal file
View file

@ -0,0 +1,22 @@
import { cliSourcePath } from './paths'
const CLI = cliSourcePath('index.ts')
export interface CLICommandResult {
stdout: string
stderr: string
exitCode: number
}
export async function runOpenPencilCLI(args: string[]): Promise<CLICommandResult> {
const proc = Bun.spawn(['bun', CLI, ...args], {
stdout: 'pipe',
stderr: 'pipe'
})
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text()
])
const exitCode = await proc.exited
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
}