@nitronjs/framework 0.2.27 → 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.
Files changed (58) hide show
  1. package/README.md +260 -170
  2. package/lib/Auth/Auth.js +2 -2
  3. package/lib/Build/CssBuilder.js +5 -7
  4. package/lib/Build/EffectivePropUsage.js +174 -0
  5. package/lib/Build/FactoryTransform.js +1 -21
  6. package/lib/Build/FileAnalyzer.js +1 -32
  7. package/lib/Build/Manager.js +354 -58
  8. package/lib/Build/PropUsageAnalyzer.js +1189 -0
  9. package/lib/Build/jsxRuntime.js +25 -155
  10. package/lib/Build/plugins.js +212 -146
  11. package/lib/Build/propUtils.js +70 -0
  12. package/lib/Console/Commands/DevCommand.js +30 -10
  13. package/lib/Console/Commands/MakeCommand.js +8 -1
  14. package/lib/Console/Output.js +0 -2
  15. package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
  16. package/lib/Console/Stubs/vendor-dev.tsx +30 -41
  17. package/lib/Console/Stubs/vendor.tsx +25 -1
  18. package/lib/Core/Config.js +0 -6
  19. package/lib/Core/Paths.js +0 -19
  20. package/lib/Database/Migration/Checksum.js +0 -3
  21. package/lib/Database/Migration/MigrationRepository.js +0 -8
  22. package/lib/Database/Migration/MigrationRunner.js +1 -2
  23. package/lib/Database/Model.js +19 -11
  24. package/lib/Database/QueryBuilder.js +25 -4
  25. package/lib/Database/Schema/Blueprint.js +10 -0
  26. package/lib/Database/Schema/Manager.js +2 -0
  27. package/lib/Date/DateTime.js +1 -1
  28. package/lib/Dev/DevContext.js +44 -0
  29. package/lib/Dev/DevErrorPage.js +990 -0
  30. package/lib/Dev/DevIndicator.js +836 -0
  31. package/lib/HMR/Server.js +16 -37
  32. package/lib/Http/Server.js +171 -23
  33. package/lib/Logging/Log.js +34 -2
  34. package/lib/Mail/Mail.js +41 -10
  35. package/lib/Route/Router.js +43 -19
  36. package/lib/Runtime/Entry.js +10 -6
  37. package/lib/Session/Manager.js +103 -1
  38. package/lib/Session/Session.js +0 -4
  39. package/lib/Support/Str.js +6 -4
  40. package/lib/Translation/Lang.js +376 -32
  41. package/lib/Translation/pluralize.js +81 -0
  42. package/lib/Validation/MagicBytes.js +120 -0
  43. package/lib/Validation/Validator.js +46 -29
  44. package/lib/View/Client/hmr-client.js +100 -90
  45. package/lib/View/Client/spa.js +121 -50
  46. package/lib/View/ClientManifest.js +60 -0
  47. package/lib/View/FlightRenderer.js +100 -0
  48. package/lib/View/Layout.js +0 -3
  49. package/lib/View/PropFilter.js +81 -0
  50. package/lib/View/View.js +230 -495
  51. package/lib/index.d.ts +22 -1
  52. package/package.json +2 -2
  53. package/skeleton/config/app.js +1 -0
  54. package/skeleton/config/server.js +13 -0
  55. package/skeleton/config/session.js +3 -0
  56. package/lib/Build/HydrationBuilder.js +0 -190
  57. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  58. package/lib/Console/Stubs/page-hydration.tsx +0 -53
