@nitronjs/framework 0.2.26 → 0.3.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/README.md +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +2 -33
- package/lib/Build/Manager.js +390 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +177 -24
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +144 -1
- package/lib/Session/Redis.js +117 -0
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +3 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +4 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- package/lib/Console/Stubs/page-hydration.tsx +0 -53
package/lib/Build/jsxRuntime.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom JSX Runtime for NitronJS
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Handles React 19's strict key prop requirements and provides
|
|
5
|
-
*
|
|
5
|
+
* server-side context helpers (csrf, request).
|
|
6
6
|
*/
|
|
7
|
+
import { pluralize, replaceParams } from "../Translation/pluralize.js";
|
|
8
|
+
|
|
7
9
|
const JSX_RUNTIME = `
|
|
8
10
|
import * as React from 'react';
|
|
9
11
|
import * as OriginalJsx from '__react_jsx_original__';
|
|
10
12
|
|
|
11
13
|
const CTX = Symbol.for('__nitron_view_context__');
|
|
12
|
-
const MARK = Symbol.for('__nitron_client_component__');
|
|
13
|
-
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype', 'key', 'ref']);
|
|
14
14
|
|
|
15
15
|
// Filter React 19 key warnings from third-party libraries
|
|
16
16
|
if (typeof console !== 'undefined' && console.error) {
|
|
@@ -19,7 +19,7 @@ if (typeof console !== 'undefined' && console.error) {
|
|
|
19
19
|
const msg = args[0];
|
|
20
20
|
if (typeof msg === 'string' && (
|
|
21
21
|
msg.includes('A props object containing a "key" prop is being spread into JSX') ||
|
|
22
|
-
msg.includes('
|
|
22
|
+
msg.includes('\\\`key\\\` is not a prop')
|
|
23
23
|
)) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -31,6 +31,9 @@ function getContext() {
|
|
|
31
31
|
return globalThis[CTX]?.getStore?.();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
${pluralize.toString()}
|
|
35
|
+
${replaceParams.toString()}
|
|
36
|
+
|
|
34
37
|
globalThis.csrf = () => getContext()?.csrf || '';
|
|
35
38
|
|
|
36
39
|
globalThis.request = () => {
|
|
@@ -38,153 +41,26 @@ globalThis.request = () => {
|
|
|
38
41
|
return ctx?.request || { path: '', method: 'GET', query: {}, params: {}, headers: {}, cookies: {}, ip: '', isAjax: false, session: null, auth: null };
|
|
39
42
|
};
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (seen.has(obj)) return undefined;
|
|
54
|
-
seen.add(obj);
|
|
55
|
-
|
|
56
|
-
if (Array.isArray(obj)) {
|
|
57
|
-
return obj.map(item => {
|
|
58
|
-
const sanitized = sanitizeProps(item, seen);
|
|
59
|
-
return sanitized === undefined ? null : sanitized;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (obj instanceof Date) return obj.toISOString();
|
|
64
|
-
if (obj._attributes && typeof obj._attributes === 'object') {
|
|
65
|
-
return sanitizeProps(obj._attributes, seen);
|
|
66
|
-
}
|
|
67
|
-
if (typeof obj.toJSON === 'function') {
|
|
68
|
-
return sanitizeProps(obj.toJSON(), seen);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const proto = Object.getPrototypeOf(obj);
|
|
72
|
-
if (proto !== Object.prototype && proto !== null) return undefined;
|
|
73
|
-
|
|
74
|
-
const result = {};
|
|
75
|
-
for (const key of Object.keys(obj)) {
|
|
76
|
-
if (UNSAFE_KEYS.has(key)) continue;
|
|
77
|
-
const value = sanitizeProps(obj[key], seen);
|
|
78
|
-
if (value !== undefined) result[key] = value;
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function wrapWithDepth(children) {
|
|
84
|
-
return OriginalJsx.jsx(DepthContext.Provider, { value: true, children });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Deep Proxy that tracks which prop paths are accessed during SSR
|
|
88
|
-
// Pre-wraps all nested objects so React's Object.freeze won't break Proxy invariants
|
|
89
|
-
function trackProps(obj) {
|
|
90
|
-
const accessed = new Set();
|
|
91
|
-
const cache = new WeakMap();
|
|
92
|
-
|
|
93
|
-
function wrap(source, prefix) {
|
|
94
|
-
if (source === null || typeof source !== 'object') return source;
|
|
95
|
-
if (cache.has(source)) return cache.get(source);
|
|
96
|
-
|
|
97
|
-
const isArr = Array.isArray(source);
|
|
98
|
-
const copy = isArr ? [] : {};
|
|
99
|
-
|
|
100
|
-
for (const key of Object.keys(source)) {
|
|
101
|
-
const val = source[key];
|
|
102
|
-
|
|
103
|
-
if (val !== null && typeof val === 'object' && typeof val !== 'function') {
|
|
104
|
-
copy[key] = wrap(val, prefix ? prefix + '.' + key : key);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
copy[key] = val;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const proxy = new Proxy(copy, {
|
|
112
|
-
get(t, prop, receiver) {
|
|
113
|
-
if (typeof prop === 'symbol') return Reflect.get(t, prop, receiver);
|
|
114
|
-
|
|
115
|
-
const val = Reflect.get(t, prop, receiver);
|
|
116
|
-
if (typeof val === 'function') return val;
|
|
117
|
-
|
|
118
|
-
if (val !== undefined) {
|
|
119
|
-
const currentPath = prefix ? prefix + '.' + String(prop) : String(prop);
|
|
120
|
-
accessed.add(currentPath);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return val;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
cache.set(source, proxy);
|
|
128
|
-
return proxy;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { proxy: wrap(obj, ''), accessed };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function resolveExists(obj, path) {
|
|
135
|
-
const parts = path.split('.');
|
|
136
|
-
let current = obj;
|
|
137
|
-
for (let i = 0; i < parts.length; i++) {
|
|
138
|
-
if (current == null || typeof current !== 'object') return false;
|
|
139
|
-
current = current[parts[i]];
|
|
140
|
-
}
|
|
141
|
-
return current !== undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Creates an island wrapper for client components (hydrated independently)
|
|
145
|
-
function createIsland(Component, name) {
|
|
146
|
-
function IslandBoundary(props) {
|
|
147
|
-
if (React.useContext(DepthContext)) {
|
|
148
|
-
return OriginalJsx.jsx(Component, props);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const id = React.useId();
|
|
152
|
-
const tracker = trackProps(props);
|
|
153
|
-
|
|
154
|
-
if (Component.__propHints) {
|
|
155
|
-
for (const hint of Component.__propHints) {
|
|
156
|
-
if (resolveExists(props, hint)) {
|
|
157
|
-
tracker.accessed.add(hint);
|
|
158
|
-
}
|
|
44
|
+
globalThis.__ = (key, params) => {
|
|
45
|
+
const ctx = getContext();
|
|
46
|
+
const translations = ctx?.translations
|
|
47
|
+
|| (typeof window !== 'undefined' ? window.__NITRON_TRANSLATIONS__ : null);
|
|
48
|
+
|
|
49
|
+
if (translations) {
|
|
50
|
+
let value = translations[key];
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
value = String(value);
|
|
53
|
+
if (params && typeof params === 'object'
|
|
54
|
+
&& params.count !== undefined && value.includes('|')) {
|
|
55
|
+
value = pluralize(value, params.count);
|
|
159
56
|
}
|
|
57
|
+
return replaceParams(value, params);
|
|
160
58
|
}
|
|
161
|
-
|
|
162
|
-
const ctx = getContext();
|
|
163
|
-
if (ctx) {
|
|
164
|
-
ctx.props = ctx.props || {};
|
|
165
|
-
ctx.trackers = ctx.trackers || {};
|
|
166
|
-
ctx.props[id] = props;
|
|
167
|
-
ctx.trackers[id] = tracker.accessed;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return OriginalJsx.jsx('div', {
|
|
171
|
-
'data-cid': id,
|
|
172
|
-
'data-island': name,
|
|
173
|
-
children: wrapWithDepth(OriginalJsx.jsx(Component, tracker.proxy))
|
|
174
|
-
});
|
|
59
|
+
return key;
|
|
175
60
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function getWrappedComponent(Component) {
|
|
182
|
-
if (!componentCache.has(Component)) {
|
|
183
|
-
const name = Component.displayName || Component.name || 'Anonymous';
|
|
184
|
-
componentCache.set(Component, createIsland(Component, name));
|
|
185
|
-
}
|
|
186
|
-
return componentCache.get(Component);
|
|
187
|
-
}
|
|
61
|
+
return globalThis.__nitron_lang_get?.(key, params) ?? key;
|
|
62
|
+
};
|
|
63
|
+
globalThis.lang = globalThis.__;
|
|
188
64
|
|
|
189
65
|
// Extracts key from props without triggering React 19's getter warning
|
|
190
66
|
function extractKey(props, key) {
|
|
@@ -207,17 +83,11 @@ function extractKey(props, key) {
|
|
|
207
83
|
|
|
208
84
|
export function jsx(type, props, key) {
|
|
209
85
|
const [cleanProps, finalKey] = extractKey(props, key);
|
|
210
|
-
if (typeof type === 'function' && type[MARK]) {
|
|
211
|
-
return OriginalJsx.jsx(getWrappedComponent(type), cleanProps, finalKey);
|
|
212
|
-
}
|
|
213
86
|
return OriginalJsx.jsx(type, cleanProps, finalKey);
|
|
214
87
|
}
|
|
215
88
|
|
|
216
89
|
export function jsxs(type, props, key) {
|
|
217
90
|
const [cleanProps, finalKey] = extractKey(props, key);
|
|
218
|
-
if (typeof type === 'function' && type[MARK]) {
|
|
219
|
-
return OriginalJsx.jsx(getWrappedComponent(type), cleanProps, finalKey);
|
|
220
|
-
}
|
|
221
91
|
return OriginalJsx.jsxs(type, cleanProps, finalKey);
|
|
222
92
|
}
|
|
223
93
|
|
package/lib/Build/plugins.js
CHANGED
|
@@ -4,9 +4,21 @@ import { parse } from "@babel/parser";
|
|
|
4
4
|
import traverse from "@babel/traverse";
|
|
5
5
|
import Paths from "../Core/Paths.js";
|
|
6
6
|
import COLORS from "./colors.js";
|
|
7
|
+
import { resolveComponentPath } from "./propUtils.js";
|
|
7
8
|
|
|
8
9
|
const _traverse = traverse.default;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Checks if a source file has a "use client" directive at the top.
|
|
13
|
+
* Allows leading single-line (//) and multi-line comments before the directive.
|
|
14
|
+
* @param {string} source - File source code.
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function hasUseClientDirective(source) {
|
|
18
|
+
const stripped = source.slice(0, 500).replace(/^\s*(\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*/g, "");
|
|
19
|
+
return /^\s*["']use client["']/.test(stripped.slice(0, 50));
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Creates an esbuild plugin for path alias resolution.
|
|
12
24
|
* Supports @/, @css/, @views/, @models/, @controllers/, @middlewares/ aliases.
|
|
@@ -24,14 +36,13 @@ export function createPathAliasPlugin() {
|
|
|
24
36
|
"@controllers/": path.join(root, "app/Controllers"), // Controllers
|
|
25
37
|
"@middlewares/": path.join(root, "app/Middlewares"), // Middlewares
|
|
26
38
|
};
|
|
27
|
-
|
|
39
|
+
|
|
40
|
+
const sortedPrefixes = Object.keys(aliases).sort((a, b) => b.length - a.length);
|
|
41
|
+
|
|
28
42
|
return {
|
|
29
43
|
name: "path-alias",
|
|
30
44
|
setup: (build) => {
|
|
31
|
-
// Handle all @ prefixed imports
|
|
32
45
|
build.onResolve({ filter: /^@[a-z]*\// }, async (args) => {
|
|
33
|
-
// Find matching alias (longest match first)
|
|
34
|
-
const sortedPrefixes = Object.keys(aliases).sort((a, b) => b.length - a.length);
|
|
35
46
|
|
|
36
47
|
for (const prefix of sortedPrefixes) {
|
|
37
48
|
if (args.path.startsWith(prefix)) {
|
|
@@ -86,7 +97,7 @@ export function createOriginalJsxPlugin() {
|
|
|
86
97
|
|
|
87
98
|
/**
|
|
88
99
|
* Creates an esbuild plugin that maps React packages to window globals.
|
|
89
|
-
* Used for client bundles to share React instance across
|
|
100
|
+
* Used for client bundles to share React instance across client components.
|
|
90
101
|
* @returns {import("esbuild").Plugin}
|
|
91
102
|
*/
|
|
92
103
|
export function createVendorGlobalsPlugin() {
|
|
@@ -218,71 +229,12 @@ export function createCssStubPlugin() {
|
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
/**
|
|
221
|
-
*
|
|
222
|
-
* Used
|
|
223
|
-
* @param {import("esbuild").BuildOptions} options - Build options.
|
|
224
|
-
* @param {boolean} isDev - Whether in development mode.
|
|
225
|
-
* @returns {import("esbuild").Plugin}
|
|
226
|
-
*/
|
|
227
|
-
export function createMarkerPlugin(options, isDev) {
|
|
228
|
-
return {
|
|
229
|
-
name: "client-marker",
|
|
230
|
-
setup: (build) => {
|
|
231
|
-
if (options.platform === "browser") {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
|
|
236
|
-
const source = await fs.promises.readFile(args.path, "utf8");
|
|
237
|
-
|
|
238
|
-
if (!/^\s*["']use client["']/.test(source.slice(0, 50))) {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
let ast;
|
|
243
|
-
try {
|
|
244
|
-
ast = parse(source, {
|
|
245
|
-
sourceType: "module",
|
|
246
|
-
plugins: ["typescript", "jsx"]
|
|
247
|
-
});
|
|
248
|
-
} catch {
|
|
249
|
-
if (isDev) {
|
|
250
|
-
console.warn(`${COLORS.yellow}⚠ Parse: ${args.path}${COLORS.reset}`);
|
|
251
|
-
}
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const exports = findExports(ast);
|
|
256
|
-
if (!exports.length) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const symbolCode = `Symbol.for('__nitron_client_component__')`;
|
|
261
|
-
let additionalCode = "\n";
|
|
262
|
-
|
|
263
|
-
for (const exp of exports) {
|
|
264
|
-
additionalCode += `try { Object.defineProperty(${exp.name}, ${symbolCode}, { value: true }); ${exp.name}.displayName = "${exp.name}"; } catch {}\n`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const hints = extractPropHints(ast, exports.map(e => e.name));
|
|
268
|
-
if (hints.length > 0) {
|
|
269
|
-
for (const exp of exports) {
|
|
270
|
-
additionalCode += `try { ${exp.name}.__propHints = ${JSON.stringify(hints)}; } catch {}\n`;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return { contents: source + additionalCode, loader: "tsx" };
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Finds all exported functions/components in an AST.
|
|
232
|
+
* Finds all exported functions/components in an AST with source positions.
|
|
233
|
+
* Used by the RSC client reference plugin to inject annotations inline.
|
|
282
234
|
* @param {import("@babel/parser").ParseResult} ast - Parsed AST.
|
|
283
|
-
* @returns {Array<{name: string
|
|
235
|
+
* @returns {Array<{name: string, isDefault: boolean, end: number}>}
|
|
284
236
|
*/
|
|
285
|
-
function
|
|
237
|
+
function findExportsWithPositions(ast) {
|
|
286
238
|
const exports = [];
|
|
287
239
|
|
|
288
240
|
_traverse(ast, {
|
|
@@ -294,31 +246,33 @@ function findExports(ast) {
|
|
|
294
246
|
name = decl.name;
|
|
295
247
|
} else if (decl.type === "FunctionDeclaration" && decl.id?.name) {
|
|
296
248
|
name = decl.id.name;
|
|
297
|
-
} else if (decl.type === "CallExpression" &&
|
|
249
|
+
} else if (decl.type === "CallExpression" &&
|
|
298
250
|
["memo", "forwardRef", "lazy"].includes(decl.callee?.name)) {
|
|
299
251
|
name = decl.arguments[0]?.name || "__default__";
|
|
300
252
|
}
|
|
301
253
|
|
|
302
|
-
exports.push({ name });
|
|
254
|
+
exports.push({ name, isDefault: true, end: p.node.end });
|
|
303
255
|
},
|
|
304
256
|
|
|
305
257
|
ExportNamedDeclaration: (p) => {
|
|
306
258
|
for (const spec of p.node.specifiers || []) {
|
|
307
259
|
if (spec.type === "ExportSpecifier") {
|
|
308
260
|
exports.push({
|
|
309
|
-
name: spec.exported.name === "default" ? spec.local.name : spec.exported.name
|
|
261
|
+
name: spec.exported.name === "default" ? spec.local.name : spec.exported.name,
|
|
262
|
+
isDefault: spec.exported.name === "default",
|
|
263
|
+
end: p.node.end
|
|
310
264
|
});
|
|
311
265
|
}
|
|
312
266
|
}
|
|
313
267
|
|
|
314
268
|
const decl = p.node.declaration;
|
|
315
269
|
if (decl?.type === "FunctionDeclaration" && decl.id?.name) {
|
|
316
|
-
exports.push({ name: decl.id.name });
|
|
270
|
+
exports.push({ name: decl.id.name, isDefault: false, end: p.node.end });
|
|
317
271
|
}
|
|
318
272
|
if (decl?.type === "VariableDeclaration") {
|
|
319
273
|
for (const d of decl.declarations) {
|
|
320
274
|
if (d.id.type === "Identifier" && d.init?.type === "ArrowFunctionExpression") {
|
|
321
|
-
exports.push({ name: d.id.name });
|
|
275
|
+
exports.push({ name: d.id.name, isDefault: false, end: p.node.end });
|
|
322
276
|
}
|
|
323
277
|
}
|
|
324
278
|
}
|
|
@@ -329,98 +283,210 @@ function findExports(ast) {
|
|
|
329
283
|
}
|
|
330
284
|
|
|
331
285
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
286
|
+
* Creates an esbuild plugin that annotates "use client" components with
|
|
287
|
+
* Flight client reference metadata in the server build.
|
|
288
|
+
*
|
|
289
|
+
* Unlike a full replacement approach, this KEEPS the original component code
|
|
290
|
+
* intact and appends $$typeof / $$id / $$async properties to each export.
|
|
291
|
+
*
|
|
292
|
+
* This allows both renderers to share the same build output:
|
|
293
|
+
* - react-dom/server ignores these properties → renders real HTML (SEO)
|
|
294
|
+
* - Flight renderer sees $$typeof CLIENT_REFERENCE → serializes as import ref
|
|
295
|
+
*
|
|
296
|
+
* Only active in the server build — client builds are unaffected.
|
|
297
|
+
*
|
|
298
|
+
* @param {boolean} isDev
|
|
299
|
+
* @returns {import("esbuild").Plugin}
|
|
338
300
|
*/
|
|
339
|
-
|
|
340
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Creates an esbuild plugin that wraps "use client" components with prop filtering
|
|
303
|
+
* at server→client boundaries. Only active in the server build.
|
|
304
|
+
*
|
|
305
|
+
* When a server component imports a client component, the plugin intercepts the import
|
|
306
|
+
* and generates a virtual wrapper module that:
|
|
307
|
+
* 1. Imports the original component (with $$typeof annotation)
|
|
308
|
+
* 2. Filters props based on effective prop usage analysis
|
|
309
|
+
* 3. Returns createElement with the original component + filtered props
|
|
310
|
+
*
|
|
311
|
+
* The wrapper has no $$typeof → React treats it as a server component → calls it.
|
|
312
|
+
* Inside, it creates an element with the real $$typeof component → Flight serializes correctly.
|
|
313
|
+
*
|
|
314
|
+
* @param {Map<string, object|null>} effectiveMap - Effective prop usage per client component path.
|
|
315
|
+
* @param {Set<string>} clientPaths - All client component absolute paths.
|
|
316
|
+
* @param {boolean} isDev
|
|
317
|
+
* @returns {import("esbuild").Plugin}
|
|
318
|
+
*/
|
|
319
|
+
export function createPropFilterPlugin(effectiveMap, clientPaths) {
|
|
320
|
+
return {
|
|
321
|
+
name: "prop-filter",
|
|
322
|
+
setup: (build) => {
|
|
323
|
+
build.onResolve({ filter: /^\./ }, (args) => {
|
|
324
|
+
if (args.namespace === "nitron-filtered") return null;
|
|
341
325
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (exportNames.includes(p.node.id?.name)) {
|
|
345
|
-
extractObjectParams(p.node.params[0], paramMap);
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
VariableDeclarator(p) {
|
|
349
|
-
if (exportNames.includes(p.node.id?.name)) {
|
|
350
|
-
const init = p.node.init;
|
|
351
|
-
if (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") {
|
|
352
|
-
extractObjectParams(init.params[0], paramMap);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
});
|
|
326
|
+
const resolvedPath = resolveComponentPath(args.path, args.resolveDir);
|
|
327
|
+
if (!resolvedPath || !clientPaths.has(resolvedPath)) return null;
|
|
357
328
|
|
|
358
|
-
|
|
329
|
+
if (clientPaths.has(args.importer)) return null;
|
|
359
330
|
|
|
360
|
-
|
|
331
|
+
const propUsage = effectiveMap.get(resolvedPath);
|
|
361
332
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
OptionalMemberExpression(p) {
|
|
367
|
-
collectPropHint(p, paramMap, hints);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
333
|
+
// Skip if no effective prop usage (null = unanalyzable, empty = no props)
|
|
334
|
+
if (!propUsage || typeof propUsage !== "object" || Object.keys(propUsage).length === 0) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
370
337
|
|
|
371
|
-
|
|
372
|
-
|
|
338
|
+
return {
|
|
339
|
+
path: resolvedPath,
|
|
340
|
+
namespace: "nitron-filtered",
|
|
341
|
+
pluginData: { propUsage }
|
|
342
|
+
};
|
|
343
|
+
});
|
|
373
344
|
|
|
374
|
-
|
|
375
|
-
|
|
345
|
+
build.onLoad({ filter: /.*/, namespace: "nitron-filtered" }, (args) => {
|
|
346
|
+
const propUsage = args.pluginData?.propUsage;
|
|
347
|
+
const importPath = args.path.replace(/\\/g, "/");
|
|
348
|
+
|
|
349
|
+
if (!propUsage || typeof propUsage !== "object" || Object.keys(propUsage).length === 0) {
|
|
350
|
+
// No prop usage or unanalyzable — import original directly (no filtering)
|
|
351
|
+
return {
|
|
352
|
+
contents: `export { default } from "${importPath}";\nexport * from "${importPath}";`,
|
|
353
|
+
loader: "js",
|
|
354
|
+
resolveDir: path.dirname(args.path)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
376
357
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
358
|
+
// Wrapper uses default import — skip wrapping for named-export-only files.
|
|
359
|
+
// Phase 2 will add per-export wrapper support for these.
|
|
360
|
+
const source = fs.readFileSync(args.path, "utf8");
|
|
361
|
+
const hasDefaultExport = /^export\s+default\b/m.test(source);
|
|
362
|
+
|
|
363
|
+
if (!hasDefaultExport) {
|
|
364
|
+
return {
|
|
365
|
+
contents: `export * from "${importPath}";`,
|
|
366
|
+
loader: "js",
|
|
367
|
+
resolveDir: path.dirname(args.path)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
385
370
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
371
|
+
const contents = generateFilterWrapper(args.path, propUsage);
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
contents,
|
|
375
|
+
loader: "js",
|
|
376
|
+
resolveDir: path.dirname(args.path)
|
|
377
|
+
};
|
|
378
|
+
});
|
|
389
379
|
}
|
|
390
|
-
|
|
391
|
-
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Generates a virtual wrapper module that filters props before passing to the real component.
|
|
385
|
+
* @param {string} componentPath - Absolute path to the client component.
|
|
386
|
+
* @param {object} propUsage - Effective prop usage tree.
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
function generateFilterWrapper(componentPath, propUsage) {
|
|
390
|
+
const importPath = componentPath.replace(/\\/g, "/");
|
|
391
|
+
const usageJson = JSON.stringify(propUsage);
|
|
392
|
+
|
|
393
|
+
return `
|
|
394
|
+
import { createElement } from "react";
|
|
395
|
+
import __Impl from "${importPath}";
|
|
396
|
+
export * from "${importPath}";
|
|
397
|
+
|
|
398
|
+
const __usage = ${usageJson};
|
|
399
|
+
|
|
400
|
+
function __prune(data, usage) {
|
|
401
|
+
if (usage === true) return data;
|
|
402
|
+
if (data == null || typeof data !== "object") return data;
|
|
403
|
+
if (Array.isArray(data)) {
|
|
404
|
+
if (usage["[]"]) {
|
|
405
|
+
return data.map(function(item) { return __prune(item, usage["[]"]); });
|
|
392
406
|
}
|
|
393
407
|
}
|
|
408
|
+
var result = {};
|
|
409
|
+
for (var key of Object.keys(usage)) {
|
|
410
|
+
var val = data[key];
|
|
411
|
+
if (val === undefined) continue;
|
|
412
|
+
result[key] = typeof usage[key] === "object" ? __prune(val, usage[key]) : val;
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
394
415
|
}
|
|
395
416
|
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
417
|
+
export default function __PropFilterWrapper(props) {
|
|
418
|
+
var filtered = {};
|
|
419
|
+
for (var key of Object.keys(props)) {
|
|
420
|
+
if (key === "children" || key === "key" || key === "ref") {
|
|
421
|
+
filtered[key] = props[key];
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
var u = __usage[key];
|
|
425
|
+
if (u === undefined) continue;
|
|
426
|
+
filtered[key] = typeof u === "object" ? __prune(props[key], u) : props[key];
|
|
401
427
|
}
|
|
428
|
+
return createElement(__Impl, filtered);
|
|
429
|
+
}
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
402
432
|
|
|
403
|
-
|
|
404
|
-
|
|
433
|
+
export function createClientReferencePlugin(isDev) {
|
|
434
|
+
return {
|
|
435
|
+
name: "rsc-client-reference",
|
|
436
|
+
setup: (build) => {
|
|
437
|
+
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
|
|
438
|
+
const source = await fs.promises.readFile(args.path, "utf8");
|
|
405
439
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
chain.unshift(current.property.name);
|
|
410
|
-
current = current.object;
|
|
411
|
-
}
|
|
440
|
+
if (!hasUseClientDirective(source)) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
412
443
|
|
|
413
|
-
|
|
444
|
+
let ast;
|
|
445
|
+
try {
|
|
446
|
+
ast = parse(source, {
|
|
447
|
+
sourceType: "module",
|
|
448
|
+
plugins: ["typescript", "jsx"]
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
if (isDev) {
|
|
453
|
+
console.warn(`${COLORS.yellow}⚠ RSC Parse: ${args.path}${COLORS.reset}`);
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
414
457
|
|
|
415
|
-
|
|
416
|
-
|
|
458
|
+
const exports = findExportsWithPositions(ast);
|
|
459
|
+
if (!exports.length) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
417
462
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
463
|
+
const fileUrl = "file:///" + args.path.replace(/\\/g, "/");
|
|
464
|
+
const symbolCode = `Symbol.for("react.client.reference")`;
|
|
421
465
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
466
|
+
// Insert annotations right after each export declaration (not at file end).
|
|
467
|
+
// esbuild drops code after the final export block for entry points,
|
|
468
|
+
// so annotations must be interleaved with declarations.
|
|
469
|
+
const sorted = [...exports].sort((a, b) => b.end - a.end);
|
|
470
|
+
let modified = source;
|
|
471
|
+
|
|
472
|
+
for (const exp of sorted) {
|
|
473
|
+
if (exp.name === "__default__") continue;
|
|
425
474
|
|
|
426
|
-
|
|
475
|
+
const exportName = exp.isDefault ? "default" : exp.name;
|
|
476
|
+
|
|
477
|
+
// Only annotate functions — skip context objects, constants, types.
|
|
478
|
+
// typeof guard prevents overwriting React's own $$typeof on contexts.
|
|
479
|
+
let annotation = `\nif (typeof ${exp.name} === "function") { try { Object.defineProperties(${exp.name}, {\n`;
|
|
480
|
+
annotation += ` $$typeof: { value: ${symbolCode} },\n`;
|
|
481
|
+
annotation += ` $$id: { value: "${fileUrl}#${exportName}" },\n`;
|
|
482
|
+
annotation += ` $$async: { value: false }\n`;
|
|
483
|
+
annotation += `}); } catch {} }\n`;
|
|
484
|
+
|
|
485
|
+
modified = modified.slice(0, exp.end) + annotation + modified.slice(exp.end);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return { contents: modified, loader: "tsx" };
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|