@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.
Files changed (51) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +11 -0
  3. package/dist/api/index.d.ts +42 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +567 -0
  6. package/dist/context/index.d.ts +61 -0
  7. package/dist/context/index.d.ts.map +1 -0
  8. package/dist/context/index.js +75 -0
  9. package/dist/index.d.ts +43 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +49 -0
  12. package/dist/middleware/index.d.ts +97 -0
  13. package/dist/middleware/index.d.ts.map +1 -0
  14. package/dist/middleware/index.js +469 -0
  15. package/dist/observability/index.d.ts +190 -0
  16. package/dist/observability/index.d.ts.map +1 -0
  17. package/dist/observability/index.js +399 -0
  18. package/dist/rsc/build-paths.d.ts +3 -0
  19. package/dist/rsc/build-paths.d.ts.map +1 -0
  20. package/dist/rsc/build-paths.js +8 -0
  21. package/dist/rsc/entries/client.d.ts +14 -0
  22. package/dist/rsc/entries/client.d.ts.map +1 -0
  23. package/dist/rsc/entries/client.js +283 -0
  24. package/dist/rsc/entries/rsc.d.ts +13 -0
  25. package/dist/rsc/entries/rsc.d.ts.map +1 -0
  26. package/dist/rsc/entries/rsc.js +932 -0
  27. package/dist/rsc/entries/ssr.d.ts +13 -0
  28. package/dist/rsc/entries/ssr.d.ts.map +1 -0
  29. package/dist/rsc/entries/ssr.js +245 -0
  30. package/dist/rsc/index.d.ts +78 -0
  31. package/dist/rsc/index.d.ts.map +1 -0
  32. package/dist/rsc/index.js +1368 -0
  33. package/dist/rsc/nitro-build.d.ts +36 -0
  34. package/dist/rsc/nitro-build.d.ts.map +1 -0
  35. package/dist/rsc/nitro-build.js +396 -0
  36. package/dist/rsc/optimized-boundary.d.ts +20 -0
  37. package/dist/rsc/optimized-boundary.d.ts.map +1 -0
  38. package/dist/rsc/optimized-boundary.js +15 -0
  39. package/dist/rsc/server-fn-transform.d.ts +6 -0
  40. package/dist/rsc/server-fn-transform.d.ts.map +1 -0
  41. package/dist/rsc/server-fn-transform.js +152 -0
  42. package/dist/rsc/types.d.ts +123 -0
  43. package/dist/rsc/types.d.ts.map +1 -0
  44. package/dist/rsc/types.js +1 -0
  45. package/dist/rsc/vite-plugin-nitro.d.ts +33 -0
  46. package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -0
  47. package/dist/rsc/vite-plugin-nitro.js +163 -0
  48. package/package.json +94 -0
  49. package/scripts/build.js +7 -0
  50. package/scripts/clean.js +6 -0
  51. package/scripts/run-nitro.mjs +18 -0
