@wix/zero-config-implementation 1.71.0 → 1.72.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/dist/{index-DT1jTZEG.js → index-CSi4SZpQ.js} +1 -1
- package/dist/{index-C4cP1dwy.js → index-CdHu4-o-.js} +20459 -18722
- package/dist/index.d.ts +15 -8
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
- package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
- package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
- package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
- package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
- package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
- package/src/component-renderer.ts +12 -9
- package/src/converters/to-editor-component.ts +24 -6
- package/src/index.ts +121 -54
- package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
- package/src/information-extractors/react/extractors/core/types.ts +4 -5
- package/src/information-extractors/react/extractors/css-properties.ts +2 -0
- package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
- package/src/information-extractors/react/types.ts +1 -1
- package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
- package/src/information-extractors/react/utils/mock-generator.ts +38 -13
- package/src/manifest-pipeline.ts +19 -14
- package/src/module-loader.test.ts +150 -0
- package/src/module-loader.ts +48 -8
- package/src/react-runtime-interceptor.ts +64 -0
- package/src/react-runtime-loader.ts +656 -0
- package/src/ref-elements/component-tag.ts +105 -0
- package/src/ref-elements/context.test.ts +179 -0
- package/src/ref-elements/context.ts +280 -0
- package/src/ref-elements/eligible-paths.ts +45 -0
- package/src/ref-elements/module-resolution.test.ts +50 -0
- package/src/ref-elements/module-resolution.ts +21 -0
- package/src/ref-elements/module-specifier.ts +17 -0
- package/src/ref-elements/path-utils.test.ts +14 -0
- package/src/ref-elements/path-utils.ts +55 -0
- package/src/ref-elements/types.ts +17 -0
- package/src/utils/css-class.ts +15 -0
- 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
|
+
}
|