@farm.js/plugin 0.1.0-beta.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 +22 -0
- package/README.md +11 -0
- package/dist/api/index.d.ts +42 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +567 -0
- package/dist/context/index.d.ts +61 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +75 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/middleware/index.d.ts +97 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +469 -0
- package/dist/observability/index.d.ts +190 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +399 -0
- package/dist/rsc/build-paths.d.ts +3 -0
- package/dist/rsc/build-paths.d.ts.map +1 -0
- package/dist/rsc/build-paths.js +8 -0
- package/dist/rsc/entries/client.d.ts +14 -0
- package/dist/rsc/entries/client.d.ts.map +1 -0
- package/dist/rsc/entries/client.js +283 -0
- package/dist/rsc/entries/rsc.d.ts +13 -0
- package/dist/rsc/entries/rsc.d.ts.map +1 -0
- package/dist/rsc/entries/rsc.js +932 -0
- package/dist/rsc/entries/ssr.d.ts +13 -0
- package/dist/rsc/entries/ssr.d.ts.map +1 -0
- package/dist/rsc/entries/ssr.js +245 -0
- package/dist/rsc/index.d.ts +78 -0
- package/dist/rsc/index.d.ts.map +1 -0
- package/dist/rsc/index.js +1368 -0
- package/dist/rsc/nitro-build.d.ts +36 -0
- package/dist/rsc/nitro-build.d.ts.map +1 -0
- package/dist/rsc/nitro-build.js +396 -0
- package/dist/rsc/optimized-boundary.d.ts +20 -0
- package/dist/rsc/optimized-boundary.d.ts.map +1 -0
- package/dist/rsc/optimized-boundary.js +15 -0
- package/dist/rsc/server-fn-transform.d.ts +6 -0
- package/dist/rsc/server-fn-transform.d.ts.map +1 -0
- package/dist/rsc/server-fn-transform.js +152 -0
- package/dist/rsc/types.d.ts +123 -0
- package/dist/rsc/types.d.ts.map +1 -0
- package/dist/rsc/types.js +1 -0
- package/dist/rsc/vite-plugin-nitro.d.ts +33 -0
- package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -0
- package/dist/rsc/vite-plugin-nitro.js +163 -0
- package/package.json +94 -0
- package/scripts/build.js +7 -0
- package/scripts/clean.js +6 -0
- package/scripts/run-nitro.mjs +18 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the browser entry file.
|
|
3
|
+
*
|
|
4
|
+
* This entry file:
|
|
5
|
+
* - Reads the embedded RSC payload from the HTML (via rsc-html-stream)
|
|
6
|
+
* - Deserializes it to React elements
|
|
7
|
+
* - Sets up client-side navigation (intercepts links, handles popstate)
|
|
8
|
+
* - Registers server action callback if enabled
|
|
9
|
+
* - Hydrates the page
|
|
10
|
+
* - Listens for HMR updates from server components
|
|
11
|
+
*/
|
|
12
|
+
export function generateClientEntry(ctx) {
|
|
13
|
+
const debugLog = `// Debug disabled`;
|
|
14
|
+
const globalStylesheetImport = ctx.globalCssPath
|
|
15
|
+
? `const farmGlobalStylesheets = import.meta.glob(${JSON.stringify(ctx.globalCssPath)}, {
|
|
16
|
+
eager: true,
|
|
17
|
+
import: 'default',
|
|
18
|
+
query: '?url',
|
|
19
|
+
});
|
|
20
|
+
export const farmGlobalStylesheet = Object.values(farmGlobalStylesheets)[0];
|
|
21
|
+
`
|
|
22
|
+
: "";
|
|
23
|
+
let imports = `${globalStylesheetImport}
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
26
|
+
import { createFromReadableStream } from '@vitejs/plugin-rsc/browser';
|
|
27
|
+
import { rscStream } from 'rsc-html-stream/client';
|
|
28
|
+
import {
|
|
29
|
+
createFarmDeploymentMismatchError,
|
|
30
|
+
createFarmDeploymentRequestHeaders,
|
|
31
|
+
isFarmDeploymentMismatchResponse,
|
|
32
|
+
} from '@farm.js/core/deployment';
|
|
33
|
+
`;
|
|
34
|
+
if (ctx.actionsEnabled) {
|
|
35
|
+
imports += `import { setServerCallback, encodeReply, createTemporaryReferenceSet } from '@vitejs/plugin-rsc/browser';
|
|
36
|
+
import { applyFarmCacheInvalidations } from '@farm.js/core/cache';
|
|
37
|
+
import {
|
|
38
|
+
beginFarmServerQueryAction,
|
|
39
|
+
completeFarmServerQueryAction,
|
|
40
|
+
} from '@farm.js/core/server-query/client';
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
// Module-level ref so the server-action callback can update UI (assigned in BrowserRoot useEffect).
|
|
44
|
+
// Set __viteRscCallServer immediately so it's never undefined when other chunks call it (then replace with real impl).
|
|
45
|
+
let actionSetup = "";
|
|
46
|
+
if (ctx.actionsEnabled) {
|
|
47
|
+
actionSetup = `
|
|
48
|
+
// Ref for payload setter (used by server action callback and refetch)
|
|
49
|
+
const setPayloadRef = { current: null };
|
|
50
|
+
|
|
51
|
+
// Ensure __viteRscCallServer is a function before any other chunk may call it (avoids "is not a function")
|
|
52
|
+
if (typeof globalThis.__viteRscCallServer !== 'function') {
|
|
53
|
+
globalThis.__viteRscCallServer = () => Promise.reject(new Error('Farm.js: server actions not ready'));
|
|
54
|
+
}
|
|
55
|
+
// Register real callback (replaces placeholder above)
|
|
56
|
+
setServerCallback(async (id, args) => {
|
|
57
|
+
debug('Invoking server action:', id);
|
|
58
|
+
const serverQueryInvocation = beginFarmServerQueryAction(id, args);
|
|
59
|
+
const refs = createTemporaryReferenceSet();
|
|
60
|
+
const body = await encodeReply(args, { temporaryReferences: refs });
|
|
61
|
+
const headers = createFarmDeploymentRequestHeaders(farmDeploymentId, {
|
|
62
|
+
'x-farm-action-id': id,
|
|
63
|
+
'Accept': 'text/x-component',
|
|
64
|
+
});
|
|
65
|
+
if (typeof body === 'string') headers.set('Content-Type', 'text/plain; charset=utf-8');
|
|
66
|
+
else if (!(body instanceof FormData)) headers.set('Content-Type', 'application/octet-stream');
|
|
67
|
+
const res = await fetch(location.href, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers,
|
|
70
|
+
body,
|
|
71
|
+
cache: 'no-store',
|
|
72
|
+
credentials: 'same-origin',
|
|
73
|
+
redirect: 'error',
|
|
74
|
+
});
|
|
75
|
+
if (isFarmDeploymentMismatchResponse(res, farmDeploymentId)) {
|
|
76
|
+
throw reportDeploymentMismatch(res);
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
const text = await res.text();
|
|
80
|
+
console.error('[Farm.js] Server action request failed:', res.status, text);
|
|
81
|
+
throw new Error('Server action failed: ' + res.status);
|
|
82
|
+
}
|
|
83
|
+
let p;
|
|
84
|
+
try {
|
|
85
|
+
p = await createFromReadableStream(res.body, { temporaryReferences: refs });
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('[Farm.js] Failed to deserialize action response:', e);
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
const hasContent = p && (typeof p.root !== 'undefined' || typeof p.rootContent !== 'undefined');
|
|
91
|
+
if (!hasContent) {
|
|
92
|
+
console.error('[Farm.js] Action response missing payload.root / payload.rootContent');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setPayloadRef.current?.(p);
|
|
96
|
+
applyFarmCacheInvalidations(p.returnValue?.invalidations);
|
|
97
|
+
if (!p.returnValue || !p.returnValue.ok) {
|
|
98
|
+
debug('Server action failed:', id);
|
|
99
|
+
const error = new Error(p.returnValue?.data?.message || 'Server function failed');
|
|
100
|
+
error.name = 'ServerActionError';
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
return completeFarmServerQueryAction(serverQueryInvocation, p.returnValue.data);
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
actionSetup = `
|
|
109
|
+
const setPayloadRef = { current: null };
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
return `${imports}
|
|
113
|
+
${actionSetup}
|
|
114
|
+
const farmDeploymentId = ${JSON.stringify(ctx.deploymentId)};
|
|
115
|
+
|
|
116
|
+
function reportDeploymentMismatch(response) {
|
|
117
|
+
const error = createFarmDeploymentMismatchError(response, farmDeploymentId);
|
|
118
|
+
globalThis.dispatchEvent?.(new CustomEvent('farm:deployment-mismatch', { detail: error }));
|
|
119
|
+
return error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Debug logging helper
|
|
123
|
+
function debug(...args) {
|
|
124
|
+
${debugLog}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function main() {
|
|
128
|
+
// Prevent double execution (e.g. script loaded twice)
|
|
129
|
+
if (globalThis.__FARM_RSC_HYDRATED) return;
|
|
130
|
+
|
|
131
|
+
debug('Starting client hydration');
|
|
132
|
+
|
|
133
|
+
// Clean duplicate DOM as early as possible (server may have sent two blocks)
|
|
134
|
+
const rootEl = document.getElementById('root');
|
|
135
|
+
if (rootEl) {
|
|
136
|
+
while (rootEl.children.length > 1) rootEl.lastElementChild.remove();
|
|
137
|
+
while (rootEl.nextElementSibling) rootEl.nextElementSibling.remove();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Deserialize the initial RSC payload embedded in HTML
|
|
141
|
+
// rscStream extracts the payload from <script> tags added by SSR
|
|
142
|
+
let initial;
|
|
143
|
+
try {
|
|
144
|
+
initial = await createFromReadableStream(rscStream);
|
|
145
|
+
debug('Initial RSC payload deserialized');
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.error('[Farm.js] Failed to deserialize RSC payload:', e);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Root component that manages RSC state
|
|
152
|
+
function BrowserRoot() {
|
|
153
|
+
const [payload, set] = React.useState(initial);
|
|
154
|
+
|
|
155
|
+
// Expose setter for external updates (navigation, actions, HMR)
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
setPayloadRef.current = (p) => React.startTransition(() => set(p));
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
// Keep document metadata in sync when an RSC navigation swaps only #root.
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
if (typeof payload.metadata?.title === 'string') {
|
|
163
|
+
document.title = payload.metadata.title;
|
|
164
|
+
} else {
|
|
165
|
+
document.title = '';
|
|
166
|
+
}
|
|
167
|
+
let description = document.querySelector('meta[name="description"]');
|
|
168
|
+
if (typeof payload.metadata?.description === 'string') {
|
|
169
|
+
if (!description) {
|
|
170
|
+
description = document.createElement('meta');
|
|
171
|
+
description.setAttribute('name', 'description');
|
|
172
|
+
document.head.appendChild(description);
|
|
173
|
+
}
|
|
174
|
+
description.setAttribute('content', payload.metadata.description);
|
|
175
|
+
} else {
|
|
176
|
+
description?.remove();
|
|
177
|
+
}
|
|
178
|
+
}, [payload.metadata?.title, payload.metadata?.description]);
|
|
179
|
+
|
|
180
|
+
// Set up client-side navigation
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
// Re-fetch RSC when URL changes
|
|
183
|
+
const nav = () => refetch(location.href);
|
|
184
|
+
|
|
185
|
+
// Handle browser back/forward
|
|
186
|
+
window.addEventListener('popstate', nav);
|
|
187
|
+
|
|
188
|
+
// Intercept link clicks for client-side navigation
|
|
189
|
+
const handleClick = (e) => {
|
|
190
|
+
const a = e.target.closest('a');
|
|
191
|
+
if (a?.href && a.origin === location.origin && !a.download && !a.target) {
|
|
192
|
+
// Check for special attributes that should skip SPA navigation
|
|
193
|
+
if (a.hasAttribute('data-native') || a.hasAttribute('data-reload')) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
history.pushState(null, '', a.href);
|
|
199
|
+
nav();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
document.addEventListener('click', handleClick, true);
|
|
204
|
+
|
|
205
|
+
return () => {
|
|
206
|
+
window.removeEventListener('popstate', nav);
|
|
207
|
+
document.removeEventListener('click', handleClick, true);
|
|
208
|
+
};
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
// Never render payload.root (full document) on the client - it creates a second visible block (entire page duplicated below).
|
|
212
|
+
// Only render rootContent (layout+page for #root).
|
|
213
|
+
const content = payload.rootContent;
|
|
214
|
+
if (content == null) {
|
|
215
|
+
if (payload.root != null) console.warn('[Farm.js] payload.rootContent missing; not using payload.root to avoid duplicate block. Keys:', Object.keys(payload));
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return content;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fetch new RSC payload for a URL
|
|
222
|
+
async function refetch(url) {
|
|
223
|
+
debug('Fetching RSC for:', url);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Request RSC format instead of HTML
|
|
227
|
+
const res = await fetch(url, {
|
|
228
|
+
headers: createFarmDeploymentRequestHeaders(farmDeploymentId, {
|
|
229
|
+
Accept: 'text/x-component',
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
if (isFarmDeploymentMismatchResponse(res, farmDeploymentId)) {
|
|
233
|
+
reportDeploymentMismatch(res);
|
|
234
|
+
location.assign(url);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
console.error('[Farm.js] RSC fetch failed:', res.status);
|
|
240
|
+
// Fall back to full page navigation
|
|
241
|
+
location.href = url;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const newPayload = await createFromReadableStream(res.body);
|
|
246
|
+
setPayloadRef.current?.(newPayload);
|
|
247
|
+
debug('RSC navigation complete');
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.error('[Farm.js] RSC navigation failed:', e);
|
|
250
|
+
// Fall back to full page navigation
|
|
251
|
+
location.href = url;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const rootElForHydrate = document.getElementById('root');
|
|
255
|
+
if (!rootElForHydrate) {
|
|
256
|
+
console.error('[Farm.js] #root element not found');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Final cleanup pass before hydrate (in case DOM changed during async)
|
|
260
|
+
while (rootElForHydrate.children.length > 1) rootElForHydrate.lastElementChild.remove();
|
|
261
|
+
while (rootElForHydrate.nextElementSibling) rootElForHydrate.nextElementSibling.remove();
|
|
262
|
+
debug('Hydrating application');
|
|
263
|
+
globalThis.__FARM_RSC_HYDRATED = true;
|
|
264
|
+
hydrateRoot(rootElForHydrate, React.createElement(BrowserRoot), {
|
|
265
|
+
formState: initial.formState,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Handle HMR for server components
|
|
269
|
+
// When server code changes, re-fetch and re-render
|
|
270
|
+
if (import.meta.hot) {
|
|
271
|
+
import.meta.hot.on('rsc:update', () => {
|
|
272
|
+
debug('HMR update received, refetching...');
|
|
273
|
+
refetch(location.href);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Start the application
|
|
279
|
+
main().catch((e) => {
|
|
280
|
+
console.error('[Farm.js] Client initialization failed:', e);
|
|
281
|
+
});
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntryContext } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generates the RSC environment entry file.
|
|
4
|
+
*
|
|
5
|
+
* This entry file:
|
|
6
|
+
* - Auto-discovers page files using import.meta.glob
|
|
7
|
+
* - Implements file-based routing by matching URL paths to page files
|
|
8
|
+
* - Handles server actions if enabled (decoding arguments, executing, returning results)
|
|
9
|
+
* - Renders the React tree to an RSC stream
|
|
10
|
+
* - Either returns the stream directly (for client navigation) or delegates to SSR (for initial page load)
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateRscEntry(ctx: EntryContext): string;
|
|
13
|
+
//# sourceMappingURL=rsc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAg7B1D"}
|