Improve searching for pages (#60)

This improves the algorithm for searching for pages. It now works like:

1. If pathname ends with /
  1. Look for PATHNAME/index.astro
  1. Look for PATHNAME/index.md
1. else
  1. Look for PATHNAME.astro
  1. Look for PATHNAME.md
1. Look for PATHNAME/index.astro
  1. 301
1. Look for PATHNAME/index.md
  1. 301
1. 404
This commit is contained in:
Matthew Phillips 2021-04-05 14:18:09 -04:00 committed by GitHub
parent d9733e8d42
commit c9bc6ffef7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 216 additions and 80 deletions

View file

@ -44,9 +44,10 @@ async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf-8
/** Utility for writing a build result to disk */
async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'utf-8') {
if (result.statusCode !== 200) {
error(logging, 'build', result.error || result.statusCode);
//return 1;
if (result.statusCode === 500 || result.statusCode === 404) {
error(logging, 'build', result.error || result.statusCode);
} else if(result.statusCode !== 200) {
error(logging, 'build', `Unexpected load result (${result.statusCode}) for ${outPath.pathname}`);
} else {
const bytes = result.contents;
await writeFilep(outPath, bytes, encoding);

View file

@ -3,6 +3,7 @@ import type { AstroConfig, RuntimeMode } from './@types/astro';
import type { LogOptions } from './logger';
import type { CompileError } from './parser/utils/error.js';
import { debug, info } from './logger.js';
import { searchForPage } from './search.js';
import { existsSync } from 'fs';
import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpackServer } from 'snowpack';
@ -25,9 +26,10 @@ type LoadResultSuccess = {
contentType?: string | false;
};
type LoadResultNotFound = { statusCode: 404; error: Error };
type LoadResultRedirect = { statusCode: 301 | 302; location: string; };
type LoadResultError = { statusCode: 500 } & ({ type: 'parse-error'; error: CompileError } | { type: 'unknown'; error: Error });
export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultError;
export type LoadResult = LoadResultSuccess | LoadResultNotFound | LoadResultRedirect | LoadResultError;
// Disable snowpack from writing to stdout/err.
snowpackLogger.level = 'silent';
@ -38,15 +40,12 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
const { astroRoot } = config.astroConfig;
const fullurl = new URL(rawPathname || '/', 'https://example.org/');
const reqPath = decodeURI(fullurl.pathname);
const selectedPage = reqPath.substr(1) || 'index';
info(logging, 'access', reqPath);
const selectedPageLoc = new URL(`./pages/${selectedPage}.astro`, astroRoot);
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, astroRoot);
// Non-Astro pages (file resources)
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
const searchResult = searchForPage(fullurl, astroRoot);
if(searchResult.statusCode === 404) {
try {
const result = await frontendSnowpack.loadUrl(reqPath);
@ -66,61 +65,52 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
}
}
for (const url of [`/_astro/pages/${selectedPage}.astro.js`, `/_astro/pages/${selectedPage}.md.js`]) {
try {
const mod = await backendSnowpackRuntime.importModule(url);
debug(logging, 'resolve', `${reqPath} -> ${url}`);
let html = (await mod.exports.__renderPage({
request: {
host: fullurl.hostname,
path: fullurl.pathname,
href: fullurl.toString(),
},
children: [],
props: {},
})) as string;
if(searchResult.statusCode === 301) {
return { statusCode: 301, location: searchResult.pathname };
}
// inject styles
// TODO: handle this in compiler
const styleTags = Array.isArray(mod.css) && mod.css.length ? mod.css.reduce((markup, href) => `${markup}\n<link rel="stylesheet" type="text/css" href="${href}" />`, '') : ``;
if (html.indexOf('</head>') !== -1) {
html = html.replace('</head>', `${styleTags}</head>`);
} else {
html = styleTags + html;
}
const snowpackURL = searchResult.location.snowpackURL;
return {
statusCode: 200,
contents: html,
};
} catch (err) {
// if this is a 404, try the next URL (will be caught at the end)
const notFoundError = err.toString().startsWith('Error: Not Found');
if (notFoundError) {
continue;
}
try {
const mod = await backendSnowpackRuntime.importModule(snowpackURL);
debug(logging, 'resolve', `${reqPath} -> ${snowpackURL}`);
let html = (await mod.exports.__renderPage({
request: {
host: fullurl.hostname,
path: fullurl.pathname,
href: fullurl.toString(),
},
children: [],
props: {},
})) as string;
if (err.code === 'parse-error') {
return {
statusCode: 500,
type: 'parse-error',
error: err,
};
}
// inject styles
// TODO: handle this in compiler
const styleTags = Array.isArray(mod.css) && mod.css.length ? mod.css.reduce((markup, href) => `${markup}\n<link rel="stylesheet" type="text/css" href="${href}" />`, '') : ``;
if (html.indexOf('</head>') !== -1) {
html = html.replace('</head>', `${styleTags}</head>`);
} else {
html = styleTags + html;
}
return {
statusCode: 200,
contents: html,
};
} catch (err) {
if (err.code === 'parse-error') {
return {
statusCode: 500,
type: 'unknown',
type: 'parse-error',
error: err,
};
}
return {
statusCode: 500,
type: 'unknown',
error: err,
};
}
// couldnt find match; 404
return {
statusCode: 404,
type: 'unknown',
error: new Error(`Could not locate ${selectedPage}`),
};
}
export interface AstroRuntime {

75
src/search.ts Normal file
View file

@ -0,0 +1,75 @@
import { existsSync } from 'fs';
interface PageLocation {
fileURL: URL;
snowpackURL: string;
}
function findAnyPage(candidates: Array<string>, astroRoot: URL): PageLocation | false {
for(let candidate of candidates) {
const url = new URL(`./pages/${candidate}`, astroRoot);
if(existsSync(url)) {
return {
fileURL: url,
snowpackURL: `/_astro/pages/${candidate}.js`
};
}
}
return false;
}
type SearchResult = {
statusCode: 200;
location: PageLocation;
pathname: string;
} | {
statusCode: 301;
location: null;
pathname: string;
} | {
statusCode: 404;
};
export function searchForPage(url: URL, astroRoot: URL): SearchResult {
const reqPath = decodeURI(url.pathname);
const base = reqPath.substr(1);
// Try to find index.astro/md paths
if(reqPath.endsWith('/')) {
const candidates = [`${base}index.astro`, `${base}index.md`];
const location = findAnyPage(candidates, astroRoot);
if(location) {
return {
statusCode: 200,
location,
pathname: reqPath
};
}
} else {
// Try to find the page by its name.
const candidates = [`${base}.astro`, `${base}.md`];
let location = findAnyPage(candidates, astroRoot);
if(location) {
return {
statusCode: 200,
location,
pathname: reqPath
};
}
}
// Try to find name/index.astro/md
const candidates = [`${base}/index.astro`, `${base}/index.md`];
const location = findAnyPage(candidates, astroRoot);
if(location) {
return {
statusCode: 301,
location: null,
pathname: reqPath + '/'
};
}
return {
statusCode: 404
};
}

View file

@ -1,29 +1,13 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { createRuntime } from '../lib/runtime.js';
import { loadConfig } from '../lib/config.js';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const Basics = suite('HMX Basics');
const Basics = suite('Search paths');
let runtime;
setup(Basics, './fixtures/astro-basic');
Basics.before(async () => {
const astroConfig = await loadConfig(new URL('./fixtures/astro-basics', import.meta.url).pathname);
const logging = {
level: 'error',
dest: process.stderr,
};
runtime = await createRuntime(astroConfig, { logging });
});
Basics.after(async () => {
(await runtime) && runtime.shutdown();
});
Basics('Can load page', async () => {
Basics('Can load page', async ({ runtime }) => {
const result = await runtime.load('/');
assert.equal(result.statusCode, 200);
@ -32,4 +16,4 @@ Basics('Can load page', async () => {
assert.equal($('h1').text(), 'Hello world!');
});
Basics.run();
Basics.run();

41
test/astro-search.test.js Normal file
View file

@ -0,0 +1,41 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { setup } from './helpers.js';
const Search = suite('Search paths');
setup(Search, './fixtures/astro-basic');
Search('Finds the root page', async ({ runtime }) => {
const result = await runtime.load('/');
assert.equal(result.statusCode, 200);
});
Search('Matches pathname to filename', async ({ runtime }) => {
const result = await runtime.load('/news');
assert.equal(result.statusCode, 200);
});
Search('A URL with a trailing slash can match a folder with an index.astro', async ({ runtime }) => {
const result = await runtime.load('/nested-astro/');
assert.equal(result.statusCode, 200);
});
Search('A URL with a trailing slash can match a folder with an index.md', async ({ runtime }) => {
const result = await runtime.load('/nested-md/');
assert.equal(result.statusCode, 200);
});
Search('A URL without a trailing slash can redirect to a folder with an index.astro', async ({ runtime }) => {
const result = await runtime.load('/nested-astro');
assert.equal(result.statusCode, 301);
assert.equal(result.location, '/nested-astro/');
});
Search('A URL without a trailing slash can redirect to a folder with an index.md', async ({ runtime }) => {
const result = await runtime.load('/nested-md');
assert.equal(result.statusCode, 301);
assert.equal(result.location, '/nested-md/');
});
Search.run();

View file

@ -0,0 +1,17 @@
---
export let content: any;
---
<!doctype html>
<html lang="en">
<head>
<title>{content.title}</title>
<meta charset="utf-8">
</head>
<body>
<h1>{content.title}</h1>
<main><slot></slot></main>
</body>
</html>

View file

@ -1,7 +1,5 @@
---
export function setup() {
return {props: {}}
}
let title = 'My App'
---
<html>

View file

@ -0,0 +1,12 @@
---
let title = 'Nested page'
---
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<h1>{title}</h1>
</body>
</html>

View file

@ -0,0 +1,6 @@
---
layout: ../../layouts/base.astro
title: My Page
---
Hello world

View file

@ -0,0 +1,12 @@
---
let title = 'The News'
---
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>