@jay-framework/jay-stack-cli 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ # Figma Vendor
2
+
3
+ Converts Figma documents to Jay HTML with full contract binding support.
4
+
5
+ ## Overview
6
+
7
+ The Figma vendor implements the complete conversion pipeline from Figma design documents to Jay-HTML, handling:
8
+
9
+ - **Data bindings** - Contract tags bound to UI elements
10
+ - **Interactive refs** - Button clicks, input fields, etc.
11
+ - **Attribute bindings** - src, value, href, etc.
12
+ - **Variant permutations** - Figma component variants to Jay if conditions
13
+ - **Repeater contexts** - forEach loops with proper path scoping
14
+ - **Plugin contracts** - Multi-component plugin support
15
+
16
+ ## Architecture
17
+
18
+ ### Conversion Pipeline
19
+
20
+ ```
21
+ FigmaVendorDocument
22
+
23
+ 1. Extract bindings from pluginData
24
+
25
+ 2. Analyze bindings (type, paths, context)
26
+
27
+ 3. Validate binding rules
28
+
29
+ 4. Route to appropriate converter
30
+
31
+ - Repeater → convertRepeaterNode()
32
+ - Variant → convertVariantNode()
33
+ - Regular → convertRegularNode()
34
+
35
+ Jay-HTML output
36
+ ```
37
+
38
+ ### Key Files
39
+
40
+ - **`index.ts`** - Main vendor implementation, conversion pipeline
41
+ - **`binding-analysis.ts`** - Binding analysis, path resolution, validation
42
+ - **`types.ts`** - Type definitions for bindings and context
43
+ - **`utils.ts`** - CSS conversion utilities
44
+ - **`converters/`** - Node-specific converters (text, rectangle, etc.)
45
+ - **`pageContractPath.ts`** - Contract path utilities
46
+
47
+ ## Binding Types
48
+
49
+ ### 1. Dynamic Content (Data)
50
+
51
+ **Figma Binding:**
52
+
53
+ ```json
54
+ {
55
+ "tagPath": ["productPage", "name"],
56
+ "pageContractPath": { ... }
57
+ }
58
+ ```
59
+
60
+ **Jay-HTML:**
61
+
62
+ ```html
63
+ <div>{productPage.name}</div>
64
+ ```
65
+
66
+ ### 2. Interactive Ref
67
+
68
+ **Figma Binding:**
69
+
70
+ ```json
71
+ {
72
+ "tagPath": ["submitButton"],
73
+ "pageContractPath": { ... }
74
+ }
75
+ ```
76
+
77
+ **Jay-HTML:**
78
+
79
+ ```html
80
+ <button ref="submitButton">Submit</button>
81
+ ```
82
+
83
+ ### 3. Dual Binding (Data + Interactive)
84
+
85
+ **Figma Binding:**
86
+
87
+ ```json
88
+ {
89
+ "tagPath": ["email"],
90
+ "pageContractPath": { ... }
91
+ }
92
+ ```
93
+
94
+ **Jay-HTML:**
95
+
96
+ ```html
97
+ <input ref="email" value="{email}" />
98
+ ```
99
+
100
+ ### 4. Attribute Binding
101
+
102
+ **Figma Binding:**
103
+
104
+ ```json
105
+ {
106
+ "tagPath": ["product", "imageUrl"],
107
+ "attribute": "src",
108
+ "pageContractPath": { ... }
109
+ }
110
+ ```
111
+
112
+ **Jay-HTML:**
113
+
114
+ ```html
115
+ <img src="{product.imageUrl}" alt="..." />
116
+ ```
117
+
118
+ **Supported Attributes:**
119
+
120
+ - `src` - Image sources (requires `semanticHtml: 'img'`)
121
+ - `href` - Link destinations (requires `semanticHtml: 'a'`)
122
+ - `value` - Input field values (requires `semanticHtml: 'input'`)
123
+ - `alt` - Image alt text
124
+ - `placeholder` - Input placeholders
125
+
126
+ #### Image Conversion (Semantic HTML: `img`)
127
+
128
+ When a node has `semanticHtml: 'img'` in its plugin data:
129
+
130
+ **Bound Image** (with `src` attribute binding):
131
+
132
+ ```html
133
+ <img src="{productPage.imageUrl}" alt="{productPage.imageAlt}" data-figma-id="..." />
134
+ ```
135
+
136
+ **Static Image** (no bindings, uses Figma fills):
137
+
138
+ ```html
139
+ <img src="/assets/images/product-hero.png" alt="Product Hero" data-figma-id="..." />
140
+ ```
141
+
142
+ For static images, the plugin must export and save the image:
143
+
144
+ ```typescript
145
+ // In plugin serialization for IMAGE fills:
146
+ if (fill.type === 'IMAGE' && fill.imageHash) {
147
+ const imageBytes = await figma.getImageByHash(fill.imageHash)?.getBytesAsync();
148
+ if (imageBytes) {
149
+ const imageUrl = await saveImageToAssets(imageBytes, node.id);
150
+ serializedFill.imageUrl = imageUrl; // ← Add this!
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### 5. Property Binding (Variants)
156
+
157
+ **Figma Bindings:**
158
+
159
+ ```json
160
+ [
161
+ { "property": "mediaType", "tagPath": ["productPage", "mediaType"] },
162
+ { "property": "selected", "tagPath": ["productPage", "isSelected"] }
163
+ ]
164
+ ```
165
+
166
+ **Jay-HTML:**
167
+
168
+ ```html
169
+ <div if="productPage.mediaType == IMAGE && productPage.isSelected == true">
170
+ <!-- IMAGE + selected variant -->
171
+ </div>
172
+ <div if="productPage.mediaType == VIDEO && productPage.isSelected == true">
173
+ <!-- VIDEO + selected variant -->
174
+ </div>
175
+ <!-- ... other permutations -->
176
+ ```
177
+
178
+ **Pseudo-CSS Variants:**
179
+
180
+ Variant values containing `:` (like `image:hover`, `:active`, `:disabled`) are automatically filtered out from Jay-HTML `if` conditions. These are pseudo-CSS class variants that should be handled via CSS display toggling instead:
181
+
182
+ ```css
183
+ /* Pseudo-variant CSS (handled separately, not in conversion) */
184
+ .mediaType_hover {
185
+ display: none;
186
+ }
187
+ .mediaType:hover .mediaType_hover {
188
+ display: block;
189
+ }
190
+ ```
191
+
192
+ This filtering prevents invalid expressions like `if="media == image:hover"` from being generated.
193
+
194
+ ### 6. Repeater
195
+
196
+ **Figma Binding:**
197
+
198
+ ```json
199
+ {
200
+ "tagPath": ["productPage", "items"],
201
+ "pageContractPath": { ... }
202
+ }
203
+ ```
204
+
205
+ **Contract:**
206
+
207
+ ```yaml
208
+ - tag: items
209
+ type: subContract
210
+ repeated: true
211
+ trackBy: id
212
+ ```
213
+
214
+ **Jay-HTML:**
215
+
216
+ ```html
217
+ <div forEach="productPage.items" trackBy="id">
218
+ <div>{title}</div>
219
+ <!-- Context-relative path -->
220
+ </div>
221
+ ```
222
+
223
+ **Important:** Only the **first child** of a repeater node is converted. This first child serves as the template that gets repeated for each item in the array. Other sibling nodes are ignored as they are not part of the repeater pattern.
224
+
225
+ ## Repeater Context
226
+
227
+ Repeaters change the path context for their children:
228
+
229
+ **Full Paths:**
230
+
231
+ - Repeater: `productPage.products`
232
+ - Child: `productPage.products.title`
233
+
234
+ **In Jay-HTML:**
235
+
236
+ ```html
237
+ <div forEach="productPage.products" trackBy="id">
238
+ <div>{title}</div>
239
+ <!-- Not productPage.products.title -->
240
+ </div>
241
+ ```
242
+
243
+ **Nested Repeaters:**
244
+
245
+ ```html
246
+ <div forEach="compKey.items" trackBy="id">
247
+ <div forEach="subItems" trackBy="id">
248
+ <!-- No prefix -->
249
+ <div>{name}</div>
250
+ <!-- Relative to subItems -->
251
+ </div>
252
+ </div>
253
+ ```
254
+
255
+ ## Validation Rules
256
+
257
+ 1. **Property bindings are exclusive** - If ANY binding has a property, ALL must have properties
258
+ 2. **Attributes + dynamic content allowed** - Multiple attribute bindings + dynamic content is valid
259
+ 3. **Interactive must be pure** - Interactive bindings cannot have attribute or property
260
+ 4. **One ref per node** - Only one interactive binding per node
261
+
262
+ ## Vendor Document Type
263
+
264
+ The Figma vendor uses the `FigmaVendorDocument` type defined in `@jay-framework/editor-protocol`. This is the **single source of truth** for the document structure.
265
+
266
+ ### For Figma Plugin Developers
267
+
268
+ Import the type from the editor protocol:
269
+
270
+ ```typescript
271
+ import { FigmaVendorDocument } from '@jay-framework/editor-protocol';
272
+
273
+ // Your Figma plugin code
274
+ const vendorDoc: FigmaVendorDocument = {
275
+ type: selectedNode.type,
276
+ name: selectedNode.name,
277
+ children: selectedNode.children,
278
+ pluginData: {
279
+ 'jpage': 'true',
280
+ 'jay-layer-bindings': JSON.stringify([
281
+ {
282
+ pageContractPath: { ... },
283
+ tagPath: ['productPage', 'name'],
284
+ jayPageSectionId: '...'
285
+ }
286
+ ])
287
+ },
288
+ // ... other Figma node properties
289
+ };
290
+
291
+ await editorProtocol.export({
292
+ vendorId: 'figma',
293
+ pageUrl: '/products/:slug',
294
+ vendorDoc,
295
+ });
296
+ ```
297
+
298
+ ### Plugin Data Format
299
+
300
+ **`jay-layer-bindings`** - Array of LayerBinding:
301
+
302
+ ```typescript
303
+ type LayerBinding = {
304
+ pageContractPath: PageContractPath;
305
+ jayPageSectionId: string;
306
+ tagPath: string[];
307
+ attribute?: string;
308
+ property?: string;
309
+ };
310
+ ```
311
+
312
+ **`jpage`** - Marks top-level section as a Jay Page:
313
+
314
+ ```typescript
315
+ pluginData: {
316
+ 'jpage': 'true',
317
+ 'urlRoute': '/products/:slug'
318
+ }
319
+ ```
320
+
321
+ **`semanticHtml`** - Specifies HTML tag:
322
+
323
+ ```typescript
324
+ pluginData: {
325
+ 'semanticHtml': 'img' // or 'button', 'input', etc.
326
+ }
327
+ ```
328
+
329
+ ## Usage Example
330
+
331
+ ```typescript
332
+ import { figmaVendor } from './vendors/figma';
333
+
334
+ const result = await figmaVendor.convertToBodyHtml(
335
+ vendorDoc,
336
+ '/products/:slug',
337
+ projectPage,
338
+ plugins,
339
+ );
340
+
341
+ console.log(result.bodyHtml);
342
+ // <div data-figma-id="...">
343
+ // <div>{productPage.name}</div>
344
+ // <img src="{productPage.image}" />
345
+ // <div forEach="productPage.items" trackBy="id">
346
+ // <div>{title}</div>
347
+ // </div>
348
+ // </div>
349
+ ```
350
+
351
+ ## Extending the Vendor
352
+
353
+ ### Adding a New Binding Type
354
+
355
+ 1. Update `BindingAnalysis` type in `types.ts`
356
+ 2. Add analysis logic in `analyzeBindings()` in `binding-analysis.ts`
357
+ 3. Add validation rules in `validateBindings()`
358
+ 4. Handle new type in `convertRegularNode()` or create new converter
359
+
360
+ ### Adding Node Type Support
361
+
362
+ 1. Create converter in `converters/` (e.g., `my-node.ts`)
363
+ 2. Import and call from `convertRegularNode()` in `index.ts`
364
+ 3. Handle node-specific styling in converter
365
+
366
+ ### Extending Path Resolution
367
+
368
+ Modify `resolveBinding()` in `binding-analysis.ts` to handle:
369
+
370
+ - New contract sources
371
+ - Custom path transformations
372
+ - Additional context types
373
+
374
+ ## Known Limitations
375
+
376
+ 1. **Variant Components:** Requires `componentPropertyDefinitions` in `FigmaVendorDocument`
377
+ 2. **Page Contracts:** Page contract resolution not fully implemented
378
+ 3. **Mixed Fonts:** Only single font per text node supported
379
+ 4. **Error Recovery:** Logs warnings but doesn't have graceful fallbacks
380
+
381
+ ## Testing
382
+
383
+ _To be added - See design-log/67 for test plan_
384
+
385
+ ## Design Documentation
386
+
387
+ See `design-log/67 - Figma Vendor Conversion Algorithm` for:
388
+
389
+ - Complete design rationale
390
+ - Implementation details
391
+ - Examples and edge cases
392
+ - Trade-off decisions
393
+
394
+ ---
395
+
396
+ **Last Updated:** January 12, 2026
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,14 +24,14 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
- "@jay-framework/compiler-jay-html": "^0.12.0",
28
- "@jay-framework/compiler-shared": "^0.12.0",
29
- "@jay-framework/dev-server": "^0.12.0",
30
- "@jay-framework/editor-server": "^0.12.0",
31
- "@jay-framework/fullstack-component": "^0.12.0",
32
- "@jay-framework/logger": "^0.12.0",
33
- "@jay-framework/plugin-validator": "^0.12.0",
34
- "@jay-framework/stack-server-runtime": "^0.12.0",
27
+ "@jay-framework/compiler-jay-html": "^0.13.0",
28
+ "@jay-framework/compiler-shared": "^0.13.0",
29
+ "@jay-framework/dev-server": "^0.13.0",
30
+ "@jay-framework/editor-server": "^0.13.0",
31
+ "@jay-framework/fullstack-component": "^0.13.0",
32
+ "@jay-framework/logger": "^0.13.0",
33
+ "@jay-framework/plugin-validator": "^0.13.0",
34
+ "@jay-framework/stack-server-runtime": "^0.13.0",
35
35
  "chalk": "^4.1.2",
36
36
  "commander": "^14.0.0",
37
37
  "express": "^5.0.1",
@@ -40,7 +40,7 @@
40
40
  "yaml": "^2.3.4"
41
41
  },
42
42
  "devDependencies": {
43
- "@jay-framework/dev-environment": "^0.12.0",
43
+ "@jay-framework/dev-environment": "^0.13.0",
44
44
  "@types/express": "^5.0.2",
45
45
  "@types/node": "^22.15.21",
46
46
  "nodemon": "^3.0.3",
@@ -0,0 +1,164 @@
1
+ # Figma Vendor Test Fixtures
2
+
3
+ This directory contains fixture-based integration tests for the Figma vendor converter.
4
+
5
+ ## Structure
6
+
7
+ Each fixture is a directory containing:
8
+
9
+ - **`page.figma.json`** - Input Figma document (serialized from Figma plugin)
10
+ - **`page.jay-contract`** - Contract defining data bindings and structure
11
+ - **`expected.jay-html`** - Expected HTML output after conversion
12
+
13
+ ## Current Fixtures
14
+
15
+ ### 1. `basic-text/` ✅ **Working**
16
+
17
+ Tests basic text rendering with multiple text nodes and font families.
18
+
19
+ **Features tested:**
20
+
21
+ - Static text conversion
22
+ - Font family collection (Inter)
23
+ - Font weight and size
24
+ - Text alignment
25
+ - Color conversion
26
+ - Basic FRAME container with children
27
+
28
+ ---
29
+
30
+ ### 2. `button-with-variants/` ✅ **Working**
31
+
32
+ Tests variant components with multiple properties.
33
+
34
+ **Features tested:**
35
+
36
+ - COMPONENT_SET with variants
37
+ - Boolean variants (disabled: true/false)
38
+ - Enum variants (variant: primary/secondary)
39
+ - Property bindings
40
+ - Variant permutation generation
41
+ - Conditional rendering with `if` attributes
42
+
43
+ ---
44
+
45
+ ### 3. `repeater-list/` ✅ **Working**
46
+
47
+ Tests repeater (forEach) functionality with nested bindings.
48
+
49
+ **Features tested:**
50
+
51
+ - Frame with repeater binding
52
+ - `forEach` attribute generation
53
+ - `trackBy` attribute
54
+ - Nested data bindings (items.title, items.description)
55
+ - Repeater context management
56
+ - Sub-contract with repeated items
57
+
58
+ ---
59
+
60
+ ### 4. `complex-page/` ✅ **Working**
61
+
62
+ Tests a realistic product page with multiple features combined.
63
+
64
+ **Features tested:**
65
+
66
+ - Dynamic text content bindings (product.name, product.price)
67
+ - Nested repeater (product.reviews)
68
+ - Parameterized route (/products/:id)
69
+ - Multiple font families (Roboto)
70
+ - Complex contract structure with nested sub-contracts
71
+
72
+ ---
73
+
74
+ ## Adding New Fixtures
75
+
76
+ 1. **Create a directory** under `fixtures/`:
77
+
78
+ ```bash
79
+ mkdir test/vendors/figma/fixtures/my-new-test
80
+ ```
81
+
82
+ 2. **Add required files**:
83
+
84
+ - `page.figma.json` - Figma document structure
85
+ - `expected.jay-html` - Expected HTML output
86
+ - `page.jay-contract` OR `page.conf.yaml` - Contract or plugin config
87
+
88
+ 3. **Add fixture content** - You can:
89
+
90
+ - Export a real Figma document using the plugin
91
+ - Hand-craft a test case
92
+ - Copy and modify an existing fixture
93
+
94
+ 4. **Run the test** to generate `actual-output.jay-html`:
95
+
96
+ ```bash
97
+ npm test -- test/vendors/figma/fixtures.test.ts -t "my-new-test"
98
+ ```
99
+
100
+ 5. **Review and copy** actual output to `expected.jay-html` if correct
101
+
102
+ 6. **That's it!** The test automatically discovers all fixture directories - no code changes needed!
103
+
104
+ ## Running Tests
105
+
106
+ Run all fixture tests (auto-discovers all fixture directories):
107
+
108
+ ```bash
109
+ npm test -- test/vendors/figma/fixtures.test.ts
110
+ ```
111
+
112
+ Run a specific fixture (via filtering):
113
+
114
+ ```bash
115
+ npm test -- test/vendors/figma/fixtures.test.ts -t "basic-text"
116
+ ```
117
+
118
+ The test runner **automatically discovers** all directories under `fixtures/` - you don't need to manually register new fixtures!
119
+
120
+ ## Tips
121
+
122
+ ### Debugging Failed Tests
123
+
124
+ When a test fails, check the `actual-output.jay-html` file saved in the fixture directory.
125
+
126
+ ### Contract Requirements
127
+
128
+ - Use `type: sub-contract` (not `subContract`)
129
+ - Include all fields referenced by `trackBy` attributes
130
+ - Check console warnings for validation errors
131
+
132
+ ### Normalizing HTML
133
+
134
+ The test normalizes HTML before comparison to handle:
135
+
136
+ - Whitespace differences
137
+ - Comment removal
138
+ - Line break variations
139
+
140
+ ## Test Coverage
141
+
142
+ Current fixtures cover:
143
+
144
+ - ✅ Static text rendering (basic-text)
145
+ - ✅ Font collection (basic-text)
146
+ - ✅ FRAME containers with layout (basic-text)
147
+ - ✅ Dynamic content bindings (repeater-list, complex-page, plugin-product-card)
148
+ - ✅ Variant components (button-with-variants)
149
+ - ✅ Repeaters with trackBy (repeater-list, complex-page)
150
+ - ✅ Nested sub-contracts (complex-page)
151
+ - ✅ Conditional rendering (button-with-variants)
152
+ - ✅ Plugin-based pages (plugin-product-card)
153
+ - ✅ Headless components (plugin-product-card)
154
+
155
+ **Current Status:** 5 fixtures fully working (22 passing tests total in vendor suite)
156
+
157
+ Missing coverage (potential future fixtures):
158
+
159
+ - ⬜ Interactive bindings (ref)
160
+ - ⬜ Dual bindings (data + interactive)
161
+ - ⬜ Error cases (invalid bindings, missing contracts)
162
+ - ⬜ Images with dynamic src bindings
163
+ - ⬜ Attribute bindings beyond variants
164
+ - ⬜ NPM-installed plugins (currently only local plugins tested)