solidstep 0.2.0 → 0.3.1

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/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,80 @@ 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
+ import { createNode, insertRoute, matchRoute, } from './utils/path-router';
12
+ // Module cache for dynamically imported modules
13
+ const moduleCache = new Map();
14
+ const getCachedModule = async (importFn) => {
15
+ const key = importFn.src;
16
+ if (moduleCache.has(key)) {
17
+ return moduleCache.get(key);
18
+ }
19
+ const module = await importFn.import();
20
+ moduleCache.set(key, module);
21
+ return module;
22
+ };
23
+ const isPageFile = (file) => file.endsWith('page.tsx') ||
24
+ file.endsWith('page.jsx') ||
25
+ file.endsWith('page.ts') ||
26
+ file.endsWith('page.js');
15
27
  const isRouteFile = (file) => file.endsWith('route.ts') || file.endsWith('route.js');
16
- const parseSegment = (part) => part.startsWith('[') ? `:${part.slice(1, -1).replace(/\.\.\./, '*')}` : part;
28
+ const getNormalizedPath = (path, clean) => {
29
+ const segments = path.split('/').slice(2);
30
+ if (clean)
31
+ return `/${segments.filter((s) => !s.startsWith('(')).join('/')}`;
32
+ return `/${segments.join('/')}`;
33
+ };
17
34
  const createRouteManifest = async () => {
18
- const entries = {};
35
+ const rootNode = createNode();
19
36
  const allRoutes = [];
20
- const allLayouts = [];
21
- const allLoadingPages = [];
22
- const allErrorPages = [];
23
- const allGroups = [];
37
+ const layoutsMap = new Map();
38
+ const loadingPagesMap = new Map();
39
+ const errorPagesMap = new Map();
40
+ const groupsMap = new Map();
24
41
  let notFoundPage;
25
42
  for (const fileRoute of fileRoutes) {
26
43
  if (fileRoute.type === 'route') {
27
44
  allRoutes.push(fileRoute);
28
45
  }
29
46
  if (fileRoute.type === 'layout') {
30
- allLayouts.push(fileRoute);
47
+ const path = getNormalizedPath(fileRoute.path);
48
+ layoutsMap.set(path, fileRoute);
31
49
  }
32
50
  if (fileRoute.type === 'not-found') {
33
51
  notFoundPage = fileRoute;
34
52
  }
35
53
  if (fileRoute.type === 'loading') {
36
- allLoadingPages.push(fileRoute);
54
+ const path = getNormalizedPath(fileRoute.path);
55
+ loadingPagesMap.set(path, fileRoute);
37
56
  }
38
57
  if (fileRoute.type === 'error') {
39
- allErrorPages.push(fileRoute);
58
+ const path = getNormalizedPath(fileRoute.path);
59
+ errorPagesMap.set(path, fileRoute);
40
60
  }
41
61
  if (fileRoute.type === 'group') {
42
- allGroups.push(fileRoute);
62
+ const parentPath = fileRoute.parent
63
+ ? getNormalizedPath(fileRoute.parent)
64
+ : '';
65
+ const existing = groupsMap.get(parentPath) || [];
66
+ existing.push(fileRoute);
67
+ groupsMap.set(parentPath, existing);
43
68
  }
44
69
  }
70
+ const regex = /\?(?:pick=.*)*/g;
45
71
  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;
72
+ const routePath = getNormalizedPath(fileRoute.path, true);
73
+ const routeMatcherPath = getNormalizedPath(fileRoute.path);
49
74
  const src = fileRoute.$handler?.src.replace(regex, '');
50
75
  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
- });
76
+ const loadingPage = loadingPagesMap.get(routeMatcherPath);
77
+ const matchedGroups = groupsMap.get(routePath);
59
78
  const groups = {};