@@ -0,0 +1,399 @@
1
+ function errorMessage(error) {
2
+ if (error instanceof Error)
3
+ return error.message;
4
+ return String(error);
5
+ }
6
+ function stableFingerprint(parts) {
7
+ return parts
8
+ .map((part) => (part === undefined || part === null ? "na" : String(part).trim()))
9
+ .join(":");
10
+ }
11
+ function toPositiveInt(value, fallback) {
12
+ if (typeof value !== "number" || !Number.isFinite(value))
13
+ return fallback;
14
+ return Math.max(1, Math.floor(value));
15
+ }
16
+ export function observabilityPlugin(options = {}) {
17
+ const now = () => Date.now();
18
+ const service = options.service || process.env.FARM_SERVICE || "farm-app";
19
+ const environment = options.environment || process.env.NODE_ENV || "development";
20
+ const tags = options.tags || {};
21
+ const telemetry = options.telemetry || {};
22
+ const detection = options.detection || {};
23
+ const workflow = options.workflow || {};
24
+ const slowRequestMs = toPositiveInt(telemetry.slowRequestMs ?? options.slowRequestMs, 300);
25
+ const dedupeWindowMs = toPositiveInt(detection.dedupeWindowMs, 60_000);
26
+ const logLifecycle = telemetry.logLifecycle ?? options.logLifecycle ?? true;
27
+ const annotateHtml = telemetry.annotateHtml ?? true;
28
+ const detectionEnabled = detection.enabled ?? true;
29
+ const continueOnStepError = workflow.continueOnStepError ?? true;
30
+ const requestStarts = new WeakMap();
31
+ const recentIncidentsByFingerprint = new Map();
32
+ let signalCounter = 0;
33
+ let incidentCounter = 0;
34
+ const nextSignalId = () => `sig_${now()}_${(signalCounter++).toString(36)}`;
35
+ const nextIncidentId = () => `inc_${now()}_${(incidentCounter++).toString(36)}`;
36
+ const detectionContext = { now };
37
+ const createDefaultIncident = (signal) => {
38
+ if (signal.kind === "runtime.error" && signal.error) {
39
+ return {
40
+ kind: "runtime-error",
41
+ title: `Runtime error in ${signal.error.phase}`,
42
+ severity: "critical",
43
+ fingerprint: stableFingerprint(["runtime-error", signal.error.phase, signal.error.message]),
44
+ summary: signal.error.message,
45
+ metadata: { phase: signal.error.phase, name: signal.error.name },
46
+ };
47
+ }
48
+ if (signal.kind === "request.completed" && signal.request) {
49
+ if (signal.request.statusCode >= 500) {
50
+ return {
51
+ kind: "request-failure",
52
+ title: `Request failure ${signal.request.method} ${signal.request.pathname}`,
53
+ severity: "high",
54
+ fingerprint: stableFingerprint([
55
+ "request-failure",
56
+ signal.request.method,
57
+ signal.request.pathname,
58
+ signal.request.statusCode,
59
+ ]),
60
+ summary: `Request returned ${signal.request.statusCode}`,
61
+ metadata: {
62
+ method: signal.request.method,
63
+ pathname: signal.request.pathname,
64
+ statusCode: signal.request.statusCode,
65
+ durationMs: signal.request.durationMs,
66
+ },
67
+ };
68
+ }
69
+ if (signal.request.durationMs >= slowRequestMs) {
70
+ return {
71
+ kind: "slow-request",
72
+ title: `Slow request ${signal.request.method} ${signal.request.pathname}`,
73
+ severity: "medium",
74
+ fingerprint: stableFingerprint([
75
+ "slow-request",
76
+ signal.request.method,
77
+ signal.request.pathname,
78
+ ]),
79
+ summary: `Request took ${signal.request.durationMs}ms`,
80
+ metadata: {
81
+ method: signal.request.method,
82
+ pathname: signal.request.pathname,
83
+ statusCode: signal.request.statusCode,
84
+ durationMs: signal.request.durationMs,
85
+ thresholdMs: slowRequestMs,
86
+ },
87
+ };
88
+ }
89
+ }
90
+ if (signal.kind === "api.response" && signal.api && signal.api.status >= 500) {
91
+ return {
92
+ kind: "api-failure",
93
+ title: `API failure ${signal.api.method} ${signal.api.pathname}`,
94
+ severity: "high",
95
+ fingerprint: stableFingerprint([
96
+ "api-failure",
97
+ signal.api.method,
98
+ signal.api.pathname,
99
+ signal.api.status,
100
+ ]),
101
+ summary: `API returned ${signal.api.status}`,
102
+ metadata: {
103
+ method: signal.api.method,
104
+ pathname: signal.api.pathname,
105
+ status: signal.api.status,
106
+ },
107
+ };
108
+ }
109
+ return null;
110
+ };
111
+ const shouldDedupe = (fingerprint) => {
112
+ const seenAt = recentIncidentsByFingerprint.get(fingerprint);
113
+ if (seenAt && now() - seenAt < dedupeWindowMs) {
114
+ return true;
115
+ }
116
+ recentIncidentsByFingerprint.set(fingerprint, now());
117
+ return false;
118
+ };
119
+ const applyRules = async (signal) => {
120
+ const rules = detection.rules || [];
121
+ for (const rule of rules) {
122
+ const matched = await rule.when(signal, detectionContext);
123
+ if (!matched)
124
+ continue;
125
+ const partial = (await rule.buildIncident?.(signal, detectionContext)) || {};
126
+ const defaultIncident = createDefaultIncident(signal);
127
+ return {
128
+ kind: partial.kind || defaultIncident?.kind || `rule:${rule.id}`,
129
+ title: partial.title || defaultIncident?.title || `Incident from rule ${rule.id}`,
130
+ severity: partial.severity || rule.severity || defaultIncident?.severity || "high",
131
+ fingerprint: partial.fingerprint ||
132
+ defaultIncident?.fingerprint ||
133
+ stableFingerprint(["rule", rule.id, signal.kind]),
134
+ summary: partial.summary || defaultIncident?.summary,
135
+ metadata: {
136
+ ...defaultIncident?.metadata,
137
+ ...partial.metadata,
138
+ ruleId: rule.id,
139
+ },
140
+ };
141
+ }
142
+ return null;
143
+ };
144
+ const resolveIncident = async (signal) => {
145
+ const mapped = await detection.mapSignalToIncident?.(signal, detectionContext);
146
+ if (mapped) {
147
+ return {
148
+ id: mapped.id || nextIncidentId(),
149
+ kind: mapped.kind || "mapped-incident",
150
+ title: mapped.title || "Mapped incident",
151
+ severity: mapped.severity || "high",
152
+ fingerprint: mapped.fingerprint || stableFingerprint(["mapped-incident", mapped.kind, signal.kind]),
153
+ detectedAt: mapped.detectedAt || now(),
154
+ signalId: mapped.signalId || signal.id,
155
+ summary: mapped.summary,
156
+ metadata: mapped.metadata,
157
+ };
158
+ }
159
+ const byRule = await applyRules(signal);
160
+ const chosen = byRule || createDefaultIncident(signal);
161
+ if (!chosen)
162
+ return null;
163
+ return {
164
+ id: nextIncidentId(),
165
+ kind: chosen.kind,
166
+ title: chosen.title,
167
+ severity: chosen.severity,
168
+ fingerprint: chosen.fingerprint,
169
+ detectedAt: now(),
170
+ signalId: signal.id,
171
+ summary: chosen.summary,
172
+ metadata: chosen.metadata,
173
+ };
174
+ };
175
+ const runWorkflow = async (signal, incident) => {
176
+ const context = {
177
+ signal,
178
+ incident,
179
+ state: {},
180
+ fixResult: null,
181
+ };
182
+ const callbacks = workflow.callbacks || {};
183
+ const userActions = workflow.actions || {};
184
+ const legacyOnIncident = workflow.onIncident;
185
+ const builtInActions = {
186
+ notify: async (ctx) => {
187
+ const onIncident = callbacks.onIncident || legacyOnIncident;
188
+ if (onIncident) {
189
+ await onIncident(ctx);
190
+ return;
191
+ }
192
+ console.warn(`[obs] incident detected kind=${ctx.incident.kind} severity=${ctx.incident.severity} id=${ctx.incident.id} title="${ctx.incident.title}"`);
193
+ },
194
+ log: async (ctx) => {
195
+ console.log(`[obs] pipeline incident=${ctx.incident.id} kind=${ctx.incident.kind} severity=${ctx.incident.severity}`);
196
+ },
197
+ };
198
+ // Backward-compatibility shims. Prefer defining these steps via workflow.actions.
199
+ const legacyActions = {};
200
+ if (workflow.runFix) {
201
+ legacyActions.fix = async (ctx) => {
202
+ const result = (await workflow.runFix?.(ctx)) || null;
203
+ ctx.fixResult = result;
204
+ return result;
205
+ };
206
+ }
207
+ if (workflow.openPullRequest) {
208
+ legacyActions.pullRequest = async (ctx) => {
209
+ if (!ctx.fixResult)
210
+ return null;
211
+ await workflow.openPullRequest?.(ctx);
212
+ return ctx.fixResult;
213
+ };
214
+ }
215
+ const actionRegistry = {
216
+ ...builtInActions,
217
+ ...legacyActions,
218
+ ...userActions,
219
+ };
220
+ const hasFixPath = typeof workflow.runFix === "function";
221
+ const hasPrPath = typeof workflow.openPullRequest === "function";
222
+ const pipeline = workflow.pipeline && workflow.pipeline.length > 0
223
+ ? workflow.pipeline
224
+ : hasFixPath
225
+ ? hasPrPath
226
+ ? ["notify", "fix", "pullRequest"]
227
+ : ["notify", "fix"]
228
+ : ["notify"];
229
+ try {
230
+ await callbacks.onPipelineStart?.(context);
231
+ for (const step of pipeline) {
232
+ const action = actionRegistry[step];
233
+ if (!action) {
234
+ const message = `[obs] pipeline step "${step}" is not registered`;
235
+ if (!continueOnStepError)
236
+ throw new Error(message);
237
+ console.warn(message);
238
+ continue;
239
+ }
240
+ try {
241
+ await callbacks.onStepStart?.({ step, context });
242
+ const result = await action(context);
243
+ if (result !== undefined) {
244
+ context.state[step] = result;
245
+ if (step === "fix") {
246
+ context.fixResult = result;
247
+ }
248
+ }
249
+ await callbacks.onStepComplete?.({ step, context, result });
250
+ }
251
+ catch (error) {
252
+ await callbacks.onStepError?.({ step, context, error });
253
+ const message = `[obs] pipeline step "${step}" failed: ${errorMessage(error)}`;
254
+ if (!continueOnStepError)
255
+ throw new Error(message);
256
+ console.error(message);
257
+ }
258
+ }
259
+ await callbacks.onPipelineComplete?.(context);
260
+ }
261
+ catch (error) {
262
+ await callbacks.onPipelineError?.({ error, context });
263
+ throw error;
264
+ }
265
+ };
266
+ const processSignal = async (signal) => {
267
+ if (!detectionEnabled)
268
+ return;
269
+ const incident = await resolveIncident(signal);
270
+ if (!incident)
271
+ return;
272
+ if (shouldDedupe(incident.fingerprint))
273
+ return;
274
+ try {
275
+ await runWorkflow(signal, incident);
276
+ }
277
+ catch (error) {
278
+ console.error(`[obs] workflow failed incident=${incident.id} error=${errorMessage(error)}`);
279
+ }
280
+ };
281
+ const createSignal = (kind, payload) => {
282
+ return {
283
+ id: nextSignalId(),
284
+ kind,
285
+ timestamp: now(),
286
+ service,
287
+ environment,
288
+ tags,
289
+ ...payload,
290
+ };
291
+ };
292
+ return {
293
+ name: "@farm.js/plugin-observability",
294
+ enforce: "post",
295
+ init() {
296
+ if (logLifecycle)
297
+ console.log("[obs] init");
298
+ },
299
+ ready() {
300
+ if (logLifecycle)
301
+ console.log("[obs] ready");
302
+ },
303
+ beforeRequest(req) {
304
+ const pathname = req.url || "/";
305
+ const method = req.method || "GET";
306
+ const requestId = `req_${now()}_${(signalCounter++).toString(36)}`;
307
+ requestStarts.set(req, {
308
+ startedAt: now(),
309
+ method,
310
+ pathname,
311
+ requestId,
312
+ });
313
+ },
314
+ async afterResponse(req, res) {
315
+ const started = requestStarts.get(req);
316
+ if (!started)
317
+ return;
318
+ requestStarts.delete(req);
319
+ const signal = createSignal("request.completed", {
320
+ request: {
321
+ method: started.method,
322
+ pathname: started.pathname,
323
+ statusCode: res.statusCode || 200,
324
+ durationMs: now() - started.startedAt,
325
+ requestId: started.requestId,
326
+ },
327
+ });
328
+ await processSignal(signal);
329
+ },
330
+ async afterApiHandler(response, api) {
331
+ const signal = createSignal("api.response", {
332
+ api: {
333
+ method: api.method,
334
+ pathname: api.pathname,
335
+ status: response.status,
336
+ },
337
+ });
338
+ await processSignal(signal);
339
+ return response;
340
+ },
341
+ afterRender(html, render) {
342
+ if (!annotateHtml)
343
+ return html;
344
+ const marker = `<!-- observability:path=${render.pathname} route=${render.routePattern ?? "unmatched"} -->`;
345
+ return html.includes("</body>")
346
+ ? html.replace("</body>", `${marker}\n</body>`)
347
+ : `${html}\n${marker}`;
348
+ },
349
+ async onError(error) {
350
+ const message = errorMessage(error.error);
351
+ const signal = createSignal("runtime.error", {
352
+ error: {
353
+ phase: error.phase,
354
+ message,
355
+ name: error.error instanceof Error ? error.error.name : undefined,
356
+ },
357
+ });
358
+ await processSignal(signal);
359
+ },
360
+ hmrUpdate(update) {
361
+ if (!logLifecycle)
362
+ return;
363
+ console.log(`[obs] hmr file=${update.file} modules=${update.modules.length}`);
364
+ },
365
+ async afterBundle(result) {
366
+ if (logLifecycle) {
367
+ const state = result.success ? "success" : "failed";
368
+ console.log(`[obs] bundle ${state} preset=${result.preset} root=${result.root}`);
369
+ }
370
+ const signal = createSignal("build.result", {
371
+ build: {
372
+ success: result.success,
373
+ preset: result.preset,
374
+ root: result.root,
375
+ },
376
+ });
377
+ await processSignal(signal);
378
+ },
379
+ async afterNitroBuild(payload) {
380
+ if (logLifecycle) {
381
+ console.log(`[obs] nitro preset=${payload.preset} output=${payload.outputDir}`);
382
+ }
383
+ const signal = createSignal("nitro.build", {
384
+ build: {
385
+ success: true,
386
+ preset: payload.preset,
387
+ root: payload.root || process.cwd(),
388
+ outputDir: payload.outputDir,
389
+ },
390
+ });
391
+ await processSignal(signal);
392
+ },
393
+ shutdown(payload) {
394
+ if (logLifecycle)
395
+ console.log(`[obs] shutdown reason=${payload.reason}`);
396
+ recentIncidentsByFingerprint.clear();
397
+ },
398
+ };
399
+ }
@@ -0,0 +1,3 @@
1
+ /** Resolve a Vite environment outDir without prefixing an already-absolute path. */
2
+ export declare function resolveRscBuildOutputPath(root: string, outDir: string, ...segments: string[]): string;
3
+ //# sourceMappingURL=build-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-paths.d.ts","sourceRoot":"","sources":["../../src/rsc/build-paths.ts"],"names":[],"mappings":"AAEA,oFAAoF;AACpF,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,GAAG,QAAQ,EAAE,MAAM,EAAE,GACpB,MAAM,CAKR"}
@@ -0,0 +1,8 @@
1
+ import path from "path";
2
+ /** Resolve a Vite environment outDir without prefixing an already-absolute path. */
3
+ export function resolveRscBuildOutputPath(root, outDir, ...segments) {
4
+ const resolvedOutDir = path.isAbsolute(outDir)
5
+ ? path.normalize(outDir)
6
+ : path.resolve(root, outDir);
7
+ return path.join(resolvedOutDir, ...segments);
8
+ }
@@ -0,0 +1,14 @@
1
+ import type { EntryContext } from "../types.js";
2
+ /**
3
+ * Generates the browser entry file.
4
+ *
5
+ * This entry file:
6
+ * - Reads the embedded RSC payload from the HTML (via rsc-html-stream)
7
+ * - Deserializes it to React elements
8
+ * - Sets up client-side navigation (intercepts links, handles popstate)
9
+ * - Registers server action callback if enabled
10
+ * - Hydrates the page
11
+ * - Listens for HMR updates from server components
12
+ */
13
+ export declare function generateClientEntry(ctx: EntryContext): string;
14
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAiR7D"}