feat: addContentEntryType integration hook
This commit is contained in:
parent
dbef6c49c8
commit
1de946d898
12 changed files with 135 additions and 42 deletions
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) };
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
25
packages/integrations/markdoc/src/utils.ts
Normal file
25
packages/integrations/markdoc/src/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue