@hyperspan/plugin-preact 2.0.0-alpha.2 → 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 +11 -10
- package/src/index.test.ts +11 -1
- package/src/index.ts +112 -38
- 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,21 +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
|
-
"vite": "^
|
|
37
|
-
"vitest": "^
|
|
38
|
+
"vite": "^8.2.1",
|
|
39
|
+
"vitest": "^4.1.10"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
|
-
"@hyperspan/
|
|
41
|
-
"@hyperspan/
|
|
42
|
-
"
|
|
43
|
-
"vite": "^
|
|
42
|
+
"@hyperspan/framework": "^2.0.0-alpha.20",
|
|
43
|
+
"@hyperspan/vite-plugin": "^2.0.0-alpha.20",
|
|
44
|
+
"preact": "^10.28.3",
|
|
45
|
+
"vite": "^8.0.0"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"debug": "^4.4.3",
|
|
47
|
-
"preact": "^10.28.3",
|
|
48
49
|
"preact-render-to-string": "^6.6.5"
|
|
49
50
|
}
|
|
50
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,42 +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
|
-
import { html } from '@hyperspan/html';
|
|
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,
|
|
17
|
+
registerClientChunkAliases,
|
|
18
|
+
registerIslandClientUrl,
|
|
19
|
+
registerRuntimeSpecifier,
|
|
20
|
+
isSsrTransform,
|
|
21
|
+
externalClientIslandRuntime,
|
|
22
|
+
isRuntimeSpecifier,
|
|
23
|
+
viteFsUrl,
|
|
24
|
+
buildIslandHtmlSource,
|
|
25
|
+
type IslandExportKind,
|
|
16
26
|
} from '@hyperspan/vite-plugin/islands';
|
|
27
|
+
import {
|
|
28
|
+
preactSingletonAliases,
|
|
29
|
+
resolvePackageDir,
|
|
30
|
+
resolvePreactSpecifier,
|
|
31
|
+
} from './resolve-package';
|
|
17
32
|
import debug from 'debug';
|
|
18
33
|
|
|
19
34
|
const log = debug('hyperspan:plugin-preact');
|
|
20
35
|
|
|
21
|
-
|
|
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;
|
|
22
50
|
|
|
23
|
-
export
|
|
24
|
-
jsId: string,
|
|
25
|
-
componentName: string,
|
|
26
|
-
esmName: string,
|
|
27
|
-
jsContent: string,
|
|
28
|
-
ssrContent: string,
|
|
29
|
-
options: { loading?: string; exportKind?: IslandExportKind } = {}
|
|
30
|
-
): string {
|
|
31
|
-
const importStmt =
|
|
32
|
-
options.exportKind === 'named'
|
|
33
|
-
? `import { ${componentName} } from "${esmName}";`
|
|
34
|
-
: `import ${componentName} from "${esmName}";`;
|
|
35
|
-
const scriptTag = `<script type="module" id="${jsId}_script" data-source-id="${jsId}">${importStmt}${jsContent}</script>`;
|
|
36
|
-
if (options.loading === 'lazy') {
|
|
37
|
-
return `<div id="${jsId}">${ssrContent}</div><div data-loading="lazy" style="height:1px;width:1px;overflow:hidden;"><template>\n${scriptTag}</template></div>`;
|
|
38
|
-
}
|
|
39
|
-
return `<div id="${jsId}">${ssrContent}</div>\n${scriptTag}`;
|
|
40
|
-
}
|
|
51
|
+
export type { IslandExportKind };
|
|
41
52
|
|
|
42
53
|
export function renderPreactSSR(Component: unknown, props: Record<string, unknown> = {}): string {
|
|
43
54
|
return preactRenderToString(h(Component as Parameters<typeof h>[0], props));
|
|
@@ -134,52 +145,110 @@ function __hs_renderIsland_${componentName}(jsContent = '', ssrContent = '', opt
|
|
|
134
145
|
`;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
const PREACT_CLIENT_SPECIFIERS = PREACT_ISLAND.specifiers;
|
|
149
|
+
|
|
137
150
|
/**
|
|
138
151
|
* Vite plugin for Preact islands.
|
|
139
152
|
*/
|
|
140
153
|
export function preactIslandPlugin(): Plugin {
|
|
154
|
+
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
141
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);
|
|
142
159
|
|
|
143
160
|
return {
|
|
144
161
|
name: 'hyperspan-preact',
|
|
145
162
|
enforce: 'pre',
|
|
146
163
|
|
|
147
|
-
config() {
|
|
164
|
+
config(config) {
|
|
165
|
+
const projectRoot = config.root ? String(config.root) : process.cwd();
|
|
166
|
+
const prerender = resolvePackageDir(pluginRoot, 'preact-render-to-string');
|
|
148
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
|
+
},
|
|
149
180
|
optimizeDeps: {
|
|
150
181
|
include: ['preact', 'preact/hooks', 'preact/jsx-runtime'],
|
|
151
182
|
},
|
|
152
183
|
};
|
|
153
184
|
},
|
|
154
185
|
|
|
155
|
-
|
|
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
|
+
},
|
|
156
209
|
|
|
157
210
|
configureServer() {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
registerImport('preact/compat', clientUrl);
|
|
164
|
-
registerImport('react', clientUrl);
|
|
165
|
-
registerImport('react-dom', clientUrl);
|
|
211
|
+
const productionUrl = '/islands/preact-client.js';
|
|
212
|
+
const devUrl = viteFsUrl(preactClientPath);
|
|
213
|
+
for (const spec of PREACT_CLIENT_SPECIFIERS) {
|
|
214
|
+
registerRuntimeSpecifier(spec, productionUrl, { devUrl });
|
|
215
|
+
}
|
|
166
216
|
},
|
|
167
217
|
|
|
168
|
-
|
|
169
|
-
|
|
218
|
+
generateBundle(_options, bundle) {
|
|
219
|
+
registerClientChunkAliases(
|
|
220
|
+
bundle,
|
|
221
|
+
(fileName) =>
|
|
222
|
+
fileName.includes('islands/preact-client') || fileName.endsWith('preact-client.js'),
|
|
223
|
+
PREACT_CLIENT_SPECIFIERS
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async transform(code, id, options) {
|
|
228
|
+
if (!isIslandModule(id, framework, ext)) return;
|
|
170
229
|
|
|
171
230
|
log('transform island', id);
|
|
172
231
|
const cleanId = splitIslandId(id).path;
|
|
173
|
-
const esmName =
|
|
232
|
+
const esmName = registerIslandClientUrl(cleanId, framework, root);
|
|
174
233
|
const exports = discoverPreactExports(code);
|
|
175
234
|
|
|
176
235
|
if (exports.length === 0) {
|
|
177
236
|
throw new Error(
|
|
178
|
-
`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}' }\`.`
|
|
179
238
|
);
|
|
180
239
|
}
|
|
181
240
|
|
|
182
|
-
|
|
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
|
+
}
|
|
183
252
|
|
|
184
253
|
const attachments = exports
|
|
185
254
|
.map((exp) => buildIslandAttachment(exp, cleanId, esmName))
|
|
@@ -188,7 +257,7 @@ export function preactIslandPlugin(): Plugin {
|
|
|
188
257
|
const moduleCode = `// hyperspan:processed
|
|
189
258
|
import { h as __hs_h, render as __hs_render, hydrate as __hs_hydrate } from 'preact';
|
|
190
259
|
import { render as __hs_renderToString } from 'preact-render-to-string';
|
|
191
|
-
|
|
260
|
+
${buildIslandHtmlSource}
|
|
192
261
|
|
|
193
262
|
${code}
|
|
194
263
|
${attachments}
|
|
@@ -216,7 +285,12 @@ ${attachments}
|
|
|
216
285
|
* Hyperspan config plugin — register in hyperspan.config.ts plugins array.
|
|
217
286
|
*/
|
|
218
287
|
export function preactPlugin(): HS.Plugin {
|
|
219
|
-
registerIslandPlugin(
|
|
288
|
+
registerIslandPlugin({
|
|
289
|
+
framework: PREACT_ISLAND.framework,
|
|
290
|
+
ext: PREACT_ISLAND.ext,
|
|
291
|
+
specifiers: PREACT_ISLAND.specifiers,
|
|
292
|
+
vitePlugin: preactIslandPlugin,
|
|
293
|
+
});
|
|
220
294
|
return () => {
|
|
221
295
|
log('preactPlugin loaded');
|
|
222
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
|
+
}
|