@wix/zero-config-implementation 1.78.0 → 1.80.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.
Files changed (37) hide show
  1. package/README.md +4 -0
  2. package/dist/{index--6I20lGB.js → index-B8oF7hWg.js} +15405 -15073
  3. package/dist/{index-DAAmOM1c.js → index-CGnbp9jn.js} +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +3 -3
  6. package/src/converters/to-editor-component.ts +2 -2
  7. package/src/extensions/context-providers/context.test.ts +181 -0
  8. package/src/extensions/context-providers/context.ts +83 -0
  9. package/src/extensions/context-providers/mock-provider.test.ts +44 -0
  10. package/src/extensions/context-providers/mock-provider.ts +210 -0
  11. package/src/extensions/context-providers/types.ts +22 -0
  12. package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
  13. package/src/extensions/ref-elements/context.ts +168 -0
  14. package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
  15. package/src/extensions/shared/ast-scanner.ts +85 -0
  16. package/src/extensions/shared/catalog-loader.ts +64 -0
  17. package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
  18. package/src/index.ts +108 -67
  19. package/src/information-extractors/css/parse.test.ts +58 -0
  20. package/src/information-extractors/css/parse.ts +54 -7
  21. package/src/information-extractors/css/sass-adapter.test.ts +137 -0
  22. package/src/information-extractors/css/sass-adapter.ts +59 -1
  23. package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
  24. package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
  25. package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
  26. package/src/information-extractors/react/utils/mock-generator.ts +104 -1
  27. package/src/manifest-pipeline.ts +4 -1
  28. package/src/module-loader.test.ts +1 -1
  29. package/src/module-loader.ts +1 -1
  30. package/src/react-runtime-loader.ts +28 -2
  31. package/src/ref-elements/context.ts +0 -280
  32. /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
  33. /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
  34. /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
  35. /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
  36. /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
  37. /package/src/{ref-elements/module-specifier.ts → extensions/shared/package-specifier.ts} +0 -0
