feat: addContentEntryType integration hook

This commit is contained in:
bholmesdev 2023-02-07 16:01:34 -05:00
parent dbef6c49c8
commit 1de946d898
12 changed files with 135 additions and 42 deletions

View file

@ -977,12 +977,27 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
integrations: AstroIntegration[];
}
export interface ContentEntryType {
extensions: string[];
getEntryInfo(params: { fileUrl: URL }): Promise<{
data: Record<string, unknown>;
/**
* Used for error hints to point to correct line and location
* Should be the untouched data as read from the file,
* including newlines
*/
rawData: string;
body: string;
slug: string;
}>;
}
export interface AstroSettings {
config: AstroConfig;
adapter: AstroAdapter | undefined;
injectedRoutes: InjectedRoute[];
pageExtensions: string[];
contentEntryTypes: ContentEntryType[];
renderers: AstroRenderer[];
scripts: {
stage: InjectedScriptStage;

View file

@ -1,5 +1,4 @@
/** TODO as const*/
export const defaultContentFileExts = ['.md', '.mdx'];
export const defaultContentEntryExts = ['.md', '.mdx'] as const;
export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets';
export const CONTENT_FLAG = 'astroContent';
export const VIRTUAL_MODULE_ID = 'astro:content';

View file

@ -7,12 +7,13 @@ import { normalizePath, ViteDevServer } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { info, LogOptions, warn } from '../core/logger/core.js';
import { isRelativePath } from '../core/path.js';
import { CONTENT_TYPES_FILE, defaultContentFileExts } from './consts.js';
import { CONTENT_TYPES_FILE } from './consts.js';
import {
ContentConfig,
ContentObservable,
ContentPaths,
EntryInfo,
getContentEntryExts,
getContentPaths,
getEntryInfo,
getEntrySlug,
@ -21,7 +22,6 @@ import {
NoCollectionError,
parseFrontmatter,
} from './utils.js';
import { contentEntryTypes } from './~dream.js';
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
type RawContentEvent = { name: ChokidarEvent; entry: string };
@ -52,10 +52,7 @@ export async function createContentTypesGenerator({
}: CreateContentGeneratorParams) {
const contentTypes: ContentTypes = {};
const contentPaths = getContentPaths(settings.config);
const contentFileExts = [
...defaultContentFileExts,
...contentEntryTypes.map((t) => t.extensions).flat(),
];
const contentEntryExts = getContentEntryExts(settings);
let events: Promise<{ shouldGenerateTypes: boolean; error?: Error }>[] = [];
let debounceTimeout: NodeJS.Timeout | undefined;
@ -116,7 +113,7 @@ export async function createContentTypesGenerator({
}
return { shouldGenerateTypes: true };
}
const fileType = getEntryType(fileURLToPath(event.entry), contentPaths, contentFileExts);
const fileType = getEntryType(fileURLToPath(event.entry), contentPaths, contentEntryExts);
if (fileType === 'ignored') {
return { shouldGenerateTypes: false };
}

View file

@ -8,7 +8,7 @@ import { z } from 'zod';
import { AstroConfig, AstroSettings } from '../@types/astro.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { appendForwardSlash } from '../core/path.js';
import { CONTENT_TYPES_FILE } from './consts.js';
import { CONTENT_TYPES_FILE, defaultContentEntryExts } from './consts.js';
export const collectionConfigParser = z.object({
schema: z.any().optional(),
@ -119,6 +119,14 @@ export async function getEntryData(
return data;
}
export function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryTypes'>) {
return [
// TODO: roll defaults into settings
...defaultContentEntryExts,
...settings.contentEntryTypes.map((t) => t.extensions).flat(),
];
}
export class NoCollectionError extends Error {}
export function getEntryInfo(

View file

@ -1,6 +1,7 @@
import npath from 'node:path';
import { pathToFileURL } from 'url';
import type { Plugin } from 'vite';
import { AstroSettings } from '../@types/astro.js';
import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js';
import { BuildInternals, getPageDataByViteID } from '../core/build/internal.js';
import { AstroBuildPlugin } from '../core/build/plugin.js';
@ -11,23 +12,30 @@ import { prependForwardSlash } from '../core/path.js';
import { getStylesForURL } from '../core/render/dev/css.js';
import { getScriptsForURL } from '../core/render/dev/scripts.js';
import {
defaultContentFileExts,
LINKS_PLACEHOLDER,
PROPAGATED_ASSET_FLAG,
SCRIPTS_PLACEHOLDER,
STYLES_PLACEHOLDER,
} from './consts.js';
import { getContentEntryExts } from './utils.js';
function isPropagatedAsset(viteId: string): boolean {
function isPropagatedAsset(viteId: string, contentEntryExts: string[]): boolean {
const url = new URL(viteId, 'file://');
return (
url.searchParams.has(PROPAGATED_ASSET_FLAG) &&
defaultContentFileExts.some((ext) => url.pathname.endsWith(ext))
contentEntryExts.some((ext) => url.pathname.endsWith(ext))
);
}
export function astroContentAssetPropagationPlugin({ mode }: { mode: string }): Plugin {
export function astroContentAssetPropagationPlugin({
mode,
settings,
}: {
mode: string;
settings: AstroSettings;
}): Plugin {
let devModuleLoader: ModuleLoader;
const contentEntryExts = getContentEntryExts(settings);
return {
name: 'astro:content-asset-propagation',
enforce: 'pre',
@ -37,7 +45,7 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }):
}
},
load(id) {
if (isPropagatedAsset(id)) {
if (isPropagatedAsset(id, contentEntryExts)) {
const basePath = id.split('?')[0];
const code = `
export async function getMod() {
@ -52,7 +60,7 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }):
},
async transform(code, id, options) {
if (!options?.ssr) return;
if (devModuleLoader && isPropagatedAsset(id)) {
if (devModuleLoader && isPropagatedAsset(id, contentEntryExts)) {
const basePath = id.split('?')[0];
if (!devModuleLoader.getModuleById(basePath)?.ssrModule) {
await devModuleLoader.import(basePath);

View file

@ -1,4 +1,3 @@
import { contentEntryTypes } from './~dream.js';
import * as devalue from 'devalue';
import type fsMod from 'node:fs';
import { pathToFileURL } from 'url';
@ -7,9 +6,10 @@ import { AstroSettings } from '../@types/astro.js';
import { AstroErrorData } from '../core/errors/errors-data.js';
import { AstroError } from '../core/errors/errors.js';
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
import { defaultContentFileExts, CONTENT_FLAG } from './consts.js';
import { CONTENT_FLAG } from './consts.js';
import {
ContentConfig,
getContentEntryExts,
getContentPaths,
getEntryData,
getEntryInfo,
@ -19,9 +19,9 @@ import {
parseFrontmatter,
} from './utils.js';
function isContentFlagImport(viteId: string) {
const { searchParams } = new URL(viteId, 'file://');
return searchParams.has(CONTENT_FLAG);
function isContentFlagImport(viteId: string, contentEntryExts: string[]) {
const { searchParams, pathname } = new URL(viteId, 'file://');
return searchParams.has(CONTENT_FLAG) && contentEntryExts.some((ext) => pathname.endsWith(ext));
}
export function astroContentImportPlugin({
@ -32,16 +32,13 @@ export function astroContentImportPlugin({
settings: AstroSettings;
}): Plugin {
const contentPaths = getContentPaths(settings.config);
const contentFileExts = [
...defaultContentFileExts,
...contentEntryTypes.map((t) => t.extensions).flat(),
];
const contentEntryExts = getContentEntryExts(settings);
return {
name: 'astro:content-imports',
async load(id) {
const { fileId } = getFileInfo(id, settings.config);
if (isContentFlagImport(id)) {
if (isContentFlagImport(id, contentEntryExts)) {
const observable = globalContentConfigObserver.get();
// Content config should be loaded before this plugin is used
@ -69,7 +66,7 @@ export function astroContentImportPlugin({
});
}
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
const contentEntryType = contentEntryTypes.find((entryType) =>
const contentEntryType = settings.contentEntryTypes.find((entryType) =>
entryType.extensions.some((ext) => fileId.endsWith(ext))
);
let body: string,
@ -124,11 +121,11 @@ export const _internal = {
viteServer.watcher.on('all', async (event, entry) => {
if (
['add', 'unlink', 'change'].includes(event) &&
getEntryType(entry, contentPaths, contentFileExts) === 'config'
getEntryType(entry, contentPaths, contentEntryExts) === 'config'
) {
// Content modules depend on config, so we need to invalidate them.
for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) {
if (isContentFlagImport(modUrl)) {
if (isContentFlagImport(modUrl, contentEntryExts)) {
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
if (mod) {
viteServer.moduleGraph.invalidateModule(mod);
@ -139,7 +136,7 @@ export const _internal = {
});
},
async transform(code, id) {
if (isContentFlagImport(id)) {
if (isContentFlagImport(id, contentEntryExts)) {
// Escape before Rollup internal transform.
// Base on MUCH trial-and-error, inspired by MDX integration 2-step transform.
return { code: escapeViteEnvReferences(code) };

View file

@ -4,9 +4,8 @@ import type { Plugin } from 'vite';
import { normalizePath } from 'vite';
import type { AstroSettings } from '../@types/astro.js';
import { appendForwardSlash, prependForwardSlash } from '../core/path.js';
import { defaultContentFileExts, VIRTUAL_MODULE_ID } from './consts.js';
import { getContentPaths } from './utils.js';
import { contentEntryTypes } from './~dream.js';
import { VIRTUAL_MODULE_ID } from './consts.js';
import { getContentEntryExts, getContentPaths } from './utils.js';
interface AstroContentVirtualModPluginParams {
settings: AstroSettings;
@ -23,12 +22,9 @@ export function astroContentVirtualModPlugin({
)
)
);
const contentFileExts = [
...defaultContentFileExts,
...contentEntryTypes.map((t) => t.extensions).flat(),
];
const contentEntryExts = getContentEntryExts(settings);
const entryGlob = `${relContentDir}**/*{${contentFileExts.join(',')}}`;
const entryGlob = `${relContentDir}**/*{${contentEntryExts.join(',')}}`;
const virtualModContents = fsMod
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
.replace('@@CONTENT_DIR@@', relContentDir)

View file

@ -15,6 +15,8 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
adapter: undefined,
injectedRoutes: [],
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
/** TODO: default Markdown entry type */
contentEntryTypes: [],
renderers: [jsxRenderer],
scripts: [],
watchFiles: [],

View file

@ -112,7 +112,7 @@ export async function createVite(
astroInjectEnvTsPlugin({ settings, logging, fs }),
astroContentVirtualModPlugin({ settings }),
astroContentImportPlugin({ fs, settings }),
astroContentAssetPropagationPlugin({ mode }),
astroContentAssetPropagationPlugin({ mode, settings }),
],
publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root),

View file

@ -8,6 +8,7 @@ import {
AstroRenderer,
AstroSettings,
BuildConfig,
ContentEntryType,
HookParameters,
RouteData,
} from '../@types/astro.js';
@ -100,11 +101,22 @@ export async function runHookConfigSetup({
const exts = (input.flat(Infinity) as string[]).map((ext) => `.${ext.replace(/^\./, '')}`);
updatedSettings.pageExtensions.push(...exts);
}
// Semi-private `addContentEntryType` hook
function addContentEntryType(contentEntryType: ContentEntryType) {
updatedSettings.contentEntryTypes.push(contentEntryType);
}
Object.defineProperty(hooks, 'addPageExtension', {
value: addPageExtension,
writable: false,
enumerable: false,
});
Object.defineProperty(hooks, 'addContentEntryType', {
value: addContentEntryType,
writable: false,
enumerable: false,
});
await withTakingALongTimeMsg({
name: integration.name,
hookResult: integration.hooks['astro:config:setup'](hooks),

View file

@ -1,13 +1,47 @@
import type { AstroIntegration } from 'astro';
import type { InlineConfig } from 'vite';
import _Markdoc from '@markdoc/markdoc';
import fs from 'node:fs';
import { parseFrontmatter } from './utils.js';
import { fileURLToPath } from 'node:url';
const contentEntryType = {
extensions: ['.mdoc'],
async getEntryInfo({ fileUrl }: { fileUrl: URL }) {
const rawContents = await fs.promises.readFile(fileUrl, 'utf-8');
const parsed = parseFrontmatter(rawContents, fileURLToPath(fileUrl));
return {
data: parsed.data,
body: parsed.content,
slug: parsed.data.slug,
rawData: parsed.matter,
};
},
async render({ entry }: { entry: any }) {
function getParsed() {
return Markdoc.parse(entry.body);
}
async function getTransformed(inlineConfig: any) {
let config = inlineConfig;
// TODO: load config file
// if (!config) {
// try {
// const importedConfig = await import('./markdoc.config.ts');
// config = importedConfig.default.transform;
// } catch {}
// }
return Markdoc.transform(getParsed(), config);
}
return { getParsed, getTransformed };
},
};
export default function markdoc(partialOptions: {} = {}): AstroIntegration {
return {
name: '@astrojs/markdoc',
hooks: {
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => {
addPageExtension('.mdoc');
'astro:config:setup': async ({ updateConfig, config, addContentEntryType, command }: any) => {
addContentEntryType(contentEntryType);
console.log('Markdoc working!');
const markdocConfigUrl = new URL('./markdoc.config.ts', config.srcDir);

View file

@ -0,0 +1,25 @@
import matter from 'gray-matter';
import type { ErrorPayload as ViteErrorPayload } from 'vite';
/**
* Match YAML exception handling from Astro core errors
* @see 'astro/src/core/errors.ts'
*/
export function parseFrontmatter(fileContents: string, filePath: string) {
try {
// `matter` is empty string on cache results
// clear cache to prevent this
(matter as any).clearCache();
return matter(fileContents);
} catch (e: any) {
if (e.name === 'YAMLException') {
const err: Error & ViteErrorPayload['err'] = e;
err.id = filePath;
err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
err.message = e.reason;
throw err;
} else {
throw e;
}
}
}