diff --git a/AGENTS.md b/AGENTS.md index 44598d0e8..3b06a90e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0772048b6..40259e255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 2b832d56d..74ae59272 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 913134838..fea92bbfd 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -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>, + 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) } } }) diff --git a/packages/cli/src/commands/dom.ts b/packages/cli/src/commands/import.ts similarity index 75% rename from packages/cli/src/commands/dom.ts rename to packages/cli/src/commands/import.ts index bfce145f4..67884a076 100644 --- a/packages/cli/src/commands/dom.ts +++ b/packages/cli/src/commands/import.ts @@ -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 { return Bun.file(requireFile(path)).text() } -async function cssTextForArgs(args: DomArgs): Promise { +async function cssTextForArgs(args: ImportArgs): Promise { 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 { +async function tailwindCandidatesForArgs(args: ImportArgs): Promise { 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> ) { @@ -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 } ]) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0e90daf7a..a960f1c8e 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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, diff --git a/packages/docs/programmable/cli/exporting.md b/packages/docs/programmable/cli/exporting.md index ca71b2024..b089f05a9 100644 --- a/packages/docs/programmable/cli/exporting.md +++ b/packages/docs/programmable/cli/exporting.md @@ -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 diff --git a/packages/docs/reference/cli.md b/packages/docs/reference/cli.md index 0646e0936..dca3812c6 100644 --- a/packages/docs/reference/cli.md +++ b/packages/docs/reference/cli.md @@ -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: `.`) | | `--scale` | `-s` | Export scale (default: 1) | | `--quality` | `-q` | Quality 0–100, 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: `.`) | | `--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 diff --git a/tests/engine/cli/export.test.ts b/tests/engine/cli/export.test.ts new file mode 100644 index 000000000..7c1be285e --- /dev/null +++ b/tests/engine/cli/export.test.ts @@ -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=') +}) diff --git a/tests/engine/cli/dom.test.ts b/tests/engine/cli/import.test.ts similarity index 61% rename from tests/engine/cli/dom.test.ts rename to tests/engine/cli/import.test.ts index 12c777749..080027d0b 100644 --- a/tests/engine/cli/dom.test.ts +++ b/tests/engine/cli/import.test.ts @@ -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 { - 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, 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 () = ` ) - 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, - '
Tailwind HTML
' - ) - - 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 () => { `

Tailwind card

` ) - const { stdout, stderr, exitCode } = await run([ - 'dom', + const { stdout, stderr, exitCode } = await runOpenPencilCLI([ + 'import', htmlPath, '--tailwind', classes.join(' '), diff --git a/tests/helpers/cli.ts b/tests/helpers/cli.ts new file mode 100644 index 000000000..e7b4ff4ec --- /dev/null +++ b/tests/helpers/cli.ts @@ -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 { + 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 } +}