@hyperspan/framework 2.0.0-alpha.2 → 2.0.0-alpha.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/framework",
3
- "version": "2.0.0-alpha.2",
3
+ "version": "2.0.0-alpha.3",
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.2",
103
+ "@hyperspan/html": "^2.0.0-alpha.3",
100
104
  "debug": "^4.4.3",
101
105
  "isbot": "^5.1.32",
102
106
  "zod": "^4.4.3"
@@ -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 = resolveClientModulePath(modulePathResolved);
96
- const hash = assetHashFn(absPath);
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
- if (existsSync(absPath)) {
103
- const source = readFileSync(absPath, 'utf-8');
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
@@ -51,6 +51,7 @@ export function createConfig(config: Partial<HS.Config> = {}): HS.Config {
51
51
  appDir: './app',
52
52
  publicDir: './public',
53
53
  plugins: [],
54
+ deployTarget: 'node',
54
55
  responseOptions: {
55
56
  disableStreaming: hyperspanDisableStreaming,
56
57
  },
package/src/types.ts CHANGED
@@ -63,10 +63,20 @@ 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
+ /** Called once before the production server is created, with platform bindings. */
79
+ beforeServerCreate?: (ctx: ServerCreateContext) => void | Promise<void>;
70
80
  // For customizing the routes and adding your own...
71
81
  beforeRoutesAdded?: (server: Hyperspan.Server) => void;
72
82
  afterRoutesAdded?: (server: Hyperspan.Server) => void;
@@ -387,6 +397,9 @@ export namespace Hyperspan {
387
397
  }
388
398
  }
389
399
 
400
+ export type DeployTarget = Hyperspan.DeployTarget;
401
+ export type ServerCreateContext = Hyperspan.ServerCreateContext;
402
+
390
403
  declare global {
391
404
  interface DocumentEventMap {
392
405
  'hs:action:before-fetch': CustomEvent<Hyperspan.ActionFetchDetail>;