Frontmatter injection for MD and MDX (#4176)
* feat: inject vfile data as exports * feat: add vfile to renderMarkdown output * feat: add safe astroExports parser to utils * refactor: expose vite-plugin-utils on astro package * feat: handle astroExports in mdx * deps: vfile * chore: lockfile * test: astroExports in mdx * refactor: merge plugin exports into forntmatter * refactor: astroExports -> astro.frontmatter * refactor: md astroExports -> astro.frontmatter * feat: astro.frontmatter vite-plugin-markdown * chore: remove unused import * fix: inline safelyGetAstroData in MDX integration * chore: check that frontmatter export is valid export name * chore: error log naming * test: mdx remark frontmatter injection * fix: inconsistent shiki mod resolution * fix: add new frontmatter and heading props * test: remark vdata * fix: spread astro.data.frontmatter * test deps: mdast-util-to-string, reading-time * fix: astro-md test package name * test: md frontmatter injection * fix: layouts * deps: remove vite-plugin-utils export * fix: package lock * chore: remove dup import * chore: changeset * chore: add comment on safelyGetAstroData source * deps: move mdast-util-to-string + reading-time to test fixture * chore: move remark plugins to test fixture * fix: override plugin frontmatter with user frontmatter * test: md injected frontmatter overrides * test: frontmatter injection overrides mdx
This commit is contained in:
parent
4678a3f358
commit
2675b8633c
32 changed files with 491 additions and 71 deletions
7
.changeset/cool-crabs-trade.md
Normal file
7
.changeset/cool-crabs-trade.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
'@astrojs/mdx': minor
|
||||||
|
'@astrojs/markdown-remark': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support frontmatter injection for MD and MDX using remark and rehype plugins
|
|
@ -1110,3 +1110,5 @@ export interface SSRResult {
|
||||||
response: ResponseInit;
|
response: ResponseInit;
|
||||||
_metadata: SSRMetadata;
|
_metadata: SSRMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MarkdownAstroData = { frontmatter: object };
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { collectErrorMetadata } from '../core/errors.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import { warn } from '../core/logger/core.js';
|
||||||
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
|
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
|
||||||
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
import { getFileInfo, safelyGetAstroData } from '../vite-plugin-utils/index.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
|
@ -44,7 +44,14 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
|
||||||
|
|
||||||
const html = renderResult.code;
|
const html = renderResult.code;
|
||||||
const { headings } = renderResult.metadata;
|
const { headings } = renderResult.metadata;
|
||||||
const frontmatter = { ...raw.data, url: fileUrl, file: fileId } as any;
|
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(renderResult.vfile.data);
|
||||||
|
const frontmatter = {
|
||||||
|
...injectedFrontmatter,
|
||||||
|
...raw.data,
|
||||||
|
url: fileUrl,
|
||||||
|
file: fileId,
|
||||||
|
} as any;
|
||||||
|
|
||||||
const { layout } = frontmatter;
|
const { layout } = frontmatter;
|
||||||
|
|
||||||
if (frontmatter.setup) {
|
if (frontmatter.setup) {
|
||||||
|
@ -94,6 +101,7 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
|
||||||
}
|
}
|
||||||
export default Content;
|
export default Content;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
meta: {
|
meta: {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { AstroConfig } from '../@types/astro';
|
import { Data } from 'vfile';
|
||||||
|
import type { AstroConfig, MarkdownAstroData } from '../@types/astro';
|
||||||
import { appendForwardSlash } from '../core/path.js';
|
import { appendForwardSlash } from '../core/path.js';
|
||||||
|
|
||||||
export function getFileInfo(id: string, config: AstroConfig) {
|
export function getFileInfo(id: string, config: AstroConfig) {
|
||||||
|
@ -15,3 +16,30 @@ export function getFileInfo(id: string, config: AstroConfig) {
|
||||||
}
|
}
|
||||||
return { fileId, fileUrl };
|
return { fileId, fileUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 function safelyGetAstroData(vfileData: Data): MarkdownAstroData {
|
||||||
|
const { astro } = vfileData;
|
||||||
|
|
||||||
|
if (!astro) return { frontmatter: {} };
|
||||||
|
if (!isValidAstroData(astro)) {
|
||||||
|
throw Error(
|
||||||
|
`[Markdown] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return astro;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
|
const FIXTURE_ROOT = './fixtures/astro-markdown-frontmatter-injection/';
|
||||||
|
|
||||||
|
describe('Astro Markdown - frontmatter injection', () => {
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: FIXTURE_ROOT,
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remark supports custom vfile data - get title', async () => {
|
||||||
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
||||||
|
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
|
||||||
|
expect(titles).to.contain('Page 1');
|
||||||
|
expect(titles).to.contain('Page 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rehype supports custom vfile data - reading time', async () => {
|
||||||
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
||||||
|
const readingTimes = frontmatterByPage.map((frontmatter = {}) => frontmatter.injectedReadingTime);
|
||||||
|
expect(readingTimes.length).to.be.greaterThan(0);
|
||||||
|
for (let readingTime of readingTimes) {
|
||||||
|
expect(readingTime).to.not.be.null;
|
||||||
|
expect(readingTime.text).match(/^\d+ min read/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overrides injected frontmatter with user frontmatter', async () => {
|
||||||
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
||||||
|
const readingTimes = frontmatterByPage.map((frontmatter = {}) => frontmatter.injectedReadingTime?.text);
|
||||||
|
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
|
||||||
|
expect(titles).to.contain('Overridden title');
|
||||||
|
expect(readingTimes).to.contain('1000 min read');
|
||||||
|
});
|
||||||
|
});
|
11
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs
vendored
Normal file
11
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs'
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://astro.build/',
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [remarkTitle],
|
||||||
|
rehypePlugins: [rehypeReadingTime],
|
||||||
|
}
|
||||||
|
});
|
11
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/package.json
vendored
Normal file
11
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/package.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "@test/astro-markdown-frontmatter-injection",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*",
|
||||||
|
"mdast-util-to-string": "^3.1.0",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"unist-util-visit": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
20
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs
vendored
Normal file
20
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import getReadingTime from 'reading-time';
|
||||||
|
import { toString } from 'mdast-util-to-string';
|
||||||
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
|
export function rehypeReadingTime() {
|
||||||
|
return function (tree, { data }) {
|
||||||
|
const readingTime = getReadingTime(toString(tree));
|
||||||
|
data.astro.frontmatter.injectedReadingTime = readingTime;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remarkTitle() {
|
||||||
|
return function (tree, { data }) {
|
||||||
|
visit(tree, ['heading'], (node) => {
|
||||||
|
if (node.depth === 1) {
|
||||||
|
data.astro.frontmatter.title = toString(node.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
6
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js
vendored
Normal file
6
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export async function get() {
|
||||||
|
const docs = await import.meta.glob('./*.md', { eager: true });
|
||||||
|
return {
|
||||||
|
body: JSON.stringify(Object.values(docs).map(doc => doc.frontmatter)),
|
||||||
|
}
|
||||||
|
}
|
3
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md
vendored
Normal file
3
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Page 1
|
||||||
|
|
||||||
|
Look at that!
|
19
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md
vendored
Normal file
19
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Page 2
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Section 1
|
||||||
|
|
||||||
|
Some text!
|
||||||
|
|
||||||
|
### Subsection 1
|
||||||
|
|
||||||
|
Some subsection test!
|
||||||
|
|
||||||
|
### Subsection 2
|
||||||
|
|
||||||
|
Oh cool, more text!
|
||||||
|
|
||||||
|
## Section 2
|
||||||
|
|
||||||
|
More content
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: 'Overridden title'
|
||||||
|
injectedReadingTime:
|
||||||
|
text: '1000 min read'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working!
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@test/astro-markdown-md-mode",
|
"name": "@test/astro-markdown",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
"remark-shiki-twoslash": "^3.1.0",
|
"remark-shiki-twoslash": "^3.1.0",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"shiki": "^0.10.1",
|
"shiki": "^0.10.1",
|
||||||
"unist-util-visit": "^4.1.0"
|
"unist-util-visit": "^4.1.0",
|
||||||
|
"vfile": "^5.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
|
|
82
packages/integrations/mdx/src/astro-data-utils.ts
Normal file
82
packages/integrations/mdx/src/astro-data-utils.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { name as isValidIdentifierName } from 'estree-util-is-identifier-name';
|
||||||
|
import type { VFile } from 'vfile';
|
||||||
|
import type { MdxjsEsm } from 'mdast-util-mdx';
|
||||||
|
import type { MarkdownAstroData } from 'astro';
|
||||||
|
import type { Data } from 'vfile';
|
||||||
|
import { jsToTreeNode } from './utils.js';
|
||||||
|
|
||||||
|
export function remarkInitializeAstroData() {
|
||||||
|
return function (tree: any, vfile: VFile) {
|
||||||
|
if (!vfile.data.astro) {
|
||||||
|
vfile.data.astro = { frontmatter: {} };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rehypeApplyFrontmatterExport(
|
||||||
|
pageFrontmatter: Record<string, any>,
|
||||||
|
exportName = 'frontmatter'
|
||||||
|
) {
|
||||||
|
return function (tree: any, vfile: VFile) {
|
||||||
|
if (!isValidIdentifierName(exportName)) {
|
||||||
|
throw new Error(
|
||||||
|
`[MDX] ${JSON.stringify(
|
||||||
|
exportName
|
||||||
|
)} is not a valid frontmatter export name! Make sure "frontmatterOptions.name" could be used as a JS export (i.e. "export const frontmatterName = ...")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data);
|
||||||
|
const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter };
|
||||||
|
let exportNodes: MdxjsEsm[] = [];
|
||||||
|
if (!exportName) {
|
||||||
|
exportNodes = Object.entries(frontmatter).map(([k, v]) => {
|
||||||
|
if (!isValidIdentifierName(k)) {
|
||||||
|
throw new Error(
|
||||||
|
`[MDX] A remark or rehype plugin tried to inject ${JSON.stringify(
|
||||||
|
k
|
||||||
|
)} as a top-level export, which is not a valid export name.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return jsToTreeNode(`export const ${k} = ${JSON.stringify(v)};`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
exportNodes = [jsToTreeNode(`export const ${exportName} = ${JSON.stringify(frontmatter)};`)];
|
||||||
|
}
|
||||||
|
tree.children = exportNodes.concat(tree.children);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from markdown utils
|
||||||
|
* @see "vite-plugin-utils"
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from markdown utils
|
||||||
|
* @see "vite-plugin-utils"
|
||||||
|
*/
|
||||||
|
export function safelyGetAstroData(vfileData: Data): MarkdownAstroData {
|
||||||
|
const { astro } = vfileData;
|
||||||
|
|
||||||
|
if (!astro) return { frontmatter: {} };
|
||||||
|
if (!isValidAstroData(astro)) {
|
||||||
|
throw Error(
|
||||||
|
`[MDX] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return astro;
|
||||||
|
}
|
|
@ -1,19 +1,18 @@
|
||||||
import { compile as mdxCompile, nodeTypes } from '@mdx-js/mdx';
|
import { compile as mdxCompile, nodeTypes } from '@mdx-js/mdx';
|
||||||
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
||||||
import type { AstroIntegration } from 'astro';
|
import type { AstroIntegration, AstroConfig } from 'astro';
|
||||||
|
import { remarkInitializeAstroData, rehypeApplyFrontmatterExport } from './astro-data-utils.js';
|
||||||
import { parse as parseESM } from 'es-module-lexer';
|
import { parse as parseESM } from 'es-module-lexer';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import remarkFrontmatter from 'remark-frontmatter';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import type { RemarkMdxFrontmatterOptions } from 'remark-mdx-frontmatter';
|
import type { RemarkMdxFrontmatterOptions } from 'remark-mdx-frontmatter';
|
||||||
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
|
|
||||||
import remarkShikiTwoslash from 'remark-shiki-twoslash';
|
import remarkShikiTwoslash from 'remark-shiki-twoslash';
|
||||||
import remarkSmartypants from 'remark-smartypants';
|
import remarkSmartypants from 'remark-smartypants';
|
||||||
import { VFile } from 'vfile';
|
import { VFile } from 'vfile';
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
import rehypeCollectHeadings from './rehype-collect-headings.js';
|
import rehypeCollectHeadings from './rehype-collect-headings.js';
|
||||||
import remarkPrism from './remark-prism.js';
|
import remarkPrism from './remark-prism.js';
|
||||||
import { getFileInfo, getFrontmatter } from './utils.js';
|
import { getFileInfo, parseFrontmatter } from './utils.js';
|
||||||
|
|
||||||
type WithExtends<T> = T | { extends: T };
|
type WithExtends<T> = T | { extends: T };
|
||||||
|
|
||||||
|
@ -37,44 +36,52 @@ function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] =
|
||||||
return [...defaults, ...(config?.extends ?? [])];
|
return [...defaults, ...(config?.extends ?? [])];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRemarkPlugins(
|
||||||
|
mdxOptions: MdxOptions,
|
||||||
|
config: AstroConfig
|
||||||
|
): MdxRollupPluginOptions['remarkPlugins'] {
|
||||||
|
let remarkPlugins = [
|
||||||
|
// Initialize vfile.data.astroExports before all plugins are run
|
||||||
|
remarkInitializeAstroData,
|
||||||
|
...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
|
||||||
|
];
|
||||||
|
if (config.markdown.syntaxHighlight === 'shiki') {
|
||||||
|
// Default export still requires ".default" chaining for some reason
|
||||||
|
// Workarounds tried:
|
||||||
|
// - "import * as remarkShikiTwoslash"
|
||||||
|
// - "import { default as remarkShikiTwoslash }"
|
||||||
|
const shikiTwoslash = (remarkShikiTwoslash as any).default ?? remarkShikiTwoslash;
|
||||||
|
remarkPlugins.push([shikiTwoslash, config.markdown.shikiConfig]);
|
||||||
|
}
|
||||||
|
if (config.markdown.syntaxHighlight === 'prism') {
|
||||||
|
remarkPlugins.push(remarkPrism);
|
||||||
|
}
|
||||||
|
return remarkPlugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRehypePlugins(
|
||||||
|
mdxOptions: MdxOptions,
|
||||||
|
config: AstroConfig
|
||||||
|
): MdxRollupPluginOptions['rehypePlugins'] {
|
||||||
|
let rehypePlugins = handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS);
|
||||||
|
|
||||||
|
if (config.markdown.syntaxHighlight === 'shiki' || config.markdown.syntaxHighlight === 'prism') {
|
||||||
|
rehypePlugins.push([rehypeRaw, { passThrough: nodeTypes }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rehypePlugins;
|
||||||
|
}
|
||||||
|
|
||||||
export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/mdx',
|
name: '@astrojs/mdx',
|
||||||
hooks: {
|
hooks: {
|
||||||
'astro:config:setup': ({ updateConfig, config, addPageExtension, command }: any) => {
|
'astro:config:setup': ({ updateConfig, config, addPageExtension, command }: any) => {
|
||||||
addPageExtension('.mdx');
|
addPageExtension('.mdx');
|
||||||
let remarkPlugins = handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS);
|
|
||||||
let rehypePlugins = handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS);
|
|
||||||
|
|
||||||
if (config.markdown.syntaxHighlight === 'shiki') {
|
|
||||||
remarkPlugins.push([
|
|
||||||
// Default export still requires ".default" chaining for some reason
|
|
||||||
// Workarounds tried:
|
|
||||||
// - "import * as remarkShikiTwoslash"
|
|
||||||
// - "import { default as remarkShikiTwoslash }"
|
|
||||||
(remarkShikiTwoslash as any).default ?? remarkShikiTwoslash,
|
|
||||||
config.markdown.shikiConfig,
|
|
||||||
]);
|
|
||||||
rehypePlugins.push([rehypeRaw, { passThrough: nodeTypes }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.markdown.syntaxHighlight === 'prism') {
|
|
||||||
remarkPlugins.push(remarkPrism);
|
|
||||||
rehypePlugins.push([rehypeRaw, { passThrough: nodeTypes }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
remarkPlugins.push(remarkFrontmatter);
|
|
||||||
remarkPlugins.push([
|
|
||||||
remarkMdxFrontmatter,
|
|
||||||
{
|
|
||||||
name: 'frontmatter',
|
|
||||||
...mdxOptions.frontmatterOptions,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const mdxPluginOpts: MdxRollupPluginOptions = {
|
const mdxPluginOpts: MdxRollupPluginOptions = {
|
||||||
remarkPlugins,
|
remarkPlugins: getRemarkPlugins(mdxOptions, config),
|
||||||
rehypePlugins,
|
rehypePlugins: getRehypePlugins(mdxOptions, config),
|
||||||
jsx: true,
|
jsx: true,
|
||||||
jsxImportSource: 'astro',
|
jsxImportSource: 'astro',
|
||||||
// Note: disable `.md` support
|
// Note: disable `.md` support
|
||||||
|
@ -93,24 +100,27 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
||||||
async transform(code, id) {
|
async transform(code, id) {
|
||||||
if (!id.endsWith('mdx')) return;
|
if (!id.endsWith('mdx')) return;
|
||||||
|
|
||||||
// If user overrides our default YAML parser,
|
let { data: frontmatter, content: pageContent } = parseFrontmatter(code, id);
|
||||||
// do not attempt to parse the `layout` via gray-matter
|
if (frontmatter.layout) {
|
||||||
if (!mdxOptions.frontmatterOptions?.parsers) {
|
const { layout, ...contentProp } = frontmatter;
|
||||||
const frontmatter = getFrontmatter(code, id);
|
pageContent += `\n\nexport default async function({ children }) {\nconst Layout = (await import(${JSON.stringify(
|
||||||
if (frontmatter.layout) {
|
frontmatter.layout
|
||||||
const { layout, ...content } = frontmatter;
|
)})).default;\nconst frontmatter=${JSON.stringify(
|
||||||
code += `\n\nexport default async function({ children }) {\nconst Layout = (await import(${JSON.stringify(
|
contentProp
|
||||||
frontmatter.layout
|
)};\nreturn <Layout frontmatter={frontmatter} content={frontmatter} headings={getHeadings()}>{children}</Layout> }`;
|
||||||
)})).default;\nconst frontmatter=${JSON.stringify(
|
|
||||||
content
|
|
||||||
)};\nreturn <Layout frontmatter={frontmatter} content={frontmatter} headings={getHeadings()}>{children}</Layout> }`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const compiled = await mdxCompile(
|
const compiled = await mdxCompile(new VFile({ value: pageContent, path: id }), {
|
||||||
new VFile({ value: code, path: id }),
|
...mdxPluginOpts,
|
||||||
mdxPluginOpts
|
rehypePlugins: [
|
||||||
);
|
...(mdxPluginOpts.rehypePlugins ?? []),
|
||||||
|
() =>
|
||||||
|
rehypeApplyFrontmatterExport(
|
||||||
|
frontmatter,
|
||||||
|
mdxOptions.frontmatterOptions?.name
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: String(compiled.value),
|
code: String(compiled.value),
|
||||||
|
|
|
@ -47,9 +47,9 @@ export function getFileInfo(id: string, config: AstroConfig): FileInfo {
|
||||||
* Match YAML exception handling from Astro core errors
|
* Match YAML exception handling from Astro core errors
|
||||||
* @see 'astro/src/core/errors.ts'
|
* @see 'astro/src/core/errors.ts'
|
||||||
*/
|
*/
|
||||||
export function getFrontmatter(code: string, id: string) {
|
export function parseFrontmatter(code: string, id: string) {
|
||||||
try {
|
try {
|
||||||
return matter(code).data;
|
return matter(code);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.name === 'YAMLException') {
|
if (e.name === 'YAMLException') {
|
||||||
const err: SSRError = e;
|
const err: SSRError = e;
|
||||||
|
|
12
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
vendored
Normal file
12
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://astro.build/',
|
||||||
|
integrations: [mdx({
|
||||||
|
remarkPlugins: [remarkTitle],
|
||||||
|
rehypePlugins: [rehypeReadingTime],
|
||||||
|
})],
|
||||||
|
});
|
12
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/package.json
vendored
Normal file
12
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/package.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "@test/mdx-frontmatter-injection",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*",
|
||||||
|
"@astrojs/mdx": "workspace:*",
|
||||||
|
"mdast-util-to-string": "^3.1.0",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"unist-util-visit": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
20
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
vendored
Normal file
20
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import getReadingTime from 'reading-time';
|
||||||
|
import { toString } from 'mdast-util-to-string';
|
||||||
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
|
export function rehypeReadingTime() {
|
||||||
|
return function (tree, { data }) {
|
||||||
|
const readingTime = getReadingTime(toString(tree));
|
||||||
|
data.astro.frontmatter.injectedReadingTime = readingTime;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remarkTitle() {
|
||||||
|
return function (tree, { data }) {
|
||||||
|
visit(tree, ['heading'], (node) => {
|
||||||
|
if (node.depth === 1) {
|
||||||
|
data.astro.frontmatter.title = toString(node.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
6
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/glob.json.js
vendored
Normal file
6
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/glob.json.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export async function get() {
|
||||||
|
const docs = await import.meta.glob('./*.mdx', { eager: true });
|
||||||
|
return {
|
||||||
|
body: JSON.stringify(Object.values(docs).map(doc => doc.frontmatter)),
|
||||||
|
}
|
||||||
|
}
|
3
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
vendored
Normal file
3
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Page 1
|
||||||
|
|
||||||
|
Look at that!
|
19
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
vendored
Normal file
19
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Page 2
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Section 1
|
||||||
|
|
||||||
|
Some text!
|
||||||
|
|
||||||
|
### Subsection 1
|
||||||
|
|
||||||
|
Some subsection test!
|
||||||
|
|
||||||
|
### Subsection 2
|
||||||
|
|
||||||
|
Oh cool, more text!
|
||||||
|
|
||||||
|
## Section 2
|
||||||
|
|
||||||
|
More content
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: 'Overridden title'
|
||||||
|
injectedReadingTime:
|
||||||
|
text: '1000 min read'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working!
|
|
@ -1,7 +1,7 @@
|
||||||
import { readingTime } from './space-ipsum.mdx';
|
import * as exps from './space-ipsum.mdx';
|
||||||
|
|
||||||
export function get() {
|
export function get() {
|
||||||
return {
|
return {
|
||||||
body: JSON.stringify(readingTime),
|
body: JSON.stringify(exps),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/headings-glob.json.js
vendored
Normal file
6
packages/integrations/mdx/test/fixtures/mdx-remark-plugins/src/pages/headings-glob.json.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export async function get() {
|
||||||
|
const docs = await import.meta.glob('./*.mdx', { eager: true });
|
||||||
|
return {
|
||||||
|
body: JSON.stringify(Object.values(docs).map(doc => doc.frontmatter)),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture } from '../../../astro/test/test-utils.js';
|
||||||
|
|
||||||
|
const FIXTURE_ROOT = new URL('./fixtures/mdx-frontmatter-injection/', import.meta.url);
|
||||||
|
|
||||||
|
describe('MDX frontmatter injection', () => {
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
fixture = await loadFixture({
|
||||||
|
root: FIXTURE_ROOT,
|
||||||
|
});
|
||||||
|
await fixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remark supports custom vfile data - get title', async () => {
|
||||||
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
||||||
|
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
|
||||||
|
expect(titles).to.contain('Page 1');
|
||||||
|
expect(titles).to.contain('Page 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rehype supports custom vfile data - reading time', async () => {
|
||||||
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
||||||
|
const readingTimes = frontmatterByPage.map((frontmatter = {}) => frontmatter.injectedReadingTime);
|
||||||
|
expect(readingTimes.length).to.be.greaterThan(0);
|
||||||
|
for (let readingTime of readingTimes) {
|
||||||
|
expect(readingTime).to.not.be.null;
|
||||||
|
expect(readingTime.text).match(/^\d+ min read/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overrides injected frontmatter with user frontmatter', async () => {
|
||||||
|
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
|
||||||
|
const readingTimes = frontmatterByPage.map((frontmatter = {}) => frontmatter.injectedReadingTime?.text);
|
||||||
|
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
|
||||||
|
expect(titles).to.contain('Overridden title');
|
||||||
|
expect(readingTimes).to.contain('1000 min read');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,15 +1,15 @@
|
||||||
import mdx from '@astrojs/mdx';
|
import mdx from '@astrojs/mdx';
|
||||||
import { jsToTreeNode } from '../dist/utils.js';
|
|
||||||
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import { parseHTML } from 'linkedom';
|
|
||||||
import getReadingTime from 'reading-time';
|
import getReadingTime from 'reading-time';
|
||||||
import { toString } from 'mdast-util-to-string';
|
import { toString } from 'mdast-util-to-string';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
import { jsToTreeNode } from '../dist/utils.js';
|
||||||
|
|
||||||
import { loadFixture } from '../../../astro/test/test-utils.js';
|
import { loadFixture } from '../../../astro/test/test-utils.js';
|
||||||
|
|
||||||
export function rehypeReadingTime() {
|
function rehypeReadingTime() {
|
||||||
return function (tree) {
|
return function (tree, { data }) {
|
||||||
const readingTime = getReadingTime(toString(tree));
|
const readingTime = getReadingTime(toString(tree));
|
||||||
tree.children.unshift(
|
tree.children.unshift(
|
||||||
jsToTreeNode(`export const readingTime = ${JSON.stringify(readingTime)}`)
|
jsToTreeNode(`export const readingTime = ${JSON.stringify(readingTime)}`)
|
||||||
|
@ -46,7 +46,7 @@ describe('MDX rehype plugins', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports custom rehype plugins - reading time', async () => {
|
it('supports custom rehype plugins - reading time', async () => {
|
||||||
const readingTime = JSON.parse(await fixture.readFile('/reading-time.json'));
|
const { readingTime } = JSON.parse(await fixture.readFile('/reading-time.json'));
|
||||||
|
|
||||||
expect(readingTime).to.not.be.null;
|
expect(readingTime).to.not.be.null;
|
||||||
expect(readingTime.text).to.match(/^\d+ min read/);
|
expect(readingTime.text).to.match(/^\d+ min read/);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import remarkPrism from './remark-prism.js';
|
||||||
import scopedStyles from './remark-scoped-styles.js';
|
import scopedStyles from './remark-scoped-styles.js';
|
||||||
import remarkShiki from './remark-shiki.js';
|
import remarkShiki from './remark-shiki.js';
|
||||||
import remarkUnwrap from './remark-unwrap.js';
|
import remarkUnwrap from './remark-unwrap.js';
|
||||||
|
import { remarkInitializeAstroData } from './remark-initialize-astro-data.js';
|
||||||
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
|
@ -45,6 +46,7 @@ export async function renderMarkdown(
|
||||||
|
|
||||||
let parser = unified()
|
let parser = unified()
|
||||||
.use(markdown)
|
.use(markdown)
|
||||||
|
.use(remarkInitializeAstroData)
|
||||||
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
||||||
|
|
||||||
if (remarkPlugins.length === 0 && rehypePlugins.length === 0) {
|
if (remarkPlugins.length === 0 && rehypePlugins.length === 0) {
|
||||||
|
@ -99,10 +101,9 @@ export async function renderMarkdown(
|
||||||
)
|
)
|
||||||
.use(rehypeStringify, { allowDangerousHtml: true });
|
.use(rehypeStringify, { allowDangerousHtml: true });
|
||||||
|
|
||||||
let result: string;
|
let vfile: VFile;
|
||||||
try {
|
try {
|
||||||
const vfile = await parser.process(input);
|
vfile = await parser.process(input);
|
||||||
result = vfile.toString();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ensure that the error message contains the input filename
|
// Ensure that the error message contains the input filename
|
||||||
// to make it easier for the user to fix the issue
|
// to make it easier for the user to fix the issue
|
||||||
|
@ -113,8 +114,9 @@ export async function renderMarkdown(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metadata: { headings, source: content, html: result.toString() },
|
metadata: { headings, source: content, html: String(vfile.value) },
|
||||||
code: result.toString(),
|
code: String(vfile.value),
|
||||||
|
vfile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { VFile } from 'vfile';
|
||||||
|
|
||||||
|
export function remarkInitializeAstroData() {
|
||||||
|
return function (tree: any, vfile: VFile) {
|
||||||
|
if (!vfile.data.astro) {
|
||||||
|
vfile.data.astro = { frontmatter: {} };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import type * as hast from 'hast';
|
||||||
import type * as mdast from 'mdast';
|
import type * as mdast from 'mdast';
|
||||||
import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki';
|
import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki';
|
||||||
import type * as unified from 'unified';
|
import type * as unified from 'unified';
|
||||||
|
import type { VFile } from 'vfile';
|
||||||
|
|
||||||
export type { Node } from 'unist';
|
export type { Node } from 'unist';
|
||||||
|
|
||||||
|
@ -58,5 +59,6 @@ export interface MarkdownMetadata {
|
||||||
|
|
||||||
export interface MarkdownRenderingResult {
|
export interface MarkdownRenderingResult {
|
||||||
metadata: MarkdownMetadata;
|
metadata: MarkdownMetadata;
|
||||||
|
vfile: VFile;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1231,6 +1231,18 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/astro-markdown-frontmatter-injection:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
mdast-util-to-string: ^3.1.0
|
||||||
|
reading-time: ^1.5.0
|
||||||
|
unist-util-visit: ^4.1.0
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
mdast-util-to-string: 3.1.0
|
||||||
|
reading-time: 1.5.0
|
||||||
|
unist-util-visit: 4.1.0
|
||||||
|
|
||||||
packages/astro/test/fixtures/astro-markdown-plugins:
|
packages/astro/test/fixtures/astro-markdown-plugins:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/preact': workspace:*
|
'@astrojs/preact': workspace:*
|
||||||
|
@ -2183,6 +2195,7 @@ importers:
|
||||||
remark-toc: ^8.0.1
|
remark-toc: ^8.0.1
|
||||||
shiki: ^0.10.1
|
shiki: ^0.10.1
|
||||||
unist-util-visit: ^4.1.0
|
unist-util-visit: ^4.1.0
|
||||||
|
vfile: ^5.3.2
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/prism': link:../../astro-prism
|
'@astrojs/prism': link:../../astro-prism
|
||||||
'@mdx-js/mdx': 2.1.2
|
'@mdx-js/mdx': 2.1.2
|
||||||
|
@ -2199,6 +2212,7 @@ importers:
|
||||||
remark-smartypants: 2.0.0
|
remark-smartypants: 2.0.0
|
||||||
shiki: 0.10.1
|
shiki: 0.10.1
|
||||||
unist-util-visit: 4.1.0
|
unist-util-visit: 4.1.0
|
||||||
|
vfile: 5.3.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/chai': 4.3.1
|
'@types/chai': 4.3.1
|
||||||
'@types/mocha': 9.1.1
|
'@types/mocha': 9.1.1
|
||||||
|
@ -2212,6 +2226,20 @@ importers:
|
||||||
reading-time: 1.5.0
|
reading-time: 1.5.0
|
||||||
remark-toc: 8.0.1
|
remark-toc: 8.0.1
|
||||||
|
|
||||||
|
packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection:
|
||||||
|
specifiers:
|
||||||
|
'@astrojs/mdx': workspace:*
|
||||||
|
astro: workspace:*
|
||||||
|
mdast-util-to-string: ^3.1.0
|
||||||
|
reading-time: ^1.5.0
|
||||||
|
unist-util-visit: ^4.1.0
|
||||||
|
dependencies:
|
||||||
|
'@astrojs/mdx': link:../../..
|
||||||
|
astro: link:../../../../../astro
|
||||||
|
mdast-util-to-string: 3.1.0
|
||||||
|
reading-time: 1.5.0
|
||||||
|
unist-util-visit: 4.1.0
|
||||||
|
|
||||||
packages/integrations/mdx/test/fixtures/mdx-page:
|
packages/integrations/mdx/test/fixtures/mdx-page:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/mdx': workspace:*
|
'@astrojs/mdx': workspace:*
|
||||||
|
@ -14710,7 +14738,6 @@ packages:
|
||||||
|
|
||||||
/reading-time/1.5.0:
|
/reading-time/1.5.0:
|
||||||
resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==}
|
resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/recast/0.20.5:
|
/recast/0.20.5:
|
||||||
resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==}
|
resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==}
|
||||||
|
|
Loading…
Reference in a new issue