feat: get entry data in there

This commit is contained in:
bholmesdev 2023-03-17 16:16:50 -04:00
parent 484ccb1c81
commit ec003b5b53
3 changed files with 161 additions and 105 deletions

View file

@ -10,6 +10,7 @@ import type {
import type * as babel from '@babel/core'; import type * as babel from '@babel/core';
import type { OutgoingHttpHeaders } from 'http'; import type { OutgoingHttpHeaders } from 'http';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
import type * as rollup from 'rollup';
import type { TsConfigJson } from 'tsconfig-resolver'; import type { TsConfigJson } from 'tsconfig-resolver';
import type * as vite from 'vite'; import type * as vite from 'vite';
import type { z } from 'zod'; import type { z } from 'zod';
@ -1034,12 +1035,25 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
integrations: AstroIntegration[]; integrations: AstroIntegration[];
} }
export type ContentEntryModule = {
id: string;
collection: string;
slug: string;
body: string;
data: Record<string, unknown>;
_internal: {
rawData: string;
filePath: string;
};
};
export interface ContentEntryType { export interface ContentEntryType {
extensions: string[]; extensions: string[];
getEntryInfo(params: { getEntryInfo(params: {
fileUrl: URL; fileUrl: URL;
contents: string; contents: string;
}): GetEntryInfoReturnType | Promise<GetEntryInfoReturnType>; }): GetEntryInfoReturnType | Promise<GetEntryInfoReturnType>;
getModule?(params: { entry: ContentEntryModule }): rollup.LoadResult | Promise<rollup.LoadResult>;
contentModuleTypes?: string; contentModuleTypes?: string;
} }

View file

