feat: addContentEntryType integration hook

This commit is contained in:
bholmesdev 2023-02-07 16:01:34 -05:00
parent b97ea127e0
commit 092a1b1541
12 changed files with 135 additions and 43 deletions

View file

@ -978,12 +978,27 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
integrations: AstroIntegration[]; 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 { export interface AstroSettings {
config: AstroConfig; config: AstroConfig;
adapter: AstroAdapter | undefined; adapter: AstroAdapter | undefined;
injectedRoutes: InjectedRoute[]; injectedRoutes: InjectedRoute[];
pageExtensions: string[]; pageExtensions: string[];
contentEntryTypes: ContentEntryType[];
renderers: AstroRenderer[]; renderers: AstroRenderer[];
scripts: { scripts: {
stage: InjectedScriptStage; stage: InjectedScriptStage;

View file

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

View file

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

View file

@ -7,8 +7,7 @@ import { ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from '
import { z } from 'zod'; import { z } from 'zod';
import { AstroConfig, AstroSettings } from '../@types/astro.js'; import { AstroConfig, AstroSettings } from '../@types/astro.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { appendForwardSlash } from '../core/path.js'; import { CONTENT_TYPES_FILE, defaultContentEntryExts } from './consts.js';
import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js';
export const collectionConfigParser = z.object({ export const collectionConfigParser = z.object({
schema: z.any().optional(), schema: z.any().optional(),
@ -119,6 +118,14 @@ export async function getEntryData(
return data; 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 class NoCollectionError extends Error {}
export function getEntryInfo( export function getEntryInfo(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,47 @@
import type { AstroIntegration } from 'astro'; import type { AstroIntegration } from 'astro';
import type { InlineConfig } from 'vite'; import type { InlineConfig } from 'vite';
import _Markdoc from '@markdoc/markdoc'; 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 { export default function markdoc(partialOptions: {} = {}): AstroIntegration {
return { return {
name: '@astrojs/markdoc', name: '@astrojs/markdoc',
hooks: { hooks: {
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => { 'astro:config:setup': async ({ updateConfig, config, addContentEntryType, command }: any) => {
addPageExtension('.mdoc'); addContentEntryType(contentEntryType);
console.log('Markdoc working!'); console.log('Markdoc working!');
const markdocConfigUrl = new URL('./markdoc.config.ts', config.srcDir); 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;
}
}
}