chore: validate tags and nodes

This commit is contained in:
bholmesdev 2023-03-02 12:50:20 -05:00
parent 8c348503fc
commit 9e2808c58c
2 changed files with 132 additions and 3 deletions

View file

@ -1,12 +1,12 @@
import type { AstroIntegration } from 'astro'; import type { AstroIntegration, AstroConfig } from 'astro';
import type { InlineConfig } from 'vite'; import type { InlineConfig } from 'vite';
import type { Config } from '@markdoc/markdoc'; import type { Config } from '@markdoc/markdoc';
import Markdoc from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc';
import { parseFrontmatter } from './utils.js'; import { getAstroConfigPath, MarkdocError, parseFrontmatter } from './utils.js';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import fs from 'node:fs'; import fs from 'node:fs';
export default function markdoc(markdocConfig: Config): AstroIntegration { export default function markdoc(markdocConfig: Config = {}): AstroIntegration {
const entryBodyByFileIdCache = new Map<string, string>(); const entryBodyByFileIdCache = new Map<string, string>();
return { return {
name: '@astrojs/markdoc', name: '@astrojs/markdoc',
@ -38,6 +38,7 @@ export default function markdoc(markdocConfig: Config): AstroIntegration {
async transform(code, id) { async transform(code, id) {
if (!id.endsWith('.mdoc')) return; if (!id.endsWith('.mdoc')) return;
validateRenderProperties(markdocConfig, config);
const body = entryBodyByFileIdCache.get(id); const body = entryBodyByFileIdCache.get(id);
if (!body) { if (!body) {
// Cache entry should exist if `getCollection()` was called // Cache entry should exist if `getCollection()` was called
@ -64,3 +65,52 @@ export default function markdoc(markdocConfig: Config): AstroIntegration {
}, },
}; };
} }
function validateRenderProperties(markdocConfig: Config, astroConfig: AstroConfig) {
const tags = markdocConfig.tags ?? {};
const nodes = markdocConfig.nodes ?? {};
for (const [name, config] of Object.entries(tags)) {
validateRenderProperty({ type: 'tag', name, config, astroConfig });
}
for (const [name, config] of Object.entries(nodes)) {
validateRenderProperty({ type: 'node', name, config, astroConfig });
}
}
function validateRenderProperty({
name,
config,
type,
astroConfig,
}: {
name: string;
config: { render?: string };
type: 'node' | 'tag';
astroConfig: Pick<AstroConfig, 'root'>;
}) {
if (typeof config.render === 'string' && config.render.length === 0) {
throw new Error(
`Invalid ${type} configuration: ${JSON.stringify(
name
)}. The "render" property cannot be an empty string.`
);
}
if (typeof config.render === 'string' && !isCapitalized(config.render)) {
const astroConfigPath = getAstroConfigPath(fs, fileURLToPath(astroConfig.root));
throw new MarkdocError({
message: `Invalid ${type} configuration: ${JSON.stringify(
name
)}. The "render" property must reference a capitalized component name. If you want to render to an HTML element, see our docs on rendering Markdoc manually [TODO docs link].`,
location: astroConfigPath
? {
file: astroConfigPath,
}
: undefined,
});
}
}
function isCapitalized(str: string) {
return str.length > 0 && str[0] === str[0].toUpperCase();
}

View file

@ -1,4 +1,6 @@
import matter from 'gray-matter'; import matter from 'gray-matter';
import path from 'node:path';
import type fsMod from 'node:fs';
import type { ErrorPayload as ViteErrorPayload } from 'vite'; import type { ErrorPayload as ViteErrorPayload } from 'vite';
/** /**
@ -23,3 +25,80 @@ export function parseFrontmatter(fileContents: string, filePath: string) {
} }
} }
} }
/**
* Matches AstroError object with types like error codes stubbed out
* @see 'astro/src/core/errors/errors.ts'
*/
export class MarkdocError extends Error {
public errorCode: number;
public loc: ErrorLocation | undefined;
public title: string | undefined;
public hint: string | undefined;
public frame: string | undefined;
type = 'MarkdocError';
constructor(props: ErrorProperties, ...params: any) {
super(...params);
const {
code = 99999,
name,
title = 'MarkdocError',
message,
stack,
location,
hint,
frame,
} = props;
this.errorCode = code;
this.title = title;
if (message) this.message = message;
// Only set this if we actually have a stack passed, otherwise uses Error's
this.stack = stack ? stack : this.stack;
this.loc = location;
this.hint = hint;
this.frame = frame;
}
}
interface ErrorLocation {
file?: string;
line?: number;
column?: number;
}
interface ErrorProperties {
code?: number;
title?: string;
name?: string;
message?: string;
location?: ErrorLocation;
hint?: string;
stack?: string;
frame?: string;
}
/**
* Matches `search` function used for resolving `astro.config` files.
* Used by Markdoc for error handling.
* @see 'astro/src/core/config/config.ts'
*/
export function getAstroConfigPath(fs: typeof fsMod, root: string): string | undefined {
const paths = [
'astro.config.mjs',
'astro.config.js',
'astro.config.ts',
'astro.config.mts',
'astro.config.cjs',
'astro.config.cts',
].map((p) => path.join(root, p));
for (const file of paths) {
if (fs.existsSync(file)) {
return file;
}
}
}