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',
|
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 }) {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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 };
|
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
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
Loading…
Reference in a new issue