@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.
@@ -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
- })
@@ -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
- }