From 69ad74fd150a512552e2190641dfee41394d24c4 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 6 Jun 2026 18:38:27 +0300 Subject: [PATCH] feat(fig): scaffold package shell --- AGENTS.md | 2 + bun.lock | 16 +++++++ package.json | 9 ++-- packages/fig/README.md | 27 ++++++++++++ packages/fig/package.json | 47 +++++++++++++++++++++ packages/fig/scripts/smoke-dist.ts | 7 +++ packages/fig/src/index.ts | 25 +++++++++++ packages/fig/tests/index.test.ts | 13 ++++++ packages/fig/tsconfig.json | 13 ++++++ packages/fig/tsconfig.test.json | 7 +++ packages/fig/tsdown.config.ts | 23 ++++++++++ tools/package-quality/src/check-metadata.ts | 2 + tools/package-quality/src/smoke.ts | 14 ++++++ 13 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 packages/fig/README.md create mode 100644 packages/fig/package.json create mode 100644 packages/fig/scripts/smoke-dist.ts create mode 100644 packages/fig/src/index.ts create mode 100644 packages/fig/tests/index.test.ts create mode 100644 packages/fig/tsconfig.json create mode 100644 packages/fig/tsconfig.test.json create mode 100644 packages/fig/tsdown.config.ts diff --git a/AGENTS.md b/AGENTS.md index 142fb8045..4b793088c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/bun.lock b/bun.lock index 58c1ee9be..fa2d482cf 100644 --- a/bun.lock +++ b/bun.lock @@ -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"], diff --git a/package.json b/package.json index c4bc62060..9929430a4 100644 --- a/package.json +++ b/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/ 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", diff --git a/packages/fig/README.md b/packages/fig/README.md new file mode 100644 index 000000000..34fc9cfac --- /dev/null +++ b/packages/fig/README.md @@ -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 +``` diff --git a/packages/fig/package.json b/packages/fig/package.json new file mode 100644 index 000000000..1bab148ba --- /dev/null +++ b/packages/fig/package.json @@ -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" + } +} diff --git a/packages/fig/scripts/smoke-dist.ts b/packages/fig/scripts/smoke-dist.ts new file mode 100644 index 000000000..466cc953d --- /dev/null +++ b/packages/fig/scripts/smoke-dist.ts @@ -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') +} diff --git a/packages/fig/src/index.ts b/packages/fig/src/index.ts new file mode 100644 index 000000000..6d2b507cc --- /dev/null +++ b/packages/fig/src/index.ts @@ -0,0 +1,25 @@ +export interface FigDocumentSource { + readonly bytes?: Uint8Array + readonly fileName?: string +} + +export interface FigDocument { + 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.' + ) +} diff --git a/packages/fig/tests/index.test.ts b/packages/fig/tests/index.test.ts new file mode 100644 index 000000000..fd0697033 --- /dev/null +++ b/packages/fig/tests/index.test.ts @@ -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') + }) +}) diff --git a/packages/fig/tsconfig.json b/packages/fig/tsconfig.json new file mode 100644 index 000000000..fd5f2add7 --- /dev/null +++ b/packages/fig/tsconfig.json @@ -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"] +} diff --git a/packages/fig/tsconfig.test.json b/packages/fig/tsconfig.test.json new file mode 100644 index 000000000..e0d10a325 --- /dev/null +++ b/packages/fig/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src", "tests"] +} diff --git a/packages/fig/tsdown.config.ts b/packages/fig/tsdown.config.ts new file mode 100644 index 000000000..3b3c4b8cf --- /dev/null +++ b/packages/fig/tsdown.config.ts @@ -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 + } +}) diff --git a/tools/package-quality/src/check-metadata.ts b/tools/package-quality/src/check-metadata.ts index b32180941..7675f9d45 100644 --- a/tools/package-quality/src/check-metadata.ts +++ b/tools/package-quality/src/check-metadata.ts @@ -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' diff --git a/tools/package-quality/src/smoke.ts b/tools/package-quality/src/smoke.ts index 8e66961fa..53b95f685 100644 --- a/tools/package-quality/src/smoke.ts +++ b/tools/package-quality/src/smoke.ts @@ -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('
OpenPencil
', { 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)