@wix/zero-config-implementation 1.71.0 → 1.73.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 (47) hide show
  1. package/dist/{index-C4cP1dwy.js → index-K3lIO4FC.js} +21613 -19661
  2. package/dist/{index-DT1jTZEG.js → index-NbI-_ozJ.js} +1 -1
  3. package/dist/index.d.ts +23 -8
  4. package/dist/index.js +1 -1
  5. package/package.json +4 -3
  6. package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
  7. package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
  8. package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
  9. package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
  10. package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
  11. package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
  12. package/src/component-renderer.ts +12 -9
  13. package/src/converters/to-editor-component.test.ts +88 -0
  14. package/src/converters/to-editor-component.ts +35 -6
  15. package/src/index.ts +121 -54
  16. package/src/information-extractors/css/parse.test.ts +49 -1
  17. package/src/information-extractors/css/parse.ts +167 -3
  18. package/src/information-extractors/css/selector-matcher.ts +3 -1
  19. package/src/information-extractors/css/types.ts +11 -0
  20. package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
  21. package/src/information-extractors/react/extractors/core/types.ts +4 -5
  22. package/src/information-extractors/react/extractors/css-properties.test.ts +294 -2
  23. package/src/information-extractors/react/extractors/css-properties.ts +248 -98
  24. package/src/information-extractors/react/extractors/index.ts +1 -0
  25. package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
  26. package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
  27. package/src/information-extractors/react/index.ts +1 -0
  28. package/src/information-extractors/react/types.ts +1 -1
  29. package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
  30. package/src/information-extractors/react/utils/mock-generator.ts +38 -13
  31. package/src/manifest-pipeline.ts +22 -15
  32. package/src/module-loader.test.ts +150 -0
  33. package/src/module-loader.ts +48 -8
  34. package/src/react-runtime-interceptor.ts +64 -0
  35. package/src/react-runtime-loader.ts +656 -0
  36. package/src/ref-elements/component-tag.ts +105 -0
  37. package/src/ref-elements/context.test.ts +179 -0
  38. package/src/ref-elements/context.ts +280 -0
  39. package/src/ref-elements/eligible-paths.ts +45 -0
  40. package/src/ref-elements/module-resolution.test.ts +50 -0
  41. package/src/ref-elements/module-resolution.ts +21 -0
  42. package/src/ref-elements/module-specifier.ts +17 -0
  43. package/src/ref-elements/path-utils.test.ts +14 -0
  44. package/src/ref-elements/path-utils.ts +55 -0
  45. package/src/ref-elements/types.ts +17 -0
  46. package/src/utils/css-class.ts +15 -0
  47. package/src/jsx-runtime-interceptor.ts +0 -245
