chore: validate tags and nodes
This commit is contained in:
parent
8c348503fc
commit
9e2808c58c
2 changed files with 132 additions and 3 deletions
|
@ -1,12 +1,12 @@
|
|||
import type { AstroIntegration } from 'astro';
|
||||
import type { AstroIntegration, AstroConfig } from 'astro';
|
||||
import type { InlineConfig } from 'vite';
|
||||
import type { Config } 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 fs from 'node:fs';
|
||||
|
||||
export default function markdoc(markdocConfig: Config): AstroIntegration {
|
||||
export default function markdoc(markdocConfig: Config = {}): AstroIntegration {
|
||||
const entryBodyByFileIdCache = new Map<string, string>();
|
||||
return {
|
||||
name: '@astrojs/markdoc',
|
||||
|
@ -38,6 +38,7 @@ export default function markdoc(markdocConfig: Config): AstroIntegration {
|
|||
async transform(code, id) {
|
||||
if (!id.endsWith('.mdoc')) return;
|
||||
|
||||
validateRenderProperties(markdocConfig, config);
|
||||
const body = entryBodyByFileIdCache.get(id);
|
||||
if (!body) {
|
||||
// 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();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import matter from 'gray-matter';
|
||||
import path from 'node:path';
|
||||
import type fsMod from 'node:fs';
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue