feat: add support for .mdc
files
This commit is contained in:
parent
af935c1b8d
commit
a68272641d
16 changed files with 190 additions and 9 deletions
|
@ -21,7 +21,7 @@ module.exports = (snowpackConfig, options = {}) => {
|
|||
name: 'snowpack-astro',
|
||||
knownEntrypoints: ['astro/dist/internal/h.js', 'astro/components/Prism.astro', 'shorthash', 'estree-util-value-to-estree', 'astring'],
|
||||
resolve: {
|
||||
input: ['.astro', '.md'],
|
||||
input: ['.astro', '.md', '.mdc'],
|
||||
output: ['.js', '.css'],
|
||||
},
|
||||
async transform({ contents, id, fileExt }) {
|
||||
|
|
|
@ -28,7 +28,7 @@ const defaultLogging: LogOptions = {
|
|||
/** Return contents of src/pages */
|
||||
async function allPages(root: URL): Promise<URL[]> {
|
||||
const cwd = fileURLToPath(root);
|
||||
const files = await glob('**/*.{astro,md}', { cwd, filesOnly: true });
|
||||
const files = await glob('**/*.{astro,md,mdc}', { cwd, filesOnly: true });
|
||||
return files.map((f) => new URL(f, root));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,13 @@ import type { CompileResult, TransformResult } from '../@types/astro';
|
|||
import type { CompileOptions } from '../@types/compiler.js';
|
||||
|
||||
import path from 'path';
|
||||
import { MarkdownRenderingOptions, renderMarkdownWithFrontmatter } from '@astrojs/markdown-support';
|
||||
import { renderFrontmatter, renderMarkdownWithFrontmatter } from '@astrojs/markdown-support';
|
||||
|
||||
import { parse } from '@astrojs/parser';
|
||||
import { transform } from './transform/index.js';
|
||||
import { codegen } from './codegen/index.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { walk } from 'estree-walker';
|
||||
|
||||
export { scopeRule } from './transform/postcss-scoped-styles/index.js';
|
||||
|
||||
|
@ -38,10 +40,75 @@ export async function convertAstroToJsx(template: string, opts: ConvertAstroOpti
|
|||
return await codegen(ast, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* .mdc -> .astro source
|
||||
*/
|
||||
export async function convertMdcToAstroSource(contents: string, { filename }: { filename: string }, compileOptions?: CompileOptions): Promise<string> {
|
||||
const opts = compileOptions?.astroConfig?.markdownOptions;
|
||||
|
||||
// 1. Parse
|
||||
const ast = parse(contents, {
|
||||
filename,
|
||||
});
|
||||
|
||||
const frontmatterContent = ast.module.content;
|
||||
const markdownContent = contents.slice(ast.module.end || 0);
|
||||
const componentNames = new Set();
|
||||
|
||||
walk(ast.html, {
|
||||
enter(node) {
|
||||
if (node.type === 'InlineComponent') {
|
||||
let name = node.name;
|
||||
if (node.name.indexOf(':') > -1) {
|
||||
name = name.split(':')[0]
|
||||
}
|
||||
if (node.name.indexOf('.') > -1) {
|
||||
name = name.split('.')[0]
|
||||
}
|
||||
componentNames.add(name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let {
|
||||
frontmatter: { layout, ...frontmatter },
|
||||
} = await renderFrontmatter(`---\n${frontmatterContent}\n---`);
|
||||
|
||||
let components;
|
||||
if (opts?.components) {
|
||||
components = path.posix.relative(path.posix.dirname(filename), fileURLToPath(opts?.components));
|
||||
}
|
||||
|
||||
if (frontmatter['astro'] !== undefined) {
|
||||
throw new Error(`"astro" is a reserved word but was used as a frontmatter value!\n\tat ${filename}`);
|
||||
}
|
||||
const contentData: any = {
|
||||
...frontmatter,
|
||||
};
|
||||
// </script> can't be anywhere inside of a JS string, otherwise the HTML parser fails.
|
||||
// Break it up here so that the HTML parser won't detect it.
|
||||
const stringifiedSetupContext = JSON.stringify(contentData).replace(/\<\/script\>/g, `</scrip" + "t>`);
|
||||
|
||||
return `---
|
||||
import { Markdown } from 'astro/components';
|
||||
${components ? `import { ${Array.from(componentNames.values()).join(', ')} } from '${components}';` : ''}
|
||||
${layout ? `import Layout from '${layout}';` : 'const Layout = Fragment;'}
|
||||
const frontmatter = ${stringifiedSetupContext};
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Markdown>
|
||||
${markdownContent}
|
||||
</Markdown>
|
||||
</Layout>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* .md -> .astro source
|
||||
*/
|
||||
export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }, opts?: MarkdownRenderingOptions): Promise<string> {
|
||||
export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }, compileOptions?: CompileOptions): Promise<string> {
|
||||
const opts = compileOptions?.astroConfig?.markdownOptions;
|
||||
let {
|
||||
content,
|
||||
frontmatter: { layout, ...frontmatter },
|
||||
|
@ -66,15 +133,28 @@ export const __content = ${stringifiedSetupContext};
|
|||
${content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* .mdc -> .jsx
|
||||
* Core function processing Markdown + Components, but along the way also calls convertAstroToJsx().
|
||||
*/
|
||||
async function convertMdcToJsx(
|
||||
contents: string,
|
||||
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string; }
|
||||
): Promise<TransformResult> {
|
||||
const raw = await convertMdcToAstroSource(contents, { filename }, compileOptions);
|
||||
const convertOptions = { compileOptions, filename, fileID };
|
||||
return await convertAstroToJsx(raw, convertOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* .md -> .jsx
|
||||
* Core function processing Markdown, but along the way also calls convertAstroToJsx().
|
||||
*/
|
||||
async function convertMdToJsx(
|
||||
contents: string,
|
||||
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }
|
||||
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string; }
|
||||
): Promise<TransformResult> {
|
||||
const raw = await convertMdToAstroSource(contents, { filename }, compileOptions.astroConfig.markdownOptions);
|
||||
const raw = await convertMdToAstroSource(contents, { filename }, compileOptions);
|
||||
const convertOptions = { compileOptions, filename, fileID };
|
||||
return await convertAstroToJsx(raw, convertOptions);
|
||||
}
|
||||
|
@ -89,6 +169,9 @@ async function transformFromSource(
|
|||
case filename.slice(-6) === '.astro':
|
||||
return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
|
||||
|
||||
case filename.slice(-4) === '.mdc':
|
||||
return await convertMdcToJsx(contents, { compileOptions, filename, fileID });
|
||||
|
||||
case filename.slice(-3) === '.md':
|
||||
return await convertMdToJsx(contents, { compileOptions, filename, fileID });
|
||||
|
||||
|
|
|
@ -80,6 +80,10 @@ function normalizeConfig(userConfig: any, root: string): AstroConfig {
|
|||
config.pages = new URL(config.pages + '/', fileProtocolRoot);
|
||||
config.public = new URL(config.public + '/', fileProtocolRoot);
|
||||
|
||||
if (config.markdownOptions.components) {
|
||||
config.markdownOptions.components = new URL(config.markdownOptions.components + '/', fileProtocolRoot);
|
||||
}
|
||||
|
||||
return config as AstroConfig;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,12 +47,13 @@ type SearchResult =
|
|||
* list of all known files that we could make instant, synchronous checks against.
|
||||
*/
|
||||
export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise<SearchResult> {
|
||||
const pageExtensions = ['.astro', '.md', '.mdc'];
|
||||
const reqPath = decodeURI(url.pathname);
|
||||
const base = reqPath.substr(1);
|
||||
|
||||
// Try to find index.astro/md paths
|
||||
if (reqPath.endsWith('/')) {
|
||||
const candidates = [`${base}index.astro`, `${base}index.md`];
|
||||
const candidates = pageExtensions.map(ext => `${base}index${ext}`);
|
||||
const location = findAnyPage(candidates, astroConfig);
|
||||
if (location) {
|
||||
return {
|
||||
|
@ -63,7 +64,7 @@ export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise
|
|||
}
|
||||
} else {
|
||||
// Try to find the page by its name.
|
||||
const candidates = [`${base}.astro`, `${base}.md`];
|
||||
const candidates = pageExtensions.map(ext => `${base}${ext}`);
|
||||
let location = findAnyPage(candidates, astroConfig);
|
||||
if (location) {
|
||||
return {
|
||||
|
@ -75,7 +76,7 @@ export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise
|
|||
}
|
||||
|
||||
// Try to find name/index.astro/md
|
||||
const candidates = [`${base}/index.astro`, `${base}/index.md`];
|
||||
const candidates = pageExtensions.map(ext => `${base}/index${ext}`);
|
||||
const location = findAnyPage(candidates, astroConfig);
|
||||
if (location) {
|
||||
return {
|
||||
|
|
30
packages/astro/test/astro-markdown-components.test.js
Normal file
30
packages/astro/test/astro-markdown-components.test.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
|
||||
const MarkdownComponents = suite('Astro Markdown Components tests');
|
||||
|
||||
setup(MarkdownComponents, './fixtures/astro-markdown-components');
|
||||
setupBuild(MarkdownComponents, './fixtures/astro-markdown-components');
|
||||
|
||||
MarkdownComponents('Can load mdc pages with Astro', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
if (result.error) throw new Error(result.error);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('h2').length, 1, 'There is an h2 added in markdown');
|
||||
assert.equal($('#counter').length, 1, 'Counter component added via a component from markdown');
|
||||
});
|
||||
|
||||
MarkdownComponents('Bundles client-side JS for prod', async (context) => {
|
||||
await context.build();
|
||||
|
||||
const complexHtml = await context.readFile('/index.html');
|
||||
assert.match(complexHtml, `import("/_astro/src/components/Counter.js"`);
|
||||
|
||||
const counterJs = await context.readFile('/_astro/src/components/Counter.js');
|
||||
assert.ok(counterJs, 'Counter.jsx is bundled for prod');
|
||||
});
|
||||
|
||||
MarkdownComponents.run();
|
11
packages/astro/test/fixtures/astro-markdown-components/astro.config.mjs
vendored
Normal file
11
packages/astro/test/fixtures/astro-markdown-components/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default {
|
||||
markdownOptions: {
|
||||
components: './src/components/index.js'
|
||||
},
|
||||
renderers: [
|
||||
'@astrojs/renderer-preact'
|
||||
],
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
3
packages/astro/test/fixtures/astro-markdown-components/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-markdown-components/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
7
packages/astro/test/fixtures/astro-markdown-components/src/components/Counter.jsx
vendored
Normal file
7
packages/astro/test/fixtures/astro-markdown-components/src/components/Counter.jsx
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
export default function () {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button id="counter" onClick={() => setCount(count + 1)}>{count}</button>;
|
||||
}
|
5
packages/astro/test/fixtures/astro-markdown-components/src/components/Example.jsx
vendored
Normal file
5
packages/astro/test/fixtures/astro-markdown-components/src/components/Example.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function () {
|
||||
return <div id="test">Testing</div>;
|
||||
}
|
5
packages/astro/test/fixtures/astro-markdown-components/src/components/Hello.jsx
vendored
Normal file
5
packages/astro/test/fixtures/astro-markdown-components/src/components/Hello.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function ({ name }) {
|
||||
return <div id="test">Hello {name}</div>;
|
||||
}
|
3
packages/astro/test/fixtures/astro-markdown-components/src/components/index.js
vendored
Normal file
3
packages/astro/test/fixtures/astro-markdown-components/src/components/index.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { default as Counter } from './Counter.jsx';
|
||||
export { default as Example } from './Example.jsx';
|
||||
export { default as Hello } from './Hello.jsx';
|
10
packages/astro/test/fixtures/astro-markdown-components/src/layouts/content.astro
vendored
Normal file
10
packages/astro/test/fixtures/astro-markdown-components/src/layouts/content.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
9
packages/astro/test/fixtures/astro-markdown-components/src/pages/index.mdc
vendored
Normal file
9
packages/astro/test/fixtures/astro-markdown-components/src/pages/index.mdc
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
layout: ../layouts/content.astro
|
||||
title: My Blog Post
|
||||
description: This is a post about some stuff.
|
||||
---
|
||||
|
||||
## Hello world!
|
||||
|
||||
<Counter client:visible />
|
|
@ -15,6 +15,14 @@ import rehypeStringify from 'rehype-stringify';
|
|||
|
||||
export { AstroMarkdownOptions, MarkdownRenderingOptions };
|
||||
|
||||
/** Internal utility for rendering a full markdown file and extracting Frontmatter data */
|
||||
export async function renderFrontmatter(contents: string) {
|
||||
// Dynamic import to ensure that "gray-matter" isn't built by Snowpack
|
||||
const { default: matter } = await import('gray-matter');
|
||||
const { data: frontmatter } = matter(contents);
|
||||
return { frontmatter };
|
||||
}
|
||||
|
||||
/** Internal utility for rendering a full markdown file and extracting Frontmatter data */
|
||||
export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) {
|
||||
// Dynamic import to ensure that "gray-matter" isn't built by Snowpack
|
||||
|
|
|
@ -4,6 +4,8 @@ export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
|
|||
export type Plugin = string | [string, unified.Settings] | UnifiedPluginImport | [UnifiedPluginImport, unified.Settings];
|
||||
|
||||
export interface AstroMarkdownOptions {
|
||||
/** Path to file which exports components for `.mda` files */
|
||||
components: string;
|
||||
/** Enable or disable footnotes syntax extension */
|
||||
footnotes: boolean;
|
||||
/** Enable or disable GitHub-flavored Markdown syntax extension */
|
||||
|
|
Loading…
Reference in a new issue