solidstep 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1094 -829
- package/index.d.ts.map +1 -1
- package/index.js +20 -3
- package/package.json +143 -72
- package/server.d.ts.map +1 -1
- package/server.js +306 -216
- package/utils/cache.d.ts.map +1 -1
- package/utils/cache.js +8 -19
- package/utils/components/form.d.ts +47 -0
- package/utils/components/form.d.ts.map +1 -0
- package/utils/components/form.js +95 -0
- package/utils/csp.d.ts.map +1 -1
- package/utils/csp.js +0 -2
- package/utils/hooks/action-state.d.ts +19 -1
- package/utils/hooks/action-state.d.ts.map +1 -1
- package/utils/hooks/action-state.js +24 -4
- package/utils/hooks/form-status.d.ts +12 -0
- package/utils/hooks/form-status.d.ts.map +1 -0
- package/utils/hooks/form-status.js +16 -0
- package/utils/instrumentation-noop.d.ts +3 -0
- package/utils/instrumentation-noop.d.ts.map +1 -0
- package/utils/instrumentation-noop.js +3 -0
- package/utils/instrumentation.d.ts +94 -0
- package/utils/instrumentation.d.ts.map +1 -0
- package/utils/instrumentation.js +132 -0
- package/utils/loader.d.ts.map +1 -1
- package/utils/loader.js +5 -11
- package/utils/middleware.d.ts +43 -0
- package/utils/middleware.d.ts.map +1 -0
- package/utils/middleware.js +48 -0
- package/utils/path-router.js +10 -10
- package/utils/router.d.ts.map +1 -1
- package/utils/router.js +3 -1
- package/utils/server-action.server.d.ts.map +1 -1
- package/utils/server-action.server.js +56 -22
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// utils/middleware.ts
|
|
2
|
+
// Composable middleware for SolidStep.
|
|
3
|
+
//
|
|
4
|
+
// Vinxi/H3 only allow a single middleware module per router. This helper lets
|
|
5
|
+
// you split request/response concerns (auth, CORS, CSRF, logging, ...) into
|
|
6
|
+
// independent units and compose them into one Vinxi-compatible middleware.
|
|
7
|
+
import { defineMiddleware as defineVinxiMiddleware } from 'vinxi/http';
|
|
8
|
+
/**
|
|
9
|
+
* Compose an ordered array of middleware into a single Vinxi middleware object.
|
|
10
|
+
*
|
|
11
|
+
* `onRequest` hooks run in array order and stop as soon as one returns a
|
|
12
|
+
* `Response` or marks the event handled. `onBeforeResponse` hooks always all
|
|
13
|
+
* run, in array order.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // app/middleware.ts
|
|
18
|
+
* import { defineMiddleware } from 'solidstep/utils/middleware';
|
|
19
|
+
*
|
|
20
|
+
* export default defineMiddleware([
|
|
21
|
+
* authMiddleware,
|
|
22
|
+
* corsMiddleware,
|
|
23
|
+
* csrfMiddleware,
|
|
24
|
+
* ]);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const defineMiddleware = (middlewares) => defineVinxiMiddleware({
|
|
28
|
+
onRequest: async (event) => {
|
|
29
|
+
for (const middleware of middlewares) {
|
|
30
|
+
if (!middleware.onRequest)
|
|
31
|
+
continue;
|
|
32
|
+
const result = await middleware.onRequest(event);
|
|
33
|
+
if (result instanceof Response) {
|
|
34
|
+
await event.respondWith(result);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (event.handled)
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
onBeforeResponse: async (event, response) => {
|
|
42
|
+
for (const middleware of middlewares) {
|
|
43
|
+
if (!middleware.onBeforeResponse)
|
|
44
|
+
continue;
|
|
45
|
+
await middleware.onBeforeResponse(event, response);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
package/utils/path-router.js
CHANGED
|
@@ -38,17 +38,17 @@ export const insertRoute = (root, path, handler) => {
|
|
|
38
38
|
node = node.paramChild.node;
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
node = node.catchAllChild.node;
|
|
50
|
-
break; // catch-all always consumes the rest
|
|
41
|
+
// Only catchAll segments reach here — static and param both continue above
|
|
42
|
+
const catchAll = parsed;
|
|
43
|
+
if (!node.catchAllChild) {
|
|
44
|
+
node.catchAllChild = {
|
|
45
|
+
name: catchAll.name,
|
|
46
|
+
optional: catchAll.optional,
|
|
47
|
+
node: createNode()
|
|
48
|
+
};
|
|
51
49
|
}
|
|
50
|
+
node = node.catchAllChild.node;
|
|
51
|
+
break; // catch-all always consumes the rest
|
|
52
52
|
}
|
|
53
53
|
node.handler = handler;
|
|
54
54
|
};
|
package/utils/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../utils/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAa,MAAM,iBAAiB,CAAC;AAElE,qBAAa,YAAa,SAAQ,oBAAoB;IAClD,MAAM,CAAC,GAAG,EAAE,MAAM;IAQlB,OAAO,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4K3B;AAED,qBAAa,YAAa,SAAQ,oBAAoB;IAClD,MAAM,CAAC,GAAG,EAAE,MAAM;IAQlB,OAAO,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../utils/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAa,MAAM,iBAAiB,CAAC;AAElE,qBAAa,YAAa,SAAQ,oBAAoB;IAClD,MAAM,CAAC,GAAG,EAAE,MAAM;IAQlB,OAAO,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4K3B;AAED,qBAAa,YAAa,SAAQ,oBAAoB;IAClD,MAAM,CAAC,GAAG,EAAE,MAAM;IAQlB,OAAO,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;CAmF3B"}
|
package/utils/router.js
CHANGED
|
@@ -249,7 +249,9 @@ export class ClientRouter extends BaseFileSystemRouter {
|
|
|
249
249
|
path: `/not-found${path}`,
|
|
250
250
|
$component: {
|
|
251
251
|
src: filePath,
|
|
252
|
-
|
|
252
|
+
// Include `$css` so the client manifest has the same variant
|
|
253
|
+
// the server looks up when rendering the not-found page.
|
|
254
|
+
pick: ['default', '$css'],
|
|
253
255
|
},
|
|
254
256
|
};
|
|
255
257
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-action.server.d.ts","sourceRoot":"","sources":["../../utils/server-action.server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server-action.server.d.ts","sourceRoot":"","sources":["../../utils/server-action.server.ts"],"names":[],"mappings":"AAoBA,OAAO,EAIH,KAAK,SAAS,EAWjB,MAAM,YAAY,CAAC;AA4IpB,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,SAAS,oBA4Q1D;;AAED,wBAAkD"}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/// <reference types='vinxi/types/server' />
|
|
2
|
-
import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from 'seroval';
|
|
3
|
-
import { CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLPlugin, URLSearchParamsPlugin } from 'seroval-plugins/web';
|
|
2
|
+
import { crossSerializeStream, fromJSON, getCrossReferenceHeader, } from 'seroval';
|
|
3
|
+
import { CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLPlugin, URLSearchParamsPlugin, } from 'seroval-plugins/web';
|
|
4
4
|
import { sharedConfig } from 'solid-js';
|
|
5
5
|
import { provideRequestEvent } from 'solid-js/web/storage';
|
|
6
|
-
import { eventHandler, setHeader, setResponseStatus, appendResponseHeader, toWebRequest, getWebRequest, getRequestIP, getResponseStatus, getResponseStatusText, getResponseHeader, getResponseHeaders, removeResponseHeader, setResponseHeader } from 'vinxi/http';
|
|
6
|
+
import { eventHandler, setHeader, setResponseStatus, appendResponseHeader, toWebRequest, getWebRequest, getRequestIP, getResponseStatus, getResponseStatusText, getResponseHeader, getResponseHeaders, removeResponseHeader, setResponseHeader, } from 'vinxi/http';
|
|
7
7
|
import invariant from 'vinxi/lib/invariant';
|
|
8
8
|
import { getManifest } from 'vinxi/manifest';
|
|
9
9
|
import { RedirectError } from './redirect';
|
|
10
10
|
import { createDiffDOM } from './diff-dom';
|
|
11
11
|
import { getCache, invalidateCache } from './cache';
|
|
12
12
|
import fetch from './fetch.server';
|
|
13
|
+
import { createRequestContext, createResponseContext, getInstrumentation, safeExecuteHook, } from '../utils/instrumentation';
|
|
13
14
|
function createChunk(data) {
|
|
14
15
|
const encodeData = new TextEncoder().encode(data);
|
|
15
16
|
const bytes = encodeData.length;
|
|
@@ -36,19 +37,21 @@ function serializeToStream(id, value) {
|
|
|
36
37
|
RequestPlugin,
|
|
37
38
|
ResponsePlugin,
|
|
38
39
|
URLSearchParamsPlugin,
|
|
39
|
-
URLPlugin
|
|
40
|
+
URLPlugin,
|
|
40
41
|
],
|
|
41
42
|
onSerialize(data, initial) {
|
|
42
|
-
controller.enqueue(createChunk(initial
|
|
43
|
+
controller.enqueue(createChunk(initial
|
|
44
|
+
? `(${getCrossReferenceHeader(id)},${data})`
|
|
45
|
+
: data));
|
|
43
46
|
},
|
|
44
47
|
onDone() {
|
|
45
48
|
controller.close();
|
|
46
49
|
},
|
|
47
50
|
onError(error) {
|
|
48
51
|
controller.error(error);
|
|
49
|
-
}
|
|
52
|
+
},
|
|
50
53
|
});
|
|
51
|
-
}
|
|
54
|
+
},
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
class HeaderProxy {
|
|
@@ -88,7 +91,7 @@ class HeaderProxy {
|
|
|
88
91
|
}
|
|
89
92
|
values() {
|
|
90
93
|
return Object.values(getResponseHeaders(this.event))
|
|
91
|
-
.map(value =>
|
|
94
|
+
.map((value) => Array.isArray(value) ? value.join(', ') : value)[Symbol.iterator]();
|
|
92
95
|
}
|
|
93
96
|
[Symbol.iterator]() {
|
|
94
97
|
return this.entries()[Symbol.iterator]();
|
|
@@ -108,14 +111,20 @@ function createResponseStub(event) {
|
|
|
108
111
|
set statusText(v) {
|
|
109
112
|
setResponseStatus(event, getResponseStatus(event), v);
|
|
110
113
|
},
|
|
111
|
-
headers: new HeaderProxy(event)
|
|
114
|
+
headers: new HeaderProxy(event),
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
117
|
export async function handleServerFunction(event) {
|
|
115
118
|
const request = toWebRequest(event);
|
|
119
|
+
const url = new URL(request.url);
|
|
120
|
+
const inst = getInstrumentation();
|
|
121
|
+
const reqCtx = createRequestContext(request, {
|
|
122
|
+
routePath: url.pathname,
|
|
123
|
+
routeType: 'server-action',
|
|
124
|
+
});
|
|
125
|
+
await safeExecuteHook('onRequest', inst?.onRequest, request, reqCtx);
|
|
116
126
|
const serverReference = request.headers.get('X-Server-Id');
|
|
117
127
|
const instance = request.headers.get('X-Server-Instance');
|
|
118
|
-
const url = new URL(request.url);
|
|
119
128
|
let functionId;
|
|
120
129
|
let name;
|
|
121
130
|
if (serverReference) {
|
|
@@ -150,8 +159,8 @@ export async function handleServerFunction(event) {
|
|
|
150
159
|
RequestPlugin,
|
|
151
160
|
ResponsePlugin,
|
|
152
161
|
URLSearchParamsPlugin,
|
|
153
|
-
URLPlugin
|
|
154
|
-
]
|
|
162
|
+
URLPlugin,
|
|
163
|
+
],
|
|
155
164
|
})
|
|
156
165
|
: json).forEach((arg) => parsed.push(arg));
|
|
157
166
|
}
|
|
@@ -164,7 +173,9 @@ export async function handleServerFunction(event) {
|
|
|
164
173
|
const isReadableStream = h3Request instanceof ReadableStream;
|
|
165
174
|
const hasReadableStream = h3Request.body instanceof ReadableStream;
|
|
166
175
|
const isH3EventBodyStreamLocked = (isReadableStream && h3Request.locked) ||
|
|
167
|
-
(hasReadableStream &&
|
|
176
|
+
(hasReadableStream &&
|
|
177
|
+
h3Request.body
|
|
178
|
+
.locked);
|
|
168
179
|
const requestBody = isReadableStream ? h3Request : h3Request.body;
|
|
169
180
|
if (contentType?.startsWith('multipart/form-data') ||
|
|
170
181
|
contentType?.startsWith('application/x-www-form-urlencoded')) {
|
|
@@ -195,8 +206,8 @@ export async function handleServerFunction(event) {
|
|
|
195
206
|
RequestPlugin,
|
|
196
207
|
ResponsePlugin,
|
|
197
208
|
URLSearchParamsPlugin,
|
|
198
|
-
URLPlugin
|
|
199
|
-
]
|
|
209
|
+
URLPlugin,
|
|
210
|
+
],
|
|
200
211
|
});
|
|
201
212
|
}
|
|
202
213
|
}
|
|
@@ -206,14 +217,23 @@ export async function handleServerFunction(event) {
|
|
|
206
217
|
response: createResponseStub(event),
|
|
207
218
|
clientAddress: getRequestIP(event),
|
|
208
219
|
locals: {},
|
|
209
|
-
nativeEvent: event
|
|
220
|
+
nativeEvent: event,
|
|
210
221
|
}, async () => {
|
|
211
222
|
sharedConfig.context = { event };
|
|
212
223
|
event.locals.serverFunctionMeta = {
|
|
213
|
-
id: `${functionId}#${name}
|
|
224
|
+
id: `${functionId}#${name}`,
|
|
214
225
|
};
|
|
215
226
|
return serverFunction(...parsed);
|
|
216
227
|
});
|
|
228
|
+
// No-JS fallback: native form submission without client-side JS
|
|
229
|
+
// When there's no X-Server-Instance header, this is a plain form POST.
|
|
230
|
+
// Execute the action and redirect back to the referring page.
|
|
231
|
+
if (!instance) {
|
|
232
|
+
const referer = request.headers.get('Referer') || '/';
|
|
233
|
+
setResponseStatus(event, 303);
|
|
234
|
+
setHeader(event, 'Location', referer);
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
217
237
|
// handle responses
|
|
218
238
|
if (result instanceof Response) {
|
|
219
239
|
if (result.headers?.has('X-Content-Raw'))
|
|
@@ -222,7 +242,8 @@ export async function handleServerFunction(event) {
|
|
|
222
242
|
// forward headers
|
|
223
243
|
// if (result.headers) mergeResponseHeaders(event, result.headers);
|
|
224
244
|
// forward non-redirect statuses
|
|
225
|
-
if (result.status &&
|
|
245
|
+
if (result.status &&
|
|
246
|
+
(result.status < 300 || result.status >= 400))
|
|
226
247
|
setResponseStatus(event, result.status);
|
|
227
248
|
if (result.customBody) {
|
|
228
249
|
result = await result.customBody();
|
|
@@ -245,14 +266,14 @@ export async function handleServerFunction(event) {
|
|
|
245
266
|
const reqUrl = new URL(request.url);
|
|
246
267
|
const serverUrl = reqUrl.origin;
|
|
247
268
|
const response = await fetch(serverUrl + revalidatePath, {
|
|
248
|
-
method: 'GET'
|
|
269
|
+
method: 'GET',
|
|
249
270
|
}, false);
|
|
250
271
|
await response.text(); // ensure the fetch is completed and cache is populated
|
|
251
272
|
const newCacheValue = getCache(revalidatePath);
|
|
252
273
|
const newHtml = newCacheValue?.rendered;
|
|
253
274
|
const dd = createDiffDOM({
|
|
254
275
|
skipSelector: 'SCRIPT, STYLE, NOSCRIPT',
|
|
255
|
-
skipMode: 'full'
|
|
276
|
+
skipMode: 'full',
|
|
256
277
|
});
|
|
257
278
|
const ddDiff = dd.diff(oldHtml, newHtml);
|
|
258
279
|
diff = structuredClone(ddDiff);
|
|
@@ -267,11 +288,15 @@ export async function handleServerFunction(event) {
|
|
|
267
288
|
return serializeToStream(instance, result);
|
|
268
289
|
}
|
|
269
290
|
catch (x) {
|
|
291
|
+
await safeExecuteHook('onRequestError', inst?.onRequestError, x instanceof Error ? x : new Error(String(x)), request, reqCtx);
|
|
270
292
|
if (x instanceof Response) {
|
|
271
293
|
// forward headers
|
|
272
294
|
// if ((x as any).headers) mergeResponseHeaders(event, (x as any).headers);
|
|
273
295
|
// forward non-redirect statuses
|
|
274
|
-
if (x.status &&
|
|
296
|
+
if (x.status &&
|
|
297
|
+
(!instance ||
|
|
298
|
+
x.status < 300 ||
|
|
299
|
+
x.status >= 400))
|
|
275
300
|
setResponseStatus(event, x.status);
|
|
276
301
|
if (x.customBody) {
|
|
277
302
|
// biome-ignore lint/suspicious/noCatchAssign: <explanation>
|
|
@@ -283,7 +308,11 @@ export async function handleServerFunction(event) {
|
|
|
283
308
|
setHeader(event, 'X-Error', 'true');
|
|
284
309
|
}
|
|
285
310
|
else if (instance) {
|
|
286
|
-
const error = x instanceof Error
|
|
311
|
+
const error = x instanceof Error
|
|
312
|
+
? x.message
|
|
313
|
+
: typeof x === 'string'
|
|
314
|
+
? x
|
|
315
|
+
: 'true';
|
|
287
316
|
setHeader(event, 'X-Error', error.replace(/[\r\n]+/g, ''));
|
|
288
317
|
if (!(x instanceof RedirectError)) {
|
|
289
318
|
setResponseStatus(event, 500);
|
|
@@ -295,5 +324,10 @@ export async function handleServerFunction(event) {
|
|
|
295
324
|
}
|
|
296
325
|
return x;
|
|
297
326
|
}
|
|
327
|
+
finally {
|
|
328
|
+
const statusCode = getResponseStatus(event) || 200;
|
|
329
|
+
const respCtx = createResponseContext(reqCtx, statusCode);
|
|
330
|
+
await safeExecuteHook('onResponseEnd', inst?.onResponseEnd, request, respCtx);
|
|
331
|
+
}
|
|
298
332
|
}
|
|
299
333
|
export default eventHandler(handleServerFunction);
|