Skip to content

Commit 5945f7a

Browse files
pavi2410shuding
andauthored
feat: Switch to yoga-layout (#689)
## Summary Replace yoga-wasm-web with yoga-layout for better performance and maintainability. ## Test Plan - [x] All tests run - [x] Lint fixes applied - [x] Prettier formatting applied - [x] Playground tested and working ## Known Issues - Some snapshot tests have mismatches - Type errors present for "auto" and percentage string support in flexBasis and (max|min)(Height|Width) properties These will be addressed in follow-up commits. --------- Co-authored-by: Shu Ding <g@shud.in>
1 parent b54f510 commit 5945f7a

File tree

42 files changed

+398
-169
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+398
-169
lines changed

.eslintrc.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,6 @@
2525
}
2626
],
2727
"parser": "@typescript-eslint/parser",
28-
"parserOptions": {
29-
"project": [
30-
"./tsconfig.json",
31-
"./playground/tsconfig.json",
32-
"./tsconfig.wasm.json"
33-
]
34-
},
3528
"plugins": ["react", "react-hooks", "@typescript-eslint"],
3629
"rules": {
3730
"no-inner-declarations": 0,

README.md

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,13 @@ Satori uses the same Flexbox [layout engine](https://yogalayout.com) as React Na
118118

119119
<tr>
120120
<td colspan="2"><code>display</code></td>
121-
<td><code>none</code> and <code>flex</code>, default to <code>flex</code></td>
121+
<td><code>flex</code>, <code>contents</code>, <code>none</code>, default to <code>flex</code></td>
122122
<td></td>
123123
</tr>
124124

125125
<tr>
126126
<td colspan="2"><code>position</code></td>
127-
<td><code>relative</code> and <code>absolute</code>, default to <code>relative</code></td>
127+
<td><code>relative</code>, <code>static</code> and <code>absolute</code>, default to <code>relative</code></td>
128128
<td></td>
129129
</tr>
130130

@@ -234,6 +234,12 @@ Satori uses the same Flexbox [layout engine](https://yogalayout.com) as React Na
234234
<td></td>
235235
</tr>
236236

237+
<tr>
238+
<td colspan="2"><code>boxSizing</code></td>
239+
<td>Supported</td>
240+
<td></td>
241+
</tr>
242+
237243
<tr>
238244
<td colspan="2"><code>boxShadow</code></td>
239245
<td>Supported</td>
@@ -289,9 +295,8 @@ Note:
289295

290296
1. Three-dimensional transforms are not supported.
291297
2. There is no `z-index` support in SVG. Elements that come later in the document will be painted on top.
292-
3. `box-sizing` is set to `border-box` for all elements.
293-
4. `calc` isn't supported.
294-
5. `currentcolor` support is only available for the `color` property.
298+
3. `calc` isn't supported.
299+
4. `currentColor` support is only available for the `color` property.
295300

296301
### Language and Typography
297302

@@ -389,30 +394,10 @@ await satori(
389394
)
390395
```
391396

392-
### Runtime and WASM
397+
### Runtime Support
393398

394399
Satori can be used in browser, Node.js (>= 16), and Web Workers.
395400

396-
By default, Satori depends on asm.js for the browser runtime, and native module in Node.js. However, you can optionally load WASM instead by importing `satori/wasm` and provide the initialized WASM module instance of Yoga to Satori:
397-
398-
```js
399-
import satori, { init } from 'satori/wasm'
400-
import initYoga from 'yoga-wasm-web'
401-
402-
const yoga = initYoga(await fetch('/yoga.wasm').then(res => res.arrayBuffer()))
403-
init(yoga)
404-
405-
await satori(...)
406-
```
407-
408-
When running in the browser or in the Node.js environment, WASM files need to be hosted and fetched before initializing. asm.js can be bundled together with the lib. In this case WASM should be faster.
409-
410-
When running on the Node.js server, native modules should be faster. However there are Node.js environments where native modules are not supported (e.g. StackBlitz's WebContainers), or other JS runtimes that support WASM (e.g. Vercel's Edge Runtime, Cloudflare Workers, or Deno).
411-
412-
Additionally, there are other difference between asm.js, native and WASM, such as security and compatibility.
413-
414-
Overall there are many trade-offs between each choice, and it's better to pick the one that works the best for your use case.
415-
416401
### Font Embedding
417402

418403
By default, Satori renders the text as `<path>` in SVG, instead of `<text>`. That means it embeds the font path data as inlined information, so succeeding processes (e.g. render the SVG on another platform) don’t need to deal with font files anymore.

package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,21 @@
2727
"./wasm": {
2828
"import": {
2929
"types": "./dist/index.d.ts",
30-
"default": "./dist/index.wasm.js"
30+
"default": "./dist/index.js"
3131
},
3232
"require": {
3333
"types": "./dist/index.d.cts",
34-
"default": "./dist/index.wasm.cjs"
34+
"default": "./dist/index.cjs"
3535
}
3636
}
3737
},
3838
"scripts": {
3939
"prepare": "husky install",
40-
"dev": "concurrently \"pnpm dev:default\" \"pnpm dev:wasm\"",
40+
"dev": "pnpm run dev:default",
4141
"dev:default": "NODE_ENV=development tsup src/index.ts --watch --ignore-watch playground",
42-
"dev:wasm": "WASM=1 NODE_ENV=development tsup src/index.ts --watch --ignore-watch playground",
4342
"dev:playground": "turbo dev --filter=satori-playground...",
44-
"build": "NODE_ENV=production pnpm run build:default && pnpm run build:wasm",
43+
"build": "NODE_ENV=production pnpm run build:default",
4544
"build:default": "tsup",
46-
"build:wasm": "WASM=1 tsup",
4745
"test": "NODE_ENV=test vitest run",
4846
"test:ui": "NODE_ENV=test vitest --ui --coverage.enabled",
4947
"test-type": "tsc -p tsconfig.json --noEmit && tsc -p playground/tsconfig.json --noEmit",
@@ -112,7 +110,7 @@
112110
"linebreak": "^1.1.0",
113111
"parse-css-color": "^0.2.1",
114112
"postcss-value-parser": "^4.2.0",
115-
"yoga-wasm-web": "^0.3.3"
113+
"yoga-layout": "^3.2.1"
116114
},
117115
"packageManager": "pnpm@8.7.0",
118116
"engines": {

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/handler/compute.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
* also returns the inherited style for children of the element.
55
*/
66

7-
import type { Node as YogaNode } from 'yoga-wasm-web'
8-
9-
import getYoga from '../yoga/index.js'
107
import presets from './presets.js'
118
import inheritable from './inheritable.js'
129
import expand, { SerializedStyle } from './expand.js'
13-
import { lengthToNumber, parseViewBox, v } from '../utils.js'
10+
import {
11+
asPointAutoPercentageLength,
12+
asPointPercentageLength,
13+
getYoga,
14+
lengthToNumber,
15+
parseViewBox,
16+
v,
17+
YogaNode,
18+
} from '../utils.js'
1419
import { resolveImageData } from './image.js'
1520

1621
type SatoriElement = keyof typeof presets
@@ -168,6 +173,7 @@ export default async function compute(
168173
{
169174
flex: Yoga.DISPLAY_FLEX,
170175
block: Yoga.DISPLAY_FLEX,
176+
contents: Yoga.DISPLAY_CONTENTS,
171177
none: Yoga.DISPLAY_NONE,
172178
'-webkit-box': Yoga.DISPLAY_FLEX,
173179
},
@@ -281,24 +287,24 @@ export default async function compute(
281287
// @TODO: node.setFlex
282288

283289
if (typeof style.flexBasis !== 'undefined') {
284-
node.setFlexBasis(style.flexBasis)
290+
node.setFlexBasis(asPointAutoPercentageLength(style.flexBasis, 'flexBasis'))
285291
}
286292
node.setFlexGrow(typeof style.flexGrow === 'undefined' ? 0 : style.flexGrow)
287293
node.setFlexShrink(
288294
typeof style.flexShrink === 'undefined' ? 0 : style.flexShrink
289295
)
290296

291297
if (typeof style.maxHeight !== 'undefined') {
292-
node.setMaxHeight(style.maxHeight)
298+
node.setMaxHeight(asPointPercentageLength(style.maxHeight, 'maxHeight'))
293299
}
294300
if (typeof style.maxWidth !== 'undefined') {
295-
node.setMaxWidth(style.maxWidth)
301+
node.setMaxWidth(asPointPercentageLength(style.maxWidth, 'maxWidth'))
296302
}
297303
if (typeof style.minHeight !== 'undefined') {
298-
node.setMinHeight(style.minHeight)
304+
node.setMinHeight(asPointPercentageLength(style.minHeight, 'minHeight'))
299305
}
300306
if (typeof style.minWidth !== 'undefined') {
301-
node.setMinWidth(style.minWidth)
307+
node.setMinWidth(asPointPercentageLength(style.minWidth, 'minWidth'))
302308
}
303309

304310
node.setOverflow(
@@ -313,10 +319,22 @@ export default async function compute(
313319
)
314320
)
315321

316-
node.setMargin(Yoga.EDGE_TOP, style.marginTop || 0)
317-
node.setMargin(Yoga.EDGE_BOTTOM, style.marginBottom || 0)
318-
node.setMargin(Yoga.EDGE_LEFT, style.marginLeft || 0)
319-
node.setMargin(Yoga.EDGE_RIGHT, style.marginRight || 0)
322+
node.setMargin(
323+
Yoga.EDGE_TOP,
324+
asPointAutoPercentageLength(style.marginTop || 0)
325+
)
326+
node.setMargin(
327+
Yoga.EDGE_BOTTOM,
328+
asPointAutoPercentageLength(style.marginBottom || 0)
329+
)
330+
node.setMargin(
331+
Yoga.EDGE_LEFT,
332+
asPointAutoPercentageLength(style.marginLeft || 0)
333+
)
334+
node.setMargin(
335+
Yoga.EDGE_RIGHT,
336+
asPointAutoPercentageLength(style.marginRight || 0)
337+
)
320338

321339
node.setBorder(Yoga.EDGE_TOP, style.borderTopWidth || 0)
322340
node.setBorder(Yoga.EDGE_BOTTOM, style.borderBottomWidth || 0)
@@ -328,38 +346,60 @@ export default async function compute(
328346
node.setPadding(Yoga.EDGE_LEFT, style.paddingLeft || 0)
329347
node.setPadding(Yoga.EDGE_RIGHT, style.paddingRight || 0)
330348

349+
node.setBoxSizing(
350+
v(
351+
style.boxSizing,
352+
{
353+
'border-box': Yoga.BOX_SIZING_BORDER_BOX,
354+
'content-box': Yoga.BOX_SIZING_CONTENT_BOX,
355+
},
356+
Yoga.BOX_SIZING_BORDER_BOX,
357+
'boxSizing'
358+
)
359+
)
360+
331361
node.setPositionType(
332362
v(
333363
style.position,
334364
{
335365
absolute: Yoga.POSITION_TYPE_ABSOLUTE,
336366
relative: Yoga.POSITION_TYPE_RELATIVE,
367+
static: Yoga.POSITION_TYPE_STATIC,
337368
},
338369
Yoga.POSITION_TYPE_RELATIVE,
339370
'position'
340371
)
341372
)
342373

343374
if (typeof style.top !== 'undefined') {
344-
node.setPosition(Yoga.EDGE_TOP, style.top)
375+
node.setPosition(Yoga.EDGE_TOP, asPointPercentageLength(style.top, 'top'))
345376
}
346377
if (typeof style.bottom !== 'undefined') {
347-
node.setPosition(Yoga.EDGE_BOTTOM, style.bottom)
378+
node.setPosition(
379+
Yoga.EDGE_BOTTOM,
380+
asPointPercentageLength(style.bottom, 'bottom')
381+
)
348382
}
349383
if (typeof style.left !== 'undefined') {
350-
node.setPosition(Yoga.EDGE_LEFT, style.left)
384+
node.setPosition(
385+
Yoga.EDGE_LEFT,
386+
asPointPercentageLength(style.left, 'left')
387+
)
351388
}
352389
if (typeof style.right !== 'undefined') {
353-
node.setPosition(Yoga.EDGE_RIGHT, style.right)
390+
node.setPosition(
391+
Yoga.EDGE_RIGHT,
392+
asPointPercentageLength(style.right, 'right')
393+
)
354394
}
355395

356396
if (typeof style.height !== 'undefined') {
357-
node.setHeight(style.height)
397+
node.setHeight(asPointAutoPercentageLength(style.height, 'height'))
358398
} else {
359399
node.setHeightAuto()
360400
}
361401
if (typeof style.width !== 'undefined') {
362-
node.setWidth(style.width)
402+
node.setWidth(asPointAutoPercentageLength(style.width, 'width'))
363403
} else {
364404
node.setWidthAuto()
365405
}

src/layout.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
*/
44

55
import type { ReactNode } from 'react'
6-
import type { Node as YogaNode } from 'yoga-wasm-web'
7-
8-
import getYoga from './yoga/index.js'
96
import {
107
isReactElement,
118
isClass,
129
buildXMLString,
1310
normalizeChildren,
1411
hasDangerouslySetInnerHTMLProp,
12+
getYoga,
13+
YogaNode,
1514
} from './utils.js'
1615
import { SVGNodeToImage } from './handler/preprocess.js'
1716
import computeStyle from './handler/compute.js'
@@ -262,10 +261,11 @@ export default async function* layout(
262261
children &&
263262
typeof children !== 'string' &&
264263
display !== 'flex' &&
265-
display !== 'none'
264+
display !== 'none' &&
265+
display !== 'contents'
266266
) {
267267
throw new Error(
268-
`Expected <div> to have explicit "display: flex" or "display: none" if it has more than one child node.`
268+
`Expected <div> to have explicit "display: flex", "display: contents", or "display: none" if it has more than one child node.`
269269
)
270270
}
271271
baseRenderResult = await rect(

src/satori.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ import type { ReactNode } from 'react'
22
import type { TwConfig } from 'twrnc'
33
import type { SatoriNode } from './layout.js'
44

5-
import getYoga, { init } from './yoga/index.js'
65
import layout from './layout.js'
76
import FontLoader, { FontOptions } from './font.js'
87
import svg from './builder/svg.js'
9-
import { segment } from './utils.js'
8+
import { getYoga, segment, TYoga } from './utils.js'
109
import { detectLanguageCode, LangCode, Locale } from './language.js'
1110
import getTw from './handler/tailwind.js'
1211
import { preProcessNode } from './handler/preprocess.js'
1312
import { cache, inflightRequests } from './handler/image.js'
14-
import type { Yoga } from 'yoga-wasm-web'
1513

1614
// We don't need to initialize the opentype instances every time.
1715
const fontCache = new WeakMap()
@@ -42,8 +40,6 @@ export type SatoriOptions = (
4240
}
4341
export type { SatoriNode }
4442

45-
export { init }
46-
4743
export default async function satori(
4844
element: ReactNode,
4945
options: SatoriOptions
@@ -197,7 +193,7 @@ export default async function satori(
197193
}
198194

199195
function getRootNode(
200-
Yoga: Yoga,
196+
Yoga: TYoga,
201197
pointScaleFactor?: SatoriOptions['pointScaleFactor']
202198
) {
203199
if (!pointScaleFactor) {

src/text/index.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* supported inline node is text. All other nodes are using block layout.
44
*/
55
import type { LayoutContext } from '../layout.js'
6-
import type { Yoga } from 'yoga-wasm-web'
7-
import getYoga from '../yoga/index.js'
86
import {
97
v,
108
segment,
@@ -13,6 +11,9 @@ import {
1311
isUndefined,
1412
isString,
1513
lengthToNumber,
14+
getYoga,
15+
TYoga,
16+
YogaNode,
1617
} from '../utils.js'
1718
import buildText, { container } from '../builder/text.js'
1819
import { buildDropShadow } from '../builder/shadow.js'
@@ -810,10 +811,7 @@ export default async function* buildTextNodes(
810811
return result
811812
}
812813

813-
function createTextContainerNode(
814-
Yoga: Yoga,
815-
textAlign: string
816-
): ReturnType<Yoga['Node']['create']> {
814+
function createTextContainerNode(Yoga: TYoga, textAlign: string): YogaNode {
817815
// Create a container node for this text fragment.
818816
const textContainer = Yoga.Node.create()
819817
textContainer.setAlignItems(Yoga.ALIGN_BASELINE)

0 commit comments

Comments
 (0)