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>
This commit is contained in:
parent
7d29feace1
commit
4299ab303b
27 changed files with 279 additions and 195 deletions
16
.changeset/forty-coins-attend.md
Normal file
16
.changeset/forty-coins-attend.md
Normal file
|
@ -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');
|
||||
```
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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">
|
||||
|
|
2
packages/astro/env.d.ts
vendored
2
packages/astro/env.d.ts
vendored
|
@ -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)
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)];
|
||||
if (allEntries.length === 0) {
|
||||
throw new Error(`[${url.pathname}] Astro.fetchContent() 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);
|
||||
function createDeprecatedFetchContentFn() {
|
||||
return () => {
|
||||
throw new Error('Deprecated: Astro.fetchContent() has been replaced with Astro.glob().');
|
||||
};
|
||||
// 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'];
|
||||
}
|
||||
|
||||
/** 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(`Astro.glob(${JSON.stringify(globValue())}) - no matches found.`);
|
||||
}
|
||||
// Map over the `import()` promises, calling to load them.
|
||||
return Promise.all(allEntries.map(fn => fn()));
|
||||
};
|
||||
// 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;
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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'}
|
||||
|
|
Loading…
Reference in a new issue