Compare commits
9 commits
main
...
feat/new-r
Author | SHA1 | Date | |
---|---|---|---|
|
68c04c28d5 | ||
|
bcf1cde198 | ||
|
77971eec8e | ||
|
b46b0ec64c | ||
|
e9866bab23 | ||
|
abff1d696c | ||
|
321164503a | ||
|
0ab7d0dbd1 | ||
|
d87544c241 |
5 changed files with 158 additions and 46 deletions
5
.changeset/sour-starfishes-behave.md
Normal file
5
.changeset/sour-starfishes-behave.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/markdoc': patch
|
||||
---
|
||||
|
||||
Allow component import paths to be passed as URLs to Markdoc tag and node renderers.
|
|
@ -9,7 +9,7 @@ import _Markdoc from '@markdoc/markdoc';
|
|||
import type { AstroInstance } from 'astro';
|
||||
import { heading } from './heading-ids.js';
|
||||
|
||||
type Render = AstroInstance['default'] | string;
|
||||
export type Render = AstroInstance['default'] | URL | string;
|
||||
|
||||
export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = Omit<
|
||||
MarkdocConfig,
|
||||
|
|
|
@ -21,6 +21,7 @@ import type * as rollup from 'rollup';
|
|||
import { normalizePath } from 'vite';
|
||||
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';
|
||||
import { setupConfig } from './runtime.js';
|
||||
import { markdocConfigId, vitePluginMarkdocConfig } from './vite-plugin-config.js';
|
||||
|
||||
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
||||
// `contentEntryType` is not a public API
|
||||
|
@ -129,52 +130,37 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
|
|||
renderComponent,
|
||||
} from 'astro/runtime/server/index.js';
|
||||
import { Renderer } from '@astrojs/markdoc/components';
|
||||
import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
|
||||
${
|
||||
markdocConfigResult
|
||||
? `import _userConfig from ${JSON.stringify(
|
||||
markdocConfigResultId
|
||||
)};\nconst userConfig = _userConfig ?? {};`
|
||||
: 'const userConfig = {};'
|
||||
}${
|
||||
astroConfig.experimental.assets
|
||||
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };`
|
||||
: ''
|
||||
}
|
||||
const stringifiedAst = ${JSON.stringify(
|
||||
import { collectHeadings, Markdoc } from '@astrojs/markdoc/runtime';
|
||||
import { getConfig, getConfigSync } from ${JSON.stringify(markdocConfigId)};
|
||||
const stringifiedAst = ${JSON.stringify(
|
||||
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
|
||||
)};
|
||||
export function getHeadings() {
|
||||
${
|
||||
/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
|
||||
TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself,
|
||||
instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
|
||||
''
|
||||
}
|
||||
const headingConfig = userConfig.nodes?.heading;
|
||||
const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {});
|
||||
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
||||
const content = Markdoc.transform(ast, config);
|
||||
return collectHeadings(Array.isArray(content) ? content : content.children);
|
||||
}
|
||||
export function getHeadings() {
|
||||
${
|
||||
/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
|
||||
TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself,
|
||||
instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
|
||||
''
|
||||
}
|
||||
const config = getConfigSync();
|
||||
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
||||
const content = Markdoc.transform(ast, config);
|
||||
return collectHeadings(Array.isArray(content) ? content : content.children);
|
||||
}
|
||||
|
||||
export const Content = createComponent({
|
||||
async factory(result, props) {
|
||||
const config = await setupConfig({
|
||||
...userConfig,
|
||||
variables: { ...userConfig.variables, ...props },
|
||||
});
|
||||
|
||||
return renderComponent(
|
||||
result,
|
||||
Renderer.name,
|
||||
Renderer,
|
||||
{ stringifiedAst, config },
|
||||
{}
|
||||
);
|
||||
},
|
||||
propagation: 'self',
|
||||
});`;
|
||||
export const Content = createComponent({
|
||||
async factory(result, props) {
|
||||
const config = await getConfig({ variables: props });
|
||||
return renderComponent(
|
||||
result,
|
||||
Renderer.name,
|
||||
Renderer,
|
||||
{ stringifiedAst, config },
|
||||
{}
|
||||
);
|
||||
},
|
||||
propagation: 'self',
|
||||
});`;
|
||||
return { code: res };
|
||||
},
|
||||
contentModuleTypes: await fs.promises.readFile(
|
||||
|
@ -227,6 +213,7 @@ export const Content = createComponent({
|
|||
}
|
||||
},
|
||||
},
|
||||
vitePluginMarkdocConfig({ astroConfig }),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { MarkdownHeading } from '@astrojs/markdown-remark';
|
||||
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc';
|
||||
import type { AstroInstance } from 'astro';
|
||||
import Markdoc, { type NodeType, type RenderableTreeNode } from '@markdoc/markdoc';
|
||||
import type { AstroMarkdocConfig } from './config.js';
|
||||
import { setupHeadingConfig } from './heading-ids.js';
|
||||
|
||||
|
@ -39,7 +40,10 @@ export function setupConfigSync(
|
|||
}
|
||||
|
||||
/** Merge function from `@markdoc/markdoc` internals */
|
||||
function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig): AstroMarkdocConfig {
|
||||
export function mergeConfig(
|
||||
configA: AstroMarkdocConfig,
|
||||
configB: AstroMarkdocConfig
|
||||
): AstroMarkdocConfig {
|
||||
return {
|
||||
...configA,
|
||||
...configB,
|
||||
|
@ -66,6 +70,22 @@ function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig):
|
|||
};
|
||||
}
|
||||
|
||||
export function resolveComponentImports(
|
||||
markdocConfig: Required<Pick<AstroMarkdocConfig, 'tags' | 'nodes'>>,
|
||||
tagComponentMap: Record<string, AstroInstance['default']>,
|
||||
nodeComponentMap: Record<NodeType, AstroInstance['default']>
|
||||
) {
|
||||
for (const [tag, render] of Object.entries(tagComponentMap)) {
|
||||
const config = markdocConfig.tags[tag];
|
||||
if (config) config.render = render;
|
||||
}
|
||||
for (const [node, render] of Object.entries(nodeComponentMap)) {
|
||||
const config = markdocConfig.nodes[node as NodeType];
|
||||
if (config) config.render = render;
|
||||
}
|
||||
return markdocConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text content as a string from a Markdoc transform AST
|
||||
*/
|
||||
|
|
100
packages/integrations/markdoc/src/vite-plugin-config.ts
Normal file
100
packages/integrations/markdoc/src/vite-plugin-config.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import type { AstroConfig } from 'astro';
|
||||
import type { Plugin } from 'vite';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import { loadMarkdocConfig } from './load-config.js';
|
||||
import type { Schema, Config as MarkdocConfig } from '@markdoc/markdoc';
|
||||
import type { Render } from './config.js';
|
||||
import { setupConfig } from './runtime.js';
|
||||
|
||||
export const markdocConfigId = 'astro:markdoc-config';
|
||||
export const resolvedMarkdocConfigId = '\x00' + markdocConfigId;
|
||||
|
||||
export function vitePluginMarkdocConfig({ astroConfig }: { astroConfig: AstroConfig }): Plugin {
|
||||
return {
|
||||
name: '@astrojs/markdoc:config',
|
||||
resolveId(this: PluginContext, id: string) {
|
||||
if (id === markdocConfigId) {
|
||||
return resolvedMarkdocConfigId;
|
||||
}
|
||||
},
|
||||
async load(id: string) {
|
||||
if (id !== resolvedMarkdocConfigId) return;
|
||||
|
||||
// TODO: invalidate on change
|
||||
const markdocConfigResult = await loadMarkdocConfig(astroConfig);
|
||||
// Only add `astro/assets` import when `experimental.assets` is enabled. Would throw without this check!
|
||||
const injectAssetsConfig = astroConfig.experimental.assets;
|
||||
|
||||
if (!markdocConfigResult) {
|
||||
return `import { mergeConfig } from '@astrojs/markdoc/runtime';${
|
||||
injectAssetsConfig
|
||||
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';`
|
||||
: ''
|
||||
}export async function getConfig(runtimeConfig = {}) { return ${
|
||||
injectAssetsConfig
|
||||
? 'mergeConfig(experimentalAssetsConfig, runtimeConfig)'
|
||||
: 'runtimeConfig'
|
||||
} }
|
||||
export function getConfigSync() { return ${
|
||||
injectAssetsConfig ? 'experimentalAssetsConfig' : '{}'
|
||||
} }`;
|
||||
}
|
||||
const { config: unresolvedConfig, fileUrl } = markdocConfigResult;
|
||||
const config = await setupConfig(unresolvedConfig);
|
||||
const tagRenderPathnameMap = getRenderUrlMap(config.tags ?? {});
|
||||
const nodeRenderPathnameMap = getRenderUrlMap(config.nodes ?? {});
|
||||
|
||||
const code = `import { setupConfig, mergeConfig, setupConfigSync, resolveComponentImports } from '@astrojs/markdoc/runtime';
|
||||
import userConfig from ${JSON.stringify(fileUrl.pathname)};${
|
||||
astroConfig.experimental.assets
|
||||
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };`
|
||||
: ''
|
||||
}
|
||||
${getStringifiedImports(tagRenderPathnameMap, 'Tag')};
|
||||
${getStringifiedImports(nodeRenderPathnameMap, 'Node')};
|
||||
|
||||
const tagComponentMap = ${getStringifiedMap(tagRenderPathnameMap, 'Tag')};
|
||||
const nodeComponentMap = ${getStringifiedMap(nodeRenderPathnameMap, 'Node')};
|
||||
|
||||
export async function getConfig(runtimeConfig = {}) {
|
||||
const config = await setupConfig(mergeConfig(userConfig, runtimeConfig));
|
||||
return resolveComponentImports(config, tagComponentMap, nodeComponentMap);
|
||||
}
|
||||
|
||||
${/* used by `getHeadings()`. This bypasses `extends` resolution, which can be async */ ''}
|
||||
export function getConfigSync() {
|
||||
return setupConfigSync(userConfig);
|
||||
}`;
|
||||
return code;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getRenderUrlMap(tagsOrNodes: Record<string, Schema<MarkdocConfig, Render>>) {
|
||||
const renderPathnameMap: Record<string, string> = {};
|
||||
for (const [name, value] of Object.entries(tagsOrNodes)) {
|
||||
if (value.render instanceof URL) {
|
||||
renderPathnameMap[name] = value.render.pathname;
|
||||
}
|
||||
}
|
||||
return renderPathnameMap;
|
||||
}
|
||||
|
||||
function getStringifiedImports(renderUrlMap: Record<string, string>, componentNamePrefix: string) {
|
||||
let stringifiedComponentImports = '';
|
||||
for (const [key, renderUrl] of Object.entries(renderUrlMap)) {
|
||||
stringifiedComponentImports += `import ${componentNamePrefix + key} from ${JSON.stringify(
|
||||
renderUrl + '?astroPropagatedAssets'
|
||||
)};\n`;
|
||||
}
|
||||
return stringifiedComponentImports;
|
||||
}
|
||||
|
||||
function getStringifiedMap(renderUrlMap: Record<string, string>, componentNamePrefix: string) {
|
||||
let stringifiedComponentMap = '{';
|
||||
for (const key in renderUrlMap) {
|
||||
stringifiedComponentMap += `${key}: ${componentNamePrefix + key},\n`;
|
||||
}
|
||||
stringifiedComponentMap += '}';
|
||||
return stringifiedComponentMap;
|
||||
}
|
Loading…
Reference in a new issue