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:
parent
d9733e8d42
commit
c9bc6ffef7
10 changed files with 216 additions and 80 deletions
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
// couldn‘t find match; 404
|
||||
return {
|
||||
statusCode: 404,
|
||||
type: 'unknown',
|
||||
error: new Error(`Could not locate ${selectedPage}`),
|
||||
};
|
||||
}
|
||||
|
||||
export interface AstroRuntime {
|
||||
|
|
75
src/search.ts
Normal file
75
src/search.ts
Normal 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
|
||||
};
|
||||
}
|
|
@ -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
41
test/astro-search.test.js
Normal 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();
|
17
test/fixtures/astro-basic/astro/layouts/base.astro
vendored
Normal file
17
test/fixtures/astro-basic/astro/layouts/base.astro
vendored
Normal 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>
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
export function setup() {
|
||||
return {props: {}}
|
||||
}
|
||||
let title = 'My App'
|
||||
---
|
||||
|
||||
<html>
|
||||
|
|
12
test/fixtures/astro-basic/astro/pages/nested-astro/index.astro
vendored
Normal file
12
test/fixtures/astro-basic/astro/pages/nested-astro/index.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
let title = 'Nested page'
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
</body>
|
||||
</html>
|
6
test/fixtures/astro-basic/astro/pages/nested-md/index.md
vendored
Normal file
6
test/fixtures/astro-basic/astro/pages/nested-md/index.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
layout: ../../layouts/base.astro
|
||||
title: My Page
|
||||
---
|
||||
|
||||
Hello world
|
12
test/fixtures/astro-basic/astro/pages/news.astro
vendored
Normal file
12
test/fixtures/astro-basic/astro/pages/news.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
let title = 'The News'
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue