feat: add support for .mdc files

This commit is contained in:
Nate Moore 2021-07-09 10:55:13 -05:00
parent af935c1b8d
commit a68272641d
16 changed files with 190 additions and 9 deletions

View file

@ -21,7 +21,7 @@ module.exports = (snowpackConfig, options = {}) => {
name: 'snowpack-astro', name: 'snowpack-astro',
knownEntrypoints: ['astro/dist/internal/h.js', 'astro/components/Prism.astro', 'shorthash', 'estree-util-value-to-estree', 'astring'], knownEntrypoints: ['astro/dist/internal/h.js', 'astro/components/Prism.astro', 'shorthash', 'estree-util-value-to-estree', 'astring'],
resolve: { resolve: {
input: ['.astro', '.md'], input: ['.astro', '.md', '.mdc'],
output: ['.js', '.css'], output: ['.js', '.css'],
}, },
async transform({ contents, id, fileExt }) { async transform({ contents, id, fileExt }) {

View file

@ -28,7 +28,7 @@ const defaultLogging: LogOptions = {
/** Return contents of src/pages */ /** Return contents of src/pages */
async function allPages(root: URL): Promise<URL[]> { async function allPages(root: URL): Promise<URL[]> {
const cwd = fileURLToPath(root); 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)); return files.map((f) => new URL(f, root));
} }

View file

@ -2,11 +2,13 @@ import type { CompileResult, TransformResult } from '../@types/astro';
import type { CompileOptions } from '../@types/compiler.js'; import type { CompileOptions } from '../@types/compiler.js';
import path from 'path'; import path from 'path';
import { MarkdownRenderingOptions, renderMarkdownWithFrontmatter } from '@astrojs/markdown-support'; import { renderFrontmatter, renderMarkdownWithFrontmatter } from '@astrojs/markdown-support';
import { parse } from '@astrojs/parser'; import { parse } from '@astrojs/parser';
import { transform } from './transform/index.js'; import { transform } from './transform/index.js';
import { codegen } from './codegen/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'; 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); 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 * .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 { let {
content, content,
frontmatter: { layout, ...frontmatter }, frontmatter: { layout, ...frontmatter },
@ -66,15 +133,28 @@ export const __content = ${stringifiedSetupContext};
${content}`; ${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 * .md -> .jsx
* Core function processing Markdown, but along the way also calls convertAstroToJsx(). * Core function processing Markdown, but along the way also calls convertAstroToJsx().
*/ */
async function convertMdToJsx( async function convertMdToJsx(
contents: string, contents: string,
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string } { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string; }
): Promise<TransformResult> { ): Promise<TransformResult> {
const raw = await convertMdToAstroSource(contents, { filename }, compileOptions.astroConfig.markdownOptions); const raw = await convertMdToAstroSource(contents, { filename }, compileOptions);
const convertOptions = { compileOptions, filename, fileID }; const convertOptions = { compileOptions, filename, fileID };
return await convertAstroToJsx(raw, convertOptions); return await convertAstroToJsx(raw, convertOptions);
} }
@ -89,6 +169,9 @@ async function transformFromSource(
case filename.slice(-6) === '.astro': case filename.slice(-6) === '.astro':
return await convertAstroToJsx(contents, { compileOptions, filename, fileID }); return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
case filename.slice(-4) === '.mdc':
return await convertMdcToJsx(contents, { compileOptions, filename, fileID });
case filename.slice(-3) === '.md': case filename.slice(-3) === '.md':
return await convertMdToJsx(contents, { compileOptions, filename, fileID }); return await convertMdToJsx(contents, { compileOptions, filename, fileID });

View file

@ -80,6 +80,10 @@ function normalizeConfig(userConfig: any, root: string): AstroConfig {
config.pages = new URL(config.pages + '/', fileProtocolRoot); config.pages = new URL(config.pages + '/', fileProtocolRoot);
config.public = new URL(config.public + '/', 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; return config as AstroConfig;
} }

View file

@ -47,12 +47,13 @@ type SearchResult =
* list of all known files that we could make instant, synchronous checks against. * list of all known files that we could make instant, synchronous checks against.
*/ */
export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise<SearchResult> { export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise<SearchResult> {
const pageExtensions = ['.astro', '.md', '.mdc'];
const reqPath = decodeURI(url.pathname); const reqPath = decodeURI(url.pathname);
const base = reqPath.substr(1); const base = reqPath.substr(1);
// Try to find index.astro/md paths // Try to find index.astro/md paths
if (reqPath.endsWith('/')) { if (reqPath.endsWith('/')) {
const candidates = [`${base}index.astro`, `${base}index.md`]; const candidates = pageExtensions.map(ext => `${base}index${ext}`);
const location = findAnyPage(candidates, astroConfig); const location = findAnyPage(candidates, astroConfig);
if (location) { if (location) {
return { return {
@ -63,7 +64,7 @@ export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise
} }
} else { } else {
// Try to find the page by its name. // 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); let location = findAnyPage(candidates, astroConfig);
if (location) { if (location) {
return { return {
@ -75,7 +76,7 @@ export async function searchForPage(url: URL, astroConfig: AstroConfig): Promise
} }
// Try to find name/index.astro/md // 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); const location = findAnyPage(candidates, astroConfig);
if (location) { if (location) {
return { return {

View 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();

View file

@ -0,0 +1,11 @@
export default {
markdownOptions: {
components: './src/components/index.js'
},
renderers: [
'@astrojs/renderer-preact'
],
buildOptions: {
sitemap: false,
},
};

View file

@ -0,0 +1,3 @@
{
"workspaceRoot": "../../../../../"
}

View 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>;
}

View file

@ -0,0 +1,5 @@
import { h } from 'preact';
export default function () {
return <div id="test">Testing</div>;
}

View file

@ -0,0 +1,5 @@
import { h } from 'preact';
export default function ({ name }) {
return <div id="test">Hello {name}</div>;
}

View 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';

View file

@ -0,0 +1,10 @@
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<div class="container">
<slot></slot>
</div>
</body>
</html>

View 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 />

View file

@ -15,6 +15,14 @@ import rehypeStringify from 'rehype-stringify';
export { AstroMarkdownOptions, MarkdownRenderingOptions }; 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 */ /** Internal utility for rendering a full markdown file and extracting Frontmatter data */
export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) { export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) {
// Dynamic import to ensure that "gray-matter" isn't built by Snowpack // Dynamic import to ensure that "gray-matter" isn't built by Snowpack

View file

@ -4,6 +4,8 @@ export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
export type Plugin = string | [string, unified.Settings] | UnifiedPluginImport | [UnifiedPluginImport, unified.Settings]; export type Plugin = string | [string, unified.Settings] | UnifiedPluginImport | [UnifiedPluginImport, unified.Settings];
export interface AstroMarkdownOptions { export interface AstroMarkdownOptions {
/** Path to file which exports components for `.mda` files */
components: string;
/** Enable or disable footnotes syntax extension */ /** Enable or disable footnotes syntax extension */
footnotes: boolean; footnotes: boolean;
/** Enable or disable GitHub-flavored Markdown syntax extension */ /** Enable or disable GitHub-flavored Markdown syntax extension */