@hyperspan/framework 2.0.0-alpha.14 → 2.0.0-alpha.16
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 +2 -2
- package/src/client/js.test.ts +81 -16
- package/src/client/js.ts +162 -51
- package/src/client/manifest.ts +5 -1
- 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.16",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.ts",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"vitest": "^4.1.10"
|
|
105
105
|
},
|
|
106
106
|
"dependencies": {
|
|
107
|
-
"@hyperspan/html": "^2.0.0-alpha.
|
|
107
|
+
"@hyperspan/html": "^2.0.0-alpha.16",
|
|
108
108
|
"debug": "^4.4.3",
|
|
109
109
|
"isbot": "^5.1.32",
|
|
110
110
|
"zod": "^4.4.3"
|
package/src/client/js.test.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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
|
+
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import { render } from '@hyperspan/html';
|
|
7
|
+
import { assetHash } from '../utils';
|
|
8
|
+
import { setAssetManifest } from './manifest';
|
|
6
9
|
import {
|
|
7
10
|
buildClientJS,
|
|
8
11
|
discoverClientExports,
|
|
@@ -10,6 +13,7 @@ import {
|
|
|
10
13
|
getClientJSEntries,
|
|
11
14
|
registerPathAliases,
|
|
12
15
|
resetClientJSEntriesForTests,
|
|
16
|
+
streamingClient,
|
|
13
17
|
} from './js';
|
|
14
18
|
|
|
15
19
|
describe('buildClientJS', () => {
|
|
@@ -17,6 +21,7 @@ describe('buildClientJS', () => {
|
|
|
17
21
|
|
|
18
22
|
beforeEach(() => {
|
|
19
23
|
resetClientJSEntriesForTests();
|
|
24
|
+
setAssetManifest({ imports: {}, css: {}, clients: {} });
|
|
20
25
|
const dir = mkdtempSync(join(tmpdir(), 'hs-client-'));
|
|
21
26
|
clientFile = join(dir, 'picker.ts');
|
|
22
27
|
writeFileSync(
|
|
@@ -28,7 +33,7 @@ describe('buildClientJS', () => {
|
|
|
28
33
|
);
|
|
29
34
|
});
|
|
30
35
|
|
|
31
|
-
test('registers entry
|
|
36
|
+
test('registers entry with a stable path identity', async () => {
|
|
32
37
|
const result = await buildClientJS(clientFile);
|
|
33
38
|
|
|
34
39
|
expect(result.esmName).toMatch(/^client-[0-9a-f]{16}$/);
|
|
@@ -37,11 +42,37 @@ describe('buildClientJS', () => {
|
|
|
37
42
|
expect(getClientJSEntries()[0].absPath).toBe(clientFile);
|
|
38
43
|
});
|
|
39
44
|
|
|
45
|
+
test('changing file contents does not change the import-map key', async () => {
|
|
46
|
+
const first = await buildClientJS(clientFile);
|
|
47
|
+
writeFileSync(clientFile, `export function mountPicker() { return 2; }\n`);
|
|
48
|
+
resetClientJSEntriesForTests();
|
|
49
|
+
const second = await buildClientJS(clientFile);
|
|
50
|
+
expect(second.esmName).toBe(first.esmName);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('publicPath uses the Vite-hashed URL from the import map', async () => {
|
|
54
|
+
const result = await buildClientJS(clientFile);
|
|
55
|
+
const hashed = `/_hs/js/${result.esmName}-a1b2c3d4.js`;
|
|
56
|
+
setAssetManifest({
|
|
57
|
+
imports: { [result.esmName]: hashed },
|
|
58
|
+
css: {},
|
|
59
|
+
clients: {},
|
|
60
|
+
});
|
|
61
|
+
expect(result.publicPath).toBe(hashed);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('absolute path and file URL of the same file share one hash', async () => {
|
|
65
|
+
const viaAbs = await buildClientJS(clientFile);
|
|
66
|
+
resetClientJSEntriesForTests();
|
|
67
|
+
const viaUrl = await buildClientJS(pathToFileURL(clientFile).href);
|
|
68
|
+
expect(viaUrl.esmName).toBe(viaAbs.esmName);
|
|
69
|
+
});
|
|
70
|
+
|
|
40
71
|
test('renderScriptTag with no loader uses import map key', async () => {
|
|
41
72
|
const result = await buildClientJS(clientFile);
|
|
42
73
|
const tag = render(result.renderScriptTag());
|
|
43
74
|
|
|
44
|
-
expect(tag).toContain(`import
|
|
75
|
+
expect(tag).toContain(`import '${result.esmName}'`);
|
|
45
76
|
expect(tag).toContain(`data-source-id="${result.assetHash}"`);
|
|
46
77
|
});
|
|
47
78
|
|
|
@@ -53,7 +84,7 @@ describe('buildClientJS', () => {
|
|
|
53
84
|
})
|
|
54
85
|
);
|
|
55
86
|
|
|
56
|
-
expect(tag).toContain(`import {mountPicker} from
|
|
87
|
+
expect(tag).toContain(`import {mountPicker} from '${result.esmName}'`);
|
|
57
88
|
expect(tag).toContain('mountPicker()');
|
|
58
89
|
});
|
|
59
90
|
|
|
@@ -64,7 +95,33 @@ describe('buildClientJS', () => {
|
|
|
64
95
|
expect(tag).toContain('({ mountPicker }) => mountPicker()');
|
|
65
96
|
});
|
|
66
97
|
|
|
67
|
-
test('
|
|
98
|
+
test('package specifiers hash the same wherever the package is installed', async () => {
|
|
99
|
+
const spec = '@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts';
|
|
100
|
+
const first = await buildClientJS(spec, { type: 'iife' });
|
|
101
|
+
resetClientJSEntriesForTests();
|
|
102
|
+
const second = await buildClientJS(spec, { type: 'iife' });
|
|
103
|
+
expect(first.esmName).toBe(second.esmName);
|
|
104
|
+
expect(first.esmName).toBe(streamingClient.esmName);
|
|
105
|
+
expect(existsSync(getClientJSEntries()[0].absPath)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
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 () => {
|
|
68
125
|
const result = await buildClientJS(import.meta.resolve('./_hs/hyperspan-streaming.client.ts'), {
|
|
69
126
|
type: 'iife',
|
|
70
127
|
});
|
|
@@ -73,11 +130,17 @@ describe('buildClientJS', () => {
|
|
|
73
130
|
expect(getClientJSEntries()[0].type).toBe('iife');
|
|
74
131
|
expect(existsSync(getClientJSEntries()[0].absPath)).toBe(true);
|
|
75
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);
|
|
76
139
|
});
|
|
77
140
|
|
|
78
|
-
test('relative paths
|
|
79
|
-
await expect(buildClientJS('../client/picker.ts')).rejects.toThrow(/
|
|
80
|
-
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/);
|
|
81
144
|
});
|
|
82
145
|
|
|
83
146
|
test('type iife renders a classic script tag', async () => {
|
|
@@ -132,14 +195,16 @@ describe('buildClientJS', () => {
|
|
|
132
195
|
expect(second.esmName).toBe(result.esmName);
|
|
133
196
|
});
|
|
134
197
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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: {} });
|
|
204
|
+
|
|
205
|
+
const result = await buildClientJS(spec);
|
|
206
|
+
expect(result.esmName).toBe(esmName);
|
|
207
|
+
expect(result.publicPath).toBe(hashed);
|
|
143
208
|
});
|
|
144
209
|
});
|
|
145
210
|
|
package/src/client/js.ts
CHANGED
|
@@ -21,7 +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;
|
|
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). */
|
|
25
35
|
assetHash: string;
|
|
26
36
|
esmName: string;
|
|
27
37
|
publicPath: string;
|
|
@@ -30,7 +40,13 @@ export type ClientJSEntry = {
|
|
|
30
40
|
type: ClientJSType;
|
|
31
41
|
};
|
|
32
42
|
|
|
43
|
+
export type ClientJSPathIdentity = {
|
|
44
|
+
modulePath: string;
|
|
45
|
+
identityKey: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
33
48
|
const CLIENT_JS_REGISTRY = Symbol.for('@hyperspan/client-js-entries');
|
|
49
|
+
const CLIENT_JS_PATH_IDENTITIES = Symbol.for('@hyperspan/client-js-path-identities');
|
|
34
50
|
const PATH_ALIASES = Symbol.for('@hyperspan/path-aliases');
|
|
35
51
|
|
|
36
52
|
function getPathAliases(): Record<string, string> {
|
|
@@ -60,6 +76,23 @@ function resolveWithAliases(specifier: string): string | undefined {
|
|
|
60
76
|
return undefined;
|
|
61
77
|
}
|
|
62
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
|
+
|
|
63
96
|
function getClientJSRegistry(): Map<string, ClientJSEntry> {
|
|
64
97
|
const globalRegistry = globalThis as {
|
|
65
98
|
[CLIENT_JS_REGISTRY]?: Map<string, ClientJSEntry>;
|
|
@@ -99,22 +132,23 @@ export const JS_IMPORT_MAP = {
|
|
|
99
132
|
};
|
|
100
133
|
|
|
101
134
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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.
|
|
104
138
|
*/
|
|
105
|
-
function
|
|
139
|
+
export function stableClientSourceKey(absPath: string): string {
|
|
106
140
|
const normalized = absPath.replace(/\\/g, '/');
|
|
141
|
+
const marker = '/node_modules/';
|
|
142
|
+
const idx = normalized.lastIndexOf(marker);
|
|
143
|
+
if (idx !== -1) {
|
|
144
|
+
return normalized.slice(idx + marker.length);
|
|
145
|
+
}
|
|
107
146
|
if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
|
|
108
147
|
const rel = relative(process.cwd(), absPath).replace(/\\/g, '/');
|
|
109
148
|
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) {
|
|
110
149
|
return rel;
|
|
111
150
|
}
|
|
112
151
|
}
|
|
113
|
-
const marker = '/node_modules/';
|
|
114
|
-
const idx = normalized.lastIndexOf(marker);
|
|
115
|
-
if (idx !== -1) {
|
|
116
|
-
return normalized.slice(idx + marker.length);
|
|
117
|
-
}
|
|
118
152
|
return normalized;
|
|
119
153
|
}
|
|
120
154
|
|
|
@@ -122,33 +156,68 @@ function toFilePath(modulePath: string): string {
|
|
|
122
156
|
return modulePath.startsWith('file://') ? fileURLToPath(modulePath) : modulePath;
|
|
123
157
|
}
|
|
124
158
|
|
|
159
|
+
function tryResolveToFile(specifier: string): string | undefined {
|
|
160
|
+
try {
|
|
161
|
+
if (typeof import.meta.resolve !== 'function') {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const resolved = import.meta.resolve(specifier);
|
|
165
|
+
if (typeof resolved === 'string' && (resolved.startsWith('file://') || isAbsolute(resolved))) {
|
|
166
|
+
return toFilePath(resolved);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Workers and unresolved package specifiers — Vite already emitted the asset.
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function clientPublicPath(esmName: string): string {
|
|
175
|
+
return resolveImport(esmName) ?? `${JS_PUBLIC_PATH}/${esmName}.js`;
|
|
176
|
+
}
|
|
177
|
+
|
|
125
178
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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.
|
|
130
185
|
*/
|
|
131
|
-
function
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
return { hashKey: stableHashKey(absPath), absPath };
|
|
186
|
+
function clientIdentityKey(specifier: string): string {
|
|
187
|
+
if (specifier.startsWith('file://') || isAbsolute(specifier)) {
|
|
188
|
+
return stableClientSourceKey(toFilePath(specifier));
|
|
135
189
|
}
|
|
190
|
+
return specifier;
|
|
191
|
+
}
|
|
136
192
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
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);
|
|
144
200
|
}
|
|
145
201
|
|
|
146
202
|
const aliased = resolveWithAliases(specifier);
|
|
147
203
|
if (aliased) {
|
|
148
|
-
return
|
|
204
|
+
return aliased;
|
|
149
205
|
}
|
|
150
206
|
|
|
151
|
-
|
|
207
|
+
const cwd =
|
|
208
|
+
typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '';
|
|
209
|
+
return tryResolveToFile(specifier) ?? (cwd ? join(cwd, specifier) : specifier);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readClientSource(absPath: string): string | undefined {
|
|
213
|
+
try {
|
|
214
|
+
if (existsSync(absPath)) {
|
|
215
|
+
return readFileSync(absPath, 'utf-8');
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Workers have no filesystem; Vite already bundled from this entry.
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
152
221
|
}
|
|
153
222
|
|
|
154
223
|
export function getClientJSEntries(): ClientJSEntry[] {
|
|
@@ -161,6 +230,7 @@ export function getClientJSEntryByEsmName(esmName: string): ClientJSEntry | unde
|
|
|
161
230
|
|
|
162
231
|
export function resetClientJSEntriesForTests(): void {
|
|
163
232
|
getClientJSRegistry().clear();
|
|
233
|
+
getPathIdentityClientJS().length = 0;
|
|
164
234
|
const aliases = getPathAliases();
|
|
165
235
|
for (const key of Object.keys(aliases)) {
|
|
166
236
|
delete aliases[key];
|
|
@@ -172,43 +242,79 @@ function registerClientJS(
|
|
|
172
242
|
options: BuildClientJSOptions = {}
|
|
173
243
|
): HS.ClientJSBuildResult {
|
|
174
244
|
const type = options.type ?? 'module';
|
|
175
|
-
const
|
|
176
|
-
|
|
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;
|
|
257
|
+
const hash = assetHashFn(identityKey);
|
|
177
258
|
const esmName = `client-${hash}`;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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;
|
|
188
280
|
}
|
|
189
281
|
|
|
190
|
-
|
|
282
|
+
const entry: ClientJSEntry = {
|
|
191
283
|
modulePath: modulePathResolved,
|
|
192
|
-
absPath
|
|
284
|
+
get absPath() {
|
|
285
|
+
return currentAbsPath();
|
|
286
|
+
},
|
|
287
|
+
identityKey,
|
|
288
|
+
identityFromPath,
|
|
193
289
|
assetHash: hash,
|
|
194
290
|
esmName,
|
|
195
|
-
publicPath
|
|
196
|
-
|
|
197
|
-
|
|
291
|
+
get publicPath() {
|
|
292
|
+
return clientPublicPath(esmName);
|
|
293
|
+
},
|
|
294
|
+
get exports() {
|
|
295
|
+
return currentExports().exports;
|
|
296
|
+
},
|
|
297
|
+
get fnArgs() {
|
|
298
|
+
return currentExports().fnArgs;
|
|
299
|
+
},
|
|
198
300
|
type,
|
|
199
|
-
}
|
|
200
|
-
|
|
301
|
+
};
|
|
302
|
+
getClientJSRegistry().set(esmName, entry);
|
|
303
|
+
registerImport(esmName, entry.publicPath);
|
|
201
304
|
|
|
202
305
|
return {
|
|
203
306
|
assetHash: hash,
|
|
204
307
|
esmName,
|
|
205
|
-
publicPath
|
|
308
|
+
get publicPath() {
|
|
309
|
+
return clientPublicPath(esmName);
|
|
310
|
+
},
|
|
206
311
|
renderScriptTag: (loadScript) => {
|
|
207
312
|
if (type === 'iife') {
|
|
208
|
-
return html`<script src="${
|
|
313
|
+
return html`<script src="${clientPublicPath(esmName)}"></script>`;
|
|
209
314
|
}
|
|
210
315
|
|
|
211
316
|
const t = typeof loadScript;
|
|
317
|
+
const { exports, fnArgs } = currentExports();
|
|
212
318
|
|
|
213
319
|
if (t === 'string') {
|
|
214
320
|
return html`
|
|
@@ -238,9 +344,12 @@ function registerClientJS(
|
|
|
238
344
|
|
|
239
345
|
/**
|
|
240
346
|
* Register a client JS module for Vite to bundle.
|
|
241
|
-
* Prefer a tsconfig alias or project-root path
|
|
242
|
-
* `app/client/foo.ts`)
|
|
243
|
-
*
|
|
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')`.
|
|
244
353
|
*/
|
|
245
354
|
export async function buildClientJS(
|
|
246
355
|
modulePathResolved: string,
|
|
@@ -249,12 +358,14 @@ export async function buildClientJS(
|
|
|
249
358
|
return registerClientJS(modulePathResolved, options);
|
|
250
359
|
}
|
|
251
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.
|
|
252
363
|
export const streamingClient = registerClientJS(
|
|
253
|
-
|
|
364
|
+
'@hyperspan/framework/client/_hs/hyperspan-streaming.client.ts',
|
|
254
365
|
{ type: 'iife' }
|
|
255
366
|
);
|
|
256
367
|
export const actionsClient = registerClientJS(
|
|
257
|
-
|
|
368
|
+
'@hyperspan/framework/client/_hs/hyperspan-actions.client.ts'
|
|
258
369
|
);
|
|
259
370
|
|
|
260
371
|
/**
|
package/src/client/manifest.ts
CHANGED
|
@@ -16,7 +16,11 @@ let _manifest: AssetManifest = {
|
|
|
16
16
|
* Set the build-time asset manifest (called by Vite plugin or build step).
|
|
17
17
|
*/
|
|
18
18
|
export function setAssetManifest(manifest: AssetManifest): void {
|
|
19
|
-
_manifest =
|
|
19
|
+
_manifest = {
|
|
20
|
+
imports: manifest.imports ?? {},
|
|
21
|
+
css: manifest.css ?? {},
|
|
22
|
+
clients: manifest.clients ?? {},
|
|
23
|
+
};
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/**
|
package/src/types.ts
CHANGED
|
@@ -430,7 +430,7 @@ export namespace Hyperspan {
|
|
|
430
430
|
* Client JS Module = ESM Module + Public Path + Render Script Tag
|
|
431
431
|
*/
|
|
432
432
|
export type ClientJSBuildResult = {
|
|
433
|
-
assetHash: string; //
|
|
433
|
+
assetHash: string; // Identity of the resolved path; production file hash comes from Vite
|
|
434
434
|
esmName: string; // Filename of the built JavaScript file without the extension
|
|
435
435
|
publicPath: string; // Full public path of the built JavaScript file
|
|
436
436
|
/**
|