Compare commits
1 commit
main
...
wip-mdx-to
Author | SHA1 | Date | |
---|---|---|---|
|
8568637379 |
8 changed files with 419 additions and 70 deletions
|
@ -153,25 +153,26 @@ export default function jsx({ settings, logging }: AstroPluginJSXOptions): Plugi
|
||||||
const { mode } = viteConfig;
|
const { mode } = viteConfig;
|
||||||
// Shortcut: only use Astro renderer for MD and MDX files
|
// Shortcut: only use Astro renderer for MD and MDX files
|
||||||
if (id.endsWith('.mdx')) {
|
if (id.endsWith('.mdx')) {
|
||||||
const { code: jsxCode } = await transformWithEsbuild(code, id, {
|
return
|
||||||
loader: getEsbuildLoader(id),
|
// const { code: jsxCode } = await transformWithEsbuild(code, id, {
|
||||||
jsx: 'preserve',
|
// loader: getEsbuildLoader(id),
|
||||||
sourcemap: 'inline',
|
// jsx: 'preserve',
|
||||||
tsconfigRaw: {
|
// sourcemap: 'inline',
|
||||||
compilerOptions: {
|
// tsconfigRaw: {
|
||||||
// Ensure client:only imports are treeshaken
|
// compilerOptions: {
|
||||||
importsNotUsedAsValues: 'remove',
|
// // Ensure client:only imports are treeshaken
|
||||||
},
|
// importsNotUsedAsValues: 'remove',
|
||||||
},
|
// },
|
||||||
});
|
// },
|
||||||
return transformJSX({
|
// });
|
||||||
code: jsxCode,
|
// return transformJSX({
|
||||||
id,
|
// code: jsxCode,
|
||||||
renderer: astroJSXRenderer,
|
// id,
|
||||||
mode,
|
// renderer: astroJSXRenderer,
|
||||||
ssr,
|
// mode,
|
||||||
root: settings.config.root,
|
// ssr,
|
||||||
});
|
// root: settings.config.root,
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
if (defaultJSXRendererEntry && jsxRenderersIntegrationOnly.size === 1) {
|
if (defaultJSXRendererEntry && jsxRenderersIntegrationOnly.size === 1) {
|
||||||
// downlevel any non-standard syntax, but preserve JSX
|
// downlevel any non-standard syntax, but preserve JSX
|
||||||
|
|
|
@ -43,7 +43,10 @@
|
||||||
"estree-util-visit": "^1.2.0",
|
"estree-util-visit": "^1.2.0",
|
||||||
"github-slugger": "^1.4.0",
|
"github-slugger": "^1.4.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"hast-util-to-html": "^8.0.4",
|
||||||
|
"html-void-elements": "^2.0.1",
|
||||||
"kleur": "^4.1.4",
|
"kleur": "^4.1.4",
|
||||||
|
"property-information": "^6.2.0",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
@ -51,7 +54,8 @@
|
||||||
"shiki": "^0.14.1",
|
"shiki": "^0.14.1",
|
||||||
"source-map": "^0.7.4",
|
"source-map": "^0.7.4",
|
||||||
"unist-util-visit": "^4.1.0",
|
"unist-util-visit": "^4.1.0",
|
||||||
"vfile": "^5.3.2"
|
"vfile": "^5.3.2",
|
||||||
|
"zwitch": "^2.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.1",
|
"@types/chai": "^4.3.1",
|
||||||
|
|
262
packages/integrations/mdx/src/hast-util-to-astro-html.ts
Normal file
262
packages/integrations/mdx/src/hast-util-to-astro-html.ts
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
import { visit } from 'estree-util-visit';
|
||||||
|
import { html, svg } from 'property-information';
|
||||||
|
import { htmlVoidElements } from 'html-void-elements';
|
||||||
|
import type { Options } from 'hast-util-to-html';
|
||||||
|
import type { State as HtmlState } from 'hast-util-to-html/lib/types';
|
||||||
|
import { zwitch } from 'zwitch';
|
||||||
|
import { comment } from 'hast-util-to-html/lib/handle/comment.js';
|
||||||
|
import { doctype } from 'hast-util-to-html/lib/handle/doctype.js';
|
||||||
|
import { element } from 'hast-util-to-html/lib/handle/element.js';
|
||||||
|
import { raw } from 'hast-util-to-html/lib/handle/raw.js';
|
||||||
|
import { root } from 'hast-util-to-html/lib/handle/root.js';
|
||||||
|
import { text } from 'hast-util-to-html/lib/handle/text.js';
|
||||||
|
import { resolvePath } from './utils.js';
|
||||||
|
|
||||||
|
interface State extends HtmlState {
|
||||||
|
tree: any;
|
||||||
|
importer: string;
|
||||||
|
metadata: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler = (node: any, index: number | undefined, parent: any, state: State) => string;
|
||||||
|
|
||||||
|
const mdxJsxFlowElement: Handler = (node, index, parent, state) => {
|
||||||
|
if (node.name.includes('.') || node.name.match(/^[A-Z]/)) {
|
||||||
|
const clientAttribute = node.attributes.find((attr: any) => attr.name.startsWith('client:'));
|
||||||
|
const clientValue = clientAttribute ? clientAttribute.name.slice(7) : undefined;
|
||||||
|
|
||||||
|
let extraAttrs: string[] = [];
|
||||||
|
if (clientValue) {
|
||||||
|
extraAttrs.push(`"client:component-hydration":"${clientValue}"`);
|
||||||
|
|
||||||
|
for (const rootChild of state.tree.children) {
|
||||||
|
if (rootChild.type === 'mdxjsEsm') {
|
||||||
|
const ast = rootChild.data.estree;
|
||||||
|
visit(ast, (n: any) => {
|
||||||
|
if (n.type === 'ImportDeclaration') {
|
||||||
|
const specifier = n.specifiers.find((s: any) => s.local.name === node.name);
|
||||||
|
if (!specifier) return;
|
||||||
|
|
||||||
|
const component = {
|
||||||
|
exportName: specifier.imported ? specifier.imported.name : 'default',
|
||||||
|
specifier: n.source.value,
|
||||||
|
resolvedPath: resolvePath(n.source.value, state.importer),
|
||||||
|
};
|
||||||
|
|
||||||
|
// $$metadata.resolvePath will be postprocessed in Vite plugin later
|
||||||
|
extraAttrs.push(`"client:component-path":${JSON.stringify(component.resolvedPath)}`);
|
||||||
|
extraAttrs.push(`"client:component-export":${JSON.stringify(component.exportName)}`);
|
||||||
|
|
||||||
|
if (clientValue === 'only') {
|
||||||
|
state.metadata.clientOnlyComponents.push(component);
|
||||||
|
} else {
|
||||||
|
state.metadata.hydratedComponents.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots: string[] = [];
|
||||||
|
const rootNode =
|
||||||
|
state.schema.space === 'html' && node.tagName === 'template' ? node.content : node;
|
||||||
|
{
|
||||||
|
const children = rootNode.children || [];
|
||||||
|
let defaultSlot = [];
|
||||||
|
let index = -1;
|
||||||
|
|
||||||
|
while (++index < children.length) {
|
||||||
|
const child = children[index];
|
||||||
|
const slotName = child.attributes?.find((a) => a.name === 'slot')?.value?.trim();
|
||||||
|
if (slotName && slotName !== 'default') {
|
||||||
|
slots.push(
|
||||||
|
`"${slotName}": () => $$render\`${state.one.call(state, child, index, rootNode)}\``
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
defaultSlot.push(state.one.call(state, child, index, rootNode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consolidate default slot
|
||||||
|
if (defaultSlot.length) {
|
||||||
|
slots.push(`"default": () => $$render\`${defaultSlot.join('')}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrStr = serializeAttributesAsObjectString(node.attributes);
|
||||||
|
|
||||||
|
return `\${$$renderComponent($$result, ${JSON.stringify(node.name)}, ${node.name}, {${
|
||||||
|
attrStr ? attrStr + ',' : ''
|
||||||
|
} ${extraAttrs.join(',')}}${slots.length ? `, {${slots.join(',')}}` : ''})}`;
|
||||||
|
} else {
|
||||||
|
return `<${node.name}${serializeAttributesAsString(node.attributes)}>${state.all(node)}</${
|
||||||
|
node.name
|
||||||
|
}>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function serializeAttributesAsString(attributes: any[]) {
|
||||||
|
return Object.values(attributes)
|
||||||
|
.map((attr) => {
|
||||||
|
// spread
|
||||||
|
if (attr.type === 'mdxJsxExpressionAttribute') {
|
||||||
|
const value = attr.value.trim();
|
||||||
|
const varName = value.slice(3);
|
||||||
|
return `\${$$spreadAttribute(varName, ${JSON.stringify(varName)})}`;
|
||||||
|
}
|
||||||
|
// normal attribute
|
||||||
|
else if (attr.type === 'mdxJsxAttribute') {
|
||||||
|
if (attr.value == null) {
|
||||||
|
return ` ${attr.name}`;
|
||||||
|
} else if (typeof attr.value === 'string') {
|
||||||
|
return ` ${attr.name}="${attr.value}"`;
|
||||||
|
} else {
|
||||||
|
return `\${$$addAttribute(${attr.value.value}, ${JSON.stringify(attr.name)})}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeAttributesAsObjectString(attributes: any[]) {
|
||||||
|
return Object.values(attributes)
|
||||||
|
.map((attr) => {
|
||||||
|
// spread
|
||||||
|
if (attr.type === 'mdxJsxExpressionAttribute') {
|
||||||
|
return attr.value;
|
||||||
|
}
|
||||||
|
// normal attribute
|
||||||
|
else if (attr.type === 'mdxJsxAttribute') {
|
||||||
|
if (attr.value == null) {
|
||||||
|
return `${JSON.stringify(attr.name)}: true`;
|
||||||
|
} else if (typeof attr.value === 'string') {
|
||||||
|
return `${JSON.stringify(attr.name)}: ${JSON.stringify(attr.value)}`;
|
||||||
|
} else {
|
||||||
|
return `${JSON.stringify(attr.name)}: ${attr.value.value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdxFlowExpression: Handler = (node, index, parent, state) => {
|
||||||
|
const value = node.value.trim();
|
||||||
|
if (!value || (value.startsWith('/*') && value.endsWith('*/'))) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return `\${${value}}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handle = zwitch('type', {
|
||||||
|
invalid,
|
||||||
|
unknown,
|
||||||
|
handlers: {
|
||||||
|
comment,
|
||||||
|
doctype,
|
||||||
|
element,
|
||||||
|
raw,
|
||||||
|
root,
|
||||||
|
text,
|
||||||
|
mdxJsxFlowElement,
|
||||||
|
mdxJsxTextElement: mdxJsxFlowElement,
|
||||||
|
mdxFlowExpression,
|
||||||
|
mdxTextExpression: mdxFlowExpression,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function toAstroHtml(tree: any, options: Options, fullTree: any, importer: string) {
|
||||||
|
const options_ = options || {};
|
||||||
|
const quote = options_.quote || '"';
|
||||||
|
const alternative = quote === '"' ? "'" : '"';
|
||||||
|
|
||||||
|
if (quote !== '"' && quote !== "'") {
|
||||||
|
throw new Error('Invalid quote `' + quote + '`, expected `\'` or `"`');
|
||||||
|
}
|
||||||
|
|
||||||
|
const state: State = {
|
||||||
|
one,
|
||||||
|
all,
|
||||||
|
settings: {
|
||||||
|
omitOptionalTags: options_.omitOptionalTags || false,
|
||||||
|
allowParseErrors: options_.allowParseErrors || false,
|
||||||
|
allowDangerousCharacters: options_.allowDangerousCharacters || false,
|
||||||
|
quoteSmart: options_.quoteSmart || false,
|
||||||
|
preferUnquoted: options_.preferUnquoted || false,
|
||||||
|
tightAttributes: options_.tightAttributes || false,
|
||||||
|
upperDoctype: options_.upperDoctype || false,
|
||||||
|
tightDoctype: options_.tightDoctype || false,
|
||||||
|
bogusComments: options_.bogusComments || false,
|
||||||
|
tightCommaSeparatedLists: options_.tightCommaSeparatedLists || false,
|
||||||
|
tightSelfClosing: options_.tightSelfClosing || false,
|
||||||
|
collapseEmptyAttributes: options_.collapseEmptyAttributes || false,
|
||||||
|
allowDangerousHtml: options_.allowDangerousHtml || false,
|
||||||
|
voids: options_.voids || htmlVoidElements,
|
||||||
|
characterReferences: options_.characterReferences || options_.entities || {},
|
||||||
|
closeSelfClosing: options_.closeSelfClosing || false,
|
||||||
|
closeEmptyElements: options_.closeEmptyElements || false,
|
||||||
|
},
|
||||||
|
schema: options_.space === 'svg' ? svg : html,
|
||||||
|
quote,
|
||||||
|
alternative,
|
||||||
|
// Add original tree to find client: import path
|
||||||
|
tree: fullTree,
|
||||||
|
importer,
|
||||||
|
metadata: {
|
||||||
|
hydratedComponents: [],
|
||||||
|
clientOnlyComponents: [],
|
||||||
|
scripts: [],
|
||||||
|
propagation: 'none',
|
||||||
|
containsHead: false,
|
||||||
|
pageOptions: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// escape backticks. this would be more performant if it's done in hast-util-to-html raw
|
||||||
|
// directly. but i don't want to reimplement it.
|
||||||
|
visit(tree, (node) => {
|
||||||
|
if (typeof node.value === 'string') {
|
||||||
|
node.value = escapeTemplateLiterals(node.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderCode = state.one(
|
||||||
|
Array.isArray(tree) ? { type: 'root', children: tree } : tree,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderCode,
|
||||||
|
metadata: state.metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function one(this: State, node: any, index: number | undefined, parent: any) {
|
||||||
|
return handle(node, index, parent, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
function all(this: State, parent: any) {
|
||||||
|
const results: string[] = [];
|
||||||
|
const children = (parent && parent.children) || [];
|
||||||
|
let index = -1;
|
||||||
|
|
||||||
|
while (++index < children.length) {
|
||||||
|
results[index] = this.one(children[index], index, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalid(node: any) {
|
||||||
|
throw new Error('Expected node, not `' + node + '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
function unknown(node: any) {
|
||||||
|
throw new Error('Cannot compile unknown node `' + node.type + '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeTemplateLiterals(str: string) {
|
||||||
|
return str.replace(/\\/g, '\\\\').replace(/\`/g, '\\`').replace(/\$\{/g, '\\${');
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
import { markdownConfigDefaults } from '@astrojs/markdown-remark';
|
import { markdownConfigDefaults } from '@astrojs/markdown-remark';
|
||||||
import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/internal.js';
|
import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/internal.js';
|
||||||
import { compile as mdxCompile } from '@mdx-js/mdx';
|
import { createProcessor } from '@mdx-js/mdx';
|
||||||
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||||||
import mdxPlugin, { type Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
import mdxPlugin, { type Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
||||||
import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
||||||
import { parse as parseESM } from 'es-module-lexer';
|
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
||||||
|
@ -98,12 +97,14 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
|
||||||
if (!id.endsWith('mdx')) return;
|
if (!id.endsWith('mdx')) return;
|
||||||
|
|
||||||
// Read code from file manually to prevent Vite from parsing `import.meta.env` expressions
|
// Read code from file manually to prevent Vite from parsing `import.meta.env` expressions
|
||||||
const { fileId } = getFileInfo(id, config);
|
const { fileUrl, fileId } = getFileInfo(id, config);
|
||||||
const code = await fs.readFile(fileId, 'utf-8');
|
const code = await fs.readFile(fileId, 'utf-8');
|
||||||
|
|
||||||
const { data: frontmatter, content: pageContent } = parseFrontmatter(code, id);
|
const { data: frontmatter, content: pageContent } = parseFrontmatter(code, id);
|
||||||
const compiled = await mdxCompile(new VFile({ value: pageContent, path: id }), {
|
const vfile = new VFile({ value: pageContent, path: id });
|
||||||
|
const processor = createProcessor({
|
||||||
...mdxPluginOpts,
|
...mdxPluginOpts,
|
||||||
|
format: 'mdx',
|
||||||
elementAttributeNameCase: 'html',
|
elementAttributeNameCase: 'html',
|
||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
// Ensure `data.astro` is available to all remark plugins
|
// Ensure `data.astro` is available to all remark plugins
|
||||||
|
@ -119,60 +120,50 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
// strip out recma plugins
|
||||||
code: escapeViteEnvReferences(String(compiled.value)),
|
const unwantedRecmaPluginNames = [
|
||||||
map: compiled.map,
|
'recmaDocument',
|
||||||
};
|
'recmaJsxRewrite',
|
||||||
},
|
'recmaJsxBuild',
|
||||||
},
|
];
|
||||||
{
|
for (let i = 0; i < processor.attachers.length; i++) {
|
||||||
name: '@astrojs/mdx-postprocess',
|
const attacher = processor.attachers[i];
|
||||||
// These transforms must happen *after* JSX runtime transformations
|
if (unwantedRecmaPluginNames.includes(attacher[0].name)) {
|
||||||
transform(code, id) {
|
processor.attachers.splice(i, 1);
|
||||||
if (!id.endsWith('.mdx')) return;
|
}
|
||||||
|
|
||||||
const [moduleImports, moduleExports] = parseESM(code);
|
|
||||||
|
|
||||||
// Fragment import should already be injected, but check just to be safe.
|
|
||||||
const importsFromJSXRuntime = moduleImports
|
|
||||||
.filter(({ n }) => n === 'astro/jsx-runtime')
|
|
||||||
.map(({ ss, se }) => code.substring(ss, se));
|
|
||||||
const hasFragmentImport = importsFromJSXRuntime.some((statement) =>
|
|
||||||
/[\s,{](Fragment,|Fragment\s*})/.test(statement)
|
|
||||||
);
|
|
||||||
if (!hasFragmentImport) {
|
|
||||||
code = 'import { Fragment } from "astro/jsx-runtime"\n' + code;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { fileUrl, fileId } = getFileInfo(id, config);
|
|
||||||
if (!moduleExports.find(({ n }) => n === 'url')) {
|
|
||||||
code += `\nexport const url = ${JSON.stringify(fileUrl)};`;
|
|
||||||
}
|
|
||||||
if (!moduleExports.find(({ n }) => n === 'file')) {
|
|
||||||
code += `\nexport const file = ${JSON.stringify(fileId)};`;
|
|
||||||
}
|
|
||||||
if (!moduleExports.find(({ n }) => n === 'Content')) {
|
|
||||||
// Make `Content` the default export so we can wrap `MDXContent` and pass in `Fragment`
|
|
||||||
code = code.replace('export default MDXContent;', '');
|
|
||||||
code += `\nexport const Content = (props = {}) => MDXContent({
|
|
||||||
...props,
|
|
||||||
components: { Fragment, ...props.components },
|
|
||||||
});
|
|
||||||
export default Content;`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compiled = await processor.process(vfile);
|
||||||
|
let compiledCode = compiled.toString();
|
||||||
|
// Remove `<></>` from the end of the file
|
||||||
|
compiledCode = compiledCode.replace('<></>;', '');
|
||||||
|
// Add metadata
|
||||||
|
compiledCode += `\nexport const url = ${JSON.stringify(fileUrl)};`;
|
||||||
|
compiledCode += `\nexport const file = ${JSON.stringify(fileId)};`;
|
||||||
// Ensures styles and scripts are injected into a `<head>`
|
// Ensures styles and scripts are injected into a `<head>`
|
||||||
// When a layout is not applied
|
// When a layout is not applied
|
||||||
code += `\nContent[Symbol.for('astro.needsHeadRendering')] = !Boolean(frontmatter.layout);`;
|
compiledCode += `\nContent[Symbol.for('astro.needsHeadRendering')] = !Boolean(frontmatter.layout);`;
|
||||||
code += `\nContent.moduleId = ${JSON.stringify(id)};`;
|
compiledCode += `\nContent.moduleId = ${JSON.stringify(id)};`;
|
||||||
|
|
||||||
if (command === 'dev') {
|
if (command === 'dev') {
|
||||||
// TODO: decline HMR updates until we have a stable approach
|
// TODO: decline HMR updates until we have a stable approach
|
||||||
code += `\nif (import.meta.hot) {
|
compiledCode += `\nif (import.meta.hot) {
|
||||||
import.meta.hot.decline();
|
import.meta.hot.decline();
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
return { code: escapeViteEnvReferences(code), map: null };
|
|
||||||
|
// console.log(compiledCode)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: escapeViteEnvReferences(compiledCode),
|
||||||
|
map: compiled.map,
|
||||||
|
meta: {
|
||||||
|
astro: vfile.data.rehypeAstro,
|
||||||
|
vite: {
|
||||||
|
lang: 'ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as VitePlugin[],
|
] as VitePlugin[],
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { remarkImageToComponent } from './remark-images-to-component.js';
|
||||||
import remarkPrism from './remark-prism.js';
|
import remarkPrism from './remark-prism.js';
|
||||||
import remarkShiki from './remark-shiki.js';
|
import remarkShiki from './remark-shiki.js';
|
||||||
import { jsToTreeNode } from './utils.js';
|
import { jsToTreeNode } from './utils.js';
|
||||||
|
import { rehypeAstro } from './rehype-astro.js';
|
||||||
|
|
||||||
// Skip nonessential plugins during performance benchmark runs
|
// Skip nonessential plugins during performance benchmark runs
|
||||||
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
|
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
|
||||||
|
@ -144,6 +145,8 @@ export function getRehypePlugins(mdxOptions: MdxOptions): MdxRollupPluginOptions
|
||||||
...(isPerformanceBenchmark ? [] : [rehypeHeadingIds, rehypeInjectHeadingsExport]),
|
...(isPerformanceBenchmark ? [] : [rehypeHeadingIds, rehypeInjectHeadingsExport]),
|
||||||
// computed from `astro.data.frontmatter` in VFile data
|
// computed from `astro.data.frontmatter` in VFile data
|
||||||
rehypeApplyFrontmatterExport,
|
rehypeApplyFrontmatterExport,
|
||||||
|
// render hast to js using Astro's runtime
|
||||||
|
rehypeAstro,
|
||||||
];
|
];
|
||||||
return rehypePlugins;
|
return rehypePlugins;
|
||||||
}
|
}
|
||||||
|
|
48
packages/integrations/mdx/src/rehype-astro.ts
Normal file
48
packages/integrations/mdx/src/rehype-astro.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { toAstroHtml } from './hast-util-to-astro-html.js';
|
||||||
|
import { jsToTreeNode } from './utils.js';
|
||||||
|
|
||||||
|
export function rehypeAstro() {
|
||||||
|
return function (tree: any, vfile: any) {
|
||||||
|
const newChildren = [];
|
||||||
|
const contentNodes = [];
|
||||||
|
|
||||||
|
// hoist all esm code to top
|
||||||
|
for (const child of tree.children) {
|
||||||
|
if (child.type === 'mdxjsEsm') {
|
||||||
|
newChildren.push(child);
|
||||||
|
} else {
|
||||||
|
contentNodes.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { renderCode, metadata } = toAstroHtml(contentNodes, {}, tree, vfile.path);
|
||||||
|
|
||||||
|
const js = `
|
||||||
|
import {
|
||||||
|
Fragment,
|
||||||
|
render as $$render,
|
||||||
|
createComponent as $$createComponent,
|
||||||
|
renderComponent as $$renderComponent,
|
||||||
|
addAttribute as $$addAttribute,
|
||||||
|
spreadAttributes as $$spreadAttributes
|
||||||
|
} from "astro/server/index.js";
|
||||||
|
|
||||||
|
export const Content = $$createComponent(async ($$result, $$props, $$slots) => {
|
||||||
|
return $$render\`${renderCode}\`;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Content;`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
newChildren.push(jsToTreeNode(js));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse', js);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mutate tree as js entirely
|
||||||
|
tree.children = newChildren;
|
||||||
|
|
||||||
|
vfile.data.rehypeAstro = metadata;
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import npath from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||||||
import type { Options as AcornOpts } from 'acorn';
|
import type { Options as AcornOpts } from 'acorn';
|
||||||
import { parse } from 'acorn';
|
import { parse } from 'acorn';
|
||||||
|
@ -106,3 +108,29 @@ export function ignoreStringPlugins(plugins: any[]): PluggableList {
|
||||||
}
|
}
|
||||||
return validPlugins;
|
return validPlugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveJsToTs(filePath: string) {
|
||||||
|
if (filePath.endsWith('.jsx') && !fs.existsSync(filePath)) {
|
||||||
|
const tryPath = filePath.slice(0, -4) + '.tsx';
|
||||||
|
if (fs.existsSync(tryPath)) {
|
||||||
|
return tryPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the hydration paths so that it can be imported in the client
|
||||||
|
*/
|
||||||
|
export function resolvePath(specifier: string, importer: string) {
|
||||||
|
if (specifier.startsWith('.')) {
|
||||||
|
const absoluteSpecifier = npath.resolve(npath.dirname(importer), specifier);
|
||||||
|
return resolveJsToTs(normalizePath(absoluteSpecifier));
|
||||||
|
} else {
|
||||||
|
return specifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePath(id: string): string {
|
||||||
|
return npath.posix.normalize(id.replace(/\\/g, '/'));
|
||||||
|
}
|
||||||
|
|
|
@ -4140,9 +4140,18 @@ importers:
|
||||||
gray-matter:
|
gray-matter:
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
|
hast-util-to-html:
|
||||||
|
specifier: ^8.0.4
|
||||||
|
version: 8.0.4
|
||||||
|
html-void-elements:
|
||||||
|
specifier: ^2.0.1
|
||||||
|
version: 2.0.1
|
||||||
kleur:
|
kleur:
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.5
|
version: 4.1.5
|
||||||
|
property-information:
|
||||||
|
specifier: ^6.2.0
|
||||||
|
version: 6.2.0
|
||||||
rehype-raw:
|
rehype-raw:
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1
|
version: 6.1.1
|
||||||
|
@ -4167,6 +4176,9 @@ importers:
|
||||||
vfile:
|
vfile:
|
||||||
specifier: ^5.3.2
|
specifier: ^5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
|
zwitch:
|
||||||
|
specifier: ^2.0.4
|
||||||
|
version: 2.0.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/chai':
|
'@types/chai':
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
|
|
Loading…
Reference in a new issue