From b69f7570ece42aa3cbe9ae843d540457b3b37151 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 12:29:23 -0400 Subject: [PATCH 1/9] refactor: remove entry prop from `getRenderModule()` --- packages/astro/src/@types/astro.ts | 3 +- packages/astro/src/content/utils.ts | 11 + .../content/vite-plugin-content-imports.ts | 255 +++++++----------- 3 files changed, 107 insertions(+), 162 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index c494cf12759a..c3372ec25db5 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1270,8 +1270,9 @@ export interface ContentEntryType { getRenderModule?( this: rollup.PluginContext, params: { + contents: string; + fileUrl: URL; viteId: string; - entry: ContentEntryModule; } ): rollup.LoadResult | Promise; contentModuleTypes?: string; diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 686176096763..fb7d2a593eea 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -10,6 +10,7 @@ import type { AstroConfig, AstroSettings, ContentEntryType, + DataEntryType, ImageInputFormat, } from '../@types/astro.js'; import { VALID_INPUT_FORMATS } from '../assets/consts.js'; @@ -181,6 +182,16 @@ export function getContentEntryConfigByExtMap(settings: Pick) { + const map: Map = new Map(); + for (const entryType of settings.dataEntryTypes) { + for (const ext of entryType.extensions) { + map.set(ext, entryType); + } + } + return map; +} export function getEntryCollectionName({ contentDir, diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index c0d0571e3f4b..00db0ab451f6 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -5,6 +5,7 @@ import type { PluginContext } from 'rollup'; import { pathToFileURL } from 'url'; import type { Plugin } from 'vite'; import type { + AstroConfig, AstroSettings, ContentEntryModule, ContentEntryType, @@ -30,7 +31,7 @@ import { parseEntrySlug, reloadContentConfigObserver, type ContentConfig, - type ContentPaths, + getDataEntryConfigByExtMap, } from './utils.js'; function getContentRendererByViteId( @@ -71,13 +72,8 @@ export function astroContentImportPlugin({ const dataEntryExts = getDataEntryExts(settings); const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings); - - const dataEntryExtToParser: Map = new Map(); - for (const entryType of settings.dataEntryTypes) { - for (const ext of entryType.extensions) { - dataEntryExtToParser.set(ext, entryType.getEntryInfo); - } - } + const dataEntryConfigByExt = getDataEntryConfigByExtMap(settings); + const { contentDir } = contentPaths; const plugins: Plugin[] = [ { @@ -89,9 +85,9 @@ export function astroContentImportPlugin({ // This cache only exists for the `render()` function specific to content. const { id, data, collection, _internal } = await getDataEntryModule({ fileId, - dataEntryExtToParser, - contentPaths, - settings, + entryConfigByExt: dataEntryConfigByExt, + contentDir, + config: settings.config, fs, pluginContext: this, }); @@ -109,8 +105,12 @@ export const _internal = { return code; } else if (hasContentFlag(viteId, CONTENT_FLAG)) { const fileId = viteId.split('?')[0]; - const { id, slug, collection, body, data, _internal } = await setContentEntryModuleCache({ + const { id, slug, collection, body, data, _internal } = await getContentEntryModule({ fileId, + entryConfigByExt: contentEntryConfigByExt, + contentDir, + config: settings.config, + fs, pluginContext: this, }); @@ -170,149 +170,16 @@ export const _internal = { if (settings.contentEntryTypes.some((t) => t.getRenderModule)) { plugins.push({ name: 'astro:content-render-imports', - async transform(_, viteId) { + async transform(contents, viteId) { const contentRenderer = getContentRendererByViteId(viteId, settings); if (!contentRenderer) return; - const { fileId } = getFileInfo(viteId, settings.config); - const entry = await getContentEntryModuleFromCache(fileId); - if (!entry) { - // Cached entry must exist (or be in-flight) when importing the module via content collections. - // This is ensured by the `astro:content-imports` plugin. - throw new AstroError({ - ...AstroErrorData.UnknownContentCollectionError, - message: `Unable to render ${JSON.stringify( - fileId - )}. Did you import this module directly without using a content collection query?`, - }); - } - - return contentRenderer.bind(this)({ entry, viteId }); + const fileId = viteId.split('?')[0]; + return contentRenderer.bind(this)({ viteId, contents, fileUrl: pathToFileURL(fileId) }); }, }); } - /** - * There are two content collection plugins that depend on the same entry data: - * - `astro:content-imports` - creates module containing the `getCollection()` result. - * - `astro:content-render-imports` - creates module containing the `collectionEntry.render()` result. - * - * We could run the same transforms to generate the slug and parsed data in each plugin, - * though this would run the user's collection schema _twice_ for each entry. - * - * Instead, we've implemented a cache for all content entry data. To avoid race conditions, - * this may store either the module itself or a queue of promises awaiting this module. - * See the implementations of `getContentEntryModuleFromCache` and `setContentEntryModuleCache`. - */ - const contentEntryModuleByIdCache = new Map< - string, - ContentEntryModule | AwaitingCacheResultQueue - >(); - type AwaitingCacheResultQueue = { - awaitingQueue: ((val: ContentEntryModule) => void)[]; - }; - function isAwaitingQueue( - cacheEntry: ReturnType - ): cacheEntry is AwaitingCacheResultQueue { - return typeof cacheEntry === 'object' && cacheEntry != null && 'awaitingQueue' in cacheEntry; - } - - function getContentEntryModuleFromCache(id: string): Promise { - const cacheEntry = contentEntryModuleByIdCache.get(id); - // It's possible to request an entry while `setContentEntryModuleCache` is still - // setting that entry. In this case, queue a promise for `setContentEntryModuleCache` - // to resolve once it is complete. - if (isAwaitingQueue(cacheEntry)) { - return new Promise((resolve, reject) => { - cacheEntry.awaitingQueue.push(resolve); - }); - } else if (cacheEntry) { - return Promise.resolve(cacheEntry); - } - return Promise.resolve(undefined); - } - - async function setContentEntryModuleCache({ - fileId, - pluginContext, - }: { - fileId: string; - pluginContext: PluginContext; - }): Promise { - // Create a queue so, if `getContentEntryModuleFromCache` is called - // while this function is running, we can resolve all requests - // in the `awaitingQueue` with the result. - contentEntryModuleByIdCache.set(fileId, { awaitingQueue: [] }); - - const contentConfig = await getContentConfigFromGlobal(); - const rawContents = await fs.promises.readFile(fileId, 'utf-8'); - const fileExt = extname(fileId); - if (!contentEntryConfigByExt.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 contentEntryConfig = contentEntryConfigByExt.get(fileExt)!; - const { - rawData, - body, - slug: frontmatterSlug, - data: unvalidatedData, - } = await contentEntryConfig.getEntryInfo({ - fileUrl: pathToFileURL(fileId), - contents: rawContents, - }); - const entry = pathToFileURL(fileId); - const { contentDir } = contentPaths; - const collection = getEntryCollectionName({ entry, contentDir }); - if (collection === undefined) - throw new AstroError(AstroErrorData.UnknownContentCollectionError); - - const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection }); - - const _internal = { filePath: fileId, rawData: rawData }; - // TODO: move slug calculation to the start of the build - // to generate a performant lookup map for `getEntryBySlug` - const slug = parseEntrySlug({ - id, - collection, - generatedSlug, - frontmatterSlug, - }); - - const collectionConfig = contentConfig?.collections[collection]; - let data = collectionConfig - ? await getEntryData( - { id, collection, _internal, unvalidatedData }, - collectionConfig, - pluginContext, - settings.config - ) - : unvalidatedData; - - const contentEntryModule: ContentEntryModule = { - id, - slug, - collection, - data, - body, - _internal, - }; - - const cacheEntry = contentEntryModuleByIdCache.get(fileId); - // Pass the entry to all promises awaiting this result - if (isAwaitingQueue(cacheEntry)) { - for (const resolve of cacheEntry.awaitingQueue) { - resolve(contentEntryModule); - } - } - contentEntryModuleByIdCache.set(fileId, contentEntryModule); - return contentEntryModule; - } - return plugins; } @@ -353,23 +220,90 @@ async function getContentConfigFromGlobal() { return contentConfig; } -type GetDataEntryModuleParams = { +type GetEntryModuleParams = { fs: typeof fsMod; fileId: string; - contentPaths: Pick; + contentDir: URL; pluginContext: PluginContext; - dataEntryExtToParser: Map; - settings: Pick; + entryConfigByExt: Map; + config: AstroConfig; }; +async function getContentEntryModule({ + fileId, + pluginContext, + fs, + config, + contentDir, + entryConfigByExt, +}: GetEntryModuleParams): Promise { + const contentConfig = await getContentConfigFromGlobal(); + const rawContents = await fs.promises.readFile(fileId, 'utf-8'); + const fileExt = extname(fileId); + if (!entryConfigByExt.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 contentEntryConfig = entryConfigByExt.get(fileExt)!; + const { + rawData, + body, + slug: frontmatterSlug, + data: unvalidatedData, + } = await contentEntryConfig.getEntryInfo({ + fileUrl: pathToFileURL(fileId), + contents: rawContents, + }); + const entry = pathToFileURL(fileId); + const collection = getEntryCollectionName({ entry, contentDir }); + if (collection === undefined) throw new AstroError(AstroErrorData.UnknownContentCollectionError); + + const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection }); + + const _internal = { filePath: fileId, rawData: rawData }; + // TODO: move slug calculation to the start of the build + // to generate a performant lookup map for `getEntryBySlug` + const slug = parseEntrySlug({ + id, + collection, + generatedSlug, + frontmatterSlug, + }); + + const collectionConfig = contentConfig?.collections[collection]; + let data = collectionConfig + ? await getEntryData( + { id, collection, _internal, unvalidatedData }, + collectionConfig, + pluginContext, + config + ) + : unvalidatedData; + + const contentEntryModule: ContentEntryModule = { + id, + slug, + collection, + data, + body, + _internal, + }; + + return contentEntryModule; +} + async function getDataEntryModule({ fileId, - dataEntryExtToParser, - contentPaths, + entryConfigByExt, + contentDir, fs, pluginContext, - settings, -}: GetDataEntryModuleParams): Promise { + config, +}: GetEntryModuleParams): Promise { const contentConfig = await getContentConfigFromGlobal(); let rawContents; try { @@ -382,9 +316,9 @@ async function getDataEntryModule({ }); } const fileExt = extname(fileId); - const dataEntryParser = dataEntryExtToParser.get(fileExt); + const dataEntryConfig = entryConfigByExt.get(fileExt); - if (!dataEntryParser) { + if (!dataEntryConfig) { throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: `No parser found for data entry ${JSON.stringify( @@ -392,12 +326,11 @@ async function getDataEntryModule({ )}. Did you apply an integration for this file type?`, }); } - const { data: unvalidatedData, rawData = '' } = await dataEntryParser({ + const { data: unvalidatedData, rawData = '' } = await dataEntryConfig.getEntryInfo({ fileUrl: pathToFileURL(fileId), contents: rawContents, }); const entry = pathToFileURL(fileId); - const { contentDir } = contentPaths; const collection = getEntryCollectionName({ entry, contentDir }); if (collection === undefined) throw new AstroError(AstroErrorData.UnknownContentCollectionError); @@ -411,7 +344,7 @@ async function getDataEntryModule({ { id, collection, _internal, unvalidatedData }, collectionConfig, pluginContext, - settings.config + config ) : unvalidatedData; From bf77d8155e54ac562ce9de6dec5006fcbc37c8a1 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 12:29:33 -0400 Subject: [PATCH 2/9] refactor: remove `$entry` from markdoc --- packages/integrations/markdoc/src/index.ts | 15 ++++++++++----- packages/integrations/markdoc/src/runtime.ts | 8 ++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 0486a44b5be1..ef50768fe607 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -11,6 +11,7 @@ import { bold, red, yellow } from 'kleur/colors'; import type * as rollup from 'rollup'; import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js'; import { setupConfig } from './runtime.js'; +import path from 'node:path'; type SetupHookParams = HookParameters<'astro:config:setup'> & { // `contentEntryType` is not a public API @@ -61,10 +62,13 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration addContentEntryType({ extensions: ['.mdoc'], getEntryInfo, - async getRenderModule({ entry, viteId }) { + async getRenderModule({ contents, fileUrl, viteId }) { + const entry = getEntryInfo({ contents, fileUrl }); const ast = Markdoc.parse(entry.body); const pluginContext = this; - const markdocConfig = await setupConfig(userMarkdocConfig, entry); + const markdocConfig = await setupConfig(userMarkdocConfig); + + const filePath = fileURLToPath(fileUrl); const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => { return ( @@ -77,10 +81,11 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration }); if (validationErrors.length) { // Heuristic: take number of newlines for `rawData` and add 2 for the `---` fences - const frontmatterBlockOffset = entry._internal.rawData.split('\n').length + 2; + const frontmatterBlockOffset = entry.rawData.split('\n').length + 2; + const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath); throw new MarkdocError({ message: [ - `**${String(entry.collection)} → ${String(entry.id)}** contains invalid content:`, + `**${String(rootRelativePath)}** contains invalid content:`, ...validationErrors.map((e) => `- ${e.error.message}`), ].join('\n'), location: { @@ -96,7 +101,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration await emitOptimizedImages(ast.children, { astroConfig, pluginContext, - filePath: entry._internal.filePath, + filePath, }); } diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts index 5bf7e4412fda..a1861c68ca67 100644 --- a/packages/integrations/markdoc/src/runtime.ts +++ b/packages/integrations/markdoc/src/runtime.ts @@ -13,13 +13,9 @@ export { default as Markdoc } from '@markdoc/markdoc'; * TODO: virtual module to merge configs per-build instead of per-file? */ export async function setupConfig( - userConfig: AstroMarkdocConfig, - entry: ContentEntryModule + userConfig: AstroMarkdocConfig ): Promise> { - let defaultConfig: AstroMarkdocConfig = { - ...setupHeadingConfig(), - variables: { entry }, - }; + let defaultConfig: AstroMarkdocConfig = setupHeadingConfig(); if (userConfig.extends) { for (let extension of userConfig.extends) { From aacc0c4b0f67c1ed4e481e6dbd21d85dfe6157fb Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 12:29:40 -0400 Subject: [PATCH 3/9] test: update entry-prop -> variables test --- packages/integrations/markdoc/test/entry-prop.test.js | 2 +- .../markdoc/test/fixtures/entry-prop/src/pages/index.astro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/integrations/markdoc/test/entry-prop.test.js b/packages/integrations/markdoc/test/entry-prop.test.js index b47ccf739b89..ca679ed4ebea 100644 --- a/packages/integrations/markdoc/test/entry-prop.test.js +++ b/packages/integrations/markdoc/test/entry-prop.test.js @@ -5,7 +5,7 @@ import markdoc from '../dist/index.js'; const root = new URL('./fixtures/entry-prop/', import.meta.url); -describe('Markdoc - Entry prop', () => { +describe('Markdoc - Variables', () => { let baseFixture; before(async () => { diff --git a/packages/integrations/markdoc/test/fixtures/entry-prop/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/entry-prop/src/pages/index.astro index d1418765132c..a2766faf051b 100644 --- a/packages/integrations/markdoc/test/fixtures/entry-prop/src/pages/index.astro +++ b/packages/integrations/markdoc/test/fixtures/entry-prop/src/pages/index.astro @@ -14,6 +14,6 @@ const { Content } = await entry.render(); Astro - + From 73e64d2d1fcebb6e22f7ab8d5c9861bf27ef3276 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 12:33:50 -0400 Subject: [PATCH 4/9] refactor: unify `getEntryConfigByExt` --- packages/astro/src/content/types-generator.ts | 4 ++-- packages/astro/src/content/utils.ts | 18 +++++------------- .../src/content/vite-plugin-content-imports.ts | 7 +++---- .../content/vite-plugin-content-virtual-mod.ts | 8 ++++---- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index ee4c9907735c..538b267ff66c 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -11,7 +11,7 @@ import { info, warn, type LogOptions } from '../core/logger/core.js'; import { isRelativePath } from '../core/path.js'; import { CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js'; import { - getContentEntryConfigByExtMap, + getEntryConfigByExtMap, getContentEntryIdAndSlug, getContentPaths, getDataEntryExts, @@ -74,7 +74,7 @@ export async function createContentTypesGenerator({ }: CreateContentGeneratorParams) { const collectionEntryMap: CollectionEntryMap = {}; const contentPaths = getContentPaths(settings.config, fs); - const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings); + const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); const contentEntryExts = [...contentEntryConfigByExt.keys()]; const dataEntryExts = getDataEntryExts(settings); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index fb7d2a593eea..40b2ab9e715b 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -173,19 +173,11 @@ export function getDataEntryExts(settings: Pick return settings.dataEntryTypes.map((t) => t.extensions).flat(); } -export function getContentEntryConfigByExtMap(settings: Pick) { - const map: Map = new Map(); - for (const entryType of settings.contentEntryTypes) { - for (const ext of entryType.extensions) { - map.set(ext, entryType); - } - } - return map; -} -// TODO: refactor to one helper -export function getDataEntryConfigByExtMap(settings: Pick) { - const map: Map = new Map(); - for (const entryType of settings.dataEntryTypes) { +export function getEntryConfigByExtMap( + entryTypes: TEntryType[] +): Map { + const map: Map = new Map(); + for (const entryType of entryTypes) { for (const ext of entryType.extensions) { map.set(ext, entryType); } diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 00db0ab451f6..3e8186335f8b 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -17,7 +17,7 @@ import { AstroError } from '../core/errors/errors.js'; import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js'; import { CONTENT_FLAG, DATA_FLAG } from './consts.js'; import { - getContentEntryConfigByExtMap, + getEntryConfigByExtMap, getContentEntryExts, getContentEntryIdAndSlug, getContentPaths, @@ -31,7 +31,6 @@ import { parseEntrySlug, reloadContentConfigObserver, type ContentConfig, - getDataEntryConfigByExtMap, } from './utils.js'; function getContentRendererByViteId( @@ -71,8 +70,8 @@ export function astroContentImportPlugin({ const contentEntryExts = getContentEntryExts(settings); const dataEntryExts = getDataEntryExts(settings); - const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings); - const dataEntryConfigByExt = getDataEntryConfigByExtMap(settings); + const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); + const dataEntryConfigByExt = getEntryConfigByExtMap(settings.dataEntryTypes); const { contentDir } = contentPaths; const plugins: Plugin[] = [ diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts index 71d41751e1de..9b0a1becc895 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -4,12 +4,12 @@ import { extname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import pLimit from 'p-limit'; import type { Plugin } from 'vite'; -import type { AstroSettings } from '../@types/astro.js'; +import type { AstroSettings, ContentEntryType } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { rootRelativePath } from '../core/util.js'; import { VIRTUAL_MODULE_ID } from './consts.js'; import { - getContentEntryConfigByExtMap, + getEntryConfigByExtMap, getContentEntryIdAndSlug, getContentPaths, getDataEntryExts, @@ -32,7 +32,7 @@ export function astroContentVirtualModPlugin({ const contentPaths = getContentPaths(settings.config); const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir); - const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings); + const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes); const contentEntryExts = [...contentEntryConfigByExt.keys()]; const dataEntryExts = getDataEntryExts(settings); @@ -92,7 +92,7 @@ export async function getStringifiedLookupMap({ root, fs, }: { - contentEntryConfigByExt: ReturnType; + contentEntryConfigByExt: Map; dataEntryExts: string[]; contentPaths: Pick; root: URL; From 4e82c0bbe4992e3b248371a476841ffc3f025ddc Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 13:13:48 -0400 Subject: [PATCH 5/9] chore: clean up shared content / data get logic --- .../content/vite-plugin-content-imports.ts | 185 +++++++++--------- 1 file changed, 94 insertions(+), 91 deletions(-) diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 3e8186335f8b..899ce02cd468 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -182,44 +182,7 @@ export const _internal = { return plugins; } -async function getContentConfigFromGlobal() { - const observable = globalContentConfigObserver.get(); - - // Content config should be loaded before being accessed from Vite plugins - if (observable.status === 'init') { - throw new AstroError({ - ...AstroErrorData.UnknownContentCollectionError, - message: 'Content config failed to load.', - }); - } - if (observable.status === 'error') { - // Throw here to bubble content config errors - // to the error overlay in development - throw observable.error; - } - - let contentConfig: ContentConfig | undefined = - observable.status === 'loaded' ? observable.config : undefined; - if (observable.status === 'loading') { - // Wait for config to load - contentConfig = await new Promise((resolve) => { - const unsubscribe = globalContentConfigObserver.subscribe((ctx) => { - if (ctx.status === 'loaded') { - resolve(ctx.config); - unsubscribe(); - } - if (ctx.status === 'error') { - resolve(undefined); - unsubscribe(); - } - }); - }); - } - - return contentConfig; -} - -type GetEntryModuleParams = { +type GetEntryModuleParams = { fs: typeof fsMod; fileId: string; contentDir: URL; @@ -228,44 +191,25 @@ type GetEntryModuleParams = { config: AstroConfig; }; -async function getContentEntryModule({ - fileId, - pluginContext, - fs, - config, - contentDir, - entryConfigByExt, -}: GetEntryModuleParams): Promise { - const contentConfig = await getContentConfigFromGlobal(); - const rawContents = await fs.promises.readFile(fileId, 'utf-8'); - const fileExt = extname(fileId); - if (!entryConfigByExt.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 contentEntryConfig = entryConfigByExt.get(fileExt)!; +async function getContentEntryModule( + params: GetEntryModuleParams +): Promise { + const { fileId, contentDir, pluginContext, config } = params; + const { collectionConfig, entryConfig, entry, rawContents, collection } = + await getEntryModuleBaseInfo(params); + const { rawData, + data: unvalidatedData, body, slug: frontmatterSlug, - data: unvalidatedData, - } = await contentEntryConfig.getEntryInfo({ + } = await entryConfig.getEntryInfo({ fileUrl: pathToFileURL(fileId), contents: rawContents, }); - const entry = pathToFileURL(fileId); - const collection = getEntryCollectionName({ entry, contentDir }); - if (collection === undefined) throw new AstroError(AstroErrorData.UnknownContentCollectionError); - + const _internal = { filePath: fileId, rawData }; const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection }); - const _internal = { filePath: fileId, rawData: rawData }; - // TODO: move slug calculation to the start of the build - // to generate a performant lookup map for `getEntryBySlug` const slug = parseEntrySlug({ id, collection, @@ -273,8 +217,7 @@ async function getContentEntryModule({ frontmatterSlug, }); - const collectionConfig = contentConfig?.collections[collection]; - let data = collectionConfig + const data = collectionConfig ? await getEntryData( { id, collection, _internal, unvalidatedData }, collectionConfig, @@ -295,14 +238,47 @@ async function getContentEntryModule({ return contentEntryModule; } -async function getDataEntryModule({ +async function getDataEntryModule( + params: GetEntryModuleParams +): Promise { + const { fileId, contentDir, pluginContext, config } = params; + const { collectionConfig, entryConfig, entry, rawContents, collection } = + await getEntryModuleBaseInfo(params); + + const { rawData = '', data: unvalidatedData } = await entryConfig.getEntryInfo({ + fileUrl: pathToFileURL(fileId), + contents: rawContents, + }); + const _internal = { filePath: fileId, rawData }; + const id = getDataEntryId({ entry, contentDir, collection }); + + const data = collectionConfig + ? await getEntryData( + { id, collection, _internal, unvalidatedData }, + collectionConfig, + pluginContext, + config + ) + : unvalidatedData; + + const dataEntryModule: DataEntryModule = { + id, + collection, + data, + _internal, + }; + + return dataEntryModule; +} + +// Shared logic for `getContentEntryModule` and `getDataEntryModule` +// Extracting to a helper was easier that conditionals and generics :) +async function getEntryModuleBaseInfo({ fileId, entryConfigByExt, contentDir, fs, - pluginContext, - config, -}: GetEntryModuleParams): Promise { +}: GetEntryModuleParams) { const contentConfig = await getContentConfigFromGlobal(); let rawContents; try { @@ -315,9 +291,9 @@ async function getDataEntryModule({ }); } const fileExt = extname(fileId); - const dataEntryConfig = entryConfigByExt.get(fileExt); + const entryConfig = entryConfigByExt.get(fileExt); - if (!dataEntryConfig) { + if (!entryConfig) { throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: `No parser found for data entry ${JSON.stringify( @@ -325,27 +301,54 @@ async function getDataEntryModule({ )}. Did you apply an integration for this file type?`, }); } - const { data: unvalidatedData, rawData = '' } = await dataEntryConfig.getEntryInfo({ - fileUrl: pathToFileURL(fileId), - contents: rawContents, - }); const entry = pathToFileURL(fileId); const collection = getEntryCollectionName({ entry, contentDir }); if (collection === undefined) throw new AstroError(AstroErrorData.UnknownContentCollectionError); - const id = getDataEntryId({ entry, contentDir, collection }); + const collectionConfig = contentConfig?.collections[collection]; - const _internal = { filePath: fileId, rawData }; + return { + collectionConfig, + entry, + entryConfig, + collection, + rawContents, + }; +} - const collectionConfig = contentConfig?.collections[collection]; - const data = collectionConfig - ? await getEntryData( - { id, collection, _internal, unvalidatedData }, - collectionConfig, - pluginContext, - config - ) - : unvalidatedData; +async function getContentConfigFromGlobal() { + const observable = globalContentConfigObserver.get(); - return { id, collection, data, _internal }; + // Content config should be loaded before being accessed from Vite plugins + if (observable.status === 'init') { + throw new AstroError({ + ...AstroErrorData.UnknownContentCollectionError, + message: 'Content config failed to load.', + }); + } + if (observable.status === 'error') { + // Throw here to bubble content config errors + // to the error overlay in development + throw observable.error; + } + + let contentConfig: ContentConfig | undefined = + observable.status === 'loaded' ? observable.config : undefined; + if (observable.status === 'loading') { + // Wait for config to load + contentConfig = await new Promise((resolve) => { + const unsubscribe = globalContentConfigObserver.subscribe((ctx) => { + if (ctx.status === 'loaded') { + resolve(ctx.config); + unsubscribe(); + } + if (ctx.status === 'error') { + resolve(undefined); + unsubscribe(); + } + }); + }); + } + + return contentConfig; } From 0610a0de3b4faa85cc195b25abf193dec58aa54a Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 13:20:52 -0400 Subject: [PATCH 6/9] docs: update `$entry` recommendation --- packages/integrations/markdoc/README.md | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/integrations/markdoc/README.md b/packages/integrations/markdoc/README.md index 011f042eea7b..da5aeb46ae15 100644 --- a/packages/integrations/markdoc/README.md +++ b/packages/integrations/markdoc/README.md @@ -290,20 +290,6 @@ export default defineMarkdocConfig({ }) ``` -### Access frontmatter and content collection information from your templates - -You can access content collection information from your Markdoc templates using the `$entry` variable. This includes the entry `slug`, `collection` name, and frontmatter `data` parsed by your content collection schema (if any). This example renders the `title` frontmatter property as a heading: - -```md ---- -title: Welcome to Markdoc 👋 ---- - -# {% $entry.data.title %} -``` - -The `$entry` object matches [the `CollectionEntry` type](https://docs.astro.build/en/reference/api-reference/#collection-entry-type), excluding the `.render()` property. - ### Markdoc config The `markdoc.config.mjs|ts` file accepts [all Markdoc configuration options](https://markdoc.dev/docs/config), including [tags](https://markdoc.dev/docs/tags) and [functions](https://markdoc.dev/docs/functions). @@ -379,6 +365,23 @@ export default defineMarkdocConfig({ }) ``` +### Access frontmatter from your Markdoc content + +To access frontmatter, you can pass the entry `data` property [as a variable](#pass-markdoc-variables) where you render your content: + +```astro +--- +import { getEntry } from 'astro:content'; + +const entry = await getEntry('docs', 'why-markdoc'); +const { Content } = await entry.render(); +--- + + +``` + +This can now be accessed as `$frontmatter` in your Markdoc. + ## Examples * The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project. From 8e8d1f586d23c85d28f98768368a014cb10d56ff Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 13:26:29 -0400 Subject: [PATCH 7/9] chore: rename entry-prop -> variables --- .../{entry-prop => variables}/astro.config.mjs | 0 .../{entry-prop => variables}/package.json | 2 +- .../src/content/blog/entry.mdoc | 0 .../src/content/config.ts | 0 .../src/pages/index.astro | 0 .../{entry-prop.test.js => variables.test.js} | 0 pnpm-lock.yaml | 18 +++++++++--------- 7 files changed, 10 insertions(+), 10 deletions(-) rename packages/integrations/markdoc/test/fixtures/{entry-prop => variables}/astro.config.mjs (100%) rename packages/integrations/markdoc/test/fixtures/{entry-prop => variables}/package.json (78%) rename packages/integrations/markdoc/test/fixtures/{entry-prop => variables}/src/content/blog/entry.mdoc (100%) rename packages/integrations/markdoc/test/fixtures/{entry-prop => variables}/src/content/config.ts (100%) rename packages/integrations/markdoc/test/fixtures/{entry-prop => variables}/src/pages/index.astro (100%) rename packages/integrations/markdoc/test/{entry-prop.test.js => variables.test.js} (100%) diff --git a/packages/integrations/markdoc/test/fixtures/entry-prop/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/variables/astro.config.mjs similarity index 100% rename from packages/integrations/markdoc/test/fixtures/entry-prop/astro.config.mjs rename to packages/integrations/markdoc/test/fixtures/variables/astro.config.mjs diff --git a/packages/integrations/markdoc/test/fixtures/entry-prop/package.json b/packages/integrations/markdoc/test/fixtures/variables/package.json similarity index 78% rename from packages/integrations/markdoc/test/fixtures/entry-prop/package.json rename to packages/integrations/markdoc/test/fixtures/variables/package.json index 149f6c35a9d6..0ac7a3c82f33 100644 --- a/packages/integrations/markdoc/test/fixtures/entry-prop/package.json +++ b/packages/integrations/markdoc/test/fixtures/variables/package.json @@ -1,5 +1,5 @@ { - "name": "@test/markdoc-entry-prop", + "name": "@test/markdoc-variables", "version": "0.0.0", "private": true, "dependencies": { diff --git a/packages/integrations/markdoc/test/fixtures/entry-prop/src/content/blog/entry.mdoc b/packages/integrations/markdoc/test/fixtures/variables/src/content/blog/entry.mdoc similarity index 100% rename from packages/integrations/markdoc/test/fixtures/entry-prop/src/content/blog/entry.mdoc rename to packages/integrations/markdoc/test/fixtures/variables/src/content/blog/entry.mdoc diff --git a/packages/integrations/markdoc/test/fixtures/entry-prop/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/variables/src/content/config.ts similarity index 100% rename from packages/integrations/markdoc/test/fixtures/entry-prop/src/content/config.ts rename to packages/integrations/markdoc/test/fixtures/variables/src/content/config.ts diff --git a/packages/integrations/markdoc/test/fixtures/entry-prop/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/variables/src/pages/index.astro similarity index 100% rename from packages/integrations/markdoc/test/fixtures/entry-prop/src/pages/index.astro rename to packages/integrations/markdoc/test/fixtures/variables/src/pages/index.astro diff --git a/packages/integrations/markdoc/test/entry-prop.test.js b/packages/integrations/markdoc/test/variables.test.js similarity index 100% rename from packages/integrations/markdoc/test/entry-prop.test.js rename to packages/integrations/markdoc/test/variables.test.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54421c429356..0b71686f93f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4044,15 +4044,6 @@ importers: specifier: workspace:* version: link:../../../../../astro - packages/integrations/markdoc/test/fixtures/entry-prop: - dependencies: - '@astrojs/markdoc': - specifier: workspace:* - version: link:../../.. - astro: - specifier: workspace:* - version: link:../../../../../astro - packages/integrations/markdoc/test/fixtures/headings: dependencies: '@astrojs/markdoc': @@ -4120,6 +4111,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/variables: + dependencies: + '@astrojs/markdoc': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/mdx: dependencies: '@astrojs/markdown-remark': From d41ca5faebbc3b542aaff85e8b204bedaeab0d1e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 13:31:16 -0400 Subject: [PATCH 8/9] chore: changeset --- .changeset/metal-bugs-drive.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/metal-bugs-drive.md diff --git a/.changeset/metal-bugs-drive.md b/.changeset/metal-bugs-drive.md new file mode 100644 index 000000000000..16b4f6b19272 --- /dev/null +++ b/.changeset/metal-bugs-drive.md @@ -0,0 +1,17 @@ +--- +'@astrojs/markdoc': minor +'astro': patch +--- + +Remove the auto-generated `$entry` variable for Markdoc entries. To access frontmatter as a variable, you can pass `entry.data` as a prop where you render your content: + +```astro +--- +import { getEntry } from 'astro:content'; + +const entry = await getEntry('docs', 'why-markdoc'); +const { Content } = await entry.render(); +--- + + +``` From 85fb722748e6f31672a3e823ebfa04346333c884 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 30 May 2023 13:36:06 -0400 Subject: [PATCH 9/9] chore: missed a spot --- packages/integrations/markdoc/test/variables.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/markdoc/test/variables.test.js b/packages/integrations/markdoc/test/variables.test.js index ca679ed4ebea..90d5fe276320 100644 --- a/packages/integrations/markdoc/test/variables.test.js +++ b/packages/integrations/markdoc/test/variables.test.js @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { loadFixture } from '../../../astro/test/test-utils.js'; import markdoc from '../dist/index.js'; -const root = new URL('./fixtures/entry-prop/', import.meta.url); +const root = new URL('./fixtures/variables/', import.meta.url); describe('Markdoc - Variables', () => { let baseFixture;