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.
@@ -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
+ });
@@ -38,17 +38,17 @@ export const insertRoute = (root, path, handler) => {
38
38
  node = node.paramChild.node;
39
39
  continue;
40
40
  }
41
- if (parsed.type === 'catchAll') {
42
- if (!node.catchAllChild) {
43
- node.catchAllChild = {
44
- name: parsed.name,
45
- optional: parsed.optional,
46
- node: createNode()
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
  };
@@ -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;;;;;;;;;;;;;;;;;CAiF3B"}
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
- pick: ['default'],
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":"AAgBA,OAAO,EAIN,KAAK,SAAS,EAWd,MAAM,YAAY,CAAC;AAuHpB,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,SAAS,oBA2M1D;;AAED,wBAAkD"}
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 ? `(${getCrossReferenceHeader(id)},${data})` : data));
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 => (Array.isArray(value) ? value.join(', ') : value))[Symbol.iterator]();
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 && h3Request.body.locked);
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 && (result.status < 300 || result.status >= 400))
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 && (!instance || x.status < 300 || x.status >= 400))
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 ? x.message : typeof x === 'string' ? x : 'true';
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);