From a68272641d70107f00b221cffd39d9628964f724 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 9 Jul 2021 10:55:13 -0500 Subject: [PATCH] feat: add support for `.mdc` files --- packages/astro/snowpack-plugin.cjs | 2 +- packages/astro/src/build.ts | 2 +- packages/astro/src/compiler/index.ts | 91 ++++++++++++++++++- packages/astro/src/config.ts | 4 + packages/astro/src/search.ts | 7 +- .../test/astro-markdown-components.test.js | 30 ++++++ .../astro.config.mjs | 11 +++ .../snowpack.config.json | 3 + .../src/components/Counter.jsx | 7 ++ .../src/components/Example.jsx | 5 + .../src/components/Hello.jsx | 5 + .../src/components/index.js | 3 + .../src/layouts/content.astro | 10 ++ .../src/pages/index.mdc | 9 ++ packages/markdown-support/src/index.ts | 8 ++ packages/markdown-support/src/types.ts | 2 + 16 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 packages/astro/test/astro-markdown-components.test.js create mode 100644 packages/astro/test/fixtures/astro-markdown-components/astro.config.mjs create mode 100644 packages/astro/test/fixtures/astro-markdown-components/snowpack.config.json create mode 100644 packages/astro/test/fixtures/astro-markdown-components/src/components/Counter.jsx create mode 100644 packages/astro/test/fixtures/astro-markdown-components/src/components/Example.jsx create mode 100644 packages/astro/test/fixtures/astro-markdown-components/src/components/Hello.jsx create mode 100644 packages/astro/test/fixtures/astro-markdown-components/src/components/index.js create mode 100644 packages/astro/test/fixtures/astro-markdown-components/src/layouts/content.astro create mode 100644 packages/astro/test/fixtures/astro-markdown-components/src/pages/index.mdc diff --git a/packages/astro/snowpack-plugin.cjs b/packages/astro/snowpack-plugin.cjs index eb3c00281..1c1122199 100644 --- a/packages/astro/snowpack-plugin.cjs +++ b/packages/astro/snowpack-plugin.cjs @@ -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 }) { diff --git a/packages/astro/src/build.ts b/packages/astro/src/build.ts index 97b616b1f..484bc096a 100644 --- a/packages/astro/src/build.ts +++ b/packages/astro/src/build.ts @@ -28,7 +28,7 @@ const defaultLogging: LogOptions = { /** Return contents of src/pages */ async function allPages(root: URL): Promise { 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)); } diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts index b36384b83..65540c235 100644 --- a/packages/astro/src/compiler/index.ts +++ b/packages/astro/src/compiler/index.ts @@ -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 { + 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, + }; + // 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, ``); + + 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}; +--- + + + + ${markdownContent} + + +`; +} + /** * .md -> .astro source */ -export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }, opts?: MarkdownRenderingOptions): Promise { +export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }, compileOptions?: CompileOptions): Promise { + 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 { + 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 { - 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 }); diff --git a/packages/astro/src/config.ts b/packages/astro/src/config.ts index b41d67003..be94c368a 100644 --- a/packages/astro/src/config.ts +++ b/packages/astro/src/config.ts @@ -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; } diff --git a/packages/astro/src/search.ts b/packages/astro/src/search.ts index 5569e1bb9..803c77eb0 100644 --- a/packages/astro/src/search.ts +++ b/packages/astro/src/search.ts @@ -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 { + 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 { diff --git a/packages/astro/test/astro-markdown-components.test.js b/packages/astro/test/astro-markdown-components.test.js new file mode 100644 index 000000000..cdcddd3b5 --- /dev/null +++ b/packages/astro/test/astro-markdown-components.test.js @@ -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(); diff --git a/packages/astro/test/fixtures/astro-markdown-components/astro.config.mjs b/packages/astro/test/fixtures/astro-markdown-components/astro.config.mjs new file mode 100644 index 000000000..027695019 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/astro.config.mjs @@ -0,0 +1,11 @@ +export default { + markdownOptions: { + components: './src/components/index.js' + }, + renderers: [ + '@astrojs/renderer-preact' + ], + buildOptions: { + sitemap: false, + }, +}; diff --git a/packages/astro/test/fixtures/astro-markdown-components/snowpack.config.json b/packages/astro/test/fixtures/astro-markdown-components/snowpack.config.json new file mode 100644 index 000000000..8f034781d --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/snowpack.config.json @@ -0,0 +1,3 @@ +{ + "workspaceRoot": "../../../../../" +} diff --git a/packages/astro/test/fixtures/astro-markdown-components/src/components/Counter.jsx b/packages/astro/test/fixtures/astro-markdown-components/src/components/Counter.jsx new file mode 100644 index 000000000..eee3abc6c --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/src/components/Counter.jsx @@ -0,0 +1,7 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; + +export default function () { + const [count, setCount] = useState(0); + return ; +} diff --git a/packages/astro/test/fixtures/astro-markdown-components/src/components/Example.jsx b/packages/astro/test/fixtures/astro-markdown-components/src/components/Example.jsx new file mode 100644 index 000000000..e1f67ee50 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/src/components/Example.jsx @@ -0,0 +1,5 @@ +import { h } from 'preact'; + +export default function () { + return
Testing
; +} diff --git a/packages/astro/test/fixtures/astro-markdown-components/src/components/Hello.jsx b/packages/astro/test/fixtures/astro-markdown-components/src/components/Hello.jsx new file mode 100644 index 000000000..d30dec516 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/src/components/Hello.jsx @@ -0,0 +1,5 @@ +import { h } from 'preact'; + +export default function ({ name }) { + return
Hello {name}
; +} diff --git a/packages/astro/test/fixtures/astro-markdown-components/src/components/index.js b/packages/astro/test/fixtures/astro-markdown-components/src/components/index.js new file mode 100644 index 000000000..f9827014a --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/src/components/index.js @@ -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'; diff --git a/packages/astro/test/fixtures/astro-markdown-components/src/layouts/content.astro b/packages/astro/test/fixtures/astro-markdown-components/src/layouts/content.astro new file mode 100644 index 000000000..925a243a9 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/src/layouts/content.astro @@ -0,0 +1,10 @@ + + + + + +
+ +
+ + diff --git a/packages/astro/test/fixtures/astro-markdown-components/src/pages/index.mdc b/packages/astro/test/fixtures/astro-markdown-components/src/pages/index.mdc new file mode 100644 index 000000000..cb04a4f02 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-components/src/pages/index.mdc @@ -0,0 +1,9 @@ +--- +layout: ../layouts/content.astro +title: My Blog Post +description: This is a post about some stuff. +--- + +## Hello world! + + diff --git a/packages/markdown-support/src/index.ts b/packages/markdown-support/src/index.ts index 168738ea2..36b767ce7 100644 --- a/packages/markdown-support/src/index.ts +++ b/packages/markdown-support/src/index.ts @@ -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 diff --git a/packages/markdown-support/src/types.ts b/packages/markdown-support/src/types.ts index 6df601ae4..3daf6228f 100644 --- a/packages/markdown-support/src/types.ts +++ b/packages/markdown-support/src/types.ts @@ -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 */