summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Drew Powers <1369770+drwpow@users.noreply.github.com> 2021-04-12 17:21:29 -0600
committerGravatar GitHub <noreply@github.com> 2021-04-12 17:21:29 -0600
commit3639190b4e1b4c97836d448fa80a58aa45c823a7 (patch)
tree31160f051aa00cb0ebb820ab2061fb7d16956ee0 /src
parent687ff5bacd8c776e514f53c4b59c3a67274d3971 (diff)
downloadastro-3639190b4e1b4c97836d448fa80a58aa45c823a7.tar.gz
astro-3639190b4e1b4c97836d448fa80a58aa45c823a7.tar.zst
astro-3639190b4e1b4c97836d448fa80a58aa45c823a7.zip
Renaming to import.meta.fetchContent (#70)
* Change to import.meta.glob() Change of plans—maintain parity with Snowpack and Vite because our Collections API will use a different interface * Get basic pagination working * Get params working * Rename to import.meta.fetchContent * Upgrade to fdir
Diffstat (limited to 'src')
-rw-r--r--src/@types/astro.ts44
-rw-r--r--src/compiler/codegen/content.ts78
-rw-r--r--src/compiler/codegen/index.ts (renamed from src/compiler/codegen.ts)222
-rw-r--r--src/compiler/codegen/utils.ts20
-rw-r--r--src/compiler/index.ts4
-rw-r--r--src/frontend/render/svelte.ts3
-rw-r--r--src/runtime.ts78
-rw-r--r--src/search.ts59
8 files changed, 409 insertions, 99 deletions
diff --git a/src/@types/astro.ts b/src/@types/astro.ts
index 509d8ddc5..e12ceed84 100644
--- a/src/@types/astro.ts
+++ b/src/@types/astro.ts
@@ -26,6 +26,8 @@ export interface TransformResult {
imports: string[];
html: string;
css?: string;
+ /** If this page exports a collection, the JS to be executed as a string */
+ createCollection?: string;
}
export interface CompileResult {
@@ -35,3 +37,45 @@ export interface CompileResult {
}
export type RuntimeMode = 'development' | 'production';
+
+export type Params = Record<string, string | number>;
+
+export interface CreateCollection<T = any> {
+ data: ({ params }: { params: Params }) => T[];
+ routes?: Params[];
+ /** tool for generating current page URL */
+ permalink?: ({ params }: { params: Params }) => string;
+ /** page size */
+ pageSize?: number;
+}
+
+export interface CollectionResult<T = any> {
+ /** result */
+ data: T[];
+
+ /** metadata */
+ /** the count of the first item on the page, starting from 0 */
+ start: number;
+ /** the count of the last item on the page, starting from 0 */
+ end: number;
+ /** total number of results */
+ total: number;
+ page: {
+ /** the current page number, starting from 1 */
+ current: number;
+ /** number of items per page (default: 25) */
+ size: number;
+ /** number of last page */
+ last: number;
+ };
+ url: {
+ /** url of the current page */
+ current: string;
+ /** url of the previous page (if there is one) */
+ prev?: string;
+ /** url of the next page (if there is one) */
+ next?: string;
+ };
+ /** Matched parameters, if any */
+ params: Params;
+}
diff --git a/src/compiler/codegen/content.ts b/src/compiler/codegen/content.ts
new file mode 100644
index 000000000..14542d533
--- /dev/null
+++ b/src/compiler/codegen/content.ts
@@ -0,0 +1,78 @@
+import path from 'path';
+import { fdir, PathsOutput } from 'fdir';
+
+/**
+ * Handling for import.meta.glob and import.meta.globEager
+ */
+
+interface GlobOptions {
+ namespace: string;
+ filename: string;
+}
+
+interface GlobResult {
+ /** Array of import statements to inject */
+ imports: Set<string>;
+ /** Replace original code with */
+ code: string;
+}
+
+const crawler = new fdir();
+
+/** General glob handling */
+function globSearch(spec: string, { filename }: { filename: string }): string[] {
+ try {
+ // Note: fdir’s glob requires you to do some work finding the closest non-glob folder.
+ // For example, this fails: .glob("./post/*.md").crawl("/…/astro/pages") ❌
+ // …but this doesn’t: .glob("*.md").crawl("/…/astro/pages/post") ✅
+ let globDir = '';
+ let glob = spec;
+ for (const part of spec.split('/')) {
+ if (!part.includes('*')) {
+ // iterate through spec until first '*' is reached
+ globDir = path.posix.join(globDir, part); // this must be POSIX-style
+ glob = glob.replace(`${part}/`, ''); // move parent dirs off spec, and onto globDir
+ } else {
+ // at first '*', exit
+ break;
+ }
+ }
+
+ const cwd = path.join(path.dirname(filename), globDir.replace(/\//g, path.sep)); // this must match OS (could be '/' or '\')
+ let found = crawler.glob(glob).crawl(cwd).sync() as PathsOutput;
+ if (!found.length) {
+ throw new Error(`No files matched "${spec}" from ${filename}`);
+ }
+ return found.map((importPath) => {
+ if (importPath.startsWith('http') || importPath.startsWith('.')) return importPath;
+ return `./` + globDir + '/' + importPath;
+ });
+ } catch (err) {
+ throw new Error(`No files matched "${spec}" from ${filename}`);
+ }
+}
+
+/** import.meta.fetchContent() */
+export function fetchContent(spec: string, { namespace, filename }: GlobOptions): GlobResult {
+ let code = '';
+ const imports = new Set<string>();
+ const importPaths = globSearch(spec, { filename });
+
+ // gather imports
+ importPaths.forEach((importPath, j) => {
+ const id = `${namespace}_${j}`;
+ imports.add(`import { __content as ${id} } from '${importPath}';`);
+
+ // add URL if this appears within the /pages/ directory (probably can be improved)
+ const fullPath = path.resolve(path.dirname(filename), importPath);
+ if (fullPath.includes(`${path.sep}pages${path.sep}`)) {
+ const url = importPath.replace(/^\./, '').replace(/\.md$/, '');
+ imports.add(`${id}.url = '${url}';`);
+ }
+ });
+
+ // generate replacement code
+ code += `${namespace} = [${importPaths.map((_, j) => `${namespace}_${j}`).join(',')}];\n`;
+
+ return { imports, code };
+}
diff --git a/src/compiler/codegen.ts b/src/compiler/codegen/index.ts
index 3e14ba069..d2ac96702 100644
--- a/src/compiler/codegen.ts
+++ b/src/compiler/codegen/index.ts
@@ -1,17 +1,22 @@
-import type { CompileOptions } from '../@types/compiler';
-import type { AstroConfig, ValidExtensionPlugins } from '../@types/astro';
-import type { Ast, Script, Style, TemplateNode } from '../parser/interfaces';
-import type { JsxItem, TransformResult } from '../@types/astro';
+import type { CompileOptions } from '../../@types/compiler';
+import type { AstroConfig, ValidExtensionPlugins } from '../../@types/astro';
+import type { Ast, Script, Style, TemplateNode } from '../../parser/interfaces';
+import type { TransformResult } from '../../@types/astro';
import eslexer from 'es-module-lexer';
import esbuild from 'esbuild';
-import { fdir, PathsOutput } from 'fdir';
import path from 'path';
import { walk } from 'estree-walker';
-import babelParser from '@babel/parser';
import _babelGenerator from '@babel/generator';
+import babelParser from '@babel/parser';
+import * as babelTraverse from '@babel/traverse';
import { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types';
+import { warn } from '../../logger.js';
+import { fetchContent } from './content.js';
+import { isImportMetaDeclaration } from './utils.js';
+import { yellow } from 'kleur/colors';
+const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default;
const babelGenerator: typeof _babelGenerator =
// @ts-ignore
_babelGenerator.default;
@@ -36,15 +41,6 @@ function internalImport(internalPath: string) {
return `/_astro_internal/${internalPath}`;
}
-/** Is this an import.meta.* built-in? You can pass an optional 2nd param to see if the name matches as well. */
-function isImportMetaDeclaration(declaration: VariableDeclarator, metaName?: string): boolean {
- const { init } = declaration;
- if (!init || init.type !== 'CallExpression' || init.callee.type !== 'MemberExpression' || init.callee.object.type !== 'MetaProperty') return false;
- // optional: if metaName specified, match that
- if (metaName && (init.callee.property.type !== 'Identifier' || init.callee.property.name !== metaName)) return false;
- return true;
-}
-
/** Retrieve attributes from TemplateNode */
function getAttributes(attrs: Attribute[]): Record<string, string> {
let result: Record<string, string> = {};
@@ -283,6 +279,12 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
type Components = Record<string, { type: string; url: string; plugin: string | undefined }>;
+interface CompileResult {
+ script: string;
+ componentPlugins: Set<ValidExtensionPlugins>;
+ createCollection?: string;
+}
+
interface CodegenState {
filename: string;
components: Components;
@@ -291,22 +293,20 @@ interface CodegenState {
dynamicImports: DynamicImportMap;
}
-// cache filesystem pings
-const miniGlobCache = new Map<string, Map<string, string[]>>();
-
/** Compile/prepare Astro frontmatter scripts */
-function compileModule(module: Script, state: CodegenState, compileOptions: CompileOptions) {
+function compileModule(module: Script, state: CodegenState, compileOptions: CompileOptions): CompileResult {
const { extensions = defaultExtensions } = compileOptions;
const componentImports: ImportDeclaration[] = [];
const componentProps: VariableDeclarator[] = [];
const componentExports: ExportNamedDeclaration[] = [];
- const collectionImports = new Map<string, string>();
+ const contentImports = new Map<string, { spec: string; declarator: string }>();
let script = '';
let propsStatement = '';
- let dataStatement = '';
+ let contentCode = ''; // code for handling import.meta.fetchContent(), if any;
+ let createCollection = ''; // function for executing collection
const componentPlugins = new Set<ValidExtensionPlugins>();
if (module) {
@@ -320,45 +320,64 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
while (--i >= 0) {
const node = body[i];
switch (node.type) {
+ case 'ExportNamedDeclaration': {
+ if (!node.declaration) break;
+ // const replacement = extract_exports(node);
+
+ if (node.declaration.type === 'VariableDeclaration') {
+ // case 1: prop (export let title)
+
+ const declaration = node.declaration.declarations[0];
+ if ((declaration.id as Identifier).name === '__layout' || (declaration.id as Identifier).name === '__content') {
+ componentExports.push(node);
+ } else {
+ componentProps.push(declaration);
+ }
+ body.splice(i, 1);
+ } else if (node.declaration.type === 'FunctionDeclaration') {
+ // case 2: createCollection (export async function)
+ if (!node.declaration.id || node.declaration.id.name !== 'createCollection') break;
+ createCollection = module.content.substring(node.declaration.start || 0, node.declaration.end || 0);
+
+ // remove node
+ body.splice(i, 1);
+ }
+ break;
+ }
+ case 'FunctionDeclaration': {
+ break;
+ }
case 'ImportDeclaration': {
componentImports.push(node);
body.splice(i, 1); // remove node
break;
}
- case 'ExportNamedDeclaration': {
- if (node.declaration?.type !== 'VariableDeclaration') {
- // const replacement = extract_exports(node);
- break;
- }
- const declaration = node.declaration.declarations[0];
- if ((declaration.id as Identifier).name === '__layout' || (declaration.id as Identifier).name === '__content') {
- componentExports.push(node);
- } else {
- componentProps.push(declaration);
- }
- body.splice(i, 1);
- break;
- }
case 'VariableDeclaration': {
for (const declaration of node.declarations) {
- // only select import.meta.collection() calls here. this utility filters those out for us.
- if (!isImportMetaDeclaration(declaration, 'collection')) continue;
- if (declaration.id.type !== 'Identifier') continue;
- const { id, init } = declaration;
- if (!id || !init || init.type !== 'CallExpression') continue;
+ // only select import.meta.fetchContent() calls here. this utility filters those out for us.
+ if (!isImportMetaDeclaration(declaration, 'fetchContent')) continue;
+
+ // remove node
+ body.splice(i, 1);
+
+ // a bit of munging
+ let { id, init } = declaration;
+ if (!id || !init || id.type !== 'Identifier') continue;
+ if (init.type === 'AwaitExpression') {
+ init = init.argument;
+ const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
+ warn(compileOptions.logging, shortname, yellow('awaiting import.meta.fetchContent() not necessary'));
+ }
+ if (init.type !== 'CallExpression') continue;
// gather data
const namespace = id.name;
- // TODO: support more types (currently we can; it’s just a matter of parsing out the expression)
if ((init as any).arguments[0].type !== 'StringLiteral') {
- throw new Error(`[import.meta.collection] Only string literals allowed, ex: \`import.meta.collection('./post/*.md')\`\n ${state.filename}`);
+ throw new Error(`[import.meta.fetchContent] Only string literals allowed, ex: \`import.meta.fetchContent('./post/*.md')\`\n ${state.filename}`);
}
const spec = (init as any).arguments[0].value;
- if (typeof spec === 'string') collectionImports.set(namespace, spec);
-
- // remove node
- body.splice(i, 1);
+ if (typeof spec === 'string') contentImports.set(namespace, { spec, declarator: node.kind });
}
break;
}
@@ -402,59 +421,73 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
propsStatement += `} = props;\n`;
}
- // handle importing data
- for (const [namespace, spec] of collectionImports.entries()) {
- // only allow for .md files
- if (!spec.endsWith('.md')) {
- throw new Error(`Only *.md pages are supported for import.meta.collection(). Attempted to load "${spec}"`);
- }
-
- // locate files
- try {
- let found: string[];
-
- // use cache
- let cachedLookups = miniGlobCache.get(state.filename);
- if (!cachedLookups) {
- cachedLookups = new Map();
- miniGlobCache.set(state.filename, cachedLookups);
- }
- if (cachedLookups.get(spec)) {
- found = cachedLookups.get(spec) as string[];
- } else {
- found = new fdir().glob(spec).withFullPaths().crawl(path.dirname(state.filename)).sync() as PathsOutput;
- cachedLookups.set(spec, found);
- miniGlobCache.set(state.filename, cachedLookups);
- }
-
- // throw error, purge cache if no results found
- if (!found.length) {
- cachedLookups.delete(spec);
- miniGlobCache.set(state.filename, cachedLookups);
- throw new Error(`No files matched "${spec}" from ${state.filename}`);
- }
-
- const data = found.map((importPath) => {
- if (importPath.startsWith('http') || importPath.startsWith('.')) return importPath;
- return `./` + importPath;
- });
-
- // add static imports (probably not the best, but async imports don‘t work just yet)
- data.forEach((importPath, j) => {
- state.importExportStatements.add(`const ${namespace}_${j} = import('${importPath}').then((m) => ({ ...m.__content, url: '${importPath.replace(/\.md$/, '')}' }));`);
- });
+ // handle createCollection, if any
+ if (createCollection) {
+ // TODO: improve this? while transforming in-place isn’t great, this happens at most once per-route
+ const ast = babelParser.parse(createCollection, {
+ sourceType: 'module',
+ });
+ traverse(ast, {
+ enter({ node }) {
+ switch (node.type) {
+ case 'VariableDeclaration': {
+ for (const declaration of node.declarations) {
+ // only select import.meta.collection() calls here. this utility filters those out for us.
+ if (!isImportMetaDeclaration(declaration, 'fetchContent')) continue;
+
+ // a bit of munging
+ let { id, init } = declaration;
+ if (!id || !init || id.type !== 'Identifier') continue;
+ if (init.type === 'AwaitExpression') {
+ init = init.argument;
+ const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
+ warn(compileOptions.logging, shortname, yellow('awaiting import.meta.fetchContent() not necessary'));
+ }
+ if (init.type !== 'CallExpression') continue;
+
+ // gather data
+ const namespace = id.name;
+
+ if ((init as any).arguments[0].type !== 'StringLiteral') {
+ throw new Error(`[import.meta.fetchContent] Only string literals allowed, ex: \`import.meta.fetchContent('./post/*.md')\`\n ${state.filename}`);
+ }
+ const spec = (init as any).arguments[0].value;
+ if (typeof spec !== 'string') break;
+
+ const globResult = fetchContent(spec, { namespace, filename: state.filename });
+
+ let imports = '';
+ for (const importStatement of globResult.imports) {
+ imports += importStatement + '\n';
+ }
+
+ createCollection =
+ imports + '\n\nexport ' + createCollection.substring(0, declaration.start || 0) + globResult.code + createCollection.substring(declaration.end || 0);
+ }
+ break;
+ }
+ }
+ },
+ });
+ }
- // expose imported data to Astro script
- dataStatement += `const ${namespace} = await Promise.all([${found.map((_, j) => `${namespace}_${j}`).join(',')}]);\n`;
- } catch (err) {
- throw new Error(`No files matched "${spec}" from ${state.filename}`);
+ // import.meta.fetchContent()
+ for (const [namespace, { declarator, spec }] of contentImports.entries()) {
+ const globResult = fetchContent(spec, { namespace, filename: state.filename });
+ for (const importStatement of globResult.imports) {
+ state.importExportStatements.add(importStatement);
}
+ contentCode += globResult.code;
}
- script = propsStatement + dataStatement + babelGenerator(program).code;
+ script = propsStatement + contentCode + babelGenerator(program).code;
}
- return { script, componentPlugins };
+ return {
+ script,
+ componentPlugins,
+ createCollection: createCollection || undefined,
+ };
}
/** Compile styles */
@@ -606,7 +639,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
dynamicImports: new Map(),
};
- const { script, componentPlugins } = compileModule(ast.module, state, compileOptions);
+ const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions);
state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolve);
compileCss(ast.css, state);
@@ -618,5 +651,6 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
imports: Array.from(state.importExportStatements),
html,
css: state.css.length ? state.css.join('\n\n') : undefined,
+ createCollection,
};
}
diff --git a/src/compiler/codegen/utils.ts b/src/compiler/codegen/utils.ts
new file mode 100644
index 000000000..5a9316fa1
--- /dev/null
+++ b/src/compiler/codegen/utils.ts
@@ -0,0 +1,20 @@
+/**
+ * Codegen utils
+ */
+
+import type { VariableDeclarator } from '@babel/types';
+
+/** Is this an import.meta.* built-in? You can pass an optional 2nd param to see if the name matches as well. */
+export function isImportMetaDeclaration(declaration: VariableDeclarator, metaName?: string): boolean {
+ let { init } = declaration;
+ if (!init) return false; // definitely not import.meta
+ // this could be `await import.meta`; if so, evaluate that:
+ if (init.type === 'AwaitExpression') {
+ init = init.argument;
+ }
+ // continue evaluating
+ if (init.type !== 'CallExpression' || init.callee.type !== 'MemberExpression' || init.callee.object.type !== 'MetaProperty') return false;
+ // optional: if metaName specified, match that
+ if (metaName && (init.callee.property.type !== 'Identifier' || init.callee.property.name !== metaName)) return false;
+ return true;
+}
diff --git a/src/compiler/index.ts b/src/compiler/index.ts
index db50abec8..c20596f65 100644
--- a/src/compiler/index.ts
+++ b/src/compiler/index.ts
@@ -12,7 +12,7 @@ import { createMarkdownHeadersCollector } from './markdown/micromark-collect-hea
import { encodeMarkdown } from './markdown/micromark-encode.js';
import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js';
import { transform } from './transform/index.js';
-import { codegen } from './codegen.js';
+import { codegen } from './codegen/index.js';
/** Return Astro internal import URL */
function internalImport(internalPath: string) {
@@ -132,6 +132,8 @@ async function __render(props, ...children) {
}
export default __render;
+${result.createCollection || ''}
+
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL.
export async function __renderPage({request, children, props}) {
diff --git a/src/frontend/render/svelte.ts b/src/frontend/render/svelte.ts
index fcb4ff24b..d3c11638d 100644
--- a/src/frontend/render/svelte.ts
+++ b/src/frontend/render/svelte.ts
@@ -12,7 +12,8 @@ const SvelteRenderer: ComponentRenderer<SvelteComponent> = {
render({ Component, root, props }) {
return `new ${Component}({
target: ${root},
- props: ${props}
+ props: ${props},
+ hydrate: true
})`;
},
};
diff --git a/src/runtime.ts b/src/runtime.ts
index 23aec0ac6..4614cb306 100644
--- a/src/runtime.ts
+++ b/src/runtime.ts
@@ -1,5 +1,5 @@
import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, SnowpackConfig } from 'snowpack';
-import type { AstroConfig, RuntimeMode } from './@types/astro';
+import type { AstroConfig, CollectionResult, CreateCollection, Params, RuntimeMode } from './@types/astro';
import type { LogOptions } from './logger';
import type { CompileError } from './parser/utils/error.js';
import { debug, info } from './logger.js';
@@ -78,6 +78,80 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
try {
const mod = await backendSnowpackRuntime.importModule(snowpackURL);
debug(logging, 'resolve', `${reqPath} -> ${snowpackURL}`);
+
+ // handle collection
+ let collection = {} as CollectionResult;
+ if (mod.exports.createCollection) {
+ const createCollection: CreateCollection = await mod.exports.createCollection();
+ for (const key of Object.keys(createCollection)) {
+ if (key !== 'data' && key !== 'routes' && key !== 'permalink' && key !== 'pageSize') {
+ throw new Error(`[createCollection] unknown option: "${key}"`);
+ }
+ }
+ let { data: loadData, routes, permalink, pageSize } = createCollection;
+ if (!pageSize) pageSize = 25; // can’t be 0
+ let currentParams: Params = {};
+
+ // params
+ if (routes || permalink) {
+ if (!routes || !permalink) {
+ throw new Error('createCollection() must have both routes and permalink options. Include both together, or omit both.');
+ }
+ let requestedParams = routes.find((p) => {
+ const baseURL = (permalink as any)({ params: p });
+ return baseURL === reqPath || `${baseURL}/${searchResult.currentPage || 1}` === reqPath;
+ });
+ if (requestedParams) {
+ currentParams = requestedParams;
+ collection.params = requestedParams;
+ }
+ }
+
+ let data: any[] = await loadData({ params: currentParams });
+
+ collection.start = 0;
+ collection.end = data.length - 1;
+ collection.total = data.length;
+ collection.page = { current: 1, size: pageSize, last: 1 };
+ collection.url = { current: reqPath };
+
+ // paginate
+ if (searchResult.currentPage) {
+ const start = (searchResult.currentPage - 1) * pageSize; // currentPage is 1-indexed
+ const end = Math.min(start + pageSize, data.length);
+
+ collection.start = start;
+ collection.end = end - 1;
+ collection.page.current = searchResult.currentPage;
+ collection.page.last = Math.ceil(data.length / pageSize);
+ // TODO: fix the .replace() hack
+ if (end < data.length) {
+ collection.url.next = collection.url.current.replace(/\d+$/, `${searchResult.currentPage + 1}`);
+ }
+ if (searchResult.currentPage > 1) {
+ collection.url.prev = collection.url.current.replace(/\d+$/, `${searchResult.currentPage - 1 || 1}`);
+ }
+
+ data = data.slice(start, end);
+ } else if (createCollection.pageSize) {
+ // TODO: fix bug where redirect doesn’t happen
+ // This happens because a pageSize is set, but the user isn’t on a paginated route. Redirect:
+ return {
+ statusCode: 301,
+ location: reqPath + '/1',
+ };
+ }
+
+ // if we’ve paginated too far, this is a 404
+ if (!data.length)
+ return {
+ statusCode: 404,
+ error: new Error('Not Found'),
+ };
+
+ collection.data = data;
+ }
+
let html = (await mod.exports.__renderPage({
request: {
host: fullurl.hostname,
@@ -85,7 +159,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
href: fullurl.toString(),
},
children: [],
- props: {},
+ props: { collection },
})) as string;
// inject styles
diff --git a/src/search.ts b/src/search.ts
index 963f8223b..1eda88365 100644
--- a/src/search.ts
+++ b/src/search.ts
@@ -1,4 +1,6 @@
import { existsSync } from 'fs';
+import path from 'path';
+import { fdir, PathsOutput } from 'fdir';
interface PageLocation {
fileURL: URL;
@@ -23,6 +25,7 @@ type SearchResult =
statusCode: 200;
location: PageLocation;
pathname: string;
+ currentPage?: number;
}
| {
statusCode: 301;
@@ -32,7 +35,8 @@ type SearchResult =
| {
statusCode: 404;
};
-/** searchForPage - look for astro or md pages */
+
+/** Given a URL, attempt to locate its source file (similar to Snowpack’s load()) */
export function searchForPage(url: URL, astroRoot: URL): SearchResult {
const reqPath = decodeURI(url.pathname);
const base = reqPath.substr(1);
@@ -72,7 +76,60 @@ export function searchForPage(url: URL, astroRoot: URL): SearchResult {
};
}
+ // Try and load collections (but only for non-extension files)
+ const hasExt = !!path.extname(reqPath);
+ if (!location && !hasExt) {
+ const collection = loadCollection(reqPath, astroRoot);
+ if (collection) {
+ return {
+ statusCode: 200,
+ location: collection.location,
+ pathname: reqPath,
+ currentPage: collection.currentPage,
+ };
+ }
+ }
+
return {
statusCode: 404,
};
}
+
+const crawler = new fdir();
+
+/** load a collection route */
+function loadCollection(url: string, astroRoot: URL): { currentPage?: number; location: PageLocation } | undefined {
+ const pages = (crawler.glob('**/*').crawl(path.join(astroRoot.pathname, 'pages')).sync() as PathsOutput).filter(
+ (filepath) => filepath.startsWith('$') || filepath.includes('/$')
+ );
+ for (const pageURL of pages) {
+ const reqURL = new RegExp('^/' + pageURL.replace(/\$([^/]+)\.astro/, '$1') + '/?(.*)');
+ const match = url.match(reqURL);
+ if (match) {
+ let currentPage: number | undefined;
+ if (match[1]) {
+ const segments = match[1].split('/').filter((s) => !!s);
+ if (segments.length) {
+ const last = segments.pop() as string;
+ if (parseInt(last, 10)) {
+ currentPage = parseInt(last, 10);
+ }
+ }
+ }
+ return {
+ location: {
+ fileURL: new URL(`./pages/${pageURL}`, astroRoot),
+ snowpackURL: `/_astro/pages/${pageURL}.js`,
+ },
+ currentPage,
+ };
+ }
+ }
+}
+
+/** convert a value to a number, if possible */
+function maybeNum(val: string): string | number {
+ const num = parseFloat(val);
+ if (num.toString() === val) return num;
+ return val;
+}