@gracile/engine 0.1.1 → 0.2.0-next.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.
Files changed (54) hide show
  1. package/ambient.d.ts +9 -4
  2. package/dist/assertions.test.d.ts +2 -0
  3. package/dist/assertions.test.d.ts.map +1 -0
  4. package/dist/assertions.test.js +22 -0
  5. package/dist/dev/dev.d.ts +6 -7
  6. package/dist/dev/dev.d.ts.map +1 -1
  7. package/dist/dev/dev.js +25 -18
  8. package/dist/plugin.d.ts +5 -0
  9. package/dist/plugin.d.ts.map +1 -0
  10. package/dist/plugin.js +175 -0
  11. package/dist/render/route-template.d.ts.map +1 -1
  12. package/dist/render/route-template.js +21 -8
  13. package/dist/routes/collect.d.ts.map +1 -1
  14. package/dist/routes/collect.js +39 -30
  15. package/dist/routes/match.js +1 -1
  16. package/dist/routes/route.d.ts +4 -5
  17. package/dist/routes/route.d.ts.map +1 -1
  18. package/dist/routes/route.js +1 -2
  19. package/dist/server/env.d.ts +2 -1
  20. package/dist/server/env.d.ts.map +1 -1
  21. package/dist/server/env.js +5 -3
  22. package/dist/server/request.d.ts +1 -1
  23. package/dist/server/request.d.ts.map +1 -1
  24. package/dist/server/request.js +139 -97
  25. package/dist/server/utils.d.ts +21 -3
  26. package/dist/server/utils.d.ts.map +1 -1
  27. package/dist/server/utils.js +61 -10
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/dist/vite/plugins/build-routes.d.ts.map +1 -1
  30. package/dist/vite/plugins/build-routes.js +1 -0
  31. package/dist/vite/plugins/virtual-routes.d.ts.map +1 -1
  32. package/dist/vite/plugins/virtual-routes.js +6 -1
  33. package/package.json +22 -26
  34. package/dist/build/build.d.ts +0 -2
  35. package/dist/build/build.d.ts.map +0 -1
  36. package/dist/build/build.js +0 -7
  37. package/dist/dev/server.d.ts +0 -30
  38. package/dist/dev/server.d.ts.map +0 -1
  39. package/dist/dev/server.js +0 -82
  40. package/dist/preview.d.ts +0 -6
  41. package/dist/preview.d.ts.map +0 -1
  42. package/dist/preview.js +0 -12
  43. package/dist/server/server.d.ts +0 -4
  44. package/dist/server/server.d.ts.map +0 -1
  45. package/dist/server/server.js +0 -25
  46. package/dist/vite/build.d.ts +0 -2
  47. package/dist/vite/build.d.ts.map +0 -1
  48. package/dist/vite/build.js +0 -114
  49. package/dist/vite/config.d.ts +0 -28
  50. package/dist/vite/config.d.ts.map +0 -1
  51. package/dist/vite/config.js +0 -74
  52. package/dist/vite/server.d.ts +0 -6
  53. package/dist/vite/server.d.ts.map +0 -1
  54. package/dist/vite/server.js +0 -20
@@ -2,73 +2,116 @@ import { Readable, Writable } from 'node:stream';
2
2
  import { logger } from '@gracile/internal-utils/logger';
3
3
  import { createServerAdapter } from '@whatwg-node/server';
4
4
  import c from 'picocolors';
5
- import { /* errorInline, */ errorPage } from '../errors/templates.js';
6
- import { renderRouteTemplate, } from '../render/route-template.js';
5
+ import { isUnknownObject } from '../assertions.js';
6
+ import { errorPage } from '../errors/templates.js';
7
+ import { renderRouteTemplate } from '../render/route-template.js';
7
8
  import { renderSsrTemplate } from '../render/utils.js';
8
9
  import { getRoute } from '../routes/match.js';
9
10
  // NOTE: Find a more canonical way to ponyfill the Node HTTP request to standard Request
10
11
  // @ts-expect-error Abusing this feature!
11
12
  const adapter = createServerAdapter((request) => request);
