@gracile/engine
Advanced tools
Comparing version 0.2.0-next.1 to 0.2.0-next.2
@@ -10,4 +10,4 @@ /// <reference types="vite/client" /> | ||
declare namespace App { | ||
declare namespace Gracile { | ||
interface Locals {} | ||
} |
@@ -7,17 +7,24 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ | ||
import { isLitNormalTemplate, isLitServerTemplate, isLitTemplate, isUnknownObject, } from './assertions.js'; | ||
describe('should assert lit templates', () => { | ||
test('assert lit', () => { | ||
const lit = html ` <div>Hello</div> `; | ||
const litServer = serverHtml ` <div>Hello</div> `; | ||
describe('should assert lit templates, unknown objects', () => { | ||
const lit = html ` <div>Hello</div> `; | ||
const litServer = serverHtml ` <div>Hello</div> `; | ||
test('assert lit any', () => { | ||
assert.equal(isLitTemplate(lit), true); | ||
assert.equal(isLitTemplate(litServer), true); | ||
}); | ||
test('assert lit normal', () => { | ||
assert.equal(isLitNormalTemplate(lit), true); | ||
}); | ||
test('assert lit server', () => { | ||
assert.equal(isLitServerTemplate(litServer), true); | ||
assert.equal(isUnknownObject({ something: 'something' }), true); | ||
// | ||
}); | ||
test('wrong lit templates', () => { | ||
assert.equal(isLitTemplate([]), false); | ||
assert.equal(isLitTemplate([]), false); | ||
assert.equal(isLitNormalTemplate([]), false); | ||
assert.equal(isLitServerTemplate([]), false); | ||
}); | ||
test('unknown object', () => { | ||
assert.equal(isUnknownObject({ something: 'something' }), true); | ||
}); | ||
// | ||
}); |
import { type ViteDevServer } from 'vite'; | ||
import { type ConnectLikeAsyncMiddleware } from '../server/request.js'; | ||
export declare function createHandlers({ vite, }: { | ||
import { type GracileAsyncMiddleware } from '../server/request.js'; | ||
export declare function createDevHandler({ vite, }: { | ||
vite: ViteDevServer; | ||
}): Promise<{ | ||
handlers: ConnectLikeAsyncMiddleware; | ||
handler: GracileAsyncMiddleware; | ||
}>; | ||
//# sourceMappingURL=dev.d.ts.map |
@@ -6,5 +6,5 @@ import { logger } from '@gracile/internal-utils/logger'; | ||
import { createGracileMiddleware, } from '../server/request.js'; | ||
export async function createHandlers({ vite, }) { | ||
export async function createDevHandler({ vite, }) { | ||
const root = vite.config.root; | ||
logger.info(c.green('creating handler…'), { timestamp: true }); | ||
logger.info(c.dim('\nCreating handler…'), { timestamp: true }); | ||
const routes = await collectRoutes(root /* vite */); | ||
@@ -28,3 +28,3 @@ vite.watcher.on('all', (event, file) => { | ||
const gracile = createGracileMiddleware({ vite, root, serverMode, routes }); | ||
return { handlers: gracile }; | ||
return { handler: gracile }; | ||
} |
@@ -1,5 +0,3 @@ | ||
import { type PluginOption } from 'vite'; | ||
export declare const gracile: (config?: { | ||
mode: "server" | "static"; | ||
}) => PluginOption; | ||
import type { GracileConfig } from './user-config.js'; | ||
export declare const gracile: (config?: GracileConfig) => any[]; | ||
//# sourceMappingURL=plugin.d.ts.map |
@@ -0,10 +1,17 @@ | ||
import { readFile } from 'node:fs/promises'; | ||
import { join } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
import { logger } from '@gracile/internal-utils/logger'; | ||
import { rename, rm } from 'fs/promises'; | ||
import c from 'picocolors'; | ||
import { build, createServer } from 'vite'; | ||
import {} from './build/static.js'; | ||
import { createHandlers } from './dev/dev.js'; | ||
import { createDevHandler } from './dev/dev.js'; | ||
import { nodeAdapter } from './server/node.js'; | ||
import { buildRoutes } from './vite/plugins/build-routes.js'; | ||
import { virtualRoutes } from './vite/plugins/virtual-routes.js'; | ||
// Return as `any` to avoid Plugin type mismatches when there are multiple Vite versions installed | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export const gracile = (config) => { | ||
const mode = config?.mode || 'static'; | ||
const outputMode = config?.output || 'static'; | ||
const clientAssets = {}; | ||
@@ -25,7 +32,13 @@ let routes = null; | ||
async configureServer(server) { | ||
// ({ plmugi: server.config.root }); | ||
const { handlers } = await createHandlers({ vite: server }); | ||
// Infos | ||
const mainPjson = fileURLToPath(new URL('../../gracile/package.json', import.meta.url)); | ||
const { version } = JSON.parse(await readFile(mainPjson, 'utf-8')); | ||
logger.info(`${c.cyan(c.italic(c.underline('🧚 Gracile ')))}` + | ||
` ${c.green(` v${version}`)}`); | ||
// --- | ||
const { handler } = await createDevHandler({ vite: server }); | ||
return () => { | ||
server.middlewares.use((req, res, next) => { | ||
Promise.resolve(handlers(req, res, next)).catch((error) => next(error)); | ||
const locals = config?.dev?.locals?.({ nodeRequest: req }); | ||
Promise.resolve(nodeAdapter(handler)(req, res, locals)).catch((error) => next(error)); | ||
}); | ||
@@ -47,3 +60,3 @@ }; | ||
_config: {}, | ||
serverMode: mode === 'server', | ||
serverMode: outputMode === 'server', | ||
}); | ||
@@ -60,3 +73,3 @@ routes = htmlPages.routes; | ||
}, | ||
outDir: mode === 'server' ? 'dist/client' : 'dist', | ||
outDir: outputMode === 'server' ? 'dist/client' : 'dist', | ||
}, | ||
@@ -69,3 +82,3 @@ }; | ||
writeBundle(_, bundle) { | ||
if (mode === 'static') | ||
if (outputMode === 'static') | ||
return; | ||
@@ -85,3 +98,3 @@ Object.entries(bundle).forEach(([, file]) => { | ||
async closeBundle() { | ||
if (mode === 'static' || !routes || !renderedRoutes) | ||
if (outputMode === 'static' || !routes || !renderedRoutes) | ||
return; | ||
@@ -88,0 +101,0 @@ await build({ |
@@ -43,3 +43,3 @@ import type { ServerRenderedTemplate } from '@lit-labs/ssr'; | ||
request: Request; | ||
locals: App.Locals; | ||
locals: Gracile.Locals; | ||
/** | ||
@@ -46,0 +46,0 @@ * Let you mutate the downstream **page** response. |
@@ -1,5 +0,4 @@ | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
import { Readable } from 'node:stream'; | ||
import type { ViteDevServer } from 'vite'; | ||
import type * as R from '../routes/route.js'; | ||
type NextFunction = (error?: unknown) => void | Promise<void>; | ||
/** | ||
@@ -11,3 +10,3 @@ * This is fully compatible with Express or bare Node HTTP | ||
*/ | ||
export type ConnectLikeAsyncMiddleware = (req: IncomingMessage, res: ServerResponse, next: NextFunction, locals?: unknown) => Promise<void | NextFunction | ServerResponse> | (void | NextFunction | ServerResponse); | ||
export type GracileAsyncMiddleware = (request: Request, locals?: unknown) => Promise<Response | Readable | null>; | ||
export declare function createGracileMiddleware({ vite, routes, routeImports, routeAssets, root, serverMode, }: { | ||
@@ -20,4 +19,3 @@ vite?: ViteDevServer | undefined; | ||
serverMode?: boolean | undefined; | ||
}): ConnectLikeAsyncMiddleware; | ||
export {}; | ||
}): GracileAsyncMiddleware; | ||
//# sourceMappingURL=request.d.ts.map |
@@ -1,4 +0,3 @@ | ||
import { Readable, Writable } from 'node:stream'; | ||
import { Readable } from 'node:stream'; | ||
import { logger } from '@gracile/internal-utils/logger'; | ||
import { createServerAdapter } from '@whatwg-node/server'; | ||
import c from 'picocolors'; | ||
@@ -10,27 +9,21 @@ import { isUnknownObject } from '../assertions.js'; | ||
import { getRoute } from '../routes/match.js'; | ||
// NOTE: Find a more canonical way to ponyfill the Node HTTP request to standard Request | ||
// @ts-expect-error Abusing this feature! | ||
const adapter = createServerAdapter((request) => request); | ||
export function createGracileMiddleware({ vite, routes, routeImports, routeAssets, root, serverMode, }) { | ||
const middleware = async (req, res, next, locals) => { | ||
async function createErrorPage(urlPath, e) { | ||
logger.error(e.message); | ||
let errorPageHtml = await renderSsrTemplate(errorPage(e)); | ||
if (vite) | ||
errorPageHtml = await vite.transformIndexHtml(urlPath, errorPageHtml); | ||
return errorPageHtml; | ||
} | ||
const middleware = async (request, locals) => { | ||
// HACK: Typing workaround | ||
if (!req.url) | ||
if (!request.url) | ||
throw Error('Incorrect url'); | ||
if (!req.method) | ||
if (!request.method) | ||
throw Error('Incorrect method'); | ||
const { url: urlPath, method } = req; | ||
const { url: urlPath, method } = request; | ||
// if (urlPath === '/favicon.ico') return next(); | ||
async function createErrorPage(e) { | ||
logger.error(e.message); | ||
let errorPageHtml = await renderSsrTemplate(errorPage(e)); | ||
if (vite) | ||
errorPageHtml = await vite.transformIndexHtml(urlPath, errorPageHtml); | ||
return errorPageHtml; | ||
} | ||
try { | ||
const requestPonyfilled = (await Promise.resolve(adapter.handleNodeRequest( | ||
// HACK: Incorrect typings | ||
req))); | ||
// NOTE: Maybe it should be constructed from `req` | ||
const fullUrl = requestPonyfilled.url; | ||
const fullUrl = request.url; | ||
// MARK: Get route infos | ||
@@ -54,9 +47,10 @@ const routeOptions = { | ||
const message = `404 not found!\n\n---\n\nCreate a /src/routes/404.{js,ts} to get a custom page.\n${method} - ${urlPath}`; | ||
res.statusCode = 404; | ||
res.statusMessage = '404 not found!'; | ||
const errorPage404 = await createErrorPage(new Error(message)); | ||
return res.end(errorPage404); | ||
const errorPage404 = await createErrorPage(urlPath, new Error(message)); | ||
return new Response(errorPage404, { | ||
status: 404, | ||
statusText: '404 not found!', | ||
}); | ||
} | ||
const routeTemplateOptions = { | ||
request: requestPonyfilled, | ||
request, | ||
vite, | ||
@@ -73,9 +67,3 @@ mode: 'dev', // vite && vite.config.mode === 'dev' ? 'dev' : 'build', | ||
let output; | ||
// TODO: should move this to `special-file` so we don't recalculate on each request | ||
// + we would be able to do some route codegen. | ||
const response = {}; | ||
// NOTE: Only for Express for now. | ||
// console.log({ locals }); | ||
let providedLocals = {}; | ||
// if ('locals' in res && isUnknownObject(res.locals)) locals = res.locals; | ||
if (locals && isUnknownObject(locals)) | ||
@@ -85,8 +73,12 @@ providedLocals = locals; | ||
const handler = routeInfos.routeModule.handler; | ||
if ('handler' in routeInfos.routeModule && | ||
typeof handler !== 'undefined') { | ||
const responseInit = {}; | ||
// TODO: should move this to `special-file` so we don't recalculate on each request | ||
// + we would be able to do some route codegen. | ||
if (('handler' in routeInfos.routeModule && | ||
typeof handler !== 'undefined') || | ||
(handler && 'GET' in handler === false && method !== 'GET')) { | ||
const routeContext = Object.freeze({ | ||
request: requestPonyfilled, | ||
request, | ||
url: new URL(fullUrl), | ||
response, | ||
response: responseInit, | ||
params: routeInfos.params, | ||
@@ -125,4 +117,4 @@ locals: providedLocals, | ||
} | ||
else if (requestPonyfilled.method in handler) { | ||
const handlerWithMethod = handler[requestPonyfilled.method]; | ||
else if (method in handler) { | ||
const handlerWithMethod = handler[method]; | ||
if (typeof handlerWithMethod !== 'function') | ||
@@ -137,2 +129,3 @@ throw TypeError('Handler must be a function.'); | ||
handlerInfos: { data: handlerOutput, method }, | ||
routeInfos, | ||
}).then((r) => r.output); | ||
@@ -142,16 +135,5 @@ } | ||
} | ||
else if (handler && | ||
'GET' in handler === false && | ||
requestPonyfilled.method === 'GET') { | ||
output = await renderRouteTemplate({ | ||
...routeTemplateOptions, | ||
handlerInfos: { data: null, method: 'GET' }, | ||
routeInfos, | ||
}).then((r) => r.output); | ||
} | ||
else { | ||
const message = `This route doesn't handle the \`${method}\` method!`; | ||
res.statusCode = 404; | ||
res.statusMessage = message; | ||
return res.end(await createErrorPage(new Error(message))); | ||
const statusText = `This route doesn't handle the \`${method}\` method!`; | ||
return new Response(statusText, { status: 405, statusText }); | ||
} | ||
@@ -163,2 +145,3 @@ } | ||
handlerInfos: { data: null, method: 'GET' }, | ||
routeInfos, | ||
}).then((r) => r.output); | ||
@@ -171,56 +154,24 @@ } | ||
const location = output.headers.get('location'); | ||
// if (location) return res.redirect(location); | ||
if (location) { | ||
res.statusCode = output.status; | ||
res.setHeader('location', location); | ||
return res.end(`Found. Redirecting to ${location}`); | ||
return Response.redirect(location, output.status); | ||
} | ||
} | ||
output.headers?.forEach((content, header) => res.setHeader(header, content)); | ||
if (output.status) | ||
res.statusCode = output.status; | ||
if (output.statusText) | ||
res.statusMessage = output.statusText; | ||
// TODO: use this with page only? | ||
// if (output.bodyUsed === false) | ||
// throw new Error('Missing body.'); | ||
if (output.body) { | ||
const piped = await output.body | ||
.pipeTo(Writable.toWeb(res)) | ||
.catch((e) => logger.error(String(e))); | ||
return piped; | ||
} | ||
// else throw new Error('Missing body.'); | ||
// NOTE: Other shapes | ||
return res.end(output); | ||
return output; | ||
// MARK: Stream page render | ||
} | ||
new Headers(response.headers)?.forEach((content, header) => res.setHeader(header, content)); | ||
if (response.status) | ||
res.statusCode = response.status; | ||
if (response.statusText) | ||
res.statusMessage = response.statusText; | ||
res.setHeader('Content-Type', 'text/html'); | ||
// MARK: Page stream error | ||
return output | ||
?.on('error', (error) => { | ||
const errorMessage = `There was an error while rendering a template chunk on server-side.\n` + | ||
`It was omitted from the resulting HTML.`; | ||
logger.error(errorMessage); | ||
logger.error(error.message); | ||
res.statusCode = 500; | ||
res.statusMessage = errorMessage; | ||
/* NOTE: Safety closing tags, maybe add more */ | ||
// Maybe just returning nothing is better to not break the page? | ||
// Should send a overlay message anyway via WebSocket | ||
// vite.ws.send() | ||
if (vite) | ||
setTimeout(() => { | ||
vite.hot.send('gracile:ssr-error', { | ||
message: errorMessage, | ||
}); | ||
}, 500); | ||
return res.end('' /* errorInline(error) */); | ||
}) | ||
.pipe(res); | ||
return !output | ||
? null | ||
: output.on('error', (error) => { | ||
const errorMessage = `There was an error while rendering a template chunk on server-side.\n` + | ||
`It was omitted from the resulting HTML.`; | ||
logger.error(errorMessage); | ||
logger.error(error.message); | ||
if (vite) | ||
setTimeout(() => { | ||
vite.hot.send('gracile:ssr-error', { | ||
message: errorMessage, | ||
}); | ||
}, 500); | ||
}); | ||
// MARK: Errors | ||
@@ -232,6 +183,7 @@ } | ||
vite.ssrFixStacktrace(error); | ||
const ultimateErrorPage = await createErrorPage(error); | ||
res.statusCode = 500; | ||
res.statusMessage = 'Gracile middleware error'; | ||
return res.end(ultimateErrorPage); | ||
const ultimateErrorPage = await createErrorPage('__gracile_error', error); | ||
return new Response(String(ultimateErrorPage), { | ||
status: 500, | ||
statusText: 'Gracile middleware error', | ||
}); | ||
} | ||
@@ -238,0 +190,0 @@ }; |
import type { IncomingMessage, Server, ServerResponse } from 'node:http'; | ||
export declare function printAddressInfos(options: { | ||
instance?: Server; | ||
address: string; | ||
server?: Server; | ||
address?: string; | ||
}): void; | ||
@@ -22,2 +22,3 @@ export declare function notFoundHandler(req: IncomingMessage, res: ServerResponse): void; | ||
}) => boolean) => BasicAuthUser; | ||
export declare function getClientDistPath(root: string): string; | ||
//# sourceMappingURL=utils.d.ts.map |
// NOTE: Util. to pretty print for user provided server. | ||
import { fileURLToPath } from 'node:url'; | ||
// import type { AddressInfo } from 'node:net'; | ||
@@ -6,7 +7,7 @@ import { logger } from '@gracile/internal-utils/logger'; | ||
import c from 'picocolors'; | ||
import { IP_EXPOSED } from './env.js'; | ||
import { CLIENT_DIST_DIR, IP_EXPOSED } from './env.js'; | ||
export function printAddressInfos(options) { | ||
let address = null; | ||
if (options.instance) { | ||
const infos = options.instance.address(); | ||
if (options.server) { | ||
const infos = options.server.address(); | ||
if (typeof infos === 'object' && infos && infos.port && infos.address) { | ||
@@ -19,3 +20,3 @@ address = `http://${infos.address}:${infos.port}/`; | ||
} | ||
else | ||
if (!address) | ||
throw new Error('Incorrect options'); | ||
@@ -26,5 +27,5 @@ logger.info(c.green(`${DEV ? 'development' : 'production'} server started`), { | ||
logger.info(` | ||
${c.dim('┃')} Local ${c.cyan(address)}` + | ||
${c.dim('┃')} Local ${c.cyan(address.replace(/::1?/, 'localhost'))}` + | ||
`${address?.includes(IP_EXPOSED) | ||
? `${c.dim('┃')} Network ${c.cyan(address)}` | ||
? `\n${c.dim('┃')} Network ${c.cyan(address)}` | ||
: ''} | ||
@@ -76,1 +77,4 @@ `); | ||
}; | ||
export function getClientDistPath(root) { | ||
return fileURLToPath(new URL(CLIENT_DIST_DIR, root)); | ||
} |
@@ -1,19 +0,13 @@ | ||
import type { UserConfig } from 'vite'; | ||
export declare class GracileConfig { | ||
port?: number; | ||
import type { Connect } from 'vite'; | ||
export interface GracileConfig { | ||
/** | ||
* Root directory for the project | ||
*/ | ||
root?: string; | ||
/** | ||
* @defaultValue 'static' | ||
*/ | ||
output?: 'static' | 'server'; | ||
vite?: UserConfig; | ||
server?: { | ||
entrypoint?: string; | ||
dev?: { | ||
locals?: (context: { | ||
nodeRequest: Connect.IncomingMessage; | ||
}) => unknown; | ||
}; | ||
constructor(options: GracileConfig); | ||
} | ||
export declare function defineConfig(options: GracileConfig): (ConfigClass: typeof GracileConfig) => GracileConfig; | ||
//# sourceMappingURL=user-config.d.ts.map |
@@ -1,19 +0,1 @@ | ||
export class GracileConfig { | ||
port; | ||
/** | ||
* Root directory for the project | ||
*/ | ||
root; | ||
/** | ||
* @defaultValue 'static' | ||
*/ | ||
output; | ||
vite; | ||
server; | ||
constructor(options) { | ||
Object.assign(this, options); | ||
} | ||
} | ||
export function defineConfig(options) { | ||
return (ConfigClass) => new ConfigClass(options); | ||
} | ||
export {}; |
{ | ||
"name": "@gracile/engine", | ||
"version": "0.2.0-next.1", | ||
"version": "0.2.0-next.2", | ||
"description": "A thin, full-stack, web framework", | ||
@@ -78,3 +78,3 @@ "keywords": [ | ||
}, | ||
"gitHead": "a6f99f391dcaf12b0a64a2bd0736ed9d1fab8adc" | ||
"gitHead": "6305693cb9f62e3ffef175e26660bf04b493bccc" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
134486
65
1751