@@ -0,0 +1,105 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ export const REF_ELEMENT_COMPONENT_TYPE_SYMBOL_KEY = 'zero-config:ref-element-component-type'
4
+ export const refElementComponentTypeSymbol = Symbol.for(REF_ELEMENT_COMPONENT_TYPE_SYMBOL_KEY)
5
+
6
+ export function readRefElementComponentType(componentType: unknown): string | undefined {
7
+ if (!componentType || (typeof componentType !== 'function' && typeof componentType !== 'object')) {
8
+ return undefined
9
+ }
10
+
11
+ const refComponentType = (componentType as Record<PropertyKey, unknown>)[refElementComponentTypeSymbol]
12
+ return typeof refComponentType === 'string' ? refComponentType : undefined
13
+ }
14
+
15
+ export function safelyTagRefElementComponent(componentValue: unknown, refComponentType: string): unknown {
16
+ if (!isComponentLike(componentValue)) {
17
+ return componentValue
18
+ }
19
+
20
+ try {
21
+ if (readRefElementComponentType(componentValue) === refComponentType) {
22
+ return componentValue
23
+ }
24
+
25
+ Object.defineProperty(componentValue, refElementComponentTypeSymbol, {
26
+ value: refComponentType,
27
+ configurable: true,
28
+ })
29
+ } catch {
30
+ return componentValue
31
+ }
32
+
33
+ return componentValue
34
+ }
35
+
36
+ export function tagRefElementComponentForCleanup(
37
+ componentValue: unknown,
38
+ refComponentType: string,
39
+ ): (() => void) | undefined {
40
+ if (!isComponentLike(componentValue)) {
41
+ return undefined
42
+ }
43
+
44
+ const existingRefComponentType = readRefElementComponentType(componentValue)
45
+ if (existingRefComponentType !== undefined) {
46
+ return undefined
47
+ }
48
+
49
+ safelyTagRefElementComponent(componentValue, refComponentType)
50
+ if (readRefElementComponentType(componentValue) !== refComponentType) {
51
+ return undefined
52
+ }
53
+
54
+ return () => {
55
+ try {
56
+ if (readRefElementComponentType(componentValue) === refComponentType) {
57
+ delete (componentValue as unknown as Record<PropertyKey, unknown>)[refElementComponentTypeSymbol]
58
+ }
59
+ } catch {
60
+ return
61
+ }
62
+ }
63
+ }
64
+
65
+ export function buildRefElementTaggerSource(): string {
66
+ return `
67
+ const REF_ELEMENT_COMPONENT_TYPE_SYMBOL_KEY = ${JSON.stringify(REF_ELEMENT_COMPONENT_TYPE_SYMBOL_KEY)};
68
+ const refElementComponentTypeSymbol = Symbol.for(REF_ELEMENT_COMPONENT_TYPE_SYMBOL_KEY);
69
+ function isComponentLike(value) {
70
+ return typeof value === 'function' || (typeof value === 'object' && value !== null && '$$typeof' in value);
71
+ }
72
+ function readRefElementComponentType(componentType) {
73
+ if (!componentType || (typeof componentType !== 'function' && typeof componentType !== 'object')) {
74
+ return undefined;
75
+ }
76
+
77
+ const refComponentType = componentType[refElementComponentTypeSymbol];
78
+ return typeof refComponentType === 'string' ? refComponentType : undefined;
79
+ }
80
+ function safelyTagRefElementComponent(componentValue, refComponentType) {
81
+ if (!isComponentLike(componentValue)) {
82
+ return componentValue;
83
+ }
84
+
85
+ try {
86
+ if (readRefElementComponentType(componentValue) === refComponentType) {
87
+ return componentValue;
88
+ }
89
+
90
+ Object.defineProperty(componentValue, refElementComponentTypeSymbol, {
91
+ value: refComponentType,
92
+ configurable: true,
93
+ });
94
+ } catch {
95
+ return componentValue;
96
+ }
97
+
98
+ return componentValue;
99
+ }
100
+ `
101
+ }
102
+
103
+ function isComponentLike(value: unknown): value is ComponentType<unknown> {
104
+ return typeof value === 'function' || (typeof value === 'object' && value !== null && '$$typeof' in value)
105
+ }
@@ -0,0 +1,179 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import ts from 'typescript'
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+
7
+ import type { ComponentInfo } from '../information-extractors/ts/types'
8
+ import { buildRefElementContext } from './context'
9
+ import { resolveModuleUrlFromEntryPath } from './module-resolution'
10
+
11
+ const temporaryDirectoryPaths: string[] = []
12
+ const compiledEntryPath = path.resolve(__dirname, 'context.test.ts')
13
+
14
+ afterEach(() => {
15
+ for (const temporaryDirectoryPath of temporaryDirectoryPaths.splice(0)) {
16
+ fs.rmSync(temporaryDirectoryPath, { force: true, recursive: true })
17
+ }
18
+ })
19
+
20
+ describe('buildRefElementContext', () => {
21
+ it('does not tag bare package imports because runtime modules are keyed by the exact externalized moduleSpecifier', async () => {
22
+ const sourceFilePath = createModuleImportFixture('@wix/editor-react-components')
23
+ const program = createProgram(sourceFilePath)
24
+
25
+ const refElementContext = await buildRefElementContext(
26
+ program,
27
+ sourceFilePath,
28
+ { componentName: 'Component', props: {} },
29
+ compiledEntryPath,
30
+ )
31
+
32
+ expect(refElementContext.runtimeModules).toEqual([])
33
+ })
34
+
35
+ it('tags the exact catalog moduleSpecifier for the same package', async () => {
36
+ const sourceFilePath = createModuleImportFixture('@wix/editor-react-components/Button')
37
+ const program = createProgram(sourceFilePath)
38
+
39
+ const refElementContext = await buildRefElementContext(
40
+ program,
41
+ sourceFilePath,
42
+ { componentName: 'Component', props: {} },
43
+ compiledEntryPath,
44
+ )
45
+
46
+ expect(refElementContext.runtimeModules).toEqual([
47
+ {
48
+ exportNames: ['default', 'Button'],
49
+ moduleSpecifier: '@wix/editor-react-components/Button',
50
+ resolvedModuleUrl: resolveModuleUrlFromEntryPath(compiledEntryPath, '@wix/editor-react-components/Button'),
51
+ refComponentType: 'wixEditorElements.Button',
52
+ },
53
+ ])
54
+ })
55
+
56
+ it('follows aliased local wrappers before classifying imports as external ref-elements', async () => {
57
+ const temporaryDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-config-ref-element-context-'))
58
+ temporaryDirectoryPaths.push(temporaryDirectoryPath)
59
+
60
+ const componentPath = path.join(temporaryDirectoryPath, 'Component.tsx')
61
+ const aliasedWrapperPath = path.join(temporaryDirectoryPath, 'ButtonWrapper.tsx')
62
+ const internalWrapperPath = path.join(temporaryDirectoryPath, 'InternalButton.tsx')
63
+
64
+ fs.writeFileSync(
65
+ componentPath,
66
+ "import ButtonWrapper from '@/ButtonWrapper'\nexport default function Component() { return <ButtonWrapper /> }\n",
67
+ )
68
+ fs.writeFileSync(
69
+ aliasedWrapperPath,
70
+ "import InternalButton from '@/InternalButton'\nexport default InternalButton\n",
71
+ )
72
+ fs.writeFileSync(
73
+ internalWrapperPath,
74
+ "export { default } from '@wix/zero-config-ref-element-fixtures/BareRefButton'\n",
75
+ )
76
+
77
+ const program = ts.createProgram({
78
+ rootNames: [componentPath, aliasedWrapperPath, internalWrapperPath],
79
+ options: {
80
+ allowSyntheticDefaultImports: true,
81
+ baseUrl: temporaryDirectoryPath,
82
+ jsx: ts.JsxEmit.ReactJSX,
83
+ module: ts.ModuleKind.ESNext,
84
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
85
+ paths: {
86
+ '@/*': ['./*'],
87
+ },
88
+ skipLibCheck: true,
89
+ target: ts.ScriptTarget.ES2020,
90
+ },
91
+ })
92
+
93
+ const refElementContext = await buildRefElementContext(
94
+ program,
95
+ componentPath,
96
+ {
97
+ componentName: 'Component',
98
+ props: {},
99
+ } satisfies ComponentInfo,
100
+ path.resolve(__dirname, '../__fixtures__/esm-ref-element-entry.mjs'),
101
+ )
102
+
103
+ expect(refElementContext.runtimeModules).toEqual([
104
+ {
105
+ exportNames: ['default', 'BareRefButton'],
106
+ moduleSpecifier: '@wix/zero-config-ref-element-fixtures/BareRefButton',
107
+ resolvedModuleUrl: resolveModuleUrlFromEntryPath(
108
+ path.resolve(__dirname, '../__fixtures__/esm-ref-element-entry.mjs'),
109
+ '@wix/zero-config-ref-element-fixtures/BareRefButton',
110
+ ),
111
+ refComponentType: 'wixEditorElements.BareRefButton',
112
+ },
113
+ ])
114
+ })
115
+
116
+ it('fails when a resolved extensions module throws during catalog loading', async () => {
117
+ const temporaryDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-config-ref-element-context-'))
118
+ temporaryDirectoryPaths.push(temporaryDirectoryPath)
119
+
120
+ const componentPath = path.join(temporaryDirectoryPath, 'Component.tsx')
121
+ const compiledEntryPath = path.join(temporaryDirectoryPath, 'dist', 'index.js')
122
+ const packageDirectoryPath = path.join(temporaryDirectoryPath, 'node_modules', 'broken-ref-elements')
123
+
124
+ fs.mkdirSync(path.dirname(compiledEntryPath), { recursive: true })
125
+ fs.mkdirSync(packageDirectoryPath, { recursive: true })
126
+
127
+ fs.writeFileSync(
128
+ componentPath,
129
+ "import Button from 'broken-ref-elements/Button'\nexport default function Component() { return <Button /> }\n",
130
+ )
131
+ fs.writeFileSync(compiledEntryPath, 'export {}\n')
132
+ fs.writeFileSync(
133
+ path.join(packageDirectoryPath, 'package.json'),
134
+ JSON.stringify(
135
+ {
136
+ exports: {
137
+ './Button': './Button.js',
138
+ './extensions': './extensions.js',
139
+ },
140
+ name: 'broken-ref-elements',
141
+ type: 'module',
142
+ },
143
+ null,
144
+ 2,
145
+ ),
146
+ )
147
+ fs.writeFileSync(path.join(packageDirectoryPath, 'Button.js'), 'export default function Button() {}\n')
148
+ fs.writeFileSync(path.join(packageDirectoryPath, 'extensions.js'), "throw new Error('broken catalog')\n")
149
+
150
+ const program = createProgram(componentPath)
151
+
152
+ await expect(
153
+ buildRefElementContext(program, componentPath, { componentName: 'Component', props: {} }, compiledEntryPath),
154
+ ).rejects.toThrow('broken catalog')
155
+ })
156
+ })
157
+
158
+ function createProgram(sourceFilePath: string): ts.Program {
159
+ return ts.createProgram({
160
+ rootNames: [sourceFilePath],
161
+ options: {
162
+ allowJs: true,
163
+ jsx: ts.JsxEmit.ReactJSX,
164
+ },
165
+ })
166
+ }
167
+
168
+ function createModuleImportFixture(moduleSpecifier: string): string {
169
+ const temporaryDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-config-ref-element-context-'))
170
+ temporaryDirectoryPaths.push(temporaryDirectoryPath)
171
+
172
+ const sourceFilePath = path.join(temporaryDirectoryPath, 'Component.tsx')
173
+ fs.writeFileSync(
174
+ sourceFilePath,
175
+ `import Button from '${moduleSpecifier}'\nexport default function Component() { return <Button /> }\n`,
176
+ )
177
+
178
+ return sourceFilePath
179
+ }
@@ -0,0 +1,280 @@
1
+ import ts from 'typescript'
2
+ import type { ComponentInfo } from '../information-extractors/ts/types'
3
+ import { extractEligibleRefElementPaths } from './eligible-paths'
4
+ import { resolveModuleUrlFromEntryPath } from './module-resolution'
5
+ import { extractBasePackageName, extractModuleLeafExportName } from './module-specifier'
6
+ import type { RefElementContext, RefElementModule } from './types'
7
+
8
+ interface RawRefElementExtensionEntry {
9
+ options?: {
10
+ type?: string
11
+ resources?: {
12
+ client?: {
13
+ moduleSpecifier?: string
14
+ }
15
+ }
16
+ }
17
+ type?: string
18
+ resources?: {
19
+ client?: {
20
+ moduleSpecifier?: string
21
+ }
22
+ }
23
+ }
24
+
25
+ interface NormalizedRefElementCatalogEntry {
26
+ moduleSpecifier: string
27
+ refComponentType: string
28
+ }
29
+
30
+ export async function buildRefElementContext(
31
+ program: ts.Program,
32
+ sourceFilePath: string,
33
+ componentInfo: ComponentInfo,
34
+ compiledEntryPath: string,
35
+ ): Promise<RefElementContext> {
36
+ const importedModuleSpecifiers = collectImportedRefElementModuleSpecifiers(program, sourceFilePath)
37
+
38
+ return {
39
+ eligiblePaths: extractEligibleRefElementPaths(componentInfo),
40
+ runtimeModules: await resolveRuntimeModules(importedModuleSpecifiers, compiledEntryPath),
41
+ }
42
+ }
43
+
44
+ async function resolveRuntimeModules(
45
+ importedModuleSpecifiers: string[],
46
+ compiledEntryPath: string,
47
+ ): Promise<RefElementModule[]> {
48
+ if (importedModuleSpecifiers.length === 0) {
49
+ return []
50
+ }
51
+
52
+ const importedModuleSpecifierSet = new Set(importedModuleSpecifiers)
53
+ const importedPackageNames = [
54
+ ...new Set(importedModuleSpecifiers.filter(isPackageModuleSpecifier).map(extractBasePackageName)),
55
+ ]
56
+ const catalogEntries = (
57
+ await Promise.all(importedPackageNames.map((packageName) => loadCatalogEntries(packageName, compiledEntryPath)))
58
+ ).flat()
59
+
60
+ const runtimeModulesBySpecifier = new Map<string, Set<string>>()
61
+
62
+ for (const catalogEntry of catalogEntries) {
63
+ if (!importedModuleSpecifierSet.has(catalogEntry.moduleSpecifier)) {
64
+ continue
65
+ }
66
+
67
+ const refComponentTypes = runtimeModulesBySpecifier.get(catalogEntry.moduleSpecifier) ?? new Set<string>()
68
+ refComponentTypes.add(catalogEntry.refComponentType)
69
+ runtimeModulesBySpecifier.set(catalogEntry.moduleSpecifier, refComponentTypes)
70
+ }
71
+
72
+ const runtimeModules: RefElementModule[] = []
73
+
74
+ for (const [moduleSpecifier, refComponentTypes] of runtimeModulesBySpecifier.entries()) {
75
+ if (refComponentTypes.size !== 1) {
76
+ continue
77
+ }
78
+
79
+ const refComponentType = [...refComponentTypes][0]
80
+ if (!refComponentType) {
81
+ continue
82
+ }
83
+
84
+ const resolvedModuleUrl = resolveModuleUrlFromEntryPath(compiledEntryPath, moduleSpecifier)
85
+ if (!resolvedModuleUrl) {
86
+ continue
87
+ }
88
+
89
+ runtimeModules.push({
90
+ exportNames: buildRefElementExportNames(moduleSpecifier),
91
+ moduleSpecifier,
92
+ resolvedModuleUrl,
93
+ refComponentType,
94
+ })
95
+ }
96
+
97
+ return runtimeModules
98
+ }
99
+
100
+ function isPackageModuleSpecifier(moduleSpecifier: string): boolean {
101
+ return !moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/') && !moduleSpecifier.startsWith('file:')
102
+ }
103
+
104
+ function collectImportedRefElementModuleSpecifiers(program: ts.Program, sourceFilePath: string): string[] {
105
+ const entrySourceFile = findSourceFile(program, sourceFilePath)
106
+ if (!entrySourceFile) {
107
+ return []
108
+ }
109
+
110
+ const importedModuleSpecifiers = new Set<string>()
111
+ const visitedSourceFiles = new Set<string>()
112
+
113
+ function visitSourceFile(sourceFile: ts.SourceFile): void {
114
+ if (visitedSourceFiles.has(sourceFile.fileName)) {
115
+ return
116
+ }
117
+
118
+ visitedSourceFiles.add(sourceFile.fileName)
119
+
120
+ for (const statement of sourceFile.statements) {
121
+ const moduleSpecifier = readReferencedModuleSpecifier(statement)
122
+ if (!moduleSpecifier) {
123
+ continue
124
+ }
125
+
126
+ if (referencesRefElementModule(statement, moduleSpecifier)) {
127
+ importedModuleSpecifiers.add(moduleSpecifier)
128
+ }
129
+
130
+ const importedSourceFile = resolveImportedSourceFile(program, sourceFile.fileName, moduleSpecifier)
131
+ if (importedSourceFile) {
132
+ visitSourceFile(importedSourceFile)
133
+ }
134
+ }
135
+ }
136
+
137
+ visitSourceFile(entrySourceFile)
138
+ return [...importedModuleSpecifiers]
139
+ }
140
+
141
+ async function loadCatalogEntries(
142
+ packageName: string,
143
+ compiledEntryPath: string,
144
+ ): Promise<NormalizedRefElementCatalogEntry[]> {
145
+ const resolvedExtensionsUrl = resolveModuleUrlFromEntryPath(compiledEntryPath, `${packageName}/extensions`)
146
+ if (!resolvedExtensionsUrl) {
147
+ return []
148
+ }
149
+
150
+ const extensionModule = (await import(resolvedExtensionsUrl)) as Record<string, unknown>
151
+
152
+ return extractRawExtensionEntries(extensionModule)
153
+ .map(normalizeExtensionEntry)
154
+ .flatMap((catalogEntry) => (catalogEntry ? [catalogEntry] : []))
155
+ }
156
+
157
+ function referencesRefElementModule(statement: ts.Statement, moduleSpecifier: string): boolean {
158
+ const moduleLeafExportName = extractModuleLeafExportName(moduleSpecifier)
159
+ if (!moduleLeafExportName) {
160
+ return false
161
+ }
162
+
163
+ if (ts.isImportDeclaration(statement)) {
164
+ return importsRefElementModule(statement, moduleLeafExportName)
165
+ }
166
+
167
+ if (ts.isExportDeclaration(statement)) {
168
+ return reExportsRefElementModule(statement, moduleLeafExportName)
169
+ }
170
+
171
+ return false
172
+ }
173
+
174
+ function importsRefElementModule(importDeclaration: ts.ImportDeclaration, moduleLeafExportName: string): boolean {
175
+ const importClause = importDeclaration.importClause
176
+ if (!importClause) {
177
+ return false
178
+ }
179
+
180
+ if (importClause.name) {
181
+ return true
182
+ }
183
+
184
+ if (!importClause.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
185
+ return false
186
+ }
187
+
188
+ return importClause.namedBindings.elements.some((importSpecifier) => {
189
+ const importedName = importSpecifier.propertyName?.text ?? importSpecifier.name.text
190
+ return importedName === moduleLeafExportName
191
+ })
192
+ }
193
+
194
+ function reExportsRefElementModule(exportDeclaration: ts.ExportDeclaration, moduleLeafExportName: string): boolean {
195
+ if (!exportDeclaration.exportClause || !ts.isNamedExports(exportDeclaration.exportClause)) {
196
+ return false
197
+ }
198
+
199
+ return exportDeclaration.exportClause.elements.some((exportSpecifier) => {
200
+ const exportedSourceName = exportSpecifier.propertyName?.text ?? exportSpecifier.name.text
201
+ return exportedSourceName === 'default' || exportedSourceName === moduleLeafExportName
202
+ })
203
+ }
204
+
205
+ function extractRawExtensionEntries(extensionModule: Record<string, unknown>): RawRefElementExtensionEntry[] {
206
+ const defaultExport = extensionModule.default
207
+ const extensionEntries = Array.isArray(defaultExport)
208
+ ? defaultExport
209
+ : ((defaultExport as { extensions?: unknown[] } | undefined)?.extensions ?? extensionModule.extensions)
210
+
211
+ return Array.isArray(extensionEntries) ? (extensionEntries as RawRefElementExtensionEntry[]) : []
212
+ }
213
+
214
+ function normalizeExtensionEntry(rawEntry: RawRefElementExtensionEntry): NormalizedRefElementCatalogEntry | undefined {
215
+ const normalizedEntry = rawEntry.options ?? rawEntry
216
+ const moduleSpecifier = normalizedEntry.resources?.client?.moduleSpecifier
217
+ const refComponentType = normalizedEntry.type
218
+
219
+ if (!moduleSpecifier || !refComponentType) {
220
+ return undefined
221
+ }
222
+
223
+ return {
224
+ moduleSpecifier,
225
+ refComponentType,
226
+ }
227
+ }
228
+
229
+ function buildRefElementExportNames(moduleSpecifier: string): string[] {
230
+ const exportNames = new Set<string>(['default'])
231
+ if (extractBasePackageName(moduleSpecifier) === moduleSpecifier) {
232
+ return [...exportNames]
233
+ }
234
+
235
+ const moduleLeafName = extractModuleLeafExportName(moduleSpecifier)
236
+ if (moduleLeafName) {
237
+ exportNames.add(moduleLeafName)
238
+ }
239
+
240
+ return [...exportNames]
241
+ }
242
+
243
+ function findSourceFile(program: ts.Program, sourceFilePath: string): ts.SourceFile | undefined {
244
+ return (
245
+ program.getSourceFile(sourceFilePath) ??
246
+ program.getSourceFiles().find((sourceFile) => sourceFile.fileName === sourceFilePath)
247
+ )
248
+ }
249
+
250
+ function resolveImportedSourceFile(
251
+ program: ts.Program,
252
+ importingFilePath: string,
253
+ moduleSpecifier: string,
254
+ ): ts.SourceFile | undefined {
255
+ const resolvedModule = ts.resolveModuleName(moduleSpecifier, importingFilePath, program.getCompilerOptions(), ts.sys)
256
+ const resolvedFileName = resolvedModule.resolvedModule?.resolvedFileName
257
+
258
+ if (!resolvedFileName) {
259
+ return undefined
260
+ }
261
+
262
+ const resolvedSourceFile = findSourceFile(program, resolvedFileName)
263
+ if (!resolvedSourceFile || resolvedSourceFile.isDeclarationFile) {
264
+ return undefined
265
+ }
266
+
267
+ return resolvedSourceFile
268
+ }
269
+
270
+ function readReferencedModuleSpecifier(statement: ts.Statement): string | undefined {
271
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
272
+ return statement.moduleSpecifier.text
273
+ }
274
+
275
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
276
+ return statement.moduleSpecifier.text
277
+ }
278
+
279
+ return undefined
280
+ }
@@ -0,0 +1,45 @@
1
+ import type { ComponentInfo, ResolvedType } from '../information-extractors/ts/types'
2
+
3
+ export function extractEligibleRefElementPaths(componentInfo: ComponentInfo): string[] {
4
+ const elementPropsInfo = componentInfo.props.elementProps
5
+ if (!elementPropsInfo) {
6
+ return []
7
+ }
8
+
9
+ const eligibleRefElementPaths = new Set<string>()
10
+ collectEligibleRefElementPaths(elementPropsInfo.resolvedType, undefined, true, eligibleRefElementPaths)
11
+ return [...eligibleRefElementPaths]
12
+ }
13
+
14
+ function collectEligibleRefElementPaths(
15
+ resolvedType: ResolvedType,
16
+ parentPath: string | undefined,
17
+ collectBranchPaths: boolean,
18
+ eligibleRefElementPaths: Set<string>,
19
+ ): void {
20
+ if (resolvedType.kind === 'union' || resolvedType.kind === 'intersection') {
21
+ for (const nestedType of resolvedType.types ?? []) {
22
+ collectEligibleRefElementPaths(nestedType, parentPath, collectBranchPaths, eligibleRefElementPaths)
23
+ }
24
+ return
25
+ }
26
+
27
+ if (resolvedType.kind !== 'object') {
28
+ return
29
+ }
30
+
31
+ for (const [propertyName, propInfo] of Object.entries(resolvedType.properties ?? {})) {
32
+ if (propertyName === 'elementProps') {
33
+ const nestedParentPath = parentPath ? `${parentPath}.elementProps` : undefined
34
+ collectEligibleRefElementPaths(propInfo.resolvedType, nestedParentPath, true, eligibleRefElementPaths)
35
+ continue
36
+ }
37
+
38
+ const propertyPath = parentPath ? `${parentPath}.${propertyName}` : propertyName
39
+ if (collectBranchPaths) {
40
+ eligibleRefElementPaths.add(propertyPath)
41
+ }
42
+
43
+ collectEligibleRefElementPaths(propInfo.resolvedType, propertyPath, false, eligibleRefElementPaths)
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { resolveModulePathFromEntryPath, resolveModuleUrlFromEntryPath } from './module-resolution'
6
+
7
+ const temporaryDirectoryPaths: string[] = []
8
+
9
+ afterEach(() => {
10
+ for (const temporaryDirectoryPath of temporaryDirectoryPaths.splice(0)) {
11
+ fs.rmSync(temporaryDirectoryPath, { force: true, recursive: true })
12
+ }
13
+ })
14
+
15
+ describe('resolveModulePathFromEntryPath', () => {
16
+ it('resolves packages from the compiled entry package, not from zero-config-implementation', () => {
17
+ const temporaryDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-config-module-resolution-'))
18
+ temporaryDirectoryPaths.push(temporaryDirectoryPath)
19
+
20
+ const packageRootPath = path.join(temporaryDirectoryPath, 'consumer-package')
21
+ const nodeModulesPath = path.join(packageRootPath, 'node_modules', '@wix')
22
+ const fixturePackagePath = path.resolve(__dirname, '../../../ref-element-fixtures')
23
+
24
+ fs.mkdirSync(nodeModulesPath, { recursive: true })
25
+ fs.symlinkSync(fixturePackagePath, path.join(nodeModulesPath, 'zero-config-ref-element-fixtures'), 'dir')
26
+
27
+ const compiledEntryPath = path.join(packageRootPath, 'dist', 'Component.mjs')
28
+ fs.mkdirSync(path.dirname(compiledEntryPath), { recursive: true })
29
+ fs.writeFileSync(compiledEntryPath, 'export default null\n')
30
+
31
+ const resolvedExtensionsPath = resolveModulePathFromEntryPath(
32
+ compiledEntryPath,
33
+ '@wix/zero-config-ref-element-fixtures/extensions',
34
+ )
35
+
36
+ expect(resolvedExtensionsPath).toBe(path.join(fixturePackagePath, 'dist', 'extensions.js'))
37
+ })
38
+ })
39
+
40
+ describe('resolveModuleUrlFromEntryPath', () => {
41
+ it('returns a file url for the resolved module path', () => {
42
+ const resolvedExtensionsUrl = resolveModuleUrlFromEntryPath(
43
+ path.resolve(__dirname, '../__fixtures__/esm-ref-element-entry.mjs'),
44
+ '@wix/zero-config-ref-element-fixtures/extensions',
45
+ )
46
+
47
+ expect(resolvedExtensionsUrl).toMatch(/^file:/)
48
+ expect(resolvedExtensionsUrl).toContain('/packages/ref-element-fixtures/dist/extensions.js')
49
+ })
50
+ })
@@ -0,0 +1,21 @@
1
+ import { fileURLToPath, pathToFileURL } from 'node:url'
2
+ import { resolve as resolveImportSpecifier } from 'import-meta-resolve'
3
+
4
+ export function resolveModulePathFromEntryPath(entryPath: string, moduleSpecifier: string): string | undefined {
5
+ const entryUrl = pathToFileURL(entryPath).href
6
+
7
+ try {
8
+ return fileURLToPath(resolveImportSpecifier(moduleSpecifier, entryUrl))
9
+ } catch {
10
+ return undefined
11
+ }
12
+ }
13
+
14
+ export function resolveModuleUrlFromEntryPath(entryPath: string, moduleSpecifier: string): string | undefined {
15
+ const resolvedModulePath = resolveModulePathFromEntryPath(entryPath, moduleSpecifier)
16
+ if (!resolvedModulePath) {
17
+ return undefined
18
+ }
19
+
20
+ return pathToFileURL(resolvedModulePath).href
21
+ }
@@ -0,0 +1,17 @@
1
+ export function extractBasePackageName(moduleSpecifier: string): string {
2
+ if (moduleSpecifier.startsWith('@')) {
3
+ const [scopeName = '', packageName = ''] = moduleSpecifier.split('/')
4
+ return `${scopeName}/${packageName}`
5
+ }
6
+
7
+ return moduleSpecifier.split('/')[0] ?? moduleSpecifier
8
+ }
9
+
10
+ export function extractModuleLeafExportName(moduleSpecifier: string): string | undefined {
11
+ const moduleLeafName = moduleSpecifier.split('/').pop()
12
+ if (!moduleLeafName) {
13
+ return undefined
14
+ }
15
+
16
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(moduleLeafName) ? moduleLeafName : undefined
17
+ }