@ -1,6 +1,8 @@
import * as devalue from 'devalue'; import * as devalue from 'devalue';
import type fsMod from 'node:fs'; import type fsMod from 'node:fs';
import type { ContentEntryModule } from '../@types/astro.js';
import { extname } from 'node:path'; import { extname } from 'node:path';
import type { PluginContext } from 'rollup';
import { pathToFileURL } from 'url'; import { pathToFileURL } from 'url';
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
import type { AstroSettings, ContentEntryType } from '../@types/astro.js'; import type { AstroSettings, ContentEntryType } from '../@types/astro.js';
@ -10,6 +12,7 @@ import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index
import { CONTENT_FLAG } from './consts.js'; import { CONTENT_FLAG } from './consts.js';
import { import {
ContentConfig, ContentConfig,
EntryInfo,
getContentEntryExts, getContentEntryExts,
getContentPaths, getContentPaths,
getEntryData, getEntryData,
@ -17,6 +20,7 @@ import {
getEntrySlug, getEntrySlug,
getEntryType, getEntryType,
globalContentConfigObserver, globalContentConfigObserver,
NoCollectionError,
patchAssets, patchAssets,
} from './utils.js'; } from './utils.js';
function isContentFlagImport(viteId: string, contentEntryExts: string[]) { function isContentFlagImport(viteId: string, contentEntryExts: string[]) {
@ -30,7 +34,7 @@ export function astroContentImportPlugin({
}: { }: {
fs: typeof fsMod; fs: typeof fsMod;
settings: AstroSettings; settings: AstroSettings;
}): Plugin { }): Plugin[] {
const contentPaths = getContentPaths(settings.config, fs); const contentPaths = getContentPaths(settings.config, fs);
const contentEntryExts = getContentEntryExts(settings); const contentEntryExts = getContentEntryExts(settings);
@ -41,116 +45,159 @@ export function astroContentImportPlugin({
} }
} }
return { async function getEntryDataById(
name: 'astro:content-imports', fileId: string,
async load(id) { pluginContext: PluginContext
const { fileId } = getFileInfo(id, settings.config); ): Promise<ContentEntryModule> {
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
if (observable.status === 'init') { if (observable.status === 'init') {
throw new AstroError({ throw new AstroError({
...AstroErrorData.UnknownContentCollectionError, ...AstroErrorData.UnknownContentCollectionError,
message: 'Content config failed to load.', message: 'Content config failed to load.',
}); });
} }
if (observable.status === 'error') { if (observable.status === 'error') {
// Throw here to bubble content config errors // Throw here to bubble content config errors
// to the error overlay in development // to the error overlay in development
throw observable.error; throw observable.error;
} }
let contentConfig: ContentConfig | undefined = let contentConfig: ContentConfig | undefined =
observable.status === 'loaded' ? observable.config : undefined; observable.status === 'loaded' ? observable.config : undefined;
if (observable.status === 'loading') { if (observable.status === 'loading') {
// Wait for config to load // Wait for config to load
contentConfig = await new Promise((resolve) => { contentConfig = await new Promise((resolve) => {
const unsubscribe = globalContentConfigObserver.subscribe((ctx) => { const unsubscribe = globalContentConfigObserver.subscribe((ctx) => {
if (ctx.status === 'loaded') { if (ctx.status === 'loaded') {
resolve(ctx.config); resolve(ctx.config);
unsubscribe(); unsubscribe();
} else if (ctx.status === 'error') { } else if (ctx.status === 'error') {
resolve(undefined); resolve(undefined);
unsubscribe(); unsubscribe();
} }
});
});
}
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
const fileExt = extname(fileId);
if (!contentEntryExtToParser.has(fileExt)) {
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `No parser found for content entry ${JSON.stringify(
fileId
)}. Did you apply an integration for this file type?`,
});
}
const contentEntryParser = contentEntryExtToParser.get(fileExt)!;
const info = await contentEntryParser.getEntryInfo({
fileUrl: pathToFileURL(fileId),
contents: rawContents,
}); });
const generatedInfo = getEntryInfo({ });
entry: pathToFileURL(fileId), }
contentDir: contentPaths.contentDir, const rawContents = await fs.promises.readFile(fileId, 'utf-8');
}); const fileExt = extname(fileId);
if (generatedInfo instanceof Error) return; if (!contentEntryExtToParser.has(fileExt)) {
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `No parser found for content entry ${JSON.stringify(
fileId
)}. Did you apply an integration for this file type?`,
});
}
const contentEntryParser = contentEntryExtToParser.get(fileExt)!;
const info = await contentEntryParser.getEntryInfo({
fileUrl: pathToFileURL(fileId),
contents: rawContents,
});
const generatedInfo = getEntryInfo({
entry: pathToFileURL(fileId),
contentDir: contentPaths.contentDir,
});
if (generatedInfo instanceof NoCollectionError) throw generatedInfo;
const _internal = { filePath: fileId, rawData: info.rawData }; const _internal = { filePath: fileId, rawData: info.rawData };
// TODO: move slug calculation to the start of the build // TODO: move slug calculation to the start of the build
// to generate a performant lookup map for `getEntryBySlug` // to generate a performant lookup map for `getEntryBySlug`
const slug = getEntrySlug({ ...generatedInfo, unvalidatedSlug: info.slug }); const slug = getEntrySlug({ ...generatedInfo, unvalidatedSlug: info.slug });
const collectionConfig = contentConfig?.collections[generatedInfo.collection]; const collectionConfig = contentConfig?.collections[generatedInfo.collection];
let data = collectionConfig let data = collectionConfig
? await getEntryData( ? await getEntryData(
{ ...generatedInfo, _internal, unvalidatedData: info.data }, { ...generatedInfo, _internal, unvalidatedData: info.data },
collectionConfig collectionConfig
) )
: info.data; : info.data;
await patchAssets(data, this.meta.watchMode, this.emitFile, settings); await patchAssets(data, pluginContext.meta.watchMode, pluginContext.emitFile, settings);
const contentEntryModule: ContentEntryModule = {
...generatedInfo,
_internal,
slug,
data,
body: info.body,
};
const code = escapeViteEnvReferences(` return contentEntryModule;
export const id = ${JSON.stringify(generatedInfo.id)}; }
export const collection = ${JSON.stringify(generatedInfo.collection)};
const plugins: Plugin[] = [
{
name: 'astro:content-imports',
async load(viteId) {
const { fileId } = getFileInfo(viteId, settings.config);
if (isContentFlagImport(viteId, contentEntryExts)) {
const { id, slug, collection, body, data, _internal } = await getEntryDataById(
fileId,
this
);
const code = escapeViteEnvReferences(`
export const id = ${JSON.stringify(id)};
export const collection = ${JSON.stringify(collection)};
export const slug = ${JSON.stringify(slug)}; export const slug = ${JSON.stringify(slug)};
export const body = ${JSON.stringify(info.body)}; export const body = ${JSON.stringify(body)};
export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */}; export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
export const _internal = { export const _internal = {
filePath: ${JSON.stringify(_internal.filePath)}, filePath: ${JSON.stringify(_internal.filePath)},
rawData: ${JSON.stringify(_internal.rawData)}, rawData: ${JSON.stringify(_internal.rawData)},
}; };
`); `);
return { code }; return { code };
} }
}, },
configureServer(viteServer) { configureServer(viteServer) {
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, contentEntryExts) === '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, contentEntryExts)) { if (
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl); isContentFlagImport(modUrl, contentEntryExts) ||
if (mod) { // TODO: refine to content types with getModule
viteServer.moduleGraph.invalidateModule(mod); contentEntryExts.some((ext) => modUrl.endsWith(ext))
) {
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
if (mod) {
viteServer.moduleGraph.invalidateModule(mod);
}
} }
} }
} }
});
},
async transform(code, 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) };
} }
}); },
}, },
async transform(code, id) { ];
if (isContentFlagImport(id, contentEntryExts)) {
// Escape before Rollup internal transform. if (settings.contentEntryTypes.some((t) => t.getModule)) {
// Base on MUCH trial-and-error, inspired by MDX integration 2-step transform. plugins.push({
return { code: escapeViteEnvReferences(code) }; name: 'astro:content-render-modules',
} async load(viteId) {
}, if (!contentEntryExts.some((ext) => viteId.endsWith(ext))) return;
};
const { fileId } = getFileInfo(viteId, settings.config);
for (const contentEntryType of settings.contentEntryTypes) {
if (contentEntryType.getModule) {
const entry = await getEntryDataById(fileId, this);
return contentEntryType.getModule({ entry });
}
}
},
});
}
return plugins;
} }

