Compare commits

...

9 commits

Author SHA1 Message Date
bholmesdev
68c04c28d5 chore: formatting 2023-06-22 12:31:32 -04:00
bholmesdev
bcf1cde198 fix: correctly merge runtime config 2023-06-22 12:31:32 -04:00
bholmesdev
77971eec8e fix: experimentalAssetsConfig missing 2023-06-22 12:31:32 -04:00
bholmesdev
b46b0ec64c fix: bad AstroMarkdocConfig type 2023-06-22 12:31:32 -04:00
bholmesdev
e9866bab23 chore: changeset 2023-06-22 12:31:32 -04:00
bholmesdev
abff1d696c feat: support extends with URL 2023-06-22 12:31:32 -04:00
bholmesdev
321164503a feat: support URL for markdoc nodes 2023-06-22 12:31:32 -04:00
bholmesdev
0ab7d0dbd1 refactor: move to separate file 2023-06-22 12:31:32 -04:00
bholmesdev
d87544c241 feat: URL support for markdoc tags 2023-06-22 12:31:32 -04:00
5 changed files with 158 additions and 46 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/markdoc': patch
---
Allow component import paths to be passed as URLs to Markdoc tag and node renderers.

View file

@ -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,

View file

@ -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,42 +130,27 @@ 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() {
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 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({
export const Content = createComponent({
async factory(result, props) {
const config = await setupConfig({
...userConfig,
variables: { ...userConfig.variables, ...props },
});
const config = await getConfig({ variables: props });
return renderComponent(
result,
Renderer.name,
@ -174,7 +160,7 @@ export const Content = createComponent({
);
},
propagation: 'self',
});`;
});`;
return { code: res };
},
contentModuleTypes: await fs.promises.readFile(
@ -227,6 +213,7 @@ export const Content = createComponent({
}
},
},
vitePluginMarkdocConfig({ astroConfig }),
],
},
});

View file

@ -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
*/

View 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;
}