@@ -128,6 +128,8 @@ function parseAllProperties(
128
128
  try {
129
129
  const ast = parse(cssString, { parseCustomProperty: true })
130
130
 
131
+ const valueAliases = parseValueAliases(ast)
132
+
131
133
  walk(ast, {
132
134
  visit: 'Rule',
133
135
  enter(this: WalkContext, rule: Rule) {
@@ -135,7 +137,7 @@ function parseAllProperties(
135
137
  const properties: CSSProperty[] = []
136
138
  for (const child of rule.block.children) {
137
139
  if (child.type !== 'Declaration') continue
138
- const extracted = extractProperty(child, varUsagesByProperty)
140
+ const extracted = extractProperty(child, varUsagesByProperty, valueAliases)
139
141
  if (extracted) {
140
142
  properties.push(extracted)
141
143
  }
@@ -181,13 +183,18 @@ function parseAllProperties(
181
183
  * Extracts property name, value, and var() references from a css-tree Declaration node.
182
184
  * Also records var() usages in the provided tracking map.
183
185
  */
184
- function extractProperty(declaration: Declaration, varUsagesByProperty: Map<string, Set<string>>): CSSProperty | null {
186
+ function extractProperty(
187
+ declaration: Declaration,
188
+ varUsagesByProperty: Map<string, Set<string>>,
189
+ valueAliases: Map<string, string>,
190
+ ): CSSProperty | null {
185
191
  try {
186
192
  const name = declaration.property
187
- const value = generate(declaration.value)
188
- if (!name || !value) return null
193
+ if (!name) return null
189
194
 
190
- const varRefs = extractVarNames(declaration.value)
195
+ const varRefs = extractVarNames(declaration.value, valueAliases)
196
+ const value = resolveVarIdentifiers(declaration.value, valueAliases)
197
+ if (!value) return null
191
198
 
192
199
  for (const varName of varRefs) {
193
200
  const usageSet = varUsagesByProperty.get(varName) ?? new Set<string>()
@@ -323,11 +330,51 @@ export function resolveCssPropertyValue(
323
330
  return resolvedPropertyValue.kind === 'resolved' ? resolvedPropertyValue.value : undefined
324
331
  }
325
332
 
333
+ /**
334
+ * Parses @value local declarations (e.g. `@value alias: --real-prop;`) from the
335
+ * CSS AST and returns a map of alias → real CSS custom property name.
336
+ * Import-form @value rules (`@value foo from "module"`) are ignored — they rely
337
+ * on the convention that the alias name matches the custom property name.
338
+ */
339
+ function parseValueAliases(cssAst: CssNode): Map<string, string> {
340
+ const aliases = new Map<string, string>()
341
+ walk(cssAst, {
342
+ visit: 'Atrule',
343
+ enter(atruleNode: Atrule) {
344
+ if (atruleNode.name !== 'value' || atruleNode.block !== null || !atruleNode.prelude) return
345
+ const preludeText = generate(atruleNode.prelude).trim()
346
+ const match = preludeText.match(/^([\w-]+)\s*:\s*(--[\w-]+)\s*$/)
347
+ if (match) aliases.set(match[1], match[2])
348
+ },
349
+ })
350
+ return aliases
351
+ }
352
+
353
+ /**
354
+ * Resolves bare var() identifiers to CSS custom property names by walking the AST.
355
+ * Local @value aliases (e.g. `@value alias: --real-prop`) are substituted with
356
+ * their real property name; unaliased identifiers get `--` prepended.
357
+ * Operating on the AST avoids false matches inside CSS string literals (e.g. content: "var(foo)").
358
+ */
359
+ function resolveVarIdentifiers(valueNode: CssNode, valueAliases: Map<string, string>): string {
360
+ walk(valueNode, {
361
+ visit: 'Function',
362
+ enter(functionNode: FunctionNode) {
363
+ if (functionNode.name !== 'var') return
364
+ const firstChild = functionNode.children.first
365
+ if (firstChild?.type === 'Identifier' && !firstChild.name.startsWith('--')) {
366
+ firstChild.name = valueAliases.get(firstChild.name) ?? `--${firstChild.name}`
367
+ }
368
+ },
369
+ })
370
+ return generate(valueNode)
371
+ }
372
+
326
373
  /**
327
374
  * Extracts all CSS custom property names referenced via var() by walking
328
375
  * the declaration value AST for Function nodes named "var".
329
376
  */
330
- function extractVarNames(valueNode: CssNode): string[] {
377
+ function extractVarNames(valueNode: CssNode, valueAliases: Map<string, string>): string[] {
331
378
  const varNames: string[] = []
332
379
 
333
380
  walk(valueNode, {
@@ -338,7 +385,7 @@ function extractVarNames(valueNode: CssNode): string[] {
338
385
  const firstChild = functionNode.children.first
339
386
  if (firstChild && firstChild.type === 'Identifier') {
340
387
  const identName = firstChild.name
341
- varNames.push(identName.startsWith('--') ? identName : `--${identName}`)
388
+ varNames.push(identName.startsWith('--') ? identName : (valueAliases.get(identName) ?? `--${identName}`))
342
389
  }
343
390
  },
344
391
  })
@@ -0,0 +1,137 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import { compileSass } from './sass-adapter'
6
+
7
+ function writePackageJson(packageDir: string, content: object) {
8
+ writeFileSync(join(packageDir, 'package.json'), JSON.stringify(content))
9
+ }
10
+
11
+ function createPackageDir(tempDir: string, packageName: string): string {
12
+ const packageDir = join(tempDir, 'node_modules', packageName)
13
+ mkdirSync(packageDir, { recursive: true })
14
+ return packageDir
15
+ }
16
+
17
+ describe('compileSass — npm package importer', () => {
18
+ let tempDir: string
19
+
20
+ beforeEach(() => {
21
+ tempDir = mkdtempSync(join(tmpdir(), 'sass-adapter-test-'))
22
+ })
23
+
24
+ afterEach(() => {
25
+ rmSync(tempDir, { recursive: true, force: true })
26
+ })
27
+
28
+ it('resolves a simple string export entry', () => {
29
+ const packageDir = createPackageDir(tempDir, 'my-pkg')
30
+ mkdirSync(join(packageDir, 'dist'), { recursive: true })
31
+ writeFileSync(join(packageDir, 'dist', '_vars.scss'), '$color: red;')
32
+ writePackageJson(packageDir, { exports: { './vars': './dist/_vars.scss' } })
33
+
34
+ const scssFile = join(tempDir, 'test.scss')
35
+ writeFileSync(scssFile, '@use "my-pkg/vars" as theme; .a { color: theme.$color; }')
36
+
37
+ const result = compileSass(scssFile)
38
+ expect(result.isOk()).toBe(true)
39
+ expect(result._unsafeUnwrap()).toContain('color: red')
40
+ })
41
+
42
+ it('resolves a conditional object export entry via "sass" key', () => {
43
+ const packageDir = createPackageDir(tempDir, 'my-pkg')
44
+ mkdirSync(join(packageDir, 'dist'), { recursive: true })
45
+ writeFileSync(join(packageDir, 'dist', '_vars.scss'), '$color: blue;')
46
+ writePackageJson(packageDir, {
47
+ exports: { './vars': { sass: './dist/_vars.scss', default: './dist/vars.css' } },
48
+ })
49
+
50
+ const scssFile = join(tempDir, 'test.scss')
51
+ writeFileSync(scssFile, '@use "my-pkg/vars" as theme; .a { color: theme.$color; }')
52
+
53
+ const result = compileSass(scssFile)
54
+ expect(result.isOk()).toBe(true)
55
+ expect(result._unsafeUnwrap()).toContain('color: blue')
56
+ })
57
+
58
+ it('resolves a conditional object export entry via "default" key when "sass" is absent', () => {
59
+ const packageDir = createPackageDir(tempDir, 'my-pkg')
60
+ mkdirSync(join(packageDir, 'dist'), { recursive: true })
61
+ writeFileSync(join(packageDir, 'dist', '_vars.scss'), '$color: green;')
62
+ writePackageJson(packageDir, {
63
+ exports: { './vars': { default: './dist/_vars.scss' } },
64
+ })
65
+
66
+ const scssFile = join(tempDir, 'test.scss')
67
+ writeFileSync(scssFile, '@use "my-pkg/vars" as theme; .a { color: theme.$color; }')
68
+
69
+ const result = compileSass(scssFile)
70
+ expect(result.isOk()).toBe(true)
71
+ expect(result._unsafeUnwrap()).toContain('color: green')
72
+ })
73
+
74
+ it('falls back to .scss file extension when exports field has no matching entry', () => {
75
+ const packageDir = createPackageDir(tempDir, 'my-pkg')
76
+ writeFileSync(join(packageDir, 'vars.scss'), '$color: orange;')
77
+ writePackageJson(packageDir, { exports: { '.': './index.js' } })
78
+
79
+ const scssFile = join(tempDir, 'test.scss')
80
+ writeFileSync(scssFile, '@use "my-pkg/vars" as theme; .a { color: theme.$color; }')
81
+
82
+ const result = compileSass(scssFile)
83
+ expect(result.isOk()).toBe(true)
84
+ expect(result._unsafeUnwrap()).toContain('color: orange')
85
+ })
86
+
87
+ it('resolves a scoped package', () => {
88
+ const packageDir = createPackageDir(tempDir, '@scope/pkg')
89
+ mkdirSync(join(packageDir, 'dist'), { recursive: true })
90
+ writeFileSync(join(packageDir, 'dist', '_theme.scss'), '$color: purple;')
91
+ writePackageJson(packageDir, { exports: { './theme': './dist/_theme.scss' } })
92
+
93
+ const scssFile = join(tempDir, 'test.scss')
94
+ writeFileSync(scssFile, '@use "@scope/pkg/theme" as theme; .a { color: theme.$color; }')
95
+
96
+ const result = compileSass(scssFile)
97
+ expect(result.isOk()).toBe(true)
98
+ expect(result._unsafeUnwrap()).toContain('color: purple')
99
+ })
100
+
101
+ it('returns err for an unresolvable package import', () => {
102
+ const scssFile = join(tempDir, 'test.scss')
103
+ writeFileSync(scssFile, '@use "nonexistent-pkg/vars";')
104
+
105
+ const result = compileSass(scssFile)
106
+ expect(result.isErr()).toBe(true)
107
+ })
108
+
109
+ it('returns err for a package with invalid JSON in package.json', () => {
110
+ const packageDir = createPackageDir(tempDir, 'bad-pkg')
111
+ mkdirSync(join(packageDir, 'dist'), { recursive: true })
112
+ writeFileSync(join(packageDir, 'package.json'), 'not valid json {{{')
113
+
114
+ const scssFile = join(tempDir, 'test.scss')
115
+ writeFileSync(scssFile, '@use "bad-pkg/vars";')
116
+
117
+ const result = compileSass(scssFile)
118
+ expect(result.isErr()).toBe(true)
119
+ })
120
+
121
+ it('traverses parent directories to find node_modules', () => {
122
+ const packageDir = createPackageDir(tempDir, 'my-pkg')
123
+ mkdirSync(join(packageDir, 'dist'), { recursive: true })
124
+ writeFileSync(join(packageDir, 'dist', '_vars.scss'), '$color: teal;')
125
+ writePackageJson(packageDir, { exports: { './vars': './dist/_vars.scss' } })
126
+
127
+ // SCSS file is in a subdirectory — node_modules is in the parent (tempDir)
128
+ const subDir = join(tempDir, 'src', 'components')
129
+ mkdirSync(subDir, { recursive: true })
130
+ const scssFile = join(subDir, 'test.scss')
131
+ writeFileSync(scssFile, '@use "my-pkg/vars" as theme; .a { color: theme.$color; }')
132
+
133
+ const result = compileSass(scssFile)
134
+ expect(result.isOk()).toBe(true)
135
+ expect(result._unsafeUnwrap()).toContain('color: teal')
136
+ })
137
+ })
@@ -1,6 +1,7 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, readFileSync } from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
3
  import { dirname, join } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
4
5
  import { Result, err } from 'neverthrow'
5
6
 
6
7
  function collectNodeModulesPaths(filePath: string): string[] {
@@ -16,6 +17,62 @@ function collectNodeModulesPaths(filePath: string): string[] {
16
17
  return paths
17
18
  }
18
19
 
20
+ function resolvePackageExport(packageDir: string, subPath: string): string | null {
21
+ const packageJsonPath = join(packageDir, 'package.json')
22
+ if (!existsSync(packageJsonPath)) return null
23
+ let packageJson: { exports?: Record<string, string | Record<string, string>> }
24
+ try {
25
+ packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
26
+ } catch (error) {
27
+ console.error(`Failed to parse ${packageJsonPath}:`, error)
28
+ return null
29
+ }
30
+ const exportEntry = packageJson.exports?.[subPath]
31
+ let exportPath: string | undefined
32
+ if (typeof exportEntry === 'string') {
33
+ exportPath = exportEntry
34
+ } else if (typeof exportEntry === 'object' && exportEntry !== null) {
35
+ exportPath = exportEntry.sass ?? exportEntry.default
36
+ }
37
+ if (typeof exportPath === 'string') {
38
+ const resolvedPath = join(packageDir, exportPath)
39
+ return existsSync(resolvedPath) ? resolvedPath : null
40
+ }
41
+ return null
42
+ }
43
+
44
+ function createNodePackageImporter(startDir: string) {
45
+ return {
46
+ findFileUrl(url: string): URL | null {
47
+ if (url.startsWith('.') || url.startsWith('/')) return null
48
+
49
+ const isScoped = url.startsWith('@')
50
+ const parts = url.split('/')
51
+ const packageName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0]
52
+ const subpathParts = isScoped ? parts.slice(2) : parts.slice(1)
53
+ const subPath = subpathParts.length > 0 ? `./${subpathParts.join('/')}` : '.'
54
+
55
+ let dir = startDir
56
+ while (true) {
57
+ const packageDir = join(dir, 'node_modules', packageName)
58
+ if (existsSync(packageDir)) {
59
+ const resolvedPath = resolvePackageExport(packageDir, subPath)
60
+ if (resolvedPath) return pathToFileURL(resolvedPath)
61
+
62
+ if (subpathParts.length > 0) {
63
+ const fallback = join(packageDir, `${subpathParts.join('/')}.scss`)
64
+ if (existsSync(fallback)) return pathToFileURL(fallback)
65
+ }
66
+ }
67
+ const parent = dirname(dir)
68
+ if (parent === dir) break
69
+ dir = parent
70
+ }
71
+ return null
72
+ },
73
+ }
74
+ }
75
+
19
76
  const require = createRequire(import.meta.url)
20
77
 
21
78
  export function compileSass(filePath: string): Result<string, Error> {
@@ -34,6 +91,7 @@ export function compileSass(filePath: string): Result<string, Error> {
34
91
  () =>
35
92
  sass.compile(filePath, {
36
93
  loadPaths,
94
+ importers: [createNodePackageImporter(dirname(filePath))],
37
95
  }).css,
38
96
  (thrown) => (thrown instanceof Error ? thrown : new Error(String(thrown))),
39
97
  )()
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { createRefElementMarker } from '../../../ref-elements/path-utils'
3
- import type { RefElementMatch } from '../../../ref-elements/types'
2
+ import { createRefElementMarker } from '../../../extensions/ref-elements/path-utils'
3
+ import type { RefElementMatch } from '../../../extensions/ref-elements/types'
4
4
  import { ExtractorStore } from './core/store'
5
5
  import { createPropTrackerExtractor } from './prop-tracker'
6
6
 
@@ -7,9 +7,9 @@
7
7
 
8
8
  import type { HTMLAttributes } from 'react'
9
9
  import { TRACE_ATTR } from '../../../component-renderer'
10
- import { readRefElementComponentType } from '../../../ref-elements/component-tag'
11
- import { parseRefElementMarker } from '../../../ref-elements/path-utils'
12
- import type { RefElementMatch } from '../../../ref-elements/types'
10
+ import { readRefElementComponentType } from '../../../extensions/ref-elements/component-tag'
11
+ import { parseRefElementMarker } from '../../../extensions/ref-elements/path-utils'
12
+ import type { RefElementMatch } from '../../../extensions/ref-elements/types'
13
13
  import { findPreferredSemanticClass, normalizeClassNames } from '../../../utils/css-class'
14
14
  import type { PropSpyMeta, TrackingStores } from '../types'
15
15
  import { type PropSpyRegistrar, generateMockProps, resetMockCounter } from '../utils/mock-generator'
@@ -1,7 +1,7 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createRefElementMarker, parseRefElementMarker } from '../../../ref-elements/path-utils'
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+ import { createRefElementMarker, parseRefElementMarker } from '../../../extensions/ref-elements/path-utils'
3
3
  import type { ComponentInfo } from '../../ts/types'
4
- import { generateMockProps, resetMockCounter } from './mock-generator'
4
+ import { generateMockContextValues, generateMockProps, resetMockCounter } from './mock-generator'
5
5
 
6
6
  describe('generateMockProps', () => {
7
7
  it('injects a dedicated ref-element id marker into each eligible elementProps branch', () => {
@@ -129,3 +129,107 @@ describe('generateMockProps', () => {
129
129
  expect(parseRefElementMarker(ctaProps.id)).toBe('card.elementProps.cta')
130
130
  })
131
131
  })
132
+
133
+ describe('generateMockContextValues', () => {
134
+ beforeEach(() => {
135
+ resetMockCounter()
136
+ })
137
+
138
+ it('returns an empty object for an empty schema', () => {
139
+ expect(generateMockContextValues({ items: {} })).toEqual({})
140
+ })
141
+
142
+ it('maps text to a string', () => {
143
+ const result = generateMockContextValues({ items: { title: { dataType: 'text' } } })
144
+ expect(typeof result.title).toBe('string')
145
+ })
146
+
147
+ it('maps number to a number', () => {
148
+ const result = generateMockContextValues({ items: { count: { dataType: 'number' } } })
149
+ expect(typeof result.count).toBe('number')
150
+ })
151
+
152
+ it('maps boolean to a boolean', () => {
153
+ const result = generateMockContextValues({ items: { active: { dataType: 'boolean' } } })
154
+ expect(typeof result.active).toBe('boolean')
155
+ })
156
+
157
+ it('maps function to a function', () => {
158
+ const result = generateMockContextValues({ items: { onAction: { dataType: 'function' } } })
159
+ expect(typeof result.onAction).toBe('function')
160
+ })
161
+
162
+ it('maps richText to an HTML paragraph string', () => {
163
+ const result = generateMockContextValues({ items: { body: { dataType: 'richText' } } })
164
+ expect(result.body as string).toMatch(/^<p>/)
165
+ })
166
+
167
+ it('maps image to an image-shaped object', () => {
168
+ const result = generateMockContextValues({ items: { photo: { dataType: 'image' } } })
169
+ expect(result.photo).toMatchObject({
170
+ uri: expect.any(String),
171
+ url: expect.any(String),
172
+ width: expect.any(Number),
173
+ height: expect.any(Number),
174
+ })
175
+ })
176
+
177
+ it('maps link to a link-shaped object', () => {
178
+ const result = generateMockContextValues({ items: { destination: { dataType: 'link' } } })
179
+ expect(result.destination).toMatchObject({ href: expect.any(String), target: expect.any(String) })
180
+ })
181
+
182
+ it('maps data with a nested schema to a nested object', () => {
183
+ const result = generateMockContextValues({
184
+ items: {
185
+ event: {
186
+ dataType: 'data',
187
+ data: { items: { name: { dataType: 'text' } } },
188
+ },
189
+ },
190
+ })
191
+ expect(result.event).toMatchObject({ name: expect.any(String) })
192
+ })
193
+
194
+ it('maps data with an empty schema to a permissive mock that handles Object.keys safely', () => {
195
+ const result = generateMockContextValues({
196
+ items: { event: { dataType: 'data', data: { items: {} } } },
197
+ })
198
+ expect(() => Object.keys(result.event as object)).not.toThrow()
199
+ expect(Object.keys(result.event as object)).toHaveLength(0)
200
+ })
201
+
202
+ it('returns a permissive fallback for undeclared context fields so deep access chains do not crash', () => {
203
+ const result = generateMockContextValues({ items: {} })
204
+ const undeclared = (result as Record<string, unknown>).anything
205
+ expect(() => Object.keys(undeclared as object)).not.toThrow()
206
+ expect(Object.keys(undeclared as object)).toHaveLength(0)
207
+ const deeplyNested = (undeclared as Record<string, unknown>).deeply
208
+ expect(() => (deeplyNested as Record<string, unknown>)?.nested).not.toThrow()
209
+ })
210
+
211
+ it('maps arrayItems with a nested schema to a single-element array containing the element object', () => {
212
+ const result = generateMockContextValues({
213
+ items: {
214
+ tickets: {
215
+ dataType: 'arrayItems',
216
+ arrayItems: { item: { dataType: 'data', data: { items: { price: { dataType: 'number' } } } } },
217
+ },
218
+ },
219
+ })
220
+ expect(result.tickets).toHaveLength(1)
221
+ expect((result.tickets as Record<string, unknown>[])[0]).toMatchObject({ price: expect.any(Number) })
222
+ })
223
+
224
+ it('maps arrayItems with no item schema to an empty array', () => {
225
+ const result = generateMockContextValues({
226
+ items: { tickets: { dataType: 'arrayItems', arrayItems: {} } },
227
+ })
228
+ expect(result.tickets).toEqual([])
229
+ })
230
+
231
+ it('falls back to a string for unknown dataTypes', () => {
232
+ const result = generateMockContextValues({ items: { something: { dataType: 'unknown-type' } } })
233
+ expect(typeof result.something).toBe('string')
234
+ })
235
+ })
@@ -4,7 +4,11 @@
4
4
  */
5
5
 
6
6
  import { faker } from '@faker-js/faker'
7
- import { createRefElementMarker, extractElementPropsPathFromPropPath } from '../../../ref-elements/path-utils'
7
+ import type { ContextSchema } from '../../../extensions/context-providers/types'
8
+ import {
9
+ createRefElementMarker,
10
+ extractElementPropsPathFromPropPath,
11
+ } from '../../../extensions/ref-elements/path-utils'
8
12
  import type { ComponentInfo, DefaultValue, PropInfo, ResolvedType } from '../../ts/types'
9
13
 
10
14
  export const PRESETS_WRAPPER_CLASS_NAME = 'mock-presets-wrapper-probe'
@@ -484,3 +488,102 @@ function generateMockMenuItems(): unknown[] {
484
488
  link: generateMockLink(),
485
489
  }))
486
490
  }
491
+
492
+ // ─────────────────────────────────────────────────────────────────────────────
493
+ // Context Schema Mock Generator
494
+ // ─────────────────────────────────────────────────────────────────────────────
495
+
496
+ /**
497
+ * A recursive Proxy that safely handles any property access, function call, or string coercion.
498
+ * Used as a fallback for context fields not declared in the schema.
499
+ */
500
+ function buildPermissiveMock(): unknown {
501
+ const mock: unknown = new Proxy((() => {}) as (...args: unknown[]) => unknown, {
502
+ get(_target, prop) {
503
+ if (prop === 'then') return undefined // prevent thenable detection
504
+ if (prop === Symbol.toPrimitive) return (_hint: string) => 'mock'
505
+ if (prop === Symbol.iterator) return function* () {}
506
+ if (prop === Symbol.asyncIterator) return async function* () {}
507
+ if (typeof prop === 'symbol') return undefined
508
+ return mock
509
+ },
510
+ ownKeys() {
511
+ return []
512
+ },
513
+ getOwnPropertyDescriptor() {
514
+ return undefined
515
+ },
516
+ apply() {
517
+ return mock
518
+ },
519
+ })
520
+ return mock
521
+ }
522
+
523
+ /**
524
+ * Wraps schema-derived values in a Proxy so that any property NOT declared in the schema
525
+ * returns a permissive mock instead of undefined, preventing crashes on undeclared field access.
526
+ */
527
+ function buildSchemaSeededProxy(schemaValues: Record<string, unknown>): Record<string, unknown> {
528
+ return new Proxy(schemaValues, {
529
+ get(target, prop) {
530
+ if (prop === 'then') return undefined // prevent thenable/Promise detection
531
+ if (typeof prop === 'symbol') return Reflect.get(target, prop)
532
+ if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop as string]
533
+ return buildPermissiveMock()
534
+ },
535
+ })
536
+ }
537
+
538
+ /** Generate mock context values from a context extension schema. */
539
+ export function generateMockContextValues(schema: ContextSchema): Record<string, unknown> {
540
+ const result: Record<string, unknown> = {}
541
+ for (const [key, item] of Object.entries(schema.items)) {
542
+ result[key] = generateValueFromResolvedType(contextDataTypeToResolvedType(item), key, `context.${key}`)
543
+ }
544
+ return buildSchemaSeededProxy(result)
545
+ }
546
+
547
+ function contextDataTypeToResolvedType(item: ContextSchema['items'][string]): ResolvedType {
548
+ switch (item.dataType) {
549
+ case 'text':
550
+ return { kind: 'primitive', value: 'string' }
551
+ case 'richText':
552
+ return { kind: 'semantic', value: 'RichText' }
553
+ case 'image':
554
+ return { kind: 'semantic', value: 'Image' }
555
+ case 'video':
556
+ return { kind: 'semantic', value: 'Video' }
557
+ case 'vectorArt':
558
+ return { kind: 'semantic', value: 'VectorArt' }
559
+ case 'link':
560
+ return { kind: 'semantic', value: 'Link' }
561
+ case 'function':
562
+ return { kind: 'function' }
563
+ case 'number':
564
+ return { kind: 'primitive', value: 'number' }
565
+ case 'boolean':
566
+ return { kind: 'primitive', value: 'boolean' }
567
+ case 'arrayItems': {
568
+ const itemEntry = item.arrayItems?.item
569
+ if (itemEntry) {
570
+ const itemValue = generateValueFromResolvedType(
571
+ contextDataTypeToResolvedType(itemEntry),
572
+ 'item',
573
+ 'context.arrayItem',
574
+ )
575
+ return { kind: 'literal', value: [itemValue] }
576
+ }
577
+ return { kind: 'literal', value: [] }
578
+ }
579
+ case 'data': {
580
+ const nestedSchema = item.data
581
+ if (nestedSchema?.items && Object.keys(nestedSchema.items).length > 0) {
582
+ return { kind: 'literal', value: generateMockContextValues(nestedSchema) }
583
+ }
584
+ return { kind: 'literal', value: buildPermissiveMock() }
585
+ }
586
+ default:
587
+ return { kind: 'primitive', value: 'string' }
588
+ }
589
+ }
@@ -15,10 +15,13 @@ import {
15
15
  } from './information-extractors/react'
16
16
  import type { CoupledComponentInfo, CoupledProp, DOMBinding, TrackingStores } from './information-extractors/react'
17
17
 
18
+ import {
19
+ extractElementPropsPathFromPropPath,
20
+ extractParentElementPropsPath,
21
+ } from './extensions/ref-elements/path-utils'
18
22
  import { matchCssSelectors, parseCss } from './information-extractors/css'
19
23
  import type { CSSParserAPI } from './information-extractors/css'
20
24
  import { compileSass } from './information-extractors/css/sass-adapter'
21
- import { extractElementPropsPathFromPropPath, extractParentElementPropsPath } from './ref-elements/path-utils'
22
25
 
23
26
  import type { ComponentType } from 'react'
24
27
 
@@ -2,9 +2,9 @@ import { createRequire } from 'node:module'
2
2
  import path from 'node:path'
3
3
  import { pathToFileURL } from 'node:url'
4
4
  import { describe, expect, it } from 'vitest'
5
+ import { readRefElementComponentType } from './extensions/ref-elements/component-tag'
5
6
  import { loadCjsModule, loadModuleForExtraction } from './module-loader'
6
7
  import { readLoaderPortRefStateForTests, readRegisteredRefElementModuleUrlsForTests } from './react-runtime-loader'
7
- import { readRefElementComponentType } from './ref-elements/component-tag'
8
8
 
9
9
  const require = createRequire(import.meta.url)
10
10
 
@@ -4,8 +4,8 @@ import React from 'react'
4
4
  import type { ComponentType } from 'react'
5
5
  import ReactDOM from 'react-dom'
6
6
 
7
+ import type { RefElementModule } from './extensions/ref-elements/types'
7
8
  import { ensureReactRuntimeLoader, loadTaggedCjsModule, prepareRefElementEsmImport } from './react-runtime-loader'
8
- import type { RefElementModule } from './ref-elements/types'
9
9
 
10
10
  /**
11
11
  * Structured failure from `loadModule` when both ESM import and CJS require fail.
@@ -2,8 +2,8 @@ import { createHash } from 'node:crypto'
2
2
  import { createRequire, register } from 'node:module'
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url'
4
4
  import { MessageChannel, MessagePort } from 'node:worker_threads'
5
- import { buildRefElementTaggerSource, tagRefElementComponentForCleanup } from './ref-elements/component-tag'
6
- import type { RefElementModule } from './ref-elements/types'
5
+ import { buildRefElementTaggerSource, tagRefElementComponentForCleanup } from './extensions/ref-elements/component-tag'
6
+ import type { RefElementModule } from './extensions/ref-elements/types'
7
7
 
8
8
  const JSX_INTERCEPTOR_STATE_KEY = 'zero-config:jsx-interceptor'
9
9
  const JSX_ORIGINALS_KEY = 'zero-config:jsx-originals'
@@ -68,6 +68,32 @@ interface RefElementRuntimeState {
68
68
  originalModuleLoad: InternalModuleApi['_load'] | null
69
69
  }
70
70
 
71
+ /**
72
+ * Returns the URL that the ESM loader will assign to a context provider module
73
+ * when it is imported transitively from a ref-entry-signed statics bundle.
74
+ *
75
+ * When runtimeModules is non-empty, the statics bundle entry URL carries a
76
+ * `?zero-config-ref-entry=HASH` signature, and `shouldPropagateRefElementEntrySignature`
77
+ * propagates that hash to every non-node_modules file: import. `captureHookContext`
78
+ * must import the provider at the same signed URL so both paths hit the same Node.js
79
+ * ESM cache entry (and therefore the same `createContext` return value).
80
+ *
81
+ * When runtimeModules is empty, no hash is ever appended, so the plain URL is correct.
82
+ */
83
+ export function computeSignedContextProviderUrl(resolvedModuleUrl: string, runtimeModules: RefElementModule[]): string {
84
+ if (runtimeModules.length === 0) {
85
+ return resolvedModuleUrl
86
+ }
87
+
88
+ const registeredModules = mergeRefElementModules(runtimeModules)
89
+ const signature = buildRefElementEntrySignature(registeredModules)
90
+ const signedUrl = new URL(resolvedModuleUrl)
91
+ if (!signedUrl.searchParams.has(REF_ELEMENT_ENTRY_SIGNATURE_QUERY_PARAM)) {
92
+ signedUrl.searchParams.set(REF_ELEMENT_ENTRY_SIGNATURE_QUERY_PARAM, signature)
93
+ }
94
+ return signedUrl.href
95
+ }
96
+
71
97
  export async function prepareRefElementEsmImport(
72
98
  entryPath: string,
73
99
  runtimeModules: RefElementModule[] = [],