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 { 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();
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue