@hyperspan/framework 2.0.0-alpha.4 → 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/adapter.test.ts +23 -0
- 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 +10 -1
- package/src/layout.test.ts +12 -0
- package/src/layout.ts +10 -18
- package/src/server.ts +3 -1
- package/src/types.ts +45 -6
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { Adapter } from './types';
|
|
3
|
+
import { createConfig } from './server';
|
|
4
|
+
|
|
5
|
+
const cloudflare: Adapter = {
|
|
6
|
+
name: 'cloudflare',
|
|
7
|
+
createEntry() {
|
|
8
|
+
return {
|
|
9
|
+
fetch: async () => new Response('ok'),
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('createConfig deployAdapter', () => {
|
|
15
|
+
test('defaults to Node when omitted or undefined', () => {
|
|
16
|
+
expect(createConfig({}).deployAdapter.name).toBe('node');
|
|
17
|
+
expect(createConfig({ deployAdapter: undefined }).deployAdapter.name).toBe('node');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('keeps an explicit adapter', () => {
|
|
21
|
+
expect(createConfig({ deployAdapter: cloudflare }).deployAdapter).toBe(cloudflare);
|
|
22
|
+
});
|
|
23
|
+
});
|
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,9 +20,18 @@ 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';
|
|
26
27
|
export { renderIsland } from './island';
|
|
27
28
|
export type { IslandRenderOptions } from './island';
|
|
28
|
-
export type {
|
|
29
|
+
export type {
|
|
30
|
+
Hyperspan,
|
|
31
|
+
ServerCreateContext,
|
|
32
|
+
DeployAdapter,
|
|
33
|
+
Adapter,
|
|
34
|
+
AdapterAfterBuildContext,
|
|
35
|
+
DeployEntry,
|
|
36
|
+
DeployEntryContext,
|
|
37
|
+
} from './types';
|
|
@@ -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
|
}
|
package/src/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { isbot } from 'isbot';
|
|
|
12
12
|
import { executeMiddleware } from './middleware';
|
|
13
13
|
import { buildUrl, parsePath, removeUndefined } from './utils';
|
|
14
14
|
import { Cookies } from './cookies';
|
|
15
|
+
import { nodeAdapter } from '@hyperspan/adapter-node';
|
|
15
16
|
|
|
16
17
|
import type { Hyperspan as HS } from './types';
|
|
17
18
|
|
|
@@ -51,7 +52,7 @@ export function createConfig(config: Partial<HS.Config> = {}): HS.Config {
|
|
|
51
52
|
appDir: './app',
|
|
52
53
|
publicDir: './public',
|
|
53
54
|
plugins: [],
|
|
54
|
-
|
|
55
|
+
deployAdapter: config.deployAdapter ?? nodeAdapter(),
|
|
55
56
|
responseOptions: {
|
|
56
57
|
disableStreaming: hyperspanDisableStreaming,
|
|
57
58
|
},
|
|
@@ -59,6 +60,7 @@ export function createConfig(config: Partial<HS.Config> = {}): HS.Config {
|
|
|
59
60
|
return {
|
|
60
61
|
...defaultConfig,
|
|
61
62
|
...config,
|
|
63
|
+
deployAdapter: config.deployAdapter ?? defaultConfig.deployAdapter,
|
|
62
64
|
responseOptions: {
|
|
63
65
|
...defaultConfig.responseOptions,
|
|
64
66
|
...config.responseOptions,
|
package/src/types.ts
CHANGED
|
@@ -63,7 +63,38 @@ export namespace Hyperspan {
|
|
|
63
63
|
disableStreaming?: DisableStreamingFn;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
export type
|
|
66
|
+
export type AdapterAfterBuildContext = {
|
|
67
|
+
root: string;
|
|
68
|
+
appDir: string;
|
|
69
|
+
outDir: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type DeployEntryContext = {
|
|
73
|
+
createHyperspanServer: () => Promise<Server>;
|
|
74
|
+
config: Config;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type DeployEntry = {
|
|
78
|
+
start?(options?: { port?: number }): unknown | Promise<unknown>;
|
|
79
|
+
fetch?(request: Request, env?: unknown, ctx?: unknown): Response | Promise<Response>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type Adapter = {
|
|
83
|
+
/** Diagnostic name, e.g. `node`, `cloudflare`. */
|
|
84
|
+
name: string;
|
|
85
|
+
/**
|
|
86
|
+
* Native ESM specifier exporting `resolveDevEnv(root)`.
|
|
87
|
+
* Imported by the Vite plugin so platform SDKs like wrangler load correctly.
|
|
88
|
+
*/
|
|
89
|
+
devModule?: string;
|
|
90
|
+
resolveDevEnv?(root: string): Promise<unknown>;
|
|
91
|
+
/** Platform entry (`start` and/or `fetch`). Called from generated `dist/server.ts`. */
|
|
92
|
+
createEntry(ctx: DeployEntryContext): DeployEntry;
|
|
93
|
+
afterBuild?(ctx: AdapterAfterBuildContext): void | Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Runtime adapter object (`nodeAdapter()`, `cloudflareAdapter()`, …). */
|
|
97
|
+
export type DeployAdapter = Adapter;
|
|
67
98
|
|
|
68
99
|
export type ServerCreateContext = {
|
|
69
100
|
env: unknown;
|
|
@@ -72,12 +103,16 @@ export namespace Hyperspan {
|
|
|
72
103
|
export type Config = {
|
|
73
104
|
appDir: string;
|
|
74
105
|
publicDir: string;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
/** Island plugins (`preactPlugin()`, `sveltePlugin()`, …). */
|
|
107
|
+
plugins: Array<Hyperspan.Plugin>;
|
|
108
|
+
/**
|
|
109
|
+
* Deployment adapter. Pass `cloudflareAdapter()` or `bunAdapter()`.
|
|
110
|
+
* Omit (or pass `nodeAdapter()`) to deploy to Node.
|
|
111
|
+
*/
|
|
112
|
+
deployAdapter: Adapter;
|
|
78
113
|
/**
|
|
79
114
|
* Called before the server is created, with platform bindings.
|
|
80
|
-
* - Vite/`hyperspan dev`: adapter
|
|
115
|
+
* - Vite/`hyperspan dev`: adapter `devModule` `resolveDevEnv(root)` when present; else `process.env`
|
|
81
116
|
* - Node/Bun production entry: `process.env`
|
|
82
117
|
* - Cloudflare Workers entry: Worker `env` bindings
|
|
83
118
|
*/
|
|
@@ -402,8 +437,12 @@ export namespace Hyperspan {
|
|
|
402
437
|
}
|
|
403
438
|
}
|
|
404
439
|
|
|
405
|
-
export type
|
|
440
|
+
export type DeployAdapter = Hyperspan.DeployAdapter;
|
|
406
441
|
export type ServerCreateContext = Hyperspan.ServerCreateContext;
|
|
442
|
+
export type Adapter = Hyperspan.Adapter;
|
|
443
|
+
export type AdapterAfterBuildContext = Hyperspan.AdapterAfterBuildContext;
|
|
444
|
+
export type DeployEntry = Hyperspan.DeployEntry;
|
|
445
|
+
export type DeployEntryContext = Hyperspan.DeployEntryContext;
|
|
407
446
|
|
|
408
447
|
declare global {
|
|
409
448
|
interface DocumentEventMap {
|