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 type { AstroInstance } from 'astro';
|
||||||
import { heading } from './heading-ids.js';
|
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<
|
export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = Omit<
|
||||||
MarkdocConfig,
|
MarkdocConfig,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import type * as rollup from 'rollup';
|
||||||
import { normalizePath } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';
|
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';
|
||||||
import { setupConfig } from './runtime.js';
|
import { setupConfig } from './runtime.js';
|
||||||
|
import { markdocConfigId, vitePluginMarkdocConfig } from './vite-plugin-config.js';
|
||||||
|
|
||||||
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
||||||
// `contentEntryType` is not a public API
|
// `contentEntryType` is not a public API
|
||||||
|
@ -129,52 +130,37 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
|
||||||
renderComponent,
|
renderComponent,
|
||||||
} from 'astro/runtime/server/index.js';
|
} from 'astro/runtime/server/index.js';
|
||||||
import { Renderer } from '@astrojs/markdoc/components';
|
import { Renderer } from '@astrojs/markdoc/components';
|
||||||
import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
|
import { collectHeadings, Markdoc } from '@astrojs/markdoc/runtime';
|
||||||
${
|
import { getConfig, getConfigSync } from ${JSON.stringify(markdocConfigId)};
|
||||||
markdocConfigResult
|
const stringifiedAst = ${JSON.stringify(
|
||||||
? `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(
|
|
||||||
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
|
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
|
||||||
)};
|
)};
|
||||||
export function getHeadings() {
|
export function getHeadings() {
|
||||||
${
|
${
|
||||||
/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
|
/* 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,
|
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. */
|
instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
const headingConfig = userConfig.nodes?.heading;
|
const config = getConfigSync();
|
||||||
const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {});
|
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
||||||
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
const content = Markdoc.transform(ast, config);
|
||||||
const content = Markdoc.transform(ast, config);
|
return collectHeadings(Array.isArray(content) ? content : content.children);
|
||||||
return collectHeadings(Array.isArray(content) ? content : content.children);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const Content = createComponent({
|
export const Content = createComponent({
|
||||||
async factory(result, props) {
|
async factory(result, props) {
|
||||||
const config = await setupConfig({
|
const config = await getConfig({ variables: props });
|
||||||
...userConfig,
|
return renderComponent(
|
||||||
variables: { ...userConfig.variables, ...props },
|
result,
|
||||||
});
|
Renderer.name,
|
||||||
|
Renderer,
|
||||||
return renderComponent(
|
{ stringifiedAst, config },
|
||||||
result,
|
{}
|
||||||
Renderer.name,
|
);
|
||||||
Renderer,
|
},
|
||||||
{ stringifiedAst, config },
|
propagation: 'self',
|
||||||
{}
|
});`;
|
||||||
);
|
|
||||||
},
|
|
||||||
propagation: 'self',
|
|
||||||
});`;
|
|
||||||
return { code: res };
|
return { code: res };
|
||||||
},
|
},
|
||||||
contentModuleTypes: await fs.promises.readFile(
|
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 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 type { AstroMarkdocConfig } from './config.js';
|
||||||
import { setupHeadingConfig } from './heading-ids.js';
|
import { setupHeadingConfig } from './heading-ids.js';
|
||||||
|
|
||||||
|
@ -39,7 +40,10 @@ export function setupConfigSync(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Merge function from `@markdoc/markdoc` internals */
|
/** Merge function from `@markdoc/markdoc` internals */
|
||||||
function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig): AstroMarkdocConfig {
|
export function mergeConfig(
|
||||||
|
configA: AstroMarkdocConfig,
|
||||||
|
configB: AstroMarkdocConfig
|
||||||
|
): AstroMarkdocConfig {
|
||||||
return {
|
return {
|
||||||
...configA,
|
...configA,
|
||||||
...configB,
|
...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
|
* 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…
Add table
Reference in a new issue