@hyperspan/framework 2.0.0-alpha.15 → 2.0.0-alpha.19
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 +6 -2
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +1 -1
- package/src/client/js.test.ts +40 -23
- package/src/client/js.ts +127 -103
- package/src/client/manifest.ts +0 -4
- package/src/html.test.ts +13 -0
- package/src/html.ts +1 -0
- package/src/island.ts +1 -1
- package/src/layout.test.ts +1 -1
- package/src/layout.ts +1 -1
- package/src/server.test.ts +1 -1
- package/src/server.ts +1 -9
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.19",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.ts",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"types": "./src/actions.ts",
|
|
56
56
|
"default": "./src/actions.ts"
|
|
57
57
|
},
|
|
58
|
+
"./html": {
|
|
59
|
+
"types": "./src/html.ts",
|
|
60
|
+
"default": "./src/html.ts"
|
|
61
|
+
},
|
|
58
62
|
"./ssr/install-server-dom-mock": {
|
|
59
63
|
"types": "./src/ssr/install-server-dom-mock.ts",
|
|
60
64
|
"default": "./src/ssr/install-server-dom-mock.ts"
|
|
@@ -104,7 +108,7 @@
|
|
|
104
108
|
"vitest": "^4.1.10"
|
|
105
109
|
},
|
|
106
110
|
"dependencies": {
|
|
107
|
-
"@hyperspan/html": "^2.0.0-alpha.
|
|
111
|
+
"@hyperspan/html": "^2.0.0-alpha.19",
|
|
108
112
|
"debug": "^4.4.3",
|
|
109
113
|
"isbot": "^5.1.32",
|
|
110
114
|
"zod": "^4.4.3"
|
package/src/actions.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, expect, describe } from 'vitest';
|
|
2
2
|
import { createAction } from './actions';
|
|
3
3
|
import { createRoute } from './server';
|
|
4
|
-
import { html, render, placeholder, type HSHtml } from '
|
|
4
|
+
import { html, render, placeholder, type HSHtml } from './html';
|
|
5
5
|
import { createContext } from './server';
|
|
6
6
|
import * as z from 'zod';
|
|
7
7
|
|
package/src/actions.ts
CHANGED
package/src/client/js.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test, beforeEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
|
-
import { render } from '
|
|
6
|
+
import { render } from '../html';
|
|
7
|
+
import { assetHash } from '../utils';
|
|
7
8
|
import { setAssetManifest } from './manifest';
|
|
8
9
|
import {
|
|
9
10
|
buildClientJS,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
getClientJSEntries,
|
|
13
14
|
registerPathAliases,
|
|
14
15
|
resetClientJSEntriesForTests,
|
|
15
|
-
stableClientSourceKey,
|
|
16
16
|
streamingClient,
|
|
17
17
|
} from './js';
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ describe('buildClientJS', () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(() => {
|
|
23
23
|
resetClientJSEntriesForTests();
|
|
24
|
-
setAssetManifest({ imports: {}, css: {}, clients: {}
|
|
24
|
+
setAssetManifest({ imports: {}, css: {}, clients: {} });
|
|
25
25
|
const dir = mkdtempSync(join(tmpdir(), 'hs-client-'));
|
|
26
26
|
clientFile = join(dir, 'picker.ts');
|
|
27
27
|
writeFileSync(
|
|
@@ -57,7 +57,6 @@ describe('buildClientJS', () => {
|
|
|
57
57
|
imports: { [result.esmName]: hashed },
|
|
58
58
|
css: {},
|
|
59
59
|
clients: {},
|
|
60
|
-
clientSources: {},
|
|
61
60
|
});
|
|
62
61
|
expect(result.publicPath).toBe(hashed);
|
|
63
62
|
});
|
|
@@ -96,7 +95,7 @@ describe('buildClientJS', () => {
|
|
|
96
95
|
expect(tag).toContain('({ mountPicker }) => mountPicker()');
|
|
97
96
|
});
|
|
98
97
|
|
|
99
|
-
test('package specifiers
|
|
98
|
+
test('package specifiers hash the same wherever the package is installed', async () => {
|
|
100
99
|
const spec = '@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts';
|
|
101
100
|
const first = await buildClientJS(spec, { type: 'iife' });
|
|
102
101
|
resetClientJSEntriesForTests();
|
|
@@ -106,21 +105,42 @@ describe('buildClientJS', () => {
|
|
|
106
105
|
expect(existsSync(getClientJSEntries()[0].absPath)).toBe(true);
|
|
107
106
|
});
|
|
108
107
|
|
|
109
|
-
|
|
108
|
+
// workerd rejects `import.meta.resolve` after bundling, which crashed Worker startup.
|
|
109
|
+
test('builtins register without resolving a path at module load', () => {
|
|
110
|
+
const src = readFileSync(new URL('./js.ts', import.meta.url), 'utf8');
|
|
111
|
+
const registrations = src.slice(src.indexOf('export const streamingClient'));
|
|
112
|
+
|
|
113
|
+
expect(registrations).toContain(
|
|
114
|
+
"'@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts'"
|
|
115
|
+
);
|
|
116
|
+
expect(registrations).toContain(
|
|
117
|
+
"'@hyperspan/framework/client/_hs/hyperspan-actions.client.ts'"
|
|
118
|
+
);
|
|
119
|
+
expect(registrations).not.toContain('import.meta.resolve');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Valid on Node/Bun, which can resolve paths at runtime. Flagged as a path identity
|
|
123
|
+
// so a Worker build can reject it instead of serving a 404 for a script in dist/.
|
|
124
|
+
test('import.meta.resolve at the call site registers a real file, flagged as a path identity', async () => {
|
|
110
125
|
const result = await buildClientJS(import.meta.resolve('./_hs/hyperspan-streaming.client.ts'), {
|
|
111
126
|
type: 'iife',
|
|
112
127
|
});
|
|
113
128
|
|
|
114
|
-
expect(result.esmName).toBe(streamingClient.esmName);
|
|
115
129
|
expect(result.publicPath).toMatch(/^\/_hs\/js\/client-[0-9a-f]{16}\.js$/);
|
|
116
130
|
expect(getClientJSEntries()[0].type).toBe('iife');
|
|
117
131
|
expect(existsSync(getClientJSEntries()[0].absPath)).toBe(true);
|
|
118
132
|
expect(getClientJSEntries()[0].absPath).toMatch(/hyperspan-streaming\.client\.ts$/);
|
|
133
|
+
expect(getClientJSEntries()[0].identityFromPath).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('specifier identities are not flagged as path identities', async () => {
|
|
137
|
+
await buildClientJS('@hyperspan/framework/client/_hs/hyperspan-actions.client.ts');
|
|
138
|
+
expect(getClientJSEntries()[0].identityFromPath).toBe(false);
|
|
119
139
|
});
|
|
120
140
|
|
|
121
|
-
test('relative paths
|
|
122
|
-
await expect(buildClientJS('../client/picker.ts')).rejects.toThrow(/
|
|
123
|
-
await expect(buildClientJS('./picker.ts')).rejects.toThrow(/
|
|
141
|
+
test('relative paths are rejected in favor of an alias', async () => {
|
|
142
|
+
await expect(buildClientJS('../client/picker.ts')).rejects.toThrow(/tsconfig alias/);
|
|
143
|
+
await expect(buildClientJS('./picker.ts')).rejects.toThrow(/tsconfig alias/);
|
|
124
144
|
});
|
|
125
145
|
|
|
126
146
|
test('type iife renders a classic script tag', async () => {
|
|
@@ -175,19 +195,16 @@ describe('buildClientJS', () => {
|
|
|
175
195
|
expect(second.esmName).toBe(result.esmName);
|
|
176
196
|
});
|
|
177
197
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
clients: {},
|
|
185
|
-
clientSources: { [stableClientSourceKey(ghost)]: builtName },
|
|
186
|
-
});
|
|
198
|
+
// A Worker has no filesystem: registration must not read the source to get a URL.
|
|
199
|
+
test('missing source files still resolve to the built URL', async () => {
|
|
200
|
+
const spec = 'app/client/gone.ts';
|
|
201
|
+
const esmName = `client-${assetHash(spec)}`;
|
|
202
|
+
const hashed = `/_hs/js/${esmName}-a1b2c3d4.js`;
|
|
203
|
+
setAssetManifest({ imports: { [esmName]: hashed }, css: {}, clients: {} });
|
|
187
204
|
|
|
188
|
-
const result = await buildClientJS(
|
|
189
|
-
expect(result.esmName).toBe(
|
|
190
|
-
expect(result.publicPath).toBe(
|
|
205
|
+
const result = await buildClientJS(spec);
|
|
206
|
+
expect(result.esmName).toBe(esmName);
|
|
207
|
+
expect(result.publicPath).toBe(hashed);
|
|
191
208
|
});
|
|
192
209
|
});
|
|
193
210
|
|
package/src/client/js.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, join, relative } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { html } from '
|
|
4
|
+
import { html } from '../html';
|
|
5
5
|
import { assetHash as assetHashFn } from '../utils';
|
|
6
|
-
import {
|
|
6
|
+
import { getImportMap, registerImport, resolveImport } from './manifest';
|
|
7
7
|
import type { Hyperspan as HS } from '../types';
|
|
8
8
|
|
|
9
9
|
export { registerImport, resolveImport, getImportMap } from './manifest';
|
|
@@ -21,10 +21,17 @@ export type BuildClientJSOptions = {
|
|
|
21
21
|
export type ClientJSEntry = {
|
|
22
22
|
/** Original path/specifier passed to `buildClientJS`. */
|
|
23
23
|
modulePath: string;
|
|
24
|
+
/** Resolved file path. Lazy — only a build/dev tool with a filesystem reads this. */
|
|
24
25
|
absPath: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/**
|
|
26
|
+
/** Runtime-stable identity of the module. Same value on Vite, Node, and Workers. */
|
|
27
|
+
identityKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* Identity came from a filesystem path rather than a logical specifier. Such an
|
|
30
|
+
* identity only matches at runtime on a runtime that can resolve paths, so it is
|
|
31
|
+
* not portable to a Worker.
|
|
32
|
+
*/
|
|
33
|
+
identityFromPath: boolean;
|
|
34
|
+
/** Identity hash of `identityKey` (not of file contents). */
|
|
28
35
|
assetHash: string;
|
|
29
36
|
esmName: string;
|
|
30
37
|
publicPath: string;
|
|
@@ -33,7 +40,13 @@ export type ClientJSEntry = {
|
|
|
33
40
|
type: ClientJSType;
|
|
34
41
|
};
|
|
35
42
|
|
|
43
|
+
export type ClientJSPathIdentity = {
|
|
44
|
+
modulePath: string;
|
|
45
|
+
identityKey: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
36
48
|
const CLIENT_JS_REGISTRY = Symbol.for('@hyperspan/client-js-entries');
|
|
49
|
+
const CLIENT_JS_PATH_IDENTITIES = Symbol.for('@hyperspan/client-js-path-identities');
|
|
37
50
|
const PATH_ALIASES = Symbol.for('@hyperspan/path-aliases');
|
|
38
51
|
|
|
39
52
|
function getPathAliases(): Record<string, string> {
|
|
@@ -63,6 +76,23 @@ function resolveWithAliases(specifier: string): string | undefined {
|
|
|
63
76
|
return undefined;
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Client scripts registered from a path rather than a logical specifier, kept outside
|
|
81
|
+
* the registry. A path identity can coincide with the equivalent project-relative
|
|
82
|
+
* specifier (`file:///proj/app/client/x.ts` and `app/client/x.ts` share one identity),
|
|
83
|
+
* so the registry entry may be replaced by whichever form registered last. Deploy
|
|
84
|
+
* checks need the record regardless of that order.
|
|
85
|
+
*/
|
|
86
|
+
export function getPathIdentityClientJS(): ClientJSPathIdentity[] {
|
|
87
|
+
const globalPathIdentities = globalThis as {
|
|
88
|
+
[CLIENT_JS_PATH_IDENTITIES]?: ClientJSPathIdentity[];
|
|
89
|
+
};
|
|
90
|
+
if (!globalPathIdentities[CLIENT_JS_PATH_IDENTITIES]) {
|
|
91
|
+
globalPathIdentities[CLIENT_JS_PATH_IDENTITIES] = [];
|
|
92
|
+
}
|
|
93
|
+
return globalPathIdentities[CLIENT_JS_PATH_IDENTITIES];
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
function getClientJSRegistry(): Map<string, ClientJSEntry> {
|
|
67
97
|
const globalRegistry = globalThis as {
|
|
68
98
|
[CLIENT_JS_REGISTRY]?: Map<string, ClientJSEntry>;
|
|
@@ -102,9 +132,9 @@ export const JS_IMPORT_MAP = {
|
|
|
102
132
|
};
|
|
103
133
|
|
|
104
134
|
/**
|
|
105
|
-
* File
|
|
106
|
-
*
|
|
107
|
-
*
|
|
135
|
+
* File paths must not become a cwd-relative pnpm path — that changes between
|
|
136
|
+
* install layouts and Workers. Prefer the path after the last `node_modules/`
|
|
137
|
+
* so published packages stay stable.
|
|
108
138
|
*/
|
|
109
139
|
export function stableClientSourceKey(absPath: string): string {
|
|
110
140
|
const normalized = absPath.replace(/\\/g, '/');
|
|
@@ -146,24 +176,27 @@ function clientPublicPath(esmName: string): string {
|
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
179
|
+
* Identity of a client module, computed with string math only — no filesystem and
|
|
180
|
+
* no `import.meta.resolve`. Workers can therefore build the same identity (and so
|
|
181
|
+
* the same public URL) that Vite used at build time.
|
|
182
|
+
*
|
|
183
|
+
* A logical specifier (package export, tsconfig alias, project-root path) is its
|
|
184
|
+
* own identity. File URLs and absolute paths fall back to a path identity.
|
|
154
185
|
*/
|
|
155
|
-
function
|
|
156
|
-
if (
|
|
157
|
-
return toFilePath(
|
|
186
|
+
function clientIdentityKey(specifier: string): string {
|
|
187
|
+
if (specifier.startsWith('file://') || isAbsolute(specifier)) {
|
|
188
|
+
return stableClientSourceKey(toFilePath(specifier));
|
|
158
189
|
}
|
|
190
|
+
return specifier;
|
|
191
|
+
}
|
|
159
192
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
193
|
+
/**
|
|
194
|
+
* Resolve an identity to a real file path. Only build/dev tooling calls this, so
|
|
195
|
+
* `import.meta.resolve` never runs during module load on a Worker.
|
|
196
|
+
*/
|
|
197
|
+
function resolveClientAbsPath(specifier: string): string {
|
|
198
|
+
if (specifier.startsWith('file://') || isAbsolute(specifier)) {
|
|
199
|
+
return toFilePath(specifier);
|
|
167
200
|
}
|
|
168
201
|
|
|
169
202
|
const aliased = resolveWithAliases(specifier);
|
|
@@ -171,7 +204,9 @@ function resolveClientAbsPath(modulePath: string): string {
|
|
|
171
204
|
return aliased;
|
|
172
205
|
}
|
|
173
206
|
|
|
174
|
-
|
|
207
|
+
const cwd =
|
|
208
|
+
typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '';
|
|
209
|
+
return tryResolveToFile(specifier) ?? (cwd ? join(cwd, specifier) : specifier);
|
|
175
210
|
}
|
|
176
211
|
|
|
177
212
|
function readClientSource(absPath: string): string | undefined {
|
|
@@ -185,121 +220,105 @@ function readClientSource(absPath: string): string | undefined {
|
|
|
185
220
|
return undefined;
|
|
186
221
|
}
|
|
187
222
|
|
|
188
|
-
function clientSourceKeys(modulePath: string, absPath: string): string[] {
|
|
189
|
-
const keys = new Set<string>([stableClientSourceKey(absPath), modulePath.replace(/\\/g, '/')]);
|
|
190
|
-
if (modulePath.startsWith('file://')) {
|
|
191
|
-
keys.add(toFilePath(modulePath));
|
|
192
|
-
}
|
|
193
|
-
return [...keys];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function resolveEsmName(sourceKeys: string[], fallbackHash: string): string {
|
|
197
|
-
const sources = getAssetManifest().clientSources;
|
|
198
|
-
if (sources) {
|
|
199
|
-
for (const key of sourceKeys) {
|
|
200
|
-
if (sources[key]) {
|
|
201
|
-
return sources[key];
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return `client-${fallbackHash}`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
223
|
export function getClientJSEntries(): ClientJSEntry[] {
|
|
209
224
|
return [...getClientJSRegistry().values()];
|
|
210
225
|
}
|
|
211
226
|
|
|
212
227
|
export function getClientJSEntryByEsmName(esmName: string): ClientJSEntry | undefined {
|
|
213
|
-
|
|
214
|
-
if (currentEsmName(entry) === esmName) {
|
|
215
|
-
return entry;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function getClientJSSourceMap(): Record<string, string> {
|
|
222
|
-
const map: Record<string, string> = {};
|
|
223
|
-
for (const entry of getClientJSEntries()) {
|
|
224
|
-
const esmName = currentEsmName(entry);
|
|
225
|
-
for (const key of entry.sourceKeys) {
|
|
226
|
-
map[key] = esmName;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return map;
|
|
228
|
+
return getClientJSRegistry().get(esmName);
|
|
230
229
|
}
|
|
231
230
|
|
|
232
231
|
export function resetClientJSEntriesForTests(): void {
|
|
233
232
|
getClientJSRegistry().clear();
|
|
233
|
+
getPathIdentityClientJS().length = 0;
|
|
234
234
|
const aliases = getPathAliases();
|
|
235
235
|
for (const key of Object.keys(aliases)) {
|
|
236
236
|
delete aliases[key];
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
function currentEsmName(entry: ClientJSEntry): string {
|
|
241
|
-
return resolveEsmName(entry.sourceKeys, entry.assetHash);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
240
|
function registerClientJS(
|
|
245
241
|
modulePathResolved: string,
|
|
246
242
|
options: BuildClientJSOptions = {}
|
|
247
243
|
): HS.ClientJSBuildResult {
|
|
248
244
|
const type = options.type ?? 'module';
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
const specifier = modulePathResolved.replace(/\\/g, '/');
|
|
246
|
+
if (specifier.startsWith('.')) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`[Hyperspan] buildClientJS(${JSON.stringify(modulePathResolved)}) got a relative path, ` +
|
|
249
|
+
`which has no stable identity across dev, build, and deploy. Use a tsconfig alias ` +
|
|
250
|
+
`like '~/app/client/file.ts' — the build resolves it, and every runtime (including ` +
|
|
251
|
+
`Workers, which cannot resolve paths) matches it in the asset manifest.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const identityKey = clientIdentityKey(specifier);
|
|
256
|
+
const identityFromPath = identityKey !== specifier;
|
|
252
257
|
const hash = assetHashFn(identityKey);
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
const esmName = `client-${hash}`;
|
|
259
|
+
|
|
260
|
+
if (identityFromPath) {
|
|
261
|
+
getPathIdentityClientJS().push({ modulePath: modulePathResolved, identityKey });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let absPath: string | undefined;
|
|
265
|
+
let discovered: { exports: string; fnArgs: string } | undefined;
|
|
266
|
+
|
|
267
|
+
// Both are lazy: a Worker renders script tags without a filesystem, and
|
|
268
|
+
// resolving there would need `import.meta.resolve`, which workerd may reject.
|
|
269
|
+
function currentAbsPath(): string {
|
|
270
|
+
return (absPath ??= resolveClientAbsPath(specifier));
|
|
271
|
+
}
|
|
272
|
+
function currentExports(): { exports: string; fnArgs: string } {
|
|
273
|
+
if (!discovered) {
|
|
274
|
+
const source = readClientSource(currentAbsPath());
|
|
275
|
+
discovered = source
|
|
276
|
+
? discoverClientExports(source)
|
|
277
|
+
: { exports: '* as _module', fnArgs: '_module' };
|
|
278
|
+
}
|
|
279
|
+
return discovered;
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
const entry: ClientJSEntry = {
|
|
264
283
|
modulePath: modulePathResolved,
|
|
265
|
-
absPath
|
|
266
|
-
|
|
267
|
-
assetHash: hash,
|
|
268
|
-
get esmName() {
|
|
269
|
-
return currentEsmName(entry);
|
|
284
|
+
get absPath() {
|
|
285
|
+
return currentAbsPath();
|
|
270
286
|
},
|
|
287
|
+
identityKey,
|
|
288
|
+
identityFromPath,
|
|
289
|
+
assetHash: hash,
|
|
290
|
+
esmName,
|
|
271
291
|
get publicPath() {
|
|
272
|
-
return clientPublicPath(
|
|
292
|
+
return clientPublicPath(esmName);
|
|
293
|
+
},
|
|
294
|
+
get exports() {
|
|
295
|
+
return currentExports().exports;
|
|
296
|
+
},
|
|
297
|
+
get fnArgs() {
|
|
298
|
+
return currentExports().fnArgs;
|
|
273
299
|
},
|
|
274
|
-
exports,
|
|
275
|
-
fnArgs,
|
|
276
300
|
type,
|
|
277
301
|
};
|
|
278
|
-
getClientJSRegistry().set(
|
|
279
|
-
registerImport(
|
|
302
|
+
getClientJSRegistry().set(esmName, entry);
|
|
303
|
+
registerImport(esmName, entry.publicPath);
|
|
280
304
|
|
|
281
305
|
return {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
},
|
|
285
|
-
get esmName() {
|
|
286
|
-
return currentEsmName(entry);
|
|
287
|
-
},
|
|
306
|
+
assetHash: hash,
|
|
307
|
+
esmName,
|
|
288
308
|
get publicPath() {
|
|
289
|
-
return clientPublicPath(
|
|
309
|
+
return clientPublicPath(esmName);
|
|
290
310
|
},
|
|
291
311
|
renderScriptTag: (loadScript) => {
|
|
292
|
-
const esmName = currentEsmName(entry);
|
|
293
|
-
const tagHash = esmName.replace(/^client-/, '');
|
|
294
312
|
if (type === 'iife') {
|
|
295
313
|
return html`<script src="${clientPublicPath(esmName)}"></script>`;
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
const t = typeof loadScript;
|
|
317
|
+
const { exports, fnArgs } = currentExports();
|
|
299
318
|
|
|
300
319
|
if (t === 'string') {
|
|
301
320
|
return html`
|
|
302
|
-
<script type="module" data-source-id="${
|
|
321
|
+
<script type="module" data-source-id="${hash}">
|
|
303
322
|
import ${exports} from '${esmName}';
|
|
304
323
|
(${html.raw(loadScript as string)})(${fnArgs});
|
|
305
324
|
</script>
|
|
@@ -307,7 +326,7 @@ function registerClientJS(
|
|
|
307
326
|
}
|
|
308
327
|
if (t === 'function') {
|
|
309
328
|
return html`
|
|
310
|
-
<script type="module" data-source-id="${
|
|
329
|
+
<script type="module" data-source-id="${hash}">
|
|
311
330
|
import ${exports} from '${esmName}';
|
|
312
331
|
(${html.raw(functionToString(loadScript))})(${fnArgs});
|
|
313
332
|
</script>
|
|
@@ -315,7 +334,7 @@ function registerClientJS(
|
|
|
315
334
|
}
|
|
316
335
|
|
|
317
336
|
return html`
|
|
318
|
-
<script type="module" data-source-id="${
|
|
337
|
+
<script type="module" data-source-id="${hash}">
|
|
319
338
|
import '${esmName}';
|
|
320
339
|
</script>
|
|
321
340
|
`;
|
|
@@ -325,9 +344,12 @@ function registerClientJS(
|
|
|
325
344
|
|
|
326
345
|
/**
|
|
327
346
|
* Register a client JS module for Vite to bundle.
|
|
328
|
-
* Prefer
|
|
329
|
-
*
|
|
330
|
-
*
|
|
347
|
+
* Prefer a package export, tsconfig alias, or project-root path
|
|
348
|
+
* (`@scope/pkg/file.ts`, `~/app/client/foo.ts`, `app/client/foo.ts`) — those need no
|
|
349
|
+
* filesystem lookup, so Vite, Node, and Workers all derive the same public URL.
|
|
350
|
+
* Vite/esbuild content-hash the emitted filename; `publicPath` reads that URL from
|
|
351
|
+
* the asset manifest. Relative `./` / `../` paths must be resolved at the call site
|
|
352
|
+
* with `import.meta.resolve('./file.ts')`.
|
|
331
353
|
*/
|
|
332
354
|
export async function buildClientJS(
|
|
333
355
|
modulePathResolved: string,
|
|
@@ -336,12 +358,14 @@ export async function buildClientJS(
|
|
|
336
358
|
return registerClientJS(modulePathResolved, options);
|
|
337
359
|
}
|
|
338
360
|
|
|
361
|
+
// Registered through the same public API an app uses. Package-export specifiers keep
|
|
362
|
+
// module load free of `import.meta.resolve`, which workerd rejects after bundling.
|
|
339
363
|
export const streamingClient = registerClientJS(
|
|
340
|
-
|
|
364
|
+
'@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts',
|
|
341
365
|
{ type: 'iife' }
|
|
342
366
|
);
|
|
343
367
|
export const actionsClient = registerClientJS(
|
|
344
|
-
|
|
368
|
+
'@hyperspan/framework/client/_hs/hyperspan-actions.client.ts'
|
|
345
369
|
);
|
|
346
370
|
|
|
347
371
|
/**
|
package/src/client/manifest.ts
CHANGED
|
@@ -4,15 +4,12 @@ export type AssetManifest = {
|
|
|
4
4
|
imports: Record<string, string>;
|
|
5
5
|
css: Record<string, string[]>;
|
|
6
6
|
clients: Record<string, string>;
|
|
7
|
-
/** Resolved path / original specifier → import-map key (`client-{id}`). */
|
|
8
|
-
clientSources?: Record<string, string>;
|
|
9
7
|
};
|
|
10
8
|
|
|
11
9
|
let _manifest: AssetManifest = {
|
|
12
10
|
imports: {},
|
|
13
11
|
css: {},
|
|
14
12
|
clients: {},
|
|
15
|
-
clientSources: {},
|
|
16
13
|
};
|
|
17
14
|
|
|
18
15
|
/**
|
|
@@ -23,7 +20,6 @@ export function setAssetManifest(manifest: AssetManifest): void {
|
|
|
23
20
|
imports: manifest.imports ?? {},
|
|
24
21
|
css: manifest.css ?? {},
|
|
25
22
|
clients: manifest.clients ?? {},
|
|
26
|
-
clientSources: { ...(manifest.clientSources ?? {}) },
|
|
27
23
|
};
|
|
28
24
|
}
|
|
29
25
|
|
package/src/html.test.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { html, placeholder, render, type HSHtml } from './html';
|
|
3
|
+
|
|
4
|
+
describe('@hyperspan/framework/html', () => {
|
|
5
|
+
test('re-exports html helpers and types from @hyperspan/html', () => {
|
|
6
|
+
const tmpl: HSHtml = html`<p>
|
|
7
|
+
${placeholder(html`<span>loading</span>`, Promise.resolve('ok'))}
|
|
8
|
+
</p>`;
|
|
9
|
+
|
|
10
|
+
expect(render(tmpl)).toContain('<p>');
|
|
11
|
+
expect(render(tmpl)).toContain('loading');
|
|
12
|
+
});
|
|
13
|
+
});
|
package/src/html.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@hyperspan/html';
|
package/src/island.ts
CHANGED
package/src/layout.test.ts
CHANGED
package/src/layout.ts
CHANGED
package/src/server.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, expect, describe } from 'vitest';
|
|
2
2
|
import { createRoute, createServer, createContext, initServerRoutes } from './server';
|
|
3
3
|
import { createAction } from './actions';
|
|
4
|
-
import { html, placeholder } from '
|
|
4
|
+
import { html, placeholder } from './html';
|
|
5
5
|
import type { Hyperspan as HS } from './types';
|
|
6
6
|
|
|
7
7
|
test('route fetch() returns a Response', async () => {
|
package/src/server.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import './ssr/install-server-dom-mock';
|
|
2
|
-
import {
|
|
3
|
-
HSHtml,
|
|
4
|
-
html,
|
|
5
|
-
isHSHtml,
|
|
6
|
-
renderStream,
|
|
7
|
-
renderAsync,
|
|
8
|
-
render,
|
|
9
|
-
_typeOf,
|
|
10
|
-
} from '@hyperspan/html';
|
|
2
|
+
import { HSHtml, html, isHSHtml, renderStream, renderAsync, render, _typeOf } from './html';
|
|
11
3
|
import { isbot } from 'isbot';
|
|
12
4
|
import { executeMiddleware } from './middleware';
|
|
13
5
|
import { buildUrl, parsePath, removeUndefined } from './utils';
|
package/src/types.ts
CHANGED