solidstep 0.1.8 → 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
@@ -370,6 +370,10 @@ export const options = {
370
370
  },
371
371
  };
372
372
  ```
373
+ - Regarding caching, setting `ttl` to `0` or omitting it will disable caching for that page.
374
+ - Setting a positive integer value will cache the page for that duration in milliseconds.
375
+ - Invalidation of cached pages can be done using the `invalidateCache` and `revalidatePath` utilities.
376
+ - The `responseHeaders` option allows you to set custom HTTP headers for the page response.
373
377
 
374
378
  ## API Routes
375
379
 
@@ -801,6 +805,16 @@ As SolidStep is built using Vite, it follows the same guide as stated in [Vite d
801
805
  - Advanced caching strategies
802
806
  - WebSocket support
803
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
+
804
818
  ## License
805
819
 
806
820
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidstep",
3
- "version": "0.1.8",
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":"AAyhBA,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,28 +360,33 @@ 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') {
383
+ const options = pageOptions?.cache;
365
384
  setCache(path, {
366
385
  rendered: rendered,
367
386
  documentMeta: meta,
368
387
  documentAssets: assets,
369
388
  loaderData: loaderData,
370
- }, cachingOptions ? cachingOptions.ttl : 0);
389
+ }, options?.ttl ? options.ttl : 0);
371
390
  }
372
391
  return {
373
392
  rendered: rendered,
@@ -379,7 +398,9 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
379
398
  let routeManifest = {};
380
399
  const hydrationScript = ({ nonce, }) => {
381
400
  const script = generateHydrationScript();
382
- return nonce ? script.replace('<script', `<script nonce="${nonce}"`) : script;
401
+ return nonce
402
+ ? script.replace('<script', `<script nonce="${nonce}"`)
403
+ : script;
383
404
  };
384
405
  const onStart = async () => {
385
406
  try {
@@ -398,8 +419,7 @@ const onStart = async () => {
398
419
  };
399
420
  onStart();
400
421
  const handler = eventHandler(async (event) => {
401
- const req = event.node.req;
402
- const res = event.node.res;
422
+ const req = toWebRequest(event);
403
423
  try {
404
424
  if (req.url?.includes('_server')) {
405
425
  return handleServerFunction(event);
@@ -409,9 +429,9 @@ const handler = eventHandler(async (event) => {
409
429
  routeManifest = await createRouteManifest();
410
430
  }
411
431
  const cspNonce = event.locals?.cspNonce;
412
- const url = req.url || '/';
432
+ const url = new URL(req.url).pathname;
413
433
  // extract route params and search params
414
- const params = {};
434
+ let params = {};
415
435
  const searchParams = {};
416
436
  const [pathnamePart, searchParamPart] = url.split('?');
417
437
  if (searchParamPart) {
@@ -420,22 +440,21 @@ const handler = eventHandler(async (event) => {
420
440
  searchParams[key] = decodeURIComponent(value || '');
421
441
  }
422
442
  }
423
- const matched = Object.entries(routeManifest).find(([path, entry]) => {
424
- const pattern = path
425
- .replace(/:\[\*[^/\]]+\]/g, '?(.*)?') // [[...slug]] -> (.*)?
426
- .replace(/:\*[^/]*/g, '.*') // :*slug or :* -> .*
427
- .replace(/:[^/]+/g, '[^/]+'); // :post -> [^/]+
428
- const re = new RegExp(`^${pattern}$`);
429
- return re.test(pathnamePart);
430
- })?.[1];
431
- const routePath = matched && matched.type === 'route'
432
- ? matched.manifestPath.split('/').slice(2).join('/')
433
- : matched && matched.type === 'page'
434
- ? matched.mainPage.manifestPath.split('/').slice(2).join('/')
435
- : '/';
436
- const routeParams = extractRouteParams(routePath, pathnamePart);
437
- if (routeParams) {
438
- 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
+ }
439
458
  }
440
459
  if (matched && matched.type === 'route') {
441
460
  const routeModule = await matched.handler.import();
@@ -443,12 +462,11 @@ const handler = eventHandler(async (event) => {
443
462
  if (reqMethod) {
444
463
  const handler = routeModule[reqMethod];
445
464
  if (typeof handler === 'function') {
446
- const result = await handler(toWebRequest(event), {
465
+ const result = await handler(req, {
447
466
  params: params,
448
467
  searchParams: searchParams,
449
468
  });
450
- await sendNodeResponse(res, result);
451
- return;
469
+ return result;
452
470
  }
453
471
  throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
454
472
  }
@@ -460,245 +478,274 @@ const handler = eventHandler(async (event) => {
460
478
  charset: {
461
479
  type: 'meta',
462
480
  attributes: {
463
- charset: 'UTF-8'
464
- }
481
+ charset: 'UTF-8',
482
+ },
465
483
  },
466
484
  viewport: {
467
485
  type: 'meta',
468
486
  attributes: {
469
487
  name: 'viewport',
470
- content: 'width=device-width, initial-scale=1.0'
471
- }
488
+ content: 'width=device-width, initial-scale=1.0',
489
+ },
472
490
  },
473
491
  title: {
474
492
  type: 'title',
475
493
  attributes: {},
476
- content: 'SolidStep'
494
+ content: 'SolidStep',
477
495
  },
478
496
  build_time: {
479
497
  type: 'meta',
480
498
  attributes: {
481
499
  name: 'x-build-time',
482
500
  content: Date.now().toString(),
483
- 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.',
484
502
  },
485
- }
503
+ },
486
504
  };
487
505
  const assets = await clientManifest.inputs[clientManifest.handler].assets();
488
506
  const manifestHtml = `<script ${cspNonce ? `nonce="${cspNonce}"` : ''}>window.manifest=${JSON.stringify(await clientManifest.json())}</script>`;
489
507
  let clientHydrationScript = undefined;
490
- res.setHeader('Content-Type', 'text/html');
491
- res.setHeader('Cache-Control', 'no-cache');
492
- try {
493
- 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));
494
514
  try {
495
- const notFoundPage = routeManifest['/'];
496
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
497
- toRender: 'not-found',
498
- entry: notFoundPage,
499
- routeParams: {},
500
- searchParams: {},
501
- req: toWebRequest(event),
502
- cspNonce,
503
- });
504
- for (const asset of documentAssets) {
505
- 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);
506
640
  }
507
- clientHydrationScript = `
508
- <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
509
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
510
- main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
511
- </script>
512
- `;
513
- html = rendered;
514
- meta = {
515
- ...meta,
516
- ...documentMeta
517
- };
518
- res.statusCode = 404;
519
- }
520
- catch (e) {
521
- console.error('404 module not found:', e);
522
- res.statusCode = 404;
523
- return res.end('Not Found');
524
641
  }
525
- }
526
- else {
527
- try {
528
- if (!matched.loadingPage) {
529
- 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;
530
649
  }
531
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
532
- toRender: 'loading',
533
- entry: matched,
534
- routeParams: params,
535
- searchParams,
536
- req: toWebRequest(event),
537
- cspNonce,
538
- });
539
- 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) => {
540
690
  const attributeString = Object.entries(asset.attrs)
541
691
  .map(([key, value]) => `${key}="${value}"`)
542
692
  .join(' ');
543
- if (asset.tag === 'script') {
544
- return `<script ${attributeString}></script>`;
545
- }
546
693
  if (asset.tag === 'link') {
547
694
  return `<link ${attributeString}>`;
548
695
  }
549
696
  if (asset.tag === 'style') {
550
697
  return `<style ${attributeString}>${asset.children || ''}</style>`;
551
698
  }
552
- }).join('\n');
553
- const html = `
554
- <!doctype html>
555
- <html lang="en">
556
- <head>
557
- ${generateHtmlHead({
558
- ...meta,
559
- ...documentMeta,
560
- })}
561
- ${assetsHtml}
562
- ${hydrationScript({ nonce: cspNonce })}
563
- </head>
564
- <noscript>
565
- Please enable JavaScript to view the content.<br/>
566
- </noscript>
567
- ${rendered}
568
- </html>
569
- `;
570
- res.write(html);
571
- res.write(`
572
- <script type="module" data-hydration="loading" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
573
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
574
- main('${matched.loadingPage?.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
575
- </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>
576
714
  `);
577
- loading = true;
578
- }
579
- catch (e) {
580
- // skip
581
- }
582
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
583
- toRender: 'main',
584
- entry: matched,
585
- routeParams: params,
586
- searchParams,
587
- req: toWebRequest(event),
588
- cspNonce,
589
- });
590
- for (const asset of documentAssets) {
591
- assets.push(asset);
592
- }
593
- clientHydrationScript = `
594
- <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
595
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
596
- main('${matched.mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
597
- </script>
598
- `;
599
- html = rendered;
600
- meta = {
601
- ...meta,
602
- ...documentMeta
603
- };
604
- }
605
- }
606
- catch (e1) {
607
- if (e1 instanceof RedirectError ||
608
- e1.name === 'RedirectError') {
609
- throw e1;
610
- }
611
- try {
612
- const errorPage = matched.errorPage;
613
- if (!errorPage) {
614
- throw e1;
615
- }
616
- const { rendered, documentMeta, documentAssets, loaderData, } = await render({
617
- toRender: 'error',
618
- entry: matched,
619
- routeParams: params,
620
- searchParams,
621
- req: toWebRequest(event),
622
- cspNonce,
623
- });
624
- for (const asset of documentAssets) {
625
- assets.push(asset);
626
- }
627
- clientHydrationScript = `
628
- <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
629
- import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
630
- main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
631
- </script>
632
- `;
633
- html = rendered;
634
- meta = {
635
- ...meta,
636
- ...documentMeta
637
- };
638
- res.statusCode = 500;
639
- }
640
- catch (e2) {
641
- throw e1;
642
- }
643
- }
644
- if (loading) {
645
- const assetsHtml = assets.map((asset) => {
646
- const attributeString = Object.entries(asset.attrs)
647
- .map(([key, value]) => `${key}="${value}"`)
648
- .join(' ');
649
- if (asset.tag === 'link') {
650
- return `<link ${attributeString}>`;
651
- }
652
- if (asset.tag === 'style') {
653
- return `<style ${attributeString}>${asset.children || ''}</style>`;
715
+ push(manifestHtml);
716
+ push(clientHydrationScript);
717
+ controller.close();
718
+ return;
654
719
  }
655
- return '';
656
- }).join('\n');
657
- res.write(`
658
- <script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
659
- const head = document.querySelector('head');
660
- const scripts = Array.from(head.querySelectorAll('script'));
661
- head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
662
- scripts.forEach(script => {
663
- head.appendChild(script);
664
- });
665
- document.querySelector('script[data-hydration="loading"]')?.remove();
666
- const loading = document.querySelector('body');
667
- loading.innerHTML = \`${html}\`;
668
- </script>
669
- `);
670
- res.write(manifestHtml);
671
- return res.end(clientHydrationScript);
672
- }
673
- const assetsHtml = assets.map((asset) => {
674
- const attributeString = Object.entries(asset.attrs)
675
- .map(([key, value]) => `${key}="${value}"`)
676
- .join(' ');
677
- if (asset.tag === 'script') {
678
- return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
679
- }
680
- if (asset.tag === 'link') {
681
- return `<link ${attributeString}>`;
682
- }
683
- if (asset.tag === 'style') {
684
- return `<style ${attributeString}>${asset.children || ''}</style>`;
685
- }
686
- }).join('\n');
687
- const transformHtml = template
688
- .replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
689
- .replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
690
- 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;
691
745
  }
692
746
  catch (e) {
693
- if (e instanceof RedirectError ||
694
- e.name === 'RedirectError') {
695
- res.statusCode = 302;
696
- res.setHeader('Location', e.message);
697
- return res.end('Redirecting...');
698
- }
699
747
  console.error(e);
700
- res.statusCode = 500;
701
- return res.end('Internal Server Error');
748
+ return new Response('Internal Server Error', { status: 500 });
702
749
  }
703
750
  });
704
751
  export default handler;
package/utils/cache.js CHANGED
@@ -37,7 +37,7 @@ const removeTail = () => {
37
37
  };
38
38
  export const getCache = (key) => {
39
39
  const entry = cacheMap.get(key);
40
- if (!entry)
40
+ if (!entry || !entry.expiresAt)
41
41
  return null;
42
42
  if (entry.expiresAt && entry.expiresAt < Date.now()) {
43
43
  cacheMap.delete(key);
@@ -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({