Skip to content

Commit

Permalink
[Markdoc] Validation and debugging improvements (#7045)
Browse files Browse the repository at this point in the history
* feat: better validation logs

* chore: add warning to restart server on config chnage

* feat: expose Markdoc global from markdoc/config

* docs: update `nodes` reference

* chore: changeset

* docs: simplify headings explainer

* chore: ignore eslint log errors

* fix: make legacyConfig prop optional
  • Loading branch information
bholmesdev authored May 9, 2023
1 parent 18d0632 commit 3a9f72c
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/nine-wolves-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/markdoc': patch
---

Improve Markdoc validation errors with full message and file preview.
14 changes: 6 additions & 8 deletions packages/integrations/markdoc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,28 +143,26 @@ Use tags like this fancy "aside" to add some *flair* to your docs.

#### Render Markdoc nodes / HTML elements as Astro components

You may also want to map standard HTML elements like headings and paragraphs to components. For this, you can configure a custom [Markdoc node][markdoc-nodes]. This example overrides Markdoc's `heading` node to render a `Heading` component, passing the built-in `level` attribute as a prop:
You may also want to map standard HTML elements like headings and paragraphs to components. For this, you can configure a custom [Markdoc node][markdoc-nodes]. This example overrides Markdoc's `heading` node to render a `Heading` component, and passes through [Markdoc's default attributes for headings](https://markdoc.dev/docs/nodes#built-in-nodes).

```js
// markdoc.config.mjs
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
import { defineMarkdocConfig, Markdoc } from '@astrojs/markdoc/config';
import Heading from './src/components/Heading.astro';

export default defineMarkdocConfig({
nodes: {
heading: {
render: Heading,
attributes: {
// Pass the attributes from Markdoc's default heading node
// as component props.
level: { type: String },
}
attributes: Markdoc.nodes.heading.attributes,
},
},
})
```

Now, all Markdown headings will render with the `Heading.astro` component. This example uses a level 3 heading, automatically passing `level: 3` as the component prop:
Now, all Markdown headings will render with the `Heading.astro` component, and pass these `attributes` as component props. For headings, Markdoc provides a `level` attribute containing the numeric heading level.

This example uses a level 3 heading, automatically passing `level: 3` as the component prop:

```md
### I'm a level 3 heading!
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/markdoc/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ConfigType as MarkdocConfig } from '@markdoc/markdoc';
export { default as Markdoc } from '@markdoc/markdoc';

export function defineMarkdocConfig(config: MarkdocConfig): MarkdocConfig {
return config;
Expand Down
60 changes: 42 additions & 18 deletions packages/integrations/markdoc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
/* eslint-disable no-console */
import type { Node } from '@markdoc/markdoc';
import Markdoc from '@markdoc/markdoc';
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js';
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
import { emitESMImage } from 'astro/assets';
import { bold, red } from 'kleur/colors';
import { bold, red, yellow } from 'kleur/colors';
import type * as rollup from 'rollup';
import { applyDefaultConfig } from './default-config.js';
import { loadMarkdocConfig } from './load-config.js';
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';

type SetupHookParams = HookParameters<'astro:config:setup'> & {
// `contentEntryType` is not a public API
// Add type defs here
addContentEntryType: (contentEntryType: ContentEntryType) => void;
};

export default function markdocIntegration(legacyConfig: any): AstroIntegration {
export default function markdocIntegration(legacyConfig?: any): AstroIntegration {
if (legacyConfig) {
// eslint-disable-next-line no-console
console.log(
`${red(
bold('[Markdoc]')
)} Passing Markdoc config from your \`astro.config\` is no longer supported. Configuration should be exported from a \`markdoc.config.mjs\` file. See the configuration docs for more: https://docs.astro.build/en/guides/integrations-guide/markdoc/#configuration`
);
process.exit(0);
}
let markdocConfigResult: MarkdocConfigResult | undefined;
return {
name: '@astrojs/markdoc',
hooks: {
'astro:config:setup': async (params) => {
const { config: astroConfig, addContentEntryType } = params as SetupHookParams;

const configLoadResult = await loadMarkdocConfig(astroConfig);
const userMarkdocConfig = configLoadResult?.config ?? {};
markdocConfigResult = await loadMarkdocConfig(astroConfig);
const userMarkdocConfig = markdocConfigResult?.config ?? {};

function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
Expand All @@ -54,17 +55,28 @@ export default function markdocIntegration(legacyConfig: any): AstroIntegration
const markdocConfig = applyDefaultConfig(userMarkdocConfig, { entry });

const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
// Ignore `variable-undefined` errors.
// Variables can be configured at runtime,
// so we cannot validate them at build time.
return e.error.id !== 'variable-undefined';
return (
// Ignore `variable-undefined` errors.
// Variables can be configured at runtime,
// so we cannot validate them at build time.
e.error.id !== 'variable-undefined' &&
(e.error.level === 'error' || e.error.level === 'critical')
);
});
if (validationErrors.length) {
// Heuristic: take number of newlines for `rawData` and add 2 for the `---` fences
const frontmatterBlockOffset = entry._internal.rawData.split('\n').length + 2;
throw new MarkdocError({
message: [
`**${String(entry.collection)}${String(entry.id)}** failed to validate:`,
...validationErrors.map((e) => e.error.id),
`**${String(entry.collection)}${String(entry.id)}** contains invalid content:`,
...validationErrors.map((e) => `- ${e.error.message}`),
].join('\n'),
location: {
// Error overlay does not support multi-line or ranges.
// Just point to the first line.
line: frontmatterBlockOffset + validationErrors[0].lines[0],
file: viteId,
},
});
}

Expand All @@ -76,13 +88,15 @@ export default function markdocIntegration(legacyConfig: any): AstroIntegration
});
}

