summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Fred K. Schott <fkschott@gmail.com> 2022-03-28 17:16:06 -0700
committerGravatar GitHub <noreply@github.com> 2022-03-28 17:16:06 -0700
commit4299ab303b0743349fbd01f85340bea61a1c16a8 (patch)
tree7012176c704d4f254fb2a602101fb7f0a01b8450
parent7d29feace103c0cf7c682634d4359b69338c2a1d (diff)
downloadastro-4299ab303b0743349fbd01f85340bea61a1c16a8.tar.gz
astro-4299ab303b0743349fbd01f85340bea61a1c16a8.tar.zst
astro-4299ab303b0743349fbd01f85340bea61a1c16a8.zip
New Markdown API (#2862)
* Implement new markdown plugin with deferred markdown rendering * feat: switch from `getContent()` fn to `<Content />` API * update types * Update packages/astro/src/@types/astro.ts Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com> * update types * Create forty-coins-attend.md Co-authored-by: Nate Moore <nate@skypack.dev> Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
-rw-r--r--.changeset/forty-coins-attend.md16
-rw-r--r--examples/blog-multiple-authors/src/components/PostPreview.astro11
-rw-r--r--examples/blog-multiple-authors/src/pages/authors/[author].astro20
-rw-r--r--examples/blog-multiple-authors/src/pages/index.astro15
-rw-r--r--examples/blog-multiple-authors/src/pages/posts/[...page].astro16
-rw-r--r--examples/blog/src/components/BlogPostPreview.astro6
-rw-r--r--examples/blog/src/pages/index.astro8
-rw-r--r--examples/portfolio/src/components/PortfolioPreview/index.jsx9
-rw-r--r--examples/portfolio/src/pages/index.astro2
-rw-r--r--examples/portfolio/src/pages/projects.astro10
-rw-r--r--packages/astro/env.d.ts2
-rw-r--r--packages/astro/package.json2
-rw-r--r--packages/astro/src/@types/astro.ts29
-rw-r--r--packages/astro/src/core/render/dev/css.ts7
-rw-r--r--packages/astro/src/runtime/server/index.ts45
-rw-r--r--packages/astro/src/vite-plugin-astro-postprocess/index.ts97
-rw-r--r--packages/astro/src/vite-plugin-markdown/index.ts94
-rw-r--r--packages/astro/test/astro-global.test.js2
-rw-r--r--packages/astro/test/fixtures/astro-global/src/pages/posts/[page].astro6
-rw-r--r--packages/astro/test/fixtures/astro-pagination/src/pages/posts/[slug]/[page].astro6
-rw-r--r--packages/astro/test/fixtures/astro-pagination/src/pages/posts/named-root-page/[page].astro2
-rw-r--r--packages/astro/test/fixtures/astro-pagination/src/pages/posts/optional-root-page/[...page].astro2
-rw-r--r--packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episodes/[...page].astro30
-rw-r--r--packages/astro/test/fixtures/debug-component/src/pages/posts/[slug].astro8
-rw-r--r--packages/astro/test/fixtures/static build/src/pages/index.astro2
-rw-r--r--packages/astro/test/static-build.test.js2
-rw-r--r--pnpm-lock.yaml21
27 files changed, 277 insertions, 193 deletions
diff --git a/.changeset/forty-coins-attend.md b/.changeset/forty-coins-attend.md
new file mode 100644
index 000000000..467e520fd
--- /dev/null
+++ b/.changeset/forty-coins-attend.md
@@ -0,0 +1,16 @@
+---
+"astro": minor
+---
+
+Implement RFC [#0017](https://github.com/withastro/rfcs/blob/main/proposals/0017-markdown-content-redesign.md)
+
+- New Markdown API
+- New `Astro.glob()` API
+- **BREAKING CHANGE:** Removed `Astro.fetchContent()` (replaced by `Astro.glob()`)
+
+```diff
+// v0.25
+- let allPosts = Astro.fetchContent('./posts/*.md');
+// v0.26+
++ let allPosts = await Astro.glob('./posts/*.md');
+```
diff --git a/examples/blog-multiple-authors/src/components/PostPreview.astro b/examples/blog-multiple-authors/src/components/PostPreview.astro
index 81e80ba6c..5a9808348 100644
--- a/examples/blog-multiple-authors/src/components/PostPreview.astro
+++ b/examples/blog-multiple-authors/src/components/PostPreview.astro
@@ -4,6 +4,7 @@ export interface Props {
author: string;
}
const { post, author } = Astro.props;
+const { frontmatter } = post;
function formatDate(date) {
return new Date(date).toUTCString().replace(/(\d\d\d\d) .*/, '$1'); // remove everything after YYYY
@@ -12,12 +13,12 @@ function formatDate(date) {
<article class="post">
<div class="data">
- <h2>{post.title}</h2>
- <a class="author" href={`/authors/${post.author}`}>{author.name}</a>
- <time class="date" datetime={post.date}>{formatDate(post.date)}</time>
+ <h2>{frontmatter.title}</h2>
+ <a class="author" href={`/authors/${frontmatter.author}`}>{author.name}</a>
+ <time class="date" datetime={frontmatter.date}>{formatDate(frontmatter.date)}</time>
<p class="description">
- {post.description}
- <a class="link" href={post.url} aria-label={`Read ${post.title}`}>Read</a>
+ {frontmatter.description}
+ <a class="link" href={post.url} aria-label={`Read ${frontmatter.title}`}>Read</a>
</p>
</div>
</article>
diff --git a/examples/blog-multiple-authors/src/pages/authors/[author].astro b/examples/blog-multiple-authors/src/pages/authors/[author].astro
index 21aab27a5..c2ba49d39 100644
--- a/examples/blog-multiple-authors/src/pages/authors/[author].astro
+++ b/examples/blog-multiple-authors/src/pages/authors/[author].astro
@@ -2,36 +2,28 @@
import MainHead from '../../components/MainHead.astro';
import Nav from '../../components/Nav.astro';
import PostPreview from '../../components/PostPreview.astro';
-import Pagination from '../../components/Pagination.astro';
import authorData from '../../data/authors.json';
-export function getStaticPaths() {
- const allPosts = Astro.fetchContent<MarkdownFrontmatter>('../post/*.md');
- let allAuthorsUnique = [...new Set(allPosts.map((p) => p.author))];
+export async function getStaticPaths() {
+ const allPosts = await Astro.glob('../post/*.md');
+ let allAuthorsUnique = [...new Set(allPosts.map((p) => p.frontmatter.author))];
return allAuthorsUnique.map((author) => ({ params: { author }, props: { allPosts } }));
}
-interface MarkdownFrontmatter {
- date: number;
- description: string;
- title: string;
- author: string;
-}
-
const { allPosts } = Astro.props;
const { params, canonicalURL } = Astro.request;
const title = 'Don’s Blog';
const description = 'An example blog on Astro';
/** filter posts by author, sort by date */
-const posts = allPosts.filter((post) => post.author === params.author).sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
-const author = authorData[posts[0].author];
+const posts = allPosts.filter((post) => post.frontmatter.author === params.author).sort((a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf());
+const author = authorData[posts[0].frontmatter.author];
---
<html lang="en">
<head>
<title>{title}</title>
- <MainHead {title} {description} image={posts[0].image} canonicalURL={canonicalURL.toString()} />
+ <MainHead {title} {description} image={posts[0].frontmatter.image} canonicalURL={canonicalURL.toString()} />
<style lang="scss">
.title {
diff --git a/examples/blog-multiple-authors/src/pages/index.astro b/examples/blog-multiple-authors/src/pages/index.astro
index 8ad01c190..518424b99 100644
--- a/examples/blog-multiple-authors/src/pages/index.astro
+++ b/examples/blog-multiple-authors/src/pages/index.astro
@@ -6,12 +6,6 @@ import PostPreview from '../components/PostPreview.astro';
import Pagination from '../components/Pagination.astro';
import authorData from '../data/authors.json';
-interface MarkdownFrontmatter {
- date: number;
- image: string;
- author: string;
-}
-
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
@@ -21,10 +15,9 @@ let description = 'An example blog on Astro';
let canonicalURL = Astro.request.canonicalURL;
// Data Fetching: List all Markdown posts in the repo.
-let allPosts = Astro.fetchContent<MarkdownFrontmatter>('./post/*.md');
-allPosts.sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
+let allPosts = await Astro.glob('./post/*.md');
+allPosts.sort((a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf());
let firstPage = allPosts.slice(0, 2);
-
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
---
@@ -32,14 +25,14 @@ let firstPage = allPosts.slice(0, 2);
<html lang="en">
<head>
<title>{title}</title>
- <MainHead {title} {description} image={allPosts[0].image} {canonicalURL} />
+ <MainHead {title} {description} image={allPosts[0].frontmatter.image} {canonicalURL} />
</head>
<body>
<Nav {title} />
<main class="wrapper">
- {allPosts.map((post) => <PostPreview post={post} author={authorData[post.author]} />)}
+ {allPosts.map((post) => <PostPreview post={post} author={authorData[post.frontmatter.author]} />)}
</main>
<footer>
diff --git a/examples/blog-multiple-authors/src/pages/posts/[...page].astro b/examples/blog-multiple-authors/src/pages/posts/[...page].astro
index d0f95ce5b..da9b06fc5 100644
--- a/examples/blog-multiple-authors/src/pages/posts/[...page].astro
+++ b/examples/blog-multiple-authors/src/pages/posts/[...page].astro
@@ -6,8 +6,8 @@ import Pagination from '../../components/Pagination.astro';
import authorData from '../../data/authors.json';
export async function getStaticPaths({ paginate, rss }) {
- const allPosts = Astro.fetchContent<MarkdownFrontmatter>('../post/*.md');
- const sortedPosts = allPosts.sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
+ const allPosts = await Astro.glob('../post/*.md');
+ const sortedPosts = allPosts.sort((a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf());
// Generate an RSS feed from this collection of posts.
// NOTE: This is disabled by default, since it requires `buildOptions.site` to be set in your "astro.config.mjs" file.
@@ -31,21 +31,13 @@ export async function getStaticPaths({ paginate, rss }) {
let title = 'Don’s Blog';
let description = 'An example blog on Astro';
let canonicalURL = Astro.request.canonicalURL;
-
-// collection
-interface MarkdownFrontmatter {
- date: number;
- description: string;
- title: string;
-}
-
const { page } = Astro.props;
---
<html lang="en">
<head>
<title>{title}</title>
- <MainHead {title} {description} image={page.data[0].image} canonicalURL={canonicalURL.toString()} prev={page.url.prev} next={page.url.next} />
+ <MainHead {title} {description} image={page.data[0].frontmatter.image} canonicalURL={canonicalURL.toString()} prev={page.url.prev} next={page.url.next} />
<style lang="scss">
.title {
@@ -70,7 +62,7 @@ const { page } = Astro.props;
<main class="wrapper">
<h2 class="title">All Posts</h2>
<small class="count">{page.start + 1}–{page.end + 1} of {page.total}</small>
- {page.data.map((post) => <PostPreview post={post} author={authorData[post.author]} />)}
+ {page.data.map((post) => <PostPreview post={post} author={authorData[post.frontmatter.author]} />)}
</main>
<footer>
diff --git a/examples/blog/src/components/BlogPostPreview.astro b/examples/blog/src/components/BlogPostPreview.astro
index 4841d3a65..f935ff8b2 100644
--- a/examples/blog/src/components/BlogPostPreview.astro
+++ b/examples/blog/src/components/BlogPostPreview.astro
@@ -8,10 +8,10 @@ const { post } = Astro.props;
<article class="post-preview">
<header>
- <p class="publish-date">{post.publishDate}</p>
- <a href={post.url}><h1 class="title">{post.title}</h1></a>
+ <p class="publish-date">{post.frontmatter.publishDate}</p>
+ <a href={post.url}><h1 class="title">{post.frontmatter.title}</h1></a>
</header>
- <p>{post.description}</p>
+ <p>{post.frontmatter.description}</p>
<a href={post.url}>Read more</a>
</article>
diff --git a/examples/blog/src/pages/index.astro b/examples/blog/src/pages/index.astro
index c7bc3ea32..1e1264533 100644
--- a/examples/blog/src/pages/index.astro
+++ b/examples/blog/src/pages/index.astro
@@ -4,10 +4,6 @@ import BaseHead from '../components/BaseHead.astro';
import BlogHeader from '../components/BlogHeader.astro';
import BlogPostPreview from '../components/BlogPostPreview.astro';
-interface MarkdownFrontmatter {
- publishDate: number;
-}
-
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
@@ -18,8 +14,8 @@ let permalink = 'https://example.com/';
// Data Fetching: List all Markdown posts in the repo.
-let allPosts = await Astro.fetchContent('./posts/*.md');
-allPosts = allPosts.sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
+let allPosts = await Astro.glob('./posts/*.md');
+allPosts = allPosts.sort((a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf());
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
diff --git a/examples/portfolio/src/components/PortfolioPreview/index.jsx b/examples/portfolio/src/components/PortfolioPreview/index.jsx
index 6957e5884..4f1627604 100644
--- a/examples/portfolio/src/components/PortfolioPreview/index.jsx
+++ b/examples/portfolio/src/components/PortfolioPreview/index.jsx
@@ -2,16 +2,17 @@ import { h } from 'preact';
import Styles from './styles.module.scss';
function PortfolioPreview({ project }) {
+ const { frontmatter } = project;
return (
<div className={Styles.card}>
- <div className={Styles.titleCard} style={`background-image:url(${project.img})`}>
- <h1 className={Styles.title}>{project.title}</h1>
+ <div className={Styles.titleCard} style={`background-image:url(${frontmatter.img})`}>
+ <h1 className={Styles.title}>{frontmatter.title}</h1>
</div>
<div className="pa3">
- <p className={`${Styles.desc} mt0 mb2`}>{project.description}</p>
+ <p className={`${Styles.desc} mt0 mb2`}>{frontmatter.description}</p>
<div className={Styles.tags}>
Tagged:
- {project.tags.map((t) => (
+ {frontmatter.tags.map((t) => (
<div className={Styles.tag} data-tag={t}>
{t}
</div>
diff --git a/examples/portfolio/src/pages/index.astro b/examples/portfolio/src/pages/index.astro
index ce11119b5..d8a9efcc2 100644
--- a/examples/portfolio/src/pages/index.astro
+++ b/examples/portfolio/src/pages/index.astro
@@ -7,7 +7,7 @@ import Footer from '../components/Footer/index.jsx';
import PortfolioPreview from '../components/PortfolioPreview/index.jsx';
// Data Fetching: List all Markdown posts in the repo.
-const projects = Astro.fetchContent('./project/**/*.md');
+const projects = await Astro.glob('./project/**/*.md');
const featuredProject = projects[0];
// Full Astro Component Syntax:
diff --git a/examples/portfolio/src/pages/projects.astro b/examples/portfolio/src/pages/projects.astro
index 991c254bc..1aa05e07f 100644
--- a/examples/portfolio/src/pages/projects.astro
+++ b/examples/portfolio/src/pages/projects.astro
@@ -4,13 +4,9 @@ import Footer from '../components/Footer/index.jsx';
import Nav from '../components/Nav/index.jsx';
import PortfolioPreview from '../components/PortfolioPreview/index.jsx';
-interface MarkdownFrontmatter {
- publishDate: number;
-}
-
-const projects = Astro.fetchContent<MarkdownFrontmatter>('./project/**/*.md')
- .filter(({ publishDate }) => !!publishDate)
- .sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
+const projects = (await Astro.glob('./project/**/*.md'))
+ .filter(({ frontmatter }) => !!frontmatter.publishDate)
+ .sort((a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf());
---
<html lang="en">
diff --git a/packages/astro/env.d.ts b/packages/astro/env.d.ts
index 88a4bcce3..ebb416dd2 100644
--- a/packages/astro/env.d.ts
+++ b/packages/astro/env.d.ts
@@ -1,6 +1,6 @@
/// <reference types="vite/client" />
-type Astro = import('./dist/types/@types/astro').AstroGlobal;
+type Astro = import('astro').AstroGlobal;
// We duplicate the description here because editors won't show the JSDoc comment from the imported type (but will for its properties, ex: Astro.request will show the AstroGlobal.request description)
/**
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 9e0a508bf..579642a1d 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -85,6 +85,7 @@
"@proload/core": "^0.2.2",
"@proload/plugin-tsm": "^0.1.1",
"@web/parse5-utils": "^1.3.0",
+ "ast-types": "^0.14.2",
"boxen": "^6.2.1",
"ci-info": "^3.3.0",
"common-ancestor-path": "^1.0.1",
@@ -112,6 +113,7 @@
"preferred-pm": "^3.0.3",
"prismjs": "^1.27.0",
"prompts": "^2.4.2",
+ "recast": "^0.20.5",
"rehype-slug": "^5.0.1",
"resolve": "^1.22.0",
"rollup": "^2.70.1",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 6e3638e52..c82d2d7d7 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1,7 +1,7 @@
import type { AddressInfo } from 'net';
import type * as babel from '@babel/core';
import type * as vite from 'vite';
-import type { z } from 'zod';
+import { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type { AstroRequest } from '../core/render/request';
@@ -60,11 +60,15 @@ export interface AstroGlobal extends AstroGlobalPartial {
}
export interface AstroGlobalPartial {
- fetchContent<T = any>(globStr: string): Promise<FetchContentResult<T>[]>;
/**
* @deprecated since version 0.24. See the {@link https://astro.build/deprecated/resolve upgrade guide} for more details.
*/
resolve: (path: string) => string;
+ /** @deprecated Use `Astro.glob()` instead. */
+ fetchContent(globStr: string): Promise<any[]>;
+ glob(globStr: `${any}.astro`): Promise<ComponentInstance[]>;
+ glob<T extends Record<string, any>>(globStr: `${any}.md`): Promise<MarkdownInstance<T>[]>;
+ glob<T extends Record<string, any>>(globStr: string): Promise<T[]>;
site: URL;
}
@@ -508,20 +512,13 @@ export interface ComponentInstance {
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}
-/**
- * Astro.fetchContent() result
- * Docs: https://docs.astro.build/reference/api-reference/#astrofetchcontent
- */
-export type FetchContentResult<T> = FetchContentResultBase & T;
-
-export type FetchContentResultBase = {
- astro: {
- headers: string[];
- source: string;
- html: string;
- };
- url: string;
-};
+export interface MarkdownInstance<T extends Record<string, any>> {
+ frontmatter: T;
+ file: string;
+ url: string | undefined;
+ Content: AstroComponentFactory;
+ getHeaders(): Promise<{ depth: number, slug: string, text: string }[]>;
+}
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string | null) => void>;
diff --git a/packages/astro/src/core/render/dev/css.ts b/packages/astro/src/core/render/dev/css.ts
index 82141c5cb..baded71a9 100644
--- a/packages/astro/src/core/render/dev/css.ts
+++ b/packages/astro/src/core/render/dev/css.ts
@@ -29,10 +29,15 @@ export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer):
: // Otherwise, you are following an import in the module import tree.
// You are safe to use getModuleById() here because Vite has already
// resolved the correct `id` for you, by creating the import you followed here.
- new Set([viteServer.moduleGraph.getModuleById(id)!]);
+ new Set([viteServer.moduleGraph.getModuleById(id)]);
// Collect all imported modules for the module(s).
for (const entry of moduleEntriesForId) {
+ // Handle this in case an module entries weren't found for ID
+ // This seems possible with some virtual IDs (ex: `astro:markdown/*.md`)
+ if (!entry) {
+ continue;
+ }
if (id === entry.id) {
scanned.add(id);
for (const importedModule of entry.importedModules) {
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index d977219ac..25d41f541 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -277,34 +277,25 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
}
/** Create the Astro.fetchContent() runtime function. */
-function createFetchContentFn(url: URL, site: URL) {
- let sitePathname = site.pathname;
- const fetchContent = (importMetaGlobResult: Record<string, any>) => {
- let allEntries = [...Object.entries(importMetaGlobResult)];
+function createDeprecatedFetchContentFn() {
+ return () => {
+ throw new Error('Deprecated: Astro.fetchContent() has been replaced with Astro.glob().');
+ };
+}
+
+/** Create the Astro.glob() runtime function. */
+function createAstroGlobFn() {
+ const globHandler = (importMetaGlobResult: Record<string, any>, globValue: () => any) => {
+ let allEntries = [...Object.values(importMetaGlobResult)];
if (allEntries.length === 0) {
- throw new Error(`[${url.pathname}] Astro.fetchContent() no matches found.`);
+ throw new Error(`Astro.glob(${JSON.stringify(globValue())}) - no matches found.`);
}
- return allEntries
- .map(([spec, mod]) => {
- // Only return Markdown files for now.
- if (!mod.frontmatter) {
- return;
- }
- const urlSpec = new URL(spec, url).pathname;
- return {
- ...mod.frontmatter,
- Content: mod.default,
- content: mod.metadata,
- file: new URL(spec, url),
- url: urlSpec.includes('/pages/') ? urlSpec.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '') : undefined,
- };
- })
- .filter(Boolean);
+ // Map over the `import()` promises, calling to load them.
+ return Promise.all(allEntries.map(fn => fn()));
};
- // This has to be cast because the type of fetchContent is the type of the function
- // that receives the import.meta.glob result, but the user is using it as
- // another type.
- return fetchContent as unknown as AstroGlobalPartial['fetchContent'];
+ // Cast the return type because the argument that the user sees (string) is different from the argument
+ // that the runtime sees post-compiler (Record<string, Module>).
+ return globHandler as unknown as AstroGlobalPartial['glob'];
}
// This is used to create the top-level Astro global; the one that you can use
@@ -313,10 +304,10 @@ export function createAstro(filePathname: string, _site: string, projectRootStr:
const site = new URL(_site);
const url = new URL(filePathname, site);
const projectRoot = new URL(projectRootStr);
- const fetchContent = createFetchContentFn(url, site);
return {
site,
- fetchContent,
+ fetchContent: createDeprecatedFetchContentFn(),
+ glob: createAstroGlobFn(),
// INVESTIGATE is there a use-case for multi args?
resolve(...segments: string[]) {
let resolved = segments.reduce((u, segment) => new URL(segment, u), url).pathname;
diff --git a/packages/astro/src/vite-plugin-astro-postprocess/index.ts b/packages/astro/src/vite-plugin-astro-postprocess/index.ts
index fc40f9891..c0e3ad551 100644
--- a/packages/astro/src/vite-plugin-astro-postprocess/index.ts
+++ b/packages/astro/src/vite-plugin-astro-postprocess/index.ts
@@ -1,10 +1,12 @@
-import type * as t from '@babel/types';
+import { parse as babelParser } from '@babel/parser';
+import type { ArrowFunctionExpressionKind, CallExpressionKind, StringLiteralKind } from 'ast-types/gen/kinds';
+import type { NodePath } from 'ast-types/lib/node-path';
+import { parse, print, types, visit } from "recast";
import type { Plugin } from 'vite';
import type { AstroConfig } from '../@types/astro';
-import * as babelTraverse from '@babel/traverse';
-import * as babel from '@babel/core';
-
+// Check for `Astro.glob()`. Be very forgiving of whitespace. False positives are okay.
+const ASTRO_GLOB_REGEX = /Astro2?\s*\.\s*glob\s*\(/;
interface AstroPluginOptions {
config: AstroConfig;
}
@@ -21,55 +23,56 @@ export default function astro({ config }: AstroPluginOptions): Plugin {
return null;
}
- // Optimization: only run on a probably match
- // Open this up if need for post-pass extends past fetchContent
- if (!code.includes('fetchContent')) {
+ // Optimization: Detect usage with a quick string match.
+ // Only perform the transform if this function is found
+ if (!ASTRO_GLOB_REGEX.test(code)) {
return null;
}
- // Handle the second-pass JS AST Traversal
- const result = await babel.transformAsync(code, {
- sourceType: 'module',
- sourceMaps: true,
- plugins: [
- () => {
- return {
- visitor: {
- StringLiteral(path: babelTraverse.NodePath<t.StringLiteral>) {
- if (
- path.parent.type !== 'CallExpression' ||
- path.parent.callee.type !== 'MemberExpression' ||
- !validAstroGlobalNames.has((path.parent.callee.object as any).name) ||
- (path.parent.callee.property as any).name !== 'fetchContent'
- ) {
- return;
- }
- const { value } = path.node;
- if (/[a-z]\:\/\//.test(value)) {
- return;
- }
- path.replaceWith({
- type: 'CallExpression',
- callee: {
- type: 'MemberExpression',
- object: { type: 'MetaProperty', meta: { type: 'Identifier', name: 'import' }, property: { type: 'Identifier', name: 'meta' } },
- property: { type: 'Identifier', name: 'globEager' },
- computed: false,
- },
- arguments: [path.node],
- } as any);
- },
- },
- };
- },
- ],
+ const ast = parse(code, {
+ // We need to use the babel parser because `import.meta.hot` is not
+ // supported by esprima (default parser). In the future, we should
+ // experiment with other parsers if Babel is too slow or heavy.
+ parser: { parse: babelParser },
});
- // Undocumented baby behavior, but possible according to Babel types.
- if (!result || !result.code) {
- return null;
- }
+ visit(ast, {
+ visitCallExpression: function (path) {
+ // Filter out anything that isn't `Astro.glob()` or `Astro2.glob()`
+ if (
+ !types.namedTypes.MemberExpression.check(path.node.callee) ||
+ !types.namedTypes.Identifier.check(path.node.callee.property) ||
+ !(path.node.callee.property.name === 'glob') ||
+ !types.namedTypes.Identifier.check(path.node.callee.object) ||
+ !(path.node.callee.object.name === 'Astro' || path.node.callee.object.name === 'Astro2')
+ ) {
+ this.traverse(path);
+ return;
+ }
+
+ // Wrap the `Astro.glob()` argument with `import.meta.glob`.
+ const argsPath = path.get('arguments', 0) as NodePath;
+ const args = argsPath.value;
+ argsPath.replace({
+ type: 'CallExpression',
+ callee: {
+ type: 'MemberExpression',
+ object: { type: 'MetaProperty', meta: { type: 'Identifier', name: 'import' }, property: { type: 'Identifier', name: 'meta' } },
+ property: { type: 'Identifier', name: 'glob' },
+ computed: false,
+ },
+ arguments: [args],
+ } as CallExpressionKind,
+ {
+ type: 'ArrowFunctionExpression',
+ body: args,
+ params: []
+ } as ArrowFunctionExpressionKind);
+ return false;
+ },
+ });
+ const result = print(ast);
return { code: result.code, map: result.map };
},
};
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index c3c75b93d..ebe578783 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -2,14 +2,20 @@ import { transform } from '@astrojs/compiler';
import ancestor from 'common-ancestor-path';
import esbuild from 'esbuild';
import fs from 'fs';
+import matter from 'gray-matter';
+import { fileURLToPath } from 'url';
import type { Plugin } from 'vite';
import type { AstroConfig } from '../@types/astro';
import { PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
+import { virtualModuleId as pagesVirtualModuleId } from '../core/build/vite-plugin-pages.js';
interface AstroPluginOptions {
config: AstroConfig;
}
+const VIRTUAL_MODULE_ID_PREFIX = 'astro:markdown';
+const VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID_PREFIX;
+
// TODO: Clean up some of the shared logic between this Markdown plugin and the Astro plugin.
// Both end up connecting a `load()` hook to the Astro compiler, and share some copy-paste
// logic in how that is done.
@@ -23,14 +29,88 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
return filename;
}
+ // Weird Vite behavior: Vite seems to use a fake "index.html" importer when you
+ // have `enforce: pre`. This can probably be removed once the vite issue is fixed.
+ // see: https://github.com/vitejs/vite/issues/5981
+ const fakeRootImporter = fileURLToPath(new URL('index.html', config.projectRoot));
+ function isRootImport(importer: string | undefined) {
+ if (!importer) {
+ return true;
+ }
+ if (importer === fakeRootImporter) {
+ return true;
+ }
+ if (importer === '\0' + pagesVirtualModuleId) {
+ return true;
+ }
+ return false;
+ }
+
return {
name: 'astro:markdown',
- enforce: 'pre', // run transforms before other plugins can
+ enforce: 'pre',
+ async resolveId(id, importer, options) {
+ // Resolve virtual modules as-is.
+ if (id.startsWith(VIRTUAL_MODULE_ID)) {
+ return id;
+ }
+ // Resolve any .md files with the `?content` cache buster. This should only come from
+ // an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
+ // Unclear if this is expected or if cache busting is just working around a Vite bug.
+ if (id.endsWith('.md?content')) {
+ const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
+ return resolvedId?.id.replace('?content', '');
+ }
+ // If the markdown file is imported from another file via ESM, resolve a JS representation
+ // that defers the markdown -> HTML rendering until it is needed. This is especially useful
+ // when fetching and then filtering many markdown files, like with import.meta.glob() or Astro.glob().
+ // Otherwise, resolve directly to the actual component.
+ if (id.endsWith('.md') && !isRootImport(importer)) {
+ const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
+ if (resolvedId) {
+ return VIRTUAL_MODULE_ID + resolvedId.id;
+ }
+ }
+ // In all other cases, we do nothing and rely on normal Vite resolution.
+ return undefined;
+ },
async load(id) {
+ // A markdown file has been imported via ESM!
+ // Return the file's JS representation, including all Markdown
+ // frontmatter and a deferred `import() of the compiled markdown content.
+ if (id.startsWith(VIRTUAL_MODULE_ID)) {
+ const sitePathname = config.buildOptions.site ? new URL(config.buildOptions.site).pathname : '/';
+ const fileId = id.substring(VIRTUAL_MODULE_ID.length);
+ const fileUrl = fileId.includes('/pages/') ? fileId.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '') : undefined;
+ const source = await fs.promises.readFile(fileId, 'utf8');
+ const { data: frontmatter } = matter(source);
+ return {
+ code: `
+ // Static
+ export const frontmatter = ${JSON.stringify(frontmatter)};
+ export const file = ${JSON.stringify(fileId)};
+ export const url = ${JSON.stringify(fileUrl)};
+
+ // Deferred
+ export default async function load() {
+ return (await import(${JSON.stringify(fileId + '?content')}));
+ };
+ export function Content(...args) {
+ return load().then((m) => m.default(...args))
+ }
+ Content.isAstroComponentFactory = true;
+ export function getHeaders() {
+ return load().then((m) => m.metadata.headers)
+ };`,
+ map: null,
+ };
+ }
+
+ // A markdown file is being rendered! This markdown file was either imported
+ // directly as a page in Vite, or it was a deferred render from a JS module.
+ // This returns the compiled markdown -> astro component that renders to HTML.
if (id.endsWith('.md')) {
const source = await fs.promises.readFile(id, 'utf8');
-
- // Transform from `.md` to valid `.astro`
let render = config.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(render)) {
@@ -40,8 +120,6 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
if (typeof render === 'string') {
({ default: render } = await import(render));
}
- let renderResult = await render(source, renderOpts);
- let { frontmatter, metadata, code: astroResult } = renderResult;
const filename = normalizeFilename(id);
const fileUrl = new URL(`file://${filename}`);
@@ -49,6 +127,9 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const hasInjectedScript = isPage && config._ctx.scripts.some((s) => s.stage === 'page-ssr');
// Extract special frontmatter keys
+ const { data: frontmatter, content: markdownContent } = matter(source);
+ let renderResult = await render(markdownContent, renderOpts);
+ let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
content.astro = metadata;
const prelude = `---
@@ -83,8 +164,7 @@ export const frontmatter = ${JSON.stringify(content)};
${tsResult}`;
// Compile from `.ts` to `.js`
- const { code, map } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
-
+ const { code } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: false, sourcefile: id });
return {
code,
map: null,
diff --git a/packages/astro/test/astro-global.test.js b/packages/astro/test/astro-global.test.js
index d46c994ec..eda1ce438 100644
--- a/packages/astro/test/astro-global.test.js
+++ b/packages/astro/test/astro-global.test.js
@@ -48,7 +48,7 @@ describe('Astro.*', () => {
expect($('#site').attr('href')).to.equal('https://mysite.dev/blog/');
});
- it('Astro.fetchContent() returns the correct "url" property, including buildOptions.site subpath', async () => {
+ it('Astro.glob() correctly returns an array of all posts', async () => {
const html = await fixture.readFile('/posts/1/index.html');
const $ = cheerio.load(html);
expect($('.post-url').attr('href')).to.equal('/blog/post/post-2');
diff --git a/packages/astro/test/fixtures/astro-global/src/pages/posts/[page].astro b/packages/astro/test/fixtures/astro-global/src/pages/posts/[page].astro
index e684161e6..1cd1c6881 100644
--- a/packages/astro/test/fixtures/astro-global/src/pages/posts/[page].astro
+++ b/packages/astro/test/fixtures/astro-global/src/pages/posts/[page].astro
@@ -1,6 +1,6 @@
---
-export function getStaticPaths({paginate}) {
- const data = Astro.fetchContent('../post/*.md');
+export async function getStaticPaths({paginate}) {
+ const data = await Astro.glob('../post/*.md');
return paginate(data, {pageSize: 1});
}
const { page } = Astro.props;
@@ -15,7 +15,7 @@ const { params, canonicalURL} = Astro.request;
<body>
{page.data.map((data) => (
<div>
- <h1>{data.title}</h1>
+ <h1>{data.frontmatter.title}</h1>
<a class="post-url" href={data.url}>Read</a>
</div>
))}
diff --git a/packages/astro/test/fixtures/astro-pagination/src/pages/posts/[slug]/[page].astro b/packages/astro/test/fixtures/astro-pagination/src/pages/posts/[slug]/[page].astro
index b3dc4be87..1b701517f 100644
--- a/packages/astro/test/fixtures/astro-pagination/src/pages/posts/[slug]/[page].astro
+++ b/packages/astro/test/fixtures/astro-pagination/src/pages/posts/[slug]/[page].astro
@@ -1,8 +1,8 @@
---
-export function getStaticPaths({paginate}) {
- const allPosts = Astro.fetchContent('../../post/*.md');
+export async function getStaticPaths({paginate}) {
+ const allPosts = await Astro.glob('../../post/*.md');
return ['red', 'blue'].map((filter) => {
- const filteredPosts = allPosts.filter((post) => post.tag === filter);
+ const filteredPosts = allPosts.filter((post) => post.frontmatter.tag === filter);
return paginate(filteredPosts, {
params: { slug: filter },
props: { filter },
diff --git a/packages/astro/test/fixtures/astro-pagination/src/pages/posts/named-root-page/[page].astro b/packages/astro/test/fixtures/astro-pagination/src/pages/posts/named-root-page/[page].astro
index d70f0673c..fef4cc887 100644
--- a/packages/astro/test/fixtures/astro-pagination/src/pages/posts/named-root-page/[page].astro
+++ b/packages/astro/test/fixtures/astro-pagination/src/pages/posts/named-root-page/[page].astro
@@ -1,6 +1,6 @@
---
export async function getStaticPaths({paginate}) {
- const data = Astro.fetchContent('../../post/*.md');
+ const data = await Astro.glob('../../post/*.md');
return paginate(data, {pageSize: 1});
}
const { page } = Astro.props;
diff --git a/packages/astro/test/fixtures/astro-pagination/src/pages/posts/optional-root-page/[...page].astro b/packages/astro/test/fixtures/astro-pagination/src/pages/posts/optional-root-page/[...page].astro
index d70f0673c..fef4cc887 100644
--- a/packages/astro/test/fixtures/astro-pagination/src/pages/posts/optional-root-page/[...page].astro
+++ b/packages/astro/test/fixtures/astro-pagination/src/pages/posts/optional-root-page/[...page].astro
@@ -1,6 +1,6 @@
---
export async function getStaticPaths({paginate}) {
- const data = Astro.fetchContent('../../post/*.md');
+ const data = await Astro.glob('../../post/*.md');
return paginate(data, {pageSize: 1});
}
const { page } = Astro.props;
diff --git a/packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episodes/[...page].astro b/packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episodes/[...page].astro
index 0c0c676ee..3732c4ba3 100644
--- a/packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episodes/[...page].astro
+++ b/packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episodes/[...page].astro
@@ -1,6 +1,6 @@
---
-export function getStaticPaths({paginate, rss}) {
- const episodes = Astro.fetchContent('../episode/*.md').sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
+export async function getStaticPaths({paginate, rss}) {
+ const episodes = (await Astro.glob('../episode/*.md')).sort((a, b) => new Date(b.frontmatter.pubDate) - new Date(a.frontmatter.pubDate));
rss({
title: 'MF Doomcast',
description: 'The podcast about the things you find on a picnic, or at a picnic table',
@@ -11,13 +11,13 @@ export function getStaticPaths({paginate, rss}) {
customData: `<language>en-us</language>` +
`<itunes:author>MF Doom</itunes:author>`,
items: episodes.map((episode) => ({
- title: episode.title,
+ title: episode.frontmatter.title,
link: episode.url,
- description: episode.description,
- pubDate: episode.pubDate + 'Z',
- customData: `<itunes:episodeType>${episode.type}</itunes:episodeType>` +
- `<itunes:duration>${episode.duration}</itunes:duration>` +
- `<itunes:explicit>${episode.explicit || false}</itunes:explicit>`,
+ description: episode.frontmatter.description,
+ pubDate: episode.frontmatter.pubDate + 'Z',
+ customData: `<itunes:episodeType>${episode.frontmatter.type}</itunes:episodeType>` +
+ `<itunes:duration>${episode.frontmatter.duration}</itunes:duration>` +
+ `<itunes:explicit>${episode.frontmatter.explicit || false}</itunes:explicit>`,
})),
dest: '/custom/feed.xml',
});
@@ -31,13 +31,13 @@ export function getStaticPaths({paginate, rss}) {
customData: `<language>en-us</language>` +
`<itunes:author>MF Doom</itunes:author>`,
items: episodes.map((episode) => ({
- title: episode.title,
+ title: episode.frontmatter.title,
link: `https://example.com${episode.url}/`,
- description: episode.description,
- pubDate: episode.pubDate + 'Z',
- customData: `<itunes:episodeType>${episode.type}</itunes:episodeType>` +
- `<itunes:duration>${episode.duration}</itunes:duration>` +
- `<itunes:explicit>${episode.explicit || false}</itunes:explicit>`,
+ description: episode.frontmatter.description,
+ pubDate: episode.frontmatter.pubDate + 'Z',
+ customData: `<itunes:episodeType>${episode.frontmatter.type}</itunes:episodeType>` +
+ `<itunes:duration>${episode.frontmatter.duration}</itunes:duration>` +
+ `<itunes:explicit>${episode.frontmatter.explicit || false}</itunes:explicit>`,
})),
dest: '/custom/feed-pregenerated-urls.xml',
});
@@ -53,6 +53,6 @@ const { page } = Astro.props;
<link rel="alternate" type="application/rss+2.0" href="/rss.xml" />
</head>
<body>
- {page.data.map((ep) => (<li>{ep.title}</li>))}
+ {page.data.map((ep) => (<li>{ep.frontmatter.title}</li>))}
</body>
</html>
diff --git a/packages/astro/test/fixtures/debug-component/src/pages/posts/[slug].astro b/packages/astro/test/fixtures/debug-component/src/pages/posts/[slug].astro
index 8b6ae00e9..ed85be913 100644
--- a/packages/astro/test/fixtures/debug-component/src/pages/posts/[slug].astro
+++ b/packages/astro/test/fixtures/debug-component/src/pages/posts/[slug].astro
@@ -3,15 +3,13 @@ import Debug from 'astro/debug';
// all the content that should be generated
export async function getStaticPaths() {
- const data = Astro.fetchContent('../../data/posts/*.md')
+ const data = await Astro.glob('../../data/posts/*.md')
- const allArticles = data.map(({ astro, file, url, ...article }) => {
+ const allArticles = data.map((article) => {
return {
- params: { slug: article.slug },
+ params: { slug: article.frontmatter.slug },
props: {
article: article,
- content: astro.html,
- md: astro.source,
}
}
})
diff --git a/packages/astro/test/fixtures/static build/src/pages/index.astro b/packages/astro/test/fixtures/static build/src/pages/index.astro
index b1bd2067e..763046d0a 100644
--- a/packages/astro/test/fixtures/static build/src/pages/index.astro
+++ b/packages/astro/test/fixtures/static build/src/pages/index.astro
@@ -2,7 +2,7 @@
import MainHead from '../components/MainHead.astro';
import Nav from '../components/Nav/index.jsx';
import { test as ssrConfigTest } from '@test/static-build-pkg';
-let allPosts = await Astro.fetchContent('./posts/*.md');
+let allPosts = await Astro.glob('./posts/*.md');
---
<html>
<head>
diff --git a/packages/astro/test/static-build.test.js b/packages/astro/test/static-build.test.js
index 89860505e..47707ea27 100644
--- a/packages/astro/test/static-build.test.js
+++ b/packages/astro/test/static-build.test.js
@@ -21,7 +21,7 @@ describe('Static build', () => {
expect(html).to.be.a('string');
});
- it('can build pages using fetchContent', async () => {
+ it('can build pages using Astro.glob()', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerioLoad(html);
const link = $('.posts a');
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7e547cbb7..e46db9db6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -473,6 +473,7 @@ importers:
'@types/send': ^0.17.1
'@types/yargs-parser': ^21.0.0
'@web/parse5-utils': ^1.3.0
+ ast-types: ^0.14.2
astro-scripts: workspace:*
boxen: ^6.2.1
chai: ^4.3.6
@@ -504,6 +505,7 @@ importers:
preferred-pm: ^3.0.3
prismjs: ^1.27.0
prompts: ^2.4.2
+ recast: ^0.20.5
rehype-slug: ^5.0.1
resolve: ^1.22.0
rollup: ^2.70.1
@@ -536,6 +538,7 @@ importers:
'@proload/core': 0.2.2
'@proload/plugin-tsm': 0.1.1_@proload+core@0.2.2
'@web/parse5-utils': 1.3.0
+ ast-types: 0.14.2
boxen: 6.2.1
ci-info: 3.3.0
common-ancestor-path: 1.0.1
@@ -563,6 +566,7 @@ importers:
preferred-pm: 3.0.3
prismjs: 1.27.0
prompts: 2.4.2
+ recast: 0.20.5
rehype-slug: 5.0.1
resolve: 1.22.0
rollup: 2.70.1
@@ -4615,6 +4619,13 @@ packages:
tslib: 2.3.1
dev: true
+ /ast-types/0.14.2:
+ resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==}
+ engines: {node: '>=4'}
+ dependencies:
+ tslib: 2.3.1
+ dev: false
+
/async/0.9.2:
resolution: {integrity: sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=}
dev: true
@@ -8807,6 +8818,16 @@ packages:
dependencies:
picomatch: 2.3.1
+ /recast/0.20.5:
+ resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==}
+ engines: {node: '>= 4'}
+ dependencies:
+ ast-types: 0.14.2
+ esprima: 4.0.1
+ source-map: 0.6.1
+ tslib: 2.3.1
+ dev: false
+
/redent/3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}