From 097ed18e5d7f0e5059ba52cde75706077ba26894 Mon Sep 17 00:00:00 2001 From: zce Date: Wed, 15 Nov 2023 20:10:22 +0800 Subject: [PATCH] feat: mdx prepare --- package.json | 11 ++- src/shared/markdown.ts | 5 +- src/shared/mdx.ts | 171 +++++++++++++++++++++++++++++++++++++++++ src/shared/shared.ts | 1 + 4 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 src/shared/mdx.ts diff --git a/package.json b/package.json index 8e0d046..d3d1842 100644 --- a/package.json +++ b/package.json @@ -44,17 +44,20 @@ }, "prettier": "@zce/prettier-config", "dependencies": { + "@esbuild-plugins/node-resolve": "latest", + "@fal-works/esbuild-plugin-global-externals": "latest", + "@mdx-js/esbuild": "latest", "cac": "latest", "esbuild": "latest", "fast-glob": "latest", - "unist-util-visit": "latest", - "unified": "latest", + "rehype-raw": "latest", + "rehype-stringify": "latest", "remark-gfm": "latest", "remark-parse": "latest", "remark-rehype": "latest", - "rehype-raw": "latest", - "rehype-stringify": "latest", "sharp": "latest", + "unified": "latest", + "unist-util-visit": "latest", "vfile": "latest", "vfile-reporter": "latest", "yaml": "latest", diff --git a/src/shared/markdown.ts b/src/shared/markdown.ts index df39a32..3601b72 100644 --- a/src/shared/markdown.ts +++ b/src/shared/markdown.ts @@ -43,8 +43,8 @@ const getBuiltInPlugins = (plugins?: BuiltinPlugins) => { }) } -export const markdown = (options: MarkdownOptions = {}) => { - return z.string().transform(async (value, ctx): Promise => { +export const markdown = (options: MarkdownOptions = {}) => + z.string().transform(async (value, ctx): Promise => { const file = await unified() .use(remarkParse) // Parse markdown content to a syntax tree .use(remarkGfm) // Support GFM (autolink literals, footnotes, strikethrough, tables, tasklists). @@ -74,4 +74,3 @@ export const markdown = (options: MarkdownOptions = {}) => { html: file.toString() } }) -} diff --git a/src/shared/mdx.ts b/src/shared/mdx.ts new file mode 100644 index 0000000..c40c7f2 --- /dev/null +++ b/src/shared/mdx.ts @@ -0,0 +1,171 @@ +// https://github.com/Modwatch/Frontend/blob/02fa6aca8341cc4eb4307272b66364c33f520429/generatepostmeta.ts#L68 + +import { dirname, extname, isAbsolute, join, resolve } from 'node:path' +import { StringDecoder } from 'node:string_decoder' +import { NodeResolvePlugin } from '@esbuild-plugins/node-resolve' +import { globalExternals } from '@fal-works/esbuild-plugin-global-externals' +import mdxPlugin from '@mdx-js/esbuild' +import { build } from 'esbuild' +import { VFile } from 'vfile' +import z from 'zod' + +import type { ModuleInfo } from '@fal-works/esbuild-plugin-global-externals' +import type { BuildOptions, Loader, Plugin } from 'esbuild' +import type { PluggableList } from 'unified' + +interface MdxBody { + // raw: string + plain: string + excerpt: string + code: string +} +interface MdxOptions { + remarkPlugins?: PluggableList + rehypePlugins?: PluggableList +} +export const mdx = (options: MdxOptions = {}) => + z.string().transform(async (value, ctx): Promise => { + const path = ctx.path[0] as string + const bundled = await build(await esbuildOptions(file, globals)) + const decoder = new StringDecoder('utf8') + + if (bundled.outputFiles === undefined || bundled.outputFiles.length === 0) { + throw new Error('Esbuild bundling error') + } + + const code = decoder.write(Buffer.from(bundled.outputFiles[0].contents)) + + return { + code: `${code};return Component` + } + + return { + // raw: value, + plain: value as string, + excerpt: value as string, + code: code || '' + } + }) + +/** + * Mostly derived from MDX Bundler, but strips out a lot of the stuff we don't need as + * well as fix some incompatabilities with esbuild resolution with pnpm and esm plugins. + */ + +export interface FrontMatter { + title: string + section: string + description?: string +} +export interface SerialiseOutput { + code: string + frontmatter: FrontMatter +} + +export type Globals = Record + +const esbuildOptions = async (source: VFile, globals: Globals): Promise => { + const absoluteFiles: Record = {} + + const entryPath = source.path ? (isAbsolute(source.path) ? source.path : join(source.cwd, source.path)) : join(source.cwd, `./_mdx_bundler_entry_point-${Math.random()}.mdx`) + absoluteFiles[entryPath] = String(source.value) + + // https://github.com/kentcdodds/mdx-bundler/pull/206 + const define: BuildOptions['define'] = {} + if (process.env.NODE_ENV !== undefined) { + define['process.env.NODE_ENV'] = JSON.stringify(process.env.NODE_ENV) + } + + // Import any imported components into esbuild resolver + const inMemoryPlugin: Plugin = { + name: 'inMemory', + setup(build) { + build.onResolve({ filter: /.*/ }, ({ path: filePath, importer }) => { + if (filePath === entryPath) { + return { + path: filePath, + pluginData: { inMemory: true, contents: absoluteFiles[filePath] } + } + } + + const modulePath = resolve(dirname(importer), filePath) + + if (modulePath in absoluteFiles) { + return { + path: modulePath, + pluginData: { + inMemory: true, + contents: absoluteFiles[modulePath] + } + } + } + + for (const ext of ['.js', '.ts', '.jsx', '.tsx', '.json', '.mdx', '.css']) { + const fullModulePath = `${modulePath}${ext}` + if (fullModulePath in absoluteFiles) { + return { + path: fullModulePath, + pluginData: { + inMemory: true, + contents: absoluteFiles[fullModulePath] + } + } + } + } + + // Return an empty object so that esbuild will handle resolving the file itself. + return {} + }) + + build.onLoad({ filter: /.*/ }, async ({ path: filePath, pluginData }) => { + if (pluginData === undefined || !pluginData.inMemory) { + // Return an empty object so that esbuild will load & parse the file contents itself. + return + } + + // the || .js allows people to exclude a file extension + const fileType = (extname(filePath) || '.jsx').slice(1) + const contents = absoluteFiles[filePath] + + if (fileType === 'mdx') return + + const loader: Loader = build.initialOptions.loader?.[`.${fileType}`] ? build.initialOptions.loader[`.${fileType}`] : (fileType as Loader) + + return { + contents, + loader + } + }) + } + } + + // This helps reduce bundles from having duplicated packages + const newGlobals: Record = {} + for (const [key, value] of Object.entries(globals)) { + newGlobals[key] = { + varName: value, + type: 'cjs' + } + } + + return { + entryPoints: [entryPath], + write: false, + define, + plugins: [ + globalExternals(newGlobals), + NodeResolvePlugin({ + extensions: ['.js', '.jsx', '.ts', '.tsx'] + }), + inMemoryPlugin, + mdxPlugin({ + remarkPlugins: [], + rehypePlugins: [] + }) + ], + bundle: true, + format: 'iife', + globalName: 'Component', + minify: process.env.NODE_ENV === 'production' + } +} diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 6f436f4..ace43bc 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -86,3 +86,4 @@ export const image = () => ) export { markdown } from './markdown' +export { mdx } from './mdx'