@hyperspan/framework 2.0.0-alpha.2 → 2.0.0-alpha.4
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/client/js.test.ts +22 -0
- package/src/client/js.ts +31 -4
- package/src/index.ts +3 -1
- package/src/register-routes.ts +64 -0
- package/src/server.ts +1 -0
- package/src/types.ts +18 -0
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.4",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.ts",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./src/utils.ts",
|
|
28
28
|
"default": "./src/utils.ts"
|
|
29
29
|
},
|
|
30
|
+
"./routes": {
|
|
31
|
+
"types": "./src/register-routes.ts",
|
|
32
|
+
"default": "./src/register-routes.ts"
|
|
33
|
+
},
|
|
30
34
|
"./layout": {
|
|
31
35
|
"types": "./src/layout.ts",
|
|
32
36
|
"default": "./src/layout.ts"
|
|
@@ -96,7 +100,7 @@
|
|
|
96
100
|
"vitest": "^3.2.4"
|
|
97
101
|
},
|
|
98
102
|
"dependencies": {
|
|
99
|
-
"@hyperspan/html": "^2.0.0-alpha.
|
|
103
|
+
"@hyperspan/html": "^2.0.0-alpha.4",
|
|
100
104
|
"debug": "^4.4.3",
|
|
101
105
|
"isbot": "^5.1.32",
|
|
102
106
|
"zod": "^4.4.3"
|
package/src/client/js.test.ts
CHANGED
|
@@ -62,6 +62,28 @@ describe('buildClientJS', () => {
|
|
|
62
62
|
|
|
63
63
|
expect(tag).toContain('({ mountPicker }) => mountPicker()');
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
test('logical app-relative paths hash consistently and resolve on disk when present', async () => {
|
|
67
|
+
const dir = mkdtempSync(join(tmpdir(), 'hs-client-logical-'));
|
|
68
|
+
const { mkdirSync } = await import('node:fs');
|
|
69
|
+
mkdirSync(join(dir, 'app/client'), { recursive: true });
|
|
70
|
+
const logicalPath = join(dir, 'app/client/stats.ts');
|
|
71
|
+
writeFileSync(logicalPath, `export function mountStats() {}\n`);
|
|
72
|
+
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
process.chdir(dir);
|
|
75
|
+
try {
|
|
76
|
+
const first = await buildClientJS('app/client/stats.ts');
|
|
77
|
+
resetClientJSEntriesForTests();
|
|
78
|
+
const second = await buildClientJS('app/client/stats.ts');
|
|
79
|
+
expect(first.esmName).toBe(second.esmName);
|
|
80
|
+
expect(getClientJSEntries()[0].absPath.replace(/^\/private/, '')).toBe(
|
|
81
|
+
logicalPath.replace(/^\/private/, '')
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
process.chdir(cwd);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
65
87
|
});
|
|
66
88
|
|
|
67
89
|
describe('extractExports', () => {
|
package/src/client/js.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { html } from '@hyperspan/html';
|
|
4
5
|
import { assetHash as assetHashFn } from '../utils';
|
|
@@ -69,6 +70,31 @@ export function resolveClientModulePath(modulePathResolved: string): string {
|
|
|
69
70
|
return modulePathResolved;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a client module path for hashing and bundling.
|
|
75
|
+
* Logical app-relative paths (e.g. app/client/foo.ts) hash consistently across
|
|
76
|
+
* Node and edge runtimes where absolute paths differ.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveClientModulePaths(modulePathResolved: string): {
|
|
79
|
+
hashKey: string;
|
|
80
|
+
absPath: string;
|
|
81
|
+
} {
|
|
82
|
+
const resolved = resolveClientModulePath(modulePathResolved).replace(/\\/g, '/');
|
|
83
|
+
|
|
84
|
+
if (!isAbsolute(resolved)) {
|
|
85
|
+
const hashKey = resolved;
|
|
86
|
+
if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
|
|
87
|
+
const candidate = join(process.cwd(), hashKey);
|
|
88
|
+
if (existsSync(candidate)) {
|
|
89
|
+
return { hashKey, absPath: candidate };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { hashKey, absPath: hashKey };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { hashKey: resolved, absPath: resolved };
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
export function getClientJSEntries(): ClientJSEntry[] {
|
|
73
99
|
return [...getClientJSRegistry().values()];
|
|
74
100
|
}
|
|
@@ -92,15 +118,16 @@ function registerClientJSEntry(entry: ClientJSEntry): void {
|
|
|
92
118
|
* and returns URLs / script-tag helpers for the browser.
|
|
93
119
|
*/
|
|
94
120
|
export async function buildClientJS(modulePathResolved: string): Promise<HS.ClientJSBuildResult> {
|
|
95
|
-
const absPath =
|
|
96
|
-
const hash = assetHashFn(
|
|
121
|
+
const { hashKey, absPath } = resolveClientModulePaths(modulePathResolved);
|
|
122
|
+
const hash = assetHashFn(hashKey);
|
|
97
123
|
const esmName = `client-${hash}`;
|
|
98
124
|
const publicPath = resolveImport(esmName) ?? `${JS_PUBLIC_PATH}/${esmName}.js`;
|
|
99
125
|
|
|
100
126
|
let exports = '* as _module';
|
|
101
127
|
let fnArgs = '_module';
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
const sourcePath = existsSync(absPath) ? absPath : null;
|
|
129
|
+
if (sourcePath) {
|
|
130
|
+
const source = readFileSync(sourcePath, 'utf-8');
|
|
104
131
|
const discovered = discoverClientExports(source);
|
|
105
132
|
exports = discovered.exports;
|
|
106
133
|
fnArgs = discovered.fnArgs;
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ export {
|
|
|
20
20
|
registerImport,
|
|
21
21
|
} from './client/manifest';
|
|
22
22
|
export { buildClientJS } from './client/js';
|
|
23
|
+
export { registerRouteModule, registerRouteModules } from './register-routes';
|
|
24
|
+
export type { RouteModuleEntry } from './register-routes';
|
|
23
25
|
export type { AssetManifest } from './client/manifest';
|
|
24
26
|
export { renderIsland } from './island';
|
|
25
27
|
export type { IslandRenderOptions } from './island';
|
|
26
|
-
export type { Hyperspan } from './types';
|
|
28
|
+
export type { Hyperspan, ServerCreateContext, DeployTarget } from './types';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getRunnableRoute } from './server';
|
|
2
|
+
import { isValidRoutePath, parsePath } from './utils';
|
|
3
|
+
import type { AssetManifest } from './client/manifest';
|
|
4
|
+
import type { Hyperspan as HS } from './types';
|
|
5
|
+
|
|
6
|
+
export type RouteModuleEntry = {
|
|
7
|
+
file: string;
|
|
8
|
+
mod: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a route or action module on a Hyperspan server.
|
|
13
|
+
* Shared by the CLI, Vite plugin, and the generated production server entry.
|
|
14
|
+
*/
|
|
15
|
+
export function registerRouteModule(
|
|
16
|
+
server: HS.Server,
|
|
17
|
+
relativeFilePath: string,
|
|
18
|
+
mod: unknown,
|
|
19
|
+
manifest?: AssetManifest
|
|
20
|
+
): HS.Route | null {
|
|
21
|
+
if (!isValidRoutePath(relativeFilePath)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const route = getRunnableRoute(mod);
|
|
26
|
+
const parsedPath = parsePath(relativeFilePath);
|
|
27
|
+
|
|
28
|
+
let path = parsedPath.path;
|
|
29
|
+
if (typeof route._path === 'function') {
|
|
30
|
+
const routePath = route._path();
|
|
31
|
+
if (routePath && routePath !== '/') path = routePath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!route._config.path) {
|
|
35
|
+
route._config.path = path;
|
|
36
|
+
if (parsedPath.params.length > 0) {
|
|
37
|
+
const params = route._config.params ?? {};
|
|
38
|
+
parsedPath.params.forEach((param) => {
|
|
39
|
+
params[param] = undefined;
|
|
40
|
+
});
|
|
41
|
+
route._config.params = params;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (manifest) {
|
|
46
|
+
const cssFiles = manifest.css?.[path] ?? manifest.css?.['*'];
|
|
47
|
+
if (cssFiles?.length) {
|
|
48
|
+
route._config.cssImports = cssFiles;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
server._routes.push(route);
|
|
53
|
+
return route;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerRouteModules(
|
|
57
|
+
server: HS.Server,
|
|
58
|
+
modules: RouteModuleEntry[],
|
|
59
|
+
manifest?: AssetManifest
|
|
60
|
+
): void {
|
|
61
|
+
for (const { file, mod } of modules) {
|
|
62
|
+
registerRouteModule(server, file, mod, manifest);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/server.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -63,10 +63,25 @@ export namespace Hyperspan {
|
|
|
63
63
|
disableStreaming?: DisableStreamingFn;
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
export type DeployTarget = 'node' | 'bun' | 'cloudflare';
|
|
67
|
+
|
|
68
|
+
export type ServerCreateContext = {
|
|
69
|
+
env: unknown;
|
|
70
|
+
};
|
|
71
|
+
|
|
66
72
|
export type Config = {
|
|
67
73
|
appDir: string;
|
|
68
74
|
publicDir: string;
|
|
69
75
|
plugins: Array<Hyperspan.Plugin>; // Loaders for client islands
|
|
76
|
+
/** Production runtime target. Controls the generated dist/server entry. */
|
|
77
|
+
deployTarget?: DeployTarget;
|
|
78
|
+
/**
|
|
79
|
+
* Called before the server is created, with platform bindings.
|
|
80
|
+
* - Vite/`hyperspan dev`: adapter `/dev` `resolveDevEnv(root)` when present; else `process.env`
|
|
81
|
+
* - Node/Bun production entry: `process.env`
|
|
82
|
+
* - Cloudflare Workers entry: Worker `env` bindings
|
|
83
|
+
*/
|
|
84
|
+
beforeServerCreate?: (ctx: ServerCreateContext) => void | Promise<void>;
|
|
70
85
|
// For customizing the routes and adding your own...
|
|
71
86
|
beforeRoutesAdded?: (server: Hyperspan.Server) => void;
|
|
72
87
|
afterRoutesAdded?: (server: Hyperspan.Server) => void;
|
|
@@ -387,6 +402,9 @@ export namespace Hyperspan {
|
|
|
387
402
|
}
|
|
388
403
|
}
|
|
389
404
|
|
|
405
|
+
export type DeployTarget = Hyperspan.DeployTarget;
|
|
406
|
+
export type ServerCreateContext = Hyperspan.ServerCreateContext;
|
|
407
|
+
|
|
390
408
|
declare global {
|
|
391
409
|
interface DocumentEventMap {
|
|
392
410
|
'hs:action:before-fetch': CustomEvent<Hyperspan.ActionFetchDetail>;
|