@gracile/engine 0.0.2
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/LICENSE +15 -0
- package/README.md +9 -0
- package/ambient.d.ts +1 -0
- package/dist/assertions.d.ts +12 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +27 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.d.ts.map +1 -0
- package/dist/build/build.js +7 -0
- package/dist/build/static.d.ts +8 -0
- package/dist/build/static.d.ts.map +1 -0
- package/dist/build/static.js +82 -0
- package/dist/dev/dev.d.ts +6 -0
- package/dist/dev/dev.d.ts.map +1 -0
- package/dist/dev/dev.js +14 -0
- package/dist/dev/request.d.ts +4 -0
- package/dist/dev/request.d.ts.map +1 -0
- package/dist/dev/request.js +151 -0
- package/dist/dev/server.d.ts +10 -0
- package/dist/dev/server.d.ts.map +1 -0
- package/dist/dev/server.js +54 -0
- package/dist/errors/templates.d.ts +3 -0
- package/dist/errors/templates.d.ts.map +1 -0
- package/dist/errors/templates.js +69 -0
- package/dist/preview.d.ts +6 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +9 -0
- package/dist/render/route-template.d.ts +15 -0
- package/dist/render/route-template.d.ts.map +1 -0
- package/dist/render/route-template.js +83 -0
- package/dist/routes/collect.d.ts +4 -0
- package/dist/routes/collect.d.ts.map +1 -0
- package/dist/routes/collect.js +111 -0
- package/dist/routes/comparator.d.ts +35 -0
- package/dist/routes/comparator.d.ts.map +1 -0
- package/dist/routes/comparator.js +132 -0
- package/dist/routes/load-module.d.ts +11 -0
- package/dist/routes/load-module.d.ts.map +1 -0
- package/dist/routes/load-module.js +23 -0
- package/dist/routes/match.d.ts +16 -0
- package/dist/routes/match.d.ts.map +1 -0
- package/dist/routes/match.js +63 -0
- package/dist/routes/route.d.ts +63 -0
- package/dist/routes/route.d.ts.map +1 -0
- package/dist/routes/route.js +32 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/user-config.d.ts +8 -0
- package/dist/user-config.d.ts.map +1 -0
- package/dist/user-config.js +10 -0
- package/dist/vite/build.d.ts +2 -0
- package/dist/vite/build.d.ts.map +1 -0
- package/dist/vite/build.js +39 -0
- package/dist/vite/config.d.ts +26 -0
- package/dist/vite/config.d.ts.map +1 -0
- package/dist/vite/config.js +64 -0
- package/dist/vite/plugins/html-static-pages.d.ts +13 -0
- package/dist/vite/plugins/html-static-pages.d.ts.map +1 -0
- package/dist/vite/plugins/html-static-pages.js +58 -0
- package/dist/vite/plugins/scss.d.ts +3 -0
- package/dist/vite/plugins/scss.d.ts.map +1 -0
- package/dist/vite/plugins/scss.js +28 -0
- package/dist/vite/server.d.ts +3 -0
- package/dist/vite/server.d.ts.map +1 -0
- package/dist/vite/server.js +18 -0
- package/dist/vite/utils.d.ts +3 -0
- package/dist/vite/utils.d.ts.map +1 -0
- package/dist/vite/utils.js +5 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Julian Cataldo — https://gracile.js.org
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Gracile — Engine
|
|
2
|
+
|
|
3
|
+
A thin, full-stack, **web** framework.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
- [Documentation website (gracile.netlify.app)](https://gracile.js.org/)
|
|
8
|
+
- [Documentation website repository](https://github.com/gracile-web/website)
|
|
9
|
+
- [Starter projects repository](https://github.com/gracile-web/starter-projects)
|
package/ambient.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ServerRenderedTemplate } from '@lit-labs/ssr';
|
|
2
|
+
import type { TemplateResult } from 'lit';
|
|
3
|
+
export type UnknownObject = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Used for user provided modules with unknown/possibly malformed shapes.
|
|
6
|
+
* Avoid this for well typed sources.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isUnknownObject(input: unknown): input is UnknownObject;
|
|
9
|
+
export declare function isLitTemplate(input: unknown): input is TemplateResult<1>;
|
|
10
|
+
export declare function isLitNormalTemplate(input: unknown): input is TemplateResult<1>;
|
|
11
|
+
export declare function isLitServerTemplate(input: unknown): input is ServerRenderedTemplate;
|
|
12
|
+
//# sourceMappingURL=assertions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAE1C,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAAC,CAAC,CAAC,CAWxE;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,OAAO,GACZ,KAAK,IAAI,cAAc,CAAC,CAAC,CAAC,CAE5B;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,OAAO,GACZ,KAAK,IAAI,sBAAsB,CAOjC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {} from '@lit-labs/ssr';
|
|
2
|
+
/**
|
|
3
|
+
* Used for user provided modules with unknown/possibly malformed shapes.
|
|
4
|
+
* Avoid this for well typed sources.
|
|
5
|
+
*/
|
|
6
|
+
export function isUnknownObject(input) {
|
|
7
|
+
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
8
|
+
}
|
|
9
|
+
export function isLitTemplate(input) {
|
|
10
|
+
return ((typeof input === 'object' &&
|
|
11
|
+
input &&
|
|
12
|
+
'_$litType$' in input &&
|
|
13
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
14
|
+
input._$litType$ === 1 &&
|
|
15
|
+
'strings' in input &&
|
|
16
|
+
Array.isArray(input.strings)) ||
|
|
17
|
+
false);
|
|
18
|
+
}
|
|
19
|
+
export function isLitNormalTemplate(input) {
|
|
20
|
+
return isLitTemplate(input) && '_$litServerRenderMode' in input === false;
|
|
21
|
+
}
|
|
22
|
+
export function isLitServerTemplate(input) {
|
|
23
|
+
return (isLitTemplate(input) &&
|
|
24
|
+
'_$litServerRenderMode' in input &&
|
|
25
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
26
|
+
input._$litServerRenderMode === 1);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/build/build.ts"],"names":[],"mappings":"AAKA,wBAAsB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,iBAIxC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
export interface RouteDefinition {
|
|
3
|
+
absoluteId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
html: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function renderRoutes(vite: ViteDevServer, root?: string): Promise<RouteDefinition[]>;
|
|
8
|
+
//# sourceMappingURL=static.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/build/static.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAM1C,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAeD,wBAAsB,YAAY,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,SAAgB,8BAwG3E"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import path, { join } from 'node:path';
|
|
3
|
+
import { logger } from '@gracile/internal-utils/logger';
|
|
4
|
+
import c from 'picocolors';
|
|
5
|
+
import { renderRouteTempalte } from '../render/route-template.js';
|
|
6
|
+
import { collectRoutes, routes } from '../routes/collect.js';
|
|
7
|
+
import { loadForeignRouteObject } from '../routes/load-module.js';
|
|
8
|
+
async function streamToString(stream) {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
11
|
+
for await (const chunk of stream) {
|
|
12
|
+
if (typeof chunk === 'string') {
|
|
13
|
+
chunks.push(Buffer.from(chunk));
|
|
14
|
+
}
|
|
15
|
+
else
|
|
16
|
+
throw new Error('Wrong buffer');
|
|
17
|
+
}
|
|
18
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
19
|
+
}
|
|
20
|
+
export async function renderRoutes(vite, root = process.cwd()) {
|
|
21
|
+
logger.info(c.green('Rendering routes…'), { timestamp: true });
|
|
22
|
+
// MARK: Collect
|
|
23
|
+
await collectRoutes(root /* vite */);
|
|
24
|
+
logger.info(c.green('Rendering routes finished'), { timestamp: true });
|
|
25
|
+
const renderedRoutes = [];
|
|
26
|
+
// MARK: Iterate modules
|
|
27
|
+
await Promise.all([...routes].map(async ([patternString, options]) => {
|
|
28
|
+
const routeModule = await loadForeignRouteObject(vite, options.filePath);
|
|
29
|
+
const routeStaticPaths = routeModule.staticPaths?.();
|
|
30
|
+
// MARK: Extract data
|
|
31
|
+
await Promise.all((routeStaticPaths ?? [patternString]).map(async (staticPathOptions) => {
|
|
32
|
+
let pathnameWithParams = patternString;
|
|
33
|
+
let params = {};
|
|
34
|
+
let props;
|
|
35
|
+
// MARK: Convert pattern
|
|
36
|
+
// to real route with static parameters + get props. for after
|
|
37
|
+
if (typeof staticPathOptions === 'object') {
|
|
38
|
+
params = staticPathOptions.params;
|
|
39
|
+
props = staticPathOptions.props;
|
|
40
|
+
Object.entries(staticPathOptions.params).forEach(([paramName, value]) => {
|
|
41
|
+
if (typeof value === 'string' || typeof value === 'undefined')
|
|
42
|
+
pathnameWithParams = pathnameWithParams
|
|
43
|
+
.replace(`:${paramName}*`, value || '')
|
|
44
|
+
.replace(`{:${paramName}}`, value || '');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// MARK: Prepare
|
|
48
|
+
// NOTE: Unused for now
|
|
49
|
+
const isErrorPage = pathnameWithParams.match(/\/__(.*)$/); /* Could add more (error, etc) */
|
|
50
|
+
const base = isErrorPage
|
|
51
|
+
? path.dirname(pathnameWithParams.slice(1))
|
|
52
|
+
: pathnameWithParams.slice(1);
|
|
53
|
+
const name = path.join(base, isErrorPage
|
|
54
|
+
? `${pathnameWithParams.split('/').at(-2)?.replace('__', '')}.html`
|
|
55
|
+
: 'index.html');
|
|
56
|
+
const url = new URL(pathnameWithParams, 'http://gracile-static');
|
|
57
|
+
// MARK: Render
|
|
58
|
+
const { output } = await renderRouteTempalte(
|
|
59
|
+
//
|
|
60
|
+
{ url: url.href }, vite, 'build', {
|
|
61
|
+
routeModule,
|
|
62
|
+
params,
|
|
63
|
+
foundRoute: options,
|
|
64
|
+
pathname: pathnameWithParams,
|
|
65
|
+
props,
|
|
66
|
+
});
|
|
67
|
+
const htmlString = await streamToString(output);
|
|
68
|
+
const existing = renderedRoutes.find((rendered) => rendered?.name === name);
|
|
69
|
+
if (existing)
|
|
70
|
+
throw new Error(`${c.red(`"${existing.name}" page was defined twice!`)}\n`);
|
|
71
|
+
renderedRoutes.push({
|
|
72
|
+
// NOTE:
|
|
73
|
+
// Vite's internal build-html plugin only expects *absolute* ids.
|
|
74
|
+
// See https://github.com/vitejs/vite/issues/13406#issuecomment-1801659561
|
|
75
|
+
absoluteId: join(root, name),
|
|
76
|
+
name,
|
|
77
|
+
html: htmlString,
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
80
|
+
}));
|
|
81
|
+
return renderedRoutes;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/dev/dev.ts"],"names":[],"mappings":"AAQA,wBAAsB,GAAG,CAAC,OAAO,EAAE;IAClC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B,iBAeA"}
|
package/dist/dev/dev.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { logger } from '@gracile/internal-utils/logger';
|
|
2
|
+
import c from 'picocolors';
|
|
3
|
+
import { getConfigs } from '../vite/config.js';
|
|
4
|
+
import { startServer } from './server.js';
|
|
5
|
+
const DEFAULT_DEV_SERVER_PORT = 9090;
|
|
6
|
+
export async function dev(options) {
|
|
7
|
+
logger.info(c.gray('\n— Development mode —\n'));
|
|
8
|
+
const { userConfigGracile } = await getConfigs(options.root ?? process.cwd(), 'dev');
|
|
9
|
+
const port = options.port ?? userConfigGracile?.port ?? DEFAULT_DEV_SERVER_PORT;
|
|
10
|
+
startServer({
|
|
11
|
+
...options,
|
|
12
|
+
port,
|
|
13
|
+
}).catch((e) => logger.error(String(e)));
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from 'express';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
3
|
+
export declare function createDevRequestHandler(vite: ViteDevServer): (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => Promise<void | ExpressResponse<any, Record<string, any>>>;
|
|
4
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/dev/request.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,YAAY,EACZ,OAAO,IAAI,cAAc,EACzB,QAAQ,IAAI,eAAe,EAC3B,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAc1C,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,SAEpD,cAAc,OACd,eAAe,QACd,YAAY,+DA+LnB"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { logger } from '@gracile/internal-utils/logger';
|
|
3
|
+
import { createServerAdapter } from '@whatwg-node/server';
|
|
4
|
+
import c from 'picocolors';
|
|
5
|
+
import { /* errorInline, */ errorPage } from '../errors/templates.js';
|
|
6
|
+
import { renderRouteTempalte, } from '../render/route-template.js';
|
|
7
|
+
import { getRoute } from '../routes/match.js';
|
|
8
|
+
import { renderSsrTemplate } from '../vite/utils.js';
|
|
9
|
+
// NOTE: Find a more canonical way to ponyfill the Node HTTP request to standard Request
|
|
10
|
+
// @ts-expect-error Abusing this feature!
|
|
11
|
+
const adapter = createServerAdapter((request) => request);
|
|
12
|
+
export function createDevRequestHandler(vite) {
|
|
13
|
+
return async (req, res, next) => {
|
|
14
|
+
const url = req.originalUrl;
|
|
15
|
+
logger.info(`[${c.yellow(req.method)}] ${c.yellow(url)}`, {
|
|
16
|
+
timestamp: true,
|
|
17
|
+
});
|
|
18
|
+
// MARK: Skip unwanted requests
|
|
19
|
+
if (
|
|
20
|
+
//
|
|
21
|
+
url.endsWith('favicon.ico') ||
|
|
22
|
+
url.endsWith('favicon.svg'))
|
|
23
|
+
return next();
|
|
24
|
+
const requestPonyfilled = (await Promise.resolve(adapter.handleNodeRequest(req)));
|
|
25
|
+
async function renderPageFn(handlerInfos, routeInfos) {
|
|
26
|
+
const { output } = await renderRouteTempalte(requestPonyfilled, vite, 'dev', routeInfos, handlerInfos);
|
|
27
|
+
return output;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// MARK: Get route infos
|
|
31
|
+
const moduleInfos = await getRoute({
|
|
32
|
+
url: requestPonyfilled.url,
|
|
33
|
+
vite,
|
|
34
|
+
});
|
|
35
|
+
let output;
|
|
36
|
+
// TODO: should move this to `special-file` so we don't recalculate on each request
|
|
37
|
+
// + we would be able to do some route codegen.
|
|
38
|
+
const response = {};
|
|
39
|
+
// MARK: Server handler
|
|
40
|
+
const handler = moduleInfos.routeModule.handler;
|
|
41
|
+
if ('handler' in moduleInfos.routeModule &&
|
|
42
|
+
typeof handler !== 'undefined') {
|
|
43
|
+
const options = {
|
|
44
|
+
request: requestPonyfilled,
|
|
45
|
+
url: new URL(requestPonyfilled.url),
|
|
46
|
+
response,
|
|
47
|
+
params: moduleInfos.params,
|
|
48
|
+
};
|
|
49
|
+
// MARK: Top level handler
|
|
50
|
+
if (typeof handler === 'function') {
|
|
51
|
+
const handlerOutput = (await Promise.resolve(handler(options)));
|
|
52
|
+
if (handlerOutput instanceof Response)
|
|
53
|
+
output = handlerOutput;
|
|
54
|
+
else
|
|
55
|
+
throw new Error('Catch-all handler must return a Response object.');
|
|
56
|
+
// MARK: Handler with method
|
|
57
|
+
}
|
|
58
|
+
else if (requestPonyfilled.method in handler) {
|
|
59
|
+
const handlerWithMethod = handler[requestPonyfilled.method];
|
|
60
|
+
if (typeof handlerWithMethod !== 'function')
|
|
61
|
+
throw Error('Handler must be a function.');
|
|
62
|
+
const handlerOutput = await Promise.resolve(handlerWithMethod(options));
|
|
63
|
+
if (handlerOutput instanceof Response)
|
|
64
|
+
output = handlerOutput;
|
|
65
|
+
else {
|
|
66
|
+
output = await renderPageFn({
|
|
67
|
+
data: handlerOutput,
|
|
68
|
+
method: requestPonyfilled.method,
|
|
69
|
+
}, moduleInfos);
|
|
70
|
+
}
|
|
71
|
+
// MARK: No GET, render page
|
|
72
|
+
}
|
|
73
|
+
else if (handler &&
|
|
74
|
+
'GET' in handler === false &&
|
|
75
|
+
requestPonyfilled.method === 'GET') {
|
|
76
|
+
output = await renderPageFn({ data: null, method: 'GET' }, moduleInfos);
|
|
77
|
+
}
|
|
78
|
+
// MARK: No handler, render page
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
output = await renderPageFn({ data: null, method: 'GET' }, moduleInfos);
|
|
82
|
+
}
|
|
83
|
+
// MARK: Return response
|
|
84
|
+
// NOTE: try directly with the requestPonyfill. This might not be necessary
|
|
85
|
+
if (output instanceof Response) {
|
|
86
|
+
if (output.status >= 300 && output.status <= 303) {
|
|
87
|
+
const location = output.headers.get('location');
|
|
88
|
+
if (location)
|
|
89
|
+
return res.redirect(location);
|
|
90
|
+
}
|
|
91
|
+
output.headers?.forEach((content, header) => res.set(header, content));
|
|
92
|
+
if (output.status)
|
|
93
|
+
res.statusCode = output.status;
|
|
94
|
+
if (output.statusText)
|
|
95
|
+
res.statusMessage = output.statusText;
|
|
96
|
+
// TODO: use this with page only?
|
|
97
|
+
// if (output.bodyUsed === false)
|
|
98
|
+
// throw new Error('Missing body.');
|
|
99
|
+
if (output.body)
|
|
100
|
+
output.body
|
|
101
|
+
.pipeTo(Writable.toWeb(res))
|
|
102
|
+
.catch((e) => logger.error(String(e)));
|
|
103
|
+
else
|
|
104
|
+
throw new Error('Missing body.');
|
|
105
|
+
// MARK: Stream page render
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
new Headers(response.headers)?.forEach((content, header) => res.set(header, content));
|
|
109
|
+
if (response.status)
|
|
110
|
+
res.statusCode = response.status;
|
|
111
|
+
if (response.statusText)
|
|
112
|
+
res.statusMessage = response.statusText;
|
|
113
|
+
res.set({ 'Content-Type': 'text/html' });
|
|
114
|
+
// MARK: Page stream error
|
|
115
|
+
output
|
|
116
|
+
?.on('error', (error) => {
|
|
117
|
+
const errorMessage = `There was an error while rendering a template chunk on server-side.\n` +
|
|
118
|
+
`It was omitted from the resulting HTML.`;
|
|
119
|
+
logger.error(errorMessage);
|
|
120
|
+
logger.error(error.message);
|
|
121
|
+
res.statusCode = 500;
|
|
122
|
+
res.statusMessage = errorMessage;
|
|
123
|
+
/* NOTE: Safety closing tags, maybe add more */
|
|
124
|
+
// Maybe just returning nothing is better to not break the page?
|
|
125
|
+
// Should send a overlay message anyway via WebSocket
|
|
126
|
+
// vite.ws.send()
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
vite.hot.send('gracile:ssr-error', {
|
|
129
|
+
message: errorMessage,
|
|
130
|
+
});
|
|
131
|
+
}, 500);
|
|
132
|
+
res.end('' /* errorInline(error) */);
|
|
133
|
+
})
|
|
134
|
+
.pipe(res);
|
|
135
|
+
}
|
|
136
|
+
// MARK: Errors
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
const error = e;
|
|
140
|
+
vite.ssrFixStacktrace(error);
|
|
141
|
+
if (error.cause === 404) {
|
|
142
|
+
return res.status(404).end('404');
|
|
143
|
+
// TODO: use a nice framework service page
|
|
144
|
+
// .redirect(new URL('/__404/', requestPonyfilled.url).href)
|
|
145
|
+
}
|
|
146
|
+
const errorTemplate = await renderSsrTemplate(errorPage(error));
|
|
147
|
+
res.status(500).end(await vite.transformIndexHtml(url, errorTemplate));
|
|
148
|
+
}
|
|
149
|
+
return next();
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
export declare function startServer(options: {
|
|
3
|
+
port?: number | undefined;
|
|
4
|
+
root?: string;
|
|
5
|
+
expose?: boolean | undefined;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
port: number;
|
|
8
|
+
instance: Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dev/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAyCxC,wBAAsB,WAAW,CAAC,OAAO,EAAE;IAC1C,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;;;GAqCA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { logger } from '@gracile/internal-utils/logger';
|
|
2
|
+
import { setCurrentWorkingDirectory } from '@gracile/internal-utils/paths';
|
|
3
|
+
import express, {} from 'express';
|
|
4
|
+
import c from 'picocolors';
|
|
5
|
+
import { collectRoutes, routes } from '../routes/collect.js';
|
|
6
|
+
import { createViteServer } from '../vite/server.js';
|
|
7
|
+
import { createDevRequestHandler } from './request.js';
|
|
8
|
+
async function createServer(_hmrPort, root = process.cwd()) {
|
|
9
|
+
logger.info(c.green('starting engine…'), {
|
|
10
|
+
timestamp: true,
|
|
11
|
+
});
|
|
12
|
+
setCurrentWorkingDirectory(root);
|
|
13
|
+
const app = express();
|
|
14
|
+
const vite = await createViteServer(root, 'dev');
|
|
15
|
+
app.use(vite.middlewares);
|
|
16
|
+
app.get('/__routes', (req, res) => {
|
|
17
|
+
return res.json([...routes]);
|
|
18
|
+
});
|
|
19
|
+
await collectRoutes(root /* vite */);
|
|
20
|
+
vite.watcher.on('all', (event, _file) => {
|
|
21
|
+
if (['add', 'unlink'].includes(event))
|
|
22
|
+
collectRoutes(root /* , vite */).catch((e) => logger.error(String(e)));
|
|
23
|
+
});
|
|
24
|
+
const handler = createDevRequestHandler(vite);
|
|
25
|
+
/* NOTE: Types are wrong! Should accept an async request handler. */
|
|
26
|
+
app.use('*', handler);
|
|
27
|
+
return { app, vite };
|
|
28
|
+
}
|
|
29
|
+
export async function startServer(options) {
|
|
30
|
+
const port = options.port ?? 9090;
|
|
31
|
+
const server = await createServer(port + 1, options.root);
|
|
32
|
+
// NOTE: `0` will auto-alocate a random available port.
|
|
33
|
+
let resultingPort = port;
|
|
34
|
+
let resultingHost;
|
|
35
|
+
const instance = await new Promise((resolve) => {
|
|
36
|
+
const inst = server.app.listen(port, options.expose ? '0.0.0.0' : '127.0.0.1', () => {
|
|
37
|
+
logger.info(c.green('development server started'), { timestamp: true });
|
|
38
|
+
resolve(inst);
|
|
39
|
+
const addressInfo = inst.address();
|
|
40
|
+
if (typeof addressInfo === 'object' && addressInfo) {
|
|
41
|
+
resultingPort = addressInfo.port;
|
|
42
|
+
// NOTE: this is not ideal. Should have the real bounded IP,
|
|
43
|
+
// like with Vite's `resolvedUrls` (unavailable in middleware mode)
|
|
44
|
+
resultingHost = addressInfo.address;
|
|
45
|
+
}
|
|
46
|
+
logger.info(`
|
|
47
|
+
${c.dim('┃')} Local ${c.cyan(`http://localhost:${resultingPort}/`)}
|
|
48
|
+
${c.dim('┃')} Network ${options.expose ? c.cyan(`http://${resultingHost}:${resultingPort}/`) : c.dim(`use ${c.bold('--host')} to expose`)}
|
|
49
|
+
`);
|
|
50
|
+
resolve(inst);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return { port: resultingPort, instance };
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/errors/templates.ts"],"names":[],"mappings":"AAEA,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,kDASvC;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,kDAyDrC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { html } from '@lit-labs/ssr';
|
|
2
|
+
export function errorInline(error) {
|
|
3
|
+
return html `<!-- --></a>
|
|
4
|
+
<div data-ssr-error>
|
|
5
|
+
<strong style="color: red">SSR Template error!</strong>
|
|
6
|
+
<details>
|
|
7
|
+
<summary style="user-select: none; cursor: pointer">Stack trace</summary>
|
|
8
|
+
<pre style="overflow: auto">${error.stack}</pre>
|
|
9
|
+
</details>
|
|
10
|
+
</div>`;
|
|
11
|
+
}
|
|
12
|
+
export function errorPage(error) {
|
|
13
|
+
return html `
|
|
14
|
+
<!-- -->
|
|
15
|
+
|
|
16
|
+
<!doctype html>
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="UTF-8" />
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
21
|
+
|
|
22
|
+
<title>Error</title>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<style>
|
|
26
|
+
html {
|
|
27
|
+
color-scheme: dark;
|
|
28
|
+
font-size: 16px;
|
|
29
|
+
line-height: 1.23rem;
|
|
30
|
+
font-family: system-ui;
|
|
31
|
+
}
|
|
32
|
+
body {
|
|
33
|
+
padding: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pre {
|
|
37
|
+
padding: 1rem;
|
|
38
|
+
|
|
39
|
+
overflow-y: auto;
|
|
40
|
+
}
|
|
41
|
+
button {
|
|
42
|
+
font-size: 2rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h1 {
|
|
46
|
+
color: tomato;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
|
|
50
|
+
<main>
|
|
51
|
+
<h1>😵 An error has occurred!</h1>
|
|
52
|
+
<button id="reload">Reload</button>
|
|
53
|
+
<!-- -->
|
|
54
|
+
<hr />
|
|
55
|
+
|
|
56
|
+
<pre>${error.stack}</pre>
|
|
57
|
+
<!-- <pre>$ {e.name}</pre> -->
|
|
58
|
+
<!-- <pre>$ {e.message}</pre> -->
|
|
59
|
+
<!-- <pre>$ {e.cause}</pre> -->
|
|
60
|
+
<hr />
|
|
61
|
+
</main>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
reload.addEventListener('click', () => document.location.reload());
|
|
65
|
+
</script>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAMA,wBAAsB,OAAO,CAAC,EAC7B,IAAI,EACJ,MAAM,EACN,IAAI,GACJ,EAAE;IACF,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,iBASA"}
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { logger } from '@gracile/internal-utils/logger';
|
|
2
|
+
import c from 'picocolors';
|
|
3
|
+
import { getConfigs } from './vite/config.js';
|
|
4
|
+
import { vitePreview } from './vite/server.js';
|
|
5
|
+
export async function preview({ port, expose, root, }) {
|
|
6
|
+
logger.info(c.gray('\n— Preview mode —\n'));
|
|
7
|
+
const { userConfigGracile } = await getConfigs(root ?? process.cwd(), 'build');
|
|
8
|
+
await vitePreview(port ?? userConfigGracile?.port ?? 9797, expose);
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
3
|
+
import type { RouteInfos } from '../routes/match.js';
|
|
4
|
+
import type { StaticRequest } from '../routes/route.js';
|
|
5
|
+
export declare const SSR_OUTLET_MARKER = "<route-template-outlet></route-template-outlet>";
|
|
6
|
+
export declare const PAGE_ASSETS_MARKER = "<!--__GRACILE_PAGE_ASSETS__-->";
|
|
7
|
+
export declare const pageAssets: import("@lit-labs/ssr").ServerRenderedTemplate;
|
|
8
|
+
export type HandlerInfos = {
|
|
9
|
+
data: unknown;
|
|
10
|
+
method: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function renderRouteTempalte(request: Request | StaticRequest, vite: ViteDevServer, mode: 'dev' | 'build', routeInfos: RouteInfos, handlerInfos?: HandlerInfos): Promise<{
|
|
13
|
+
output: Readable;
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=route-template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-template.d.ts","sourceRoot":"","sources":["../../src/render/route-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAuB,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAa7E,eAAO,MAAM,iBAAiB,oDACoB,CAAC;AAGnD,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,eAAO,MAAM,UAAU,gDAA6C,CAAC;AAErE,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,OAAO,GAAG,aAAa,EAChC,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,KAAK,GAAG,OAAO,EACrB,UAAU,EAAE,UAAU,EACtB,YAAY,CAAC,EAAE,YAAY;;GAiH3B"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { html } from '@gracile/internal-utils/dummy-literals';
|
|
3
|
+
import { html as LitSsrHtml, render as renderLitSsr } from '@lit-labs/ssr';
|
|
4
|
+
import { collectResult } from '@lit-labs/ssr/lib/render-result.js';
|
|
5
|
+
import { isLitServerTemplate, isLitTemplate } from '../assertions.js';
|
|
6
|
+
async function* concatStreams(...readables) {
|
|
7
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
8
|
+
for (const readable of readables) {
|
|
9
|
+
// eslint-disable-next-line no-restricted-syntax, no-await-in-loop
|
|
10
|
+
for await (const chunk of readable) {
|
|
11
|
+
yield chunk;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// export const SSR_OUTLET_MARKER = '________SSR_OUTLET________';
|
|
16
|
+
export const SSR_OUTLET_MARKER = '<route-template-outlet></route-template-outlet>';
|
|
17
|
+
// const SSR_OUTLET = unsafeHTML(SSR_OUTLET_MARKER);
|
|
18
|
+
export const PAGE_ASSETS_MARKER = '<!--__GRACILE_PAGE_ASSETS__-->';
|
|
19
|
+
// FIXME: cannot be used with `unsafeHTML`, so must be duplicated…
|
|
20
|
+
export const pageAssets = LitSsrHtml `<!--__GRACILE_PAGE_ASSETS__-->`;
|
|
21
|
+
export async function renderRouteTempalte(request, vite, mode, routeInfos, handlerInfos) {
|
|
22
|
+
// MARK: Context
|
|
23
|
+
const context = {
|
|
24
|
+
url: new URL(request.url),
|
|
25
|
+
params: routeInfos.params,
|
|
26
|
+
props: handlerInfos?.data
|
|
27
|
+
? {
|
|
28
|
+
[handlerInfos.method]: handlerInfos.data,
|
|
29
|
+
}
|
|
30
|
+
: routeInfos.props,
|
|
31
|
+
};
|
|
32
|
+
// MARK: Fragment
|
|
33
|
+
if (!routeInfos.routeModule.document) {
|
|
34
|
+
const fragmentOutput = await Promise.resolve(routeInfos.routeModule.template?.(context));
|
|
35
|
+
if (isLitTemplate(fragmentOutput) === false)
|
|
36
|
+
throw Error(`Wrong template result for fragment template ${routeInfos.foundRoute.filePath}.`);
|
|
37
|
+
const fragmentRender = renderLitSsr(fragmentOutput);
|
|
38
|
+
// NOTE: Should use RenderResultReadable instead?
|
|
39
|
+
const output = Readable.from(fragmentRender);
|
|
40
|
+
return { output };
|
|
41
|
+
}
|
|
42
|
+
// MARK: Document
|
|
43
|
+
if (!routeInfos.routeModule.document ||
|
|
44
|
+
typeof routeInfos.routeModule.document !== 'function')
|
|
45
|
+
throw new Error(`Route document must be a function ${routeInfos.foundRoute.filePath}.`);
|
|
46
|
+
const baseDocTemplateResult = await Promise.resolve(routeInfos.routeModule.document?.(context));
|
|
47
|
+
if (isLitServerTemplate(baseDocTemplateResult) === false)
|
|
48
|
+
throw new Error(`Incorrect document template result for ${routeInfos.foundRoute.filePath}.`);
|
|
49
|
+
const baseDocRendered = await collectResult(renderLitSsr(baseDocTemplateResult));
|
|
50
|
+
// MARK: Sibling assets
|
|
51
|
+
const baseDocRenderedWithAssets = baseDocRendered.replace(PAGE_ASSETS_MARKER, html `
|
|
52
|
+
<!-- PAGE ASSETS -->
|
|
53
|
+
${routeInfos.foundRoute.pageAssets.map((path) => {
|
|
54
|
+
//
|
|
55
|
+
if (/\.(js|ts)$/.test(path)) {
|
|
56
|
+
return html `<script type="module" src="/${path}"></script>`;
|
|
57
|
+
}
|
|
58
|
+
if (/\.(scss|css)$/.test(path)) {
|
|
59
|
+
return html `<link rel="stylesheet" href="/${path}" />`;
|
|
60
|
+
}
|
|
61
|
+
throw new Error('Unknown asset.');
|
|
62
|
+
})}
|
|
63
|
+
<!-- /PAGE ASSETS -->
|
|
64
|
+
`);
|
|
65
|
+
// MARK: Base document
|
|
66
|
+
const baseDocHtml = mode === 'dev'
|
|
67
|
+
? await vite.transformIndexHtml(routeInfos.pathname, baseDocRenderedWithAssets)
|
|
68
|
+
: baseDocRenderedWithAssets;
|
|
69
|
+
const index = baseDocHtml.indexOf(SSR_OUTLET_MARKER);
|
|
70
|
+
const baseDocRenderStreamPre = Readable.from(baseDocHtml.substring(0, index));
|
|
71
|
+
const baseDocRenderStreamPost = Readable.from(baseDocHtml.substring(index + SSR_OUTLET_MARKER.length + 1));
|
|
72
|
+
// MARK: Page
|
|
73
|
+
if (routeInfos.routeModule.template) {
|
|
74
|
+
const routeOutput = (await Promise.resolve(routeInfos.routeModule.template(context)));
|
|
75
|
+
if (isLitTemplate(routeOutput) === false)
|
|
76
|
+
throw Error(`Wrong template result for page template ${routeInfos.foundRoute.filePath}.`);
|
|
77
|
+
const renderStream = Readable.from(renderLitSsr(routeOutput));
|
|
78
|
+
const output = Readable.from(concatStreams(baseDocRenderStreamPre, renderStream, baseDocRenderStreamPost));
|
|
79
|
+
return { output };
|
|
80
|
+
}
|
|
81
|
+
const output = Readable.from(baseDocHtml);
|
|
82
|
+
return { output };
|
|
83
|
+
}
|