@nexus_js/server 0.6.0
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/actions.d.ts +158 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +396 -0
- package/dist/actions.js.map +1 -0
- package/dist/context.d.ts +41 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +68 -0
- package/dist/context.js.map +1 -0
- package/dist/csrf.d.ts +56 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +153 -0
- package/dist/csrf.js.map +1 -0
- package/dist/dev-assets.d.ts +31 -0
- package/dist/dev-assets.d.ts.map +1 -0
- package/dist/dev-assets.js +198 -0
- package/dist/dev-assets.js.map +1 -0
- package/dist/error-boundary.d.ts +87 -0
- package/dist/error-boundary.d.ts.map +1 -0
- package/dist/error-boundary.js +181 -0
- package/dist/error-boundary.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +277 -0
- package/dist/index.js.map +1 -0
- package/dist/load-module.d.ts +26 -0
- package/dist/load-module.d.ts.map +1 -0
- package/dist/load-module.js +288 -0
- package/dist/load-module.js.map +1 -0
- package/dist/logger.d.ts +63 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +158 -0
- package/dist/logger.js.map +1 -0
- package/dist/navigate.d.ts +21 -0
- package/dist/navigate.d.ts.map +1 -0
- package/dist/navigate.js +45 -0
- package/dist/navigate.js.map +1 -0
- package/dist/rate-limit.d.ts +71 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +136 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/renderer.d.ts +92 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +386 -0
- package/dist/renderer.js.map +1 -0
- package/dist/streaming.d.ts +98 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +216 -0
- package/dist/streaming.js.map +1 -0
- package/package.json +68 -0
package/dist/context.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Request Context — passed to every page, layout and server action.
|
|
3
|
+
* Inspired by SvelteKit's RequestEvent and Next.js's Request/Response pattern.
|
|
4
|
+
*/
|
|
5
|
+
/** Internal redirect signal */
|
|
6
|
+
export class RedirectSignal {
|
|
7
|
+
location;
|
|
8
|
+
status;
|
|
9
|
+
constructor(location, status) {
|
|
10
|
+
this.location = location;
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Internal not-found signal */
|
|
15
|
+
export class NotFoundSignal {
|
|
16
|
+
}
|
|
17
|
+
export function createContext(request, params = {}) {
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
const responseHeaders = new Headers();
|
|
20
|
+
const cookies = parseCookies(request.headers.get('cookie') ?? '');
|
|
21
|
+
return {
|
|
22
|
+
request,
|
|
23
|
+
params,
|
|
24
|
+
url,
|
|
25
|
+
headers: request.headers,
|
|
26
|
+
locals: {},
|
|
27
|
+
setHeader(key, value) {
|
|
28
|
+
responseHeaders.set(key, value);
|
|
29
|
+
},
|
|
30
|
+
setCookie(name, value, opts = {}) {
|
|
31
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
32
|
+
if (opts.path)
|
|
33
|
+
parts.push(`Path=${opts.path}`);
|
|
34
|
+
if (opts.domain)
|
|
35
|
+
parts.push(`Domain=${opts.domain}`);
|
|
36
|
+
if (opts.maxAge !== undefined)
|
|
37
|
+
parts.push(`Max-Age=${opts.maxAge}`);
|
|
38
|
+
if (opts.expires)
|
|
39
|
+
parts.push(`Expires=${opts.expires.toUTCString()}`);
|
|
40
|
+
if (opts.httpOnly)
|
|
41
|
+
parts.push('HttpOnly');
|
|
42
|
+
if (opts.secure)
|
|
43
|
+
parts.push('Secure');
|
|
44
|
+
if (opts.sameSite)
|
|
45
|
+
parts.push(`SameSite=${opts.sameSite}`);
|
|
46
|
+
responseHeaders.append('Set-Cookie', parts.join('; '));
|
|
47
|
+
},
|
|
48
|
+
getCookie(name) {
|
|
49
|
+
return cookies[name];
|
|
50
|
+
},
|
|
51
|
+
redirect(location, status = 302) {
|
|
52
|
+
throw new RedirectSignal(location, status);
|
|
53
|
+
},
|
|
54
|
+
notFound() {
|
|
55
|
+
throw new NotFoundSignal();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function parseCookies(header) {
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const part of header.split(';')) {
|
|
62
|
+
const [key, ...vals] = part.trim().split('=');
|
|
63
|
+
if (key)
|
|
64
|
+
result[key.trim()] = decodeURIComponent(vals.join('=').trim());
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/dist/csrf.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Action Integrity — Anti-CSRF & Anti-Replay Protection.
|
|
3
|
+
*
|
|
4
|
+
* Every Server Action invocation is protected by an ephemeral, HMAC-signed,
|
|
5
|
+
* single-use token. The flow:
|
|
6
|
+
*
|
|
7
|
+
* 1. Server renders an island → calls generateActionToken(sessionId, action)
|
|
8
|
+
* 2. Token is embedded in the HTML as a data attribute on the island
|
|
9
|
+
* 3. Client POSTs the action with the token in x-nexus-action-token header
|
|
10
|
+
* 4. Server calls validateActionToken() — verifies HMAC, session, expiry, and
|
|
11
|
+
* "burns" the token (single-use). A replay of the same token → blocked.
|
|
12
|
+
*
|
|
13
|
+
* Properties:
|
|
14
|
+
* - HMAC-SHA256 signed with the app secret (tamper-proof)
|
|
15
|
+
* - Bound to sessionId + actionName (no cross-action reuse)
|
|
16
|
+
* - Expires after ACTION_TOKEN_TTL (default 15 min)
|
|
17
|
+
* - Single-use: consumed on first valid use (replay attack prevention)
|
|
18
|
+
* - Constant-time comparison to prevent timing attacks
|
|
19
|
+
*/
|
|
20
|
+
/** Request header name for the action token. */
|
|
21
|
+
export declare const ACTION_TOKEN_HEADER = "x-nexus-action-token";
|
|
22
|
+
export interface TokenValidationResult {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
reason?: string;
|
|
25
|
+
/** True if the token was valid but has been consumed (replay attempt). */
|
|
26
|
+
replayed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generates a signed, single-use action token.
|
|
30
|
+
*
|
|
31
|
+
* @param sessionId Unique identifier for the current user session (cookie, JWT sub, etc.)
|
|
32
|
+
* @param actionName The action being authorized (e.g. 'capture', 'update-favorites')
|
|
33
|
+
* @param secret App-level secret — should come from process.env.NEXUS_SECRET
|
|
34
|
+
* @returns base64url-encoded token safe to embed in HTML
|
|
35
|
+
*/
|
|
36
|
+
export declare function generateActionToken(sessionId: string, actionName: string, secret: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Validates and consumes an action token. Calling this twice with the same
|
|
39
|
+
* token will return `{ valid: false, replayed: true }` on the second call.
|
|
40
|
+
*
|
|
41
|
+
* @param token Token from x-nexus-action-token header
|
|
42
|
+
* @param sessionId Current user's session ID
|
|
43
|
+
* @param actionName The action being invoked
|
|
44
|
+
* @param secret Same secret used during generation
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateActionToken(token: string, sessionId: string, actionName: string, secret: string): TokenValidationResult;
|
|
47
|
+
/**
|
|
48
|
+
* Extracts session ID from a request using common patterns.
|
|
49
|
+
* Override with your own session logic in production.
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractSessionId(request: Request): string;
|
|
52
|
+
/**
|
|
53
|
+
* Generates a session ID that is safe to store in a cookie.
|
|
54
|
+
*/
|
|
55
|
+
export declare function generateSessionId(): string;
|
|
56
|
+
//# sourceMappingURL=csrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAK,OAAO,CAAC;IAClB,MAAM,CAAC,EAAG,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAG,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAM,MAAM,GACjB,MAAM,CAMR;AAID;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAO,MAAM,EAClB,SAAS,EAAG,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAM,MAAM,GACjB,qBAAqB,CAiDvB;AAkCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAczD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
package/dist/csrf.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Action Integrity — Anti-CSRF & Anti-Replay Protection.
|
|
3
|
+
*
|
|
4
|
+
* Every Server Action invocation is protected by an ephemeral, HMAC-signed,
|
|
5
|
+
* single-use token. The flow:
|
|
6
|
+
*
|
|
7
|
+
* 1. Server renders an island → calls generateActionToken(sessionId, action)
|
|
8
|
+
* 2. Token is embedded in the HTML as a data attribute on the island
|
|
9
|
+
* 3. Client POSTs the action with the token in x-nexus-action-token header
|
|
10
|
+
* 4. Server calls validateActionToken() — verifies HMAC, session, expiry, and
|
|
11
|
+
* "burns" the token (single-use). A replay of the same token → blocked.
|
|
12
|
+
*
|
|
13
|
+
* Properties:
|
|
14
|
+
* - HMAC-SHA256 signed with the app secret (tamper-proof)
|
|
15
|
+
* - Bound to sessionId + actionName (no cross-action reuse)
|
|
16
|
+
* - Expires after ACTION_TOKEN_TTL (default 15 min)
|
|
17
|
+
* - Single-use: consumed on first valid use (replay attack prevention)
|
|
18
|
+
* - Constant-time comparison to prevent timing attacks
|
|
19
|
+
*/
|
|
20
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
21
|
+
/** Token lifetime — 15 minutes. Matches typical form interaction time. */
|
|
22
|
+
const ACTION_TOKEN_TTL_MS = 15 * 60 * 1_000;
|
|
23
|
+
/**
|
|
24
|
+
* In-memory set of consumed tokens. In a multi-process / serverless deployment
|
|
25
|
+
* this should be backed by Redis or a distributed cache. For single-process
|
|
26
|
+
* (Node.js server) this is sufficient.
|
|
27
|
+
*/
|
|
28
|
+
const USED_TOKENS = new Set();
|
|
29
|
+
/** Request header name for the action token. */
|
|
30
|
+
export const ACTION_TOKEN_HEADER = 'x-nexus-action-token';
|
|
31
|
+
// ── Token generation ──────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Generates a signed, single-use action token.
|
|
34
|
+
*
|
|
35
|
+
* @param sessionId Unique identifier for the current user session (cookie, JWT sub, etc.)
|
|
36
|
+
* @param actionName The action being authorized (e.g. 'capture', 'update-favorites')
|
|
37
|
+
* @param secret App-level secret — should come from process.env.NEXUS_SECRET
|
|
38
|
+
* @returns base64url-encoded token safe to embed in HTML
|
|
39
|
+
*/
|
|
40
|
+
export function generateActionToken(sessionId, actionName, secret) {
|
|
41
|
+
const ts = Date.now().toString(36); // compact timestamp
|
|
42
|
+
const nonce = randomBytes(8).toString('hex'); // 8 bytes = 64 bits of entropy
|
|
43
|
+
const payload = `${sessionId}\x00${actionName}\x00${ts}\x00${nonce}`;
|
|
44
|
+
const sig = sign(payload, secret);
|
|
45
|
+
return Buffer.from(`${payload}\x00${sig}`).toString('base64url');
|
|
46
|
+
}
|
|
47
|
+
// ── Token validation ──────────────────────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Validates and consumes an action token. Calling this twice with the same
|
|
50
|
+
* token will return `{ valid: false, replayed: true }` on the second call.
|
|
51
|
+
*
|
|
52
|
+
* @param token Token from x-nexus-action-token header
|
|
53
|
+
* @param sessionId Current user's session ID
|
|
54
|
+
* @param actionName The action being invoked
|
|
55
|
+
* @param secret Same secret used during generation
|
|
56
|
+
*/
|
|
57
|
+
export function validateActionToken(token, sessionId, actionName, secret) {
|
|
58
|
+
// 1. Replay check (fast path before expensive HMAC)
|
|
59
|
+
if (USED_TOKENS.has(token)) {
|
|
60
|
+
return { valid: false, replayed: true, reason: 'Token already used — replay attack prevented' };
|
|
61
|
+
}
|
|
62
|
+
// 2. Decode
|
|
63
|
+
let raw;
|
|
64
|
+
try {
|
|
65
|
+
raw = Buffer.from(token, 'base64url').toString('utf-8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { valid: false, reason: 'Malformed token (invalid base64url)' };
|
|
69
|
+
}
|
|
70
|
+
const parts = raw.split('\x00');
|
|
71
|
+
if (parts.length !== 5) {
|
|
72
|
+
return { valid: false, reason: 'Malformed token (wrong structure)' };
|
|
73
|
+
}
|
|
74
|
+
const [tokenSession, tokenAction, tsBase36, , sig] = parts;
|
|
75
|
+
const payloadWithoutSig = parts.slice(0, 4).join('\x00');
|
|
76
|
+
// 3. Verify HMAC in constant time (prevents timing side-channel)
|
|
77
|
+
const expectedSig = sign(payloadWithoutSig, secret);
|
|
78
|
+
if (!safeEqual(sig, expectedSig)) {
|
|
79
|
+
return { valid: false, reason: 'Invalid signature — possible CSRF attack' };
|
|
80
|
+
}
|
|
81
|
+
// 4. Verify session binding
|
|
82
|
+
if (tokenSession !== sessionId) {
|
|
83
|
+
return { valid: false, reason: 'Session mismatch — token not issued for this user' };
|
|
84
|
+
}
|
|
85
|
+
// 5. Verify action binding
|
|
86
|
+
if (tokenAction !== actionName) {
|
|
87
|
+
return { valid: false, reason: `Action mismatch — expected "${actionName}", got "${tokenAction}"` };
|
|
88
|
+
}
|
|
89
|
+
// 6. Check expiry
|
|
90
|
+
const issuedAt = parseInt(tsBase36, 36);
|
|
91
|
+
if (Date.now() - issuedAt > ACTION_TOKEN_TTL_MS) {
|
|
92
|
+
return { valid: false, reason: `Token expired (issued ${Math.round((Date.now() - issuedAt) / 60_000)}m ago)` };
|
|
93
|
+
}
|
|
94
|
+
// 7. Consume (burn) the token — prevents replay
|
|
95
|
+
USED_TOKENS.add(token);
|
|
96
|
+
pruneUsedTokens();
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
100
|
+
function sign(payload, secret) {
|
|
101
|
+
return createHmac('sha256', secret).update(payload).digest('hex');
|
|
102
|
+
}
|
|
103
|
+
function safeEqual(a, b) {
|
|
104
|
+
try {
|
|
105
|
+
return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Prune the used-token set to prevent unbounded memory growth.
|
|
113
|
+
* Tokens older than TTL are safe to forget — they'd fail the expiry check.
|
|
114
|
+
*/
|
|
115
|
+
function pruneUsedTokens() {
|
|
116
|
+
if (USED_TOKENS.size < 50_000)
|
|
117
|
+
return;
|
|
118
|
+
// Tokens are not stored with timestamps — simplest safe eviction is to
|
|
119
|
+
// clear the oldest ~10% (insertion-order Set).
|
|
120
|
+
const deleteCount = Math.floor(USED_TOKENS.size * 0.1);
|
|
121
|
+
const iter = USED_TOKENS.values();
|
|
122
|
+
for (let i = 0; i < deleteCount; i++) {
|
|
123
|
+
const v = iter.next().value;
|
|
124
|
+
if (v !== undefined)
|
|
125
|
+
USED_TOKENS.delete(v);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ── Middleware helper ─────────────────────────────────────────────────────────
|
|
129
|
+
/**
|
|
130
|
+
* Extracts session ID from a request using common patterns.
|
|
131
|
+
* Override with your own session logic in production.
|
|
132
|
+
*/
|
|
133
|
+
export function extractSessionId(request) {
|
|
134
|
+
// Try standard session cookie patterns
|
|
135
|
+
const cookie = request.headers.get('cookie') ?? '';
|
|
136
|
+
const sessionMatch = cookie.match(/(?:nexus-session|__session|session)=([^;]+)/) ??
|
|
137
|
+
cookie.match(/([a-f0-9]{32,})/);
|
|
138
|
+
if (sessionMatch?.[1])
|
|
139
|
+
return sessionMatch[1];
|
|
140
|
+
// Fall back to IP + User-Agent fingerprint (not ideal, but workable for anonymous sessions)
|
|
141
|
+
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|
|
142
|
+
?? request.headers.get('x-real-ip')
|
|
143
|
+
?? 'unknown';
|
|
144
|
+
const ua = request.headers.get('user-agent') ?? '';
|
|
145
|
+
return `anon:${sign(`${ip}:${ua}`, 'nexus-anon-fp').slice(0, 16)}`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Generates a session ID that is safe to store in a cookie.
|
|
149
|
+
*/
|
|
150
|
+
export function generateSessionId() {
|
|
151
|
+
return randomBytes(24).toString('base64url');
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=csrf.js.map
|
package/dist/csrf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvE,0EAA0E;AAC1E,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;AAE5C;;;;GAIG;AACH,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;AAEtC,gDAAgD;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,sBAAsB,CAAC;AAS1D,iFAAiF;AAEjF;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAkB,EAClB,UAAkB,EAClB,MAAkB;IAElB,MAAM,EAAE,GAAM,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAW,oBAAoB;IACrE,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAI,+BAA+B;IAChF,MAAM,OAAO,GAAG,GAAG,SAAS,OAAO,UAAU,OAAO,EAAE,OAAO,KAAK,EAAE,CAAC;IACrE,MAAM,GAAG,GAAK,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,OAAO,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,iFAAiF;AAEjF;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAkB,EAClB,SAAkB,EAClB,UAAkB,EAClB,MAAkB;IAElB,oDAAoD;IACpD,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,8CAA8C,EAAE,CAAC;IAClG,CAAC;IAED,YAAY;IACZ,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC;IACzE,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IACvE,CAAC;IAED,MAAM,CAAC,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,AAAD,EAAG,GAAG,CAAC,GAAG,KAAiD,CAAC;IACvG,MAAM,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEzD,iEAAiE;IACjE,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACpD,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAC;IAC9E,CAAC;IAED,4BAA4B;IAC5B,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mDAAmD,EAAE,CAAC;IACvF,CAAC;IAED,2BAA2B;IAC3B,IAAI,WAAW,KAAK,UAAU,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,+BAA+B,UAAU,WAAW,WAAW,GAAG,EAAE,CAAC;IACtG,CAAC;IAED,kBAAkB;IAClB,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,GAAG,mBAAmB,EAAE,CAAC;QAChD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,yBAAyB,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IACjH,CAAC;IAED,gDAAgD;IAChD,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,eAAe,EAAE,CAAC;IAElB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,iFAAiF;AAEjF,SAAS,IAAI,CAAC,OAAe,EAAE,MAAc;IAC3C,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,SAAS,CAAC,CAAS,EAAE,CAAS;IACrC,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe;IACtB,IAAI,WAAW,CAAC,IAAI,GAAG,MAAM;QAAE,OAAO;IACtC,uEAAuE;IACvE,+CAA+C;IAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;QAC5B,IAAI,CAAC,KAAK,SAAS;YAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,uCAAuC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnD,MAAM,YAAY,GAChB,MAAM,CAAC,KAAK,CAAC,6CAA6C,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAClC,IAAI,YAAY,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9C,4FAA4F;IAC5F,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE;WACnE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;WAChC,SAAS,CAAC;IACf,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;IACnD,OAAO,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,eAAe,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACrE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev / Node server assets: /@nexus_js/runtime ESM mirror + aggregated scoped CSS from .nx files.
|
|
3
|
+
*/
|
|
4
|
+
export declare function bustAggregatedStylesCache(): void;
|
|
5
|
+
/**
|
|
6
|
+
* Locate `@nexus_js/runtime/dist` without `require.resolve` (pnpm "exports" blocks package.json / bare resolve from apps).
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveRuntimeDistDir(appRoot: string): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Locate `@nexus_js/serialize/dist` — served to the browser at `/_nexus/rt/serialize.js`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveSerializeDistFile(appRoot: string): string | null;
|
|
13
|
+
/** Safe basename-only files under runtime dist (no ..). */
|
|
14
|
+
export declare function runtimeModulePath(runtimeDist: string, pathname: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Concatenate scoped CSS from every .nx under src/ (routes + components).
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildAggregatedNxStylesheet(appRoot: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Compiles a .nx file's client island bundle for dynamic import() during hydration.
|
|
21
|
+
*/
|
|
22
|
+
export declare function compileIslandClientBundle(appRoot: string, url: URL): Promise<{
|
|
23
|
+
body: string;
|
|
24
|
+
status: number;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function isIslandClientRequest(pathname: string): boolean;
|
|
27
|
+
export declare function tryServeRuntimeAsset(pathname: string, appRoot: string): Promise<{
|
|
28
|
+
body: Buffer;
|
|
29
|
+
contentType: string;
|
|
30
|
+
} | null>;
|
|
31
|
+
//# sourceMappingURL=dev-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-assets.d.ts","sourceRoot":"","sources":["../src/dev-assets.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASpE;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKtF;AAyBD;;GAEG;AACH,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBlF;AAID;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,GAAG,GACP,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA0D3C;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA2BvD"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev / Node server assets: /@nexus_js/runtime ESM mirror + aggregated scoped CSS from .nx files.
|
|
3
|
+
*/
|
|
4
|
+
import { compile } from '@nexus_js/compiler';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
7
|
+
import { join, relative, resolve, normalize, sep } from 'node:path';
|
|
8
|
+
const RT_PREFIX = '/_nexus/rt/';
|
|
9
|
+
const LAYER_DECL = '@layer nexus.scoped, nexus.global;\n';
|
|
10
|
+
let aggregatedCssCache = null;
|
|
11
|
+
export function bustAggregatedStylesCache() {
|
|
12
|
+
aggregatedCssCache = null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Locate `@nexus_js/runtime/dist` without `require.resolve` (pnpm "exports" blocks package.json / bare resolve from apps).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveRuntimeDistDir(appRoot) {
|
|
18
|
+
const candidates = [
|
|
19
|
+
join(appRoot, 'node_modules', '@nexus_js', 'runtime', 'dist'),
|
|
20
|
+
join(appRoot, '..', '..', 'packages', 'runtime', 'dist'),
|
|
21
|
+
];
|
|
22
|
+
for (const d of candidates) {
|
|
23
|
+
if (existsSync(join(d, 'index.js')))
|
|
24
|
+
return d;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Locate `@nexus_js/serialize/dist` — served to the browser at `/_nexus/rt/serialize.js`.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveSerializeDistFile(appRoot) {
|
|
32
|
+
const candidates = [
|
|
33
|
+
join(appRoot, 'node_modules', '@nexus_js', 'serialize', 'dist', 'index.js'),
|
|
34
|
+
join(appRoot, '..', '..', 'packages', 'serialize', 'dist', 'index.js'),
|
|
35
|
+
];
|
|
36
|
+
for (const f of candidates) {
|
|
37
|
+
if (existsSync(f))
|
|
38
|
+
return f;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** Safe basename-only files under runtime dist (no ..). */
|
|
43
|
+
export function runtimeModulePath(runtimeDist, pathname) {
|
|
44
|
+
if (!pathname.startsWith(RT_PREFIX))
|
|
45
|
+
return null;
|
|
46
|
+
const name = pathname.slice(RT_PREFIX.length);
|
|
47
|
+
if (!/^[\w.-]+\.js$/.test(name) || name.includes('..'))
|
|
48
|
+
return null;
|
|
49
|
+
return join(runtimeDist, name);
|
|
50
|
+
}
|
|
51
|
+
async function collectNxFiles(dir) {
|
|
52
|
+
const out = [];
|
|
53
|
+
async function walk(d) {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const e of entries) {
|
|
62
|
+
const p = join(d, e.name);
|
|
63
|
+
if (e.isDirectory()) {
|
|
64
|
+
if (e.name === 'node_modules')
|
|
65
|
+
continue;
|
|
66
|
+
await walk(p);
|
|
67
|
+
}
|
|
68
|
+
else if (e.name.endsWith('.nx')) {
|
|
69
|
+
out.push(p);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await walk(dir);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Concatenate scoped CSS from every .nx under src/ (routes + components).
|
|
78
|
+
*/
|
|
79
|
+
export async function buildAggregatedNxStylesheet(appRoot) {
|
|
80
|
+
if (aggregatedCssCache !== null)
|
|
81
|
+
return aggregatedCssCache;
|
|
82
|
+
const srcDir = join(appRoot, 'src');
|
|
83
|
+
const files = await collectNxFiles(srcDir);
|
|
84
|
+
const parts = [LAYER_DECL];
|
|
85
|
+
for (const filepath of files) {
|
|
86
|
+
const source = await readFile(filepath, 'utf-8');
|
|
87
|
+
const result = compile(source, filepath, {
|
|
88
|
+
mode: 'server',
|
|
89
|
+
dev: true,
|
|
90
|
+
ssr: true,
|
|
91
|
+
emitIslandManifest: false,
|
|
92
|
+
target: 'node',
|
|
93
|
+
});
|
|
94
|
+
if (result.css)
|
|
95
|
+
parts.push(`/* ${relative(appRoot, filepath)} */\n${result.css}`);
|
|
96
|
+
}
|
|
97
|
+
aggregatedCssCache = parts.join('\n');
|
|
98
|
+
return aggregatedCssCache;
|
|
99
|
+
}
|
|
100
|
+
const ISLAND_CLIENT_PATH = '/_nexus/islands/client.mjs';
|
|
101
|
+
/**
|
|
102
|
+
* Compiles a .nx file's client island bundle for dynamic import() during hydration.
|
|
103
|
+
*/
|
|
104
|
+
export async function compileIslandClientBundle(appRoot, url) {
|
|
105
|
+
const pathParam = url.searchParams.get('path');
|
|
106
|
+
const absParam = url.searchParams.get('abs');
|
|
107
|
+
const rootReal = resolve(appRoot);
|
|
108
|
+
function isUnderRoot(file) {
|
|
109
|
+
const rel = relative(rootReal, resolve(file));
|
|
110
|
+
if (rel === '..')
|
|
111
|
+
return false;
|
|
112
|
+
if (rel.startsWith(`..${sep}`))
|
|
113
|
+
return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
let nxPath = null;
|
|
117
|
+
if (pathParam) {
|
|
118
|
+
if (pathParam.includes('..') || pathParam.startsWith('/')) {
|
|
119
|
+
return { body: 'Invalid path', status: 400 };
|
|
120
|
+
}
|
|
121
|
+
const joined = resolve(join(rootReal, normalize(pathParam)));
|
|
122
|
+
if (!isUnderRoot(joined)) {
|
|
123
|
+
return { body: 'Path escapes app root', status: 400 };
|
|
124
|
+
}
|
|
125
|
+
nxPath = joined;
|
|
126
|
+
}
|
|
127
|
+
else if (absParam) {
|
|
128
|
+
const decoded = decodeURIComponent(absParam);
|
|
129
|
+
const abs = resolve(decoded);
|
|
130
|
+
if (!isUnderRoot(abs)) {
|
|
131
|
+
return { body: 'Invalid abs path', status: 400 };
|
|
132
|
+
}
|
|
133
|
+
nxPath = abs;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return { body: 'Missing path or abs query', status: 400 };
|
|
137
|
+
}
|
|
138
|
+
if (!nxPath.endsWith('.nx')) {
|
|
139
|
+
return { body: 'Not an .nx source', status: 400 };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await stat(nxPath);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return { body: 'Source not found', status: 404 };
|
|
146
|
+
}
|
|
147
|
+
const source = await readFile(nxPath, 'utf-8');
|
|
148
|
+
const result = compile(source, nxPath, {
|
|
149
|
+
mode: 'server',
|
|
150
|
+
dev: true,
|
|
151
|
+
ssr: true,
|
|
152
|
+
emitIslandManifest: false,
|
|
153
|
+
target: 'node',
|
|
154
|
+
appRoot: rootReal,
|
|
155
|
+
});
|
|
156
|
+
if (!result.clientCode) {
|
|
157
|
+
return { body: 'No client island for this module', status: 404 };
|
|
158
|
+
}
|
|
159
|
+
return { body: result.clientCode, status: 200 };
|
|
160
|
+
}
|
|
161
|
+
export function isIslandClientRequest(pathname) {
|
|
162
|
+
return pathname === ISLAND_CLIENT_PATH;
|
|
163
|
+
}
|
|
164
|
+
export async function tryServeRuntimeAsset(pathname, appRoot) {
|
|
165
|
+
// Special alias: /_nexus/rt/serialize.js → @nexus_js/serialize/dist/index.js
|
|
166
|
+
if (pathname === '/_nexus/rt/serialize.js') {
|
|
167
|
+
const serializeFile = resolveSerializeDistFile(appRoot);
|
|
168
|
+
if (!serializeFile)
|
|
169
|
+
return null;
|
|
170
|
+
try {
|
|
171
|
+
const s = await stat(serializeFile);
|
|
172
|
+
if (!s.isFile())
|
|
173
|
+
return null;
|
|
174
|
+
const body = await readFile(serializeFile);
|
|
175
|
+
return { body, contentType: 'application/javascript; charset=utf-8' };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const dist = resolveRuntimeDistDir(appRoot);
|
|
182
|
+
if (!dist)
|
|
183
|
+
return null;
|
|
184
|
+
const file = runtimeModulePath(dist, pathname);
|
|
185
|
+
if (!file)
|
|
186
|
+
return null;
|
|
187
|
+
try {
|
|
188
|
+
const s = await stat(file);
|
|
189
|
+
if (!s.isFile())
|
|
190
|
+
return null;
|
|
191
|
+
const body = await readFile(file);
|
|
192
|
+
return { body, contentType: 'application/javascript; charset=utf-8' };
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=dev-assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-assets.js","sourceRoot":"","sources":["../src/dev-assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus Error Boundaries — file-based resilience system.
|
|
3
|
+
*
|
|
4
|
+
* Convention (mirrors Next.js App Router):
|
|
5
|
+
*
|
|
6
|
+
* routes/
|
|
7
|
+
* ├── error.nx ← catches errors in the root layout
|
|
8
|
+
* ├── dashboard/
|
|
9
|
+
* │ ├── error.nx ← catches errors in /dashboard/**
|
|
10
|
+
* │ ├── +layout.nx
|
|
11
|
+
* │ └── +page.nx
|
|
12
|
+
* └── blog/
|
|
13
|
+
* ├── [slug]/
|
|
14
|
+
* │ ├── error.nx ← catches errors ONLY in /blog/:slug
|
|
15
|
+
* │ └── +page.nx
|
|
16
|
+
* └── not-found.nx ← custom 404 for /blog/**
|
|
17
|
+
*
|
|
18
|
+
* The error.nx component receives:
|
|
19
|
+
* - error: { message, name, digest? }
|
|
20
|
+
* - reset: () => void (client-side — triggers re-render)
|
|
21
|
+
*
|
|
22
|
+
* Island hydration for error.nx:
|
|
23
|
+
* The "reset" button must be an island (client:load) to be interactive.
|
|
24
|
+
* The error info is server-rendered (no JS needed to display it).
|
|
25
|
+
*
|
|
26
|
+
* Usage in error.nx:
|
|
27
|
+
* ---
|
|
28
|
+
* const { error } = ctx.errorBoundary;
|
|
29
|
+
* ---
|
|
30
|
+
* <script>
|
|
31
|
+
* const { reset } = $props();
|
|
32
|
+
* </script>
|
|
33
|
+
* <div class="error-boundary" client:load>
|
|
34
|
+
* <h2>Something went wrong</h2>
|
|
35
|
+
* <p>{error.message}</p>
|
|
36
|
+
* <button onclick={reset}>Try again</button>
|
|
37
|
+
* </div>
|
|
38
|
+
*/
|
|
39
|
+
import type { NexusContext } from './context.js';
|
|
40
|
+
export interface BoundaryError {
|
|
41
|
+
message: string;
|
|
42
|
+
name: string;
|
|
43
|
+
/** Opaque server-side error digest (safe to show in prod) */
|
|
44
|
+
digest?: string;
|
|
45
|
+
/** Full stack trace (dev only) */
|
|
46
|
+
stack?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ErrorBoundaryContext {
|
|
49
|
+
error: BoundaryError;
|
|
50
|
+
pathname: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Finds the nearest `error.nx` file to a given route filepath.
|
|
54
|
+
* Walks up the directory tree until it finds one or reaches the routes root.
|
|
55
|
+
*/
|
|
56
|
+
export declare function findErrorBoundary(routeFilepath: string, routesRoot: string): Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Finds the nearest `not-found.nx` for a given route directory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findNotFoundBoundary(routeFilepath: string, routesRoot: string): Promise<string | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Renders an error boundary file to HTML.
|
|
63
|
+
* Sanitizes error info for production (hides stack, generates digest).
|
|
64
|
+
*/
|
|
65
|
+
export declare function renderErrorBoundary(boundaryFile: string, originalError: unknown, ctx: NexusContext, opts: {
|
|
66
|
+
dev: boolean;
|
|
67
|
+
}): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Default error page when no error.nx is found.
|
|
70
|
+
* Shows full details in dev, sanitized message in production.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildDefaultErrorHTML(error: BoundaryError, dev: boolean): string;
|
|
73
|
+
/**
|
|
74
|
+
* Wraps a render function with error boundary protection.
|
|
75
|
+
* If the render throws, catches it and renders the nearest error.nx.
|
|
76
|
+
*/
|
|
77
|
+
export declare function withErrorBoundary<T>(fn: () => Promise<T>, opts: {
|
|
78
|
+
routeFilepath: string;
|
|
79
|
+
routesRoot: string;
|
|
80
|
+
ctx: NexusContext;
|
|
81
|
+
dev: boolean;
|
|
82
|
+
fallback?: string;
|
|
83
|
+
}): Promise<T | {
|
|
84
|
+
html: string;
|
|
85
|
+
status: number;
|
|
86
|
+
}>;
|
|
87
|
+
//# sourceMappingURL=error-boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-boundary.d.ts","sourceRoot":"","sources":["../src/error-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBxB;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,OAAO,EACtB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAuBhF;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,EAAE;IACJ,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,OAAO,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/C"}
|