@@ -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
- * island architecture support for client components.
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('\`key\` is not a prop')
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
- const DepthContext = React.createContext(false);
42
- const componentCache = new WeakMap();
43
-
44
- // Sanitizes props for client-side hydration (removes functions, symbols, circular refs)
45
- function sanitizeProps(obj, seen = new WeakSet()) {
46
- if (obj == null) return obj;
47
-
48
- const type = typeof obj;
49
- if (type === 'function' || type === 'symbol') return undefined;
50
- if (type === 'bigint') return obj.toString();
51
- if (type !== 'object') return obj;
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
- IslandBoundary.displayName = 'Island(' + name + ')';
178
- return IslandBoundary;
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
 
@@ -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 islands.
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
- * Creates an esbuild plugin that marks client components with a symbol.
222
- * Used for Islands Architecture to identify hydration boundaries.
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}>} Array of export names.
235
+ * @returns {Array<{name: string, isDefault: boolean, end: number}>}
284
236
  */
285
- function findExports(ast) {
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
- * Extracts prop access hints from a "use client" component's AST.
333
- * Finds all property access patterns on destructured props, including
334
- * those inside event handlers and other non-SSR code paths.
335
- * @param {import("@babel/parser").ParseResult} ast - Parsed AST.
336
- * @param {string[]} exportNames - Names of exported components.
337
- * @returns {string[]} Array of prop access paths.
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
- function extractPropHints(ast, exportNames) {
340
- const paramMap = new Map();
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
- _traverse(ast, {
343
- FunctionDeclaration(p) {
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
- if (paramMap.size === 0) return [];
329
+ if (clientPaths.has(args.importer)) return null;
359
330
 
360
- const hints = new Set();
331
+ const propUsage = effectiveMap.get(resolvedPath);
361
332
 
362
- _traverse(ast, {
363
- MemberExpression(p) {
364
- collectPropHint(p, paramMap, hints);
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
- return [...hints].sort();
372
- }
338
+ return {
339
+ path: resolvedPath,
340
+ namespace: "nitron-filtered",
341
+ pluginData: { propUsage }
342
+ };
343
+ });
373
344
 
374
- function extractObjectParams(param, map) {
375
- if (!param || param.type !== "ObjectPattern") return;
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
- for (const prop of param.properties) {
378
- if (prop.type === "ObjectProperty") {
379
- const propName = prop.key.name || prop.key.value;
380
- const localName = prop.value.type === "Identifier"
381
- ? prop.value.name
382
- : prop.value.type === "AssignmentPattern" && prop.value.left?.type === "Identifier"
383
- ? prop.value.left.name
384
- : null;
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
- if (propName && localName) {
387
- map.set(localName, propName);
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
- else if (prop.type === "RestElement" && prop.argument?.type === "Identifier") {
391
- map.set(prop.argument.name, prop.argument.name);
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 collectPropHint(path, paramMap, hints) {
397
- const parent = path.parent;
398
-
399
- if ((parent.type === "MemberExpression" || parent.type === "OptionalMemberExpression") && parent.object === path.node) {
400
- return;
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
- const chain = [];
404
- let current = path.node;
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
- while (current.type === "MemberExpression" || current.type === "OptionalMemberExpression") {
407
- if (current.computed) return;
408
- if (current.property?.type !== "Identifier") return;
409
- chain.unshift(current.property.name);
410
- current = current.object;
411
- }
440
+ if (!hasUseClientDirective(source)) {
441
+ return null;
442
+ }
412
443
 
413
- if (current.type !== "Identifier") return;
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
- const rootProp = paramMap.get(current.name);
416
- if (!rootProp) return;
458
+ const exports = findExportsWithPositions(ast);
459
+ if (!exports.length) {
460
+ return null;
461
+ }
417
462
 
418
- if (parent.type === "CallExpression" && parent.callee === path.node && chain.length > 0) {
419
- chain.pop();
420
- }
463
+ const fileUrl = "file:///" + args.path.replace(/\\/g, "/");
464
+ const symbolCode = `Symbol.for("react.client.reference")`;
421
465
 
422
- const fullPath = chain.length > 0 ? rootProp + "." + chain.join(".") : rootProp;
423
- hints.add(fullPath);
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
- export { findExports, extractPropHints };
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
+ }