feat(fig): scaffold package shell

This commit is contained in:
Danila Poyarkov 2026-06-06 18:38:27 +03:00
parent 3c04249124
commit 69ad74fd15
13 changed files with 201 additions and 4 deletions

View file

@ -13,6 +13,8 @@ Bun workspace packages:
- `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/kiwi` — `@open-pencil/kiwi`: pure Kiwi schema/runtime/protocol package. Owns low-level Figma Kiwi codec/container/parse helpers and stays SceneGraph-agnostic.
- `packages/fig` — `@open-pencil/fig`: scaffold for future `.fig` document policy package. Planned home for SceneGraph ⇄ NodeChange conversion, raw metadata invalidation, and component/instance interpretation; production `.fig` APIs still live in core for now.
- `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.

View file

@ -17,6 +17,7 @@
"@open-pencil/cli": "workspace:*",
"@open-pencil/core": "workspace:*",
"@open-pencil/dom-css": "workspace:*",
"@open-pencil/fig": "workspace:*",
"@open-pencil/kiwi": "workspace:*",
"@open-pencil/vue": "workspace:*",
"@openrouter/ai-sdk-provider": "^2.9.0",
@ -176,6 +177,19 @@
"@open-pencil/core": "workspace:*",
},
},
"packages/fig": {
"name": "@open-pencil/fig",
"version": "0.13.2",
"devDependencies": {
"@types/bun": "^1.3.14",
"tsdown": "^0.21.7",
"typescript": "~5.8.3",
},
"peerDependencies": {
"@open-pencil/core": "workspace:*",
"@open-pencil/kiwi": "workspace:*",
},
},
"packages/kiwi": {
"name": "@open-pencil/kiwi",
"version": "0.13.2",
@ -798,6 +812,8 @@
"@open-pencil/dom-css": ["@open-pencil/dom-css@workspace:packages/dom-css"],
"@open-pencil/fig": ["@open-pencil/fig@workspace:packages/fig"],
"@open-pencil/kiwi": ["@open-pencil/kiwi@workspace:packages/kiwi"],
"@open-pencil/mcp": ["@open-pencil/mcp@workspace:packages/mcp"],

View file

@ -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/ packages/dom-css/src/ packages/kiwi/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/ packages/dom-css/tests/ packages/dom-css/scripts/ packages/kiwi/src/ packages/kiwi/tests/ packages/kiwi/scripts/ 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/ packages/dom-css/tests/ packages/dom-css/scripts/ packages/kiwi/src/ packages/kiwi/tests/ packages/kiwi/scripts/ 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/ packages/kiwi/src/ packages/fig/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/ packages/dom-css/tests/ packages/dom-css/scripts/ packages/kiwi/src/ packages/kiwi/tests/ packages/kiwi/scripts/ packages/fig/src/ packages/fig/tests/ packages/fig/scripts/ 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/ packages/dom-css/tests/ packages/dom-css/scripts/ packages/kiwi/src/ packages/kiwi/tests/ packages/kiwi/scripts/ packages/fig/src/ packages/fig/tests/ packages/fig/scripts/ 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",
@ -31,7 +31,7 @@
"test:tools": "bun tools/test.ts",
"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/kiwi build && 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",
"build:packages": "bun --filter @open-pencil/kiwi build && bun --filter @open-pencil/fig build && 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",
@ -52,6 +52,7 @@
"@open-pencil/cli": "workspace:*",
"@open-pencil/core": "workspace:*",
"@open-pencil/dom-css": "workspace:*",
"@open-pencil/fig": "workspace:*",
"@open-pencil/kiwi": "workspace:*",
"@open-pencil/vue": "workspace:*",
"@openrouter/ai-sdk-provider": "^2.9.0",

27
packages/fig/README.md Normal file
View file

@ -0,0 +1,27 @@
# @open-pencil/fig
`.fig` document policy package for OpenPencil.
This package is currently a scaffold for the next package-split stage. Use `@open-pencil/core` for production `.fig` read/write APIs until behavior moves here.
Planned ownership:
- `.fig` read/write orchestration
- SceneGraph ⇄ Figma NodeChange conversion policy
- raw Figma metadata preservation and invalidation
- component/instance interpretation
- oracle-backed `.fig` fixtures and package-local tests
Non-goals:
- low-level Kiwi schema/runtime/codec internals — use `@open-pencil/kiwi`
- editor actions, renderer behavior, Vue/app UI, CLI formatting, or MCP transport
See `packages/docs/development/fig-package-plan.md` for the staged migration plan.
## Checks
```sh
cd packages/fig
bun run check
```

47
packages/fig/package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "@open-pencil/fig",
"version": "0.13.2",
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"bun": "./src/index.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/src/index.d.ts",
"sideEffects": false,
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "bunx tsdown --config tsdown.config.ts && bunx tsc --emitDeclarationOnly -p tsconfig.json",
"check": "bun run typecheck && bun run test && bun run build && bun run smoke:dist",
"prepack": "bun run build",
"smoke:dist": "bun scripts/smoke-dist.ts",
"test": "bun test tests",
"typecheck": "bunx tsc --noEmit -p tsconfig.test.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/open-pencil/open-pencil.git",
"directory": "packages/fig"
},
"publishConfig": {
"access": "public",
"provenance": true
},
"peerDependencies": {
"@open-pencil/core": "workspace:*",
"@open-pencil/kiwi": "workspace:*"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"tsdown": "^0.21.7",
"typescript": "~5.8.3"
}
}

View file

@ -0,0 +1,7 @@
export {}
const mod = await import('../dist/index.js')
if (mod.FIG_PACKAGE_STATUS !== 'scaffold') {
throw new Error('Expected @open-pencil/fig scaffold export')
}

25
packages/fig/src/index.ts Normal file
View file

@ -0,0 +1,25 @@
export interface FigDocumentSource {
readonly bytes?: Uint8Array
readonly fileName?: string
}
export interface FigDocument<Graph = unknown> {
readonly graph: Graph
readonly source?: FigDocumentSource
}
export interface ReadFigOptions {
readonly preserveRawMetadata?: boolean
}
export interface WriteFigOptions {
readonly source?: FigDocumentSource
}
export const FIG_PACKAGE_STATUS = 'scaffold' as const
export function assertFigPackageReady(): void {
throw new Error(
'@open-pencil/fig is scaffolded for package-boundary work; use @open-pencil/core for .fig read/write APIs for now.'
)
}

View file

@ -0,0 +1,13 @@
import { describe, expect, it } from 'bun:test'
import { FIG_PACKAGE_STATUS, assertFigPackageReady } from '../src/index'
describe('@open-pencil/fig package shell', () => {
it('exports scaffold status', () => {
expect(FIG_PACKAGE_STATUS).toBe('scaffold')
})
it('directs consumers to core until behavior moves', () => {
expect(() => assertFigPackageReady()).toThrow('@open-pencil/fig is scaffolded')
})
})

View file

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
"types": ["bun"]
},
"include": ["src"]
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src", "tests"]
}

View file

@ -0,0 +1,23 @@
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: {
index: './src/index.ts'
},
platform: 'neutral',
format: ['esm'],
dts: false,
sourcemap: true,
hash: false,
clean: true,
outDir: './dist',
treeshake: {
moduleSideEffects: false
},
deps: {
onlyBundle: false
},
outputOptions: {
minifyInternalExports: false
}
})

View file

@ -4,6 +4,8 @@ import { join } from 'node:path'
const publicPackages = [
'packages/core',
'packages/dom-css',
'packages/kiwi',
'packages/fig',
'packages/cli',
'packages/mcp',
'packages/vue'

View file

@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
const rootDir = fileURLToPath(new URL('../../..', import.meta.url))
const packageDirs = [
'packages/kiwi',
'packages/fig',
'packages/core',
'packages/dom-css',
'packages/vue',
@ -69,6 +70,7 @@ try {
nodeEval("await import('@open-pencil/kiwi/fig/container')", tempDir)
nodeEval("await import('@open-pencil/kiwi/fig/guid')", tempDir)
nodeEval("await import('@open-pencil/kiwi/fig/parse')", tempDir)
nodeEval("await import('@open-pencil/fig')", tempDir)
nodeEval("await import('@open-pencil/core')", tempDir)
nodeEval("await import('@open-pencil/core/scene-graph')", tempDir)
nodeEval("await import('@open-pencil/dom-css')", tempDir)
@ -82,10 +84,22 @@ try {
"const { guidToString } = await import('@open-pencil/kiwi/fig/guid'); if (guidToString({ sessionID: 1, localID: 2 }) !== '1:2') throw new Error('Kiwi GUID subpath failed')",
tempDir
)
nodeEval(
"const { buildFigKiwi, parseFigKiwiChunks } = await import('@open-pencil/kiwi/fig/container'); const chunks = parseFigKiwiChunks(buildFigKiwi(new Uint8Array([1]), new Uint8Array([2]))); if (chunks?.length !== 2) throw new Error('Kiwi container subpath failed')",
tempDir
)
nodeEval(
"const { FIG_PACKAGE_STATUS } = await import('@open-pencil/fig'); if (FIG_PACKAGE_STATUS !== 'scaffold') throw new Error('Fig package smoke failed')",
tempDir
)
nodeEval(
"const { htmlToSceneGraph } = await import('@open-pencil/dom-css'); const graph = await htmlToSceneGraph('<div class=card>OpenPencil</div>', { cssText: '.card { width: 320px; }' }); if (graph.getPages()[0].width !== 320) throw new Error('DOM/CSS scene graph smoke failed')",
tempDir
)
nodeEval(
"const { jsx, jsxToDesignDocument } = await import('@open-pencil/dom-css/jsx-runtime'); const document = await jsxToDesignDocument(jsx('section', { class: 'card', style: { width: '120px' }, children: 'OpenPencil' })); const node = document.children[0]; if (node?.type !== 'element' || node.inlineStyle?.width !== '120px') throw new Error('DOM/CSS JSX runtime smoke failed')",
tempDir
)
run(['node', 'node_modules/.bin/openpencil', '--help'], tempDir)
run(['node', 'node_modules/.bin/openpencil-mcp', '--help'], tempDir)