@hyperspan/framework 2.0.0-alpha.5 → 2.0.0-alpha.6
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 +1 -1
- package/src/client/js.test.ts +20 -0
- package/src/client/js.ts +48 -8
- package/src/client/manifest.ts +2 -6
- package/src/index.ts +1 -0
- package/src/layout.test.ts +12 -0
- package/src/layout.ts +10 -18
package/package.json
CHANGED
package/src/client/js.test.ts
CHANGED
|
@@ -63,6 +63,26 @@ describe('buildClientJS', () => {
|
|
|
63
63
|
expect(tag).toContain('({ mountPicker }) => mountPicker()');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test('package export specifiers resolve on disk like app logical paths', async () => {
|
|
67
|
+
const result = await buildClientJS(
|
|
68
|
+
'@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts',
|
|
69
|
+
{ type: 'iife' }
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result.publicPath).toMatch(/^\/_hs\/js\/client-[0-9a-f]{16}\.js$/);
|
|
73
|
+
expect(getClientJSEntries()[0].type).toBe('iife');
|
|
74
|
+
expect(getClientJSEntries()[0].absPath).toMatch(/hyperspan-streaming\.client\.ts$/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('type iife renders a classic script tag', async () => {
|
|
78
|
+
const result = await buildClientJS(clientFile, { type: 'iife' });
|
|
79
|
+
const tag = render(result.renderScriptTag());
|
|
80
|
+
|
|
81
|
+
expect(tag).toContain(`<script src="${result.publicPath}"`);
|
|
82
|
+
expect(tag).not.toContain('type="module"');
|
|
83
|
+
expect(getClientJSEntries()[0].type).toBe('iife');
|
|
84
|
+
});
|
|
85
|
+
|
|
66
86
|
test('logical app-relative paths hash consistently and resolve on disk when present', async () => {
|
|
67
87
|
const dir = mkdtempSync(join(tmpdir(), 'hs-client-logical-'));
|
|
68
88
|
const { mkdirSync } = await import('node:fs');
|
package/src/client/js.ts
CHANGED
|
@@ -11,6 +11,13 @@ export { registerImport, resolveImport, getImportMap } from './manifest';
|
|
|
11
11
|
export const JS_PUBLIC_PATH = '/_hs/js';
|
|
12
12
|
export const JS_ISLAND_PUBLIC_PATH = '/_hs/js/islands';
|
|
13
13
|
|
|
14
|
+
export type ClientJSType = 'module' | 'iife';
|
|
15
|
+
|
|
16
|
+
export type BuildClientJSOptions = {
|
|
17
|
+
/** `iife` emits a classic <script> (runs during HTML streaming). Default `module`. */
|
|
18
|
+
type?: ClientJSType;
|
|
19
|
+
};
|
|
20
|
+
|
|
14
21
|
export type ClientJSEntry = {
|
|
15
22
|
absPath: string;
|
|
16
23
|
assetHash: string;
|
|
@@ -18,6 +25,7 @@ export type ClientJSEntry = {
|
|
|
18
25
|
publicPath: string;
|
|
19
26
|
exports: string;
|
|
20
27
|
fnArgs: string;
|
|
28
|
+
type: ClientJSType;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
const CLIENT_JS_REGISTRY = Symbol.for('@hyperspan/client-js-entries');
|
|
@@ -89,6 +97,14 @@ export function resolveClientModulePaths(modulePathResolved: string): {
|
|
|
89
97
|
return { hashKey, absPath: candidate };
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
try {
|
|
101
|
+
const url = import.meta.resolve(resolved);
|
|
102
|
+
if (typeof url === 'string' && url.startsWith('file://')) {
|
|
103
|
+
return { hashKey, absPath: fileURLToPath(url) };
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Bare specifiers that aren't installed yet still hash stably.
|
|
107
|
+
}
|
|
92
108
|
return { hashKey, absPath: hashKey };
|
|
93
109
|
}
|
|
94
110
|
|
|
@@ -111,13 +127,11 @@ function registerClientJSEntry(entry: ClientJSEntry): void {
|
|
|
111
127
|
getClientJSRegistry().set(entry.esmName, entry);
|
|
112
128
|
}
|
|
113
129
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*/
|
|
120
|
-
export async function buildClientJS(modulePathResolved: string): Promise<HS.ClientJSBuildResult> {
|
|
130
|
+
function registerClientJS(
|
|
131
|
+
modulePathResolved: string,
|
|
132
|
+
options: BuildClientJSOptions = {}
|
|
133
|
+
): HS.ClientJSBuildResult {
|
|
134
|
+
const type = options.type ?? 'module';
|
|
121
135
|
const { hashKey, absPath } = resolveClientModulePaths(modulePathResolved);
|
|
122
136
|
const hash = assetHashFn(hashKey);
|
|
123
137
|
const esmName = `client-${hash}`;
|
|
@@ -133,7 +147,7 @@ export async function buildClientJS(modulePathResolved: string): Promise<HS.Clie
|
|
|
133
147
|
fnArgs = discovered.fnArgs;
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
registerClientJSEntry({ absPath, assetHash: hash, esmName, publicPath, exports, fnArgs });
|
|
150
|
+
registerClientJSEntry({ absPath, assetHash: hash, esmName, publicPath, exports, fnArgs, type });
|
|
137
151
|
registerImport(esmName, publicPath);
|
|
138
152
|
|
|
139
153
|
return {
|
|
@@ -141,6 +155,10 @@ export async function buildClientJS(modulePathResolved: string): Promise<HS.Clie
|
|
|
141
155
|
esmName,
|
|
142
156
|
publicPath,
|
|
143
157
|
renderScriptTag: (loadScript) => {
|
|
158
|
+
if (type === 'iife') {
|
|
159
|
+
return html`<script src="${publicPath}"></script>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
const t = typeof loadScript;
|
|
145
163
|
|
|
146
164
|
if (t === 'string') {
|
|
@@ -169,6 +187,28 @@ export async function buildClientJS(modulePathResolved: string): Promise<HS.Clie
|
|
|
169
187
|
};
|
|
170
188
|
}
|
|
171
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Build (or look up) a client JS module and return a helper for rendering script tags.
|
|
192
|
+
*
|
|
193
|
+
* Never evaluates the module on the server — only registers the path for Vite to bundle
|
|
194
|
+
* and returns URLs / script-tag helpers for the browser.
|
|
195
|
+
*/
|
|
196
|
+
export async function buildClientJS(
|
|
197
|
+
modulePathResolved: string,
|
|
198
|
+
options: BuildClientJSOptions = {}
|
|
199
|
+
): Promise<HS.ClientJSBuildResult> {
|
|
200
|
+
return registerClientJS(modulePathResolved, options);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Same `buildClientJS` path an app would use for a package export. */
|
|
204
|
+
export const streamingClient = registerClientJS(
|
|
205
|
+
'@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts',
|
|
206
|
+
{ type: 'iife' }
|
|
207
|
+
);
|
|
208
|
+
export const actionsClient = registerClientJS(
|
|
209
|
+
'@hyperspan/framework/client/_hs/hyperspan-actions.client.ts'
|
|
210
|
+
);
|
|
211
|
+
|
|
172
212
|
/**
|
|
173
213
|
* Discover export names from source or bundled client JS.
|
|
174
214
|
* Prefers bundled `export{...}` form, then common source export patterns.
|
package/src/client/manifest.ts
CHANGED
|
@@ -3,11 +3,7 @@ import type { Hyperspan as HS } from '../types';
|
|
|
3
3
|
export type AssetManifest = {
|
|
4
4
|
imports: Record<string, string>;
|
|
5
5
|
css: Record<string, string[]>;
|
|
6
|
-
clients:
|
|
7
|
-
streaming?: string;
|
|
8
|
-
actions?: string;
|
|
9
|
-
scripts?: string;
|
|
10
|
-
};
|
|
6
|
+
clients: Record<string, string>;
|
|
11
7
|
};
|
|
12
8
|
|
|
13
9
|
let _manifest: AssetManifest = {
|
|
@@ -67,7 +63,7 @@ export function getClientJSFromManifest(
|
|
|
67
63
|
moduleId: string,
|
|
68
64
|
_exportNames = 'default'
|
|
69
65
|
): ClientJSBuildResult {
|
|
70
|
-
const publicPath = _manifest.clients[moduleId
|
|
66
|
+
const publicPath = _manifest.clients[moduleId];
|
|
71
67
|
if (!publicPath) {
|
|
72
68
|
throw new Error(
|
|
73
69
|
`[Hyperspan] Client module "${moduleId}" not found in asset manifest. Run hyperspan build or start dev server.`
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
registerImport,
|
|
21
21
|
} from './client/manifest';
|
|
22
22
|
export { buildClientJS } from './client/js';
|
|
23
|
+
export type { BuildClientJSOptions, ClientJSType } from './client/js';
|
|
23
24
|
export { registerRouteModule, registerRouteModules } from './register-routes';
|
|
24
25
|
export type { RouteModuleEntry } from './register-routes';
|
|
25
26
|
export type { AssetManifest } from './client/manifest';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render } from '@hyperspan/html';
|
|
3
|
+
import { hyperspanScriptTags } from './layout';
|
|
4
|
+
import { streamingClient } from './client/js';
|
|
5
|
+
|
|
6
|
+
test('streaming bootstrap injects a classic script so chunks swap in during parse', () => {
|
|
7
|
+
const html = render(hyperspanScriptTags());
|
|
8
|
+
expect(html).toContain("document.createElement('script')");
|
|
9
|
+
expect(html).toContain(`script.src = '${streamingClient.publicPath}'`);
|
|
10
|
+
expect(html).not.toContain("type = 'module'");
|
|
11
|
+
expect(html).not.toContain(`import('${streamingClient.publicPath}')`);
|
|
12
|
+
});
|
package/src/layout.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
import { html } from '@hyperspan/html';
|
|
2
2
|
import { CSS_PUBLIC_PATH } from './client/css';
|
|
3
|
-
import { getImportMap, getRouteCss
|
|
3
|
+
import { getImportMap, getRouteCss } from './client/manifest';
|
|
4
|
+
import { actionsClient, streamingClient } from './client/js';
|
|
4
5
|
import type { Hyperspan as HS } from './types';
|
|
5
6
|
|
|
6
|
-
function streamingClientPath(): string {
|
|
7
|
-
return getClientJSFromManifest('streaming').publicPath;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
7
|
/**
|
|
11
|
-
* Output the importmap for the client so we can use ESModules on the client to load JS files on demand
|
|
8
|
+
* Output the importmap for the client so we can use ESModules on the client to load JS files on demand.
|
|
9
|
+
*
|
|
10
|
+
* The streaming client MUST be loaded as a classic <script>, not type=module.
|
|
11
|
+
* Browsers defer all ESM until the document finishes parsing, which would hold
|
|
12
|
+
* every streaming chunk until the whole page is done. A dynamically inserted
|
|
13
|
+
* classic script runs as soon as it downloads.
|
|
12
14
|
*/
|
|
13
15
|
export function hyperspanScriptTags() {
|
|
14
16
|
const imports = getImportMap();
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
streamingPath = streamingClientPath();
|
|
18
|
-
} catch {
|
|
19
|
-
streamingPath = '/_hs/js/hyperspan-streaming.client.js';
|
|
20
|
-
}
|
|
17
|
+
const streamingPath = streamingClient.publicPath;
|
|
21
18
|
|
|
22
19
|
return html`
|
|
23
20
|
<script type="importmap">
|
|
@@ -65,10 +62,5 @@ export function hyperspanStyleTags(context: HS.Context) {
|
|
|
65
62
|
* Render the actions client script tag (used by createAction).
|
|
66
63
|
*/
|
|
67
64
|
export function hyperspanActionsScriptTag() {
|
|
68
|
-
|
|
69
|
-
const actions = getClientJSFromManifest('actions');
|
|
70
|
-
return html`<script type="module" src="${actions.publicPath}"></script>`;
|
|
71
|
-
} catch {
|
|
72
|
-
return html`<script type="module" src="/_hs/js/hyperspan-actions.client.js"></script>`;
|
|
73
|
-
}
|
|
65
|
+
return html`<script type="module" src="${actionsClient.publicPath}"></script>`;
|
|
74
66
|
}
|