@hyperspan/plugin-preact 1.0.1 → 1.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/plugin-preact",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Hyperspan Plugin for Preact",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -11,7 +11,7 @@
11
11
  "author": "Vance Lucas <vance@vancelucas.com>",
12
12
  "license": "BSD-3-Clause",
13
13
  "keywords": [],
14
- "homepage": "https://www.hyperspan.dev/docs/islands-architecture",
14
+ "homepage": "https://www.hyperspan.dev/docs/clientjs/islands",
15
15
  "bugs": {
16
16
  "url": "https://github.com/vlucas/hyperspan/issues"
17
17
  },
@@ -32,6 +32,7 @@
32
32
  "@hyperspan/framework": "^1.0.1"
33
33
  },
34
34
  "dependencies": {
35
+ "debug": "^4.4.3",
35
36
  "preact": "^10.28.3",
36
37
  "preact-render-to-string": "^6.6.5"
37
38
  }
@@ -0,0 +1,190 @@
1
+ import { test, describe, expect } from 'bun:test';
2
+ import { h } from 'preact';
3
+ import { buildIslandHtml, renderPreactSSR, renderPreactIsland } from './index';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Simple Preact components defined inline — no .tsx compilation needed
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function Hello({ name = 'World', count = 0 }: { name?: string; count?: number }) {
10
+ return h('div', { class: 'hello' }, [
11
+ h('h1', null, `Hello ${name}!`),
12
+ h('p', null, `Count: ${count}`),
13
+ ]);
14
+ }
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // buildIslandHtml
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe('buildIslandHtml', () => {
21
+ const jsId = 'abc123';
22
+ const componentName = 'Hello';
23
+ const esmName = 'hello-component';
24
+
25
+ test('wraps SSR content in a div with the island id', () => {
26
+ const result = buildIslandHtml(jsId, componentName, esmName, '', '<p>SSR</p>');
27
+ expect(result).toContain(`<div id="${jsId}">`);
28
+ expect(result).toContain('<p>SSR</p>');
29
+ });
30
+
31
+ test('includes a module script tag with the correct source id', () => {
32
+ const result = buildIslandHtml(jsId, componentName, esmName, 'console.log(1)', '');
33
+ expect(result).toContain(`<script type="module" id="${jsId}_script" data-source-id="${jsId}">`);
34
+ expect(result).toContain(`import ${componentName} from "${esmName}"`);
35
+ expect(result).toContain('console.log(1)');
36
+ });
37
+
38
+ test('lazy loading wraps script in a template inside a hidden div', () => {
39
+ const result = buildIslandHtml(jsId, componentName, esmName, '', '<p>SSR</p>', {
40
+ loading: 'lazy',
41
+ });
42
+ expect(result).toContain('data-loading="lazy"');
43
+ expect(result).toContain('<template>');
44
+ expect(result).toContain('<p>SSR</p>');
45
+ });
46
+
47
+ test('eager loading (default) does not include a template tag', () => {
48
+ const result = buildIslandHtml(jsId, componentName, esmName, '', '<p>SSR</p>');
49
+ expect(result).not.toContain('data-loading="lazy"');
50
+ expect(result).not.toContain('<template>');
51
+ });
52
+ });
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // renderPreactSSR
56
+ // ---------------------------------------------------------------------------
57
+
58
+ describe('renderPreactSSR', () => {
59
+ test('renders a component to an HTML string', () => {
60
+ const output = renderPreactSSR(Hello, { name: 'World' });
61
+ expect(output).toContain('<div');
62
+ expect(output).toContain('Hello World!');
63
+ });
64
+
65
+ test('passes props to the component', () => {
66
+ const output = renderPreactSSR(Hello, { name: 'Alice', count: 42 });
67
+ expect(output).toContain('Hello Alice!');
68
+ expect(output).toContain('Count: 42');
69
+ });
70
+
71
+ test('uses component defaults when props are omitted', () => {
72
+ const output = renderPreactSSR(Hello, {});
73
+ expect(output).toContain('Hello World!');
74
+ expect(output).toContain('Count: 0');
75
+ });
76
+
77
+ test('returns a string', () => {
78
+ const output = renderPreactSSR(Hello, {});
79
+ expect(typeof output).toBe('string');
80
+ });
81
+
82
+ test('escapes dangerous characters in prop values', () => {
83
+ const output = renderPreactSSR(Hello, { name: '<script>alert(1)</script>' });
84
+ expect(output).not.toContain('<script>alert');
85
+ });
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // renderPreactIsland
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe('renderPreactIsland', () => {
93
+ test('throws when component has no __HS_ISLAND property', () => {
94
+ expect(() => renderPreactIsland(Hello, {})).toThrow(
95
+ 'was not loaded with an island plugin'
96
+ );
97
+ });
98
+
99
+ test('returns an html_safe object', () => {
100
+ const jsId = 'island-test-1';
101
+ const esmName = 'hello-ssr';
102
+ (Hello as any).__HS_ISLAND = {
103
+ id: jsId,
104
+ render: (props: any, options: any = {}) => {
105
+ const ssrContent = renderPreactSSR(Hello, props);
106
+ const jsContent = `import { h as __hs_h, hydrate as __hs_hydrate } from 'preact';__hs_hydrate(__hs_h(Hello, ${JSON.stringify(props)}), document.getElementById("${jsId}"));`;
107
+ return buildIslandHtml(jsId, 'Hello', esmName, jsContent, ssrContent, options);
108
+ },
109
+ };
110
+
111
+ const result = renderPreactIsland(Hello, { name: 'World' });
112
+ expect(result).toHaveProperty('_kind', 'html_safe');
113
+ });
114
+
115
+ test('island output contains SSR-rendered HTML', () => {
116
+ const jsId = 'island-test-2';
117
+ const esmName = 'hello-ssr';
118
+ (Hello as any).__HS_ISLAND = {
119
+ id: jsId,
120
+ render: (props: any, options: any = {}) => {
121
+ const ssrContent = renderPreactSSR(Hello, props);
122
+ const jsContent = '';
123
+ return buildIslandHtml(jsId, 'Hello', esmName, jsContent, ssrContent, options);
124
+ },
125
+ };
126
+
127
+ const result = renderPreactIsland(Hello, { name: 'Alice', count: 7 });
128
+ expect(result.content).toContain('Hello Alice!');
129
+ expect(result.content).toContain('Count: 7');
130
+ });
131
+
132
+ test('island output contains the wrapper div with the correct id', () => {
133
+ const jsId = 'island-test-3';
134
+ (Hello as any).__HS_ISLAND = {
135
+ id: jsId,
136
+ render: (props: any, options: any = {}) =>
137
+ buildIslandHtml(jsId, 'Hello', 'hello', '', renderPreactSSR(Hello, props), options),
138
+ };
139
+
140
+ const result = renderPreactIsland(Hello, { name: 'Bob' });
141
+ expect(result.content).toContain(`<div id="${jsId}">`);
142
+ });
143
+
144
+ test('island output contains a hydration script tag', () => {
145
+ const jsId = 'island-test-4';
146
+ (Hello as any).__HS_ISLAND = {
147
+ id: jsId,
148
+ render: (props: any, options: any = {}) => {
149
+ const ssrContent = renderPreactSSR(Hello, props);
150
+ const jsContent = `import { h as __hs_h, hydrate as __hs_hydrate } from 'preact';__hs_hydrate(__hs_h(Hello, ${JSON.stringify(props)}), document.getElementById("${jsId}"));`;
151
+ return buildIslandHtml(jsId, 'Hello', 'hello', jsContent, ssrContent, options);
152
+ },
153
+ };
154
+
155
+ const result = renderPreactIsland(Hello, { name: 'World' });
156
+ expect(result.content).toContain('<script type="module"');
157
+ expect(result.content).toContain('hydrate');
158
+ });
159
+
160
+ test('ssr: false produces no SSR content', () => {
161
+ const jsId = 'island-test-5';
162
+ (Hello as any).__HS_ISLAND = {
163
+ id: jsId,
164
+ render: (props: any, options: any = {}) => {
165
+ if (options.ssr === false) {
166
+ return buildIslandHtml(jsId, 'Hello', 'hello', 'console.log("mount")', '', options);
167
+ }
168
+ return buildIslandHtml(jsId, 'Hello', 'hello', '', renderPreactSSR(Hello, props), options);
169
+ },
170
+ };
171
+
172
+ const result = renderPreactIsland(Hello, { name: 'World' }, { ssr: false, loading: undefined });
173
+ expect(result.content).not.toContain('Hello World!');
174
+ expect(result.content).toContain(`<div id="${jsId}"></div>`);
175
+ });
176
+
177
+ test('loading: lazy wraps the script in a template', () => {
178
+ const jsId = 'island-test-6';
179
+ (Hello as any).__HS_ISLAND = {
180
+ id: jsId,
181
+ render: (props: any, options: any = {}) =>
182
+ buildIslandHtml(jsId, 'Hello', 'hello', 'console.log(1)', renderPreactSSR(Hello, props), options),
183
+ };
184
+
185
+ const result = renderPreactIsland(Hello, { name: 'World' }, { ssr: true, loading: 'lazy' } as any);
186
+ expect(result.content).toContain('data-loading="lazy"');
187
+ expect(result.content).toContain('<template>');
188
+ expect(result.content).toContain('Hello World!');
189
+ });
190
+ });
package/src/index.ts CHANGED
@@ -4,31 +4,96 @@ 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 { h } from 'preact';
8
+ import { render as preactRenderToString } from 'preact-render-to-string';
9
+ import debug from 'debug';
10
+
11
+ const log = debug('hyperspan:plugin-preact');
12
+
13
+ /** Dev: stable `[name].js` via Bun default. Prod: hashed filenames for caching. */
14
+ const ISLAND_JS_NAMING = IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined;
15
+
16
+ function islandBundleBaseName(outputPath: string): string {
17
+ return String(outputPath.split('/').reverse()[0]!.replace(/\.js$/i, ''));
18
+ }
19
+
20
+ function pickEntryPointJsOutput(
21
+ outputs: ReadonlyArray<{ path: string; kind?: string }>,
22
+ entrySourcePath: string
23
+ ): { path: string } {
24
+ const js = outputs.filter((o) => o.path.endsWith('.js'));
25
+ const entry = js.find((o) => o.kind === 'entry-point');
26
+ if (entry) return entry;
27
+ const sourceBase = entrySourcePath.split('/').pop()!.replace(/\.(tsx|ts|jsx|js)$/i, '');
28
+ const byName = js.find((o) => {
29
+ const b = islandBundleBaseName(o.path);
30
+ return b === sourceBase || b.startsWith(`${sourceBase}-`);
31
+ });
32
+ if (byName) return byName;
33
+ if (js[0]) return js[0];
34
+ throw new Error('[Hyperspan] Preact island build produced no JS output');
35
+ }
36
+
37
+ /**
38
+ * Build the island wrapper HTML: a div for SSR content + a module script tag for client hydration.
39
+ * Exported so it can be imported by generated island module code and used directly in tests.
40
+ */
41
+ export function buildIslandHtml(
42
+ jsId: string,
43
+ componentName: string,
44
+ esmName: string,
45
+ jsContent: string,
46
+ ssrContent: string,
47
+ options: { loading?: string } = {}
48
+ ): string {
49
+ const scriptTag = `<script type="module" id="${jsId}_script" data-source-id="${jsId}">import ${componentName} from "${esmName}";${jsContent}</script>`;
50
+ if (options.loading === 'lazy') {
51
+ return `<div id="${jsId}">${ssrContent}</div><div data-loading="lazy" style="height:1px;width:1px;overflow:hidden;"><template>\n${scriptTag}</template></div>`;
52
+ }
53
+ return `<div id="${jsId}">${ssrContent}</div>\n${scriptTag}`;
54
+ }
55
+
56
+ /**
57
+ * Render a Preact component to an HTML string (SSR).
58
+ * Exported for direct use in tests and external tooling.
59
+ */
60
+ export function renderPreactSSR(Component: any, props: any = {}): string {
61
+ return preactRenderToString(h(Component, props));
62
+ }
7
63
 
8
64
  // External ESM = https://esm.sh/preact@10.26.4/compat
9
- const PREACT_ISLAND_CACHE = new Map<string, string>();
65
+ type PreactIslandCacheEntry = { contents: string; esmName: string };
66
+
67
+ const PREACT_ISLAND_CACHE = new Map<string, PreactIslandCacheEntry>();
10
68
 
11
69
  /**
12
70
  * Build Preact client JS and copy to public folder
13
71
  */
14
72
  async function copyPreactToPublicFolder(config: HS.Config) {
73
+ const currentNodeEnv = process.env.NODE_ENV || 'production';
15
74
  const sourceFile = resolve(__dirname, './preact-client.ts');
75
+
76
+ // Preact client JS is always production mode
77
+ process.env.NODE_ENV = 'production';
16
78
  const result = await Bun.build({
17
79
  entrypoints: [sourceFile],
18
80
  outdir: join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH),
19
- naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
81
+ naming: ISLAND_JS_NAMING,
20
82
  minify: true,
21
83
  format: 'esm',
22
84
  target: 'browser',
23
85
  });
86
+ process.env.NODE_ENV = currentNodeEnv;
24
87
 
25
- const builtFileName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
88
+ const preactEntry = pickEntryPointJsOutput(result.outputs, sourceFile);
89
+ const builtFileName = islandBundleBaseName(preactEntry.path);
26
90
  const builtFilePath = `${JS_ISLAND_PUBLIC_PATH}/${builtFileName}.js`;
27
91
 
28
92
  JS_IMPORT_MAP.set('preact', builtFilePath);
29
93
  JS_IMPORT_MAP.set('preact/compat', builtFilePath);
30
94
  JS_IMPORT_MAP.set('preact/hooks', builtFilePath);
31
95
  JS_IMPORT_MAP.set('preact/jsx-runtime', builtFilePath);
96
+ JS_IMPORT_MAP.set('preact/jsx-dev-runtime', builtFilePath);
32
97
 
33
98
  if (!JS_IMPORT_MAP.has('react')) {
34
99
  JS_IMPORT_MAP.set('react', builtFilePath);
@@ -41,92 +106,103 @@ async function copyPreactToPublicFolder(config: HS.Config) {
41
106
  */
42
107
  export function preactPlugin(): HS.Plugin {
43
108
  return async (config: HS.Config) => {
44
- // Ensure Preact can be loaded on the client
45
- if (!JS_IMPORT_MAP.has('preact')) {
46
- await copyPreactToPublicFolder(config);
47
- }
109
+ try {
110
+ log('plugin loaded');
111
+ // Ensure Preact can be loaded on the client
112
+ if (!JS_IMPORT_MAP.has('preact')) {
113
+ await copyPreactToPublicFolder(config);
114
+ }
48
115
 
49
- // Define a Bun plugin to handle .tsx files
50
- await Bun.plugin({
51
- name: 'Hyperspan Preact Loader',
52
- async setup(build) {
53
- // when a .tsx file is imported...
54
- build.onLoad({ filter: /\.tsx$/ }, async (args) => {
55
- const jsId = assetHash(args.path);
116
+ // Define a Bun plugin to handle .tsx files
117
+ await Bun.plugin({
118
+ name: 'Hyperspan Preact Loader',
119
+ async setup(build) {
120
+ // when a .tsx file is imported...
121
+ build.onLoad({ filter: /\.tsx$/ }, async (args) => {
122
+ log('tsx file loaded', args.path);
123
+ const jsId = assetHash(args.path);
56
124
 
57
- // Cache: Avoid re-processing the same file
58
- if (PREACT_ISLAND_CACHE.has(jsId)) {
59
- return {
60
- contents: PREACT_ISLAND_CACHE.get(jsId) || '',
61
- loader: 'js',
62
- };
63
- }
64
-
65
- // We need to build the file to ensure we can ship it to the client with dependencies
66
- // Ironic, right? Calling Bun.build() inside of a plugin that runs on Bun.build()?
67
- const result = await Bun.build({
68
- entrypoints: [args.path],
69
- outdir: join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH),
70
- naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
71
- external: Array.from(JS_IMPORT_MAP.keys()),
72
- minify: true,
73
- format: 'esm',
74
- target: 'browser',
75
- env: 'APP_PUBLIC_*',
76
- });
125
+ if (!JS_IMPORT_MAP.has('preact')) {
126
+ await copyPreactToPublicFolder(config);
127
+ }
128
+
129
+ // Cache: Avoid re-processing the same file
130
+ if (PREACT_ISLAND_CACHE.has(jsId)) {
131
+ const hit = PREACT_ISLAND_CACHE.get(jsId)!;
132
+ JS_IMPORT_MAP.set(hit.esmName, `${JS_ISLAND_PUBLIC_PATH}/${hit.esmName}.js`);
133
+ log('tsx file cached', args.path);
134
+ return {
135
+ contents: hit.contents,
136
+ loader: 'js',
137
+ };
138
+ }
139
+
140
+ log('tsx file not cached, building...', args.path);
141
+ // We need to build the file to ensure we can ship it to the client with dependencies
142
+ // Ironic, right? Calling Bun.build() inside of a plugin that runs on Bun.build()?
143
+ const result = await Bun.build({
144
+ entrypoints: [args.path],
145
+ outdir: join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH),
146
+ naming: ISLAND_JS_NAMING,
147
+ external: Array.from(JS_IMPORT_MAP.keys()),
148
+ minify: true,
149
+ format: 'esm',
150
+ target: 'browser',
151
+ env: 'APP_PUBLIC_*',
152
+ });
153
+
154
+ const entryOut = pickEntryPointJsOutput(result.outputs, args.path);
155
+ const esmName = islandBundleBaseName(entryOut.path);
156
+
157
+ // Add output file to import map
158
+ JS_IMPORT_MAP.set(esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
159
+ log('added to import map', esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
160
+
161
+ let contents = await Bun.file(entryOut.path).text();
162
+
163
+ // Look for the default export
164
+ const RE_EXPORT_DEFAULT = /export\{([^\s]+) as default\}/;
165
+ const RE_EXPORT_DEFAULT_FN = /export default function\s+([^\s]+)/;
166
+ const RE_EXPORT_DEFAULT_CONST = /export default const\s+([^\s]+)/;
167
+ const RE_EXPORT_DEFAULT_ANY = /export default\s+([^\s]+)/;
168
+
169
+ const exportedDefault = contents.match(RE_EXPORT_DEFAULT);
170
+ const exportedDefaultFn = contents.match(RE_EXPORT_DEFAULT_FN);
171
+ const exportedDefaultConst = contents.match(RE_EXPORT_DEFAULT_CONST);
172
+ const exportedDefaultAny = contents.match(RE_EXPORT_DEFAULT_ANY);
173
+
174
+ const componentName =
175
+ exportedDefault?.[1] ||
176
+ exportedDefaultFn?.[1] ||
177
+ exportedDefaultConst?.[1] ||
178
+ exportedDefaultAny?.[1];
77
179
 
78
- // Add output file to import map
79
- const esmName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
80
- JS_IMPORT_MAP.set(esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
81
-
82
- let contents = await result.outputs[0].text();
83
-
84
- // Look for the default export
85
- const RE_EXPORT_DEFAULT = /export\{([^\s]+) as default\}/;
86
- const RE_EXPORT_DEFAULT_FN = /export default function\s+([^\s]+)/;
87
- const RE_EXPORT_DEFAULT_CONST = /export default const\s+([^\s]+)/;
88
- const RE_EXPORT_DEFAULT_ANY = /export default\s+([^\s]+)/;
89
-
90
- const exportedDefault = contents.match(RE_EXPORT_DEFAULT);
91
- const exportedDefaultFn = contents.match(RE_EXPORT_DEFAULT_FN);
92
- const exportedDefaultConst = contents.match(RE_EXPORT_DEFAULT_CONST);
93
- const exportedDefaultAny = contents.match(RE_EXPORT_DEFAULT_ANY);
94
-
95
- const componentName =
96
- exportedDefault?.[1] ||
97
- exportedDefaultFn?.[1] ||
98
- exportedDefaultConst?.[1] ||
99
- exportedDefaultAny?.[1];
100
-
101
- if (!componentName) {
102
- throw new Error(
103
- `No default export found in ${args.path}. Did you forget to export a component?`
104
- );
105
- }
106
-
107
- // Add to contents so this is in the client JS as well
108
- contents = `import { h as __hs_h, render as __hs_render, hydrate as __hs_hydrate } from 'preact';${contents}`;
109
-
110
- // Some _interesting_ work at play here...
111
- // We have to modify the original file contents to add an __HS_PLUGIN export that the renderIsland() function can use to render the component.
112
- // 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.
113
- // 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.
114
- // 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.
115
- // 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.
116
- const moduleCode = `// hyperspan:processed
180
+ if (!componentName) {
181
+ log('ERROR: no default export found', args.path);
182
+ throw new Error(
183
+ `No default export found in ${args.path}. Did you forget to export a component?`
184
+ );
185
+ }
186
+
187
+ // Add to contents so this is in the client JS as well
188
+ contents = `import { h as __hs_h, render as __hs_render, hydrate as __hs_hydrate } from 'preact';${contents}`;
189
+
190
+ // Some _interesting_ work at play here...
191
+ // We have to modify the original file contents to add an __HS_PLUGIN export that the renderIsland() function can use to render the component.
192
+ // 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.
193
+ // 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.
194
+ // 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.
195
+ // 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.
196
+ const moduleCode = `// hyperspan:processed
117
197
  import { render as __hs_renderToString } from 'preact-render-to-string';
198
+ import { buildIslandHtml as __hs_buildIslandHtml } from '@hyperspan/plugin-preact';
118
199
 
119
200
  // Original file contents
120
201
  ${contents}
121
202
 
122
203
  // hyperspan:preact-plugin
123
204
  function __hs_renderIsland(jsContent = '', ssrContent = '', options = {}) {
124
- const scriptTag = \`<script type="module" id="${jsId}_script" data-source-id="${jsId}">import ${componentName} from "${esmName}";\${jsContent}</script>\`;
125
- if (options.loading === 'lazy') {
126
- return \`<div id="${jsId}">\${ssrContent}</div><div data-loading="lazy" style="height:1px;width:1px;overflow:hidden;"><template>\n\${scriptTag}</template></div>\`;
127
- }
128
-
129
- return \`<div id="${jsId}">\${ssrContent}</div>\n\${scriptTag}\`;
205
+ return __hs_buildIslandHtml("${jsId}", "${componentName}", "${esmName}", jsContent, ssrContent, options);
130
206
  }
131
207
  ${componentName}.__HS_ISLAND = {
132
208
  id: "${jsId}",
@@ -144,15 +220,21 @@ ${componentName}.__HS_ISLAND = {
144
220
  }
145
221
  `;
146
222
 
147
- PREACT_ISLAND_CACHE.set(jsId, moduleCode);
223
+ PREACT_ISLAND_CACHE.set(jsId, { contents: moduleCode, esmName });
148
224
 
149
- return {
150
- contents: moduleCode,
151
- loader: 'js',
152
- };
153
- });
154
- },
155
- });
225
+ return {
226
+ contents: moduleCode,
227
+ loader: 'js',
228
+ };
229
+ });
230
+ },
231
+ });
232
+ } catch (e) {
233
+ log('ERROR: plugin build error', e);
234
+ console.error('[Hyperspan] @hyperspan/plugin-preact build error');
235
+ console.error(e);
236
+ throw e;
237
+ }
156
238
  };
157
239
  }
158
240