60
79
  if (matchedGroups && matchedGroups.length > 0) {
61
80
  for (const group of matchedGroups) {
62
- const groupName = group.path.split('/').filter(s => !(s.startsWith('('))).map(parseSegment).at(-1);
81
+ const groupName = group.path
82
+ .split('/')
83
+ .filter((s) => !s.startsWith('('))
84
+ .at(-1);
63
85
  if (!groupName)
64
86
  continue;
65
87
  groups[groupName] = {
@@ -69,20 +91,18 @@ const createRouteManifest = async () => {
69
91
  };
70
92
  }
71
93
  }
94
+ const segments = routeMatcherPath.split('/').filter(Boolean);
72
95
  let errorPage;
73
96
  const layouts = [];
74
- for (let i = segments.length; i > (routePath === '/' ? 0 : -1); i--) {
75
- const path = `/${segments.slice(0, i).join('/')}`;
97
+ // We need to traverse from root to leaf to build layouts order correctly?
98
+ // Original code: i = segments.length down to 0. unshift matches.
99
+ // i=length: /a/b/c. i=0: /.
100
+ for (let i = segments.length; i >= 0; i--) {
101
+ const path = i === 0 ? '/' : `/${segments.slice(0, i).join('/')}`;
76
102
  if (!errorPage) {
77
- errorPage = allErrorPages.find(route => {
78
- const routePath = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
79
- return routePath === path;
80
- });
103
+ errorPage = errorPagesMap.get(path);
81
104
  }
82
- const layout = allLayouts.find(route => {
83
- const routePath = `/${route.path.split('/').slice(2).map(parseSegment).join('/')}`;
84
- return routePath === path;
85
- });
105
+ const layout = layoutsMap.get(path);
86
106
  if (layout) {
87
107
  layouts.unshift({
88
108
  manifestPath: layout.path,
@@ -92,7 +112,7 @@ const createRouteManifest = async () => {
92
112
  });
93
113
  }
94
114
  }
95
- entries[routePath] = {
115
+ const entry = {
96
116
  type: 'page',
97
117
  mainPage: {
98
118
  manifestPath: fileRoute.path,
@@ -101,69 +121,50 @@ const createRouteManifest = async () => {
101
121
  generateMeta: fileRoute.$generateMeta,
102
122
  options: fileRoute.$options,
103
123
  },
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,
124
+ loadingPage: loadingPage
125
+ ? {
126
+ page: loadingPage.$component,
127
+ generateMeta: loadingPage.$generateMeta,
128
+ manifestPath: loadingPage.path,
129
+ }
130
+ : undefined,
131
+ errorPage: errorPage
132
+ ? {
133
+ page: errorPage.$component,
134
+ generateMeta: errorPage.$generateMeta,
135
+ manifestPath: errorPage.path,
136
+ }
137
+ : undefined,
138
+ notFoundPage: routePath === '/' && notFoundPage
139
+ ? {
140
+ page: notFoundPage.$component,
141
+ generateMeta: notFoundPage.$generateMeta,
142
+ manifestPath: notFoundPage.path,
143
+ }
144
+ : undefined,
119
145
  layouts: layouts,
120
146
  groups: groups,
121
147
  };
148
+ insertRoute(rootNode, routePath, entry);
122
149
  }
123
150
  else if (src && isRouteFile(src)) {
124
- entries[routePath] = {
151
+ const entry = {
125
152
  type: 'route',
153
+ routePath,
126
154
  handler: fileRoute.$handler,
127
155
  manifestPath: fileRoute.path,
128
156
  };
157
+ insertRoute(rootNode, routePath, entry);
129
158
  }
130
159
  }
131
- return entries;
160
+ return rootNode;
132
161
  };
133
- const extractRouteParams = (route, url) => {
134
- const routeSegments = route.split('/').filter(s => !(s.startsWith('('))).filter(Boolean);
135
- const urlSegments = url.split('/').filter(Boolean);
136
- const params = {};
137
- let matched = true;
138
- for (let i = 0; i < routeSegments.length; i++) {
139
- const routeSeg = routeSegments[i];
140
- const urlSeg = urlSegments[i];
141
- const isDynamic = routeSeg.startsWith('[') && routeSeg.endsWith(']');
142
- if (isDynamic) {
143
- if (routeSeg.includes('...')) {
144
- // Catch-all parameter
145
- const isCatchAll = routeSeg.startsWith('[[') && routeSeg.endsWith(']]');
146
- const paramName = routeSeg.slice(isCatchAll ? 5 : 4, isCatchAll ? -2 : -1);
147
- params[paramName] = urlSegments.slice(i);
148
- break; // No more segments to match
149
- }
150
- const paramName = routeSeg.slice(1, -1);
151
- params[paramName] = urlSeg;
152
- }
153
- else if (routeSeg !== urlSeg) {
154
- matched = false;
155
- break;
156
- }
157
- }
158
- if (matched)
159
- return { route, params };
160
- };
161
- const template = `
162
- <!DOCTYPE html>
163
- <html lang="en">
164
- <head><!--app-head--></head>
165
- <!--app-body-->
166
- </html>
162
+ const template = `
163
+ <!DOCTYPE html>
164
+ <html lang="en">
165
+ <head><!--app-head--></head>
166
+ <!--app-body-->
167
+ </html>
167
168
  `;
168
169
  const generateHtmlHead = (meta) => {
169
170
  const head = Object.entries(meta)
@@ -177,7 +178,9 @@ const generateHtmlHead = (meta) => {
177
178
  .join(' ');
178
179
  return `<meta ${attrs}>`;
179
180
  }
180
- if (value.type === 'link' || value.type === 'style' || value.type === 'script') {
181
+ if (value.type === 'link' ||
182
+ value.type === 'style' ||
183
+ value.type === 'script') {
181
184
  const attrs = Object.entries(value.attributes)
182
185
  .map(([attrKey, attrValue]) => `${attrKey}="${attrValue}"`)
183
186
  .join(' ');
@@ -188,45 +191,11 @@ const generateHtmlHead = (meta) => {
188
191
  .join('\n');
189
192
  return head;
190
193
  };
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, }) => {
194
+ const render = async ({ toRender, entry, routeParams, searchParams, req, pageOptions, cspNonce, error, }) => {
218
195
  const url = new URL(req.url);
219
196
  const path = url.pathname;
220
197
  const cachedEntry = getCache(path);
221
198
  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
199
  return {
231
200
  rendered: cachedEntry.rendered,
232
201
  documentMeta: cachedEntry.documentMeta,
@@ -234,7 +203,6 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
234
203
  loaderData: cachedEntry.loaderData,
235
204
  };
236
205
  }
237
- let cachingOptions = undefined;
238
206
  let meta = {};
239
207
  const loaderData = {};
240
208
  const clientManifest = getManifest('client');
@@ -242,12 +210,14 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
242
210
  const compose = entry.layouts.reduceRight((children, layout, index) => async () => {
243
211
  const moduleSrc = `${layout.layout.src}&pick=$css`;
244
212
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
245
- for (const asset of moduleAssets) {
246
- assets.push(asset);
247
- }
248
- 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 };
213
+ assets.push(...moduleAssets);
214
+ const { default: layoutModule } = await getCachedModule(layout.layout);
215
+ const { loader: layoutLoader } = layout.loader
216
+ ? await getCachedModule(layout.loader)
217
+ : { loader: null };
218
+ const { generateMeta: generateMetaPage } = layout.generateMeta
219
+ ? await getCachedModule(layout.generateMeta)
220
+ : { generateMeta: null };
251
221
  let data = {};
252
222
  if (generateMetaPage) {
253
223
  const metaData = await generateMetaPage({
@@ -257,7 +227,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
257
227
  if (metaData) {
258
228
  meta = {
259
229
  ...meta,
260
- ...metaData
230
+ ...metaData,
261
231
  };
262
232
  }
263
233
  }
@@ -275,11 +245,11 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
275
245
  slotPromises.push((async () => {
276
246
  const moduleSrc = `${group.page.src}&pick=$css`;
277
247
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
278
- for (const asset of moduleAssets) {
279
- assets.push(asset);
280
- }
281
- const { default: groupPage } = await group.page.import();
282
- const { loader: groupLoader } = group.loader ? await group.loader.import() : { loader: null };
248
+ assets.push(...moduleAssets);
249
+ const { default: groupPage } = await getCachedModule(group.page);
250
+ const { loader: groupLoader } = group.loader
251
+ ? await getCachedModule(group.loader)
252
+ : { loader: null };
283
253
  let data = {};
284
254
  if (groupLoader) {
285
255
  const result = await groupLoader.loader(req);
@@ -289,7 +259,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
289
259
  slots[groupName.replace('@', '')] = () => groupPage({
290
260
  routeParams,
291
261
  searchParams,
292
- loaderData: data
262
+ loaderData: data,
293
263
  });
294
264
  })());
295
265
  }
@@ -303,7 +273,7 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
303
273
  slots: slots,
304
274
  locals: {
305
275
  cspNonce: cspNonce,
306
- }
276
+ },
307
277
  });
308
278
  }, async () => {
309
279
  const pageToRender = toRender === 'loading'
@@ -315,23 +285,14 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
315
285
  : entry.mainPage;
316
286
  const moduleSrc = `${pageToRender.page.src}&pick=$css`;
317
287
  const moduleAssets = await clientManifest.inputs[moduleSrc].assets();
318
- for (const asset of moduleAssets) {
319
- assets.push(asset);
320
- }
321
- 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
- }
288
+ assets.push(...moduleAssets);
289
+ const { default: page } = await getCachedModule(pageToRender.page);
290
+ const { loader: pageLoader } = pageToRender.loader
291
+ ? await getCachedModule(pageToRender.loader)
292
+ : { loader: null };
293
+ const { generateMeta } = pageToRender.generateMeta
294
+ ? await getCachedModule(pageToRender.generateMeta)
295
+ : { generateMeta: null };
335
296
  let data = {};
336
297
  if (pageLoader) {
337
298
  const result = await pageLoader.loader(req);
@@ -346,23 +307,27 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
346
307
  if (metaData) {
347
308
  meta = {
348
309
  ...meta,
349
- ...metaData
310
+ ...metaData,
350
311
  };
351
312
  }
352
313
  }
353
- return () => page({
314
+ const props = {
354
315
  routeParams,
355
316
  searchParams,
356
317
  loaderData: data,
357
318
  locals: {
358
319
  cspNonce: cspNonce,
359
- }
360
- });
320
+ },
321
+ };
322
+ if (toRender === 'error') {
323
+ props.error = error;
324
+ }
325
+ return () => page(props);
361
326
  });
362
327
  const composed = await compose();
363
328
  const rendered = await renderToString(() => composed());
364
329
  if (toRender === 'main') {
365
- const options = cachingOptions;
330
+ const options = pageOptions?.cache;
366
331
  setCache(path, {
367
332
  rendered: rendered,
368
333
  documentMeta: meta,
@@ -377,10 +342,12 @@ const render = async ({ toRender, entry, routeParams, searchParams, req, cspNonc
377
342
  loaderData: loaderData,
378
343
  };
379
344
  };
380
- let routeManifest = {};
345
+ let routeManifest = null;
381
346
  const hydrationScript = ({ nonce, }) => {
382
347
  const script = generateHydrationScript();
383
- return nonce ? script.replace('<script', `<script nonce="${nonce}"`) : script;
348
+ return nonce
349
+ ? script.replace('<script', `<script nonce="${nonce}"`)
350
+ : script;
384
351
  };
385
352
  const onStart = async () => {
386
353
  try {
@@ -398,58 +365,34 @@ const onStart = async () => {
398
365
  }
399
366
  };
400
367
  onStart();
368
+ const clientManifest = getManifest('client');
401
369
  const handler = eventHandler(async (event) => {
402
- const req = event.node.req;
403
- const res = event.node.res;
370
+ const req = toWebRequest(event);
404
371
  try {
405
372
  if (req.url?.includes('_server')) {
406
373
  return handleServerFunction(event);
407
374
  }
408
- const clientManifest = getManifest('client');
409
- if (!routeManifest || Object.keys(routeManifest).length === 0) {
375
+ if (!routeManifest) {
410
376
  routeManifest = await createRouteManifest();
411
377
  }
412
378
  const cspNonce = event.locals?.cspNonce;
413
- const url = req.url || '/';
414
- // extract route params and search params
415
- const params = {};
416
- const searchParams = {};
417
- const [pathnamePart, searchParamPart] = url.split('?');
418
- if (searchParamPart) {
419
- for (const param of searchParamPart.split('&')) {
420
- const [key, value] = param.split('=');
421
- searchParams[key] = decodeURIComponent(value || '');
422
- }
423
- }
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);
440
- }
379
+ const urlObj = new URL(req.url);
380
+ const pathnamePart = urlObj.pathname;
381
+ const searchParams = Object.fromEntries(urlObj.searchParams);
382
+ const match = matchRoute(routeManifest, pathnamePart);
383
+ const matched = match?.handler;
384
+ const params = match?.params || {};
441
385
  if (matched && matched.type === 'route') {
442
- const routeModule = await matched.handler.import();
386
+ const routeModule = await getCachedModule(matched.handler);
443
387
  const reqMethod = req.method?.toUpperCase();
444
388
  if (reqMethod) {
445
389
  const handler = routeModule[reqMethod];
446
390
  if (typeof handler === 'function') {
447
- const result = await handler(toWebRequest(event), {
391
+ const result = await handler(req, {
448
392
  params: params,
449
393
  searchParams: searchParams,
450
394
  });
451
- await sendNodeResponse(res, result);
452
- return;
395
+ return result;
453
396
  }
454
397
  throw new Error(`Method ${reqMethod} not implemented in ${matched.handler.src}`);
455
398
  }
@@ -461,245 +404,281 @@ const handler = eventHandler(async (event) => {
461
404
  charset: {
462
405
  type: 'meta',
463
406
  attributes: {
464
- charset: 'UTF-8'
465
- }
407
+ charset: 'UTF-8',
408
+ },
466
409
  },
467
410
  viewport: {
468
411
  type: 'meta',
469
412
  attributes: {
470
413
  name: 'viewport',
471
- content: 'width=device-width, initial-scale=1.0'
472
- }
414
+ content: 'width=device-width, initial-scale=1.0',
415
+ },
473
416
  },
474
417
  title: {
475
418
  type: 'title',
476
419
  attributes: {},
477
- content: 'SolidStep'
420
+ content: 'SolidStep',
478
421
  },
479
422
  build_time: {
480
423
  type: 'meta',
481
424
  attributes: {
482
425
  name: 'x-build-time',
483
426
  content: Date.now().toString(),
484
- description: 'IMPORTANT: This tag indicates the build time of the application and should not be removed.'
427
+ description: 'IMPORTANT: This tag indicates the build time of the application and should not be removed.',
485
428
  },
486
- }
429
+ },
487
430
  };
488
431
  const assets = await clientManifest.inputs[clientManifest.handler].assets();
489
432
  const manifestHtml = `<script ${cspNonce ? `nonce="${cspNonce}"` : ''}>window.manifest=${JSON.stringify(await clientManifest.json())}</script>`;
490
433
  let clientHydrationScript = undefined;
491
- res.setHeader('Content-Type', 'text/html');
492
- res.setHeader('Cache-Control', 'no-cache');
493
- try {
494
- if (!matched) {
434
+ setHeader('Content-Type', 'text/html');
435
+ setHeader('Cache-Control', 'no-cache');
436
+ const stream = new ReadableStream({
437
+ async start(controller) {
438
+ const encoder = new TextEncoder();
439
+ const push = (text) => controller.enqueue(encoder.encode(text));
495
440
  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);
441
+ if (!matched) {
442
+ try {
443
+ const match = matchRoute(routeManifest, '/');
444
+ const notFoundEntry = match.handler;
445
+ if (!notFoundEntry) {
446
+ throw new Error('No not-found page configured');
447
+ }
448
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
449
+ toRender: 'not-found',
450
+ entry: notFoundEntry,
451
+ routeParams: {},
452
+ searchParams: {},
453
+ req: req,
454
+ pageOptions: {},
455
+ cspNonce,
456
+ });
457
+ assets.push(...documentAssets);
458
+ clientHydrationScript = `
459
+ <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
460
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
461
+ main('/not-found/',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
462
+ </script>
463
+ `;
464
+ html = rendered;
465
+ meta = {
466
+ ...meta,
467
+ ...documentMeta,
468
+ };
469
+ setResponseStatus(404);
470
+ }
471
+ catch (e) {
472
+ console.error('404 module not found:', e);
473
+ setResponseStatus(404);
474
+ push('Not Found');
475
+ controller.close();
476
+ return;
477
+ }
478
+ }
479
+ else {
480
+ const { options } = matched
481
+ .mainPage.options
482
+ ? await getCachedModule(matched.mainPage
483
+ .options)
484
+ : { options: {} };
485
+ if (options?.responseHeaders) {
486
+ const headers = options.responseHeaders;
487
+ for (const [key, value] of Object.entries(headers)) {
488
+ setHeader(key, value);
489
+ }
490
+ }
491
+ try {
492
+ if (!matched.loadingPage) {
493
+ throw new Error('No loading page');
494
+ }
495
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
496
+ toRender: 'loading',
497
+ entry: matched,
498
+ routeParams: params,
499
+ searchParams,
500
+ req: req,
501
+ pageOptions: options,
502
+ cspNonce,
503
+ });
504
+ const assetsHtml = assets
505
+ .concat(documentAssets)
506
+ .map((asset) => {
507
+ const attributeString = Object.entries(asset.attrs)
508
+ .map(([key, value]) => `${key}="${value}"`)
509
+ .join(' ');
510
+ if (asset.tag === 'script') {
511
+ return `<script ${attributeString}></script>`;
512
+ }
513
+ if (asset.tag === 'link') {
514
+ return `<link ${attributeString}>`;
515
+ }
516
+ if (asset.tag === 'style') {
517
+ return `<style ${attributeString}>${asset.children || ''}</style>`;
518
+ }
519
+ })
520
+ .join('\n');
521
+ const html = `
522
+ <!doctype html>
523
+ <html lang="en">
524
+ <head>
525
+ ${generateHtmlHead({
526
+ ...meta,
527
+ ...documentMeta,
528
+ })}
529
+ ${assetsHtml}
530
+ ${hydrationScript({ nonce: cspNonce })}
531
+ </head>
532
+ <noscript>
533
+ Please enable JavaScript to view the content.<br/>
534
+ </noscript>
535
+ ${rendered}
536
+ </html>
537
+ `;
538
+ push(html);
539
+ push(`
540
+ <script type="module" data-hydration="loading" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
541
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
542
+ main('${matched.loadingPage?.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
543
+ </script>
544
+ `);
545
+ loading = true;
546
+ }
547
+ catch (e) {
548
+ // skip
549
+ }
550
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
551
+ toRender: 'main',
552
+ entry: matched,
553
+ routeParams: params,
554
+ searchParams,
555
+ req: req,
556
+ pageOptions: options,
557
+ cspNonce,
558
+ });
559
+ assets.push(...documentAssets);
560
+ clientHydrationScript = `
561
+ <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
562
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
563
+ main('${matched.mainPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
564
+ </script>
565
+ `;
566
+ html = rendered;
567
+ meta = {
568
+ ...meta,
569
+ ...documentMeta,
570
+ };
571
+ setResponseStatus(200);
507
572
  }
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
573
  }
526
- }
527
- else {
528
- try {
529
- if (!matched.loadingPage) {
530
- throw new Error('No loading page');
574
+ catch (e1) {
575
+ if (e1 instanceof RedirectError ||
576
+ e1.name === 'RedirectError') {
577
+ setHeader('Location', e1.message);
578
+ setResponseStatus(302);
579
+ controller.close();
580
+ return;
531
581
  }
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) => {
582
+ if (import.meta.env.DEV) {
583
+ console.error(e1);
584
+ }
585
+ try {
586
+ const errorPage = matched
587
+ .errorPage;
588
+ if (!errorPage) {
589
+ throw e1;
590
+ }
591
+ const { rendered, documentMeta, documentAssets, loaderData, } = await render({
592
+ toRender: 'error',
593
+ entry: matched,
594
+ routeParams: params,
595
+ searchParams,
596
+ req: req,
597
+ pageOptions: {},
598
+ cspNonce,
599
+ error: e1,
600
+ });
601
+ assets.push(...documentAssets);
602
+ clientHydrationScript = `
603
+ <script type="module" ${cspNonce ? `nonce="${cspNonce}"` : ''}>
604
+ import main from '${clientManifest.inputs[clientManifest.handler].output.path}';
605
+ main('${errorPage.manifestPath}',${JSON.stringify(params)},${JSON.stringify(searchParams)}, ${JSON.stringify(loaderData)});
606
+ </script>
607
+ `;
608
+ html = rendered;
609
+ meta = {
610
+ ...meta,
611
+ ...documentMeta,
612
+ };
613
+ // statusCode = 500;
614
+ setResponseStatus(500);
615
+ }
616
+ catch (e2) {
617
+ throw e1;
618
+ }
619
+ }
620
+ if (loading) {
621
+ const assetsHtml = assets
622
+ .map((asset) => {
541
623
  const attributeString = Object.entries(asset.attrs)
542
624
  .map(([key, value]) => `${key}="${value}"`)
543
625
  .join(' ');
544
- if (asset.tag === 'script') {
545
- return `<script ${attributeString}></script>`;
546
- }
547
626
  if (asset.tag === 'link') {
548
627
  return `<link ${attributeString}>`;
549
628
  }
550
629
  if (asset.tag === 'style') {
551
630
  return `<style ${attributeString}>${asset.children || ''}</style>`;
552
631
  }
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>
632
+ return '';
633
+ })
634
+ .join('\n');
635
+ push(`
636
+ <script ${cspNonce ? `nonce="${cspNonce}"` : ''}>
637
+ const head = document.querySelector('head');
638
+ const scripts = Array.from(head.querySelectorAll('script'));
639
+ head.innerHTML = \`${generateHtmlHead(meta) + assetsHtml}\`;
640
+ scripts.forEach(script => {
641
+ head.appendChild(script);
642
+ });
643
+ document.querySelector('script[data-hydration="loading"]')?.remove();
644
+ const loading = document.querySelector('body');
645
+ loading.innerHTML = \`${html}\`;
646
+ </script>
577
647
  `);
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>`;
648
+ push(manifestHtml);
649
+ push(clientHydrationScript);
650
+ controller.close();
651
+ return;
655
652
  }
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);
653
+ const assetsHtml = assets
654
+ .map((asset) => {
655
+ const attributeString = Object.entries(asset.attrs)
656
+ .map(([key, value]) => `${key}="${value}"`)
657
+ .join(' ');
658
+ if (asset.tag === 'script') {
659
+ return `<script ${attributeString} ${cspNonce ? `nonce="${cspNonce}"` : ''}></script>`;
660
+ }
661
+ if (asset.tag === 'link') {
662
+ return `<link ${attributeString}>`;
663
+ }
664
+ if (asset.tag === 'style') {
665
+ return `<style ${attributeString}>${asset.children || ''}</style>`;
666
+ }
667
+ })
668
+ .join('\n');
669
+ const transformHtml = template
670
+ .replace('<!--app-head-->', `${generateHtmlHead(meta)}\n${assetsHtml}\n${hydrationScript({ nonce: cspNonce })}`)
671
+ .replace('<!--app-body-->', (html ?? '') + manifestHtml + clientHydrationScript);
672
+ push(transformHtml);
673
+ controller.close();
674
+ return;
675
+ },
676
+ });
677
+ return stream;
692
678
  }
693
679
  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
680
  console.error(e);
701
- res.statusCode = 500;
702
- return res.end('Internal Server Error');
681
+ return new Response('Internal Server Error', { status: 500 });
703
682
  }
704
683
  });
705
684
  export default handler;