@hyperspan/plugin-preact 2.0.0-alpha.19 → 2.0.0-alpha.20
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/package.json +8 -6
- package/src/index.test.ts +11 -1
- package/src/index.ts +99 -41
- package/src/preact-client.ts +39 -4
- package/src/resolve-package.test.ts +68 -0
- package/src/resolve-package.ts +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/plugin-preact",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.20",
|
|
4
4
|
"description": "Hyperspan Plugin for Preact",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./src/index.ts",
|
|
16
16
|
"default": "./src/index.ts"
|
|
17
|
-
}
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
18
19
|
},
|
|
19
20
|
"author": "Vance Lucas <vance@vancelucas.com>",
|
|
20
21
|
"license": "BSD-3-Clause",
|
|
@@ -30,20 +31,21 @@
|
|
|
30
31
|
"url": "git+https://github.com/vlucas/hyperspan.git"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@hyperspan/vite-plugin": "^2.0.0-alpha.
|
|
34
|
+
"@hyperspan/vite-plugin": "^2.0.0-alpha.20",
|
|
35
|
+
"preact": "^10.28.3",
|
|
34
36
|
"prettier": "^3.8.1",
|
|
35
37
|
"typescript": "^5.9.3",
|
|
36
38
|
"vite": "^8.2.1",
|
|
37
39
|
"vitest": "^4.1.10"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
|
-
"@hyperspan/framework": "^2.0.0-alpha.
|
|
41
|
-
"@hyperspan/vite-plugin": "^2.0.0-alpha.
|
|
42
|
+
"@hyperspan/framework": "^2.0.0-alpha.20",
|
|
43
|
+
"@hyperspan/vite-plugin": "^2.0.0-alpha.20",
|
|
44
|
+
"preact": "^10.28.3",
|
|
42
45
|
"vite": "^8.0.0"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
48
|
"debug": "^4.4.3",
|
|
46
|
-
"preact": "^10.28.3",
|
|
47
49
|
"preact-render-to-string": "^6.6.5"
|
|
48
50
|
}
|
|
49
51
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { test, describe, expect } from 'vitest';
|
|
2
2
|
import { h } from 'preact';
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from 'preact/hooks';
|
|
4
|
+
import { buildIslandHtml } from '@hyperspan/vite-plugin/islands';
|
|
5
|
+
import { renderPreactSSR, renderPreactIsland } from './index';
|
|
4
6
|
|
|
5
7
|
// ---------------------------------------------------------------------------
|
|
6
8
|
// Simple Preact components defined inline — no .tsx compilation needed
|
|
@@ -91,6 +93,14 @@ describe('renderPreactSSR', () => {
|
|
|
91
93
|
const output = renderPreactSSR(Hello, { name: '<script>alert(1)</script>' });
|
|
92
94
|
expect(output).not.toContain('<script>alert');
|
|
93
95
|
});
|
|
96
|
+
|
|
97
|
+
test('renders components that use hooks (same Preact as the renderer)', () => {
|
|
98
|
+
function Counter({ count: initial = 0 }: { count?: number }) {
|
|
99
|
+
const [count] = useState(initial);
|
|
100
|
+
return h('p', null, `Count: ${count}`);
|
|
101
|
+
}
|
|
102
|
+
expect(renderPreactSSR(Counter, { count: 3 })).toContain('Count: 3');
|
|
103
|
+
});
|
|
94
104
|
});
|
|
95
105
|
|
|
96
106
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -2,43 +2,53 @@ import type { Plugin } from 'vite';
|
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { assetHash } from '@hyperspan/framework/utils';
|
|
5
|
-
import { registerImport, JS_ISLAND_PUBLIC_PATH } from '@hyperspan/framework/client/js';
|
|
6
5
|
import { renderIsland } from '@hyperspan/framework';
|
|
7
6
|
import type { Hyperspan as HS } from '@hyperspan/framework';
|
|
8
7
|
import { html } from '@hyperspan/framework/html';
|
|
9
8
|
import { h } from 'preact';
|
|
10
9
|
import { render as preactRenderToString } from 'preact-render-to-string';
|
|
10
|
+
|
|
11
|
+
export { render as renderToString } from 'preact-render-to-string';
|
|
11
12
|
import {
|
|
12
13
|
registerIslandPlugin,
|
|
13
14
|
isIslandModule,
|
|
14
15
|
islandPluginResolveId,
|
|
15
16
|
splitIslandId,
|
|
16
17
|
registerClientChunkAliases,
|
|
18
|
+
registerIslandClientUrl,
|
|
19
|
+
registerRuntimeSpecifier,
|
|
20
|
+
isSsrTransform,
|
|
21
|
+
externalClientIslandRuntime,
|
|
22
|
+
isRuntimeSpecifier,
|
|
23
|
+
viteFsUrl,
|
|
24
|
+
buildIslandHtmlSource,
|
|
25
|
+
type IslandExportKind,
|
|
17
26
|
} from '@hyperspan/vite-plugin/islands';
|
|
27
|
+
import {
|
|
28
|
+
preactSingletonAliases,
|
|
29
|
+
resolvePackageDir,
|
|
30
|
+
resolvePreactSpecifier,
|
|
31
|
+
} from './resolve-package';
|
|
18
32
|
import debug from 'debug';
|
|
19
33
|
|
|
20
34
|
const log = debug('hyperspan:plugin-preact');
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return `<div id="${jsId}">${ssrContent}</div><div data-loading="lazy" style="height:1px;width:1px;overflow:hidden;"><template>\n${scriptTag}</template></div>`;
|
|
39
|
-
}
|
|
40
|
-
return `<div id="${jsId}">${ssrContent}</div>\n${scriptTag}`;
|
|
41
|
-
}
|
|
36
|
+
const PREACT_ISLAND = {
|
|
37
|
+
framework: 'preact',
|
|
38
|
+
ext: '.tsx',
|
|
39
|
+
specifiers: [
|
|
40
|
+
'preact',
|
|
41
|
+
'preact/hooks',
|
|
42
|
+
'preact/jsx-runtime',
|
|
43
|
+
'preact/jsx-dev-runtime',
|
|
44
|
+
'preact/compat',
|
|
45
|
+
'react',
|
|
46
|
+
'react-dom',
|
|
47
|
+
] as const,
|
|
48
|
+
runtimePrefixes: ['preact', 'react', 'react-dom', 'preact-render-to-string'] as const,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type { IslandExportKind };
|
|
42
52
|
|
|
43
53
|
export function renderPreactSSR(Component: unknown, props: Record<string, unknown> = {}): string {
|
|
44
54
|
return preactRenderToString(h(Component as Parameters<typeof h>[0], props));
|
|
@@ -135,40 +145,73 @@ function __hs_renderIsland_${componentName}(jsContent = '', ssrContent = '', opt
|
|
|
135
145
|
`;
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
const PREACT_CLIENT_SPECIFIERS =
|
|
139
|
-
'preact',
|
|
140
|
-
'preact/hooks',
|
|
141
|
-
'preact/jsx-runtime',
|
|
142
|
-
'preact/jsx-dev-runtime',
|
|
143
|
-
'preact/compat',
|
|
144
|
-
'react',
|
|
145
|
-
'react-dom',
|
|
146
|
-
] as const;
|
|
148
|
+
const PREACT_CLIENT_SPECIFIERS = PREACT_ISLAND.specifiers;
|
|
147
149
|
|
|
148
150
|
/**
|
|
149
151
|
* Vite plugin for Preact islands.
|
|
150
152
|
*/
|
|
151
153
|
export function preactIslandPlugin(): Plugin {
|
|
154
|
+
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
152
155
|
const preactClientPath = resolve(dirname(fileURLToPath(import.meta.url)), './preact-client.ts');
|
|
156
|
+
let root = process.cwd();
|
|
157
|
+
const { framework, ext } = PREACT_ISLAND;
|
|
158
|
+
const resolveIslandId = islandPluginResolveId(framework, ext);
|
|
153
159
|
|
|
154
160
|
return {
|
|
155
161
|
name: 'hyperspan-preact',
|
|
156
162
|
enforce: 'pre',
|
|
157
163
|
|
|
158
|
-
config() {
|
|
164
|
+
config(config) {
|
|
165
|
+
const projectRoot = config.root ? String(config.root) : process.cwd();
|
|
166
|
+
const prerender = resolvePackageDir(pluginRoot, 'preact-render-to-string');
|
|
159
167
|
return {
|
|
168
|
+
resolve: {
|
|
169
|
+
dedupe: ['preact', 'preact/hooks', 'preact/jsx-runtime'],
|
|
170
|
+
},
|
|
171
|
+
ssr: {
|
|
172
|
+
noExternal: ['preact', 'preact/hooks', 'preact-render-to-string'],
|
|
173
|
+
resolve: {
|
|
174
|
+
alias: {
|
|
175
|
+
...preactSingletonAliases(projectRoot, pluginRoot),
|
|
176
|
+
...(prerender ? { 'preact-render-to-string': prerender } : {}),
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
160
180
|
optimizeDeps: {
|
|
161
181
|
include: ['preact', 'preact/hooks', 'preact/jsx-runtime'],
|
|
162
182
|
},
|
|
163
183
|
};
|
|
164
184
|
},
|
|
165
185
|
|
|
166
|
-
|
|
186
|
+
configResolved(config) {
|
|
187
|
+
root = config.root;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async resolveId(id, importer, options) {
|
|
191
|
+
const island = await resolveIslandId.call(this, id, importer, options);
|
|
192
|
+
if (island) return island;
|
|
193
|
+
const ssr = Boolean(options?.ssr) || this.environment?.name === 'ssr';
|
|
194
|
+
if (!ssr) {
|
|
195
|
+
const isRuntime = (specifier: string) =>
|
|
196
|
+
isRuntimeSpecifier(specifier, PREACT_ISLAND.runtimePrefixes);
|
|
197
|
+
if (
|
|
198
|
+
(this.environment?.mode === 'dev' || this.meta.watchMode) &&
|
|
199
|
+
importer &&
|
|
200
|
+
isIslandModule(importer, framework, ext) &&
|
|
201
|
+
isRuntime(id)
|
|
202
|
+
) {
|
|
203
|
+
return preactClientPath;
|
|
204
|
+
}
|
|
205
|
+
return externalClientIslandRuntime(id, importer, { ssr: false }, framework, ext, isRuntime);
|
|
206
|
+
}
|
|
207
|
+
return resolvePreactSpecifier(id, root, pluginRoot);
|
|
208
|
+
},
|
|
167
209
|
|
|
168
210
|
configureServer() {
|
|
169
|
-
const
|
|
211
|
+
const productionUrl = '/islands/preact-client.js';
|
|
212
|
+
const devUrl = viteFsUrl(preactClientPath);
|
|
170
213
|
for (const spec of PREACT_CLIENT_SPECIFIERS) {
|
|
171
|
-
|
|
214
|
+
registerRuntimeSpecifier(spec, productionUrl, { devUrl });
|
|
172
215
|
}
|
|
173
216
|
},
|
|
174
217
|
|
|
@@ -181,21 +224,31 @@ export function preactIslandPlugin(): Plugin {
|
|
|
181
224
|
);
|
|
182
225
|
},
|
|
183
226
|
|
|
184
|
-
async transform(code, id) {
|
|
185
|
-
if (!isIslandModule(id,
|
|
227
|
+
async transform(code, id, options) {
|
|
228
|
+
if (!isIslandModule(id, framework, ext)) return;
|
|
186
229
|
|
|
187
230
|
log('transform island', id);
|
|
188
231
|
const cleanId = splitIslandId(id).path;
|
|
189
|
-
const esmName =
|
|
232
|
+
const esmName = registerIslandClientUrl(cleanId, framework, root);
|
|
190
233
|
const exports = discoverPreactExports(code);
|
|
191
234
|
|
|
192
235
|
if (exports.length === 0) {
|
|
193
236
|
throw new Error(
|
|
194
|
-
`No component exports found in ${cleanId}. Export a default or named Preact component and import with \`with { island: '
|
|
237
|
+
`No component exports found in ${cleanId}. Export a default or named Preact component and import with \`with { island: '${framework}' }\`.`
|
|
195
238
|
);
|
|
196
239
|
}
|
|
197
240
|
|
|
198
|
-
|
|
241
|
+
if (!isSsrTransform(this, options)) {
|
|
242
|
+
const esbuild = await import('esbuild');
|
|
243
|
+
const result = await esbuild.transform(code, {
|
|
244
|
+
loader: 'tsx',
|
|
245
|
+
jsx: 'transform',
|
|
246
|
+
jsxFactory: 'h',
|
|
247
|
+
jsxFragment: 'Fragment',
|
|
248
|
+
sourcefile: cleanId,
|
|
249
|
+
});
|
|
250
|
+
return { code: result.code, map: result.map || null };
|
|
251
|
+
}
|
|
199
252
|
|
|
200
253
|
const attachments = exports
|
|
201
254
|
.map((exp) => buildIslandAttachment(exp, cleanId, esmName))
|
|
@@ -204,7 +257,7 @@ export function preactIslandPlugin(): Plugin {
|
|
|
204
257
|
const moduleCode = `// hyperspan:processed
|
|
205
258
|
import { h as __hs_h, render as __hs_render, hydrate as __hs_hydrate } from 'preact';
|
|
206
259
|
import { render as __hs_renderToString } from 'preact-render-to-string';
|
|
207
|
-
|
|
260
|
+
${buildIslandHtmlSource}
|
|
208
261
|
|
|
209
262
|
${code}
|
|
210
263
|
${attachments}
|
|
@@ -232,7 +285,12 @@ ${attachments}
|
|
|
232
285
|
* Hyperspan config plugin — register in hyperspan.config.ts plugins array.
|
|
233
286
|
*/
|
|
234
287
|
export function preactPlugin(): HS.Plugin {
|
|
235
|
-
registerIslandPlugin(
|
|
288
|
+
registerIslandPlugin({
|
|
289
|
+
framework: PREACT_ISLAND.framework,
|
|
290
|
+
ext: PREACT_ISLAND.ext,
|
|
291
|
+
specifiers: PREACT_ISLAND.specifiers,
|
|
292
|
+
vitePlugin: preactIslandPlugin,
|
|
293
|
+
});
|
|
236
294
|
return () => {
|
|
237
295
|
log('preactPlugin loaded');
|
|
238
296
|
};
|
package/src/preact-client.ts
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
1
|
+
// Named re-exports only — `export *` from compat + hooks + preact collides
|
|
2
|
+
// (h, useState, Fragment, …) and Vite's native ESM module then fails to load.
|
|
3
|
+
export {
|
|
4
|
+
h,
|
|
5
|
+
render,
|
|
6
|
+
hydrate,
|
|
7
|
+
Component,
|
|
8
|
+
Fragment,
|
|
9
|
+
createContext,
|
|
10
|
+
createRef,
|
|
11
|
+
toChildArray,
|
|
12
|
+
cloneElement,
|
|
13
|
+
createElement,
|
|
14
|
+
isValidElement,
|
|
15
|
+
} from 'preact';
|
|
16
|
+
export {
|
|
17
|
+
useState,
|
|
18
|
+
useEffect,
|
|
19
|
+
useLayoutEffect,
|
|
20
|
+
useReducer,
|
|
21
|
+
useRef,
|
|
22
|
+
useMemo,
|
|
23
|
+
useCallback,
|
|
24
|
+
useContext,
|
|
25
|
+
useId,
|
|
26
|
+
useErrorBoundary,
|
|
27
|
+
useDebugValue,
|
|
28
|
+
useImperativeHandle,
|
|
29
|
+
} from 'preact/hooks';
|
|
30
|
+
export { jsx, jsxs } from 'preact/jsx-runtime';
|
|
31
|
+
export {
|
|
32
|
+
memo,
|
|
33
|
+
forwardRef,
|
|
34
|
+
Children,
|
|
35
|
+
createPortal,
|
|
36
|
+
lazy,
|
|
37
|
+
Suspense,
|
|
38
|
+
PureComponent,
|
|
39
|
+
} from 'preact/compat';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
preactSingletonAliases,
|
|
7
|
+
resolvePackageDir,
|
|
8
|
+
resolvePackageFrom,
|
|
9
|
+
resolvePreactSpecifier,
|
|
10
|
+
} from './resolve-package';
|
|
11
|
+
|
|
12
|
+
function fakePackage(root: string, name: string, file = 'index.js') {
|
|
13
|
+
const dir = join(root, 'node_modules', ...name.split('/'));
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, main: file }));
|
|
16
|
+
writeFileSync(join(dir, file), 'export {}\n');
|
|
17
|
+
mkdirSync(join(dir, 'hooks'), { recursive: true });
|
|
18
|
+
writeFileSync(
|
|
19
|
+
join(dir, 'hooks', 'package.json'),
|
|
20
|
+
JSON.stringify({ name: `${name}/hooks`, main: 'index.js' })
|
|
21
|
+
);
|
|
22
|
+
writeFileSync(join(dir, 'hooks', 'index.js'), 'export {}\n');
|
|
23
|
+
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'app', type: 'module' }));
|
|
24
|
+
return realpathSync(dir);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('resolvePackageFrom', () => {
|
|
28
|
+
test('resolves a package from the starting directory', () => {
|
|
29
|
+
const root = mkdtempSync(join(tmpdir(), 'hs-preact-resolve-'));
|
|
30
|
+
const dir = fakePackage(root, 'preact');
|
|
31
|
+
expect(resolvePackageFrom(root, 'preact')).toBe(join(dir, 'index.js'));
|
|
32
|
+
expect(resolvePackageDir(root, 'preact')).toBe(dir);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('prefers the app copy when pinning preact', () => {
|
|
36
|
+
const app = mkdtempSync(join(tmpdir(), 'hs-preact-app-'));
|
|
37
|
+
const plugin = mkdtempSync(join(tmpdir(), 'hs-preact-plugin-'));
|
|
38
|
+
const appPreact = fakePackage(app, 'preact');
|
|
39
|
+
fakePackage(plugin, 'preact');
|
|
40
|
+
|
|
41
|
+
expect(preactSingletonAliases(app, plugin)).toEqual({ preact: appPreact });
|
|
42
|
+
expect(resolvePreactSpecifier('preact/hooks', app, plugin)).toContain(join(appPreact, 'hooks'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('falls back to the plugin copy when the app has no preact', () => {
|
|
46
|
+
const app = mkdtempSync(join(tmpdir(), 'hs-preact-empty-'));
|
|
47
|
+
const plugin = mkdtempSync(join(tmpdir(), 'hs-preact-plugin-'));
|
|
48
|
+
writeFileSync(join(app, 'package.json'), JSON.stringify({ name: 'app' }));
|
|
49
|
+
const pluginPreact = fakePackage(plugin, 'preact');
|
|
50
|
+
|
|
51
|
+
expect(preactSingletonAliases(app, plugin)).toEqual({ preact: pluginPreact });
|
|
52
|
+
expect(resolvePreactSpecifier('preact', app, plugin)).toBe(join(pluginPreact, 'index.js'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('resolves preact-render-to-string from the plugin when the app lacks it', () => {
|
|
56
|
+
const app = mkdtempSync(join(tmpdir(), 'hs-prerender-app-'));
|
|
57
|
+
const plugin = mkdtempSync(join(tmpdir(), 'hs-prerender-plugin-'));
|
|
58
|
+
writeFileSync(join(app, 'package.json'), JSON.stringify({ name: 'app' }));
|
|
59
|
+
const prerender = fakePackage(plugin, 'preact-render-to-string');
|
|
60
|
+
expect(resolvePreactSpecifier('preact-render-to-string', app, plugin)).toBe(
|
|
61
|
+
join(prerender, 'index.js')
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('ignores non-preact specifiers', () => {
|
|
66
|
+
expect(resolvePreactSpecifier('vue', '/', '/')).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname, join, sep } from 'node:path';
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
function realpath(path: string): string {
|
|
7
|
+
try {
|
|
8
|
+
return realpathSync(path);
|
|
9
|
+
} catch {
|
|
10
|
+
return path;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a specifier from a project's directory (its package.json / node_modules).
|
|
16
|
+
*/
|
|
17
|
+
export function resolvePackageFrom(fromDir: string, specifier: string): string | undefined {
|
|
18
|
+
const parent = pathToFileURL(join(fromDir, 'package.json')).href;
|
|
19
|
+
try {
|
|
20
|
+
return realpath(fileURLToPath(import.meta.resolve(specifier, parent)));
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
return realpath(createRequire(join(fromDir, 'package.json')).resolve(specifier));
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Directory of an installed package, starting from `fromDir`.
|
|
32
|
+
*/
|
|
33
|
+
export function resolvePackageDir(fromDir: string, name: string): string | undefined {
|
|
34
|
+
const file = resolvePackageFrom(fromDir, name);
|
|
35
|
+
if (!file) return undefined;
|
|
36
|
+
|
|
37
|
+
const needle = `${sep}node_modules${sep}${name.replaceAll('/', sep)}${sep}`;
|
|
38
|
+
const idx = file.lastIndexOf(needle);
|
|
39
|
+
if (idx !== -1) {
|
|
40
|
+
return realpath(file.slice(0, idx + needle.length - 1));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let dir = dirname(file);
|
|
44
|
+
while (true) {
|
|
45
|
+
const pkgFile = join(dir, 'package.json');
|
|
46
|
+
if (existsSync(pkgFile)) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(pkgFile, 'utf8')) as { name?: string };
|
|
49
|
+
if (pkg.name === name) return dir;
|
|
50
|
+
} catch {
|
|
51
|
+
// keep walking
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const parent = dirname(dir);
|
|
55
|
+
if (parent === dir) return undefined;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Force every `preact` / `preact/*` import onto one physical install.
|
|
62
|
+
* Prefer the app's copy; fall back to the plugin's so file: / monorepo links
|
|
63
|
+
* do not load hooks from one Preact and render-to-string from another.
|
|
64
|
+
*/
|
|
65
|
+
export function preactSingletonAliases(
|
|
66
|
+
projectRoot: string,
|
|
67
|
+
fallbackDir: string
|
|
68
|
+
): Record<string, string> {
|
|
69
|
+
const dir = resolvePackageDir(projectRoot, 'preact') ?? resolvePackageDir(fallbackDir, 'preact');
|
|
70
|
+
if (!dir) return {};
|
|
71
|
+
return { preact: dir };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function resolvePreactSpecifier(
|
|
75
|
+
specifier: string,
|
|
76
|
+
projectRoot: string,
|
|
77
|
+
fallbackDir: string
|
|
78
|
+
): string | undefined {
|
|
79
|
+
if (specifier === 'preact-render-to-string') {
|
|
80
|
+
return resolvePackageFrom(fallbackDir, specifier) ?? resolvePackageFrom(projectRoot, specifier);
|
|
81
|
+
}
|
|
82
|
+
if (specifier !== 'preact' && !specifier.startsWith('preact/')) return undefined;
|
|
83
|
+
return resolvePackageFrom(projectRoot, specifier) ?? resolvePackageFrom(fallbackDir, specifier);
|
|
84
|
+
}
|