0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-17 23:11:29 -05:00

Clean up Astro metadata in vfile.data (#11861)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Bjorn Lu 2024-09-02 21:43:34 +08:00 committed by GitHub
parent 2bdde80cd3
commit 3ab3b4efbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 115 additions and 128 deletions

View file

@ -0,0 +1,13 @@
---
'@astrojs/markdown-remark': major
'astro': major
---
Cleans up Astro-specfic metadata attached to `vfile.data` in Remark and Rehype plugins. Previously, the metadata was attached in different locations with inconsistent names. The metadata is now renamed as below:
- `vfile.data.__astroHeadings` -> `vfile.data.astro.headings`
- `vfile.data.imagePaths` -> `vfile.data.astro.imagePaths`
The types of `imagePaths` has also been updated from `Set<string>` to `string[]`. The `vfile.data.astro.frontmatter` metadata is left unchanged.
While we don't consider these APIs public, they can be accessed by Remark and Rehype plugins that want to re-use Astro's metadata. If you are using these APIs, make sure to access them in the new locations.

View file

@ -0,0 +1,5 @@
---
'@astrojs/markdown-remark': major
---
Removes `InvalidAstroDataError`, `safelyGetAstroData`, and `setVfileFrontmatter` APIs in favour of `isFrontmatterValid`

View file

@ -0,0 +1,5 @@
---
'@astrojs/mdx': patch
---
Updates `@astrojs/markdown-remark` and handle its breaking changes

View file

@ -32,10 +32,7 @@ export const markdownContentEntryType: ContentEntryType = {
});
return {
html: result.code,
metadata: {
...result.metadata,
imagePaths: Array.from(result.metadata.imagePaths),
},
metadata: result.metadata,
};
};
},

View file

