-Error: !!! OH NO !!! I AM A FAKE ERROR !!!
- at RouteModule.template (__REPLACED_FOR_TESTS__)
- at renderRouteTemplate (__REPLACED_FOR_TESTS__)
- at async middleware (__REPLACED_FOR_TESTS__)
- at async nodeHandler (__REPLACED_FOR_TESTS__)
-
-
-
-
-
-
-
+
+
+
+
500: TypeError | Internal Server Error
diff --git a/integration/__fixtures__/static-site/src/routes/throws.ts b/integration/__fixtures__/static-site/src/routes/throws.ts
index 8c37ba2..523e1f4 100644
--- a/integration/__fixtures__/static-site/src/routes/throws.ts
+++ b/integration/__fixtures__/static-site/src/routes/throws.ts
@@ -1,5 +1,6 @@
import { defineRoute } from '@gracile/gracile/route';
import { html } from '@gracile/gracile/server-html';
+// import { GracileUserError } from '@gracile/gracile/error';
import { document } from '../documents/document-minimal.js';
@@ -7,7 +8,7 @@ export default defineRoute({
document: (context) => document({ ...context, title: 'Gracile - Oh no' }),
template: (context) => {
- throw new Error('!!! OH NO !!! I AM A FAKE ERROR !!!');
+ throw new TypeError('!!! OH NO !!! I AM A FAKE ERROR !!!');
html`
ā ļø Arrrrrhh !!
diff --git a/integration/manual-tests.sh b/integration/manual-tests.sh
index efaf2c4..a99992d 100644
--- a/integration/manual-tests.sh
+++ b/integration/manual-tests.sh
@@ -1,4 +1,7 @@
# pnpm tsx --test -C test src/addons/client-router.test.ts
+
+# set -e
+
pnpm tsx --test -C test src/routes-premises.test.ts
pnpm tsx --test -C test src/addons/metadata.test.ts
diff --git a/integration/src/exports.test.ts b/integration/src/exports.test.ts
index e46f8d7..56a10c4 100644
--- a/integration/src/exports.test.ts
+++ b/integration/src/exports.test.ts
@@ -9,6 +9,8 @@ import { fileURLToPath } from 'node:url';
import { constants as serverConstants } from '@gracile/engine/server/constants';
import * as internalLogger from '@gracile/gracile/_internals/logger';
+import * as route from '@gracile/gracile/_internals/route';
+import * as routeModule from '@gracile/gracile/_internals/route-module';
import * as serverRuntime from '@gracile/gracile/_internals/server-runtime';
import * as document from '@gracile/gracile/document';
import { env as envFromNodeConditions } from '@gracile/gracile/env';
@@ -150,5 +152,8 @@ describe('gracile package should do its exports correctly', () => {
test('internals', () => {
assert.equal(typeof internalLogger.createLogger, 'function');
+ assert.equal(typeof route.RequestMethod, 'object');
+ assert.equal(typeof routeModule.RouteModule, 'function');
+ assert.equal(typeof serverRuntime.createGracileHandler, 'function');
});
});
diff --git a/integration/src/server-express/_common.ts b/integration/src/server-express/_common.ts
index f416a06..77fcf95 100644
--- a/integration/src/server-express/_common.ts
+++ b/integration/src/server-express/_common.ts
@@ -151,13 +151,15 @@ async function tests(mode: string, item: string, writeActual: boolean) {
));
// TODO: Test with "accept: json" when implemented
- await it(`load an error page when a route throws - ${item}`, async () =>
+ await it(`load an error page when a route throws - ${item}`, async () => {
+ const ressource = await fetchResource(ADDRESS, ['throws']);
+
assert.equal(
- (await fetchResource(ADDRESS, ['throws'])).includes(
- 'Error: !!! OH NO !!! I AM A FAKE ERROR !!!',
- ),
+ // should take over just after (its a client only component) in DEV
+ ressource.includes('500: Error | Internal Server Error'),
true,
- ));
+ );
+ });
await it(`should redirect`, async () =>
checkResponse(
diff --git a/packages/engine/src/dev/dev.ts b/packages/engine/src/dev/dev.ts
index 999bcee..651dbad 100644
--- a/packages/engine/src/dev/dev.ts
+++ b/packages/engine/src/dev/dev.ts
@@ -2,7 +2,7 @@ import { getLogger } from '@gracile/internal-utils/logger/helpers';
import c from 'picocolors';
import { type ViteDevServer } from 'vite';
-import { collectRoutes } from '../routes/collect.js';
+import { collectRoutes, WATCHED_FILES_REGEX } from '../routes/collect.js';
import type { RoutesManifest } from '../routes/route.js';
import {
createGracileHandler,
@@ -14,7 +14,7 @@ import { generateRoutesTypings } from './route-typings.js';
export async function createDevHandler({
routes,
vite,
- gracileConfig,dd
+ gracileConfig,
}: {
routes: RoutesManifest;
vite: ViteDevServer;
@@ -27,34 +27,36 @@ export async function createDevHandler({
const root = vite.config.root;
- logger.info(c.dim('\nCreating handlerā¦'), { timestamp: true });
+ logger.info('');
+ logger.info(c.dim('Creating the request handlerā¦'), { timestamp: true });
- await collectRoutes(routes, root, gracileConfig.routes?.exclude);
+ const collect = async () => {
+ await collectRoutes(routes, root, gracileConfig.routes?.exclude);
- if (gracileConfig.experimental?.generateRoutesTypings)
- generateRoutesTypings(root, routes).catch((error) =>
- logger.error(String(error)),
- );
+ if (gracileConfig.experimental?.generateRoutesTypings)
+ await generateRoutesTypings(root, routes).catch((error) =>
+ logger.error(String(error)),
+ );
+ };
+ await collect();
+
+ let wait: ReturnType;
vite.watcher.on('all', (event, file) => {
- // console.log({ event });
if (
- file.match(
- /\/src\/routes\/(.*)\.(ts|js|css|scss|sass|less|styl|stylus)$/,
- ) &&
+ file.match(WATCHED_FILES_REGEX) &&
['add', 'unlink'].includes(event)
- )
- collectRoutes(routes, root, gracileConfig.routes?.exclude)
- .then(() => {
- vite.hot.send('vite:invalidate');
-
- if (gracileConfig.experimental?.generateRoutesTypings)
- generateRoutesTypings(root, routes).catch((error) =>
- logger.error(String(error)),
- );
- })
- .catch((e) => logger.error(String(e)));
+ //
+ ) {
+ clearTimeout(wait);
+ wait = setTimeout(() => {
+ collect()
+ .then(() => vite.hot.send('vite:invalidate'))
+ .catch((error) => logger.error(String(error)));
+ }, 100);
+ }
});
+
//
// NOTE: Wrong place?
diff --git a/packages/engine/src/errors/create-vite-better-error.ts b/packages/engine/src/errors/create-vite-better-error.ts
new file mode 100644
index 0000000..f83c83c
--- /dev/null
+++ b/packages/engine/src/errors/create-vite-better-error.ts
@@ -0,0 +1,50 @@
+import { pathToFileURL } from 'node:url';
+
+import { getLogger } from '@gracile/internal-utils/logger/helpers';
+import { formatErrorMessage } from '@gracile-labs/better-errors/dev/logger';
+import { collectErrorMetadata } from '@gracile-labs/better-errors/dev/utils';
+import { getViteErrorPayload } from '@gracile-labs/better-errors/dev/vite';
+import type { ErrorPayload, ViteDevServer } from 'vite';
+
+import { renderLitTemplate } from '../render/utils.js';
+import { GRACILE_JS_ERRORS_DOCS_BASE, GracileErrorData } from './errors.js';
+import { builtInErrorPage } from './pages.js';
+
+const logger = getLogger();
+
+export async function emitViteBetterError({
+ error,
+ vite,
+}: {
+ error: Error;
+ vite: ViteDevServer;
+}) {
+ const errorWithMetadata = collectErrorMetadata(
+ error,
+ pathToFileURL(vite.config.root),
+ );
+ logger.error(
+ formatErrorMessage(
+ errorWithMetadata,
+ false,
+ GracileErrorData,
+ GRACILE_JS_ERRORS_DOCS_BASE,
+ ),
+ {
+ timestamp: true,
+ },
+ );
+ const payload = await getViteErrorPayload({
+ docsBaseUrl: GRACILE_JS_ERRORS_DOCS_BASE,
+ errorsData: GracileErrorData,
+ err: errorWithMetadata,
+ });
+ setTimeout(() => {
+ vite.hot.send(payload as ErrorPayload);
+ // NOTE: Arbitrary value. Lower seems to be too fast, higher is not guaranteed to work.
+ }, 200);
+ const errorPage = builtInErrorPage(error.name ?? 'Error', true);
+
+ const renderedErrorPage = await renderLitTemplate(errorPage);
+ return renderedErrorPage;
+}
diff --git a/packages/engine/src/errors/errors-data.ts b/packages/engine/src/errors/errors-data.ts
new file mode 100644
index 0000000..db9678d
--- /dev/null
+++ b/packages/engine/src/errors/errors-data.ts
@@ -0,0 +1,113 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export interface ErrorData {
+ name: string;
+ title: string;
+ message?: string | ((...params: any) => string) | undefined;
+ hint?: string | ((...params: any) => string) | undefined;
+}
+
+/**
+ * @docs
+ * @kind heading
+ * @name Gracile Errors
+ */
+// Gracile Errors, most errors will go here!
+
+/**
+ *
+ */
+export const FailedToLoadModuleSSR = {
+ name: 'FailedToLoadModuleSSR',
+ title: 'Could not import file.',
+ message: (importName: string) => `Could not import \`${importName}\`.`,
+ hint: 'This is often caused by a typo in the import path. Please make sure the file exists.',
+} as const satisfies ErrorData;
+/**
+ *
+ */
+// export const FailedToGlobalLogger = {
+// name: 'FailedToGlobalLogger',
+// title: 'Could not get the global logger.',
+// message: undefined,
+// hint: 'This a Gracile internal fault.',
+// };
+// /**
+// *
+// */
+// export const FailedToGetGracileVersion = {
+// name: 'FailedToGetGracileVersion',
+// title: 'Version must be set before.',
+// message: undefined,
+// hint: 'This a Gracile internal fault.',
+// } as const satisfies ErrorData;
+/**
+ *
+ */
+export const InvalidRequestInAdapter = {
+ name: 'InvalidRequestInAdapter',
+ title: 'Invalid request in adapter.',
+ message: (adapterName: string) =>
+ `Invalid request for adapter name: \`${adapterName}\`.`,
+ hint: 'Check that you have configured the adapter correctly.',
+} as const satisfies ErrorData;
+/**
+ *
+ */
+export const InvalidResponseInAdapter = {
+ name: 'InvalidResponseInAdapter',
+ title: 'Invalid response in adapter.',
+ message: (adapterName: string) =>
+ `Invalid response for adapter name: \`${adapterName}\`.`,
+ hint: undefined,
+} as const satisfies ErrorData;
+/**
+ *
+ */
+export const InvalidRouteDocument = {
+ name: 'RoutePageRender',
+ title: 'Invalid route document configuration.',
+ message: (routePath: string) =>
+ `Route document must be a function for: \`${routePath}\`.`,
+ hint: undefined,
+} as const satisfies ErrorData;
+/**
+ *
+ */
+export const InvalidRouteDocumentResult = {
+ name: 'RoutePageRender',
+ title: 'Incorrect document template result.',
+ message: (routePath: string) =>
+ `Incorrect document template result for: \`${routePath}\`.`,
+ hint: undefined,
+} as const satisfies ErrorData;
+/**
+ *
+ */
+export const InvalidRouteExport = {
+ name: 'InvalidRouteExport',
+ title: 'Invalid route export.',
+ message: (routePath: string) => `Incorrect route module: \`${routePath}\`.`,
+ hint: `Should export a default \`defineRoute\` function.`,
+} as const satisfies ErrorData;
+/**
+ *
+ */
+export const CouldNotRenderRouteDocument = {
+ name: 'CouldNotRenderRouteDocument',
+ title: 'Could not render the route document.',
+ message: (routePath: string) =>
+ `Could not render the route document for: \`${routePath}\`.`,
+ hint: undefined,
+} as const satisfies ErrorData;
+/**
+ *
+ */
+// export const NoRoutesFound = {
+// name: 'NoRoutesFound',
+// title: 'Could not find any routes.',
+// message: `Could not find any routes for your project.`,
+// hint: 'You have to populate the `src/routes` folder.',
+// };
+
+// `Wrong template result for fragment template ${routeInfos.foundRoute.filePath}.`,
diff --git a/packages/engine/src/errors/errors.ts b/packages/engine/src/errors/errors.ts
new file mode 100644
index 0000000..8ea0862
--- /dev/null
+++ b/packages/engine/src/errors/errors.ts
@@ -0,0 +1,159 @@
+/* eslint-disable max-classes-per-file */
+/* eslint-disable @typescript-eslint/lines-between-class-members */
+// NOTE: Taken and adapted from https://github.com/withGracile/Gracile/blob/cf65476b27053333cf5a36f6f9f46b794c98dfa2/packages/Gracile/src/core/errors/errors.ts
+
+import {
+ BetterError,
+ type BuiltinErrorTypes,
+} from '@gracile-labs/better-errors/errors';
+
+// import { codeFrame } from '@gracile-labs/better-errors/printer';
+
+export * as GracileErrorData from './errors-data.js';
+
+export const GRACILE_JS_ERRORS_DOCS_BASE =
+ 'https://gracile.js.org/docs/references/errors';
+
+type ErrorTypes =
+ | BuiltinErrorTypes
+ | 'GracileError'
+ | 'TemplateError'
+ | 'InternalError'
+ | 'TypeGuardError'
+ | 'AggregateError';
+
+// | 'BetterError'
+// | 'GracileUserError'
+// | 'CompilerError'
+// | 'CSSError'
+// | 'MarkdownError'
+
+export class GracileError extends BetterError {
+ type: ErrorTypes = 'GracileError';
+
+ static is(err: unknown): err is GracileError {
+ return (err as GracileError).type === 'GracileError';
+ }
+}
+
+export class TemplateError extends GracileError {
+ type: ErrorTypes = 'TemplateError';
+
+ static is(err: unknown): err is TemplateError {
+ return (err as TemplateError).type === 'TemplateError';
+ }
+}
+
+export class InternalError extends GracileError {
+ type: ErrorTypes = 'InternalError';
+
+ static is(err: unknown): err is InternalError {
+ return (err as InternalError).type === 'InternalError';
+ }
+}
+
+//
+// /* eslint-disable @typescript-eslint/lines-between-class-members */
+// /* eslint-disable max-classes-per-file */
+
+// import type { buildErrorMessage } from 'vite';
+
+// export type RollupError = Parameters['0'];
+
+// export abstract class BetterError extends Error implements RollupError {
+// // --- RollupError
+// watchFiles?: string[];
+// binding?: string;
+// cause?: unknown;
+// code?: string;
+// exporter?: string;
+// frame?: string;
+// hook?: string;
+// id?: string;
+// ids?: string[];
+// loc?: { column: number; file?: string; line: number };
+// meta?: unknown;
+// names?: string[];
+// plugin?: string;
+// pluginCode?: unknown;
+// pos?: number;
+// reexporter?: string;
+// stack?: string;
+// url?: string;
+// // --- End RollupError
+
+// constructor(message: string, options?: { cause?: unknown }) {
+// super(message);
+// this.name = 'BetterError';
+// this.cause = options?.cause;
+// }
+// }
+
+// export class GracileUnknownError extends BetterError {
+// constructor(message: string, options?: { cause?: unknown }) {
+// super(message, options);
+// this.name = 'GracileUnknownError';
+// }
+// }
+
+// export class SsrError extends BetterError {
+// constructor(message: string, options?: { cause?: unknown }) {
+// super(message, options);
+// this.name = 'SsrError';
+// }
+// }
+
+// export function isViteError(error: unknown): error is RollupError {
+// if (
+// typeof error === 'object' &&
+// error &&
+// 'message' in error &&
+// 'plugin' in error &&
+// typeof error.plugin === 'string' &&
+// error.plugin.startsWith(`vite:`)
+// )
+// return true;
+// return false;
+// }
+
+// // export type EsbuildError = {
+// // plugin: 'vite:esbuild';
+// // } & RollupError;
+
+// // export class GracileRouterError extends Error implements e {
+// // constructor(message: string, options: { cause: unknown }) {
+// // super(message);
+// // this.name = 'GracileRouterError';
+// // this.cause = options.cause;
+// // }
+// // }
+
+// /**
+// * Special error that is exposed to users.
+// * Compared to BetterError, it contains a subset of information.
+// */
+// export class GracileUserError extends Error {
+// type: ErrorTypes = 'GracileUserError';
+// /**
+// * A message that explains to the user how they can fix the error.
+// */
+// hint: string | undefined;
+// name = 'GracileUserError';
+
+// constructor(message: string, hint?: string) {
+// super();
+// this.message = message;
+// this.hint = hint;
+// }
+
+// static is(err: unknown): err is GracileUserError {
+// return (err as GracileUserError).type === 'GracileUserError';
+// }
+// }
+
+// ---
+
+/**
+ * An error util. that should never be called in theory.
+ */
+export class TypeGuardError extends Error {}
diff --git a/packages/engine/src/errors/pages.ts b/packages/engine/src/errors/pages.ts
new file mode 100644
index 0000000..b2ce3d4
--- /dev/null
+++ b/packages/engine/src/errors/pages.ts
@@ -0,0 +1,143 @@
+import { html } from '@lit-labs/ssr';
+
+export function errorInline(error: Error) {
+ return html`
+