hadars 0.4.1 → 0.4.2
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/dist/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
- package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
- package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
- package/dist/cli.js +462 -496
- package/dist/cloudflare.cjs +11 -11
- package/dist/cloudflare.js +3 -3
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/lambda.cjs +11 -11
- package/dist/lambda.js +7 -7
- package/dist/loader.cjs +90 -54
- package/dist/slim-react/index.cjs +13 -13
- package/dist/slim-react/index.js +2 -2
- package/dist/slim-react/jsx-runtime.cjs +2 -4
- package/dist/slim-react/jsx-runtime.js +1 -1
- package/dist/ssr-render-worker.js +174 -161
- package/dist/ssr-watch.js +40 -74
- package/package.json +8 -10
- package/cli-lib.ts +0 -676
- package/cli.ts +0 -36
- package/index.ts +0 -17
- package/src/build.ts +0 -805
- package/src/cloudflare.ts +0 -140
- package/src/index.tsx +0 -41
- package/src/lambda.ts +0 -287
- package/src/slim-react/context.ts +0 -55
- package/src/slim-react/dispatcher.ts +0 -87
- package/src/slim-react/hooks.ts +0 -137
- package/src/slim-react/index.ts +0 -232
- package/src/slim-react/jsx-runtime.ts +0 -7
- package/src/slim-react/jsx.ts +0 -53
- package/src/slim-react/render.ts +0 -1101
- package/src/slim-react/renderContext.ts +0 -294
- package/src/slim-react/types.ts +0 -33
- package/src/source/context.ts +0 -113
- package/src/source/graphiql.ts +0 -101
- package/src/source/inference.ts +0 -260
- package/src/source/runner.ts +0 -138
- package/src/source/store.ts +0 -50
- package/src/ssr-render-worker.ts +0 -116
- package/src/ssr-watch.ts +0 -62
- package/src/static.ts +0 -109
- package/src/types/global.d.ts +0 -5
- package/src/types/hadars.ts +0 -350
- package/src/utils/Head.tsx +0 -462
- package/src/utils/clientScript.tsx +0 -71
- package/src/utils/cookies.ts +0 -16
- package/src/utils/loader.ts +0 -335
- package/src/utils/proxyHandler.tsx +0 -104
- package/src/utils/request.tsx +0 -9
- package/src/utils/response.tsx +0 -141
- package/src/utils/rspack.ts +0 -467
- package/src/utils/runtime.ts +0 -19
- package/src/utils/serve.ts +0 -155
- package/src/utils/ssrHandler.ts +0 -239
- package/src/utils/staticFile.ts +0 -43
- package/src/utils/template.html +0 -11
- package/src/utils/upgradeRequest.tsx +0 -19
package/src/cloudflare.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Workers adapter for hadars.
|
|
3
|
-
*
|
|
4
|
-
* After running `hadars build`, bundle your app with:
|
|
5
|
-
*
|
|
6
|
-
* hadars export cloudflare
|
|
7
|
-
*
|
|
8
|
-
* This produces a self-contained `cloudflare.mjs` that you deploy with:
|
|
9
|
-
*
|
|
10
|
-
* wrangler deploy
|
|
11
|
-
*
|
|
12
|
-
* Static assets (JS, CSS, fonts) under `.hadars/static/` must be served from
|
|
13
|
-
* R2 or another CDN — the Worker only handles HTML rendering. Route requests
|
|
14
|
-
* for static file extensions to R2 and everything else to the Worker.
|
|
15
|
-
*
|
|
16
|
-
* @example wrangler.toml
|
|
17
|
-
* name = "my-app"
|
|
18
|
-
* main = "cloudflare.mjs"
|
|
19
|
-
* compatibility_date = "2024-09-23"
|
|
20
|
-
* compatibility_flags = ["nodejs_compat"]
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import React from 'react';
|
|
24
|
-
import { parseRequest } from './utils/request';
|
|
25
|
-
import { createProxyHandler } from './utils/proxyHandler';
|
|
26
|
-
import { getReactResponse, buildHeadHtml } from './utils/response';
|
|
27
|
-
import { buildSsrHtml, makePrecontentHtmlGetter, createRenderCache } from './utils/ssrHandler';
|
|
28
|
-
import type { HadarsOptions, HadarsEntryModule, HadarsProps } from './types/hadars';
|
|
29
|
-
|
|
30
|
-
// ── Public types ──────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Pre-loaded SSR module and HTML template for single-file Cloudflare bundles
|
|
34
|
-
* produced by `hadars export cloudflare`. All I/O is eliminated at runtime —
|
|
35
|
-
* the Worker is fully self-contained.
|
|
36
|
-
*/
|
|
37
|
-
export interface CloudflareBundled {
|
|
38
|
-
/** The compiled SSR module — import it statically in your entry shim. */
|
|
39
|
-
ssrModule: HadarsEntryModule<any>;
|
|
40
|
-
/**
|
|
41
|
-
* The contents of `.hadars/static/out.html` — esbuild inlines this as a
|
|
42
|
-
* string when `hadars export cloudflare` runs.
|
|
43
|
-
*/
|
|
44
|
-
outHtml: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The shape of a Cloudflare Workers export object.
|
|
49
|
-
* Return this as the default export of your Worker entry file.
|
|
50
|
-
*/
|
|
51
|
-
export interface CloudflareHandler {
|
|
52
|
-
fetch(request: Request, env: unknown, ctx: unknown): Promise<Response>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Handler factory ───────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Creates a Cloudflare Workers handler from a hadars config and a pre-bundled
|
|
59
|
-
* SSR module. Use this as the default export of your Worker entry.
|
|
60
|
-
*
|
|
61
|
-
* Unlike the Lambda adapter, Cloudflare Workers receive a standard Web
|
|
62
|
-
* `Request` and return a standard `Response` — no event format conversion is
|
|
63
|
-
* required. Static assets must be routed to R2/CDN via wrangler rules; this
|
|
64
|
-
* Worker handles only HTML rendering and API routes.
|
|
65
|
-
*
|
|
66
|
-
* @example — generated entry shim (created by `hadars export cloudflare`)
|
|
67
|
-
* import * as ssrModule from './.hadars/index.ssr.js';
|
|
68
|
-
* import outHtml from './.hadars/static/out.html';
|
|
69
|
-
* import { createCloudflareHandler } from 'hadars/cloudflare';
|
|
70
|
-
* import config from './hadars.config';
|
|
71
|
-
* export default createCloudflareHandler(config, { ssrModule, outHtml });
|
|
72
|
-
*/
|
|
73
|
-
export function createCloudflareHandler(
|
|
74
|
-
options: HadarsOptions,
|
|
75
|
-
bundled: CloudflareBundled,
|
|
76
|
-
): CloudflareHandler {
|
|
77
|
-
const fetchHandler = options.fetch;
|
|
78
|
-
const handleProxy = createProxyHandler(options);
|
|
79
|
-
const getPrecontentHtml = makePrecontentHtmlGetter(Promise.resolve(bundled.outHtml));
|
|
80
|
-
const { ssrModule } = bundled;
|
|
81
|
-
|
|
82
|
-
const runHandler = async (req: Request): Promise<Response> => {
|
|
83
|
-
const request = parseRequest(req);
|
|
84
|
-
|
|
85
|
-
if (fetchHandler) {
|
|
86
|
-
const res = await fetchHandler(request);
|
|
87
|
-
if (res) return res;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const proxied = await handleProxy(request);
|
|
91
|
-
if (proxied) return proxied;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const { default: Component, getInitProps, getFinalProps } = ssrModule;
|
|
95
|
-
|
|
96
|
-
const { head, status, getAppBody, finalize } = await getReactResponse(request, {
|
|
97
|
-
document: {
|
|
98
|
-
body: Component as React.FC<HadarsProps<object>>,
|
|
99
|
-
lang: 'en',
|
|
100
|
-
getInitProps,
|
|
101
|
-
getFinalProps,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Data-only requests from client-side navigation.
|
|
106
|
-
if (request.headers.get('Accept') === 'application/json') {
|
|
107
|
-
const { clientProps } = await finalize();
|
|
108
|
-
const serverData = (clientProps as any).__serverData ?? {};
|
|
109
|
-
return new Response(JSON.stringify({ serverData }), {
|
|
110
|
-
status,
|
|
111
|
-
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const bodyHtml = await getAppBody();
|
|
116
|
-
const { clientProps } = await finalize();
|
|
117
|
-
const headHtml = buildHeadHtml(head);
|
|
118
|
-
const html = await buildSsrHtml(bodyHtml, clientProps, headHtml, getPrecontentHtml);
|
|
119
|
-
|
|
120
|
-
return new Response(html, {
|
|
121
|
-
status,
|
|
122
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
123
|
-
});
|
|
124
|
-
} catch (err: any) {
|
|
125
|
-
console.error('[hadars] SSR render error:', err);
|
|
126
|
-
options.onError?.(err, request)?.catch?.(() => {});
|
|
127
|
-
return new Response('Internal Server Error', { status: 500 });
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const finalFetch = options.cache
|
|
132
|
-
? createRenderCache(options.cache, (req) => runHandler(req))
|
|
133
|
-
: (req: Request) => runHandler(req);
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
fetch: async (request: Request, _env: unknown, ctx: unknown): Promise<Response> => {
|
|
137
|
-
return (await finalFetch(request, ctx)) ?? new Response('Not Found', { status: 404 });
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
package/src/index.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
HadarsOptions,
|
|
3
|
-
HadarsProps,
|
|
4
|
-
HadarsRequest,
|
|
5
|
-
HadarsGetFinalProps,
|
|
6
|
-
HadarsGetInitialProps,
|
|
7
|
-
HadarsGetClientProps,
|
|
8
|
-
HadarsEntryModule,
|
|
9
|
-
HadarsApp,
|
|
10
|
-
HadarsStaticContext,
|
|
11
|
-
GraphQLExecutor,
|
|
12
|
-
HadarsSourceEntry,
|
|
13
|
-
HadarsDocumentNode,
|
|
14
|
-
} from "./types/hadars";
|
|
15
|
-
export { Head as HadarsHead, useServerData, useGraphQL, initServerDataCache } from './utils/Head';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Dynamically loads a module with target-aware behaviour:
|
|
19
|
-
*
|
|
20
|
-
* - **Browser** (after loader transform): becomes `import('./path')`, which
|
|
21
|
-
* rspack splits into a separate chunk for true code splitting.
|
|
22
|
-
* - **SSR** (after loader transform): becomes
|
|
23
|
-
* `Promise.resolve(require('./path'))`, which rspack bundles statically.
|
|
24
|
-
*
|
|
25
|
-
* The hadars rspack loader must be active for the transform to apply.
|
|
26
|
-
* This runtime fallback uses a plain dynamic import (no code splitting).
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* // Code-split React component:
|
|
30
|
-
* const MyComp = React.lazy(() => loadModule('./MyComp'));
|
|
31
|
-
*
|
|
32
|
-
* // Dynamic data:
|
|
33
|
-
* const { default: fn } = await loadModule<typeof import('./util')>('./util');
|
|
34
|
-
*/
|
|
35
|
-
export function loadModule<T = any>(path: string): Promise<T> {
|
|
36
|
-
// webpackIgnore suppresses rspack's "critical dependency" warning for the
|
|
37
|
-
// variable import. This body is a fallback only — the hadars loader
|
|
38
|
-
// transforms every loadModule('./...') call site at compile time, so this
|
|
39
|
-
// function is never invoked in a bundled build.
|
|
40
|
-
return import(/* webpackIgnore: true */ path) as Promise<T>;
|
|
41
|
-
}
|
package/src/lambda.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AWS Lambda adapter for hadars.
|
|
3
|
-
*
|
|
4
|
-
* After running `hadars build`, import this in your Lambda entry file:
|
|
5
|
-
*
|
|
6
|
-
* // lambda.ts
|
|
7
|
-
* import { createLambdaHandler } from 'hadars/lambda';
|
|
8
|
-
* import config from './hadars.config';
|
|
9
|
-
* export const handler = createLambdaHandler(config);
|
|
10
|
-
*
|
|
11
|
-
* The returned handler speaks the API Gateway HTTP API (v2) payload format.
|
|
12
|
-
* API Gateway REST API (v1) events are also accepted.
|
|
13
|
-
*
|
|
14
|
-
* Static assets (JS, CSS, fonts) are served directly from the bundled
|
|
15
|
-
* `.hadars/static/` directory. For production use, front the Lambda with
|
|
16
|
-
* CloudFront and route static paths to an S3 origin instead.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import React from 'react';
|
|
20
|
-
import pathMod from 'node:path';
|
|
21
|
-
import { pathToFileURL } from 'node:url';
|
|
22
|
-
import fs from 'node:fs/promises';
|
|
23
|
-
import { createProxyHandler } from './utils/proxyHandler';
|
|
24
|
-
import { parseRequest } from './utils/request';
|
|
25
|
-
import { tryServeFile } from './utils/staticFile';
|
|
26
|
-
import { getReactResponse, buildHeadHtml } from './utils/response';
|
|
27
|
-
import { buildSsrResponse, buildSsrHtml, makePrecontentHtmlGetter, createRenderCache } from './utils/ssrHandler';
|
|
28
|
-
import type { HadarsOptions, HadarsEntryModule, HadarsProps } from './types/hadars';
|
|
29
|
-
|
|
30
|
-
// ── Lambda event / response types ────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/** API Gateway HTTP API (v2) event. */
|
|
33
|
-
export interface APIGatewayV2Event {
|
|
34
|
-
version: '2.0';
|
|
35
|
-
routeKey: string;
|
|
36
|
-
rawPath: string;
|
|
37
|
-
rawQueryString: string;
|
|
38
|
-
cookies?: string[];
|
|
39
|
-
headers: Record<string, string>;
|
|
40
|
-
requestContext: {
|
|
41
|
-
http: { method: string };
|
|
42
|
-
domainName: string;
|
|
43
|
-
};
|
|
44
|
-
body?: string;
|
|
45
|
-
isBase64Encoded: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** API Gateway REST API (v1) event. */
|
|
49
|
-
export interface APIGatewayV1Event {
|
|
50
|
-
httpMethod: string;
|
|
51
|
-
path: string;
|
|
52
|
-
queryStringParameters?: Record<string, string> | null;
|
|
53
|
-
headers?: Record<string, string> | null;
|
|
54
|
-
body?: string | null;
|
|
55
|
-
isBase64Encoded: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type APIGatewayEvent = APIGatewayV2Event | APIGatewayV1Event;
|
|
59
|
-
|
|
60
|
-
export interface LambdaResponse {
|
|
61
|
-
statusCode: number;
|
|
62
|
-
headers: Record<string, string>;
|
|
63
|
-
body: string;
|
|
64
|
-
isBase64Encoded: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type LambdaHandler = (event: APIGatewayEvent) => Promise<LambdaResponse>;
|
|
68
|
-
|
|
69
|
-
// ── Event → Request ───────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
function eventToRequest(event: APIGatewayEvent): Request {
|
|
72
|
-
let method: string;
|
|
73
|
-
let path: string;
|
|
74
|
-
let queryString: string;
|
|
75
|
-
let headers: Record<string, string>;
|
|
76
|
-
let rawBody: string | undefined | null;
|
|
77
|
-
let isBase64: boolean;
|
|
78
|
-
let host: string;
|
|
79
|
-
|
|
80
|
-
if ('version' in event && event.version === '2.0') {
|
|
81
|
-
method = event.requestContext.http.method;
|
|
82
|
-
path = event.rawPath;
|
|
83
|
-
queryString = event.rawQueryString;
|
|
84
|
-
headers = { ...event.headers };
|
|
85
|
-
// v2 puts cookies in a separate array; merge them back into the header
|
|
86
|
-
if (event.cookies?.length) headers['cookie'] = event.cookies.join('; ');
|
|
87
|
-
rawBody = event.body;
|
|
88
|
-
isBase64 = event.isBase64Encoded;
|
|
89
|
-
host = event.requestContext.domainName;
|
|
90
|
-
} else {
|
|
91
|
-
const e = event as APIGatewayV1Event;
|
|
92
|
-
method = e.httpMethod;
|
|
93
|
-
path = e.path;
|
|
94
|
-
const qs = e.queryStringParameters;
|
|
95
|
-
queryString = qs ? new URLSearchParams(qs as Record<string, string>).toString() : '';
|
|
96
|
-
headers = (e.headers as Record<string, string>) ?? {};
|
|
97
|
-
rawBody = e.body;
|
|
98
|
-
isBase64 = e.isBase64Encoded;
|
|
99
|
-
host = headers['host'] ?? 'lambda';
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const url = `https://${host}${path}${queryString ? '?' + queryString : ''}`;
|
|
103
|
-
let body: BodyInit | undefined;
|
|
104
|
-
if (rawBody) body = isBase64 ? Buffer.from(rawBody, 'base64') : rawBody;
|
|
105
|
-
|
|
106
|
-
return new Request(url, {
|
|
107
|
-
method,
|
|
108
|
-
headers,
|
|
109
|
-
body: ['GET', 'HEAD'].includes(method) ? undefined : body,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Response → Lambda ─────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
const TEXT_RE = /\b(?:text\/|application\/(?:json|javascript|xml)|image\/svg\+xml)/;
|
|
116
|
-
|
|
117
|
-
async function responseToLambda(response: Response): Promise<LambdaResponse> {
|
|
118
|
-
const headers: Record<string, string> = {};
|
|
119
|
-
response.headers.forEach((v, k) => { headers[k] = v; });
|
|
120
|
-
|
|
121
|
-
const contentType = response.headers.get('Content-Type') ?? '';
|
|
122
|
-
// A Content-Encoding header means the body is already compressed (binary).
|
|
123
|
-
// Calling .text() on gzip/br/deflate bytes would corrupt the data, so always
|
|
124
|
-
// treat compressed responses as binary regardless of Content-Type.
|
|
125
|
-
const encoding = response.headers.get('Content-Encoding') ?? '';
|
|
126
|
-
if (!encoding && TEXT_RE.test(contentType)) {
|
|
127
|
-
return {
|
|
128
|
-
statusCode: response.status,
|
|
129
|
-
headers,
|
|
130
|
-
body: await response.text(),
|
|
131
|
-
isBase64Encoded: false,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Binary response (images, fonts, pre-compressed assets, gzip cache hits, etc.)
|
|
136
|
-
return {
|
|
137
|
-
statusCode: response.status,
|
|
138
|
-
headers,
|
|
139
|
-
body: Buffer.from(await response.arrayBuffer()).toString('base64'),
|
|
140
|
-
isBase64Encoded: true,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ── Public API ────────────────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
const HadarsFolder = './.hadars';
|
|
147
|
-
const StaticPath = `${HadarsFolder}/static`;
|
|
148
|
-
const SSR_FILENAME = 'index.ssr.js';
|
|
149
|
-
const noopCtx = { upgrade: () => false };
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Pre-loaded module and HTML for single-file Lambda bundles created by
|
|
153
|
-
* `hadars export lambda`. Passing this bypasses all runtime file I/O so
|
|
154
|
-
* the handler works without a `.hadars/` directory on disk.
|
|
155
|
-
*/
|
|
156
|
-
export interface LambdaBundled {
|
|
157
|
-
/** The compiled SSR module — import it statically in your entry shim. */
|
|
158
|
-
ssrModule: HadarsEntryModule<any>;
|
|
159
|
-
/**
|
|
160
|
-
* The contents of `.hadars/static/out.html` — load it as a text string
|
|
161
|
-
* at build time (esbuild's `--loader:.html=text` does this automatically
|
|
162
|
-
* when `hadars export lambda` runs).
|
|
163
|
-
*/
|
|
164
|
-
outHtml: string;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Creates an AWS Lambda handler from a hadars config.
|
|
169
|
-
*
|
|
170
|
-
* Must be called after `hadars build` has produced the `.hadars/` output
|
|
171
|
-
* directory. The handler is stateless — Lambda can reuse the same instance
|
|
172
|
-
* across invocations; the SSR module and HTML template are cached in memory
|
|
173
|
-
* after the first warm invocation.
|
|
174
|
-
*
|
|
175
|
-
* Pass a {@link LambdaBundled} object as the second argument when using
|
|
176
|
-
* `hadars export lambda` to produce a single-file bundle — in that mode all
|
|
177
|
-
* I/O is eliminated and the handler is fully self-contained.
|
|
178
|
-
*
|
|
179
|
-
* @example — standard (file-based) deployment
|
|
180
|
-
* export const handler = createLambdaHandler(config);
|
|
181
|
-
*
|
|
182
|
-
* @example — single-file bundle (generated entry shim)
|
|
183
|
-
* import * as ssrModule from './.hadars/index.ssr.js';
|
|
184
|
-
* import outHtml from './.hadars/static/out.html';
|
|
185
|
-
* export const handler = createLambdaHandler(config, { ssrModule, outHtml });
|
|
186
|
-
*/
|
|
187
|
-
export function createLambdaHandler(options: HadarsOptions, bundled?: LambdaBundled): LambdaHandler {
|
|
188
|
-
const cwd = process.cwd();
|
|
189
|
-
const fetchHandler = options.fetch;
|
|
190
|
-
const handleProxy = createProxyHandler(options);
|
|
191
|
-
|
|
192
|
-
const getPrecontentHtml = bundled
|
|
193
|
-
? makePrecontentHtmlGetter(Promise.resolve(bundled.outHtml))
|
|
194
|
-
: makePrecontentHtmlGetter(fs.readFile(pathMod.join(cwd, StaticPath, 'out.html'), 'utf-8'));
|
|
195
|
-
|
|
196
|
-
// Start loading the SSR module immediately — during Lambda's init phase this
|
|
197
|
-
// is "free" time not billed against request latency. Lazy loading would
|
|
198
|
-
// push the module parse cost onto the first request.
|
|
199
|
-
const ssrModulePromise: Promise<HadarsEntryModule<any>> = bundled
|
|
200
|
-
? Promise.resolve(bundled.ssrModule)
|
|
201
|
-
: import(pathToFileURL(pathMod.resolve(cwd, HadarsFolder, SSR_FILENAME)).href) as Promise<HadarsEntryModule<any>>;
|
|
202
|
-
const getSsrModule = () => ssrModulePromise;
|
|
203
|
-
|
|
204
|
-
const runHandler = async (req: Request): Promise<Response> => {
|
|
205
|
-
const request = parseRequest(req);
|
|
206
|
-
|
|
207
|
-
if (fetchHandler) {
|
|
208
|
-
const res = await fetchHandler(request);
|
|
209
|
-
if (res) return res;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const proxied = await handleProxy(request);
|
|
213
|
-
if (proxied) return proxied;
|
|
214
|
-
|
|
215
|
-
// parseRequest already extracted pathname — no need to re-parse the URL.
|
|
216
|
-
const urlPath = request.pathname;
|
|
217
|
-
|
|
218
|
-
if (!bundled) {
|
|
219
|
-
// File-based deployment: serve static assets from disk.
|
|
220
|
-
// Run the three candidate lookups in parallel to avoid sequential stat() syscalls.
|
|
221
|
-
const projectStaticPath = pathMod.resolve(cwd, 'static');
|
|
222
|
-
const routeClean = urlPath.replace(/(^\/|\/$)/g, '');
|
|
223
|
-
const [staticRes, projectRes, routeRes] = await Promise.all([
|
|
224
|
-
tryServeFile(pathMod.join(cwd, StaticPath, urlPath)),
|
|
225
|
-
tryServeFile(pathMod.join(projectStaticPath, urlPath)),
|
|
226
|
-
routeClean
|
|
227
|
-
? tryServeFile(pathMod.join(cwd, StaticPath, routeClean, 'index.html'))
|
|
228
|
-
: Promise.resolve(null),
|
|
229
|
-
]);
|
|
230
|
-
if (staticRes) return staticRes;
|
|
231
|
-
if (projectRes) return projectRes;
|
|
232
|
-
if (routeRes) return routeRes;
|
|
233
|
-
}
|
|
234
|
-
// bundled mode: static assets are not served from Lambda — route them to S3/CDN.
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const {
|
|
238
|
-
default: Component,
|
|
239
|
-
getInitProps,
|
|
240
|
-
getFinalProps,
|
|
241
|
-
} = await getSsrModule();
|
|
242
|
-
|
|
243
|
-
const { head, status, getAppBody, finalize } = await getReactResponse(request, {
|
|
244
|
-
document: {
|
|
245
|
-
body: Component as React.FC<HadarsProps<object>>,
|
|
246
|
-
lang: 'en',
|
|
247
|
-
getInitProps,
|
|
248
|
-
getFinalProps,
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (request.headers.get('Accept') === 'application/json') {
|
|
253
|
-
const { clientProps } = await finalize();
|
|
254
|
-
const serverData = (clientProps as any).__serverData ?? {};
|
|
255
|
-
return new Response(JSON.stringify({ serverData }), {
|
|
256
|
-
status,
|
|
257
|
-
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Build the HTML string directly — avoids creating a ReadableStream
|
|
262
|
-
// that would immediately be drained by the Lambda response serialiser.
|
|
263
|
-
const bodyHtml = await getAppBody();
|
|
264
|
-
const { clientProps } = await finalize();
|
|
265
|
-
const headHtml = buildHeadHtml(head);
|
|
266
|
-
const html = await buildSsrHtml(bodyHtml, clientProps, headHtml, getPrecontentHtml);
|
|
267
|
-
return new Response(html, {
|
|
268
|
-
status,
|
|
269
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
270
|
-
});
|
|
271
|
-
} catch (err: any) {
|
|
272
|
-
console.error('[hadars] SSR render error:', err);
|
|
273
|
-
options.onError?.(err, request)?.catch?.(() => {});
|
|
274
|
-
return new Response('Internal Server Error', { status: 500 });
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const finalHandler: (req: Request, ctx: any) => Promise<Response | undefined> = options.cache
|
|
279
|
-
? createRenderCache(options.cache, (req) => runHandler(req))
|
|
280
|
-
: (req: Request) => runHandler(req);
|
|
281
|
-
|
|
282
|
-
return async (event: APIGatewayEvent): Promise<LambdaResponse> => {
|
|
283
|
-
const req = eventToRequest(event);
|
|
284
|
-
const response = (await finalHandler(req, noopCtx)) ?? new Response('Not Found', { status: 404 });
|
|
285
|
-
return responseToLambda(response);
|
|
286
|
-
};
|
|
287
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { SlimNode } from "./types";
|
|
2
|
-
import { getContextValue } from "./renderContext";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Minimal Context implementation for SSR.
|
|
6
|
-
*
|
|
7
|
-
* Because SSR is single-pass and synchronous within each component,
|
|
8
|
-
* we just track the "current" value on the context object and
|
|
9
|
-
* save / restore around Provider renders (handled by the renderer).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface Context<T> {
|
|
13
|
-
_defaultValue: T;
|
|
14
|
-
_currentValue: T; // kept for external compat (real React contexts passed to useContext)
|
|
15
|
-
Provider: ContextProvider<T>;
|
|
16
|
-
Consumer: (props: { children: (value: T) => SlimNode }) => SlimNode;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type ContextProvider<T> = ((props: {
|
|
20
|
-
value: T;
|
|
21
|
-
children?: SlimNode;
|
|
22
|
-
}) => SlimNode) & {
|
|
23
|
-
_context: Context<T>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export function createContext<T>(defaultValue: T): Context<T> {
|
|
27
|
-
const context: Context<T> = {
|
|
28
|
-
_defaultValue: defaultValue,
|
|
29
|
-
_currentValue: defaultValue,
|
|
30
|
-
Provider: null!,
|
|
31
|
-
Consumer: null!,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Provider is a function component recognised by the renderer.
|
|
35
|
-
// The `_context` tag tells the renderer to push / pop the value.
|
|
36
|
-
const Provider = function ContextProvider({
|
|
37
|
-
children,
|
|
38
|
-
}: {
|
|
39
|
-
value: T;
|
|
40
|
-
children?: SlimNode;
|
|
41
|
-
}): SlimNode {
|
|
42
|
-
return children ?? null;
|
|
43
|
-
} as unknown as ContextProvider<T>;
|
|
44
|
-
|
|
45
|
-
Provider._context = context;
|
|
46
|
-
context.Provider = Provider;
|
|
47
|
-
|
|
48
|
-
context.Consumer = ({ children }) => {
|
|
49
|
-
return (children as unknown as (value: T) => SlimNode)(
|
|
50
|
-
getContextValue<T>(context),
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
return context;
|
|
55
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React dispatcher shim for slim-react SSR.
|
|
3
|
-
*
|
|
4
|
-
* During a slim-react render, `React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H`
|
|
5
|
-
* is null, so any component that calls `React.useId()` (or another hook) via
|
|
6
|
-
* React's own package will hit `resolveDispatcher()` → null → error.
|
|
7
|
-
*
|
|
8
|
-
* We install a minimal dispatcher object for the duration of each component
|
|
9
|
-
* call so that `React.useId()` routes through slim-react's tree-aware
|
|
10
|
-
* `makeId()`. All other hooks already have working SSR stubs in hooks.ts;
|
|
11
|
-
* they are forwarded here so libraries that call them via `React.*` also work.
|
|
12
|
-
*
|
|
13
|
-
* In the Rspack SSR bundle `react` is aliased to `slim-react`, which does NOT
|
|
14
|
-
* export `__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE`.
|
|
15
|
-
* Accessing that property via a namespace import (`import * as`) always returns
|
|
16
|
-
* `undefined` safely — `import default from "react"` would crash via interop
|
|
17
|
-
* because Rspack compiles it as `require("react").default`, and slim-react has
|
|
18
|
-
* no `default` export, so `.default` itself is `undefined` and the subsequent
|
|
19
|
-
* property access throws.
|
|
20
|
-
*
|
|
21
|
-
* With the namespace import, `_internals` is `undefined` in the SSR bundle and
|
|
22
|
-
* the install/restore functions become no-ops, which is correct: the SSR bundle
|
|
23
|
-
* already routes all `React.*` hook calls to slim-react's own implementations.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { makeId, getContextValue } from "./renderContext";
|
|
27
|
-
import {
|
|
28
|
-
useState, useReducer, useEffect, useLayoutEffect, useInsertionEffect,
|
|
29
|
-
useRef, useMemo, useCallback, useDebugValue, useImperativeHandle,
|
|
30
|
-
useSyncExternalStore, useTransition, useDeferredValue,
|
|
31
|
-
useOptimistic, useActionState, use,
|
|
32
|
-
} from "./hooks";
|
|
33
|
-
|
|
34
|
-
// Use namespace import so that when `react` is aliased to slim-react in the
|
|
35
|
-
// Rspack SSR bundle, `ReactNS` is always an object (never undefined), and
|
|
36
|
-
// accessing a missing property returns `undefined` rather than throwing.
|
|
37
|
-
import * as ReactNS from "react";
|
|
38
|
-
|
|
39
|
-
// React 19 exposes its shared internals under this key.
|
|
40
|
-
// Bracket notation prevents rspack from treating this as a named-export access
|
|
41
|
-
// on the "react" module (which would produce an ESModulesLinkingWarning).
|
|
42
|
-
const _reactInternalsKey = "__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE";
|
|
43
|
-
const _internals: { H: object | null } | undefined =
|
|
44
|
-
(ReactNS as any)[_reactInternalsKey];
|
|
45
|
-
|
|
46
|
-
// The dispatcher object we install. We keep a stable reference so the same
|
|
47
|
-
// object is reused across every component call.
|
|
48
|
-
const slimDispatcher: Record<string, unknown> = {
|
|
49
|
-
useId: makeId,
|
|
50
|
-
readContext: (ctx: any) => getContextValue(ctx),
|
|
51
|
-
useContext: (ctx: any) => getContextValue(ctx),
|
|
52
|
-
useState,
|
|
53
|
-
useReducer,
|
|
54
|
-
useEffect,
|
|
55
|
-
useLayoutEffect,
|
|
56
|
-
useInsertionEffect,
|
|
57
|
-
useRef,
|
|
58
|
-
useMemo,
|
|
59
|
-
useCallback,
|
|
60
|
-
useDebugValue,
|
|
61
|
-
useImperativeHandle,
|
|
62
|
-
useSyncExternalStore,
|
|
63
|
-
useTransition,
|
|
64
|
-
useDeferredValue,
|
|
65
|
-
useOptimistic,
|
|
66
|
-
useActionState,
|
|
67
|
-
use,
|
|
68
|
-
// React internals that compiled output may call
|
|
69
|
-
useMemoCache: (size: number) => new Array(size).fill(undefined),
|
|
70
|
-
useCacheRefresh: () => () => {},
|
|
71
|
-
useHostTransitionStatus: () => false,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Install the slim dispatcher and return the previous value.
|
|
76
|
-
* Call `restoreDispatcher(prev)` when the component finishes.
|
|
77
|
-
*/
|
|
78
|
-
export function installDispatcher(): object | null {
|
|
79
|
-
if (!_internals) return null;
|
|
80
|
-
const prev = _internals.H;
|
|
81
|
-
_internals.H = slimDispatcher;
|
|
82
|
-
return prev;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function restoreDispatcher(prev: object | null): void {
|
|
86
|
-
if (_internals) _internals.H = prev;
|
|
87
|
-
}
|