feat(dom-css): add DOM/CSS package skeleton
This commit is contained in:
parent
8983c11f45
commit
4d61c002fa
|
|
@ -12,6 +12,7 @@ Bun workspace with three packages:
|
|||
- `packages/cli` — `@open-pencil/cli`: headless CLI for .fig inspection, export, linting. Uses `citty` + `agentfmt`.
|
||||
- `packages/docs` — `@open-pencil/docs`: VitePress documentation site. Run with `cd packages/docs && bun run dev`.
|
||||
- `packages/mcp` — `@open-pencil/mcp`: MCP server for AI coding tools. Stdio + HTTP (Hono). Reuses `createServer()` factory with all core tools.
|
||||
- `packages/dom-css` — `@open-pencil/dom-css`: DOM/CSS projection layer for HTML/CSS/JSX/Tailwind compatibility. Owns DesignDOM types and browser/headless CSS runtime adapters; depends on core scene-graph types but keeps DOM/CSS parser dependencies out of core.
|
||||
|
||||
- `packages/vue` — `@open-pencil/vue`: headless Vue 3 SDK (Reka UI-style) for building custom OpenPencil-powered editor shells and embedded editing surfaces. Renderless components and composables. The app is one consumer of the SDK.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
- Add design JSX variable helpers so color props can use `designVar()` / `defineVars()` references and emit graph variable bindings.
|
||||
- Add structured design JSX paint helpers for solid fills, multiple fills, and gradients.
|
||||
- Add structured design JSX effect helpers for shadows and blur effects.
|
||||
- Add the `@open-pencil/dom-css` package skeleton for DOM/CSS projection and browser/headless CSS runtime adapters.
|
||||
- Add type-validated `bindVariable`/`unbindVariable` with event emission and indexed binding format (`fills/N/color` instead of `fills[N]`).
|
||||
- Add `unbind_variable` MCP tool for removing variable bindings.
|
||||
- Add `openpencil analyze overlaps`, the `analyze_overlaps` RPC command, and the `analyze_overlaps` ToolDef for heuristic overlap detection. The command reports sibling overlaps, children overflowing non-clipping parents, and overlay/backdrop patterns, with filters for page/page ID, scope, category, severity, min area/ratio, node type, hidden/locked/absolute nodes, result limit, and `--json` output.
|
||||
|
|
|
|||
14
bun.lock
14
bun.lock
|
|
@ -16,6 +16,7 @@
|
|||
"@chenglou/pretext": "^0.0.7",
|
||||
"@open-pencil/cli": "workspace:*",
|
||||
"@open-pencil/core": "workspace:*",
|
||||
"@open-pencil/dom-css": "workspace:*",
|
||||
"@open-pencil/vue": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
|
|
@ -155,6 +156,17 @@
|
|||
"vitepress-plugin-llms": "1.12.2",
|
||||
},
|
||||
},
|
||||
"packages/dom-css": {
|
||||
"name": "@open-pencil/dom-css",
|
||||
"version": "0.13.2",
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.21.7",
|
||||
"typescript": "~5.8.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@open-pencil/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/mcp": {
|
||||
"name": "@open-pencil/mcp",
|
||||
"version": "0.13.2",
|
||||
|
|
@ -760,6 +772,8 @@
|
|||
|
||||
"@open-pencil/docs": ["@open-pencil/docs@workspace:packages/docs"],
|
||||
|
||||
"@open-pencil/dom-css": ["@open-pencil/dom-css@workspace:packages/dom-css"],
|
||||
|
||||
"@open-pencil/mcp": ["@open-pencil/mcp@workspace:packages/mcp"],
|
||||
|
||||
"@open-pencil/vue": ["@open-pencil/vue@workspace:packages/vue"],
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -12,9 +12,9 @@
|
|||
"build": "bun run build:packages && bun run lint && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"lint": "bun run lint:structure && oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/",
|
||||
"lint:structure": "oxlint -c oxlint.json vite.config.ts vite/ src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ tests/ scripts/ tools/",
|
||||
"format": "oxfmt --write .oxfmtrc.json vite.config.ts vite/ src/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/vue/src/ tests scripts/ tools/",
|
||||
"lint": "bun run lint:structure && oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ packages/dom-css/src/",
|
||||
"lint:structure": "oxlint -c oxlint.json vite.config.ts vite/ src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ packages/dom-css/src/ tests/ scripts/ tools/",
|
||||
"format": "oxfmt --write .oxfmtrc.json vite.config.ts vite/ src/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/vue/src/ packages/dom-css/src/ tests scripts/ tools/",
|
||||
"format:check": "bun run format && status=$(git status --porcelain -uall) && test -z \"$status\" || (echo \"$status\" && exit 1)",
|
||||
"check": "bun run build:packages && bun run lint && tsgo --noEmit && bun run check:vue && bun run check:i18n && bun run check:packages && bun run check:arch && bun run test:type-shapes && bun run test:tools && bun run test:dupes",
|
||||
"check:i18n": "bun tools/i18n/src/check-locales.ts",
|
||||
|
|
@ -29,9 +29,9 @@
|
|||
"test:coverage": "bun test --coverage ./tests/engine",
|
||||
"test:type-shapes": "bun tools/type-shapes/src/index.ts",
|
||||
"test:tools": "bun tools/test.ts",
|
||||
"test:dupes": "jscpd packages/core/src packages/cli/src src --min-lines 5 --min-tokens 50 --format typescript --threshold 0",
|
||||
"test:dupes": "jscpd packages/core/src packages/cli/src packages/dom-css/src src --min-lines 5 --min-tokens 50 --format typescript --threshold 0",
|
||||
"test:packages": "bun tools/package-quality/src/check-metadata.ts && bun tools/package-quality/src/smoke.ts",
|
||||
"build:packages": "bun --filter @open-pencil/core build && bun --filter @open-pencil/vue build && bun --filter @open-pencil/mcp build && bun --filter @open-pencil/cli build",
|
||||
"build:packages": "bun --filter @open-pencil/core build && bun --filter @open-pencil/dom-css build && bun --filter @open-pencil/vue build && bun --filter @open-pencil/mcp build && bun --filter @open-pencil/cli build",
|
||||
"open-pencil": "bun packages/cli/src/index.ts",
|
||||
"docs:dev": "bun --filter @open-pencil/docs dev",
|
||||
"docs:build": "bun --filter @open-pencil/docs build",
|
||||
|
|
@ -51,6 +51,7 @@
|
|||
"@chenglou/pretext": "^0.0.7",
|
||||
"@open-pencil/cli": "workspace:*",
|
||||
"@open-pencil/core": "workspace:*",
|
||||
"@open-pencil/dom-css": "workspace:*",
|
||||
"@open-pencil/vue": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
|
|
|
|||
17
packages/dom-css/README.md
Normal file
17
packages/dom-css/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# @open-pencil/dom-css
|
||||
|
||||
DOM and CSS projection utilities for OpenPencil.
|
||||
|
||||
This package is the compatibility layer between OpenPencil's scene graph and DOM-shaped design documents. It is intentionally separate from `@open-pencil/core` so browser/CSS parser integrations can evolve without adding DOM dependencies to the renderer and editor core.
|
||||
|
||||
Current scope:
|
||||
|
||||
- DOM-shaped `DesignDocument` / `DesignElement` types
|
||||
- Browser-backed runtime adapter for native HTML parsing, serialization, and computed-style extraction
|
||||
- Headless runtime placeholder for future `parse5` / CSSOM-backed execution
|
||||
|
||||
Planned scope:
|
||||
|
||||
- SceneGraph ⇄ DesignDOM conversion
|
||||
- CSSOM and cascade support in headless contexts
|
||||
- Tailwind-generated CSS ingestion
|
||||
48
packages/dom-css/package.json
Normal file
48
packages/dom-css/package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@open-pencil/dom-css",
|
||||
"version": "0.13.2",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#dom-css/*": {
|
||||
"types": "./src/*",
|
||||
"bun": "./src/*",
|
||||
"default": "./dist/*"
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"bun": "./src/index.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bunx tsdown --config tsdown.config.ts",
|
||||
"prepack": "bun run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/open-pencil/open-pencil.git",
|
||||
"directory": "packages/dom-css"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"provenance": true
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@open-pencil/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.21.7",
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
}
|
||||
12
packages/dom-css/src/index.ts
Normal file
12
packages/dom-css/src/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export { serializeHTML, serializeNode } from './serialize'
|
||||
export { createBrowserCssRuntime, createCssRuntime, createHeadlessCssRuntime } from './runtime'
|
||||
export type {
|
||||
CssComputeOptions,
|
||||
CssRuntime,
|
||||
DesignDocument,
|
||||
DesignElement,
|
||||
DesignNode,
|
||||
DesignStyleDeclaration,
|
||||
DesignStyleSheet,
|
||||
DesignText
|
||||
} from './types'
|
||||
201
packages/dom-css/src/runtime/browser.ts
Normal file
201
packages/dom-css/src/runtime/browser.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import { serializeHTML } from '../serialize'
|
||||
import type {
|
||||
CssComputeOptions,
|
||||
CssRuntime,
|
||||
DesignDocument,
|
||||
DesignElement,
|
||||
DesignNode
|
||||
} from '../types'
|
||||
|
||||
const DEFAULT_COMPUTED_PROPERTIES = [
|
||||
'align-items',
|
||||
'background-color',
|
||||
'background-image',
|
||||
'border-bottom-color',
|
||||
'border-bottom-left-radius',
|
||||
'border-bottom-right-radius',
|
||||
'border-bottom-width',
|
||||
'border-left-color',
|
||||
'border-left-width',
|
||||
'border-radius',
|
||||
'border-right-color',
|
||||
'border-right-width',
|
||||
'border-top-color',
|
||||
'border-top-left-radius',
|
||||
'border-top-right-radius',
|
||||
'border-top-width',
|
||||
'box-shadow',
|
||||
'color',
|
||||
'display',
|
||||
'flex-direction',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-style',
|
||||
'font-weight',
|
||||
'gap',
|
||||
'height',
|
||||
'justify-content',
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'opacity',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'padding-right',
|
||||
'padding-top',
|
||||
'text-align',
|
||||
'text-decoration-line',
|
||||
'width'
|
||||
] as const
|
||||
|
||||
function requireBrowserDocument(): Document {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new TypeError('Browser CSS runtime requires a DOM document')
|
||||
}
|
||||
return document
|
||||
}
|
||||
|
||||
function attributesToRecord(element: Element): Record<string, string> {
|
||||
const attrs: Record<string, string> = {}
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
attrs[attr.name] = attr.value
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
function styleToRecord(style: CSSStyleDeclaration): Record<string, string> | undefined {
|
||||
const entries: Record<string, string> = {}
|
||||
for (const property of Array.from(style)) {
|
||||
const value = style.getPropertyValue(property)
|
||||
if (value) entries[property] = value
|
||||
}
|
||||
return Object.keys(entries).length > 0 ? entries : undefined
|
||||
}
|
||||
|
||||
function domNodeToDesignNode(node: Node): DesignNode | null {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent ?? ''
|
||||
return text.length > 0 ? { type: 'text', text } : null
|
||||
}
|
||||
|
||||
if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof Element)) return null
|
||||
|
||||
const children = Array.from(node.childNodes)
|
||||
.map(domNodeToDesignNode)
|
||||
.filter((child): child is DesignNode => child !== null)
|
||||
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: node.tagName.toLowerCase(),
|
||||
attrs: attributesToRecord(node),
|
||||
children,
|
||||
inlineStyle: node instanceof HTMLElement ? styleToRecord(node.style) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
function parseHTML(html: string): DesignDocument {
|
||||
requireBrowserDocument()
|
||||
const parsed = new DOMParser().parseFromString(html, 'text/html')
|
||||
return {
|
||||
type: 'document',
|
||||
children: Array.from(parsed.body.childNodes)
|
||||
.map(domNodeToDesignNode)
|
||||
.filter((node): node is DesignNode => node !== null)
|
||||
}
|
||||
}
|
||||
|
||||
function collectElementPairs(
|
||||
designNode: DesignNode,
|
||||
domNode: Node,
|
||||
pairs: [DesignElement, Element][]
|
||||
): void {
|
||||
if (designNode.type === 'text') return
|
||||
if (!(domNode instanceof Element)) return
|
||||
|
||||
pairs.push([designNode, domNode])
|
||||
|
||||
const elementChildren = designNode.children.filter(
|
||||
(child): child is DesignElement => child.type === 'element'
|
||||
)
|
||||
const domChildren = Array.from(domNode.children)
|
||||
for (const [index, child] of elementChildren.entries()) {
|
||||
const domChild = domChildren.at(index)
|
||||
if (domChild) collectElementPairs(child, domChild, pairs)
|
||||
}
|
||||
}
|
||||
|
||||
function computedStyleToRecord(
|
||||
style: CSSStyleDeclaration,
|
||||
options: CssComputeOptions
|
||||
): Record<string, string> {
|
||||
const entries: Record<string, string> = {}
|
||||
const properties = options.includeBrowserDefaults
|
||||
? Array.from(style)
|
||||
: DEFAULT_COMPUTED_PROPERTIES
|
||||
|
||||
for (const property of properties) {
|
||||
const value = style.getPropertyValue(property)
|
||||
if (value) entries[property] = value
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
async function computeStyles(
|
||||
designDocument: DesignDocument,
|
||||
cssText = '',
|
||||
options: CssComputeOptions = {}
|
||||
): Promise<DesignDocument> {
|
||||
const browserDocument = requireBrowserDocument()
|
||||
const host = browserDocument.createElement('div')
|
||||
host.style.cssText = [
|
||||
'position: fixed',
|
||||
'left: -100000px',
|
||||
'top: 0',
|
||||
'width: 1000px',
|
||||
'height: auto',
|
||||
'visibility: hidden',
|
||||
'pointer-events: none',
|
||||
'contain: layout style paint'
|
||||
].join(';')
|
||||
|
||||
const shadow = host.attachShadow({ mode: 'open' })
|
||||
const style = browserDocument.createElement('style')
|
||||
style.textContent = cssText
|
||||
shadow.append(style)
|
||||
|
||||
const content = browserDocument.createElement('div')
|
||||
content.innerHTML = serializeHTML(designDocument)
|
||||
shadow.append(content)
|
||||
browserDocument.body.append(host)
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => resolve())
|
||||
})
|
||||
|
||||
const nextDocument = structuredClone(designDocument)
|
||||
const pairs: [DesignElement, Element][] = []
|
||||
const domChildren = Array.from(content.childNodes)
|
||||
for (const [index, child] of nextDocument.children.entries()) {
|
||||
const domChild = domChildren.at(index)
|
||||
if (domChild) collectElementPairs(child, domChild, pairs)
|
||||
}
|
||||
|
||||
for (const [designElement, domElement] of pairs) {
|
||||
designElement.computedStyle = computedStyleToRecord(getComputedStyle(domElement), options)
|
||||
}
|
||||
|
||||
return nextDocument
|
||||
} finally {
|
||||
host.remove()
|
||||
}
|
||||
}
|
||||
|
||||
export function createBrowserCssRuntime(): CssRuntime {
|
||||
return {
|
||||
kind: 'browser',
|
||||
parseHTML,
|
||||
serializeHTML,
|
||||
computeStyles
|
||||
}
|
||||
}
|
||||
23
packages/dom-css/src/runtime/headless.ts
Normal file
23
packages/dom-css/src/runtime/headless.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { serializeHTML } from '../serialize'
|
||||
import type { CssRuntime, DesignDocument } from '../types'
|
||||
|
||||
function unavailable(feature: string): never {
|
||||
throw new Error(
|
||||
`${feature} requires a headless DOM/CSS adapter. Planned dependencies: parse5 + CSSOM.`
|
||||
)
|
||||
}
|
||||
|
||||
export function createHeadlessCssRuntime(): CssRuntime {
|
||||
return {
|
||||
kind: 'headless',
|
||||
parseHTML() {
|
||||
return unavailable('parseHTML')
|
||||
},
|
||||
serializeHTML(document: DesignDocument) {
|
||||
return serializeHTML(document)
|
||||
},
|
||||
computeStyles() {
|
||||
return unavailable('computeStyles')
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/dom-css/src/runtime/index.ts
Normal file
10
packages/dom-css/src/runtime/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { CssRuntime } from '../types'
|
||||
import { createBrowserCssRuntime } from './browser'
|
||||
import { createHeadlessCssRuntime } from './headless'
|
||||
|
||||
export { createBrowserCssRuntime } from './browser'
|
||||
export { createHeadlessCssRuntime } from './headless'
|
||||
|
||||
export function createCssRuntime(): CssRuntime {
|
||||
return typeof document !== 'undefined' ? createBrowserCssRuntime() : createHeadlessCssRuntime()
|
||||
}
|
||||
54
packages/dom-css/src/serialize.ts
Normal file
54
packages/dom-css/src/serialize.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { DesignDocument, DesignElement, DesignNode, DesignText } from './types'
|
||||
|
||||
const VOID_ELEMENTS = new Set([
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr'
|
||||
])
|
||||
|
||||
function escapeText(value: string): string {
|
||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
}
|
||||
|
||||
function escapeAttr(value: string): string {
|
||||
return escapeText(value).replaceAll('"', '"')
|
||||
}
|
||||
|
||||
function serializeText(node: DesignText): string {
|
||||
return escapeText(node.text)
|
||||
}
|
||||
|
||||
function serializeAttrs(node: DesignElement): string {
|
||||
const attrs = Object.entries(node.attrs)
|
||||
.filter(([, value]) => value !== '')
|
||||
.map(([name, value]) => `${name}="${escapeAttr(value)}"`)
|
||||
|
||||
if (attrs.length === 0) return ''
|
||||
return ` ${attrs.join(' ')}`
|
||||
}
|
||||
|
||||
function serializeElement(node: DesignElement): string {
|
||||
const tagName = node.tagName.toLowerCase()
|
||||
const attrs = serializeAttrs(node)
|
||||
if (VOID_ELEMENTS.has(tagName)) return `<${tagName}${attrs}>`
|
||||
return `<${tagName}${attrs}>${node.children.map(serializeNode).join('')}</${tagName}>`
|
||||
}
|
||||
|
||||
export function serializeNode(node: DesignNode): string {
|
||||
return node.type === 'text' ? serializeText(node) : serializeElement(node)
|
||||
}
|
||||
|
||||
export function serializeHTML(document: DesignDocument): string {
|
||||
return document.children.map(serializeNode).join('')
|
||||
}
|
||||
49
packages/dom-css/src/types.ts
Normal file
49
packages/dom-css/src/types.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { SceneGraph, SceneNode } from '@open-pencil/core/scene-graph'
|
||||
|
||||
export type DesignNode = DesignElement | DesignText
|
||||
|
||||
export interface DesignDocument {
|
||||
type: 'document'
|
||||
children: DesignNode[]
|
||||
stylesheets?: DesignStyleSheet[]
|
||||
sourceGraph?: SceneGraph
|
||||
}
|
||||
|
||||
export interface DesignElement {
|
||||
type: 'element'
|
||||
tagName: string
|
||||
attrs: Record<string, string>
|
||||
children: DesignNode[]
|
||||
inlineStyle?: DesignStyleDeclaration
|
||||
computedStyle?: DesignStyleDeclaration
|
||||
sourceSceneNodeId?: string
|
||||
sourceSceneNode?: SceneNode
|
||||
}
|
||||
|
||||
export interface DesignText {
|
||||
type: 'text'
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface DesignStyleSheet {
|
||||
type: 'stylesheet'
|
||||
cssText: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
export type DesignStyleDeclaration = Record<string, string>
|
||||
|
||||
export interface CssComputeOptions {
|
||||
includeBrowserDefaults?: boolean
|
||||
}
|
||||
|
||||
export interface CssRuntime {
|
||||
readonly kind: 'browser' | 'headless'
|
||||
parseHTML(html: string): DesignDocument
|
||||
serializeHTML(document: DesignDocument): string
|
||||
computeStyles(
|
||||
document: DesignDocument,
|
||||
cssText?: string,
|
||||
options?: CssComputeOptions
|
||||
): Promise<DesignDocument>
|
||||
}
|
||||
24
packages/dom-css/tsconfig.json
Normal file
24
packages/dom-css/tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist",
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"#dom-css/*": ["./src/*"],
|
||||
"#core/*": ["../core/src/*"],
|
||||
"@open-pencil/core": ["../core/src/index.ts"],
|
||||
"@open-pencil/core/*": ["../core/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
21
packages/dom-css/tsdown.config.ts
Normal file
21
packages/dom-css/tsdown.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: './src/index.ts'
|
||||
},
|
||||
platform: 'neutral',
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
hash: false,
|
||||
clean: true,
|
||||
outDir: './dist',
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
},
|
||||
deps: {
|
||||
neverBundle: ['@open-pencil/core', /^@open-pencil\/core\//],
|
||||
onlyBundle: false
|
||||
}
|
||||
})
|
||||
43
tests/engine/dom-css/runtime.test.ts
Normal file
43
tests/engine/dom-css/runtime.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, it } from 'bun:test'
|
||||
|
||||
import {
|
||||
createCssRuntime,
|
||||
createHeadlessCssRuntime,
|
||||
serializeHTML,
|
||||
type DesignDocument
|
||||
} from '@open-pencil/dom-css'
|
||||
|
||||
const documentFixture: DesignDocument = {
|
||||
type: 'document',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
attrs: { class: 'card', 'data-id': 'node-1' },
|
||||
children: [{ type: 'text', text: 'OpenPencil' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
describe('@open-pencil/dom-css', () => {
|
||||
it('serializes DesignDOM as HTML', () => {
|
||||
expect(serializeHTML(documentFixture)).toBe(
|
||||
'<div class="card" data-id="node-1">OpenPencil</div>'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the headless runtime outside browser contexts', () => {
|
||||
const runtime = createCssRuntime()
|
||||
|
||||
expect(runtime.kind).toBe('headless')
|
||||
expect(runtime.serializeHTML(documentFixture)).toBe(
|
||||
'<div class="card" data-id="node-1">OpenPencil</div>'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps headless HTML parsing explicit until the parser adapter is added', () => {
|
||||
const runtime = createHeadlessCssRuntime()
|
||||
|
||||
expect(() => runtime.parseHTML('<div />')).toThrow('headless DOM/CSS adapter')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const publicPackages = ['packages/core', 'packages/cli', 'packages/mcp', 'packages/vue']
|
||||
const publicPackages = [
|
||||
'packages/core',
|
||||
'packages/dom-css',
|
||||
'packages/cli',
|
||||
'packages/mcp',
|
||||
'packages/vue'
|
||||
]
|
||||
|
||||
interface PackageJson {
|
||||
name: string
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import { basename, join } from 'node:path'
|
|||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const rootDir = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
const packageDirs = ['packages/core', 'packages/vue', 'packages/mcp', 'packages/cli']
|
||||
const packageDirs = [
|
||||
'packages/core',
|
||||
'packages/dom-css',
|
||||
'packages/vue',
|
||||
'packages/mcp',
|
||||
'packages/cli'
|
||||
]
|
||||
|
||||
function run(command: string[], cwd = rootDir): string {
|
||||
const proc = Bun.spawnSync(command, { cwd, stdout: 'pipe', stderr: 'pipe' })
|
||||
|
|
@ -57,6 +63,7 @@ try {
|
|||
|
||||
nodeEval("await import('@open-pencil/core')", tempDir)
|
||||
nodeEval("await import('@open-pencil/core/scene-graph')", tempDir)
|
||||
nodeEval("await import('@open-pencil/dom-css')", tempDir)
|
||||
nodeEval("await import('@open-pencil/vue')", tempDir)
|
||||
nodeEval("await import('@open-pencil/mcp')", tempDir)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@
|
|||
"#core/*": ["./packages/core/src/*"],
|
||||
"#cli/*": ["./packages/cli/src/*"],
|
||||
"#mcp/*": ["./packages/mcp/src/*"],
|
||||
"#dom-css/*": ["./packages/dom-css/src/*"],
|
||||
"#tests/*": ["./tests/*"],
|
||||
"tinykeys": ["./node_modules/tinykeys/dist/tinykeys.d.ts"],
|
||||
"@open-pencil/vue": ["./packages/vue/src/index.ts"],
|
||||
"@open-pencil/core": ["./packages/core/src/index.ts"],
|
||||
"@open-pencil/core/*": ["./packages/core/src/*"]
|
||||
"@open-pencil/core/*": ["./packages/core/src/*"],
|
||||
"@open-pencil/dom-css": ["./packages/dom-css/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"],
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ export function createOpenPencilAliases(rootDir: string) {
|
|||
'@': resolve(rootDir, 'src'),
|
||||
'#vue': resolve(rootDir, 'packages/vue/src'),
|
||||
'#core': resolve(rootDir, 'packages/core/src'),
|
||||
'#dom-css': resolve(rootDir, 'packages/dom-css/src'),
|
||||
'@open-pencil/vue': resolve(rootDir, 'packages/vue/src'),
|
||||
'@open-pencil/core': resolve(rootDir, 'packages/core/src'),
|
||||
'@open-pencil/dom-css': resolve(rootDir, 'packages/dom-css/src'),
|
||||
'opentype.js': resolve(rootDir, 'node_modules/opentype.js/dist/opentype.module.js'),
|
||||
mermaid: resolve(rootDir, 'src/app/shell/markdown/index.ts'),
|
||||
'beautiful-mermaid': resolve(rootDir, 'src/app/shell/markdown/index.ts')
|
||||
|
|
|
|||
Loading…
Reference in a new issue