@ -1,9 +1,9 @@
import fs from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import {
InvalidAstroDataError,
type MarkdownProcessor,
createMarkdownProcessor,
isFrontmatterValid,
} from '@astrojs/markdown-remark';
import type { Plugin } from 'vite';
import { normalizePath } from 'vite';
@ -69,26 +69,23 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
);
}
const renderResult = await processor
.render(raw.content, {
// @ts-expect-error passing internal prop
fileURL,
frontmatter: raw.data,
})
.catch((err) => {
// Improve error message for invalid astro data
if (err instanceof InvalidAstroDataError) {
throw new AstroError(AstroErrorData.InvalidFrontmatterInjectionError);
}
throw err;
});
const renderResult = await processor.render(raw.content, {
// @ts-expect-error passing internal prop
fileURL,
frontmatter: raw.data,
});
// Improve error message for invalid astro frontmatter
if (!isFrontmatterValid(renderResult.metadata.frontmatter)) {
throw new AstroError(AstroErrorData.InvalidFrontmatterInjectionError);
}
let html = renderResult.code;
const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata;
// Resolve all the extracted images from the content
const imagePaths: MarkdownImagePath[] = [];
for (const imagePath of rawImagePaths.values()) {
for (const imagePath of rawImagePaths) {
imagePaths.push({
raw: imagePath,
safeName: shorthash(imagePath),

View file

@ -83,7 +83,7 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList {
}
rehypePlugins.push(
// Render info from `vfile.data.astro.data.frontmatter` as JS
// Render info from `vfile.data.astro.frontmatter` as JS
rehypeApplyFrontmatterExport,
// Analyze MDX nodes and attach to `vfile.data.__astroMetadata`
rehypeAnalyzeAstroMetadata,

View file

@ -1,18 +1,16 @@
import { InvalidAstroDataError } from '@astrojs/markdown-remark';
import { safelyGetAstroData } from '@astrojs/markdown-remark/dist/internal.js';
import { isFrontmatterValid } from '@astrojs/markdown-remark';
import type { VFile } from 'vfile';
import { jsToTreeNode } from './utils.js';
export function rehypeApplyFrontmatterExport() {
return function (tree: any, vfile: VFile) {
const astroData = safelyGetAstroData(vfile.data);
if (astroData instanceof InvalidAstroDataError)
const frontmatter = vfile.data.astro?.frontmatter;
if (!frontmatter || !isFrontmatterValid(frontmatter))
throw new Error(
// Copied from Astro core `errors-data`
// TODO: find way to import error data from core
'[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.',
);
const { frontmatter } = astroData;
const exportNodes = [
jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
];

View file

@ -1,9 +1,9 @@
import type { MarkdownHeading, MarkdownVFile } from '@astrojs/markdown-remark';
import type { VFile } from 'vfile';
import { jsToTreeNode } from './utils.js';
export function rehypeInjectHeadingsExport() {
return function (tree: any, file: MarkdownVFile) {
const headings: MarkdownHeading[] = file.data.__astroHeadings || [];
return function (tree: any, file: VFile) {
const headings = file.data.astro?.headings ?? [];
tree.children.unshift(
jsToTreeNode(`export function getHeadings() { return ${JSON.stringify(headings)} }`),
);

View file

@ -1,8 +1,8 @@
import type { MarkdownVFile } from '@astrojs/markdown-remark';
import type { Properties, Root } from 'hast';
import type { MdxJsxAttribute, MdxjsEsm } from 'mdast-util-mdx';
import type { MdxJsxFlowElementHast } from 'mdast-util-mdx-jsx';
import { visit } from 'unist-util-visit';
import type { VFile } from 'vfile';
import { jsToTreeNode } from './utils.js';
export const ASTRO_IMAGE_ELEMENT = 'astro-image';
@ -72,18 +72,18 @@ function getImageComponentAttributes(props: Properties): MdxJsxAttribute[] {
}
export function rehypeImageToComponent() {
return function (tree: Root, file: MarkdownVFile) {
if (!file.data.imagePaths) return;
return function (tree: Root, file: VFile) {
if (!file.data.astro?.imagePaths) return;
const importsStatements: MdxjsEsm[] = [];
const importedImages = new Map<string, string>();
visit(tree, 'element', (node, index, parent) => {
if (!file.data.imagePaths || node.tagName !== 'img' || !node.properties.src) return;
if (!file.data.astro?.imagePaths || node.tagName !== 'img' || !node.properties.src) return;
const src = decodeURI(String(node.properties.src));
if (!file.data.imagePaths.has(src)) return;
if (!file.data.astro.imagePaths?.includes(src)) return;
let importName = importedImages.get(src);

View file

@ -1,4 +1,3 @@
import { setVfileFrontmatter } from '@astrojs/markdown-remark';
import type { SSRError } from 'astro';
import { getAstroMetadata } from 'astro/jsx/rehype.js';
import { VFile } from 'vfile';
@ -47,9 +46,15 @@ export function vitePluginMdx(mdxOptions: MdxOptions): Plugin {
const { data: frontmatter, content: pageContent, matter } = parseFrontmatter(code, id);
const frontmatterLines = matter ? matter.match(/\n/g)?.join('') + '\n\n' : '';
const vfile = new VFile({ value: frontmatterLines + pageContent, path: id });
// Ensure `data.astro` is available to all remark plugins
setVfileFrontmatter(vfile, frontmatter);
const vfile = new VFile({
value: frontmatterLines + pageContent,
path: id,
data: {
astro: {
frontmatter,
},
},
});
// `processor` is initialized in `configResolved`, and removed in `buildEnd`. `transform`
// should be called in between those two lifecycle, so this error should never happen

View file

@ -13,8 +13,7 @@
"homepage": "https://astro.build",
"main": "./dist/index.js",
"exports": {
".": "./dist/index.js",
"./dist/internal.js": "./dist/internal.js"
".": "./dist/index.js"
},
"imports": {
"#import-plugin": {

View file

@ -1,34 +0,0 @@
import type { VFileData as Data, VFile } from 'vfile';
import type { MarkdownAstroData } from './types.js';
function isValidAstroData(obj: unknown): obj is MarkdownAstroData {
if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) {
const { frontmatter } = obj as any;
try {
// ensure frontmatter is JSON-serializable
JSON.stringify(frontmatter);
} catch {
return false;
}
return typeof frontmatter === 'object' && frontmatter !== null;
}
return false;
}
export class InvalidAstroDataError extends TypeError {}
export function safelyGetAstroData(vfileData: Data): MarkdownAstroData | InvalidAstroDataError {
const { astro } = vfileData;
if (!astro || !isValidAstroData(astro)) {
return new InvalidAstroDataError();
}
return astro;
}
export function setVfileFrontmatter(vfile: VFile, frontmatter: Record<string, any>) {
vfile.data ??= {};
vfile.data.astro ??= {};
(vfile.data.astro as any).frontmatter = frontmatter;
}

View file

@ -0,0 +1,9 @@
export function isFrontmatterValid(frontmatter: Record<string, any>) {
try {
// ensure frontmatter is JSON-serializable
JSON.stringify(frontmatter);
} catch {
return false;
}
return typeof frontmatter === 'object' && frontmatter !== null;
}

View file

@ -1,10 +1,5 @@
import type { AstroMarkdownOptions, MarkdownProcessor, MarkdownVFile } from './types.js';
import type { AstroMarkdownOptions, MarkdownProcessor } from './types.js';
import {
InvalidAstroDataError,
safelyGetAstroData,
setVfileFrontmatter,
} from './frontmatter-injection.js';
import { loadPlugins } from './load-plugins.js';
import { rehypeHeadingIds } from './rehype-collect-headings.js';
import { rehypePrism } from './rehype-prism.js';
@ -21,11 +16,11 @@ import { unified } from 'unified';
import { VFile } from 'vfile';
import { rehypeImages } from './rehype-images.js';
export { InvalidAstroDataError, setVfileFrontmatter } from './frontmatter-injection.js';
export { rehypeHeadingIds } from './rehype-collect-headings.js';
export { remarkCollectImages } from './remark-collect-images.js';
export { rehypePrism } from './rehype-prism.js';
export { rehypeShiki } from './rehype-shiki.js';
export { isFrontmatterValid } from './frontmatter.js';
export {
createShikiHighlighter,
type ShikiHighlighter,
@ -128,10 +123,17 @@ export async function createMarkdownProcessor(
return {
async render(content, renderOpts) {
const vfile = new VFile({ value: content, path: renderOpts?.fileURL });
setVfileFrontmatter(vfile, renderOpts?.frontmatter ?? {});
const vfile = new VFile({
value: content,
path: renderOpts?.fileURL,
data: {
astro: {
frontmatter: renderOpts?.frontmatter ?? {},
},
},
});
const result: MarkdownVFile = await parser.process(vfile).catch((err) => {
const result = await parser.process(vfile).catch((err) => {
// Ensure that the error message contains the input filename
// to make it easier for the user to fix the issue
err = prefixError(err, `Failed to parse Markdown file "${vfile.path}"`);
@ -140,17 +142,12 @@ export async function createMarkdownProcessor(
throw err;
});
const astroData = safelyGetAstroData(result.data);
if (astroData instanceof InvalidAstroDataError) {
throw astroData;
}
return {
code: String(result.value),
metadata: {
headings: result.data.__astroHeadings ?? [],
imagePaths: result.data.imagePaths ?? new Set(),
frontmatter: astroData.frontmatter ?? {},
headings: result.data.astro?.headings ?? [],
imagePaths: result.data.astro?.imagePaths ?? [],
frontmatter: result.data.astro?.frontmatter ?? {},
},
};
},

View file

@ -1 +0,0 @@
export { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js';

View file

@ -3,19 +3,18 @@ import Slugger from 'github-slugger';
import type { MdxTextExpression } from 'mdast-util-mdx-expression';
import type { Node } from 'unist';
import { visit } from 'unist-util-visit';
import { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js';
import type { MarkdownAstroData, MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
import type { VFile } from 'vfile';
import type { MarkdownHeading, RehypePlugin } from './types.js';
const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
const codeTagNames = new Set(['code', 'pre']);
export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
return function (tree, file: MarkdownVFile) {
return function (tree, file) {
const headings: MarkdownHeading[] = [];
const frontmatter = file.data.astro?.frontmatter;
const slugger = new Slugger();
const isMDX = isMDXFile(file);
const astroData = safelyGetAstroData(file.data);
visit(tree, (node) => {
if (node.type !== 'element') return;
const { tagName } = node;
@ -37,10 +36,13 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
if (rawNodeTypes.has(child.type)) {
if (isMDX || codeTagNames.has(parent.tagName)) {
let value = child.value;
if (isMdxTextExpression(child) && !(astroData instanceof InvalidAstroDataError)) {
if (isMdxTextExpression(child) && frontmatter) {
const frontmatterPath = getMdxFrontmatterVariablePath(child);
if (Array.isArray(frontmatterPath) && frontmatterPath.length > 0) {
const frontmatterValue = getMdxFrontmatterVariableValue(astroData, frontmatterPath);
const frontmatterValue = getMdxFrontmatterVariableValue(
frontmatter,
frontmatterPath,
);
if (typeof frontmatterValue === 'string') {
value = frontmatterValue;
}
@ -65,11 +67,12 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
headings.push({ depth, slug: node.properties.id, text });
});
file.data.__astroHeadings = headings;
file.data.astro ??= {};
file.data.astro.headings = headings;
};
}
function isMDXFile(file: MarkdownVFile) {
function isMDXFile(file: VFile) {
return Boolean(file.history[0]?.endsWith('.mdx'));
}
@ -109,8 +112,8 @@ function getMdxFrontmatterVariablePath(node: MdxTextExpression): string[] | Erro
return expressionPath.reverse();
}
function getMdxFrontmatterVariableValue(astroData: MarkdownAstroData, path: string[]) {
let value: MdxFrontmatterVariableValue = astroData.frontmatter;
function getMdxFrontmatterVariableValue(frontmatter: Record<string, any>, path: string[]) {
let value = frontmatter;
for (const key of path) {
if (!value[key]) return undefined;
@ -124,6 +127,3 @@ function getMdxFrontmatterVariableValue(astroData: MarkdownAstroData, path: stri
function isMdxTextExpression(node: Node): node is MdxTextExpression {
return node.type === 'mdxTextExpression';
}
type MdxFrontmatterVariableValue =
MarkdownAstroData['frontmatter'][keyof MarkdownAstroData['frontmatter']];

View file

@ -1,9 +1,9 @@
import { visit } from 'unist-util-visit';
import type { MarkdownVFile } from './types.js';
import type { VFile } from 'vfile';
export function rehypeImages() {
return () =>
function (tree: any, file: MarkdownVFile) {
function (tree: any, file: VFile) {
const imageOccurrenceMap = new Map();
visit(tree, (node) => {
@ -13,7 +13,7 @@ export function rehypeImages() {
if (node.properties?.src) {
node.properties.src = decodeURI(node.properties.src);
if (file.data.imagePaths?.has(node.properties.src)) {
if (file.data.astro?.imagePaths?.includes(node.properties.src)) {
const { ...props } = node.properties;
// Initialize or increment occurrence count for this image

View file

@ -1,10 +1,10 @@
import type { Image, ImageReference } from 'mdast';
import { definitions } from 'mdast-util-definitions';
import { visit } from 'unist-util-visit';
import type { MarkdownVFile } from './types.js';
import type { VFile } from 'vfile';
export function remarkCollectImages() {
return function (tree: any, vfile: MarkdownVFile) {
return function (tree: any, vfile: VFile) {
if (typeof vfile?.path !== 'string') return;
const definition = definitions(tree);
@ -22,7 +22,8 @@ export function remarkCollectImages() {
}
});
vfile.data.imagePaths = imagePaths;
vfile.data.astro ??= {};
vfile.data.astro.imagePaths = Array.from(imagePaths);
};
}

View file

@ -3,14 +3,19 @@ import type * as mdast from 'mdast';
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
import type { BuiltinTheme } from 'shiki';
import type * as unified from 'unified';
import type { DataMap, VFile } from 'vfile';
import type { CreateShikiHighlighterOptions, ShikiHighlighterHighlightOptions } from './shiki.js';
export type { Node } from 'unist';
export type MarkdownAstroData = {
frontmatter: Record<string, any>;
};
declare module 'vfile' {
interface DataMap {
astro: {
headings?: MarkdownHeading[];
imagePaths?: string[];
frontmatter?: Record<string, any>;
};
}
}
export type RemarkPlugin<PluginParameters extends any[] = any[]> = unified.Plugin<
PluginParameters,
@ -62,7 +67,7 @@ export interface MarkdownProcessorRenderResult {
code: string;
metadata: {
headings: MarkdownHeading[];
imagePaths: Set<string>;
imagePaths: string[];
frontmatter: Record<string, any>;
};
}
@ -72,12 +77,3 @@ export interface MarkdownHeading {
slug: string;
text: string;
}
// TODO: Remove `MarkdownVFile` and move all additional properties to `DataMap` instead
export interface MarkdownVFile extends VFile {
data: Record<string, unknown> &
Partial<DataMap> & {
__astroHeadings?: MarkdownHeading[];
imagePaths?: Set<string>;
};
}

View file

@ -23,7 +23,7 @@ describe('collect images', async () => {
'<p>Hello <img __ASTRO_IMAGE_="{&#x22;src&#x22;:&#x22;./img.png&#x22;,&#x22;alt&#x22;:&#x22;inline image url&#x22;,&#x22;index&#x22;:0}"></p>',
);
assert.deepEqual(Array.from(imagePaths), ['./img.png']);
assert.deepEqual(imagePaths, ['./img.png']);
});
it('should add image paths from definition', async () => {
@ -37,6 +37,6 @@ describe('collect images', async () => {
'<p>Hello <img __ASTRO_IMAGE_="{&#x22;src&#x22;:&#x22;./img.webp&#x22;,&#x22;alt&#x22;:&#x22;image ref&#x22;,&#x22;index&#x22;:0}"></p>',
);
assert.deepEqual(Array.from(metadata.imagePaths), ['./img.webp']);
assert.deepEqual(metadata.imagePaths, ['./img.webp']);
});
});