@wix/zero-config-implementation 1.62.0 → 1.64.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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.62.0",
7
+ "version": "1.64.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -39,6 +39,7 @@
39
39
  }
40
40
  },
41
41
  "dependencies": {
42
+ "@wix/builder-services-wrapper": "^1.42.0",
42
43
  "@wix/react-component-schema": "1.6.0"
43
44
  },
44
45
  "devDependencies": {
@@ -80,5 +81,5 @@
80
81
  ]
81
82
  }
82
83
  },
83
- "falconPackageHash": "bd7035914229aba9dd44c1b5098381d537a4c3b4e7c58e03bd0c5e84"
84
+ "falconPackageHash": "7a686c953d35285ef8246e7de2f6deefabd15fa341615b1e38ff1f27"
84
85
  }
@@ -200,4 +200,162 @@ describe('resolveLonghand', () => {
200
200
  expect(resolveLonghand('background', values)).toBe('#001')
201
201
  })
202
202
  })
203
+
204
+ describe('font composition from longhands', () => {
205
+ it('composes size and family alone', () => {
206
+ const values = new Map([
207
+ ['fontSize', '14px'],
208
+ ['fontFamily', 'Arial'],
209
+ ])
210
+ expect(resolveLonghand('font', values)).toBe('14px Arial')
211
+ })
212
+
213
+ it('includes weight before size', () => {
214
+ const values = new Map([
215
+ ['fontSize', '16px'],
216
+ ['fontFamily', "'Helvetica Neue'"],
217
+ ['fontWeight', '600'],
218
+ ])
219
+ expect(resolveLonghand('font', values)).toBe("600 16px 'Helvetica Neue'")
220
+ })
221
+
222
+ it('joins size and line-height with /', () => {
223
+ const values = new Map([
224
+ ['fontSize', '14px'],
225
+ ['fontFamily', 'sans-serif'],
226
+ ['lineHeight', '1.5'],
227
+ ])
228
+ expect(resolveLonghand('font', values)).toBe('14px/1.5 sans-serif')
229
+ })
230
+
231
+ it('combines weight, size/line-height, family in spec order', () => {
232
+ const values = new Map([
233
+ ['fontStyle', 'italic'],
234
+ ['fontWeight', '700'],
235
+ ['fontSize', '20px'],
236
+ ['lineHeight', '1.3'],
237
+ ['fontFamily', 'serif'],
238
+ ])
239
+ expect(resolveLonghand('font', values)).toBe('italic 700 20px/1.3 serif')
240
+ })
241
+
242
+ it("omits modifiers with literal value 'normal'", () => {
243
+ const values = new Map([
244
+ ['fontStyle', 'normal'],
245
+ ['fontWeight', 'normal'],
246
+ ['fontSize', '16px'],
247
+ ['lineHeight', 'normal'],
248
+ ['fontFamily', 'Arial'],
249
+ ])
250
+ expect(resolveLonghand('font', values)).toBe('16px Arial')
251
+ })
252
+
253
+ it('returns undefined when fontSize is missing (strict)', () => {
254
+ const values = new Map([['fontFamily', 'Arial']])
255
+ expect(resolveLonghand('font', values)).toBeUndefined()
256
+ })
257
+
258
+ it('returns undefined when fontFamily is missing (strict)', () => {
259
+ const values = new Map([['fontSize', '14px']])
260
+ expect(resolveLonghand('font', values)).toBeUndefined()
261
+ })
262
+
263
+ it('returns undefined when only optional modifiers are present', () => {
264
+ const values = new Map([
265
+ ['fontWeight', '700'],
266
+ ['fontStyle', 'italic'],
267
+ ])
268
+ expect(resolveLonghand('font', values)).toBeUndefined()
269
+ })
270
+
271
+ it('prefers literal font shorthand over longhand composition', () => {
272
+ const values = new Map([
273
+ ['font', '400 14px Arial'],
274
+ ['fontSize', '99px'],
275
+ ['fontFamily', 'WrongFont'],
276
+ ])
277
+ expect(resolveLonghand('font', values)).toBe('400 14px Arial')
278
+ })
279
+ })
280
+
281
+ describe('gap composition from row-gap + column-gap', () => {
282
+ it('composes two distinct axis values in row/column order', () => {
283
+ const values = new Map([
284
+ ['rowGap', '8px'],
285
+ ['columnGap', '10px'],
286
+ ])
287
+ expect(resolveLonghand('gap', values)).toBe('8px 10px')
288
+ })
289
+
290
+ it('collapses to a single value when both axes are equal', () => {
291
+ const values = new Map([
292
+ ['rowGap', '4px'],
293
+ ['columnGap', '4px'],
294
+ ])
295
+ expect(resolveLonghand('gap', values)).toBe('4px')
296
+ })
297
+
298
+ it('returns undefined when rowGap is missing (strict)', () => {
299
+ const values = new Map([['columnGap', '10px']])
300
+ expect(resolveLonghand('gap', values)).toBeUndefined()
301
+ })
302
+
303
+ it('returns undefined when columnGap is missing (strict)', () => {
304
+ const values = new Map([['rowGap', '8px']])
305
+ expect(resolveLonghand('gap', values)).toBeUndefined()
306
+ })
307
+
308
+ it('prefers literal gap shorthand over longhand composition', () => {
309
+ const values = new Map([
310
+ ['gap', '12px'],
311
+ ['rowGap', '99px'],
312
+ ['columnGap', '99px'],
313
+ ])
314
+ expect(resolveLonghand('gap', values)).toBe('12px')
315
+ })
316
+ })
317
+
318
+ describe('textDecorationLine extraction from text-decoration shorthand', () => {
319
+ it('extracts the lone line keyword', () => {
320
+ expect(resolveLonghand('textDecorationLine', new Map([['textDecoration', 'underline']]))).toBe('underline')
321
+ })
322
+
323
+ it('extracts the line keyword and drops style + color tokens', () => {
324
+ expect(resolveLonghand('textDecorationLine', new Map([['textDecoration', 'underline wavy red']]))).toBe(
325
+ 'underline',
326
+ )
327
+ })
328
+
329
+ it('ignores token order — line keyword can appear anywhere', () => {
330
+ expect(resolveLonghand('textDecorationLine', new Map([['textDecoration', 'red wavy underline']]))).toBe(
331
+ 'underline',
332
+ )
333
+ })
334
+
335
+ it('keeps multiple line keywords joined (e.g. underline + overline)', () => {
336
+ expect(resolveLonghand('textDecorationLine', new Map([['textDecoration', 'underline overline']]))).toBe(
337
+ 'underline overline',
338
+ )
339
+ })
340
+
341
+ it('extracts `none`', () => {
342
+ expect(resolveLonghand('textDecorationLine', new Map([['textDecoration', 'none']]))).toBe('none')
343
+ })
344
+
345
+ it('returns undefined when shorthand has no line keyword (just color/style)', () => {
346
+ expect(resolveLonghand('textDecorationLine', new Map([['textDecoration', 'red']]))).toBeUndefined()
347
+ })
348
+
349
+ it('returns undefined when textDecoration is absent', () => {
350
+ expect(resolveLonghand('textDecorationLine', new Map())).toBeUndefined()
351
+ })
352
+
353
+ it('prefers literal textDecorationLine over extracting from shorthand', () => {
354
+ const values = new Map([
355
+ ['textDecorationLine', 'overline'],
356
+ ['textDecoration', 'underline red'],
357
+ ])
358
+ expect(resolveLonghand('textDecorationLine', values)).toBe('overline')
359
+ })
360
+ })
203
361
  })
