refactor(cli): replace DOM command with import and HTML export
This commit is contained in:
parent
13ef7ac941
commit
8b1c452cc3
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
])
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 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: `<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
|
||||
|
|
|
|||
84
tests/engine/cli/export.test.ts
Normal file
84
tests/engine/cli/export.test.ts
Normal 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=')
|
||||
})
|
||||
|
|
@ -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
22
tests/helpers/cli.ts
Normal 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 }
|
||||
}
|
||||
Loading…
Reference in a new issue