@wix/zero-config-implementation 1.6.0 → 1.8.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/converters/utils.d.ts +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +25891 -37144
- package/dist/information-extractors/react/extractors/core/index.d.ts +1 -1
- package/dist/information-extractors/react/extractors/core/runner.d.ts +5 -1
- package/dist/information-extractors/react/extractors/index.d.ts +1 -1
- package/dist/information-extractors/react/index.d.ts +1 -1
- package/dist/manifest-pipeline.d.ts +2 -2
- package/dist/module-loader.d.ts +20 -0
- package/dist/schema.d.ts +1 -0
- package/package.json +2 -5
- package/src/converters/data-item-builder.ts +4 -5
- package/src/converters/utils.test.ts +16 -0
- package/src/converters/utils.ts +8 -0
- package/src/index.ts +9 -5
- package/src/information-extractors/react/extractors/core/index.ts +1 -1
- package/src/information-extractors/react/extractors/core/runner.ts +10 -1
- package/src/information-extractors/react/extractors/index.ts +1 -0
- package/src/information-extractors/react/index.ts +1 -0
- package/src/information-extractors/ts/components.ts +13 -1
- package/src/information-extractors/ts/utils/semantic-type-resolver.ts +2 -4
- package/src/manifest-pipeline.ts +3 -1
- package/src/module-loader.ts +76 -0
- package/src/schema.ts +33 -1
- package/vite.config.ts +6 -0
- package/dist/component-loader.d.ts +0 -42
- package/src/component-loader.test.ts +0 -277
- package/src/component-loader.ts +0 -256
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
const mockModuleRequire = vi.fn()
|
|
5
|
-
|
|
6
|
-
vi.mock('node:module', () => ({
|
|
7
|
-
createRequire: () => mockModuleRequire,
|
|
8
|
-
}))
|
|
9
|
-
|
|
10
|
-
vi.mock('read-package-up', () => ({
|
|
11
|
-
readPackageUpSync: vi.fn(),
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
vi.mock('pkg-entry-points', () => ({
|
|
15
|
-
getPackageEntryPoints: vi.fn(),
|
|
16
|
-
}))
|
|
17
|
-
|
|
18
|
-
import { getPackageEntryPoints } from 'pkg-entry-points'
|
|
19
|
-
import type { NormalizedPackageJson } from 'read-package-up'
|
|
20
|
-
import { readPackageUpSync } from 'read-package-up'
|
|
21
|
-
import { createComponentLoader } from './component-loader'
|
|
22
|
-
|
|
23
|
-
const mockReadPackageUp = vi.mocked(readPackageUpSync)
|
|
24
|
-
const mockGetEntryPoints = vi.mocked(getPackageEntryPoints)
|
|
25
|
-
|
|
26
|
-
const fakePkgDir = '/fake/project'
|
|
27
|
-
const fakePkgJsonPath = join(fakePkgDir, 'package.json')
|
|
28
|
-
|
|
29
|
-
function mockPackage(name: string) {
|
|
30
|
-
mockReadPackageUp.mockReturnValue({
|
|
31
|
-
packageJson: { name } as NormalizedPackageJson,
|
|
32
|
-
path: fakePkgJsonPath,
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
vi.resetAllMocks()
|
|
38
|
-
mockModuleRequire.mockImplementation(() => {
|
|
39
|
-
throw new Error('Cannot find module')
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
describe('createComponentLoader', () => {
|
|
44
|
-
it('returns error when no package.json is found', async () => {
|
|
45
|
-
mockReadPackageUp.mockReturnValue(undefined)
|
|
46
|
-
|
|
47
|
-
const result = await createComponentLoader('/some/Component.tsx')
|
|
48
|
-
|
|
49
|
-
expect(result.isErr()).toBe(true)
|
|
50
|
-
if (result.isErr()) {
|
|
51
|
-
expect(result.error.message).toBe('No package.json found')
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns error when package has no resolvable entry points', async () => {
|
|
56
|
-
mockPackage('my-pkg')
|
|
57
|
-
mockGetEntryPoints.mockResolvedValue({})
|
|
58
|
-
|
|
59
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
60
|
-
|
|
61
|
-
expect(result.isErr()).toBe(true)
|
|
62
|
-
if (result.isErr()) {
|
|
63
|
-
expect(result.error.message).toContain('no resolvable entry point')
|
|
64
|
-
expect(result.error.message).toContain('my-pkg')
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('returns error when only types exports exist', async () => {
|
|
69
|
-
mockPackage('types-only')
|
|
70
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
71
|
-
'.': [[['types'], './dist/index.d.ts']],
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
75
|
-
|
|
76
|
-
expect(result.isErr()).toBe(true)
|
|
77
|
-
if (result.isErr()) {
|
|
78
|
-
expect(result.error.message).toContain('no loadable exports')
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('returns error with path when single entry point fails to load', async () => {
|
|
83
|
-
mockPackage('bad-entry')
|
|
84
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
85
|
-
'.': [[['import'], './dist/index.js']],
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
89
|
-
|
|
90
|
-
expect(result.isErr()).toBe(true)
|
|
91
|
-
if (result.isErr()) {
|
|
92
|
-
expect(result.error.message).toContain('Failed to load')
|
|
93
|
-
expect(result.error.message).toContain(join(fakePkgDir, 'dist/index.js'))
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('returns error when multiple subpath exports all fail to load', async () => {
|
|
98
|
-
mockPackage('multi-fail')
|
|
99
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
100
|
-
'./foo': [[['import'], './dist/foo.js']],
|
|
101
|
-
'./bar': [[['import'], './dist/bar.js']],
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
105
|
-
|
|
106
|
-
expect(result.isErr()).toBe(true)
|
|
107
|
-
if (result.isErr()) {
|
|
108
|
-
expect(result.error.message).toContain('Failed to load')
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('skips types-only entries in multi-subpath exports', async () => {
|
|
113
|
-
mockPackage('mixed-types')
|
|
114
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
115
|
-
'./types': [[['types'], './dist/types.d.ts']],
|
|
116
|
-
'./runtime': [[['import'], './dist/does-not-exist.js']],
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
120
|
-
|
|
121
|
-
// ./types should be skipped (selectBestPath returns undefined), ./runtime fails to load
|
|
122
|
-
expect(result.isErr()).toBe(true)
|
|
123
|
-
if (result.isErr()) {
|
|
124
|
-
expect(result.error.message).toContain('Failed to load')
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
describe('selectBestPath (via createComponentLoader)', () => {
|
|
130
|
-
it('prefers import condition over node and default', async () => {
|
|
131
|
-
mockPackage('prio-test')
|
|
132
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
133
|
-
'.': [
|
|
134
|
-
[['default'], './dist/default.js'],
|
|
135
|
-
[['node'], './dist/node.js'],
|
|
136
|
-
[['import'], './dist/import.js'],
|
|
137
|
-
],
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
141
|
-
|
|
142
|
-
// Module load will fail since these are fake paths, but entryPath is in the error
|
|
143
|
-
expect(result.isErr()).toBe(true)
|
|
144
|
-
if (result.isErr()) {
|
|
145
|
-
expect(result.error.message).toContain(join(fakePkgDir, 'dist/import.js'))
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('falls back to node condition when import is not available', async () => {
|
|
150
|
-
mockPackage('node-fallback')
|
|
151
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
152
|
-
'.': [
|
|
153
|
-
[['default'], './dist/default.js'],
|
|
154
|
-
[['node'], './dist/node.js'],
|
|
155
|
-
],
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
159
|
-
|
|
160
|
-
expect(result.isErr()).toBe(true)
|
|
161
|
-
if (result.isErr()) {
|
|
162
|
-
expect(result.error.message).toContain(join(fakePkgDir, 'dist/node.js'))
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('falls back to default condition when import and node are not available', async () => {
|
|
167
|
-
mockPackage('default-fallback')
|
|
168
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
169
|
-
'.': [[['default'], './dist/default.js']],
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
173
|
-
|
|
174
|
-
expect(result.isErr()).toBe(true)
|
|
175
|
-
if (result.isErr()) {
|
|
176
|
-
expect(result.error.message).toContain(join(fakePkgDir, 'dist/default.js'))
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('skips entries that include types condition', async () => {
|
|
181
|
-
mockPackage('skip-types')
|
|
182
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
183
|
-
'.': [
|
|
184
|
-
[['import', 'types'], './dist/index.d.ts'],
|
|
185
|
-
[['import'], './dist/index.js'],
|
|
186
|
-
],
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
190
|
-
|
|
191
|
-
expect(result.isErr()).toBe(true)
|
|
192
|
-
if (result.isErr()) {
|
|
193
|
-
expect(result.error.message).toContain(join(fakePkgDir, 'dist/index.js'))
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
describe('error message quality', () => {
|
|
199
|
-
it('includes package name in error messages', async () => {
|
|
200
|
-
mockPackage('@scope/my-components')
|
|
201
|
-
mockGetEntryPoints.mockResolvedValue({})
|
|
202
|
-
|
|
203
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
204
|
-
|
|
205
|
-
expect(result.isErr()).toBe(true)
|
|
206
|
-
if (result.isErr()) {
|
|
207
|
-
expect(result.error.message).toContain('@scope/my-components')
|
|
208
|
-
}
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('includes package.json path in no-entry-point error', async () => {
|
|
212
|
-
mockPackage('no-entry')
|
|
213
|
-
mockGetEntryPoints.mockResolvedValue({})
|
|
214
|
-
|
|
215
|
-
const result = await createComponentLoader(join(fakePkgDir, 'src/Component.tsx'))
|
|
216
|
-
|
|
217
|
-
expect(result.isErr()).toBe(true)
|
|
218
|
-
if (result.isErr()) {
|
|
219
|
-
expect(result.error.message).toContain(fakePkgJsonPath)
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
describe('component resolution behavior', () => {
|
|
225
|
-
it('fails today for direct named export when function name is minified', async () => {
|
|
226
|
-
const packageDir = '/fake/minified-export-test'
|
|
227
|
-
const packageJsonPath = join(packageDir, 'package.json')
|
|
228
|
-
|
|
229
|
-
mockReadPackageUp.mockReturnValue({
|
|
230
|
-
packageJson: { name: 'minified-export-test' } as NormalizedPackageJson,
|
|
231
|
-
path: packageJsonPath,
|
|
232
|
-
})
|
|
233
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
234
|
-
'.': [[['import'], './dist/index.js']],
|
|
235
|
-
})
|
|
236
|
-
mockModuleRequire.mockImplementation(() => ({
|
|
237
|
-
CardX: function j() {
|
|
238
|
-
return null
|
|
239
|
-
},
|
|
240
|
-
}))
|
|
241
|
-
|
|
242
|
-
const result = await createComponentLoader(join(packageDir, 'src/CardX.tsx'))
|
|
243
|
-
|
|
244
|
-
expect(result.isOk()).toBe(true)
|
|
245
|
-
if (result.isOk()) {
|
|
246
|
-
const component = result.value.loadComponent('CardX')
|
|
247
|
-
// TDD red phase: this currently returns null because function.name === "j".
|
|
248
|
-
expect(component).not.toBeNull()
|
|
249
|
-
}
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('rejects direct function exports with non-PascalCase export keys', async () => {
|
|
253
|
-
const packageDir = '/fake/lowercase-export-test'
|
|
254
|
-
const packageJsonPath = join(packageDir, 'package.json')
|
|
255
|
-
|
|
256
|
-
mockReadPackageUp.mockReturnValue({
|
|
257
|
-
packageJson: { name: 'lowercase-export-test' } as NormalizedPackageJson,
|
|
258
|
-
path: packageJsonPath,
|
|
259
|
-
})
|
|
260
|
-
mockGetEntryPoints.mockResolvedValue({
|
|
261
|
-
'.': [[['import'], './dist/index.js']],
|
|
262
|
-
})
|
|
263
|
-
mockModuleRequire.mockImplementation(() => ({
|
|
264
|
-
helper: function j() {
|
|
265
|
-
return null
|
|
266
|
-
},
|
|
267
|
-
}))
|
|
268
|
-
|
|
269
|
-
const result = await createComponentLoader(join(packageDir, 'src/helper.ts'))
|
|
270
|
-
|
|
271
|
-
expect(result.isOk()).toBe(true)
|
|
272
|
-
if (result.isOk()) {
|
|
273
|
-
const component = result.value.loadComponent('helper')
|
|
274
|
-
expect(component).toBeNull()
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
})
|
package/src/component-loader.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module'
|
|
2
|
-
import { dirname, join, resolve } from 'node:path'
|
|
3
|
-
import { pascalCase } from 'case-anything'
|
|
4
|
-
import { ResultAsync, errAsync, okAsync } from 'neverthrow'
|
|
5
|
-
import { getPackageEntryPoints } from 'pkg-entry-points'
|
|
6
|
-
import type { ComponentType } from 'react'
|
|
7
|
-
import { readPackageUpSync } from 'read-package-up'
|
|
8
|
-
import { IoError, NotFoundError } from './errors'
|
|
9
|
-
|
|
10
|
-
type NotFoundErrorInstance = InstanceType<typeof NotFoundError>
|
|
11
|
-
type IoErrorInstance = InstanceType<typeof IoError>
|
|
12
|
-
export type LoaderError = NotFoundErrorInstance | IoErrorInstance
|
|
13
|
-
|
|
14
|
-
export interface ComponentLoaderResult {
|
|
15
|
-
loadComponent: (name: string) => ComponentType<unknown> | null
|
|
16
|
-
packageName: string
|
|
17
|
-
entryPath: string | undefined
|
|
18
|
-
exportNames: string[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Creates a component loader that dynamically loads components from the
|
|
23
|
-
* user's package. Finds the nearest package.json, resolves its entry point,
|
|
24
|
-
* and loads the module.
|
|
25
|
-
*
|
|
26
|
-
* The returned `loadComponent` is synchronous (required by processComponent).
|
|
27
|
-
* ESM modules are loaded via async `import()` during creation, so this
|
|
28
|
-
* function itself is async.
|
|
29
|
-
*
|
|
30
|
-
* @returns A `ResultAsync` containing a `ComponentLoaderResult` on success.
|
|
31
|
-
* @errors {NotFoundError} When no package.json is found or the package has no resolvable entry points.
|
|
32
|
-
* @errors {IoError} When module loading fails (CJS require or ESM import).
|
|
33
|
-
*/
|
|
34
|
-
export function createComponentLoader(componentPath: string): ResultAsync<ComponentLoaderResult, LoaderError> {
|
|
35
|
-
// 1. Find nearest package.json
|
|
36
|
-
const result = readPackageUpSync({ cwd: dirname(resolve(componentPath)) })
|
|
37
|
-
if (!result) {
|
|
38
|
-
return errAsync(new NotFoundError('No package.json found', { props: { phase: 'load' } }))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const { packageJson, path: packageJsonPath } = result
|
|
42
|
-
const packageDir = dirname(packageJsonPath)
|
|
43
|
-
const packageName = packageJson.name
|
|
44
|
-
|
|
45
|
-
// 2. Resolve entry point(s) via pkg-entry-points (handles exports + legacy main/module)
|
|
46
|
-
return ResultAsync.fromPromise(
|
|
47
|
-
getPackageEntryPoints(packageDir),
|
|
48
|
-
(err) =>
|
|
49
|
-
new IoError(`Failed to resolve entry points for "${packageName}"`, {
|
|
50
|
-
cause: err instanceof Error ? err : new Error(String(err)),
|
|
51
|
-
props: { phase: 'load' },
|
|
52
|
-
}),
|
|
53
|
-
).andThen((entryPoints) => {
|
|
54
|
-
return loadFromEntryPoints(entryPoints, packageDir, packageName, packageJsonPath)
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Given resolved entry points, load the appropriate module(s) and construct the
|
|
60
|
-
* `ComponentLoaderResult`.
|
|
61
|
-
*
|
|
62
|
-
* @returns A `ResultAsync` containing a `ComponentLoaderResult` on success.
|
|
63
|
-
* @errors {NotFoundError} When the package has no resolvable or loadable entry points.
|
|
64
|
-
* @errors {IoError} When module loading fails.
|
|
65
|
-
*/
|
|
66
|
-
function loadFromEntryPoints(
|
|
67
|
-
entryPoints: Record<string, [string[], string][]>,
|
|
68
|
-
packageDir: string,
|
|
69
|
-
packageName: string | undefined,
|
|
70
|
-
packageJsonPath: string,
|
|
71
|
-
): ResultAsync<ComponentLoaderResult, LoaderError> {
|
|
72
|
-
const subpaths = Object.keys(entryPoints)
|
|
73
|
-
|
|
74
|
-
if (subpaths.length === 0) {
|
|
75
|
-
return errAsync(
|
|
76
|
-
new NotFoundError(
|
|
77
|
-
`Package "${packageName}" has no resolvable entry point. Add an "exports", "module", or "main" field to ${packageJsonPath}`,
|
|
78
|
-
{ props: { phase: 'load' } },
|
|
79
|
-
),
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 3. Load module(s)
|
|
84
|
-
const dotEntry = entryPoints['.']
|
|
85
|
-
if (dotEntry || subpaths.length === 1) {
|
|
86
|
-
// Single entry point: either explicit "." or the only available subpath
|
|
87
|
-
const tuples = dotEntry ?? entryPoints[subpaths[0]]
|
|
88
|
-
const resolved = selectBestPath(tuples)
|
|
89
|
-
if (!resolved) {
|
|
90
|
-
return errAsync(
|
|
91
|
-
new NotFoundError(`Package "${packageName}" has no loadable exports`, {
|
|
92
|
-
props: { phase: 'load' },
|
|
93
|
-
}),
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
const entryPath = join(packageDir, resolved)
|
|
97
|
-
return loadModule(entryPath).map((exports) => {
|
|
98
|
-
return {
|
|
99
|
-
loadComponent: (name: string) => findComponent(exports, name),
|
|
100
|
-
packageName: packageName ?? packageDir,
|
|
101
|
-
entryPath,
|
|
102
|
-
exportNames: Object.keys(exports),
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Multiple subpaths (no ".") — load all
|
|
108
|
-
return loadAllSubpaths(entryPoints, subpaths, packageDir, packageName)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Loads all subpath entry points and merges them into a single loader result.
|
|
113
|
-
*
|
|
114
|
-
* @returns A `ResultAsync` containing a `ComponentLoaderResult` on success.
|
|
115
|
-
* @errors {NotFoundError} When none of the subpath modules could be loaded.
|
|
116
|
-
* @errors {IoError} When module loading fails.
|
|
117
|
-
*/
|
|
118
|
-
function loadAllSubpaths(
|
|
119
|
-
entryPoints: Record<string, [string[], string][]>,
|
|
120
|
-
subpaths: string[],
|
|
121
|
-
packageDir: string,
|
|
122
|
-
packageName: string | undefined,
|
|
123
|
-
): ResultAsync<ComponentLoaderResult, LoaderError> {
|
|
124
|
-
const allModules: Record<string, unknown>[] = []
|
|
125
|
-
const allExportNames: string[] = []
|
|
126
|
-
const loadErrors: IoErrorInstance[] = []
|
|
127
|
-
|
|
128
|
-
// Build a sequential chain that loads each subpath
|
|
129
|
-
let chain: ResultAsync<void, never> = okAsync(undefined)
|
|
130
|
-
|
|
131
|
-
for (const subpath of subpaths) {
|
|
132
|
-
const resolved = selectBestPath(entryPoints[subpath])
|
|
133
|
-
if (!resolved) {
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
const subEntryPath = join(packageDir, resolved)
|
|
137
|
-
|
|
138
|
-
chain = chain.andThen(() =>
|
|
139
|
-
loadModule(subEntryPath)
|
|
140
|
-
.map((exports) => {
|
|
141
|
-
allModules.push(exports)
|
|
142
|
-
allExportNames.push(...Object.keys(exports))
|
|
143
|
-
})
|
|
144
|
-
.orElse((err) => {
|
|
145
|
-
loadErrors.push(err)
|
|
146
|
-
return okAsync(undefined)
|
|
147
|
-
}),
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return chain.andThen(() => {
|
|
152
|
-
if (allModules.length === 0) {
|
|
153
|
-
const message = loadErrors[0]?.message ?? `Package "${packageName}" has no loadable exports`
|
|
154
|
-
return errAsync(
|
|
155
|
-
new NotFoundError(message, {
|
|
156
|
-
cause: loadErrors[0],
|
|
157
|
-
props: { phase: 'load' },
|
|
158
|
-
}),
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return okAsync({
|
|
163
|
-
loadComponent: (name: string) => {
|
|
164
|
-
for (const mod of allModules) {
|
|
165
|
-
const found = findComponent(mod, name)
|
|
166
|
-
if (found) return found
|
|
167
|
-
}
|
|
168
|
-
return null
|
|
169
|
-
},
|
|
170
|
-
packageName: packageName ?? packageDir,
|
|
171
|
-
entryPath: undefined,
|
|
172
|
-
exportNames: allExportNames,
|
|
173
|
-
} satisfies ComponentLoaderResult)
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Attempts to load a module, first via ESM `import()`, then via CJS `require`.
|
|
179
|
-
*
|
|
180
|
-
* ESM `import()` is preferred because it participates in the ESM loader hook
|
|
181
|
-
* pipeline (registered via `module.register()` in the CLI). This is required
|
|
182
|
-
* for JSX interception to work — the loader hook redirects `react/jsx-runtime`
|
|
183
|
-
* to the interceptable version. CJS `require()` bypasses ESM hooks even when
|
|
184
|
-
* loading ESM modules (Node 22+), so it's only used as a fallback.
|
|
185
|
-
*
|
|
186
|
-
* @param entryPath - Absolute path to the module entry point.
|
|
187
|
-
* @returns A `ResultAsync` containing the module exports on success.
|
|
188
|
-
* @errors {IoError} When both ESM import and CJS require fail.
|
|
189
|
-
*/
|
|
190
|
-
export function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, IoErrorInstance> {
|
|
191
|
-
return ResultAsync.fromPromise(import(entryPath) as Promise<Record<string, unknown>>, () => null).orElse(() => {
|
|
192
|
-
try {
|
|
193
|
-
const require = createRequire(import.meta.url)
|
|
194
|
-
const exports = require(entryPath)
|
|
195
|
-
return okAsync(exports as Record<string, unknown>)
|
|
196
|
-
} catch (requireErr) {
|
|
197
|
-
const cause = requireErr instanceof Error ? requireErr : new Error(String(requireErr))
|
|
198
|
-
return errAsync(
|
|
199
|
-
new IoError(`Failed to load ${entryPath}: ${cause.message}`, {
|
|
200
|
-
cause,
|
|
201
|
-
props: { phase: 'load' },
|
|
202
|
-
}),
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function selectBestPath(tuples: [string[], string][]): string | undefined {
|
|
209
|
-
const preferred = ['import', 'node', 'default']
|
|
210
|
-
for (const cond of preferred) {
|
|
211
|
-
const match = tuples.find(([c]) => c.includes(cond) && !c.includes('types'))
|
|
212
|
-
if (match) {
|
|
213
|
-
return match[1]
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const fallback = tuples.find(([c]) => !c.includes('types'))?.[1]
|
|
217
|
-
return fallback
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function isPascalCase(name: string): boolean {
|
|
221
|
-
return name.length > 0 && pascalCase(name) === name
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function isComponent(value: unknown): value is ComponentType<unknown> {
|
|
225
|
-
if (typeof value === 'function') return isPascalCase(value.name)
|
|
226
|
-
// React.memo() and React.forwardRef() return objects, not functions
|
|
227
|
-
if (typeof value === 'object' && value !== null && '$$typeof' in value) return true
|
|
228
|
-
return false
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function findComponent(moduleExports: Record<string, unknown>, name: string): ComponentType<unknown> | null {
|
|
232
|
-
// Direct named export
|
|
233
|
-
const direct = moduleExports[name]
|
|
234
|
-
if ((typeof direct === 'function' && isPascalCase(name)) || isComponent(direct)) {
|
|
235
|
-
return direct as ComponentType<unknown>
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Check default export
|
|
239
|
-
const defaultExport = moduleExports.default
|
|
240
|
-
if (defaultExport) {
|
|
241
|
-
// default is the component itself (matches by function name)
|
|
242
|
-
if (typeof defaultExport === 'function' && defaultExport.name === name) {
|
|
243
|
-
return defaultExport as ComponentType<unknown>
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// CJS interop: default is an object with named exports
|
|
247
|
-
if (typeof defaultExport === 'object' && defaultExport !== null) {
|
|
248
|
-
const nested = (defaultExport as Record<string, unknown>)[name]
|
|
249
|
-
if (isComponent(nested)) {
|
|
250
|
-
return nested as ComponentType<unknown>
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return null
|
|
256
|
-
}
|