Skip to content

Commit

Permalink
feat: add "errors" directory with file-router logic;
Browse files Browse the repository at this point in the history
- exact > (4xx | 5xx) > (xxx | index)
- "index" may take place of "xxx", but xxx takes priority
- if no "xxx" key, then default error from `uikit` is attached
- using custom error page(s) can attach layout(s)
- if no custom error pages, then `uikit` used & WILL NOT attach layouts
- the `ERRORS` dictionary filled via Runtime plugin along w/ TREE routes
- all SSR and DOM runtimes attaches
  • Loading branch information
lukeed committed Oct 17, 2020
1 parent a7ed7ee commit 8e8feaa
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 80 deletions.
53 changes: 31 additions & 22 deletions packages/@freshie/ssr.node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@ import { join } from 'path';
import parse from '@polka/url';
import regexparam from 'regexparam';
import { createServer } from 'http';
import * as ErrorPage from '!!~error~!!';
import { HTML } from '!!~html~!!';

const Tree = new Map;
const ERRORS = { /* <ERRORS> */ };

// NOTE: ideally `layout` here
function define(route, ...Tags) {
function prepare(Tags, extra={}) {
let i=0, tmp, loaders=[], views=[];
let { keys, pattern } = regexparam(route);

for (; i < Tags.length; i++) {
tmp = Tags[i];
views.push(tmp.default);
if (tmp.preload) loaders.push(tmp.preload);
}
return { ...extra, loaders, views };
}

Tree.set(pattern, { keys, loaders, views });
// NOTE: ideally `layout` here
function define(route, ...Tags) {
let { keys, pattern } = regexparam(route);
let entry = prepare(Tags, { keys });
Tree.set(pattern, entry);
}

function toError(status, message='') {
Expand Down Expand Up @@ -60,10 +63,25 @@ export function start(options={}) {
{ dev: __DEV__ } // all from options.ssr?
);

// TODO: req.url vs req.href disparity
async function draw(req, route, context) {
let props = { url: req.href };

if (route.loaders.length > 0) {
await Promise.all(
route.loaders.map(p => p(req, context))
).then(list => {
// TODO? deep merge props
Object.assign(props, ...list);
});
}

return render(route.views, props);
}

return createServer(async (req, res) => {
let props={ url: req.url }, page={};
let route, isAsset, request=parse(req, decode);
let context = { status: 0, ssr: true, dev: __DEV__ };
let page={}, route, isAsset, request=parse(req, decode);
context.headers = { 'Content-Type': 'text/html;charset=utf-8' };

request.query = request.query || {};
Expand All @@ -82,23 +100,14 @@ export function start(options={}) {
});

request.params = route.params;

if (route.loaders.length > 0) {
await Promise.all(
route.loaders.map(p => p(request, context))
).then(list => {
// TODO? deep merge props
Object.assign(props, ...list);
});
}

page = await render(route.views, props);
page = await draw(request, route, context);
} catch (err) {
context.error = err;
let next = { url: req.url };
context.status = context.status || err.statusCode || err.status || 500;
if (ErrorPage.preload) Object.assign(next, await ErrorPage.preload(request, context));
page = await render([ErrorPage.default], next);
// look up error by specificity
const key = String(context.status);
const route = ERRORS[key] || ERRORS[key[0] + 'xx'] || ERRORS['xxx']
page = await draw(request, route, context);
} finally {
if (isAsset) return; // handled
// props.head=head; props.body=body;
Expand Down
62 changes: 35 additions & 27 deletions packages/@freshie/ssr.worker/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import regexparam from 'regexparam';
import * as ErrorPage from '!!~error~!!';
import { HTML } from '!!~html~!!';

export const TREE = {};
export const Cache = caches.default;
const ERRORS = { /* <ERRORS> */ };

var render, decode=true;
export function setup(options={}) {
Expand All @@ -17,16 +17,19 @@ export function setup(options={}) {
}
}

// NOTE: ideally `layout` here
export function define(route, ...Tags) {
function prepare(Tags) {
let i=0, tmp, loaders=[], views=[];

for (; i < Tags.length; i++) {
tmp = Tags[i];
views.push(tmp.default);
if (tmp.preload) loaders.push(tmp.preload);
}
return { loaders, views };
}

// NOTE: ideally `layout` here
export function define(route, ...Tags) {
let { views, loaders } = prepare(Tags);
add('GET', route, views, loaders);
}

Expand Down Expand Up @@ -101,28 +104,40 @@ function toError(status, message='') {
throw error;
}

async function draw(req, route, context) {
let props = { url: req.url };

if (route.loaders.length > 0) {
await Promise.all(
route.loaders.map(p => p(req, context))
).then(list => {
// TODO? deep merge props
Object.assign(props, ...list);
});
}

return render(route.views, props);
}

export async function run(event) {
const { request } = event;
const { url, method, headers } = request;

const isGET = /^(GET|HEAD)$/.test(method);
const { pathname, search, searchParams } = new URL(url);
const query = search ? Object.fromEntries(searchParams) : {};
const path = decode ? decodeURIComponent(pathname) : pathname;
const req = { url, method, headers, path, query, search, params:{}, body:null };

let props={ url }, page={};
let context = { status: 0, ssr: true, dev: __DEV__ };
let page={}, context = { status: 0, ssr: true, dev: __DEV__ };
context.headers = { 'Content-Type': 'text/html;charset=utf-8' };

try {
// TODO: detach if has custom
if (!isGET) return toError(405);

let { pathname, search, searchParams } = new URL(url);
if (decode) pathname = decodeURIComponent(pathname);

const query = search ? Object.fromEntries(searchParams) : {};
const req = { url, method, headers, params:{}, path:pathname, query, search, body:null };

const route = find(method, pathname);
const route = find(method, path);
if (!route) return toError(404);
req.params = route.params;

// TODO: only if has custom
if (false && request.body) {
Expand All @@ -135,22 +150,15 @@ export async function run(event) {
}
}

if (route.loaders.length > 0) {
await Promise.all(
route.loaders.map(p => p(req, context))
).then(list => {
// TODO? deep merge props
Object.assign(props, ...list);
});
}

page = await render(route.views, props);
req.params = route.params;
page = await draw(req, route, context);
} catch (err) {
let next = { url };
context.error = err;
context.status = context.status || err.statusCode || err.status || 500;
if (ErrorPage.preload) Object.assign(next, await ErrorPage.preload(request, context));
page = await render([ErrorPage.default], next);
// look up error by specificity
const key = String(context.status);
const route = ERRORS[key] || ERRORS[key[0] + 'xx'] || ERRORS['xxx']
page = await draw(req, route, context);
} finally {
// props.head=head; props.body=body;
// TODO: static HTML vs HTML component file
Expand Down
6 changes: 6 additions & 0 deletions packages/freshie/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ declare namespace Build {
layout: Nullable<string>;
wild: Nullable<string>;
}

interface Error {
key: string;
file: string;
layout: Nullable<string>;
}
}

declare namespace Runtime {
Expand Down
15 changes: 9 additions & 6 deletions packages/freshie/runtime/index.dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Router from 'freshie/router';

export var router;
var target, render, hydrate;
var ERRORS = { /* <ERRORS> */ };

// var hasSW = ('serviceWorker' in navigator);
// var root = document.body;
Expand Down Expand Up @@ -47,12 +48,15 @@ function run(Tags, params, ctx, req) {
}
}

function toError(code) {
var key = String(code);
return ERRORS[key] || ERRORS[key[0] + 'xx'] || ERRORS['xxx'];
}

function ErrorPage(params, ctx) {
import('!!~error~!!').then(m => {
var err = ctx.error || {};
ctx.status = ctx.status || err.statusCode || err.status || 500;
run([m], params, ctx);
});
var err = ctx.error || {};
ctx.status = ctx.status || err.statusCode || err.status || 500;
toError(ctx.status)().then(arr => run(arr, params, ctx));
}

// TODO: accept multiple layouts
Expand Down Expand Up @@ -96,7 +100,6 @@ export function start(options) {
// TODO: options.target
target = document.body;


router = Router(options.base || '/', is404);
/* <ROUTES> */

Expand Down
21 changes: 15 additions & 6 deletions packages/freshie/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ export async function load(argv: Argv.Options): Promise<Config.Group> {
const routes = await utils.routes(src, options.templates);
if (!routes.length) throw new Error('No routes found!');

// find/parse "errors" directory
// TODO: global default, regardless of uikit?
const errors = await utils.errors(src, options.templates);
if (uikit && !errors.find(x => x.key === 'xxx')) errors.push({
file: options.alias.entries['!!~error~!!'],
layout: null,
key: 'xxx',
});

// auto-detect entries; set SSR fallback
const entries = await fs.list(src).then(files => {
// dom: index.{ext} || index.dom.{ext}
Expand All @@ -115,7 +124,7 @@ export async function load(argv: Argv.Options): Promise<Config.Group> {
if (!entries.html) throw new Error('Missing HTML template file!');

// build DOM configuration
const client = Client(argv, routes, DOM.options, DOM.context);
const client = Client(argv, routes, errors, DOM.options, DOM.context);
client.plugins.unshift(Plugin.HTML(entries.html, options));
client.input = entries.dom; // inject entry point

Expand Down Expand Up @@ -144,7 +153,7 @@ export async function load(argv: Argv.Options): Promise<Config.Group> {
} // else error?

// Create SSR bundle config
server = Server(argv, routes, SSR.options, SSR.context);
server = Server(argv, routes, errors, SSR.options, SSR.context);
server.input = entries.ssr || SSR.options.ssr.entry; // inject entry point
}

Expand All @@ -160,7 +169,7 @@ export async function load(argv: Argv.Options): Promise<Config.Group> {
return { options, client, server };
}

export function Client(argv: Argv.Options, routes: Build.Route[], options: Config.Options, context: Config.Context): Rollup.Config {
export function Client(argv: Argv.Options, routes: Build.Route[], errors: Build.Error[], options: Config.Options, context: Config.Context): Rollup.Config {
const { src, dest, minify } = argv;
const { isProd } = context;

Expand All @@ -183,7 +192,7 @@ export function Client(argv: Argv.Options, routes: Build.Route[], options: Confi
plugins: [
Plugin.Router,
Plugin.Copy(options.copy),
Plugin.Runtime(routes, true),
Plugin.Runtime(routes, errors, true),
require('@rollup/plugin-alias')(options.alias),
// Assets.Plugin,
require('@rollup/plugin-replace')({
Expand All @@ -206,7 +215,7 @@ export function Client(argv: Argv.Options, routes: Build.Route[], options: Confi
};
}

export function Server(argv: Argv.Options, routes: Build.Route[], options: Config.Options, context: Config.Context): Rollup.Config {
export function Server(argv: Argv.Options, routes: Build.Route[], errors: Build.Error[], options: Config.Options, context: Config.Context): Rollup.Config {
const { src, dest, minify } = argv;
const { isProd } = context;

Expand All @@ -227,7 +236,7 @@ export function Server(argv: Argv.Options, routes: Build.Route[], options: Confi
},
plugins: [
Plugin.Template(template),
Plugin.Runtime(routes, false),
Plugin.Runtime(routes, errors, false),
require('@rollup/plugin-alias')(options.alias),
// Assets.Plugin,
require('@rollup/plugin-replace')({
Expand Down
45 changes: 32 additions & 13 deletions packages/freshie/src/config/plugins/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ import * as fs from '../../utils/fs';

const RUNTIME = join(__dirname, '..', 'runtime', 'index.dom.js');

async function xform(file: string, routes: Build.Route[], isDOM: boolean): Promise<string> {
let count=0, imports='', $routes='';
const Layouts: Map<string, string> = new Map;
async function xform(file: string, routes: Build.Route[], errors: Build.Error[], isDOM: boolean): Promise<string> {
const fdata = await fs.read(file, 'utf8');
const Layouts: Map<string, string> = new Map;
let count=0, imports='', $routes='', $errors='';

function to_layout(file: string | void): string | void {
let local = file && Layouts.get(file);
if (file && local) return local;
if (file && !local) {
Layouts.set(file, local = `$Layout${count++}`);
imports += `import * as ${local} from '${file}';\n`;
return local;
}
}

// TODO: multiple layout nesting
routes.forEach((tmp, idx) => {
Expand All @@ -17,31 +27,40 @@ async function xform(file: string, routes: Build.Route[], isDOM: boolean): Promi
if (tmp.layout) views.unshift(`import('${tmp.layout}')`);
$routes += `define('${tmp.pattern}', () => Promise.all([ ${views} ]));`;
} else {
let layout = tmp.layout && Layouts.get(tmp.layout);
if (tmp.layout && !layout) {
Layouts.set(tmp.layout, layout = `$Layout${count++}`);
imports += `import * as ${layout} from '${tmp.layout}';\n`;
}

let views = [`$Route${idx}`];
let layout = to_layout(tmp.layout);
if (layout) views.unshift(layout);
imports += `import * as $Route${idx} from '${tmp.file}';\n`;
$routes += `define('${tmp.pattern}', ${views});`;
}
});

errors.forEach((tmp, idx) => {
if (isDOM) {
let views = [`import('${tmp.file}')`];
if (tmp.layout) views.unshift(`import('${tmp.layout}')`);
$errors += `'${tmp.key}': () => Promise.all([ ${views} ]),`;
} else {
let views = [`$Error${idx}`];
let layout = to_layout(tmp.layout);
if (layout) views.unshift(layout);
imports += `import * as $Error${idx} from '${tmp.file}';\n`;
$errors += `'${tmp.key}': prepare([${views}]),`;
}
});

if (imports) imports += '\n';
return imports + fdata.replace('/* <ROUTES> */', $routes);
return imports + fdata.replace('/* <ROUTES> */', $routes).replace('/* <ERRORS> */', $errors);
}

export function Runtime(routes: Build.Route[], isDOM: boolean): Rollup.Plugin {
export function Runtime(routes: Build.Route[], errors: Build.Error[], isDOM: boolean): Rollup.Plugin {
const ident = 'freshie/runtime';

const Plugin: Rollup.Plugin = {
name: 'plugins/runtime',
load: id => {
if (id === ident) return xform(RUNTIME, routes, isDOM);
if (/[\\\/]+@freshie\/ssr/.test(id)) return xform(id, routes, isDOM);
if (id === ident) return xform(RUNTIME, routes, errors, isDOM);
if (/[\\\/]+@freshie\/ssr/.test(id)) return xform(id, routes, errors, isDOM);
}
};

Expand Down
Loading

0 comments on commit 8e8feaa

Please sign in to comment.