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!

+ +