@getforma/core 1.3.0 → 1.5.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/dist/{chunk-AJP4OVCN.js → chunk-3U2IQIKB.js} +4 -4
- package/dist/{chunk-AJP4OVCN.js.map → chunk-3U2IQIKB.js.map} +1 -1
- package/dist/{chunk-FBM7V4NE.cjs → chunk-D46EJ3VS.cjs} +43 -43
- package/dist/{chunk-FBM7V4NE.cjs.map → chunk-D46EJ3VS.cjs.map} +1 -1
- package/dist/{chunk-Y3A2EVVS.js → chunk-F5QUU76U.js} +4 -4
- package/dist/{chunk-Y3A2EVVS.js.map → chunk-F5QUU76U.js.map} +1 -1
- package/dist/{chunk-ETPJTPYD.js → chunk-FAIWRZKJ.js} +6 -3
- package/dist/chunk-FAIWRZKJ.js.map +1 -0
- package/dist/{chunk-H7MDUHS5.cjs → chunk-HAULTMNX.cjs} +6 -2
- package/dist/chunk-HAULTMNX.cjs.map +1 -0
- package/dist/{chunk-KUXNZ5MG.cjs → chunk-PAKPQRVE.cjs} +14 -12
- package/dist/{chunk-KUXNZ5MG.cjs.map → chunk-PAKPQRVE.cjs.map} +1 -1
- package/dist/formajs.global.js +69 -11
- package/dist/formajs.global.js.map +1 -1
- package/dist/http.cjs +11 -11
- package/dist/http.js +2 -2
- package/dist/index.cjs +143 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.ts +39 -5
- package/dist/index.js +67 -16
- package/dist/index.js.map +1 -1
- package/dist/{resource-D6HfVgiA.d.cts → resource-CBTcorBJ.d.ts} +2 -2
- package/dist/{resource-ljs2X5NM.d.ts → resource-CaFfIB5i.d.cts} +2 -2
- package/dist/runtime.cjs +28 -28
- package/dist/runtime.js +3 -3
- package/dist/server.cjs +69 -16
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +31 -5
- package/dist/server.d.ts +31 -5
- package/dist/server.js +64 -12
- package/dist/server.js.map +1 -1
- package/dist/{signal-CRBJYQ6B.d.cts → signal-CIXTR4Qz.d.cts} +29 -1
- package/dist/{signal-CRBJYQ6B.d.ts → signal-CIXTR4Qz.d.ts} +29 -1
- package/dist/ssr/index.cjs +22 -4
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +7 -0
- package/dist/ssr/index.d.ts +7 -0
- package/dist/ssr/index.js +22 -4
- package/dist/ssr/index.js.map +1 -1
- package/dist/storage.cjs +66 -16
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.js +66 -16
- package/dist/storage.js.map +1 -1
- package/dist/tc39-compat.cjs +3 -3
- package/dist/tc39-compat.d.cts +1 -1
- package/dist/tc39-compat.d.ts +1 -1
- package/dist/tc39-compat.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ETPJTPYD.js.map +0 -1
- package/dist/chunk-H7MDUHS5.cjs.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { batch } from './chunk-
|
|
2
|
-
import { createSignal } from './chunk-
|
|
1
|
+
import { batch } from './chunk-F5QUU76U.js';
|
|
2
|
+
import { createSignal } from './chunk-FAIWRZKJ.js';
|
|
3
3
|
|
|
4
4
|
// src/server/action.ts
|
|
5
5
|
function createAction(serverFn, options) {
|
|
@@ -119,18 +119,55 @@ function getServerFunction(endpoint) {
|
|
|
119
119
|
function getRegisteredEndpoints() {
|
|
120
120
|
return [...registry.keys()];
|
|
121
121
|
}
|
|
122
|
+
var globalGuard;
|
|
123
|
+
function setRPCGuard(fn) {
|
|
124
|
+
globalGuard = fn;
|
|
125
|
+
}
|
|
126
|
+
var FORBIDDEN_ARG_KEYS = ["__proto__", "constructor", "prototype"];
|
|
127
|
+
function deepStripForbidden(value) {
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
for (const v of value) deepStripForbidden(v);
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
if (value && typeof value === "object") {
|
|
133
|
+
for (const key of FORBIDDEN_ARG_KEYS) {
|
|
134
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
135
|
+
const desc = Object.getOwnPropertyDescriptor(value, key);
|
|
136
|
+
if (desc?.configurable) delete value[key];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of Object.keys(value)) {
|
|
140
|
+
deepStripForbidden(value[k]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
122
145
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
123
|
-
async function handleRPC(endpoint, body, revalidateData) {
|
|
146
|
+
async function handleRPC(endpoint, body, revalidateData, options) {
|
|
124
147
|
const endpointName = endpoint.split("/").pop() ?? "";
|
|
125
148
|
if (FORBIDDEN_KEYS.has(endpointName)) {
|
|
126
|
-
return { error: "Forbidden endpoint name" };
|
|
149
|
+
return { error: "Forbidden endpoint name", status: 403 };
|
|
150
|
+
}
|
|
151
|
+
if (!body || !Array.isArray(body.args)) {
|
|
152
|
+
return { error: "Invalid RPC request: missing args array", status: 400 };
|
|
127
153
|
}
|
|
128
154
|
const fn = registry.get(endpoint);
|
|
129
155
|
if (!fn) {
|
|
130
|
-
return { error: `Unknown server function: ${endpoint}
|
|
156
|
+
return { error: `Unknown server function: ${endpoint}`, status: 404 };
|
|
157
|
+
}
|
|
158
|
+
const args = deepStripForbidden([...body.args]);
|
|
159
|
+
const guard = options?.authorize ?? globalGuard;
|
|
160
|
+
if (guard) {
|
|
161
|
+
let ok;
|
|
162
|
+
try {
|
|
163
|
+
ok = !!await guard(endpoint, args, options?.context ?? {});
|
|
164
|
+
} catch {
|
|
165
|
+
return { error: "Forbidden", status: 403 };
|
|
166
|
+
}
|
|
167
|
+
if (!ok) return { error: "Forbidden", status: 403 };
|
|
131
168
|
}
|
|
132
169
|
try {
|
|
133
|
-
const result = await fn(...
|
|
170
|
+
const result = await fn(...args);
|
|
134
171
|
if (revalidateData) {
|
|
135
172
|
return { data: result, __revalidate: revalidateData };
|
|
136
173
|
}
|
|
@@ -138,29 +175,44 @@ async function handleRPC(endpoint, body, revalidateData) {
|
|
|
138
175
|
} catch (err) {
|
|
139
176
|
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
|
|
140
177
|
const message = isDev && err instanceof Error ? err.message : "Internal server error";
|
|
141
|
-
return { error: message };
|
|
178
|
+
return { error: message, status: 500 };
|
|
142
179
|
}
|
|
143
180
|
}
|
|
144
|
-
function createRPCMiddleware() {
|
|
181
|
+
function createRPCMiddleware(opts) {
|
|
145
182
|
return async (req, res) => {
|
|
146
183
|
if (req.method !== "POST") {
|
|
147
184
|
res.status(405).json({ error: "Method not allowed" });
|
|
148
185
|
return;
|
|
149
186
|
}
|
|
187
|
+
const h = req.headers ?? {};
|
|
188
|
+
if (h["x-forma-rpc"] !== "1") {
|
|
189
|
+
res.status(403).json({ error: "Missing X-Forma-RPC header" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const ct = String(h["content-type"] ?? "");
|
|
193
|
+
if (!ct.includes("application/json")) {
|
|
194
|
+
res.status(415).json({ error: "Unsupported Media Type" });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
150
197
|
const body = req.body;
|
|
151
198
|
if (!body || !Array.isArray(body.args)) {
|
|
152
199
|
res.status(400).json({ error: "Invalid RPC request: missing args array" });
|
|
153
200
|
return;
|
|
154
201
|
}
|
|
155
|
-
const
|
|
202
|
+
const path = req.path ?? req.url.split("?")[0];
|
|
203
|
+
const result = await handleRPC(path, body, void 0, {
|
|
204
|
+
authorize: opts?.guard,
|
|
205
|
+
context: { req, headers: h }
|
|
206
|
+
});
|
|
156
207
|
if (result.error) {
|
|
157
|
-
res.status(500).json(result);
|
|
208
|
+
res.status(result.status ?? 500).json({ error: result.error });
|
|
158
209
|
} else {
|
|
159
|
-
|
|
210
|
+
const { status: _status, ...rest } = result;
|
|
211
|
+
res.json(rest);
|
|
160
212
|
}
|
|
161
213
|
};
|
|
162
214
|
}
|
|
163
215
|
|
|
164
|
-
export { $$serverFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, unregisterResource, withRevalidation };
|
|
216
|
+
export { $$serverFunction, applyRevalidation, createAction, createRPCMiddleware, enableAutoRevalidation, getRegisteredEndpoints, getServerFunction, handleRPC, registerResource, registerServerFunction, setRPCGuard, unregisterResource, withRevalidation };
|
|
165
217
|
//# sourceMappingURL=server.js.map
|
|
166
218
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/action.ts","../src/server/mutation.ts","../src/server/rpc-client.ts","../src/server/rpc-handler.ts"],"names":[],"mappings":";;;;AAiFO,SAAS,YAAA,CACd,UACA,OAAA,EACsB;AACtB,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,aAAa,KAAK,CAAA;AAChD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,aAAsB,MAAS,CAAA;AAEzD,EAAA,MAAM,MAAA,GAAS,UAAU,IAAA,KAAgC;AACvD,IAAA,UAAA,CAAW,IAAI,CAAA;AACf,IAAA,QAAA,CAAS,MAAS,CAAA;AAGlB,IAAA,IAAI,SAAS,UAAA,EAAY;AACvB,MAAA,IAAI;AACF,QAAA,KAAA,CAAM,MAAM,OAAA,CAAQ,UAAA,CAAY,GAAG,IAAI,CAAC,CAAA;AAAA,MAC1C,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,GAAG,IAAI,CAAA;AAGrC,MAAA,IAAI,SAAS,SAAA,EAAW;AACtB,QAAA,KAAA,CAAM,MAAM,OAAA,CAAQ,SAAA,CAAW,MAAA,EAAQ,GAAG,IAAI,CAAC,CAAA;AAAA,MACjD;AAGA,MAAA,IAAI,SAAS,WAAA,EAAa;AACxB,QAAA,KAAA,MAAW,QAAA,IAAY,QAAQ,WAAA,EAAa;AAC1C,UAAA,QAAA,CAAS,OAAA,EAAQ;AAAA,QACnB;AAAA,MACF;AAEA,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,GAAA,EAAK;AAEZ,MAAA,IAAI,SAAS,OAAA,EAAS;AACpB,QAAA,IAAI;AACF,UAAA,KAAA,CAAM,MAAM,OAAA,CAAQ,OAAA,CAAS,GAAA,EAAK,GAAG,IAAI,CAAC,CAAA;AAAA,QAC5C,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAEA,MAAA,QAAA,CAAS,GAAG,CAAA;AACZ,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA;AAGA,EAAA,MAAM,WAAA,GAAc,MAAA;AACpB,EAAA,WAAA,CAAY,OAAA,GAAU,OAAA;AACtB,EAAA,WAAA,CAAY,KAAA,GAAQ,KAAA;AACpB,EAAA,WAAA,CAAY,UAAA,GAAa,MAAM,QAAA,CAAS,MAAS,CAAA;AAEjD,EAAA,OAAO,WAAA;AACT;;;ACvGA,IAAM,gBAAA,uBAAuB,GAAA,EAA+B;AAarD,SAAS,gBAAA,CAAiB,KAAa,QAAA,EAAmC;AAC/E,EAAA,gBAAA,CAAiB,GAAA,CAAI,KAAK,QAAQ,CAAA;AACpC;AAKO,SAAS,mBAAmB,GAAA,EAAmB;AACpD,EAAA,gBAAA,CAAiB,OAAO,GAAG,CAAA;AAC7B;AAOO,SAAS,kBAAkB,cAAA,EAA+C;AAC/E,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,SAAS,KAAK,MAAA,CAAO,OAAA,CAAQ,cAAc,CAAA,EAAG;AAC7D,IAAA,MAAM,QAAA,GAAW,gBAAA,CAAiB,GAAA,CAAI,GAAG,CAAA;AACzC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,QAAA,CAAS,OAAO,SAAS,CAAA;AAAA,IAC3B;AAAA,EACF;AACF;AAaO,SAAS,sBAAA,GAAqC;AACnD,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAEjC,IAAA,OAAO,MAAM;AAAA,IAAC,CAAA;AAAA,EAChB;AAEA,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAiB;AAChC,IAAA,MAAM,SAAU,KAAA,CAAsB,MAAA;AACtC,IAAA,IAAI,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,EAAU;AACxC,MAAA,iBAAA,CAAkB,MAAiC,CAAA;AAAA,IACrD;AAAA,EACF,CAAA;AAEA,EAAA,MAAA,CAAO,gBAAA,CAAiB,oBAAoB,OAAO,CAAA;AAGnD,EAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,kBAAA,EAAoB,OAAO,CAAA;AACrE;AAkBO,SAAS,gBAAA,CAAoB,MAAS,UAAA,EAA0D;AACrG,EAAA,OAAO,EAAE,IAAA,EAAM,YAAA,EAAc,UAAA,EAAW;AAC1C;;;AC9GO,SAAS,iBACd,QAAA,EACG;AACH,EAAA,MAAM,KAAA,GAAQ,UAAU,IAAA,KAAsC;AAC5D,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,QAAA,EAAU;AAAA,MACrC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,aAAA,EAAe;AAAA,OACjB;AAAA,MACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,MAAM;AAAA,KAC9B,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,SAAS,MAAM,CAAA,GAAA,EAAM,SAAS,CAAA,CAAE,CAAA;AAAA,IAC7E;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,IAAA,EAAK;AAInC,IAAA,IAAI,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,IAAY,kBAAkB,MAAA,EAAQ;AAEpE,MAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,QAAA,MAAA,CAAO,aAAA,CAAc,IAAI,WAAA,CAAY,kBAAA,EAAoB;AAAA,UACvD,QAAQ,MAAA,CAAO;AAAA,SAChB,CAAC,CAAA;AAAA,MACJ;AACA,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAEA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO,KAAA;AACT;;;ACxCA,IAAM,QAAA,uBAAe,GAAA,EAA4B;AAM1C,SAAS,sBAAA,CAAuB,UAAkB,EAAA,EAA0B;AACjF,EAAA,QAAA,CAAS,GAAA,CAAI,UAAU,EAAE,CAAA;AAC3B;AAKO,SAAS,kBAAkB,QAAA,EAA8C;AAC9E,EAAA,OAAO,QAAA,CAAS,IAAI,QAAQ,CAAA;AAC9B;AAKO,SAAS,sBAAA,GAAmC;AACjD,EAAA,OAAO,CAAC,GAAG,QAAA,CAAS,IAAA,EAAM,CAAA;AAC5B;AAwCA,IAAM,iCAAiB,IAAI,GAAA,CAAI,CAAC,WAAA,EAAa,aAAA,EAAe,WAAW,CAAC,CAAA;AAExE,eAAsB,SAAA,CACpB,QAAA,EACA,IAAA,EACA,cAAA,EACsB;AAEtB,EAAA,MAAM,eAAe,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,KAAI,IAAK,EAAA;AAClD,EAAA,IAAI,cAAA,CAAe,GAAA,CAAI,YAAY,CAAA,EAAG;AACpC,IAAA,OAAO,EAAE,OAAO,yBAAA,EAA0B;AAAA,EAC5C;AAEA,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAA;AAChC,EAAA,IAAI,CAAC,EAAA,EAAI;AACP,IAAA,OAAO,EAAE,KAAA,EAAO,CAAA,yBAAA,EAA4B,QAAQ,CAAA,CAAA,EAAG;AAAA,EACzD;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,GAAG,KAAK,IAAI,CAAA;AAEpC,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,YAAA,EAAc,cAAA,EAAe;AAAA,IACtD;AAEA,IAAA,OAAO,EAAE,MAAM,MAAA,EAAO;AAAA,EACxB,SAAS,GAAA,EAAK;AAEZ,IAAA,MAAM,QAAQ,OAAO,OAAA,KAAY,WAAA,IAC5B,OAAA,CAAQ,KAAK,QAAA,KAAa,aAAA;AAC/B,IAAA,MAAM,OAAA,GAAU,KAAA,IAAS,GAAA,YAAe,KAAA,GACpC,IAAI,OAAA,GACJ,uBAAA;AACJ,IAAA,OAAO,EAAE,OAAO,OAAA,EAAQ;AAAA,EAC1B;AACF;AAUO,SAAS,mBAAA,GAAsB;AACpC,EAAA,OAAO,OAAO,KAAsD,GAAA,KAAwG;AAC1K,IAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,sBAAsB,CAAA;AACpD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,OAAO,GAAA,CAAI,IAAA;AACjB,IAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAA,EAAG;AACtC,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,2CAA2C,CAAA;AACzE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,GAAA,CAAI,KAAK,IAAI,CAAA;AAC5C,IAAA,IAAI,OAAO,KAAA,EAAO;AAChB,MAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,MAAM,CAAA;AAAA,IAC7B,CAAA,MAAO;AACL,MAAA,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,IACjB;AAAA,EACF,CAAA;AACF","file":"server.js","sourcesContent":["/**\n * FormaJS Server - Action\n *\n * Creates an action that wraps a server function with optimistic UI support.\n * The action immediately applies an optimistic update, then reconciles when\n * the server responds (or rolls back on error).\n */\n\nimport { createSignal, batch } from '../reactive/index.js';\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ActionOptions<Args extends unknown[], Result> {\n /**\n * Apply an optimistic update immediately when the action is called.\n * This runs BEFORE the server function.\n * Store whatever you need to roll back in the return value or via closures.\n */\n optimistic?: (...args: Args) => void;\n\n /**\n * Called when the server function resolves successfully.\n * Use this to apply the server result to local state.\n */\n onSuccess?: (result: Result, ...args: Args) => void;\n\n /**\n * Called when the server function rejects.\n * Use this to roll back the optimistic update.\n */\n onError?: (error: unknown, ...args: Args) => void;\n\n /**\n * Resources to refetch after the action completes successfully.\n * Used with single-flight mutations: if the server response includes\n * revalidation data, these resources are updated without a refetch.\n */\n invalidates?: Resource<unknown>[];\n}\n\nexport interface Action<Args extends unknown[], Result> {\n /** Execute the action. */\n (...args: Args): Promise<Result>;\n /** Whether the action is currently in-flight. */\n pending: () => boolean;\n /** The last error from the action (or undefined). */\n error: () => unknown;\n /** Clear the error state. */\n clearError: () => void;\n}\n\n/**\n * Create an action that wraps a server function with optimistic UI.\n *\n * ```ts\n * const addTodo = createAction(\n * serverCreateTodo,\n * {\n * optimistic: (text) => {\n * // Immediately add to local list\n * setTodos(prev => [...prev, { text, done: false, id: 'temp' }]);\n * },\n * onSuccess: (result) => {\n * // Replace temp item with server result\n * setTodos(prev => prev.map(t => t.id === 'temp' ? result : t));\n * },\n * onError: (err, text) => {\n * // Remove the optimistic item\n * setTodos(prev => prev.filter(t => t.id !== 'temp'));\n * },\n * invalidates: [todosResource],\n * },\n * );\n *\n * // Use:\n * await addTodo('Buy milk');\n * ```\n */\nexport function createAction<Args extends unknown[], Result>(\n serverFn: (...args: Args) => Promise<Result>,\n options?: ActionOptions<Args, Result>,\n): Action<Args, Result> {\n const [pending, setPending] = createSignal(false);\n const [error, setError] = createSignal<unknown>(undefined);\n\n const action = async (...args: Args): Promise<Result> => {\n setPending(true);\n setError(undefined);\n\n // Apply optimistic update immediately\n if (options?.optimistic) {\n try {\n batch(() => options.optimistic!(...args));\n } catch {\n // Swallow errors in optimistic callback\n }\n }\n\n try {\n const result = await serverFn(...args);\n\n // Apply success handler\n if (options?.onSuccess) {\n batch(() => options.onSuccess!(result, ...args));\n }\n\n // Revalidate dependent resources\n if (options?.invalidates) {\n for (const resource of options.invalidates) {\n resource.refetch();\n }\n }\n\n setPending(false);\n return result;\n } catch (err) {\n // Apply error/rollback handler\n if (options?.onError) {\n try {\n batch(() => options.onError!(err, ...args));\n } catch {\n // Swallow errors in rollback\n }\n }\n\n setError(err);\n setPending(false);\n throw err;\n }\n };\n\n // Attach signal accessors to the action function\n const typedAction = action as Action<Args, Result>;\n typedAction.pending = pending;\n typedAction.error = error;\n typedAction.clearError = () => setError(undefined);\n\n return typedAction;\n}\n","/**\n * FormaJS Server - Mutation\n *\n * Single-flight mutation pattern: the server response carries both the\n * mutation result AND fresh data for dependent resources in one round trip.\n *\n * Without single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { id: 1, text: \"Buy milk\" }\n * Client -> Server: GET /api/todos (refetch to update list)\n * Server -> Client: [all todos]\n *\n * With single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { data: {...}, __revalidate: { \"/api/todos\": [all todos] } }\n * (No second request needed!)\n */\n\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationResponse<T> {\n /** The mutation result. */\n data: T;\n /**\n * Fresh data for dependent resources, keyed by resource identifier.\n * When present, the client updates these resources directly instead of refetching.\n */\n __revalidate?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Resource registry for revalidation\n// ---------------------------------------------------------------------------\n\nconst resourceRegistry = new Map<string, Resource<unknown>>();\n\n/**\n * Register a resource with a key so it can be revalidated by single-flight mutations.\n *\n * ```ts\n * const todos = createResource(\n * () => true,\n * () => fetch('/api/todos').then(r => r.json()),\n * );\n * registerResource('/api/todos', todos);\n * ```\n */\nexport function registerResource(key: string, resource: Resource<unknown>): void {\n resourceRegistry.set(key, resource);\n}\n\n/**\n * Unregister a resource (call on cleanup/unmount).\n */\nexport function unregisterResource(key: string): void {\n resourceRegistry.delete(key);\n}\n\n/**\n * Apply revalidation data from a single-flight mutation response.\n * For each key in the revalidate map, find the matching resource\n * and mutate it directly with the fresh data (skipping a refetch).\n */\nexport function applyRevalidation(revalidateData: Record<string, unknown>): void {\n for (const [key, freshData] of Object.entries(revalidateData)) {\n const resource = resourceRegistry.get(key);\n if (resource) {\n resource.mutate(freshData);\n }\n }\n}\n\n/**\n * Listen for revalidation events dispatched by $$serverFunction.\n * Call this once during app initialization to enable automatic\n * single-flight mutation handling.\n *\n * ```ts\n * // In your app entry:\n * import { enableAutoRevalidation } from 'forma/server/mutation';\n * enableAutoRevalidation();\n * ```\n */\nexport function enableAutoRevalidation(): () => void {\n if (typeof window === 'undefined') {\n // SSR/Node.js — no-op\n return () => {};\n }\n\n const handler = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n if (detail && typeof detail === 'object') {\n applyRevalidation(detail as Record<string, unknown>);\n }\n };\n\n window.addEventListener('forma:revalidate', handler);\n\n // Return cleanup function\n return () => window.removeEventListener('forma:revalidate', handler);\n}\n\n/**\n * Wrap a server function response to include revalidation data.\n * Use this on the server side to enable single-flight mutations.\n *\n * ```ts\n * // Server-side:\n * async function createTodo(text: string) {\n * \"use server\";\n * const newTodo = await db.insert('todos', { text, done: false });\n * const allTodos = await db.query('todos');\n * return withRevalidation(newTodo, {\n * '/api/todos': allTodos,\n * });\n * }\n * ```\n */\nexport function withRevalidation<T>(data: T, revalidate: Record<string, unknown>): MutationResponse<T> {\n return { data, __revalidate: revalidate };\n}\n","/**\n * FormaJS Server - RPC Client\n *\n * Provides the client-side stub function that replaces \"use server\" function\n * bodies after compilation. Each call becomes a fetch POST to the server endpoint.\n */\n\n/**\n * Create an RPC stub function for a server function.\n * This replaces the original function body on the client side.\n *\n * @param endpoint - The RPC endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @returns An async function that sends args to the server and returns the result\n */\nexport function $$serverFunction<T extends (...args: unknown[]) => Promise<unknown>>(\n endpoint: string,\n): T {\n const rpcFn = async (...args: unknown[]): Promise<unknown> => {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Forma-RPC': '1',\n },\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Server function failed (${response.status}): ${errorText}`);\n }\n\n const result = await response.json();\n\n // If the response includes revalidation data (single-flight mutations),\n // return it alongside the result\n if (result && typeof result === 'object' && '__revalidate' in result) {\n // Dispatch a custom event so createAction can pick up the revalidation data\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('forma:revalidate', {\n detail: result.__revalidate,\n }));\n }\n return result.data;\n }\n\n return result;\n };\n\n return rpcFn as T;\n}\n","/**\n * FormaJS Server - RPC Handler\n *\n * Server-side registry and request handler for \"use server\" functions.\n * Framework-agnostic — works with any Node.js HTTP server, Express, Hono, etc.\n */\n\nexport type ServerFunction = (...args: unknown[]) => Promise<unknown>;\n\n/** Registry of server functions by endpoint path. */\nconst registry = new Map<string, ServerFunction>();\n\n/**\n * Register a server function at a specific endpoint.\n * Called by the server-side compiled output.\n */\nexport function registerServerFunction(endpoint: string, fn: ServerFunction): void {\n registry.set(endpoint, fn);\n}\n\n/**\n * Get a registered server function by endpoint.\n */\nexport function getServerFunction(endpoint: string): ServerFunction | undefined {\n return registry.get(endpoint);\n}\n\n/**\n * Get all registered server function endpoints.\n */\nexport function getRegisteredEndpoints(): string[] {\n return [...registry.keys()];\n}\n\nexport interface RPCRequest {\n args: unknown[];\n}\n\nexport interface RPCResponse {\n data?: unknown;\n error?: string;\n __revalidate?: Record<string, unknown>;\n}\n\n/**\n * Handle an incoming RPC request.\n * Call this from your HTTP server's request handler.\n *\n * Usage with any HTTP framework:\n * ```ts\n * import { handleRPC } from 'forma/server/rpc-handler';\n *\n * // Express example:\n * app.post('/rpc/:endpoint', async (req, res) => {\n * const result = await handleRPC(req.path, req.body);\n * res.json(result);\n * });\n *\n * // Hono example:\n * app.post('/rpc/*', async (c) => {\n * const body = await c.req.json();\n * const result = await handleRPC(c.req.path, body);\n * return c.json(result);\n * });\n * ```\n *\n * @param endpoint - The full endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @param body - The parsed request body containing { args: [...] }\n * @param revalidateData - Optional revalidation data to include in the response\n * (for single-flight mutations)\n */\n/** \"Crash Barrier\": block prototype pollution attacks via endpoint names. */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport async function handleRPC(\n endpoint: string,\n body: RPCRequest,\n revalidateData?: Record<string, unknown>,\n): Promise<RPCResponse> {\n // Security: prevent prototype pollution via crafted endpoint names\n const endpointName = endpoint.split('/').pop() ?? '';\n if (FORBIDDEN_KEYS.has(endpointName)) {\n return { error: 'Forbidden endpoint name' };\n }\n\n const fn = registry.get(endpoint);\n if (!fn) {\n return { error: `Unknown server function: ${endpoint}` };\n }\n\n try {\n const result = await fn(...body.args);\n\n if (revalidateData) {\n return { data: result, __revalidate: revalidateData };\n }\n\n return { data: result };\n } catch (err) {\n // Don't leak internal error details to clients\n const isDev = typeof process !== 'undefined'\n && process.env?.NODE_ENV === 'development';\n const message = isDev && err instanceof Error\n ? err.message\n : 'Internal server error';\n return { error: message };\n }\n}\n\n/**\n * Create a middleware-style handler for use with Express-like frameworks.\n *\n * ```ts\n * import { createRPCMiddleware } from 'forma/server/rpc-handler';\n * app.use('/rpc', createRPCMiddleware());\n * ```\n */\nexport function createRPCMiddleware() {\n return async (req: { url: string; method: string; body?: unknown }, res: { json: (data: unknown) => void; status: (code: number) => { json: (data: unknown) => void } }) => {\n if (req.method !== 'POST') {\n res.status(405).json({ error: 'Method not allowed' });\n return;\n }\n\n const body = req.body as RPCRequest;\n if (!body || !Array.isArray(body.args)) {\n res.status(400).json({ error: 'Invalid RPC request: missing args array' });\n return;\n }\n\n const result = await handleRPC(req.url, body);\n if (result.error) {\n res.status(500).json(result);\n } else {\n res.json(result);\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/server/action.ts","../src/server/mutation.ts","../src/server/rpc-client.ts","../src/server/rpc-handler.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\n * FormaJS Server - Action\n *\n * Creates an action that wraps a server function with optimistic UI support.\n * The action immediately applies an optimistic update, then reconciles when\n * the server responds (or rolls back on error).\n */\n\nimport { createSignal, batch } from '../reactive/index.js';\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ActionOptions<Args extends unknown[], Result> {\n /**\n * Apply an optimistic update immediately when the action is called.\n * This runs BEFORE the server function.\n * Store whatever you need to roll back in the return value or via closures.\n */\n optimistic?: (...args: Args) => void;\n\n /**\n * Called when the server function resolves successfully.\n * Use this to apply the server result to local state.\n */\n onSuccess?: (result: Result, ...args: Args) => void;\n\n /**\n * Called when the server function rejects.\n * Use this to roll back the optimistic update.\n */\n onError?: (error: unknown, ...args: Args) => void;\n\n /**\n * Resources to refetch after the action completes successfully.\n * Used with single-flight mutations: if the server response includes\n * revalidation data, these resources are updated without a refetch.\n */\n invalidates?: Resource<unknown>[];\n}\n\nexport interface Action<Args extends unknown[], Result> {\n /** Execute the action. */\n (...args: Args): Promise<Result>;\n /** Whether the action is currently in-flight. */\n pending: () => boolean;\n /** The last error from the action (or undefined). */\n error: () => unknown;\n /** Clear the error state. */\n clearError: () => void;\n}\n\n/**\n * Create an action that wraps a server function with optimistic UI.\n *\n * ```ts\n * const addTodo = createAction(\n * serverCreateTodo,\n * {\n * optimistic: (text) => {\n * // Immediately add to local list\n * setTodos(prev => [...prev, { text, done: false, id: 'temp' }]);\n * },\n * onSuccess: (result) => {\n * // Replace temp item with server result\n * setTodos(prev => prev.map(t => t.id === 'temp' ? result : t));\n * },\n * onError: (err, text) => {\n * // Remove the optimistic item\n * setTodos(prev => prev.filter(t => t.id !== 'temp'));\n * },\n * invalidates: [todosResource],\n * },\n * );\n *\n * // Use:\n * await addTodo('Buy milk');\n * ```\n */\nexport function createAction<Args extends unknown[], Result>(\n serverFn: (...args: Args) => Promise<Result>,\n options?: ActionOptions<Args, Result>,\n): Action<Args, Result> {\n const [pending, setPending] = createSignal(false);\n const [error, setError] = createSignal<unknown>(undefined);\n\n const action = async (...args: Args): Promise<Result> => {\n setPending(true);\n setError(undefined);\n\n // Apply optimistic update immediately\n if (options?.optimistic) {\n try {\n batch(() => options.optimistic!(...args));\n } catch {\n // Swallow errors in optimistic callback\n }\n }\n\n try {\n const result = await serverFn(...args);\n\n // Apply success handler\n if (options?.onSuccess) {\n batch(() => options.onSuccess!(result, ...args));\n }\n\n // Revalidate dependent resources\n if (options?.invalidates) {\n for (const resource of options.invalidates) {\n resource.refetch();\n }\n }\n\n setPending(false);\n return result;\n } catch (err) {\n // Apply error/rollback handler\n if (options?.onError) {\n try {\n batch(() => options.onError!(err, ...args));\n } catch {\n // Swallow errors in rollback\n }\n }\n\n setError(err);\n setPending(false);\n throw err;\n }\n };\n\n // Attach signal accessors to the action function\n const typedAction = action as Action<Args, Result>;\n typedAction.pending = pending;\n typedAction.error = error;\n typedAction.clearError = () => setError(undefined);\n\n return typedAction;\n}\n","/**\n * FormaJS Server - Mutation\n *\n * Single-flight mutation pattern: the server response carries both the\n * mutation result AND fresh data for dependent resources in one round trip.\n *\n * Without single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { id: 1, text: \"Buy milk\" }\n * Client -> Server: GET /api/todos (refetch to update list)\n * Server -> Client: [all todos]\n *\n * With single-flight:\n * Client -> Server: createTodo(\"Buy milk\")\n * Server -> Client: { data: {...}, __revalidate: { \"/api/todos\": [all todos] } }\n * (No second request needed!)\n */\n\nimport type { Resource } from '../reactive/resource.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationResponse<T> {\n /** The mutation result. */\n data: T;\n /**\n * Fresh data for dependent resources, keyed by resource identifier.\n * When present, the client updates these resources directly instead of refetching.\n */\n __revalidate?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Resource registry for revalidation\n// ---------------------------------------------------------------------------\n\nconst resourceRegistry = new Map<string, Resource<unknown>>();\n\n/**\n * Register a resource with a key so it can be revalidated by single-flight mutations.\n *\n * ```ts\n * const todos = createResource(\n * () => true,\n * () => fetch('/api/todos').then(r => r.json()),\n * );\n * registerResource('/api/todos', todos);\n * ```\n */\nexport function registerResource(key: string, resource: Resource<unknown>): void {\n resourceRegistry.set(key, resource);\n}\n\n/**\n * Unregister a resource (call on cleanup/unmount).\n */\nexport function unregisterResource(key: string): void {\n resourceRegistry.delete(key);\n}\n\n/**\n * Apply revalidation data from a single-flight mutation response.\n * For each key in the revalidate map, find the matching resource\n * and mutate it directly with the fresh data (skipping a refetch).\n */\nexport function applyRevalidation(revalidateData: Record<string, unknown>): void {\n for (const [key, freshData] of Object.entries(revalidateData)) {\n const resource = resourceRegistry.get(key);\n if (resource) {\n resource.mutate(freshData);\n }\n }\n}\n\n/**\n * Listen for revalidation events dispatched by $$serverFunction.\n * Call this once during app initialization to enable automatic\n * single-flight mutation handling.\n *\n * ```ts\n * // In your app entry:\n * import { enableAutoRevalidation } from 'forma/server/mutation';\n * enableAutoRevalidation();\n * ```\n */\nexport function enableAutoRevalidation(): () => void {\n if (typeof window === 'undefined') {\n // SSR/Node.js — no-op\n return () => {};\n }\n\n const handler = (event: Event) => {\n const detail = (event as CustomEvent).detail;\n if (detail && typeof detail === 'object') {\n applyRevalidation(detail as Record<string, unknown>);\n }\n };\n\n window.addEventListener('forma:revalidate', handler);\n\n // Return cleanup function\n return () => window.removeEventListener('forma:revalidate', handler);\n}\n\n/**\n * Wrap a server function response to include revalidation data.\n * Use this on the server side to enable single-flight mutations.\n *\n * ```ts\n * // Server-side:\n * async function createTodo(text: string) {\n * \"use server\";\n * const newTodo = await db.insert('todos', { text, done: false });\n * const allTodos = await db.query('todos');\n * return withRevalidation(newTodo, {\n * '/api/todos': allTodos,\n * });\n * }\n * ```\n */\nexport function withRevalidation<T>(data: T, revalidate: Record<string, unknown>): MutationResponse<T> {\n return { data, __revalidate: revalidate };\n}\n","/**\n * FormaJS Server - RPC Client\n *\n * Provides the client-side stub function that replaces \"use server\" function\n * bodies after compilation. Each call becomes a fetch POST to the server endpoint.\n */\n\n/**\n * Create an RPC stub function for a server function.\n * This replaces the original function body on the client side.\n *\n * @param endpoint - The RPC endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @returns An async function that sends args to the server and returns the result\n */\nexport function $$serverFunction<T extends (...args: unknown[]) => Promise<unknown>>(\n endpoint: string,\n): T {\n const rpcFn = async (...args: unknown[]): Promise<unknown> => {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Forma-RPC': '1',\n },\n body: JSON.stringify({ args }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Server function failed (${response.status}): ${errorText}`);\n }\n\n const result = await response.json();\n\n // If the response includes revalidation data (single-flight mutations),\n // return it alongside the result\n if (result && typeof result === 'object' && '__revalidate' in result) {\n // Dispatch a custom event so createAction can pick up the revalidation data\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent('forma:revalidate', {\n detail: result.__revalidate,\n }));\n }\n return result.data;\n }\n\n return result;\n };\n\n return rpcFn as T;\n}\n","/**\n * FormaJS Server - RPC Handler\n *\n * Server-side registry and request handler for \"use server\" functions.\n * Framework-agnostic — works with any Node.js HTTP server, Express, Hono, etc.\n */\n\nexport type ServerFunction = (...args: unknown[]) => Promise<unknown>;\n\n/** Registry of server functions by endpoint path. */\nconst registry = new Map<string, ServerFunction>();\n\n/**\n * Register a server function at a specific endpoint.\n * Called by the server-side compiled output.\n */\nexport function registerServerFunction(endpoint: string, fn: ServerFunction): void {\n registry.set(endpoint, fn);\n}\n\n/**\n * Get a registered server function by endpoint.\n */\nexport function getServerFunction(endpoint: string): ServerFunction | undefined {\n return registry.get(endpoint);\n}\n\n/**\n * Get all registered server function endpoints.\n */\nexport function getRegisteredEndpoints(): string[] {\n return [...registry.keys()];\n}\n\nexport interface RPCRequest {\n args: unknown[];\n}\n\nexport interface RPCResponse {\n data?: unknown;\n error?: string;\n /** Suggested HTTP status for the middleware to map (error paths only). Not sent in the body. */\n status?: number;\n __revalidate?: Record<string, unknown>;\n}\n\n/** Context passed to an {@link RPCGuard} (the request and its headers, if available). */\nexport interface RPCContext {\n req?: unknown;\n headers?: Record<string, string | undefined>;\n}\n\n/**\n * Authorization guard for RPC calls. Return false (or a rejected/false promise)\n * to deny the call with 403. handleRPC performs NO authentication by itself —\n * install a guard to authenticate/authorize.\n */\nexport type RPCGuard = (endpoint: string, args: unknown[], ctx: RPCContext) => boolean | Promise<boolean>;\n\nlet globalGuard: RPCGuard | undefined;\n\n/** Install (or clear) a process-global RPC authorization guard. */\nexport function setRPCGuard(fn: RPCGuard | undefined): void {\n globalGuard = fn;\n}\n\n/** Options for {@link handleRPC}. */\nexport interface HandleRPCOptions {\n /** Per-call guard; overrides the global guard when provided. */\n authorize?: RPCGuard;\n /** Context forwarded to the guard. */\n context?: RPCContext;\n}\n\n/** Keys that enable prototype-pollution; stripped from RPC args before invocation. */\nconst FORBIDDEN_ARG_KEYS = ['__proto__', 'constructor', 'prototype'];\n\n/** Recursively delete prototype-pollution keys from parsed RPC arguments (mutating). */\nfunction deepStripForbidden<T>(value: T): T {\n if (Array.isArray(value)) {\n for (const v of value) deepStripForbidden(v);\n return value;\n }\n if (value && typeof value === 'object') {\n for (const key of FORBIDDEN_ARG_KEYS) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n // Guard the delete: it throws on a frozen/sealed (non-configurable) prop.\n const desc = Object.getOwnPropertyDescriptor(value, key);\n if (desc?.configurable) delete (value as Record<string, unknown>)[key];\n }\n }\n for (const k of Object.keys(value as Record<string, unknown>)) {\n deepStripForbidden((value as Record<string, unknown>)[k]);\n }\n }\n return value;\n}\n\n/**\n * Handle an incoming RPC request.\n * Call this from your HTTP server's request handler.\n *\n * Usage with any HTTP framework:\n * ```ts\n * import { handleRPC } from 'forma/server/rpc-handler';\n *\n * // Express example:\n * app.post('/rpc/:endpoint', async (req, res) => {\n * const result = await handleRPC(req.path, req.body);\n * res.json(result);\n * });\n *\n * // Hono example:\n * app.post('/rpc/*', async (c) => {\n * const body = await c.req.json();\n * const result = await handleRPC(c.req.path, body);\n * return c.json(result);\n * });\n * ```\n *\n * @param endpoint - The full endpoint path (e.g. \"/rpc/createTodo_a1b2c3\")\n * @param body - The parsed request body containing { args: [...] }\n * @param revalidateData - Optional revalidation data to include in the response\n * (for single-flight mutations)\n */\n/** \"Crash Barrier\": block prototype pollution attacks via endpoint names. */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport async function handleRPC(\n endpoint: string,\n body: RPCRequest,\n revalidateData?: Record<string, unknown>,\n options?: HandleRPCOptions,\n): Promise<RPCResponse> {\n // Security: prevent prototype pollution via crafted endpoint names\n const endpointName = endpoint.split('/').pop() ?? '';\n if (FORBIDDEN_KEYS.has(endpointName)) {\n return { error: 'Forbidden endpoint name', status: 403 };\n }\n\n // Enforce the args-array contract on the direct path too (not just middleware).\n if (!body || !Array.isArray(body.args)) {\n return { error: 'Invalid RPC request: missing args array', status: 400 };\n }\n\n const fn = registry.get(endpoint);\n if (!fn) {\n return { error: `Unknown server function: ${endpoint}`, status: 404 };\n }\n\n // Strip prototype-pollution keys from client-controlled args before invocation.\n const args = deepStripForbidden([...body.args]);\n\n // Authorization is the deployment's responsibility — run any installed guard.\n // A guard that throws or rejects is treated as a denial (fail-closed): return\n // 403 without leaking the guard's error to the client.\n const guard = options?.authorize ?? globalGuard;\n if (guard) {\n let ok: boolean;\n try {\n ok = !!(await guard(endpoint, args, options?.context ?? {}));\n } catch {\n return { error: 'Forbidden', status: 403 };\n }\n if (!ok) return { error: 'Forbidden', status: 403 };\n }\n\n try {\n const result = await fn(...args);\n\n if (revalidateData) {\n return { data: result, __revalidate: revalidateData };\n }\n\n return { data: result };\n } catch (err) {\n // Don't leak internal error details to clients\n const isDev = typeof process !== 'undefined'\n && process.env?.NODE_ENV === 'development';\n const message = isDev && err instanceof Error\n ? err.message\n : 'Internal server error';\n return { error: message, status: 500 };\n }\n}\n\n/**\n * Create a middleware-style handler for use with Express-like frameworks.\n *\n * ```ts\n * import { createRPCMiddleware } from 'forma/server/rpc-handler';\n * app.use('/rpc', createRPCMiddleware());\n * ```\n */\nexport function createRPCMiddleware(opts?: { guard?: RPCGuard }) {\n return async (\n req: { url: string; method: string; path?: string; body?: unknown; headers?: Record<string, string | undefined> },\n res: { json: (data: unknown) => void; status: (code: number) => { json: (data: unknown) => void } },\n ) => {\n if (req.method !== 'POST') {\n res.status(405).json({ error: 'Method not allowed' });\n return;\n }\n\n // CSRF mitigation: require the custom header (which forces a CORS preflight\n // and cannot be set by a cross-site HTML form) and a JSON content type.\n const h = req.headers ?? {};\n if (h['x-forma-rpc'] !== '1') {\n res.status(403).json({ error: 'Missing X-Forma-RPC header' });\n return;\n }\n const ct = String(h['content-type'] ?? '');\n if (!ct.includes('application/json')) {\n res.status(415).json({ error: 'Unsupported Media Type' });\n return;\n }\n\n const body = req.body as RPCRequest;\n if (!body || !Array.isArray(body.args)) {\n res.status(400).json({ error: 'Invalid RPC request: missing args array' });\n return;\n }\n\n // Resolve the endpoint path without the query string.\n const path = req.path ?? req.url.split('?')[0]!;\n const result = await handleRPC(path, body, undefined, {\n authorize: opts?.guard,\n context: { req, headers: h },\n });\n if (result.error) {\n res.status(result.status ?? 500).json({ error: result.error });\n } else {\n // Do not leak the internal `status` discriminator into the success body.\n const { status: _status, ...rest } = result;\n res.json(rest);\n }\n };\n}\n"]}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forma Reactive - Signal
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained reactive primitive backed by alien-signals.
|
|
5
|
+
* API: createSignal returns [getter, setter] tuple following SolidJS conventions.
|
|
6
|
+
*
|
|
7
|
+
* TC39 Signals equivalent: Signal.State
|
|
8
|
+
*/
|
|
9
|
+
/** Debug name of a signal getter, if one was provided (dev-only). */
|
|
10
|
+
declare function getSignalName(fn: unknown): string | undefined;
|
|
1
11
|
/** A function that reads the current value of a signal. */
|
|
2
12
|
type SignalGetter<T> = () => T;
|
|
3
13
|
/** A function that updates a signal — accepts a value or an updater function. */
|
|
@@ -23,6 +33,24 @@ interface SignalOptions<T> {
|
|
|
23
33
|
*/
|
|
24
34
|
equals?: (prev: T, next: T) => boolean;
|
|
25
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a value so the setter treats it as a literal value, not a functional updater.
|
|
38
|
+
*
|
|
39
|
+
* When `T` is itself a function type, passing a function to the setter is
|
|
40
|
+
* ambiguous -- it looks like a functional update (`prev => next`). Use
|
|
41
|
+
* `value()` to disambiguate:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const [getFn, setFn] = createSignal<() => void>(() => console.log('a'));
|
|
45
|
+
*
|
|
46
|
+
* // BUG: interpreted as a functional update -- calls the arrow with prev
|
|
47
|
+
* // setFn(() => console.log('b'));
|
|
48
|
+
*
|
|
49
|
+
* // Correct: wraps in a thunk so the setter stores it as-is
|
|
50
|
+
* setFn(value(() => console.log('b')));
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function value<T>(v: T): () => T;
|
|
26
54
|
/**
|
|
27
55
|
* Create a reactive signal.
|
|
28
56
|
*
|
|
@@ -44,4 +72,4 @@ interface SignalOptions<T> {
|
|
|
44
72
|
*/
|
|
45
73
|
declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
|
|
46
74
|
|
|
47
|
-
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c };
|
|
75
|
+
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c, getSignalName as g, value as v };
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forma Reactive - Signal
|
|
3
|
+
*
|
|
4
|
+
* Fine-grained reactive primitive backed by alien-signals.
|
|
5
|
+
* API: createSignal returns [getter, setter] tuple following SolidJS conventions.
|
|
6
|
+
*
|
|
7
|
+
* TC39 Signals equivalent: Signal.State
|
|
8
|
+
*/
|
|
9
|
+
/** Debug name of a signal getter, if one was provided (dev-only). */
|
|
10
|
+
declare function getSignalName(fn: unknown): string | undefined;
|
|
1
11
|
/** A function that reads the current value of a signal. */
|
|
2
12
|
type SignalGetter<T> = () => T;
|
|
3
13
|
/** A function that updates a signal — accepts a value or an updater function. */
|
|
@@ -23,6 +33,24 @@ interface SignalOptions<T> {
|
|
|
23
33
|
*/
|
|
24
34
|
equals?: (prev: T, next: T) => boolean;
|
|
25
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a value so the setter treats it as a literal value, not a functional updater.
|
|
38
|
+
*
|
|
39
|
+
* When `T` is itself a function type, passing a function to the setter is
|
|
40
|
+
* ambiguous -- it looks like a functional update (`prev => next`). Use
|
|
41
|
+
* `value()` to disambiguate:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const [getFn, setFn] = createSignal<() => void>(() => console.log('a'));
|
|
45
|
+
*
|
|
46
|
+
* // BUG: interpreted as a functional update -- calls the arrow with prev
|
|
47
|
+
* // setFn(() => console.log('b'));
|
|
48
|
+
*
|
|
49
|
+
* // Correct: wraps in a thunk so the setter stores it as-is
|
|
50
|
+
* setFn(value(() => console.log('b')));
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function value<T>(v: T): () => T;
|
|
26
54
|
/**
|
|
27
55
|
* Create a reactive signal.
|
|
28
56
|
*
|
|
@@ -44,4 +72,4 @@ interface SignalOptions<T> {
|
|
|
44
72
|
*/
|
|
45
73
|
declare function createSignal<T>(initialValue: T, options?: SignalOptions<T>): [get: SignalGetter<T>, set: SignalSetter<T>];
|
|
46
74
|
|
|
47
|
-
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c };
|
|
75
|
+
export { type SignalGetter as S, type SignalOptions as a, type SignalSetter as b, createSignal as c, getSignalName as g, value as v };
|
package/dist/ssr/index.cjs
CHANGED
|
@@ -266,6 +266,23 @@ function shSuspense(fallback, asyncChildren) {
|
|
|
266
266
|
children: [asyncChildren]
|
|
267
267
|
};
|
|
268
268
|
}
|
|
269
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("forma-suspense-timeout");
|
|
270
|
+
function raceTimeout(p, ms) {
|
|
271
|
+
if (ms == null || ms <= 0) return p;
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const t = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
274
|
+
p.then(
|
|
275
|
+
(v) => {
|
|
276
|
+
clearTimeout(t);
|
|
277
|
+
resolve(v);
|
|
278
|
+
},
|
|
279
|
+
(e) => {
|
|
280
|
+
clearTimeout(t);
|
|
281
|
+
reject(e);
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
269
286
|
async function* renderToStreamNew(node, options) {
|
|
270
287
|
const state = {
|
|
271
288
|
suspenseCounter: 0,
|
|
@@ -281,17 +298,18 @@ async function* renderToStreamNew(node, options) {
|
|
|
281
298
|
if (options?.bootstrapScript) {
|
|
282
299
|
yield options.bootstrapScript;
|
|
283
300
|
}
|
|
301
|
+
const suspenseTimeout = options?.suspenseTimeout;
|
|
284
302
|
while (state.pendingBoundaries.length > 0) {
|
|
285
303
|
const pending = [...state.pendingBoundaries];
|
|
286
304
|
state.pendingBoundaries = [];
|
|
287
305
|
const results = await Promise.allSettled(
|
|
288
306
|
pending.map(async (boundary) => {
|
|
289
|
-
const html = await boundary.promise;
|
|
307
|
+
const html = await raceTimeout(boundary.promise, suspenseTimeout);
|
|
290
308
|
return { id: boundary.id, html };
|
|
291
309
|
})
|
|
292
310
|
);
|
|
293
311
|
for (const result of results) {
|
|
294
|
-
if (result.status === "fulfilled") {
|
|
312
|
+
if (result.status === "fulfilled" && result.value.html !== TIMED_OUT) {
|
|
295
313
|
yield getSwapTag(result.value.id, result.value.html);
|
|
296
314
|
}
|
|
297
315
|
}
|
|
@@ -322,9 +340,9 @@ function renderStreamNode(node, parts, state) {
|
|
|
322
340
|
parts.push(`<div id="${id}" style="display:contents">`);
|
|
323
341
|
renderSync(fallback, parts);
|
|
324
342
|
parts.push("</div>");
|
|
325
|
-
const promise =
|
|
343
|
+
const promise = Promise.resolve().then(asyncFn).then((resolved) => {
|
|
326
344
|
const resolvedParts = [];
|
|
327
|
-
|
|
345
|
+
renderStreamNode(resolved, resolvedParts, state);
|
|
328
346
|
return resolvedParts.join("");
|
|
329
347
|
});
|
|
330
348
|
state.pendingBoundaries.push({ id, promise });
|
package/dist/ssr/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ssr/render.ts","../../src/ssr/ssr-reactive.ts","../../src/ssr/client-script.ts","../../src/ssr/stream.ts"],"names":["isEventHandlerAttr","isSafeAttrName","isUrlAttr","isDangerousUrl"],"mappings":"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","file":"index.cjs","sourcesContent":["// Void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n]);\n\n// Map of prop names to HTML attribute names\nexport const PROP_TO_ATTR: Record<string, string> = {\n className: 'class',\n htmlFor: 'for',\n tabIndex: 'tabindex',\n};\n\n// Escape HTML entities\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Escape attribute values\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, ''');\n}\n\nimport { isDangerousUrl, isUrlAttr, isEventHandlerAttr, isSafeAttrName } from '../security/url-safety.js';\n\n// Dangerous URI detection for href, src, action, formaction attributes.\n// Retained for backwards compatibility with external importers; the actual\n// scheme check now goes through `isDangerousUrl`, which additionally strips the\n// control characters browsers ignore in a URL scheme (see security/url-safety).\nexport const DANGEROUS_URI_ATTRS = new Set(['href', 'src', 'action', 'formaction']);\nexport const DANGEROUS_URI_RE = /^\\s*(javascript|vbscript|data\\s*:\\s*text\\/html)/i;\n\n/**\n * Decide whether a resolved prop should be emitted as an attribute, and return\n * the escaped `name=\"value\"` fragment (or `name` for boolean `true`). Returns\n * null when the attribute must be dropped for safety. Shared by the plain and\n * hydration-aware renderers so the security rules cannot drift apart.\n */\nexport function renderAttr(key: string, resolved: unknown): string | null {\n const attrName = PROP_TO_ATTR[key] ?? key;\n // Never emit event-handler or malformed attribute names — an attacker who\n // controls a prop key could otherwise inject `onload=…` or break out of the\n // attribute entirely with whitespace/quotes.\n if (isEventHandlerAttr(attrName) || !isSafeAttrName(attrName)) return null;\n if (resolved === true) return ' ' + attrName;\n if (resolved === false || resolved == null) return null;\n const str = String(resolved);\n if (isUrlAttr(attrName) && isDangerousUrl(str)) return null;\n return ' ' + attrName + '=\"' + escapeAttr(str) + '\"';\n}\n\nexport interface VNode {\n tag: string;\n props: Record<string, unknown> | null;\n children: unknown[];\n}\n\n/**\n * Server-side hyperscript — creates a virtual node instead of a DOM element.\n */\nexport function sh(tag: string, props?: Record<string, unknown> | null, ...children: unknown[]): VNode {\n return { tag, props: props ?? null, children };\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string.\n *\n * Uses an array buffer internally to avoid O(n^2) string concatenation\n * for large trees, then joins once at the end.\n *\n * Usage:\n * ```ts\n * import { renderToString, sh } from '@getforma/core/ssr';\n *\n * const html = renderToString(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()), // signal getters resolved synchronously\n * )\n * );\n * ```\n */\nexport function renderToString(node: unknown): string {\n const parts: string[] = [];\n renderToBuffer(node, parts);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer.\n * Avoids O(n^2) string concatenation for large trees.\n */\nfunction renderToBuffer(node: unknown, parts: string[]): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter) → resolve and render\n if (typeof node === 'function') { renderToBuffer(node(), parts); return; }\n\n // Array → render each\n if (Array.isArray(node)) {\n for (const child of node) renderToBuffer(child, parts);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBuffer(child, parts);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\nexport function isVNode(v: unknown): v is VNode {\n return v != null && typeof v === 'object' && 'tag' in v && 'children' in v;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration-aware rendering\n// ---------------------------------------------------------------------------\n\n/** Per-render hydration context — avoids shared module-level counter. */\ninterface HydrationContext {\n id: number;\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string with hydration markers.\n *\n * Like `renderToString`, but injects comment markers and `data-forma-h`\n * attributes so the client-side `hydrate()` function can adopt existing\n * DOM nodes without re-creating them.\n *\n * Each call creates its own hydration counter, so concurrent calls\n * (e.g. multiple SSR requests in the same process) produce independent,\n * non-overlapping hydration IDs.\n *\n * Marker types:\n * - `data-forma-h=\"N\"` — element boundary (attribute on the element)\n * - `<!--forma-t:N-->` / `<!--/forma-t:N-->` — reactive text boundary\n * - `<!--forma-l:N-->` / `<!--/forma-l:N-->` — list boundary\n *\n * Usage:\n * ```ts\n * import { renderToStringWithHydration, sh } from '@getforma/core/ssr';\n *\n * const html = renderToStringWithHydration(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()),\n * )\n * );\n * ```\n */\nexport function renderToStringWithHydration(node: unknown): string {\n const ctx: HydrationContext = { id: 0 };\n const parts: string[] = [];\n renderToBufferHydrated(node, parts, ctx);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer with hydration markers.\n * The `ctx` object carries the hydration counter so concurrent renders are isolated.\n */\nfunction renderToBufferHydrated(node: unknown, parts: string[], ctx: HydrationContext): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter / reactive text) → wrap with text markers\n if (typeof node === 'function') {\n const id = ctx.id++;\n parts.push(`<!--forma-t:${id}-->`);\n renderToBufferHydrated(node(), parts, ctx);\n parts.push(`<!--/forma-t:${id}-->`);\n return;\n }\n\n // Array → wrap with list markers\n if (Array.isArray(node)) {\n const id = ctx.id++;\n parts.push(`<!--forma-l:${id}-->`);\n for (const child of node) renderToBufferHydrated(child, parts, ctx);\n parts.push(`<!--/forma-l:${id}-->`);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const id = ctx.id++;\n const { tag, props, children } = node;\n\n // Add hydration data-attribute to element\n parts.push('<', tag, ` data-forma-h=\"${id}\"`);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBufferHydrated(child, parts, ctx);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\n","/**\n * SSR-only signal: just a value holder, no reactivity needed on server.\n */\nexport function ssrSignal<T>(initial: T): [get: () => T, set: (v: T) => void] {\n let value = initial;\n return [() => value, (v) => { value = v; }];\n}\n\nexport function ssrComputed<T>(fn: () => T): () => T {\n return fn; // On server, just call the function directly\n}\n","/**\n * FormaJS SSR - Client Swap Script\n *\n * Tiny inline script injected into the HTML stream that handles\n * out-of-order Suspense boundary resolution.\n *\n * When the server resolves a Suspense boundary, it sends:\n * <script>$FORMA_SWAP(\"forma-s:0\",\"<resolved html>\")</script>\n *\n * This script replaces the fallback placeholder with the resolved content.\n */\n\n/**\n * Returns the client-side swap script as a string, ready to be injected\n * into the HTML stream (typically right after <body> or before first Suspense).\n */\nexport function getSwapScript(): string {\n return `<script>\nfunction $FORMA_SWAP(id,html){\n var el=document.getElementById(id);\n if(el){\n var tpl=document.createElement('template');\n tpl.innerHTML=html;\n el.replaceWith(tpl.content);\n }\n}\n</script>`;\n}\n\n/**\n * Serializes a value to a JSON string that is safe to embed inside a\n * `<script>` block. Standard `JSON.stringify` does not escape `<` or `>`,\n * so a payload containing `</script>` would prematurely close the script\n * tag, enabling an injection attack. We also escape U+2028 and U+2029\n * which are valid JSON but treated as line terminators in JavaScript source.\n */\nfunction safeJsonStringify(val: unknown): string {\n return JSON.stringify(val)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * Returns a script tag that swaps a specific Suspense boundary's content.\n * Called when an async resource resolves during streaming.\n */\nexport function getSwapTag(id: string, html: string): string {\n // safeJsonStringify escapes <, >, U+2028, and U+2029 so the serialized\n // content cannot break out of the surrounding <script> block.\n return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;\n}\n","/**\n * FormaJS SSR - Streaming Renderer\n *\n * Implements true chunked HTML streaming with out-of-order Suspense resolution.\n *\n * How it works:\n * 1. Yields shell HTML immediately (opening tags, head, static content)\n * 2. When a Suspense boundary is encountered:\n * - Yields the fallback HTML with a placeholder: <div id=\"forma-s:N\" style=\"display:contents\">\n * - Continues rendering siblings without blocking\n * - When the async resource resolves, yields a swap script\n * 3. Suspense boundaries resolve independently in any order (out-of-order streaming)\n */\n\nimport { getSwapScript, getSwapTag } from './client-script.js';\nimport { escapeHtml, isVNode, VOID_ELEMENTS, renderAttr, type VNode } from './render.js';\n\n// ---------------------------------------------------------------------------\n// Suspense boundary tracking\n// ---------------------------------------------------------------------------\n\ninterface PendingBoundary {\n id: string;\n promise: Promise<string>;\n}\n\n/** Per-render mutable state, scoped to a single renderToStreamNew call. */\ninterface StreamState {\n suspenseCounter: number;\n pendingBoundaries: PendingBoundary[];\n}\n\n// ---------------------------------------------------------------------------\n// Synchronous render to buffer (for non-async content)\n// ---------------------------------------------------------------------------\n\nfunction renderSync(node: unknown, parts: string[]): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderSync(node(), parts); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderSync(child, parts);\n return;\n }\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderSync(child, parts);\n }\n parts.push('</', tag, '>');\n return;\n }\n parts.push(escapeHtml(String(node)));\n}\n\n// ---------------------------------------------------------------------------\n// Suspense VNode detection\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense boundary VNode has a special shape:\n * { tag: 'forma-suspense', props: { fallback: VNode }, children: [asyncChildren] }\n *\n * Or it can be created via:\n * shSuspense(fallbackVNode, () => asyncContent)\n */\nexport interface SuspenseVNode {\n tag: 'forma-suspense';\n props: { fallback: unknown };\n children: [() => Promise<unknown>];\n}\n\nfunction isSuspenseVNode(v: unknown): v is SuspenseVNode {\n return isVNode(v) && v.tag === 'forma-suspense';\n}\n\n/**\n * Create a server-side Suspense VNode for streaming SSR.\n */\nexport function shSuspense(fallback: unknown, asyncChildren: () => Promise<unknown>): SuspenseVNode {\n return {\n tag: 'forma-suspense',\n props: { fallback },\n children: [asyncChildren],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming renderer\n// ---------------------------------------------------------------------------\n\nexport interface StreamOptions {\n /** Script to bootstrap the client-side app (e.g. <script src=\"/app.js\"></script>) */\n bootstrapScript?: string;\n /** Whether to inject the $FORMA_SWAP client script (default: true) */\n injectSwapScript?: boolean;\n}\n\n/**\n * Render a FormaJS virtual tree to a stream of HTML chunks.\n *\n * Yields HTML progressively:\n * - Static content is yielded immediately\n * - Suspense boundaries yield their fallback, then resolve later\n * - Resolved Suspense boundaries yield swap scripts\n *\n * Usage:\n * ```ts\n * import { renderToStream, shSuspense, sh } from '@getforma/core/ssr';\n *\n * const stream = renderToStream(\n * sh('html', null,\n * sh('body', null,\n * sh('h1', null, 'Hello'),\n * shSuspense(\n * sh('div', null, 'Loading...'),\n * async () => {\n * const data = await fetchData();\n * return sh('div', null, data.name);\n * },\n * ),\n * ),\n * ),\n * );\n *\n * for await (const chunk of stream) {\n * response.write(chunk);\n * }\n * ```\n */\nexport async function* renderToStreamNew(\n node: unknown,\n options?: StreamOptions,\n): AsyncGenerator<string> {\n // Per-render state — scoped to this generator invocation\n const state: StreamState = {\n suspenseCounter: 0,\n pendingBoundaries: [],\n };\n\n const injectSwap = options?.injectSwapScript !== false;\n\n // Inject the swap script first (if there could be Suspense boundaries)\n if (injectSwap) {\n yield getSwapScript();\n }\n\n // Render the main tree synchronously, collecting Suspense boundaries\n const mainParts: string[] = [];\n renderStreamNode(node, mainParts, state);\n yield mainParts.join('');\n\n // Yield bootstrap script if provided\n if (options?.bootstrapScript) {\n yield options.bootstrapScript;\n }\n\n // Wait for and yield all pending Suspense boundaries\n // Each resolves independently (out-of-order)\n while (state.pendingBoundaries.length > 0) {\n const pending = [...state.pendingBoundaries];\n state.pendingBoundaries = [];\n\n // Race all pending boundaries — yield each as it resolves\n const results = await Promise.allSettled(\n pending.map(async (boundary) => {\n const html = await boundary.promise;\n return { id: boundary.id, html };\n }),\n );\n\n for (const result of results) {\n if (result.status === 'fulfilled') {\n yield getSwapTag(result.value.id, result.value.html);\n }\n // On rejection, the fallback stays visible (no swap)\n }\n }\n}\n\n/**\n * Render a node synchronously. If a Suspense boundary is encountered,\n * render the fallback inline and queue the async resolution.\n */\nfunction renderStreamNode(node: unknown, parts: string[], state: StreamState): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderStreamNode(node(), parts, state); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderStreamNode(child, parts, state);\n return;\n }\n\n // Suspense boundary: render fallback, queue async resolution\n if (isSuspenseVNode(node)) {\n const id = `forma-s:${state.suspenseCounter++}`;\n const fallback = node.props.fallback;\n const asyncFn = node.children[0]!;\n\n // Render fallback inside a replaceable container\n parts.push(`<div id=\"${id}\" style=\"display:contents\">`);\n renderSync(fallback, parts);\n parts.push('</div>');\n\n // Queue the async resolution\n const promise = asyncFn().then((resolved: unknown) => {\n const resolvedParts: string[] = [];\n renderSync(resolved, resolvedParts);\n return resolvedParts.join('');\n });\n state.pendingBoundaries.push({ id, promise });\n return;\n }\n\n // Regular VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'fallback') continue; // skip internal suspense props\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderStreamNode(child, parts, state);\n }\n parts.push('</', tag, '>');\n return;\n }\n\n parts.push(escapeHtml(String(node)));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/ssr/render.ts","../../src/ssr/ssr-reactive.ts","../../src/ssr/client-script.ts","../../src/ssr/stream.ts"],"names":["isEventHandlerAttr","isSafeAttrName","isUrlAttr","isDangerousUrl"],"mappings":"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","file":"index.cjs","sourcesContent":["// Void elements that don't have closing tags\nexport const VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr',\n]);\n\n// Map of prop names to HTML attribute names\nexport const PROP_TO_ATTR: Record<string, string> = {\n className: 'class',\n htmlFor: 'for',\n tabIndex: 'tabindex',\n};\n\n// Escape HTML entities\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Escape attribute values\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/'/g, ''');\n}\n\nimport { isDangerousUrl, isUrlAttr, isEventHandlerAttr, isSafeAttrName } from '../security/url-safety.js';\n\n// Dangerous URI detection for href, src, action, formaction attributes.\n// Retained for backwards compatibility with external importers; the actual\n// scheme check now goes through `isDangerousUrl`, which additionally strips the\n// control characters browsers ignore in a URL scheme (see security/url-safety).\nexport const DANGEROUS_URI_ATTRS = new Set(['href', 'src', 'action', 'formaction']);\nexport const DANGEROUS_URI_RE = /^\\s*(javascript|vbscript|data\\s*:\\s*text\\/html)/i;\n\n/**\n * Decide whether a resolved prop should be emitted as an attribute, and return\n * the escaped `name=\"value\"` fragment (or `name` for boolean `true`). Returns\n * null when the attribute must be dropped for safety. Shared by the plain and\n * hydration-aware renderers so the security rules cannot drift apart.\n */\nexport function renderAttr(key: string, resolved: unknown): string | null {\n const attrName = PROP_TO_ATTR[key] ?? key;\n // Never emit event-handler or malformed attribute names — an attacker who\n // controls a prop key could otherwise inject `onload=…` or break out of the\n // attribute entirely with whitespace/quotes.\n if (isEventHandlerAttr(attrName) || !isSafeAttrName(attrName)) return null;\n if (resolved === true) return ' ' + attrName;\n if (resolved === false || resolved == null) return null;\n const str = String(resolved);\n if (isUrlAttr(attrName) && isDangerousUrl(str)) return null;\n return ' ' + attrName + '=\"' + escapeAttr(str) + '\"';\n}\n\nexport interface VNode {\n tag: string;\n props: Record<string, unknown> | null;\n children: unknown[];\n}\n\n/**\n * Server-side hyperscript — creates a virtual node instead of a DOM element.\n */\nexport function sh(tag: string, props?: Record<string, unknown> | null, ...children: unknown[]): VNode {\n return { tag, props: props ?? null, children };\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string.\n *\n * Uses an array buffer internally to avoid O(n^2) string concatenation\n * for large trees, then joins once at the end.\n *\n * Usage:\n * ```ts\n * import { renderToString, sh } from '@getforma/core/ssr';\n *\n * const html = renderToString(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()), // signal getters resolved synchronously\n * )\n * );\n * ```\n */\nexport function renderToString(node: unknown): string {\n const parts: string[] = [];\n renderToBuffer(node, parts);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer.\n * Avoids O(n^2) string concatenation for large trees.\n */\nfunction renderToBuffer(node: unknown, parts: string[]): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter) → resolve and render\n if (typeof node === 'function') { renderToBuffer(node(), parts); return; }\n\n // Array → render each\n if (Array.isArray(node)) {\n for (const child of node) renderToBuffer(child, parts);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBuffer(child, parts);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\nexport function isVNode(v: unknown): v is VNode {\n return v != null && typeof v === 'object' && 'tag' in v && 'children' in v;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration-aware rendering\n// ---------------------------------------------------------------------------\n\n/** Per-render hydration context — avoids shared module-level counter. */\ninterface HydrationContext {\n id: number;\n}\n\n/**\n * Render a FormaJS virtual tree to an HTML string with hydration markers.\n *\n * Like `renderToString`, but injects comment markers and `data-forma-h`\n * attributes so the client-side `hydrate()` function can adopt existing\n * DOM nodes without re-creating them.\n *\n * Each call creates its own hydration counter, so concurrent calls\n * (e.g. multiple SSR requests in the same process) produce independent,\n * non-overlapping hydration IDs.\n *\n * Marker types:\n * - `data-forma-h=\"N\"` — element boundary (attribute on the element)\n * - `<!--forma-t:N-->` / `<!--/forma-t:N-->` — reactive text boundary\n * - `<!--forma-l:N-->` / `<!--/forma-l:N-->` — list boundary\n *\n * Usage:\n * ```ts\n * import { renderToStringWithHydration, sh } from '@getforma/core/ssr';\n *\n * const html = renderToStringWithHydration(\n * sh('div', { class: 'app' },\n * sh('h1', null, 'Hello SSR!'),\n * sh('p', null, () => count()),\n * )\n * );\n * ```\n */\nexport function renderToStringWithHydration(node: unknown): string {\n const ctx: HydrationContext = { id: 0 };\n const parts: string[] = [];\n renderToBufferHydrated(node, parts, ctx);\n return parts.join('');\n}\n\n/**\n * Internal: recursively render into a string array buffer with hydration markers.\n * The `ctx` object carries the hydration counter so concurrent renders are isolated.\n */\nfunction renderToBufferHydrated(node: unknown, parts: string[], ctx: HydrationContext): void {\n // null/undefined/boolean → empty\n if (node == null || node === true || node === false) return;\n\n // String → escaped text\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n\n // Number → stringified\n if (typeof node === 'number') { parts.push(String(node)); return; }\n\n // Function (signal getter / reactive text) → wrap with text markers\n if (typeof node === 'function') {\n const id = ctx.id++;\n parts.push(`<!--forma-t:${id}-->`);\n renderToBufferHydrated(node(), parts, ctx);\n parts.push(`<!--/forma-t:${id}-->`);\n return;\n }\n\n // Array → wrap with list markers\n if (Array.isArray(node)) {\n const id = ctx.id++;\n parts.push(`<!--forma-l:${id}-->`);\n for (const child of node) renderToBufferHydrated(child, parts, ctx);\n parts.push(`<!--/forma-l:${id}-->`);\n return;\n }\n\n // VNode\n if (isVNode(node)) {\n const id = ctx.id++;\n const { tag, props, children } = node;\n\n // Add hydration data-attribute to element\n parts.push('<', tag, ` data-forma-h=\"${id}\"`);\n\n // Render props as attributes\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n // Skip refs and internal props; event handlers and unsafe attribute\n // names are dropped inside renderAttr.\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n\n // Resolve reactive values\n const resolved = typeof value === 'function' ? value() : value;\n\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n\n // Void elements\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n\n parts.push('>');\n\n // dangerouslySetInnerHTML\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw === 'object' && raw != null && '__html' in raw) {\n const html = (raw as { __html: unknown }).__html;\n if (typeof html === 'string') {\n parts.push(html);\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n } else {\n // Render children\n for (const child of children) {\n renderToBufferHydrated(child, parts, ctx);\n }\n }\n\n parts.push('</', tag, '>');\n return;\n }\n\n // Fallback: stringify\n parts.push(escapeHtml(String(node)));\n}\n\n","/**\n * SSR-only signal: just a value holder, no reactivity needed on server.\n */\nexport function ssrSignal<T>(initial: T): [get: () => T, set: (v: T) => void] {\n let value = initial;\n return [() => value, (v) => { value = v; }];\n}\n\nexport function ssrComputed<T>(fn: () => T): () => T {\n return fn; // On server, just call the function directly\n}\n","/**\n * FormaJS SSR - Client Swap Script\n *\n * Tiny inline script injected into the HTML stream that handles\n * out-of-order Suspense boundary resolution.\n *\n * When the server resolves a Suspense boundary, it sends:\n * <script>$FORMA_SWAP(\"forma-s:0\",\"<resolved html>\")</script>\n *\n * This script replaces the fallback placeholder with the resolved content.\n */\n\n/**\n * Returns the client-side swap script as a string, ready to be injected\n * into the HTML stream (typically right after <body> or before first Suspense).\n */\nexport function getSwapScript(): string {\n return `<script>\nfunction $FORMA_SWAP(id,html){\n var el=document.getElementById(id);\n if(el){\n var tpl=document.createElement('template');\n tpl.innerHTML=html;\n el.replaceWith(tpl.content);\n }\n}\n</script>`;\n}\n\n/**\n * Serializes a value to a JSON string that is safe to embed inside a\n * `<script>` block. Standard `JSON.stringify` does not escape `<` or `>`,\n * so a payload containing `</script>` would prematurely close the script\n * tag, enabling an injection attack. We also escape U+2028 and U+2029\n * which are valid JSON but treated as line terminators in JavaScript source.\n */\nfunction safeJsonStringify(val: unknown): string {\n return JSON.stringify(val)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * Returns a script tag that swaps a specific Suspense boundary's content.\n * Called when an async resource resolves during streaming.\n */\nexport function getSwapTag(id: string, html: string): string {\n // safeJsonStringify escapes <, >, U+2028, and U+2029 so the serialized\n // content cannot break out of the surrounding <script> block.\n return `<script>$FORMA_SWAP(${safeJsonStringify(id)},${safeJsonStringify(html)})</script>`;\n}\n","/**\n * FormaJS SSR - Streaming Renderer\n *\n * Implements true chunked HTML streaming with out-of-order Suspense resolution.\n *\n * How it works:\n * 1. Yields shell HTML immediately (opening tags, head, static content)\n * 2. When a Suspense boundary is encountered:\n * - Yields the fallback HTML with a placeholder: <div id=\"forma-s:N\" style=\"display:contents\">\n * - Continues rendering siblings without blocking\n * - When the async resource resolves, yields a swap script\n * 3. Suspense boundaries resolve independently in any order (out-of-order streaming)\n */\n\nimport { getSwapScript, getSwapTag } from './client-script.js';\nimport { escapeHtml, isVNode, VOID_ELEMENTS, renderAttr, type VNode } from './render.js';\n\n// ---------------------------------------------------------------------------\n// Suspense boundary tracking\n// ---------------------------------------------------------------------------\n\ninterface PendingBoundary {\n id: string;\n promise: Promise<string>;\n}\n\n/** Per-render mutable state, scoped to a single renderToStreamNew call. */\ninterface StreamState {\n suspenseCounter: number;\n pendingBoundaries: PendingBoundary[];\n}\n\n// ---------------------------------------------------------------------------\n// Synchronous render to buffer (for non-async content)\n// ---------------------------------------------------------------------------\n\nfunction renderSync(node: unknown, parts: string[]): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderSync(node(), parts); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderSync(child, parts);\n return;\n }\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderSync(child, parts);\n }\n parts.push('</', tag, '>');\n return;\n }\n parts.push(escapeHtml(String(node)));\n}\n\n// ---------------------------------------------------------------------------\n// Suspense VNode detection\n// ---------------------------------------------------------------------------\n\n/**\n * A Suspense boundary VNode has a special shape:\n * { tag: 'forma-suspense', props: { fallback: VNode }, children: [asyncChildren] }\n *\n * Or it can be created via:\n * shSuspense(fallbackVNode, () => asyncContent)\n */\nexport interface SuspenseVNode {\n tag: 'forma-suspense';\n props: { fallback: unknown };\n children: [() => Promise<unknown>];\n}\n\nfunction isSuspenseVNode(v: unknown): v is SuspenseVNode {\n return isVNode(v) && v.tag === 'forma-suspense';\n}\n\n/**\n * Create a server-side Suspense VNode for streaming SSR.\n */\nexport function shSuspense(fallback: unknown, asyncChildren: () => Promise<unknown>): SuspenseVNode {\n return {\n tag: 'forma-suspense',\n props: { fallback },\n children: [asyncChildren],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Streaming renderer\n// ---------------------------------------------------------------------------\n\nexport interface StreamOptions {\n /** Script to bootstrap the client-side app (e.g. <script src=\"/app.js\"></script>) */\n bootstrapScript?: string;\n /** Whether to inject the $FORMA_SWAP client script (default: true) */\n injectSwapScript?: boolean;\n /**\n * Max ms to wait for each Suspense boundary before giving up (its fallback\n * stays visible and no swap is emitted). Omit or `<= 0` to wait forever — a\n * never-settling resource would otherwise hang the stream and the HTTP\n * connection indefinitely.\n */\n suspenseTimeout?: number;\n}\n\n/** A Suspense boundary that did not settle within suspenseTimeout. */\nconst TIMED_OUT = Symbol('forma-suspense-timeout');\n\n/** Resolve to TIMED_OUT if `p` does not settle within `ms` (no timeout if ms is unset/<=0). */\nfunction raceTimeout<T>(p: Promise<T>, ms: number | undefined): Promise<T | typeof TIMED_OUT> {\n if (ms == null || ms <= 0) return p;\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => resolve(TIMED_OUT), ms);\n p.then(\n (v) => { clearTimeout(t); resolve(v); },\n (e) => { clearTimeout(t); reject(e); },\n );\n });\n}\n\n/**\n * Render a FormaJS virtual tree to a stream of HTML chunks.\n *\n * Yields HTML progressively:\n * - Static content is yielded immediately\n * - Suspense boundaries yield their fallback, then resolve later\n * - Resolved Suspense boundaries yield swap scripts\n *\n * Usage:\n * ```ts\n * import { renderToStream, shSuspense, sh } from '@getforma/core/ssr';\n *\n * const stream = renderToStream(\n * sh('html', null,\n * sh('body', null,\n * sh('h1', null, 'Hello'),\n * shSuspense(\n * sh('div', null, 'Loading...'),\n * async () => {\n * const data = await fetchData();\n * return sh('div', null, data.name);\n * },\n * ),\n * ),\n * ),\n * );\n *\n * for await (const chunk of stream) {\n * response.write(chunk);\n * }\n * ```\n */\nexport async function* renderToStreamNew(\n node: unknown,\n options?: StreamOptions,\n): AsyncGenerator<string> {\n // Per-render state — scoped to this generator invocation\n const state: StreamState = {\n suspenseCounter: 0,\n pendingBoundaries: [],\n };\n\n const injectSwap = options?.injectSwapScript !== false;\n\n // Inject the swap script first (if there could be Suspense boundaries)\n if (injectSwap) {\n yield getSwapScript();\n }\n\n // Render the main tree synchronously, collecting Suspense boundaries\n const mainParts: string[] = [];\n renderStreamNode(node, mainParts, state);\n yield mainParts.join('');\n\n // Yield bootstrap script if provided\n if (options?.bootstrapScript) {\n yield options.bootstrapScript;\n }\n\n // Wait for and yield all pending Suspense boundaries\n // Each resolves independently (out-of-order)\n const suspenseTimeout = options?.suspenseTimeout;\n while (state.pendingBoundaries.length > 0) {\n const pending = [...state.pendingBoundaries];\n state.pendingBoundaries = [];\n\n // Race all pending boundaries against the optional timeout — yield each as\n // it resolves. A timed-out boundary resolves to TIMED_OUT (no swap), so the\n // loop drains and the generator returns instead of hanging.\n const results = await Promise.allSettled(\n pending.map(async (boundary) => {\n const html = await raceTimeout(boundary.promise, suspenseTimeout);\n return { id: boundary.id, html };\n }),\n );\n\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value.html !== TIMED_OUT) {\n yield getSwapTag(result.value.id, result.value.html as string);\n }\n // On timeout or rejection, the fallback stays visible (no swap).\n }\n }\n}\n\n/**\n * Render a node synchronously. If a Suspense boundary is encountered,\n * render the fallback inline and queue the async resolution.\n */\nfunction renderStreamNode(node: unknown, parts: string[], state: StreamState): void {\n if (node == null || node === true || node === false) return;\n if (typeof node === 'string') { parts.push(escapeHtml(node)); return; }\n if (typeof node === 'number') { parts.push(String(node)); return; }\n if (typeof node === 'function') { renderStreamNode(node(), parts, state); return; }\n if (Array.isArray(node)) {\n for (const child of node) renderStreamNode(child, parts, state);\n return;\n }\n\n // Suspense boundary: render fallback, queue async resolution\n if (isSuspenseVNode(node)) {\n const id = `forma-s:${state.suspenseCounter++}`;\n const fallback = node.props.fallback;\n const asyncFn = node.children[0]!;\n\n // Render fallback inside a replaceable container\n parts.push(`<div id=\"${id}\" style=\"display:contents\">`);\n renderSync(fallback, parts);\n parts.push('</div>');\n\n // Queue the async resolution. Invoke asyncFn via Promise.resolve().then so a\n // SYNCHRONOUS throw becomes a rejection routed through allSettled (fallback\n // stays) instead of aborting the whole generator before the shell is flushed.\n // Render resolved content through the Suspense-aware path so a NESTED\n // forma-suspense inside it is detected and queued onto the shared state\n // (drained out-of-order), rather than emitted as a literal element.\n const promise = Promise.resolve().then(asyncFn).then((resolved: unknown) => {\n const resolvedParts: string[] = [];\n renderStreamNode(resolved, resolvedParts, state);\n return resolvedParts.join('');\n });\n state.pendingBoundaries.push({ id, promise });\n return;\n }\n\n // Regular VNode\n if (isVNode(node)) {\n const { tag, props, children } = node;\n parts.push('<', tag);\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (key === 'fallback') continue; // skip internal suspense props\n if (key === 'ref' || key === 'dangerouslySetInnerHTML') continue;\n const resolved = typeof value === 'function' ? value() : value;\n const frag = renderAttr(key, resolved);\n if (frag !== null) parts.push(frag);\n }\n }\n if (VOID_ELEMENTS.has(tag)) { parts.push(' />'); return; }\n parts.push('>');\n if (props?.['dangerouslySetInnerHTML']) {\n const raw = props['dangerouslySetInnerHTML'];\n if (typeof raw !== 'object' || raw == null || !('__html' in raw) || typeof (raw as { __html: unknown }).__html !== 'string') {\n throw new TypeError('dangerouslySetInnerHTML must be { __html: string }');\n }\n parts.push((raw as { __html: string }).__html);\n } else {\n for (const child of children) renderStreamNode(child, parts, state);\n }\n parts.push('</', tag, '>');\n return;\n }\n\n parts.push(escapeHtml(String(node)));\n}\n"]}
|
package/dist/ssr/index.d.cts
CHANGED
|
@@ -98,6 +98,13 @@ interface StreamOptions {
|
|
|
98
98
|
bootstrapScript?: string;
|
|
99
99
|
/** Whether to inject the $FORMA_SWAP client script (default: true) */
|
|
100
100
|
injectSwapScript?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Max ms to wait for each Suspense boundary before giving up (its fallback
|
|
103
|
+
* stays visible and no swap is emitted). Omit or `<= 0` to wait forever — a
|
|
104
|
+
* never-settling resource would otherwise hang the stream and the HTTP
|
|
105
|
+
* connection indefinitely.
|
|
106
|
+
*/
|
|
107
|
+
suspenseTimeout?: number;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* Render a FormaJS virtual tree to a stream of HTML chunks.
|
package/dist/ssr/index.d.ts
CHANGED
|
@@ -98,6 +98,13 @@ interface StreamOptions {
|
|
|
98
98
|
bootstrapScript?: string;
|
|
99
99
|
/** Whether to inject the $FORMA_SWAP client script (default: true) */
|
|
100
100
|
injectSwapScript?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Max ms to wait for each Suspense boundary before giving up (its fallback
|
|
103
|
+
* stays visible and no swap is emitted). Omit or `<= 0` to wait forever — a
|
|
104
|
+
* never-settling resource would otherwise hang the stream and the HTTP
|
|
105
|
+
* connection indefinitely.
|
|
106
|
+
*/
|
|
107
|
+
suspenseTimeout?: number;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* Render a FormaJS virtual tree to a stream of HTML chunks.
|