feat: get entry data in there
This commit is contained in:
parent
484ccb1c81
commit
ec003b5b53
3 changed files with 161 additions and 105 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue