diff --git a/src/compiler/utils/error.ts b/src/compiler/utils/error.ts index 5c2cf3732..438d8c500 100644 --- a/src/compiler/utils/error.ts +++ b/src/compiler/utils/error.ts @@ -3,7 +3,7 @@ import { locate } from 'locate-character'; import get_code_frame from './get_code_frame.js'; -class CompileError extends Error { +export class CompileError extends Error { code: string; start: { line: number; column: number }; end: { line: number; column: number }; diff --git a/src/dev.ts b/src/dev.ts index 49faafd40..d2a268ef6 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -1,16 +1,24 @@ import type { AstroConfig } from './@types/astro'; -import { loadConfiguration, startServer as startSnowpackServer } from 'snowpack'; +import type { LogOptions } from './logger.js'; +import { loadConfiguration, startServer as startSnowpackServer, logger as snowpackLogger } from 'snowpack'; import { existsSync, promises as fsPromises } from 'fs'; import http from 'http'; -import { createRequire } from 'module'; +import { relative as pathRelative } from 'path'; +import { defaultLogDestination, info, error, parseError } from './logger.js'; const { readFile } = fsPromises; -const require = createRequire(import.meta.url); - const hostname = '127.0.0.1'; const port = 3000; +// Disable snowpack from writing to stdout/err. +snowpackLogger.level = 'silent'; + +const logging: LogOptions = { + level: 'debug', + dest: defaultLogDestination +}; + export default async function (astroConfig: AstroConfig) { const { projectRoot, hmxRoot } = astroConfig; @@ -58,7 +66,7 @@ export default async function (astroConfig: AstroConfig) { const fullurl = new URL(req.url || '/', 'https://example.org/'); const reqPath = decodeURI(fullurl.pathname); const selectedPage = reqPath.substr(1) || 'index'; - console.log(reqPath, selectedPage); + info(logging, 'access', reqPath); const selectedPageLoc = new URL(`./pages/${selectedPage}.hmx`, hmxRoot); const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, hmxRoot); @@ -74,7 +82,7 @@ export default async function (astroConfig: AstroConfig) { res.write(result.contents); res.end(); } catch (err) { - console.log('Not found', reqPath); + error(logging, 'static', 'Not found', reqPath); res.statusCode = 404; res.setHeader('Content-Type', 'text/plain'); res.end('Not Found'); @@ -89,7 +97,18 @@ export default async function (astroConfig: AstroConfig) { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end(html); } catch (err) { - console.log(err); + switch(err.code) { + case 'parse-error': { + err.filename = pathRelative(projectRoot.pathname, err.filename); + debugger; + parseError(logging, err); + break; + } + default: { + error(logging, 'running hmx', err); + break; + } + } } }); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..dccc3e85e --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,124 @@ +import type { CompileError } from './compiler/utils/error.js'; +import { bold, blue, red, grey, underline } from 'kleur/colors'; +import { Writable } from 'stream'; + +type ConsoleStream = Writable & { + fd: 1 | 2 +}; + +export const defaultLogDestination = new Writable({ + objectMode: true, + write(event: LogMessage, _, callback) { + let dest: ConsoleStream = process.stderr; + if(levels[event.level] < levels['error']) { + dest = process.stdout; + } + let type = event.type; + if(event.level === 'info') { + type = bold(blue(type)); + } else if(event.level === 'error') { + type = bold(red(type)); + } + + dest.write(`[${type}] `); + dest.write(event.message); + dest.write('\n'); + + callback(); + } +}); + +interface LogWritable extends Writable { + write: (chunk: T) => boolean; +} + +export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino +export type LoggerEvent = 'debug' | 'info' | 'warn' | 'error'; + +export interface LogOptions { + dest: LogWritable; + level: LoggerLevel +} + +export const defaultLogOptions: LogOptions = { + dest: defaultLogDestination, + level: 'info' +}; + +export interface LogMessage { + type: string; + level: LoggerLevel, + message: string; +} + +const levels: Record = { + debug: 20, + info: 30, + warn: 40, + error: 50, + silent: 90, +}; + +export function log(opts: LogOptions = defaultLogOptions, level: LoggerLevel, type: string, ...messages: Array) { + let event: LogMessage = { + type, + level, + message: '' + }; + + if(messages.length === 1 && typeof messages[0] === 'object') { + Object.assign(event, messages[0]); + } else { + event.message = messages.join(' '); + } + + // test if this level is enabled or not + if (levels[opts.level] > levels[level]) { + return; // do nothing + } + + opts.dest.write(event); +} + +export function debug(opts: LogOptions, type: string, ...messages: Array) { + return log(opts, 'debug', type, ...messages); +} + +export function info(opts: LogOptions, type: string, ...messages: Array) { + return log(opts, 'info', type, ...messages); +} + +export function warn(opts: LogOptions, type: string, ...messages: Array) { + return log(opts, 'warn', type, ...messages); +} + +export function error(opts: LogOptions, type: string, ...messages: Array) { + return log(opts, 'error', type, ...messages); +} + +export function parseError(opts: LogOptions, err: CompileError) { + let frame = err.frame + // Switch colons for pipes + .replace(/^([0-9]+)(:)/mg, `${bold('$1')} │`) + // Make the caret red. + .replace(/(?<=^\s+)(\^)/mg, bold(red(' ^'))) + // Add identation + .replace(/^/mg, ' '); + + error(opts, 'parse-error', ` + + ${underline(bold(grey(`${err.filename}:${err.start.line}:${err.start.column}`)))} + + ${bold(red(`𝘅 ${err.message}`))} + +${frame} +`); +} + +// A default logger for when too lazy to pass LogOptions around. +export const logger = { + debug: debug.bind(null, defaultLogOptions), + info: info.bind(null, defaultLogOptions), + warn: warn.bind(null, defaultLogOptions), + error: error.bind(null, defaultLogOptions) +}; \ No newline at end of file diff --git a/src/transform2.ts b/src/transform2.ts index 197a38ea1..15f36c777 100644 --- a/src/transform2.ts +++ b/src/transform2.ts @@ -1,5 +1,6 @@ +import type { LogOptions } from './logger.js'; + import path from 'path'; -import astring from 'astring'; import esbuild from 'esbuild'; import eslexer from 'es-module-lexer'; import micromark from 'micromark'; @@ -10,6 +11,7 @@ import { walk } from 'estree-walker'; import { parse } from './compiler/index.js'; import markdownEncode from './markdown-encode.js'; import { TemplateNode } from './compiler/interfaces.js'; +import { defaultLogOptions, info } from './logger.js'; const { transformSync } = esbuild; @@ -22,13 +24,13 @@ interface Attribute { } interface CompileOptions { + logging: LogOptions; resolve: (p: string) => string; } const defaultCompileOptions: CompileOptions = { - resolve(p: string) { - return p; - }, + logging: defaultLogOptions, + resolve: (p: string) => p }; function internalImport(internalPath: string) { @@ -177,10 +179,12 @@ function compileScriptSafe(raw: string, loader: 'jsx' | 'tsx'): string { return code; } -async function convertHmxToJsx(template: string, compileOptions: CompileOptions) { +async function convertHmxToJsx(template: string, filename: string, compileOptions: CompileOptions) { await eslexer.init; - const ast = parse(template, {}); + const ast = parse(template, { + filename + }); const script = compileScriptSafe(ast.instance ? ast.instance.content : '', 'tsx'); // Compile scripts as TypeScript, always @@ -357,7 +361,7 @@ async function convertHmxToJsx(template: string, compileOptions: CompileOptions) }; } -async function convertMdToJsx(contents: string, compileOptions: CompileOptions) { +async function convertMdToJsx(contents: string, filename: string, compileOptions: CompileOptions) { // This doesn't work. const { data: _frontmatterData, content } = matter(contents); const mdHtml = micromark(content, { @@ -385,6 +389,7 @@ async function convertMdToJsx(contents: string, compileOptions: CompileOptions) `${mdHtml}`, + filename, compileOptions ); } @@ -392,9 +397,9 @@ async function convertMdToJsx(contents: string, compileOptions: CompileOptions) async function transformFromSource(contents: string, filename: string, compileOptions: CompileOptions): Promise> { switch (path.extname(filename)) { case '.hmx': - return convertHmxToJsx(contents, compileOptions); + return convertHmxToJsx(contents, filename, compileOptions); case '.md': - return convertMdToJsx(contents, compileOptions); + return convertMdToJsx(contents, filename, compileOptions); default: throw new Error('Not Supported!'); }