From 60d541738100c19f064425107f75f07ac5c5c306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Mart=C3=ADn=20Seery?= Date: Mon, 11 Apr 2022 20:01:12 -0300 Subject: [PATCH] feat: markdown config typechecking (#2970) * Added schemas to markdown plugin * Added new schemas to main package * Changesets * typeraw * Explaination about the weird type hack * Added markdown.mode to config * Added comment * Formatted * Moved validation to `astro` and added RemarkPlugin ad RehypePlugin * Removed the ability to have a custom markdown renderer internally * Fixed plugin type * Removed unused renderMarkdownWithFrontmatter * Added missing dependency * Dynamically import astro markdown * Cache import --- package.json | 2 ++ src/@types/astro.ts | 30 +++++++++++-------- src/core/app/index.ts | 2 +- src/core/app/types.ts | 6 ++-- src/core/build/generate.ts | 3 +- src/core/build/vite-plugin-ssr.ts | 5 +--- src/core/config.ts | 49 ++++++++++++++++++++++--------- src/core/render/core.ts | 9 +++--- src/core/render/dev/index.ts | 3 +- src/core/render/result.ts | 46 ++++++++++++----------------- src/vite-plugin-markdown/index.ts | 5 ++-- 11 files changed, 85 insertions(+), 75 deletions(-) diff --git a/package.json b/package.json index 4997f3e331cf..3436ac823cd4 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "execa": "^6.1.0", "fast-glob": "^3.2.11", "fast-xml-parser": "^4.0.7", + "gray-matter": "^4.0.3", "html-entities": "^2.3.3", "html-escaper": "^3.0.3", "htmlparser2": "^7.2.0", @@ -156,6 +157,7 @@ "@types/resolve": "^1.20.1", "@types/rimraf": "^3.0.2", "@types/send": "^0.17.1", + "@types/unist": "^2.0.6", "@types/yargs-parser": "^21.0.0", "astro-scripts": "workspace:*", "chai": "^4.3.6", diff --git a/src/@types/astro.ts b/src/@types/astro.ts index a8b76324e0b5..bd45f510899a 100644 --- a/src/@types/astro.ts +++ b/src/@types/astro.ts @@ -2,7 +2,7 @@ import type { AddressInfo } from 'net'; import type * as babel from '@babel/core'; import type * as vite from 'vite'; import { z } from 'zod'; -import type { ShikiConfig, Plugin } from '@astrojs/markdown-remark'; +import type { ShikiConfig, RemarkPlugins, RehypePlugins } from '@astrojs/markdown-remark'; import type { AstroConfigSchema } from '../core/config'; import type { AstroComponentFactory, Metadata } from '../runtime/server'; import type { ViteConfigWithSSR } from '../core/create-vite'; @@ -481,14 +481,24 @@ export interface AstroUserConfig { */ drafts?: boolean; + /** + * @docs + * @name markdown.mode + * @type {'md' | 'mdx'} + * @default `mdx` + * @description + * Control wheater to allow components inside markdown files ('mdx') or not ('md'). + */ + mode?: 'md' | 'mdx'; + /** * @docs * @name markdown.shikiConfig - * @type {ShikiConfig} + * @typeraw {Partial} * @description * Shiki configuration options. See [the markdown configuration docs](https://docs.astro.build/en/guides/markdown-content/#shiki-configuration) for usage. */ - shikiConfig?: ShikiConfig; + shikiConfig?: Partial; /** * @docs @@ -515,7 +525,7 @@ export interface AstroUserConfig { /** * @docs * @name markdown.remarkPlugins - * @type {Plugin[]} + * @type {RemarkPlugins} * @description * Pass a custom [Remark](https://github.com/remarkjs/remark) plugin to customize how your Markdown is built. * @@ -530,11 +540,11 @@ export interface AstroUserConfig { * }; * ``` */ - remarkPlugins?: Plugin[]; + remarkPlugins?: RemarkPlugins; /** * @docs * @name markdown.rehypePlugins - * @type {Plugin[]} + * @type {RehypePlugins} * @description * Pass a custom [Rehype](https://github.com/remarkjs/remark-rehype) plugin to customize how your Markdown is built. * @@ -549,7 +559,7 @@ export interface AstroUserConfig { * }; * ``` */ - rehypePlugins?: Plugin[]; + rehypePlugins?: RehypePlugins; }; /** @@ -757,12 +767,6 @@ export interface ManifestData { routes: RouteData[]; } -export type MarkdownRenderOptions = [string | MarkdownParser, Record]; -export type MarkdownParser = ( - contents: string, - options?: Record -) => MarkdownParserResponse | PromiseLike; - export interface MarkdownParserResponse { frontmatter: { [key: string]: any; diff --git a/src/core/app/index.ts b/src/core/app/index.ts index 81e19f914ce1..8f5113ca97fb 100644 --- a/src/core/app/index.ts +++ b/src/core/app/index.ts @@ -82,7 +82,7 @@ export class App { legacyBuild: false, links, logging: this.#logging, - markdownRender: manifest.markdown.render, + markdown: manifest.markdown, mod, origin: url.origin, pathname: url.pathname, diff --git a/src/core/app/types.ts b/src/core/app/types.ts index 27af8b665568..95ccfeba197f 100644 --- a/src/core/app/types.ts +++ b/src/core/app/types.ts @@ -1,10 +1,10 @@ import type { RouteData, SerializedRouteData, - MarkdownRenderOptions, ComponentInstance, SSRLoadedRenderer, } from '../../@types/astro'; +import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; export type ComponentPath = string; @@ -22,9 +22,7 @@ export type SerializedRouteInfo = Omit & { export interface SSRManifest { routes: RouteInfo[]; site?: string; - markdown: { - render: MarkdownRenderOptions; - }; + markdown: MarkdownRenderingOptions; pageMap: Map; renderers: SSRLoadedRenderer[]; entryModules: Record; diff --git a/src/core/build/generate.ts b/src/core/build/generate.ts index 974e2acd681f..b1c03465b478 100644 --- a/src/core/build/generate.ts +++ b/src/core/build/generate.ts @@ -1,4 +1,3 @@ -import astroRemark from '@astrojs/markdown-remark'; import fs from 'fs'; import { bgGreen, black, cyan, dim, green, magenta } from 'kleur/colors'; import npath from 'path'; @@ -197,7 +196,7 @@ async function generatePath( legacyBuild: false, links, logging, - markdownRender: [astroRemark, astroConfig.markdown], + markdown: astroConfig.markdown, mod, origin, pathname, diff --git a/src/core/build/vite-plugin-ssr.ts b/src/core/build/vite-plugin-ssr.ts index c6e21b83a044..3e7257f02e76 100644 --- a/src/core/build/vite-plugin-ssr.ts +++ b/src/core/build/vite-plugin-ssr.ts @@ -1,4 +1,3 @@ -import astroRemark from '@astrojs/markdown-remark'; import type { Plugin as VitePlugin } from 'vite'; import type { BuildInternals } from './internal.js'; import type { AstroAdapter } from '../../@types/astro'; @@ -110,9 +109,7 @@ function buildManifest(opts: StaticBuildOptions, internals: BuildInternals): Ser const ssrManifest: SerializedSSRManifest = { routes, site: astroConfig.site, - markdown: { - render: [astroRemark, astroConfig.markdown], - }, + markdown: astroConfig.markdown, pageMap: null as any, renderers: [], entryModules, diff --git a/src/core/config.ts b/src/core/config.ts index a3350f1349f3..e127131c5b2b 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,11 +1,14 @@ import type { AstroConfig, AstroUserConfig, CLIFlags } from '../@types/astro'; import type { Arguments as Flags } from 'yargs-parser'; import type * as Postcss from 'postcss'; +import type { ILanguageRegistration, IThemeRegistration, Theme } from 'shiki'; +import type { RemarkPlugin, RehypePlugin } from '@astrojs/markdown-remark'; import * as colors from 'kleur/colors'; import path from 'path'; import { pathToFileURL, fileURLToPath } from 'url'; import { mergeConfig as mergeViteConfig } from 'vite'; +import { BUNDLED_THEMES } from 'shiki'; import { z } from 'zod'; import load, { ProloadError } from '@proload/core'; import loadTypeScript from '@proload/plugin-tsm'; @@ -142,24 +145,42 @@ export const AstroConfigSchema = z.object({ .default({}), markdown: z .object({ - drafts: z.boolean().optional().default(false), - mode: z - .union([z.literal('md'), z.literal('mdx')]) - .optional() - // NOTE: "mdx" allows us to parse/compile Astro components in markdown. - // TODO: This should probably be updated to something more like "md" | "astro" - .default('mdx'), + // NOTE: "mdx" allows us to parse/compile Astro components in markdown. + // TODO: This should probably be updated to something more like "md" | "astro" + mode: z.enum(['md', 'mdx']).default('mdx'), + drafts: z.boolean().default(false), syntaxHighlight: z .union([z.literal('shiki'), z.literal('prism'), z.literal(false)]) - .optional() .default('shiki'), - // TODO: add better type checking - shikiConfig: z.any().optional().default({}), - remarkPlugins: z.array(z.any()).optional().default([]), - rehypePlugins: z.array(z.any()).optional().default([]), + shikiConfig: z + .object({ + langs: z.custom().array().default([]), + theme: z + .enum(BUNDLED_THEMES as [Theme, ...Theme[]]) + .or(z.custom()) + .default('github-dark'), + wrap: z.boolean().or(z.null()).default(false), + }) + .default({}), + remarkPlugins: z + .union([ + z.string(), + z.tuple([z.string(), z.any()]), + z.custom((data) => typeof data === 'function'), + z.tuple([z.custom((data) => typeof data === 'function'), z.any()]), + ]) + .array() + .default([]), + rehypePlugins: z + .union([ + z.string(), + z.tuple([z.string(), z.any()]), + z.custom((data) => typeof data === 'function'), + z.tuple([z.custom((data) => typeof data === 'function'), z.any()]), + ]) + .array() + .default([]), }) - .passthrough() - .optional() .default({}), vite: z.any().optional().default({}), experimental: z diff --git a/src/core/render/core.ts b/src/core/render/core.ts index 1be4cb0c24af..59ae6f010237 100644 --- a/src/core/render/core.ts +++ b/src/core/render/core.ts @@ -1,13 +1,12 @@ import type { ComponentInstance, - EndpointHandler, - MarkdownRenderOptions, Params, Props, SSRLoadedRenderer, RouteData, SSRElement, } from '../../@types/astro'; +import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import type { LogOptions } from '../logger/core.js'; import { renderHead, renderPage } from '../../runtime/server/index.js'; @@ -70,7 +69,7 @@ export interface RenderOptions { legacyBuild: boolean; logging: LogOptions; links: Set; - markdownRender: MarkdownRenderOptions; + markdown: MarkdownRenderingOptions; mod: ComponentInstance; origin: string; pathname: string; @@ -92,7 +91,7 @@ export async function render( links, logging, origin, - markdownRender, + markdown, mod, pathname, scripts, @@ -132,7 +131,7 @@ export async function render( legacyBuild, links, logging, - markdownRender, + markdown, origin, params, pathname, diff --git a/src/core/render/dev/index.ts b/src/core/render/dev/index.ts index 39e3ad5522cc..f245bc31ab11 100644 --- a/src/core/render/dev/index.ts +++ b/src/core/render/dev/index.ts @@ -1,4 +1,3 @@ -import astroRemark from '@astrojs/markdown-remark'; import { fileURLToPath } from 'url'; import type * as vite from 'vite'; import type { @@ -164,7 +163,7 @@ export async function render( legacyBuild: isLegacyBuild, links, logging, - markdownRender: [astroRemark, astroConfig.markdown], + markdown: astroConfig.markdown, mod, origin, pathname, diff --git a/src/core/render/result.ts b/src/core/render/result.ts index 241807a1f443..8fd856084b6c 100644 --- a/src/core/render/result.ts +++ b/src/core/render/result.ts @@ -2,13 +2,12 @@ import { bold } from 'kleur/colors'; import type { AstroGlobal, AstroGlobalPartial, - MarkdownParser, - MarkdownRenderOptions, Params, SSRElement, SSRLoadedRenderer, SSRResult, } from '../../@types/astro'; +import type { MarkdownRenderingOptions } from '@astrojs/markdown-remark'; import { renderSlot } from '../../runtime/server/index.js'; import { LogOptions, warn } from '../logger/core.js'; import { createCanonicalURL, isCSSRequest } from './util.js'; @@ -26,7 +25,7 @@ export interface CreateResultArgs { legacyBuild: boolean; logging: LogOptions; origin: string; - markdownRender: MarkdownRenderOptions; + markdown: MarkdownRenderingOptions; params: Params; pathname: string; renderers: SSRLoadedRenderer[]; @@ -99,8 +98,10 @@ class Slots { } } +let renderMarkdown: any = null; + export function createResult(args: CreateResultArgs): SSRResult { - const { legacyBuild, markdownRender, params, pathname, renderers, request, resolve, site } = args; + const { legacyBuild, markdown, params, pathname, renderers, request, resolve, site } = args; const url = new URL(request.url); const canonicalURL = createCanonicalURL('.' + pathname, site ?? url.origin); @@ -179,31 +180,22 @@ ${extra}` // Ensure this API is not exposed to users enumerable: false, writable: false, - // TODO: remove 1. markdown parser logic 2. update MarkdownRenderOptions to take a function only - // also needs the same `astroConfig.markdownOptions.render` as `.md` pages - value: async function (content: string, opts: any) { - let [mdRender, renderOpts] = markdownRender; - let parser: MarkdownParser | null = null; - //let renderOpts = {}; - if (Array.isArray(mdRender)) { - renderOpts = mdRender[1]; - mdRender = mdRender[0]; - } - // ['rehype-toc', opts] - if (typeof mdRender === 'string') { - const mod: { default: MarkdownParser } = await import(mdRender); - parser = mod.default; + // TODO: Remove this hole "Deno" logic once our plugin gets Deno support + value: async function (content: string, opts: MarkdownRenderingOptions) { + // @ts-ignore + if (typeof Deno !== 'undefined') { + throw new Error('Markdown is not supported in Deno SSR'); } - // [import('rehype-toc'), opts] - else if (mdRender instanceof Promise) { - const mod: { default: MarkdownParser } = await mdRender; - parser = mod.default; - } else if (typeof mdRender === 'function') { - parser = mdRender; - } else { - throw new Error('No Markdown parser found.'); + + if (!renderMarkdown) { + // The package is saved in this variable because Vite is too smart + // and will try to inline it in buildtime + let astroRemark = '@astrojs/markdown-remark'; + + renderMarkdown = (await import(astroRemark)).renderMarkdown; } - const { code } = await parser(content, { ...renderOpts, ...(opts ?? {}) }); + + const { code } = await renderMarkdown(content, { ...markdown, ...(opts ?? {}) }); return code; }, }); diff --git a/src/vite-plugin-markdown/index.ts b/src/vite-plugin-markdown/index.ts index 615894fca990..95747a402abb 100644 --- a/src/vite-plugin-markdown/index.ts +++ b/src/vite-plugin-markdown/index.ts @@ -1,4 +1,4 @@ -import astroRemark from '@astrojs/markdown-remark'; +import { renderMarkdown } from '@astrojs/markdown-remark'; import { transform } from '@astrojs/compiler'; import ancestor from 'common-ancestor-path'; import esbuild from 'esbuild'; @@ -118,7 +118,6 @@ export default function markdown({ config }: AstroPluginOptions): Plugin { // This returns the compiled markdown -> astro component that renders to HTML. if (id.endsWith('.md')) { const source = await fs.promises.readFile(id, 'utf8'); - const render = astroRemark; const renderOpts = config.markdown; const filename = normalizeFilename(id); @@ -128,7 +127,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin { // Extract special frontmatter keys const { data: frontmatter, content: markdownContent } = matter(source); - let renderResult = await render(markdownContent, renderOpts); + let renderResult = await renderMarkdown(markdownContent, renderOpts); let { code: astroResult, metadata } = renderResult; const { layout = '', components = '', setup = '', ...content } = frontmatter; content.astro = metadata;