View file

@ -3,13 +3,8 @@ import Markdoc from '@markdoc/markdoc';
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro'; import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
import fs from 'node:fs'; import fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import type { InlineConfig, TransformResult } from 'vite'; import type * as rollup from 'rollup';
import { import { getAstroConfigPath, MarkdocError, parseFrontmatter } from './utils.js';
getAstroConfigPath,
MarkdocError,
parseFrontmatter,
prependForwardSlash,
} from './utils.js';
type IntegrationWithPrivateHooks = { type IntegrationWithPrivateHooks = {
name: string; name: string;
@ -41,7 +36,7 @@ export default function markdoc(markdocConfig: Config = {}): IntegrationWithPriv
addContentEntryType({ addContentEntryType({
extensions: ['.mdoc'], extensions: ['.mdoc'],
getEntryInfo, getEntryInfo,
getModule(entry: any /* TODO: typing */): Partial<TransformResult> { getModule({ entry }: any): Partial<rollup.LoadResult> {
validateRenderProperties(markdocConfig, config); validateRenderProperties(markdocConfig, config);
const ast = Markdoc.parse(entry.body); const ast = Markdoc.parse(entry.body);
const content = Markdoc.transform(ast, { const content = Markdoc.transform(ast, {
@ -49,13 +44,13 @@ export default function markdoc(markdocConfig: Config = {}): IntegrationWithPriv
variables: { variables: {
...markdocConfig.variables, ...markdocConfig.variables,
entry, entry,
} },
}); });
return { return {
code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify( code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
content content
)};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`; )};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`,
} };
}, },
contentModuleTypes: await fs.promises.readFile( contentModuleTypes: await fs.promises.readFile(
new URL('../template/content-module-types.d.ts', import.meta.url), new URL('../template/content-module-types.d.ts', import.meta.url),