diff --git a/src/build.ts b/src/build.ts
index 831d0be0e9..aef96b9184 100644
--- a/src/build.ts
+++ b/src/build.ts
@@ -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);
diff --git a/src/runtime.ts b/src/runtime.ts
index c12fb3e140..62bbdb09c1 100644
--- a/src/runtime.ts
+++ b/src/runtime.ts
@@ -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`, '') : ``;
- if (html.indexOf('') !== -1) {
- html = html.replace('', `${styleTags}`);
- } 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`, '') : ``;
+ if (html.indexOf('') !== -1) {
+ html = html.replace('', `${styleTags}`);
+ } 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 {
diff --git a/src/search.ts b/src/search.ts
new file mode 100644
index 0000000000..d9e2fa00ce
--- /dev/null
+++ b/src/search.ts
@@ -0,0 +1,75 @@
+import { existsSync } from 'fs';
+
+interface PageLocation {
+ fileURL: URL;
+ snowpackURL: string;
+}
+
+function findAnyPage(candidates: Array, 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
+ };
+}
\ No newline at end of file
diff --git a/test/astro-basic.test.js b/test/astro-basic.test.js
index b06cdfcd01..053bf2fbb0 100644
--- a/test/astro-basic.test.js
+++ b/test/astro-basic.test.js
@@ -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();
\ No newline at end of file
diff --git a/test/astro-search.test.js b/test/astro-search.test.js
new file mode 100644
index 0000000000..415bc44324
--- /dev/null
+++ b/test/astro-search.test.js
@@ -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();
diff --git a/test/fixtures/astro-basic/astro/layouts/base.astro b/test/fixtures/astro-basic/astro/layouts/base.astro
new file mode 100644
index 0000000000..ec996a32f7
--- /dev/null
+++ b/test/fixtures/astro-basic/astro/layouts/base.astro
@@ -0,0 +1,17 @@
+---
+export let content: any;
+---
+
+
+
+
+ {content.title}
+
+
+
+
+ {content.title}
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/astro-basic/astro/pages/index.astro b/test/fixtures/astro-basic/astro/pages/index.astro
index e6b8a1235f..5ae5380c52 100644
--- a/test/fixtures/astro-basic/astro/pages/index.astro
+++ b/test/fixtures/astro-basic/astro/pages/index.astro
@@ -1,7 +1,5 @@
---
- export function setup() {
- return {props: {}}
- }
+let title = 'My App'
---
diff --git a/test/fixtures/astro-basic/astro/pages/nested-astro/index.astro b/test/fixtures/astro-basic/astro/pages/nested-astro/index.astro
new file mode 100644
index 0000000000..a28992ee6b
--- /dev/null
+++ b/test/fixtures/astro-basic/astro/pages/nested-astro/index.astro
@@ -0,0 +1,12 @@
+---
+let title = 'Nested page'
+---
+
+
+
+
+
+
+ {title}
+
+
diff --git a/test/fixtures/astro-basic/astro/pages/nested-md/index.md b/test/fixtures/astro-basic/astro/pages/nested-md/index.md
new file mode 100644
index 0000000000..23374f9b89
--- /dev/null
+++ b/test/fixtures/astro-basic/astro/pages/nested-md/index.md
@@ -0,0 +1,6 @@
+---
+layout: ../../layouts/base.astro
+title: My Page
+---
+
+Hello world
\ No newline at end of file
diff --git a/test/fixtures/astro-basic/astro/pages/news.astro b/test/fixtures/astro-basic/astro/pages/news.astro
new file mode 100644
index 0000000000..71a00b8a98
--- /dev/null
+++ b/test/fixtures/astro-basic/astro/pages/news.astro
@@ -0,0 +1,12 @@
+---
+let title = 'The News'
+---
+
+
+
+ {title}
+
+
+ Hello world!
+
+