@@ -1,5 +1,6 @@
1
1
  import type { CSS_PROPERTIES } from '@wix/react-component-schema'
2
2
  import { type Value, generate, parse } from 'css-tree'
3
+ import { SHORTHAND_FALLBACKS } from './css-shorthand-composer'
3
4
 
4
5
  /**
5
6
  * Resolves a CSS longhand to its default value by reading the developer's
@@ -195,6 +196,25 @@ function fromCorner(from: string, corner: keyof CornerValues): Fallback {
195
196
  }
196
197
  }
197
198
 
199
+ /**
200
+ * `text-decoration: <line> || <style> || <color>` — order is free and any
201
+ * subset is valid. The schema asks for `textDecorationLine` specifically, so
202
+ * we filter the shorthand's tokens down to just the line-keyword ones.
203
+ *
204
+ * Closed set of legal line keywords per the CSS spec (case-insensitive).
205
+ */
206
+ const TEXT_DECORATION_LINE_KEYWORDS = new Set(['none', 'underline', 'overline', 'line-through', 'blink'])
207
+
208
+ const fromTextDecorationLine: Fallback = (values) => {
209
+ const shorthand = values.get('textDecoration')
210
+ if (shorthand === undefined) return undefined
211
+ const lineTokens = splitTopLevelTokens(shorthand).filter((token) =>
212
+ TEXT_DECORATION_LINE_KEYWORDS.has(token.toLowerCase()),
213
+ )
214
+ if (lineTokens.length === 0) return undefined
215
+ return lineTokens.join(' ')
216
+ }
217
+
198
218
  // ─────────────────────────────────────────────────────────────────────────────
199
219
  // Resolution table
200
220
  //
@@ -230,6 +250,14 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
230
250
  // Background — populate the schema's `background` from `background-color` when
231
251
  // the developer only wrote the color.
232
252
  background: [fromKey('backgroundColor')],
253
+
254
+ // Text-decoration — `text-decoration: underline wavy red` carries line, style,
255
+ // and color in one declaration. Pick out just the line tokens for the schema.
256
+ textDecorationLine: [fromTextDecorationLine],
257
+
258
+ // Shorthand targets (e.g. `font`) live in css-shorthand-composer.ts and are
259
+ // merged in here — composer-driven, conceptually the inverse of the rows above.
260
+ ...SHORTHAND_FALLBACKS,
233
261
  }
234
262
 
235
263
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Composes a CSS shorthand value (`font`, …) from longhand declarations the
3
+ * developer wrote. Inverse of the splitters in `css-longhand-resolver.ts`:
4
+ *
5
+ * resolver splits: `padding: 1rem` → `paddingTop: "1rem"`
6
+ * composer assembles: `font-family + font-size` → `font: "16px Arial"`
7
+ *
8
+ * Strict policy: when the CSS spec marks a piece as required and it's missing
9
+ * from the map, the composer returns `undefined` rather than emit a partial
10
+ * string the editor's value parser would reject as invalid shorthand.
11
+ */
12
+
13
+ type Composer = (values: Map<string, string>) => string | undefined
14
+
15
+ /**
16
+ * Builds a `font` shorthand value from longhand declarations, per CSS spec:
17
+ * [ <font-style> || <font-variant> || <font-weight> || <font-stretch> ]?
18
+ * <font-size> [ / <line-height> ]? <font-family>
19
+ *
20
+ * Returns undefined unless both required pieces (font-size and font-family)
21
+ * are present. Modifiers with the literal value `normal` are omitted (they're
22
+ * the shorthand's implicit default and would add noise to the emitted string).
23
+ */
24
+ export function composeFontShorthand(values: Map<string, string>): string | undefined {
25
+ const size = values.get('fontSize')
26
+ const family = values.get('fontFamily')
27
+ if (size === undefined || family === undefined) return undefined
28
+
29
+ const parts: string[] = []
30
+
31
+ // Optional modifiers (style, variant, weight, stretch). Order matters per CSS
32
+ // spec but their order relative to each other is free; size always follows.
33
+ for (const key of ['fontStyle', 'fontVariant', 'fontWeight', 'fontStretch']) {
34
+ const value = values.get(key)
35
+ if (value !== undefined && value.toLowerCase() !== 'normal') parts.push(value)
36
+ }
37
+
38
+ const lineHeight = values.get('lineHeight')
39
+ if (lineHeight !== undefined && lineHeight.toLowerCase() !== 'normal') {
40
+ parts.push(`${size}/${lineHeight}`)
41
+ } else {
42
+ parts.push(size)
43
+ }
44
+
45
+ // Family names may carry CSS double-quotes (`"Helvetica Neue"`). Normalize to
46
+ // single quotes so the emitted shorthand can be embedded in JSON / snapshot
47
+ // strings without the inner `"` colliding with the outer delimiter. CSS
48
+ // accepts both quote styles equally for family names.
49
+ parts.push(family.replace(/"/g, "'"))
50
+ return parts.join(' ')
51
+ }
52
+
53
+ /**
54
+ * Builds a `gap` shorthand value from `row-gap` + `column-gap` longhands.
55
+ *
56
+ * gap: <row-gap> [<column-gap>]?
57
+ *
58
+ * Strict: both axes required. Composing from just one would lie about intent —
59
+ * `gap: 8px` sets BOTH axes to 8px, whereas the developer's `row-gap: 8px`
60
+ * alone leaves column-gap at its initial value. Collapses to a single value
61
+ * when the two axes are equal, matching how developers write it by hand.
62
+ */
63
+ export function composeGapShorthand(values: Map<string, string>): string | undefined {
64
+ const rowGap = values.get('rowGap')
65
+ const columnGap = values.get('columnGap')
66
+ if (rowGap === undefined || columnGap === undefined) return undefined
67
+
68
+ if (rowGap === columnGap) return rowGap
69
+ return `${rowGap} ${columnGap}`
70
+ }
71
+
72
+ /**
73
+ * Routing table for shorthand targets the schema asks for. Maps each target
74
+ * to the composer(s) that build its value from longhand declarations. The
75
+ * resolver in `css-longhand-resolver.ts` merges this in when dispatching
76
+ * `resolveLonghand`.
77
+ *
78
+ * Add new shorthand targets here as they're needed (background, border, …).
79
+ */
80
+ export const SHORTHAND_FALLBACKS: Record<string, Composer[]> = {
81
+ font: [composeFontShorthand],
82
+ gap: [composeGapShorthand],
83
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import { WixServicesWrapper } from '@wix/builder-services-wrapper'
1
2
  import type { EditorReactComponent } from '@wix/react-component-schema'
2
3
  import { Result, type ResultAsync, errAsync, okAsync } from 'neverthrow'
3
- import type { ComponentType } from 'react'
4
+ import React, { type ComponentType } from 'react'
4
5
 
5
6
  import { toEditorReactComponent } from './converters'
6
7
  import { IoError, type NotFoundError, ParseError } from './errors'
@@ -12,6 +13,12 @@ import { findComponent, findDefaultComponent, loadModule } from './module-loader
12
13
  import type { LoadModuleFailure } from './module-loader'
13
14
  import { compileTsFile } from './ts-compiler'
14
15
 
16
+ const defaultWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => {
17
+ const WrappedComponent: ComponentType<unknown> = (props) =>
18
+ React.createElement(WixServicesWrapper, null, React.createElement(Component, props as Record<string, unknown>))
19
+ return WrappedComponent
20
+ }
21
+
15
22
  // ─────────────────────────────────────────────────────────────────────────────
16
23
  // Types
17
24
  // ─────────────────────────────────────────────────────────────────────────────
@@ -54,6 +61,11 @@ export function extractComponentManifestResult(
54
61
  options?.onError?.(error)
55
62
  }
56
63
 
64
+ const optionsWithDefaults: ExtractComponentManifestOptions = {
65
+ ...options,
66
+ wrapper: options?.wrapper ?? defaultWrapper,
67
+ }
68
+
57
69
  // Step 1: Compile TypeScript (fatal)
58
70
  return compileTsFile(componentPath)
59
71
  .andThen((program) => {
@@ -125,7 +137,7 @@ export function extractComponentManifestResult(
125
137
  cssImportPaths,
126
138
  !!failure,
127
139
  report,
128
- options,
140
+ optionsWithDefaults,
129
141
  compiledEntryPath,
130
142
  )
131
143
  if (!processResult.ok) {