diff options
author | 2021-04-12 17:21:29 -0600 | |
---|---|---|
committer | 2021-04-12 17:21:29 -0600 | |
commit | 3639190b4e1b4c97836d448fa80a58aa45c823a7 (patch) | |
tree | 31160f051aa00cb0ebb820ab2061fb7d16956ee0 /src | |
parent | 687ff5bacd8c776e514f53c4b59c3a67274d3971 (diff) | |
download | astro-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.ts | 44 | ||||
-rw-r--r-- | src/compiler/codegen/content.ts | 78 | ||||
-rw-r--r-- | src/compiler/codegen/index.ts (renamed from src/compiler/codegen.ts) | 222 | ||||
-rw-r--r-- | src/compiler/codegen/utils.ts | 20 | ||||
-rw-r--r-- | src/compiler/index.ts | 4 | ||||
-rw-r--r-- | src/frontend/render/svelte.ts | 3 | ||||
-rw-r--r-- | src/runtime.ts | 78 | ||||
-rw-r--r-- | src/search.ts | 59 |
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; +} |