@hyperspan/plugin-preact 1.0.0 → 1.0.2
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 -7
- package/src/index.ts +105 -81
- package/src/preact-client.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/plugin-preact",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Hyperspan Plugin for Preact",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -23,16 +23,17 @@
|
|
|
23
23
|
"url": "git+https://github.com/vlucas/hyperspan.git"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/bun": "^1.
|
|
27
|
-
"prettier": "^3.
|
|
28
|
-
"typescript": "^5.
|
|
26
|
+
"@types/bun": "^1.3.8",
|
|
27
|
+
"prettier": "^3.8.1",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@hyperspan/html": "^1.0.0",
|
|
32
|
-
"@hyperspan/framework": "^1.0.
|
|
32
|
+
"@hyperspan/framework": "^1.0.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"preact
|
|
35
|
+
"debug": "^4.4.3",
|
|
36
|
+
"preact": "^10.28.3",
|
|
37
|
+
"preact-render-to-string": "^6.6.5"
|
|
37
38
|
}
|
|
38
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { IS_PROD } from '@hyperspan/framework/server';
|
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import type { Hyperspan as HS } from '@hyperspan/framework';
|
|
6
6
|
import { html } from '@hyperspan/html';
|
|
7
|
+
import debug from 'debug';
|
|
8
|
+
|
|
9
|
+
const log = debug('hyperspan:plugin-preact');
|
|
7
10
|
|
|
8
11
|
// External ESM = https://esm.sh/preact@10.26.4/compat
|
|
9
12
|
const PREACT_ISLAND_CACHE = new Map<string, string>();
|
|
@@ -12,7 +15,11 @@ const PREACT_ISLAND_CACHE = new Map<string, string>();
|
|
|
12
15
|
* Build Preact client JS and copy to public folder
|
|
13
16
|
*/
|
|
14
17
|
async function copyPreactToPublicFolder(config: HS.Config) {
|
|
18
|
+
const currentNodeEnv = process.env.NODE_ENV || 'production';
|
|
15
19
|
const sourceFile = resolve(__dirname, './preact-client.ts');
|
|
20
|
+
|
|
21
|
+
// Preact client JS is always production mode
|
|
22
|
+
process.env.NODE_ENV = 'production';
|
|
16
23
|
const result = await Bun.build({
|
|
17
24
|
entrypoints: [sourceFile],
|
|
18
25
|
outdir: join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH),
|
|
@@ -21,6 +28,7 @@ async function copyPreactToPublicFolder(config: HS.Config) {
|
|
|
21
28
|
format: 'esm',
|
|
22
29
|
target: 'browser',
|
|
23
30
|
});
|
|
31
|
+
process.env.NODE_ENV = currentNodeEnv;
|
|
24
32
|
|
|
25
33
|
const builtFileName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
|
|
26
34
|
const builtFilePath = `${JS_ISLAND_PUBLIC_PATH}/${builtFileName}.js`;
|
|
@@ -29,6 +37,7 @@ async function copyPreactToPublicFolder(config: HS.Config) {
|
|
|
29
37
|
JS_IMPORT_MAP.set('preact/compat', builtFilePath);
|
|
30
38
|
JS_IMPORT_MAP.set('preact/hooks', builtFilePath);
|
|
31
39
|
JS_IMPORT_MAP.set('preact/jsx-runtime', builtFilePath);
|
|
40
|
+
JS_IMPORT_MAP.set('preact/jsx-dev-runtime', builtFilePath);
|
|
32
41
|
|
|
33
42
|
if (!JS_IMPORT_MAP.has('react')) {
|
|
34
43
|
JS_IMPORT_MAP.set('react', builtFilePath);
|
|
@@ -41,79 +50,88 @@ async function copyPreactToPublicFolder(config: HS.Config) {
|
|
|
41
50
|
*/
|
|
42
51
|
export function preactPlugin(): HS.Plugin {
|
|
43
52
|
return async (config: HS.Config) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
exportedDefault
|
|
97
|
-
exportedDefaultFn
|
|
98
|
-
exportedDefaultConst
|
|
99
|
-
exportedDefaultAny
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
53
|
+
try {
|
|
54
|
+
log('plguin loaded');
|
|
55
|
+
// Ensure Preact can be loaded on the client
|
|
56
|
+
if (!JS_IMPORT_MAP.has('preact')) {
|
|
57
|
+
await copyPreactToPublicFolder(config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Define a Bun plugin to handle .tsx files
|
|
61
|
+
await Bun.plugin({
|
|
62
|
+
name: 'Hyperspan Preact Loader',
|
|
63
|
+
async setup(build) {
|
|
64
|
+
// when a .tsx file is imported...
|
|
65
|
+
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
|
|
66
|
+
log('tsx file loaded', args.path);
|
|
67
|
+
const jsId = assetHash(args.path);
|
|
68
|
+
|
|
69
|
+
// Cache: Avoid re-processing the same file
|
|
70
|
+
if (PREACT_ISLAND_CACHE.has(jsId)) {
|
|
71
|
+
log('tsx file cached', args.path);
|
|
72
|
+
return {
|
|
73
|
+
contents: PREACT_ISLAND_CACHE.get(jsId) || '',
|
|
74
|
+
loader: 'js',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log('tsx file not cached, building...', args.path);
|
|
79
|
+
// We need to build the file to ensure we can ship it to the client with dependencies
|
|
80
|
+
// Ironic, right? Calling Bun.build() inside of a plugin that runs on Bun.build()?
|
|
81
|
+
const result = await Bun.build({
|
|
82
|
+
entrypoints: [args.path],
|
|
83
|
+
outdir: join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH),
|
|
84
|
+
naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
|
|
85
|
+
external: Array.from(JS_IMPORT_MAP.keys()),
|
|
86
|
+
minify: true,
|
|
87
|
+
format: 'esm',
|
|
88
|
+
target: 'browser',
|
|
89
|
+
env: 'APP_PUBLIC_*',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Add output file to import map
|
|
93
|
+
const esmName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
|
|
94
|
+
JS_IMPORT_MAP.set(esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
|
|
95
|
+
log('added to import map', esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
|
|
96
|
+
|
|
97
|
+
let contents = await result.outputs[0].text();
|
|
98
|
+
|
|
99
|
+
// Look for the default export
|
|
100
|
+
const RE_EXPORT_DEFAULT = /export\{([^\s]+) as default\}/;
|
|
101
|
+
const RE_EXPORT_DEFAULT_FN = /export default function\s+([^\s]+)/;
|
|
102
|
+
const RE_EXPORT_DEFAULT_CONST = /export default const\s+([^\s]+)/;
|
|
103
|
+
const RE_EXPORT_DEFAULT_ANY = /export default\s+([^\s]+)/;
|
|
104
|
+
|
|
105
|
+
const exportedDefault = contents.match(RE_EXPORT_DEFAULT);
|
|
106
|
+
const exportedDefaultFn = contents.match(RE_EXPORT_DEFAULT_FN);
|
|
107
|
+
const exportedDefaultConst = contents.match(RE_EXPORT_DEFAULT_CONST);
|
|
108
|
+
const exportedDefaultAny = contents.match(RE_EXPORT_DEFAULT_ANY);
|
|
109
|
+
|
|
110
|
+
const componentName =
|
|
111
|
+
exportedDefault?.[1] ||
|
|
112
|
+
exportedDefaultFn?.[1] ||
|
|
113
|
+
exportedDefaultConst?.[1] ||
|
|
114
|
+
exportedDefaultAny?.[1];
|
|
115
|
+
|
|
116
|
+
if (!componentName) {
|
|
117
|
+
log('ERROR: no default export found', args.path);
|
|
118
|
+
throw new Error(
|
|
119
|
+
`No default export found in ${args.path}. Did you forget to export a component?`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
log('component name', componentName);
|
|
124
|
+
|
|
125
|
+
// Add to contents so this is in the client JS as well
|
|
126
|
+
contents = `import { h as __hs_h, render as __hs_render, hydrate as __hs_hydrate } from 'preact';${contents}`;
|
|
127
|
+
|
|
128
|
+
// Some _interesting_ work at play here...
|
|
129
|
+
// We have to modify the original file contents to add an __HS_PLUGIN export that the renderIsland() function can use to render the component.
|
|
130
|
+
// A lot of this work actaully has to be done now, ahead of time, to ensure we use the same Preact instance to hydrate and render the component so there are no errors.
|
|
131
|
+
// So... we have to import the preact-render-to-string library to render the component to a string here, with simple functions to do that work and return HTML.
|
|
132
|
+
// All imports needed for this work are prefixed with __hs_ to avoid clashing with other imports in the module, as some of them may be duplicates.
|
|
133
|
+
// Finally, we need to export all of the functions that do this work in a special way so we don't change the default export or other functions in the module, so that only the Hyperspan renderIsland() function can use them.
|
|
134
|
+
const moduleCode = `// hyperspan:processed
|
|
117
135
|
import { render as __hs_renderToString } from 'preact-render-to-string';
|
|
118
136
|
|
|
119
137
|
// Original file contents
|
|
@@ -144,15 +162,21 @@ ${componentName}.__HS_ISLAND = {
|
|
|
144
162
|
}
|
|
145
163
|
`;
|
|
146
164
|
|
|
147
|
-
|
|
165
|
+
PREACT_ISLAND_CACHE.set(jsId, moduleCode);
|
|
148
166
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
return {
|
|
168
|
+
contents: moduleCode,
|
|
169
|
+
loader: 'js',
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
} catch (e) {
|
|
175
|
+
log('ERROR: plugin build error', e);
|
|
176
|
+
console.error('[Hyperspan] @hyperspan/plugin-preact build error');
|
|
177
|
+
console.error(e);
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
156
180
|
};
|
|
157
181
|
}
|
|
158
182
|
|
package/src/preact-client.ts
CHANGED