solidstep 0.2.0 → 0.3.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 CHANGED
@@ -805,6 +805,16 @@ As SolidStep is built using Vite, it follows the same guide as stated in [Vite d
805
805
  - Advanced caching strategies
806
806
  - WebSocket support
807
807
 
808
+ ## Testing
809
+
810
+ SolidStep does not include a built-in testing framework. However, we recommend setting up testing using Vitest ecosystem. You can use [Vitest](https://vitest.dev/) for unit and integration tests, and [Playwright](https://playwright.dev/) for end-to-end testing.
811
+
812
+ ### Testing Server Actions
813
+
814
+ When testing server actions, you can use Vitest to accomplish this. Just test as you would with any other async function.
815
+
816
+ When testing pages (e2e tests), you can trigger server actions by simulating user interactions that would call those actions. If needed, you can also intercept network requests to directly test the action endpoints. Use the testing framework's capabilities to intercept the requests and ensure the responses have the expected results. If the server action returns json data, stringify it and add it to the response body as well as setting the content-type header to 'application/json'. If the action has a more complex return type, use seroval to serialize the response before sending it back.
817
+
808
818
  ## License
809
819
 
810
820
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidstep",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Next Step SolidJS Framework for building web applications.",
5
5
  "type": "module",
6
6
  "author": "HamzaKV <hamzakv333@gmail.com>",
package/server.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare const handler: import("vinxi/http").EventHandler<import("vinxi/http").EventHandlerRequest, Promise<unknown>>;
1
+ declare const handler: import("vinxi/http").EventHandler<import("vinxi/http").EventHandlerRequest, Promise<any>>;
2
2
  export default handler;
3
3
  //# sourceMappingURL=server.d.ts.map
package/server.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AA0hBA,QAAA,MAAM,OAAO,+FAgVX,CAAC;AAEH,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AA4kBA,QAAA,MAAM,OAAO,2FA0XX,CAAC;AAEH,eAAe,OAAO,CAAC"}
package/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { eventHandler, toWebRequest, setResponseHeader, getEvent } from 'vinxi/http';
1
+ import { eventHandler, toWebRequest, setHeader, setResponseStatus } from 'vinxi/http';
2
2
  import { getManifest } from 'vinxi/manifest';
3
3
  import { generateHydrationScript, renderToString } from 'solid-js/web';
4
4
  import fileRoutes, {} from 'vinxi/routes';
@@ -8,58 +8,102 @@ import { handleServerFunction } from './utils/server-action.server';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { dirname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
- const isPageFile = (file) => file.endsWith('page.tsx')
12
- || file.endsWith('page.jsx')
13
- || file.endsWith('page.ts')
14
- || file.endsWith('page.js');
11
+ const isPageFile = (file) => file.endsWith('page.tsx') ||
12
+ file.endsWith('page.jsx') ||
13
+ file.endsWith('page.ts') ||
14
+ file.endsWith('page.js');
15
15
  const isRouteFile = (file) => file.endsWith('route.ts') || file.endsWith('route.js');
16
- const parseSegment = (part) => part.startsWith('[') ? `:${part.slice(1, -1).replace(/\.\.\./, '*')}` : part;
16
+ const parseSegment = (part) => {
17
+ if (part.startsWith('[[') && part.endsWith(']]')) {
18
+ return `:^*${part.slice(5, -2)}`;
19
+ }
20
+ return part.startsWith('[')
21
+ ? `:${part.slice(1, -1).replace(/\.\.\./, '*')}`
22
+ : part;
23
+ };
24
+ const createMatcher = (routePath) => {
25
+ const keys = [];
26
+ const segments = routePath.split('/').map((part) => {
27
+ if (part.startsWith(':')) {
28
+ if (part.startsWith(':^*')) {
29
+ // Catch-all inclusive
30
+ keys.push(part.slice(3));
31
+ return '?(.*)?';
32
+ }
33
+ if (part.startsWith(':*')) {
34
+ // Catch-all
35
+ keys.push(part.slice(2));
36
+ return '.*';
37
+ }
38
+ // Standard param
39
+ keys.push(part.slice(1));
40
+ return '([^/]+)';
41
+ }
42
+ return part;
43
+ });
44
+ return {
45
+ regex: new RegExp(`^${segments.join('/')}$`),
46
+ keys,
47
+ };
48
+ };
49
+ const getNormalizedPath = (path, clean) => {
50
+ const segments = path.split('/').slice(2).map(parseSegment);
51
+ if (clean)
52
+ return `/${segments.filter((s) => !s.startsWith('(')).join('/')}`;
53
+ return `/${segments.join('/')}`;
54
+ };
17
55
  const createRouteManifest = async () => {
18
56
  const entries = {};
19
57
  const allRoutes = [];
20
- const allLayouts = [];
21
- const allLoadingPages = [];
22
- const allErrorPages = [];
23
- const allGroups = [];
58
+ const layoutsMap = new Map();
59
+ const loadingPagesMap = new Map();
60
+ const errorPagesMap = new Map();
61
+ const groupsMap = new Map();
24
62
  let notFoundPage;
25
63
  for (const fileRoute of fileRoutes) {
26
64
  if (fileRoute.type === 'route') {
27
65
  allRoutes.push(fileRoute);
28
66
  }
29
67
  if (fileRoute.type === 'layout') {
30
- allLayouts.push(fileRoute);
68
+ const path = getNormalizedPath(fileRoute.path);
69
+ layoutsMap.set(path, fileRoute);
31
70
  }
32
71
  if (fileRoute.type === 'not-found') {
33
72
  notFoundPage = fileRoute;
34
73
  }
35
74
  if (fileRoute.type === 'loading') {
36
- allLoadingPages.push(fileRoute);
75
+ const path = getNormalizedPath(fileRoute.path);
76
+ loadingPagesMap.set(path, fileRoute);
37
77
  }
38
78
  if (fileRoute.type === 'error') {
39
- allErrorPages.push(fileRoute);
79
+ const path = getNormalizedPath(fileRoute.path);
80
+ errorPagesMap.set(path, fileRoute);
40
81
  }
41
82
  if (fileRoute.type === 'group') {
42
- allGroups.push(fileRoute);
83
+ const parentPath = fileRoute.parent
84
+ ? getNormalizedPath(fileRoute.parent)
85
+ : '';
86
+ const existing = groupsMap.get(parentPath) || [];
87
+ existing.push(fileRoute);
88
+ groupsMap.set(parentPath, existing);
43
89
  }
44
90
  }
91
+ const regex = /\?(?:pick=.*)*/g;
45
92
  for (const fileRoute of allRoutes) {
46
- const segments = fileRoute.path.split('/').slice(2).map(parseSegment);
47
- const routePath = `/${segments.filter(s => !(s.startsWith('('))).join('/')}`;
48
- const regex = /\?(?:pick=.*)*/g;
93
+ const routePath = getNormalizedPath(fileRoute.path, true);
94
+ const routeMatcherPath = getNormalizedPath(fileRoute.path);
49
95
  const src = fileRoute.$handler?.src.replace(regex, '');
50
96
  if (src && isPageFile(src)) {
51
- const loadingPage = allLoadingPages.find(route => {
52
- const path = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
53
- return path === routePath;
54
- });
55
- const matchedGroups = allGroups.filter(route => {
56
- const parentPath = route.parent ? `/${route.parent.split('/').slice(2).map(parseSegment).join('/')}` : '';
57
- return parentPath === routePath;
58
- });
97
+ const loadingPage = loadingPagesMap.get(routeMatcherPath);
98
+ const matchedGroups = groupsMap.get(routePath);
59
99
  const groups = {};
60
100
  if (matchedGroups && matchedGroups.length > 0) {
61
101
  for (const group of matchedGroups) {
62
- const groupName = group.path.split('/').filter(s => !(s.startsWith('('))).map(parseSegment).at(-1);
102
+ const groupName = group.path
103
+ .split('/')
104
+ .filter((s) => !s.startsWith('('))
105
+ .map(parseSegment)
106
+ .at(-1);
63
107
  if (!groupName)
64
108
  continue;
65
109
  groups[groupName] = {
@@ -69,20 +113,18 @@ const createRouteManifest = async () => {
69
113
  };
70
114
  }
71
115
  }
116
+ const segments = routeMatcherPath.split('/').filter(Boolean);
72
117
  let errorPage;
73
118
  const layouts = [];
74
- for (let i = segments.length; i > (routePath === '/' ? 0 : -1); i--) {
75
- const path = `/${segments.slice(0, i).join('/')}`;
119
+ // We need to traverse from root to leaf to build layouts order correctly?
120
+ // Original code: i = segments.length down to 0. unshift matches.
121
+ // i=length: /a/b/c. i=0: /.
122
+ for (let i = segments.length; i >= 0; i--) {
123
+ const path = i === 0 ? '/' : `/${segments.slice(0, i).join('/')}`;
76
124
  if (!errorPage) {
77
- errorPage = allErrorPages.find(route => {
78
- const routePath = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
79
- return routePath === path;
80
- });
125
+ errorPage = errorPagesMap.get(path);
81
126
  }
82
- const layout = allLayouts.find(route => {
83
- const routePath = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
84
- return routePath === path;
85
- });
127
+ const layout = layoutsMap.get(path);
86
128
  if (layout) {
87
129
  layouts.unshift({
88
130
  manifestPath: layout.path,
@@ -92,8 +134,11 @@ const createRouteManifest = async () => {
92
134
  });
93
135
  }
94
136
  }
137
+ const { regex: matcherRegex, keys } = createMatcher(routePath);
95
138
  entries[routePath] = {
96
139
  type: 'page',
140
+ regex: matcherRegex,
141
+ paramKeys: keys,
97
142
  mainPage: {
98
143
  manifestPath: fileRoute.path,
99
144
  page: fileRoute.$component,
@@ -101,28 +146,37 @@ const createRouteManifest = async () => {
101
146
  generateMeta: fileRoute.$generateMeta,
102
147
  options: fileRoute.$options,
103
148
  },
104
- loadingPage: loadingPage ? {
105
- page: loadingPage.$component,
106
- generateMeta: loadingPage.$generateMeta,
107
- manifestPath: loadingPage.path,
108
- } : undefined,
109
- errorPage: errorPage ? {
110
- page: errorPage.$component,
111
- generateMeta: errorPage.$generateMeta,
112
- manifestPath: errorPage.path,
113
- } : undefined,
114
- notFoundPage: routePath === '/' && notFoundPage ? {
115
- page: notFoundPage.$component,
116
- generateMeta: notFoundPage.$generateMeta,
117
- manifestPath: notFoundPage.path,
118
- } : undefined,
149
+ loadingPage: loadingPage
150
+ ? {
151
+ page: loadingPage.$component,
152
+ generateMeta: loadingPage.$generateMeta,
153
+ manifestPath: loadingPage.path,
154
+ }
155
+ : undefined,
156
+ errorPage: errorPage
157
+ ? {
158
+ page: errorPage.$component,
159
+ generateMeta: errorPage.$generateMeta,
160
+ manifestPath: errorPage.path,
161
+ }
162
+ : undefined,
163
+ notFoundPage: routePath === '/' && notFoundPage
164
+ ? {
165
+ page: notFoundPage.$component,
166
+ generateMeta: notFoundPage.$generateMeta,
167
+ manifestPath: notFoundPage.path,
168
+ }
169
+ : undefined,
119
170
  layouts: layouts,
120
171
  groups: groups,
121
172
  };
122
173
  }
123
174
  else if (src && isRouteFile(src)) {
175
+ const { regex: matcherRegex, keys } = createMatcher(routePath);
124
176
  entries[routePath] = {
125
177
  type: 'route',
178
+ regex: matcherRegex,
179
+ paramKeys: keys,
126
180
  handler: fileRoute.$handler,
127
181
  manifestPath: fileRoute.path,
128
182
  };
@@ -158,12 +212,12 @@ const extractRouteParams = (route, url) => {
158
212
  if (matched)
159
213
  return { route, params };
160
214
  };
161
- const template = `
162
- <!DOCTYPE html>
163
- <html lang="en">
164
- <head><!--app-head--></head>
165
- <!--app-body-->
166
- </html>
215
+ const template = `
216
+ <!DOCTYPE html>
217
+ <html lang="en">
218
+ <head><!--app-head--></head>
219
+ <!--app-body-->
220
+ </html>
167
221
  `;
168
222
  const generateHtmlHead = (meta) => {
169
223
  const head = Object.entries(meta)
@@ -177,7 +231,9 @@ const generateHtmlHead = (meta) => {
177
231
  .join(' ');
178
232
  return `<meta ${attrs}>`;
179
233
  }
180
- if (value.type === 'link' || value.type === 'style' || value.type === 'script') {
234
+ if (value.type === 'link' ||
235
+ value.type === 'style' ||
236
+ value.type === 'script') {
181
237
  const attrs = Object.entries(value.attributes)
182
238
  .map(([attrKey, attrValue]) => `${attrKey}="${attrValue}"`)
183
239
  .join(' ');
@@ -188,45 +244,11 @@ const generateHtmlHead = (meta) => {
188
244
  .join('\n');
189
245
  return head;
190
246
  };
191
- const sendNodeResponse = async (res, response) => {
192
- // Set status code
193
- res.statusCode = response.status;
194
- // Set headers
195
- response.headers.forEach((value, key) => {
196
- res.setHeader(key, value);
197
- });
198
- // Stream the body
199
- if (response.body) {
200
- const reader = response.body.getReader();
201
- const push = async () => {
202
- const { done, value } = await reader.read();
203
- if (done) {
204
- res.end();
205
- return;
206
- }
207
- res.write(Buffer.from(value));
208
- await push();
209
- };
210
- await push();
211
- }
212
- else {
213
- const text = await response.text();
214
- res.end(text);
215
- }
216
- };
217
- const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonce, }) => {
247
+ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOptions, cspNonce, error }) => {
218
248
  const url = new URL(req.url);
219
249
  const path = url.pathname;
220
250
  const cachedEntry = getCache(path);
221
251
  if (cachedEntry && toRender === 'main') {
222
- const { options } = entry.mainPage.options ? await entry.mainPage.options.import() : { options: {} };
223
- if (options?.responseHeaders) {
224
- const headers = options.responseHeaders;
225
- const event = getEvent();
226
- for (const [key, value] of Object.entries(headers)) {
227
- setResponseHeader(event, key, value);
228
- }
229
- }
230
252
  return {
231
253
  rendered: cachedEntry.rendered,
232
254
  documentMeta: cachedEntry.documentMeta,
@@ -234,7 +256,6 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
234
256
  loaderData: cachedEntry.loaderData,
235
257
  };
236
258
  }
237
- let cachingOptions = undefined;
238
259
  let meta = {};
239
260
  const loaderData = {};
240
261
  const clientManifest = getManifest('client');
@@ -242,12 +263,14 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
242
263
  const compose = entry.layouts.reduceRight((children, layout, index) => async () => {
243
264
  const moduleSrc = `${layout.layout.src}&pick=$css`;
244
265
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
245
- for (const asset of moduleAssets) {
246
- assets.push(asset);
247
- }
266
+ assets.push(...moduleAssets);
248
267
  const { default: layoutModule } = await layout.layout.import();
249
- const { loader: layoutLoader } = layout.loader ? await layout.loader.import() : { loader: null };
250
- const { generateMeta: generateMetaPage } = layout.generateMeta ? await layout.generateMeta.import() : { generateMeta: null };
268
+ const { loader: layoutLoader } = layout.loader
269
+ ? await layout.loader.import()
270
+ : { loader: null };
271
+ const { generateMeta: generateMetaPage } = layout.generateMeta
272
+ ? await layout.generateMeta.import()
273
+ : { generateMeta: null };
251
274
  let data = {};
252
275
  if (generateMetaPage) {
253
276
  const metaData = await generateMetaPage({
@@ -257,7 +280,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
257
280
  if (metaData) {
258
281
  meta = {
259
282
  ...meta,
260
- ...metaData
283
+ ...metaData,
261
284
  };
262
285
  }
263
286
  }
@@ -275,11 +298,11 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
275
298
  slotPromises.push((async () => {
276
299
  const moduleSrc = `${group.page.src}&pick=$css`;
277
300
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
278
- for (const asset of moduleAssets) {
279
- assets.push(asset);
280
- }
301
+ assets.push(...moduleAssets);
281
302
  const { default: groupPage } = await group.page.import();
282
- const { loader: groupLoader } = group.loader ? await group.loader.import() : { loader: null };
303
+ const { loader: groupLoader } = group.loader
304
+ ? await group.loader.import()
305
+ : { loader: null };
283
306
  let data = {};
284
307
  if (groupLoader) {
285
308
  const result = await groupLoader.loader(req);
@@ -289,7 +312,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
289
312
  slots[groupName.replace('@', '')] = () => groupPage({
290
313
  routeParams,
291
314
  searchParams,
292
- loaderData: data
315
+ loaderData: data,
293
316
  });
294
317
  })());
295
318
  }
@@ -303,7 +326,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
303
326
  slots: slots,
304
327
  locals: {
305
328
  cspNonce: cspNonce,
306
- }
329
+ },
307
330
  });
308
331
  }, async () => {
309
332
  const pageToRender = toRender === 'loading'
@@ -315,23 +338,14 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
315
338
  : entry.mainPage;
316
339
  const moduleSrc = `${pageToRender.page.src}&pick=$css`;
317
340
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
318
- for (const asset of moduleAssets) {
319
- assets.push(asset);
320
- }
341
+ assets.push(...moduleAssets);
321
342
  const { default: page } = await pageToRender.page.import();
322
- const { loader: pageLoader } = pageToRender.loader ? await pageToRender.loader.import() : { loader: null };
323
- const { generateMeta } = pageToRender.generateMeta ? await pageToRender.generateMeta.import() : { generateMeta: null };
324
- const { options } = pageToRender.options ? await pageToRender.options.import() : { options: {} };
325
- if (options?.cache) {
326
- cachingOptions = options.cache;
327
- }
328
- if (options?.responseHeaders) {
329
- const headers = options.responseHeaders;
330
- const event = getEvent();
331
- for (const [key, value] of Object.entries(headers)) {
332
- setResponseHeader(event, key, value);
333
- }
334
- }
343
+ const { loader: pageLoader } = pageToRender.loader
344
+ ? await pageToRender.loader.import()
345
+ : { loader: null };
346
+ const { generateMeta } = pageToRender.generateMeta
347
+ ? await pageToRender.generateMeta.import()
348
+ : { generateMeta: null };
335
349
  let data = {};
336
350
  if (pageLoader) {
337
351
  const result = await pageLoader.loader(req);
@@ -346,23 +360,27 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
346
360
  if (metaData) {
347
361
  meta = {
348
362
  ...meta,
349
- ...metaData
363
+ ...metaData,
350
364
  };
351
365
  }
352
366
  }
353
- return () => page({
367
+ const props = {
354
368
  routeParams,
355
369
  searchParams,
356
370
  loaderData: data,
357
371
  locals: {
358
372
  cspNonce: cspNonce,
359
- }
360
- });
373
+ },
374
+ };
375
+ if (toRender === 'error') {
376
+ props.error = error;
377
+ }
378
+ return () => page(props);
361
379
  });
362
380
  const composed = await compose();
363
381
  const rendered = await renderToString(() => composed());
364
382
  if (toRender === 'main') {
365
- const options = cachingOptions;
383
+ const options = pageOptions?.cache;
366
384
  setCache(path, {
367
385
  rendered: rendered,
368
386
  documentMeta: meta,
@@ -380,7 +398,9 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
380
398
  let routeManifest = {};
381
399
  const hydrationScript = ({ nonce, }) => {
382
400
  const script = generateHydrationScript();
383
- return nonce ? script.replace('<script', `<script nonce="${nonce}"`) : script;
401
+ return nonce
402
+ ? script.replace('<script', `<script nonce="${nonce}"`)
403
+ : script;
384
404
  };
385
405
  const onStart = async () => {
386
406
  try {
@@ -399,8 +419,7 @@ const onStart = async () => {
399
419
  };
400
420
  onStart();
401
421
  const handler = eventHandler(async (event) => {
402
- const req = event.node.req;
403
- const res = event.node.res;
422
+ const req = toWebRequest(event);
404
423
  try {
405
424
  if (req.url?.includes('_server')) {
406
425
  return handleServerFunction(event);
@@ -410,9 +429,9 @@ const handler = eventHandler(async (event) => {
410
429
  routeManifest = await createRouteManifest();
411
430
  }
412
431
  const cspNonce = event.locals?.cspNonce;
413
- const url = req.url || '/';
432
+ const url = new URL(req.url).pathname;
414
433
  // extract route params and search params
415
- const params = {};
434
+ let params = {};
416
435
  const searchParams = {};
417
436
  const [pathnamePart, searchParamPart] = url.split('?');
418
437
  if (searchParamPart) {
@@ -421,22 +440,21 @@ const handler = eventHandler(async (event) => {
421
440
  searchParams[key] = decodeURIComponent(value || '');
422
441
  }
423
442
  }
424
- const matched = Object.entries(routeManifest).find(([path, entry]) => {
425
- const pattern = path
426
- .replace(/:\[\*[^/\]]+\]/g, '?(.*)?') // [[...slug]] -> (.*)?
427
- .replace(/:\*[^/]*/g, '.*') // :*slug or :* -> .*
428
- .replace(/:[^/]+/g, '[^/]+'); // :post -> [^/]+
429
- const re = new RegExp(`^${pattern}$`);
430
- return re.test(pathnamePart);
431
- })?.[1];
432
- const routePath = matched && matched.type === 'route'
433
- ? matched.manifestPath.split('/').slice(2).join('/')
434
- : matched && matched.type === 'page'
435
- ? matched.mainPage.manifestPath.split('/').slice(2).join('/')
436
- : '/';
437
- const routeParams = extractRouteParams(routePath, pathnamePart);
438
- if (routeParams) {
439
- Object.assign(params, routeParams.params);
443
+ let matched;
444
+ for (const entry of Object.values(routeManifest)) {
445
+ const match = entry.regex.exec(pathnamePart);
446
+ if (match) {
447
+ matched = entry;
448
+ if (entry.paramKeys) {
449
+ const routePath = matched && matched.type === 'route'
450
+ ? matched.manifestPath.split('/').slice(2).join('/')
451
+ : matched && matched.type === 'page'
452
+ ? matched.mainPage.manifestPath.split('/').slice(2).join('/')
453
+ : '/';
454
+ params = extractRouteParams(routePath, pathnamePart)?.params || {};
455
+ }
456
+ break;
457
+ }
440
458
  }
441
459
  if (matched && matched.type === 'route') {
442
460
  const routeModule = await matched.handler.import();
@@ -444,12 +462,11 @@ const handler = eventHandler(async (event) => {
444
462
  if (reqMethod) {
445
463
  const handler = routeModule[reqMethod];
446
464
  if (typeof handler === 'function') {
447
- const result = await handler(toWebRequest(event), {
465
+ const result = await handler(req, {
448
466
  params: params,
449
467
  searchParams: searchParams,
450
468
  });
451
- await sendNodeResponse(res, result);
452
- return;
469
+ return result;
453
470
  }
454
471
  throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
455
472
  }
@@ -461,245 +478,274 @@ const handler = eventHandler(async (event) => {
461
478
  charset: {
462
479
  type: 'meta',
463
480
  attributes: {
464
- charset: 'UTF-8'
465
- }
481
+ charset: 'UTF-8',
482
+ },
466
483
  },
467
484
  viewport: {
468
485
  type: 'meta',
469
486
  attributes: {
470
487
  name: 'viewport',
471
- content: 'width=device-width, initial-scale=1.0'
472
- }
488
+ content: 'width=device-width, initial-scale=1.0',
489
+ },
473
490
  },
474
491
  title: {
475
492
  type: 'title',
476
493
  attributes: {},
477
- content: 'SolidStep'
494
+ content: 'SolidStep',
478
495
  },
479
496
  build_time: {
480
497
  type: 'meta',
481
498
  attributes: {
482
499
  name: 'x-build-time',
483
500
  content: Date.now().toString(),
484
- description: 'IMPORTANT: This tag indicates the build time of the application and should not be removed.'
501
+ description: 'IMPORTANT: This tag indicates the build time of the application and should not be removed.',
485
502
  },
486
- }
503
+ },
487
504
  };
488
505
  const assets = await clientManifest.inputs[clientManifest.handler].assets();
489
506
  const manifestHtml = `<script ${cspNonce ? `nonce="${cspNonce}"` : ''}>window.manifest=${JSON.stringify(await clientManifest.json())}</script>`;
490
507
  let clientHydrationScript = undefined;
491
- res.setHeader('Content-Type', 'text/html');
492
- res.setHeader('Cache-Control', 'no-cache');
493
- try {
494
- if (!matched) {
508
+ setHeader('Content-Type', 'text/html');
509
+ setHeader('Cache-Control', 'no-cache');
510
+ const stream = new ReadableStream({
511
+ async start(controller) {
512
+ const encoder = new TextEncoder();
513
+ const push = (text) => controller.enqueue(encoder.encode(text));
495
514
  try {
496
- const notFoundPage = routeManifest['/'];
497
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
498
- toRender: 'not-found',
499
- entry: notFoundPage,
500
- routeParams: {},
501
- searchParams: {},
502
- req: toWebRequest(event),
503
- cspNonce,
504
- });
505
- for (const asset of documentAssets) {
506
- assets.push(asset);
515
+ if (!matched) {
516
+ try {
517
+ const notFoundPage = routeManifest['/'];
518
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
519
+ toRender: 'not-found',
520
+ entry: notFoundPage,
521
+ routeParams: {},
522
+ searchParams: {},
523
+ req: req,
524
+ pageOptions: {},
525
+ cspNonce,
526
+ });
527
+ assets.push(...documentAssets);
528
+ clientHydrationScript = `
529
+ <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
530
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
531
+ main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
532
+ </script>
533
+ `;
534
+ html = rendered;
535
+ meta = {
536
+ ...meta,
537
+ ...documentMeta,
538
+ };
539
+ setResponseStatus(404);
540
+ }
541
+ catch (e) {
542
+ console.error('404 module not found:', e);
543
+ setResponseStatus(404);
544
+ push('Not Found');
545
+ controller.close();
546
+ return;
547
+ }
548
+ }
549
+ else {
550
+ const { options } = matched.mainPage.options
551
+ ? await matched.mainPage.options.import()
552
+ : { options: {} };
553
+ if (options?.responseHeaders) {
554
+ const headers = options.responseHeaders;
555
+ for (const [key, value] of Object.entries(headers)) {
556
+ setHeader(key, value);
557
+ }
558
+ }
559
+ try {
560
+ if (!matched.loadingPage) {
561
+ throw new Error('No loading page');
562
+ }
563
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
564
+ toRender: 'loading',
565
+ entry: matched,
566
+ routeParams: params,
567
+ searchParams,
568
+ req: req,
569
+ pageOptions: options,
570
+ cspNonce,
571
+ });
572
+ const assetsHtml = assets
573
+ .concat(documentAssets)
574
+ .map((asset) => {
575
+ const attributeString = Object.entries(asset.attrs)
576
+ .map(([key, value]) => `${key}="${value}"`)
577
+ .join(' ');
578
+ if (asset.tag === 'script') {
579
+ return `<script ${attributeString}></script>`;
580
+ }
581
+ if (asset.tag === 'link') {
582
+ return `<link ${attributeString}>`;
583
+ }
584
+ if (asset.tag === 'style') {
585
+ return `<style ${attributeString}>${asset.children || ''}</style>`;
586
+ }
587
+ })
588
+ .join('\n');
589
+ const html = `
590
+ <!doctype html>
591
+ <html lang="en">
592
+ <head>
593
+ ${generateHtmlHead({
594
+ ...meta,
595
+ ...documentMeta,
596
+ })}
597
+ ${assetsHtml}
598
+ ${hydrationScript({ nonce: cspNonce })}
599
+ </head>
600
+ <noscript>
601
+ Please enable JavaScript to view the content.<br/>
602
+ </noscript>
603
+ ${rendered}
604
+ </html>
605
+ `;
606
+ push(html);
607
+ push(`
608
+ <script type="module" data-hydration="loading" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
609
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
610
+ main('${matched.loadingPage?.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
611
+ </script>
612
+ `);
613
+ loading = true;
614
+ }
615
+ catch (e) {
616
+ // skip
617
+ }
618
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
619
+ toRender: 'main',
620
+ entry: matched,
621
+ routeParams: params,
622
+ searchParams,
623
+ req: req,
624
+ pageOptions: options,
625
+ cspNonce,
626
+ });
627
+ assets.push(...documentAssets);
628
+ clientHydrationScript = `
629
+ <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
630
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
631
+ main('${matched.mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
632
+ </script>
633
+ `;
634
+ html = rendered;
635
+ meta = {
636
+ ...meta,
637
+ ...documentMeta,
638
+ };
639
+ setResponseStatus(200);
507
640
  }
508
- clientHydrationScript = `
509
- <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
510
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
511
- main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
512
- </script>
513
- `;
514
- html = rendered;
515
- meta = {
516
- ...meta,
517
- ...documentMeta
518
- };
519
- res.statusCode = 404;
520
- }
521
- catch (e) {
522
- console.error('404 module not found:', e);
523
- res.statusCode = 404;
524
- return res.end('Not Found');
525
641
  }
526
- }
527
- else {
528
- try {
529
- if (!matched.loadingPage) {
530
- throw new Error('No loading page');
642
+ catch (e1) {
643
+ if (e1 instanceof RedirectError ||
644
+ e1.name === 'RedirectError') {
645
+ setHeader('Location', e1.message);
646
+ setResponseStatus(302);
647
+ controller.close();
648
+ return;
531
649
  }
532
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
533
- toRender: 'loading',
534
- entry: matched,
535
- routeParams: params,
536
- searchParams,
537
- req: toWebRequest(event),
538
- cspNonce,
539
- });
540
- const assetsHtml = assets.concat(documentAssets).map((asset) => {
650
+ if (import.meta.env.DEV) {
651
+ console.error(e1);
652
+ }
653
+ try {
654
+ const errorPage = matched.errorPage;
655
+ if (!errorPage) {
656
+ throw e1;
657
+ }
658
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
659
+ toRender: 'error',
660
+ entry: matched,
661
+ routeParams: params,
662
+ searchParams,
663
+ req: req,
664
+ pageOptions: {},
665
+ cspNonce,
666
+ error: e1
667
+ });
668
+ assets.push(...documentAssets);
669
+ clientHydrationScript = `
670
+ <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
671
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
672
+ main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
673
+ </script>
674
+ `;
675
+ html = rendered;
676
+ meta = {
677
+ ...meta,
678
+ ...documentMeta,
679
+ };
680
+ // statusCode = 500;
681
+ setResponseStatus(500);
682
+ }
683
+ catch (e2) {
684
+ throw e1;
685
+ }
686
+ }
687
+ if (loading) {
688
+ const assetsHtml = assets
689
+ .map((asset) => {
541
690
  const attributeString = Object.entries(asset.attrs)
542
691
  .map(([key, value]) => `${key}="${value}"`)
543
692
  .join(' ');
544
- if (asset.tag === 'script') {
545
- return `<script ${attributeString}></script>`;
546
- }
547
693
  if (asset.tag === 'link') {
548
694
  return `<link ${attributeString}>`;
549
695
  }
550
696
  if (asset.tag === 'style') {
551
697
  return `<style ${attributeString}>${asset.children || ''}</style>`;
552
698
  }
553
- }).join('\n');
554
- const html = `
555
- <!doctype html>
556
- <html lang="en">
557
- <head>
558
- ${generateHtmlHead({
559
- ...meta,
560
- ...documentMeta,
561
- })}
562
- ${assetsHtml}
563
- ${hydrationScript({ nonce: cspNonce })}
564
- </head>
565
- <noscript>
566
- Please enable JavaScript to view the content.<br/>
567
- </noscript>
568
- ${rendered}
569
- </html>
570
- `;
571
- res.write(html);
572
- res.write(`
573
- <script type="module" data-hydration="loading" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
574
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
575
- main('${matched.loadingPage?.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
576
- </script>
699
+ return '';
700
+ })
701
+ .join('\n');
702
+ push(`
703
+ <script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
704
+ const head = document.querySelector('head');
705
+ const scripts = Array.from(head.querySelectorAll('script'));
706
+ head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
707
+ scripts.forEach(script => {
708
+ head.appendChild(script);
709
+ });
710
+ document.querySelector('script[data-hydration="loading"]')?.remove();
711
+ const loading = document.querySelector('body');
712
+ loading.innerHTML = \`${html}\`;
713
+ </script>
577
714
  `);
578
- loading = true;
579
- }
580
- catch (e) {
581
- // skip
582
- }
583
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
584
- toRender: 'main',
585
- entry: matched,
586
- routeParams: params,
587
- searchParams,
588
- req: toWebRequest(event),
589
- cspNonce,
590
- });
591
- for (const asset of documentAssets) {
592
- assets.push(asset);
593
- }
594
- clientHydrationScript = `
595
- <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
596
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
597
- main('${matched.mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
598
- </script>
599
- `;
600
- html = rendered;
601
- meta = {
602
- ...meta,
603
- ...documentMeta
604
- };
605
- }
606
- }
607
- catch (e1) {
608
- if (e1 instanceof RedirectError ||
609
- e1.name === 'RedirectError') {
610
- throw e1;
611
- }
612
- try {
613
- const errorPage = matched.errorPage;
614
- if (!errorPage) {
615
- throw e1;
616
- }
617
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
618
- toRender: 'error',
619
- entry: matched,
620
- routeParams: params,
621
- searchParams,
622
- req: toWebRequest(event),
623
- cspNonce,
624
- });
625
- for (const asset of documentAssets) {
626
- assets.push(asset);
627
- }
628
- clientHydrationScript = `
629
- <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
630
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
631
- main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
632
- </script>
633
- `;
634
- html = rendered;
635
- meta = {
636
- ...meta,
637
- ...documentMeta
638
- };
639
- res.statusCode = 500;
640
- }
641
- catch (e2) {
642
- throw e1;
643
- }
644
- }
645
- if (loading) {
646
- const assetsHtml = assets.map((asset) => {
647
- const attributeString = Object.entries(asset.attrs)
648
- .map(([key, value]) => `${key}="${value}"`)
649
- .join(' ');
650
- if (asset.tag === 'link') {
651
- return `<link ${attributeString}>`;
652
- }
653
- if (asset.tag === 'style') {
654
- return `<style ${attributeString}>${asset.children || ''}</style>`;
715
+ push(manifestHtml);
716
+ push(clientHydrationScript);
717
+ controller.close();
718
+ return;
655
719
  }
656
- return '';
657
- }).join('\n');
658
- res.write(`
659
- <script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
660
- const head = document.querySelector('head');
661
- const scripts = Array.from(head.querySelectorAll('script'));
662
- head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
663
- scripts.forEach(script => {
664
- head.appendChild(script);
665
- });
666
- document.querySelector('script[data-hydration="loading"]')?.remove();
667
- const loading = document.querySelector('body');
668
- loading.innerHTML = \`${html}\`;
669
- </script>
670
- `);
671
- res.write(manifestHtml);
672
- return res.end(clientHydrationScript);
673
- }
674
- const assetsHtml = assets.map((asset) => {
675
- const attributeString = Object.entries(asset.attrs)
676
- .map(([key, value]) => `${key}="${value}"`)
677
- .join(' ');
678
- if (asset.tag === 'script') {
679
- return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
680
- }
681
- if (asset.tag === 'link') {
682
- return `<link ${attributeString}>`;
683
- }
684
- if (asset.tag === 'style') {
685
- return `<style ${attributeString}>${asset.children || ''}</style>`;
686
- }
687
- }).join('\n');
688
- const transformHtml = template
689
- .replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
690
- .replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
691
- return res.end(transformHtml);
720
+ const assetsHtml = assets
721
+ .map((asset) => {
722
+ const attributeString = Object.entries(asset.attrs)
723
+ .map(([key, value]) => `${key}="${value}"`)
724
+ .join(' ');
725
+ if (asset.tag === 'script') {
726
+ return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
727
+ }
728
+ if (asset.tag === 'link') {
729
+ return `<link ${attributeString}>`;
730
+ }
731
+ if (asset.tag === 'style') {
732
+ return `<style ${attributeString}>${asset.children || ''}</style>`;
733
+ }
734
+ })
735
+ .join('\n');
736
+ const transformHtml = template
737
+ .replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
738
+ .replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
739
+ push(transformHtml);
740
+ controller.close();
741
+ return;
742
+ },
743
+ });
744
+ return stream;
692
745
  }
693
746
  catch (e) {
694
- if (e instanceof RedirectError ||
695
- e.name === 'RedirectError') {
696
- res.statusCode = 302;
697
- res.setHeader('Location', e.message);
698
- return res.end('Redirecting...');
699
- }
700
747
  console.error(e);
701
- res.statusCode = 500;
702
- return res.end('Internal Server Error');
748
+ return new Response('Internal Server Error', { status: 500 });
703
749
  }
704
750
  });
705
751
  export default handler;
@@ -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,oBA0M1D;;AAED,wBAAkD"}
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"}
@@ -244,9 +244,10 @@ export async function handleServerFunction(event) {
244
244
  // Step 4: diff the cache with new html from server
245
245
  const reqUrl = new URL(request.url);
246
246
  const serverUrl = reqUrl.origin;
247
- await fetch(serverUrl + revalidatePath, {
247
+ const response = await fetch(serverUrl + revalidatePath, {
248
248
  method: 'GET'
249
249
  }, false);
250
+ await response.text(); // ensure the fetch is completed and cache is populated
250
251
  const newCacheValue = getCache(revalidatePath);
251
252
  const newHtml = newCacheValue?.rendered;
252
253
  const dd = createDiffDOM({