12
13
  export function createGracileMiddleware({ vite, routes, routeImports, routeAssets, root, serverMode, }) {
13
- const middleware = async (req, res, next) => {
14
- // Typing workaround
14
+ const middleware = async (req, res, next, locals) => {
15
+ // HACK: Typing workaround
15
16
  if (!req.url)
16
17
  throw Error('Incorrect url');
17
18
  if (!req.method)
18
19
  throw Error('Incorrect method');
19
- logger.info(`[${c.yellow(req.method)}] ${c.yellow(req.url)}`, {
20
- timestamp: true,
21
- });
22
- // MARK: Skip unwanted requests
23
- if (
24
- //
25
- req.url.endsWith('favicon.ico') ||
26
- req.url.endsWith('favicon.svg'))
27
- return next();
28
- const requestPonyfilled = (await Promise.resolve(adapter.handleNodeRequest(req)));
29
- async function renderPageFn(handlerInfos, routeInfos) {
30
- const { output } = await renderRouteTemplate({
31
- request: requestPonyfilled,
32
- vite,
33
- mode: 'dev',
34
- routeInfos,
35
- handlerInfos,
36
- routeAssets,
37
- root,
38
- serverMode,
39
- });
40
- return output || undefined;
20
+ const { url: urlPath, method } = req;
21
+ // if (urlPath === '/favicon.ico') return next();
22
+ async function createErrorPage(e) {
23
+ logger.error(e.message);
24
+ let errorPageHtml = await renderSsrTemplate(errorPage(e));
25
+ if (vite)
26
+ errorPageHtml = await vite.transformIndexHtml(urlPath, errorPageHtml);
27
+ return errorPageHtml;
41
28
  }
42
29
  try {
30
+ const requestPonyfilled = (await Promise.resolve(adapter.handleNodeRequest(
31
+ // HACK: Incorrect typings
32
+ req)));
33
+ // NOTE: Maybe it should be constructed from `req`
34
+ const fullUrl = requestPonyfilled.url;
43
35
  // MARK: Get route infos
44
- const moduleInfos = await getRoute({
45
- url: requestPonyfilled.url,
36
+ const routeOptions = {
37
+ url: fullUrl,
46
38
  vite,
47
39
  routes,
48
40
  routeImports,
41
+ };
42
+ const routeInfos = await getRoute(routeOptions).catch(async (error) => {
43
+ // MARK: User defined Gracile 404
44
+ logger.error(String(error));
45
+ const url = new URL('/404/', fullUrl).href;
46
+ const options = { ...routeOptions, url };
47
+ const notFound = await getRoute(options).catch(() => null);
48
+ return notFound;
49
+ });
50
+ if (routeInfos === null) {
51
+ // MARK: Default, fallback 404
52
+ const message = `404 not found!\n\n---\n\nCreate a /src/routes/404.{js,ts} to get a custom page.\n${method} - ${urlPath}`;
53
+ res.statusCode = 404;
54
+ res.statusMessage = '404 not found!';
55
+ const errorPage404 = await createErrorPage(new Error(message));
56
+ return res.end(errorPage404);
57
+ }
58
+ const routeTemplateOptions = {
59
+ request: requestPonyfilled,
60
+ vite,
61
+ mode: 'dev', // vite && vite.config.mode === 'dev' ? 'dev' : 'build',
62
+ routeAssets,
63
+ root,
64
+ serverMode,
65
+ routeInfos,
66
+ };
67
+ logger.info(`[${c.yellow(method)}] ${c.yellow(urlPath)}`, {
68
+ timestamp: true,
49
69
  });
50
70
  let output;
51
71
  // TODO: should move this to `special-file` so we don't recalculate on each request
52
72
  // + we would be able to do some route codegen.
53
73
  const response = {};
54
74
  // NOTE: Only for Express for now.
55
- let locals = null;
56
- if ('locals' in res)
57
- locals = moduleInfos.routeModule.locals?.(res.locals);
75
+ // console.log({ locals });
76
+ let providedLocals = {};
77
+ // if ('locals' in res && isUnknownObject(res.locals)) locals = res.locals;
78
+ if (locals && isUnknownObject(locals))
79
+ providedLocals = locals;
58
80
  // MARK: Server handler
59
- const handler = moduleInfos.routeModule.handler;
60
- if ('handler' in moduleInfos.routeModule &&
81
+ const handler = routeInfos.routeModule.handler;
82
+ if ('handler' in routeInfos.routeModule &&
61
83
  typeof handler !== 'undefined') {
62
- const options = {
84
+ const routeContext = Object.freeze({
63
85
  request: requestPonyfilled,
64
- url: new URL(requestPonyfilled.url),
86
+ url: new URL(fullUrl),
65
87
  response,
66
- params: moduleInfos.params,
67
- locals,
68
- };
88
+ params: routeInfos.params,
89
+ locals: providedLocals,
90
+ });
91
+ // MARK: Run user middleware
92
+ // NOTE: Experimental
93
+ // eslint-disable-next-line no-inner-declarations
94
+ // async function useHandler() {}
95
+ // if (vite) {
96
+ // const middleware = await vite
97
+ // .ssrLoadModule('/src/middleware.ts')
98
+ // .catch(() => null)
99
+ // .then((m) => m.default);
100
+ // if (middleware)
101
+ // await middleware(
102
+ // routeContext,
103
+ // async () => {
104
+ // await useHandler();
105
+ // },
106
+ // );
107
+ // else await useHandler();
108
+ // } else {
109
+ // await useHandler();
110
+ // }
111
+ //
69
112
  // MARK: Top level handler
70
113
  if (typeof handler === 'function') {
71
- const handlerOutput = (await Promise.resolve(handler(options)));
114
+ const handlerOutput = (await Promise.resolve(handler(routeContext)));
72
115
  if (handlerOutput instanceof Response)
73
116
  output = handlerOutput;
74
117
  else
@@ -79,26 +122,38 @@ export function createGracileMiddleware({ vite, routes, routeImports, routeAsset
79
122
  const handlerWithMethod = handler[requestPonyfilled.method];
80
123
  if (typeof handlerWithMethod !== 'function')
81
124
  throw TypeError('Handler must be a function.');
82
- const handlerOutput = await Promise.resolve(handlerWithMethod(options));
125
+ const handlerOutput = await Promise.resolve(handlerWithMethod(routeContext));
83
126
  if (handlerOutput instanceof Response)
84
127
  output = handlerOutput;
85
128
  else {
86
- output = await renderPageFn({
87
- data: handlerOutput,
88
- method: requestPonyfilled.method,
89
- }, moduleInfos);
129
+ output = await renderRouteTemplate({
130
+ ...routeTemplateOptions,
131
+ handlerInfos: { data: handlerOutput, method },
132
+ }).then((r) => r.output);
90
133
  }
91
134
  // MARK: No GET, render page
92
135
  }
93
136
  else if (handler &&
94
137
  'GET' in handler === false &&
95
138
  requestPonyfilled.method === 'GET') {
96
- output = await renderPageFn({ data: null, method: 'GET' }, moduleInfos);
139
+ output = await renderRouteTemplate({
140
+ ...routeTemplateOptions,
141
+ handlerInfos: { data: null, method: 'GET' },
142
+ routeInfos,
143
+ }).then((r) => r.output);
144
+ }
145
+ else {
146
+ const message = `This route doesn't handle the \`${method}\` method!`;
147
+ res.statusCode = 404;
148
+ res.statusMessage = message;
149
+ return res.end(await createErrorPage(new Error(message)));
97
150
  }
98
- // MARK: No handler, render page
99
151
  }
100
152
  else {
101
- output = await renderPageFn({ data: null, method: 'GET' }, moduleInfos);
153
+ output = await renderRouteTemplate({
154
+ ...routeTemplateOptions,
155
+ handlerInfos: { data: null, method: 'GET' },
156
+ }).then((r) => r.output);
102
157
  }
103
158
  // MARK: Return response
104
159
  // NOTE: try directly with the requestPonyfill. This might not be necessary
@@ -120,69 +175,56 @@ export function createGracileMiddleware({ vite, routes, routeImports, routeAsset
120
175
  // TODO: use this with page only?
121
176
  // if (output.bodyUsed === false)
122
177
  // throw new Error('Missing body.');
123
- if (output.body)
124
- output.body
178
+ if (output.body) {
179
+ const piped = await output.body
125
180
  .pipeTo(Writable.toWeb(res))
126
181
  .catch((e) => logger.error(String(e)));
182
+ return piped;
183
+ }
127
184
  // else throw new Error('Missing body.');
128
- else
129
- return res.end(output);
185
+ // NOTE: Other shapes
186
+ return res.end(output);
130
187
  // MARK: Stream page render
131
188
  }
132
- else {
133
- new Headers(response.headers)?.forEach((content, header) => res.setHeader(header, content));
134
- if (response.status)
135
- res.statusCode = response.status;
136
- if (response.statusText)
137
- res.statusMessage = response.statusText;
138
- res.setHeader('Content-Type', 'text/html');
139
- // MARK: Page stream error
140
- output
141
- ?.on('error', (error) => {
142
- const errorMessage = `There was an error while rendering a template chunk on server-side.\n` +
143
- `It was omitted from the resulting HTML.`;
144
- logger.error(errorMessage);
145
- logger.error(error.message);
146
- res.statusCode = 500;
147
- res.statusMessage = errorMessage;
148
- /* NOTE: Safety closing tags, maybe add more */
149
- // Maybe just returning nothing is better to not break the page?
150
- // Should send a overlay message anyway via WebSocket
151
- // vite.ws.send()
152
- if (vite)
153
- setTimeout(() => {
154
- vite.hot.send('gracile:ssr-error', {
155
- message: errorMessage,
156
- });
157
- }, 500);
158
- res.end('' /* errorInline(error) */);
159
- })
160
- .pipe(res);
161
- }
189
+ new Headers(response.headers)?.forEach((content, header) => res.setHeader(header, content));
190
+ if (response.status)
191
+ res.statusCode = response.status;
192
+ if (response.statusText)
193
+ res.statusMessage = response.statusText;
194
+ res.setHeader('Content-Type', 'text/html');
195
+ // MARK: Page stream error
196
+ return output
197
+ ?.on('error', (error) => {
198
+ const errorMessage = `There was an error while rendering a template chunk on server-side.\n` +
199
+ `It was omitted from the resulting HTML.`;
200
+ logger.error(errorMessage);
201
+ logger.error(error.message);
202
+ res.statusCode = 500;
203
+ res.statusMessage = errorMessage;
204
+ /* NOTE: Safety closing tags, maybe add more */
205
+ // Maybe just returning nothing is better to not break the page?
206
+ // Should send a overlay message anyway via WebSocket
207
+ // vite.ws.send()
208
+ if (vite)
209
+ setTimeout(() => {
210
+ vite.hot.send('gracile:ssr-error', {
211
+ message: errorMessage,
212
+ });
213
+ }, 500);
214
+ return res.end('' /* errorInline(error) */);
215
+ })
216
+ .pipe(res);
162
217
  // MARK: Errors
163
218
  }
164
219
  catch (e) {
165
220
  const error = e;
166
221
  if (vite)
167
222
  vite.ssrFixStacktrace(error);
168
- // else
169
- logger.error(error.message);
170
- if (error.cause === 404) {
171
- // TODO: Handle 404 with dedicated page
172
- if (!vite)
173
- return next();
174
- res.statusCode = 404;
175
- return res.end('404');
176
- // TODO: use a nice framework service page
177
- // .redirect(new URL('/__404/', requestPonyfilled.url).href)
178
- }
179
- let errorTemplate = await renderSsrTemplate(errorPage(error));
180
- if (vite)
181
- errorTemplate = await vite.transformIndexHtml(req.url, errorTemplate);
223
+ const ultimateErrorPage = await createErrorPage(error);
182
224
  res.statusCode = 500;
183
- return res.end(errorTemplate);
225
+ res.statusMessage = 'Gracile middleware error';
226
+ return res.end(ultimateErrorPage);
184
227
  }
185
- return next();
186
228
  };
187
229
  return middleware;
188
230
  }
@@ -1,4 +1,22 @@
1
- import type { Server } from 'http';
2
- import type { AddressInfo } from 'net';
3
- export declare function printNodeHttpServerAddressInfos(instance: Server): AddressInfo;
1
+ import type { IncomingMessage, Server, ServerResponse } from 'node:http';
2
+ export declare function printAddressInfos(options: {
3
+ instance?: Server;
4
+ address: string;
5
+ }): void;
6
+ export declare function notFoundHandler(req: IncomingMessage, res: ServerResponse): void;
7
+ export type LocalMiddlewareContext = {
8
+ request: Request;
9
+ response: ResponseInit;
10
+ };
11
+ export type BasicAuthUser = {
12
+ userName: string | null;
13
+ };
14
+ /**
15
+ * @param context
16
+ * @returns
17
+ */
18
+ export declare const authenticateBasic: (context: LocalMiddlewareContext, validate: (user: {
19
+ name: string;
20
+ pass: string;
21
+ }) => boolean) => BasicAuthUser;
4
22
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/server/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAKvC,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAsB7E"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/server/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AASzE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB,QA2BA;AASD,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,QAaxE;AAED,MAAM,MAAM,sBAAsB,GAAG;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,YACpB,sBAAsB,YACrB,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,KACzD,aAwBF,CAAC"}
@@ -1,21 +1,72 @@
1
1
  // NOTE: Util. to pretty print for user provided server.
2
+ // import type { AddressInfo } from 'node:net';
2
3
  import { logger } from '@gracile/internal-utils/logger';
3
4
  import { DEV } from 'esm-env';
4
5
  import c from 'picocolors';
5
6
  import { IP_EXPOSED } from './env.js';
6
- export function printNodeHttpServerAddressInfos(instance) {
7
- const infos = instance.address();
7
+ export function printAddressInfos(options) {
8
+ let address = null;
9
+ if (options.instance) {
10
+ const infos = options.instance.address();
11
+ if (typeof infos === 'object' && infos && infos.port && infos.address) {
12
+ address = `http://${infos.address}:${infos.port}/`;
13
+ }
14
+ }
15
+ else if (options.address) {
16
+ address = options.address;
17
+ }
18
+ else
19
+ throw new Error('Incorrect options');
8
20
  logger.info(c.green(`${DEV ? 'development' : 'production'} server started`), {
9
21
  timestamp: true,
10
22
  });
11
- if (typeof infos === 'object' && infos && infos.port && infos.address) {
12
- logger.info(`
13
- ${c.dim('┃')} Local ${c.cyan(`http://localhost:${infos.port}/`)}` +
14
- `${infos.address === IP_EXPOSED
15
- ? `${c.dim('')} Network ${c.cyan(`http://${infos.address}:${infos.port}/`)}`
16
- : ''}
23
+ logger.info(`
24
+ ${c.dim('┃')} Local ${c.cyan(address)}` +
25
+ `${address?.includes(IP_EXPOSED)
26
+ ? `${c.dim('┃')} Network ${c.cyan(address)}`
27
+ : ''}
17
28
  `);
18
- return infos;
29
+ }
30
+ function sendHtml(res, payload) {
31
+ res.setHeader('content/type', 'text/html');
32
+ res.end(payload);
33
+ }
34
+ function fallback404(res) {
35
+ return sendHtml(res, '404 — Not found!');
36
+ }
37
+ export function notFoundHandler(req, res) {
38
+ const host = req.headers.host;
39
+ if (!host) {
40
+ fallback404(res);
41
+ return;
19
42
  }
20
- throw Error('Invalid address/port.');
43
+ const url = new URL('/404/', `http://${host}`);
44
+ fetch(url)
45
+ .then((t) => t.text())
46
+ .then((r) => sendHtml(res, r))
47
+ .catch(() => fallback404(res));
21
48
  }
49
+ /**
50
+ * @param context
51
+ * @returns
52
+ */
53
+ export const authenticateBasic = (context, validate) => {
54
+ const b64 = context.request.headers.get('authorization')?.split(' ')?.[1];
55
+ if (b64) {
56
+ const [name, pass] = Buffer.from(b64, 'base64').toString().split(':');
57
+ if (name && pass) {
58
+ validate({ name, pass });
59
+ return { userName: name };
60
+ }
61
+ }
62
+ /**
63
+ * Authentication failed
64
+ */
65
+ context.response.headers = new Headers({
66
+ ...context.response.headers,
67
+ 'WWW-Authenticate': 'Basic',
68
+ });
69
+ context.response.status = 401;
70
+ context.response.statusText = 'You are not authenticated!';
71
+ return { userName: null };
72
+ };