const code = {
return {
code: `import { jsx as h } from 'astro/jsx-runtime';
import { applyDefaultConfig } from '@astrojs/markdoc/default-config';
import { Renderer } from '@astrojs/markdoc/components';
import * as entry from ${JSON.stringify(viteId + '?astroContent')};${
configLoadResult
? `\nimport userConfig from ${JSON.stringify(configLoadResult.fileUrl.pathname)};`
markdocConfigResult
? `\nimport userConfig from ${JSON.stringify(
markdocConfigResult.fileUrl.pathname
)};`
: ''
}${
astroConfig.experimental.assets
Expand All @@ -94,7 +108,7 @@ const stringifiedAst = ${JSON.stringify(
)};
export async function Content (props) {
const config = applyDefaultConfig(${
configLoadResult
markdocConfigResult
? '{ ...userConfig, variables: { ...userConfig.variables, ...props } }'
: '{ variables: props }'
}, { entry });${
Expand All @@ -104,14 +118,24 @@ export async function Content (props) {
}
return h(Renderer, { stringifiedAst, config }); };`,
};
return code;
},
contentModuleTypes: await fs.promises.readFile(
new URL('../template/content-module-types.d.ts', import.meta.url),
'utf-8'
),
});
},
'astro:server:setup': async ({ server }) => {
server.watcher.on('all', (event, entry) => {
if (pathToFileURL(entry).pathname === markdocConfigResult?.fileUrl.pathname) {
console.log(
yellow(
`${bold('[Markdoc]')} Restart the dev server for config changes to take effect.`
)
);
}
});
},
},
};
}
Expand Down
9 changes: 8 additions & 1 deletion packages/integrations/markdoc/src/load-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ const SUPPORTED_MARKDOC_CONFIG_FILES = [
'markdoc.config.ts',
];

export async function loadMarkdocConfig(astroConfig: Pick<AstroConfig, 'root'>) {
export type MarkdocConfigResult = {
config: MarkdocConfig;
fileUrl: URL;
};

export async function loadMarkdocConfig(
astroConfig: Pick<AstroConfig, 'root'>
): Promise<MarkdocConfigResult | undefined> {
let markdocConfigUrl: URL | undefined;
for (const filename of SUPPORTED_MARKDOC_CONFIG_FILES) {
const filePath = new URL(filename, astroConfig.root);
Expand Down

0 comments on commit 3a9f72c

Please sign in to comment.