diff --git a/examples/with-markdoc/src/content.config.ts b/examples/with-markdoc/src/content.config.ts index 79743326eac3..a991e1ea1038 100644 --- a/examples/with-markdoc/src/content.config.ts +++ b/examples/with-markdoc/src/content.config.ts @@ -1,5 +1,5 @@ import { defineCollection } from 'astro:content'; export const collections = { - docs: defineCollection({}) + docs: defineCollection({}), }; diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts index 76218d7e8aea..c2902cb70b13 100644 --- a/packages/astro/src/content/consts.ts +++ b/packages/astro/src/content/consts.ts @@ -41,3 +41,4 @@ export const COLLECTIONS_MANIFEST_FILE = 'collections/collections.json'; export const COLLECTIONS_DIR = 'collections/'; export const CONTENT_LAYER_TYPE = 'content_layer'; +export const LIVE_CONTENT_TYPE = 'live'; diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts index 4c2d8a3598f1..4385fb33f4cb 100644 --- a/packages/astro/src/content/loaders/types.ts +++ b/packages/astro/src/content/loaders/types.ts @@ -2,7 +2,11 @@ import type { FSWatcher } from 'vite'; import type { ZodSchema } from 'zod'; import type { AstroIntegrationLogger } from '../../core/logger/core.js'; import type { AstroConfig } from '../../types/public/config.js'; -import type { ContentEntryType } from '../../types/public/content.js'; +import type { + ContentEntryType, + LiveDataCollection, + LiveDataEntry, +} from '../../types/public/content.js'; import type { DataStore, MetaStore } from '../mutable-data-store.js'; export type { DataStore, MetaStore }; @@ -49,3 +53,26 @@ export interface Loader { /** Optionally, define the schema of the data. Will be overridden by user-defined schema */ schema?: ZodSchema | Promise | (() => ZodSchema | Promise); } + +export interface LoadEntryContext { + filter: TEntryFilter extends never ? { id: string } : TEntryFilter; +} + +export interface LoadCollectionContext { + filter?: TCollectionFilter; +} + +export interface LiveLoader< + TData extends Record = Record, + TEntryFilter extends Record | never = never, + TCollectionFilter extends Record | never = never, +> { + /** Unique name of the loader, e.g. the npm package name */ + name: string; + /** Load a single entry */ + loadEntry: (context: LoadEntryContext) => Promise | undefined>; + /** Load a collection of entries */ + loadCollection: ( + context: LoadCollectionContext, + ) => Promise>; +} diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index 233c35c4210a..27d1f441c41d 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -19,14 +19,19 @@ import { render as serverRender, unescapeHTML, } from '../runtime/server/index.js'; -import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from './consts.js'; +import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX, LIVE_CONTENT_TYPE } from './consts.js'; import { type DataEntry, globalDataStore } from './data-store.js'; +import type { LiveLoader } from './loaders/types.js'; import type { ContentLookupMap } from './utils.js'; type LazyImport = () => Promise; type GlobResult = Record; type CollectionToEntryMap = Record; type GetEntryImport = (collection: string, lookupId: string) => Promise; +type LiveCollectionConfigMap = Record< + string, + { loader: LiveLoader; type: typeof LIVE_CONTENT_TYPE } +>; export function getImporterFilename() { // The 4th line in the stack trace should be the importer filename @@ -40,6 +45,27 @@ export function getImporterFilename() { } export function defineCollection(config: any) { + const isInLiveConfig = getImporterFilename()?.endsWith('/live.config.ts'); + + if (config.type === LIVE_CONTENT_TYPE) { + if (!isInLiveConfig) { + throw new AstroUserError( + `Collections with type "live" must be defined in a \`src/live.config.ts\` file. Check your collection definitions in ${getImporterFilename() ?? 'your content config file'}.`, + ); + } + if (!config.loader) { + throw new AstroUserError( + `Collections that use the Live Content API must have a \`loader\` defined. Check your collection definitions in ${getImporterFilename() ?? 'your live content config file'}.`, + ); + } + return config; + } + if (isInLiveConfig) { + throw new AstroUserError( + `Collections in a \`live.config.ts\` file must be defined with the type "live". Check your collection definitions.`, + ); + } + if ('loader' in config) { if (config.type && config.type !== CONTENT_LAYER_TYPE) { throw new AstroUserError( @@ -76,13 +102,40 @@ export function createGetCollection({ dataCollectionToEntryMap, getRenderEntryImport, cacheEntriesByCollection, + liveCollections, }: { contentCollectionToEntryMap: CollectionToEntryMap; dataCollectionToEntryMap: CollectionToEntryMap; getRenderEntryImport: GetEntryImport; cacheEntriesByCollection: Map; + liveCollections: LiveCollectionConfigMap; }) { - return async function getCollection(collection: string, filter?: (entry: any) => unknown) { + return async function getCollection( + collection: string, + filter?: ((entry: any) => unknown) | Record, + ) { + if (collection in liveCollections) { + if (typeof filter === 'function') { + throw new AstroError({ + ...AstroErrorData.UnknownContentCollectionError, + message: `The filter function is not supported for live collections. Please use a filter object instead.`, + }); + } + + const context = { + filter, + }; + + const response = await ( + liveCollections[collection].loader as LiveLoader> + )?.loadCollection?.(context); + + return { + ...response, + collection, + }; + } + const hasFilter = typeof filter === 'function'; const store = await globalDataStore.get(); let type: 'content' | 'data'; @@ -297,27 +350,29 @@ export function createGetEntry({ getEntryImport, getRenderEntryImport, collectionNames, + liveCollections, }: { getEntryImport: GetEntryImport; getRenderEntryImport: GetEntryImport; collectionNames: Set; + liveCollections: LiveCollectionConfigMap; }) { return async function getEntry( // Can either pass collection and identifier as 2 positional args, // Or pass a single object with the collection and identifier as properties. // This means the first positional arg can have different shapes. collectionOrLookupObject: string | EntryLookupObject, - _lookupId?: string, + lookup?: string | Record, ): Promise { - let collection: string, lookupId: string; + let collection: string, lookupId: string | Record; if (typeof collectionOrLookupObject === 'string') { collection = collectionOrLookupObject; - if (!_lookupId) + if (!lookup) throw new AstroError({ ...AstroErrorData.UnknownContentCollectionError, message: '`getEntry()` requires an entry identifier as the second argument.', }); - lookupId = _lookupId; + lookupId = lookup; } else { collection = collectionOrLookupObject.collection; // Identifier could be `slug` for content entries, or `id` for data entries @@ -327,6 +382,39 @@ export function createGetEntry({ : collectionOrLookupObject.slug; } + if (collection in liveCollections) { + if (!lookup) { + throw new AstroError({ + ...AstroErrorData.UnknownContentCollectionError, + message: '`getEntry()` requires an entry identifier as the second argument.', + }); + } + + const lookupObject = { + filter: typeof lookup === 'string' ? { id: lookup } : lookup, + }; + + const entry = await ( + liveCollections[collection].loader as LiveLoader< + Record, + Record + > + )?.loadEntry?.(lookupObject); + + if (!entry) { + return; + } + return { + ...entry, + collection, + }; + } + if (typeof lookupId === 'object') { + throw new AstroError({ + ...AstroErrorData.UnknownContentCollectionError, + message: `The entry identifier must be a string. Received object.`, + }); + } const store = await globalDataStore.get(); if (store.hasCollection(collection)) { diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index c6a362a24c2f..08250d9fee94 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -7,7 +7,7 @@ import { type ViteDevServer, normalizePath } from 'vite'; import { type ZodSchema, z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { AstroError } from '../core/errors/errors.js'; -import { AstroErrorData } from '../core/errors/index.js'; +import { AstroErrorData, AstroUserError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import { isRelativePath } from '../core/path.js'; import type { AstroSettings } from '../types/astro.js'; @@ -16,6 +16,7 @@ import { COLLECTIONS_DIR, CONTENT_LAYER_TYPE, CONTENT_TYPES_FILE, + LIVE_CONTENT_TYPE, VIRTUAL_MODULE_ID, } from './consts.js'; import { @@ -53,6 +54,10 @@ type CollectionEntryMap = { | { type: 'data' | typeof CONTENT_LAYER_TYPE; entries: Record; + } + | { + type: typeof LIVE_CONTENT_TYPE; + entries: Record; }; }; @@ -493,6 +498,11 @@ async function writeContentFiles({ const collectionEntryKeys = Object.keys(collection.entries).sort(); const dataType = await typeForCollection(collectionConfig, collectionKey); switch (resolvedType) { + case LIVE_CONTENT_TYPE: + // This error should never be thrown, as it should have been caught earlier in the process + throw new AstroUserError( + `Invalid definition for collection ${collectionKey}: Live content collections must be defined in "src/live.config.ts"`, + ); case 'content': if (collectionEntryKeys.length === 0) { contentTypesStr += `${collectionKey}: Record;\n`; @@ -579,17 +589,28 @@ async function writeContentFiles({ contentPaths.config.url.pathname, ); + const liveConfigPathRelativeToCacheDir = contentPaths.liveConfig?.exists + ? normalizeConfigPath(settings.dotAstroDir.pathname, contentPaths.liveConfig.url.pathname) + : undefined; + for (const contentEntryType of contentEntryTypes) { if (contentEntryType.contentModuleTypes) { typeTemplateContent = contentEntryType.contentModuleTypes + '\n' + typeTemplateContent; } } - typeTemplateContent = typeTemplateContent.replace('// @@CONTENT_ENTRY_MAP@@', contentTypesStr); - typeTemplateContent = typeTemplateContent.replace('// @@DATA_ENTRY_MAP@@', dataTypesStr); - typeTemplateContent = typeTemplateContent.replace( - "'@@CONTENT_CONFIG_TYPE@@'", - contentConfig ? `typeof import(${configPathRelativeToCacheDir})` : 'never', - ); + typeTemplateContent = typeTemplateContent + .replace('// @@CONTENT_ENTRY_MAP@@', contentTypesStr) + .replace('// @@DATA_ENTRY_MAP@@', dataTypesStr) + .replace( + "'@@CONTENT_CONFIG_TYPE@@'", + contentConfig ? `typeof import(${configPathRelativeToCacheDir})` : 'never', + ) + .replace( + "'@@LIVE_CONTENT_CONFIG_TYPE@@'", + liveConfigPathRelativeToCacheDir + ? `typeof import(${liveConfigPathRelativeToCacheDir})` + : 'never', + ); // If it's the first time, we inject types the usual way. sync() will handle creating files and references. If it's not the first time, we just override the dts content if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) { diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index a319dfd297cd..ba84ef5f2a17 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -22,6 +22,7 @@ import { CONTENT_MODULE_FLAG, DEFERRED_MODULE, IMAGE_IMPORT_PREFIX, + LIVE_CONTENT_TYPE, PROPAGATED_ASSET_FLAG, } from './consts.js'; import { glob } from './loaders/glob.js'; @@ -103,6 +104,11 @@ const collectionConfigParser = z.union([ /** deprecated */ _legacy: z.boolean().optional(), }), + z.object({ + type: z.literal(LIVE_CONTENT_TYPE), + schema: z.any().optional(), + loader: z.function(), + }), ]); const contentConfigParser = z.object({ @@ -556,7 +562,10 @@ async function autogenerateCollections({ const dataPattern = globWithUnderscoresIgnored('', dataExts); let usesContentLayer = false; for (const collectionName of Object.keys(collections)) { - if (collections[collectionName]?.type === 'content_layer') { + if ( + collections[collectionName]?.type === 'content_layer' || + collections[collectionName]?.type === 'live' + ) { usesContentLayer = true; // This is already a content layer, skip continue; @@ -704,13 +713,25 @@ export type ContentPaths = { exists: boolean; url: URL; }; + liveConfig: { + exists: boolean; + url: URL; + }; }; export function getContentPaths( - { srcDir, legacy, root }: Pick, + { + srcDir, + legacy, + root, + experimental, + }: Pick, fs: typeof fsMod = fsMod, ): ContentPaths { - const configStats = search(fs, srcDir, legacy?.collections); + const configStats = searchConfig(fs, srcDir, legacy?.collections); + const liveConfigStats = experimental?.liveContentLoaders + ? searchLiveConfig(fs, srcDir) + : { exists: false, url: new URL('./', srcDir) }; const pkgBase = new URL('../../', import.meta.url); return { root: new URL('./', root), @@ -719,9 +740,15 @@ export function getContentPaths( typesTemplate: new URL('templates/content/types.d.ts', pkgBase), virtualModTemplate: new URL('templates/content/module.mjs', pkgBase), config: configStats, + liveConfig: liveConfigStats, }; } -function search(fs: typeof fsMod, srcDir: URL, legacy?: boolean) { + +function searchConfig( + fs: typeof fsMod, + srcDir: URL, + legacy?: boolean, +): { exists: boolean; url: URL } { const paths = [ ...(legacy ? [] @@ -730,13 +757,28 @@ function search(fs: typeof fsMod, srcDir: URL, legacy?: boolean) { 'content/config.js', 'content/config.mts', 'content/config.ts', - ].map((p) => new URL(`./${p}`, srcDir)); - for (const file of paths) { + ]; + return search(fs, srcDir, paths); +} + +function searchLiveConfig(fs: typeof fsMod, srcDir: URL): { exists: boolean; url: URL } { + const paths = [ + 'live.config.mjs', + 'live.config.js', + 'live.config.mts', + 'live.config.ts', + ]; + return search(fs, srcDir, paths); +} + +function search(fs: typeof fsMod, srcDir: URL, paths: string[]): { exists: boolean; url: URL } { + const urls = paths.map((p) => new URL(`./${p}`, srcDir)); + for (const file of urls) { if (fs.existsSync(file)) { return { exists: true, url: file }; } } - return { exists: false, url: paths[0] }; + return { exists: false, url: urls[0] }; } /** 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 ff0435844b5c..d52241aa0a0a 100644 --- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts +++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts @@ -247,7 +247,14 @@ async function generateContentEntryFile({ .replace("'@@CONTENT_ENTRY_GLOB_PATH@@'", contentEntryGlobResult) .replace("'@@DATA_ENTRY_GLOB_PATH@@'", dataEntryGlobResult) .replace("'@@RENDER_ENTRY_GLOB_PATH@@'", renderEntryGlobResult) - .replace('/* @@LOOKUP_MAP_ASSIGNMENT@@ */', `lookupMap = ${JSON.stringify(lookupMap)};`); + .replace('/* @@LOOKUP_MAP_ASSIGNMENT@@ */', `lookupMap = ${JSON.stringify(lookupMap)};`) + .replace( + '/* @@LIVE_CONTENT_CONFIG@@ */', + contentPaths.liveConfig.exists + ? // Dynamic import so it extracts the chunk and avoids a circular import + `const liveCollections = (await import(${JSON.stringify(fileURLToPath(contentPaths.liveConfig.url))})).collections;` + : 'const liveCollections = {};', + ); } return virtualModContents; @@ -258,13 +265,7 @@ async function generateContentEntryFile({ * This is used internally to resolve entry imports when using `getEntry()`. * @see `templates/content/module.mjs` */ -async function generateLookupMap({ - settings, - fs, -}: { - settings: AstroSettings; - fs: typeof nodeFs; -}) { +async function generateLookupMap({ settings, fs }: { settings: AstroSettings; fs: typeof nodeFs }) { const { root } = settings.config; const contentPaths = getContentPaths(settings.config); const relContentDir = rootRelativePath(root, contentPaths.contentDir, false); diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 3743ca2658bb..afdd537acee8 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -2268,14 +2268,14 @@ export interface ViteUserConfig extends OriginalViteUserConfig { */ preserveScriptOrder?: boolean; - /** + /** * @name experimental.liveContentLoaders * @type {boolean} * @default `false` * @version 5.7 * @description * Enables the use of live content loaders. - * + * */ liveContentLoaders?: boolean; }; diff --git a/packages/astro/src/types/public/content.ts b/packages/astro/src/types/public/content.ts index 171c8d550e4d..9810ac1c480b 100644 --- a/packages/astro/src/types/public/content.ts +++ b/packages/astro/src/types/public/content.ts @@ -124,3 +124,17 @@ export interface DataEntryType { } export type GetDataEntryInfoReturnType = { data: Record; rawData?: string }; + +export interface LiveDataEntry = Record> { + /** The ID of the entry. Unique per collection. */ + id: string; + /** The parsed entry data */ + data: TData; +} + +export interface LiveDataCollection< + TData extends Record = Record, +> { + entries: Array>; + // TODO: pagination etc. +} diff --git a/packages/astro/templates/content/module.mjs b/packages/astro/templates/content/module.mjs index 7947574c341c..3d1c8123f0cb 100644 --- a/packages/astro/templates/content/module.mjs +++ b/packages/astro/templates/content/module.mjs @@ -12,6 +12,8 @@ import { export { defineCollection, renderEntry as render } from 'astro/content/runtime'; export { z } from 'astro/zod'; +/* @@LIVE_CONTENT_CONFIG@@ */ + const contentDir = '@@CONTENT_DIR@@'; const contentEntryGlob = '@@CONTENT_ENTRY_GLOB_PATH@@'; @@ -56,12 +58,14 @@ export const getCollection = createGetCollection({ dataCollectionToEntryMap, getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap), cacheEntriesByCollection, + liveCollections, }); export const getEntry = createGetEntry({ getEntryImport: createGlobLookup(collectionToEntryMap), getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap), collectionNames, + liveCollections, }); export const getEntryBySlug = createGetEntryBySlug({ diff --git a/packages/astro/templates/content/types.d.ts b/packages/astro/templates/content/types.d.ts index 2d1058d271f7..cba03fb374b3 100644 --- a/packages/astro/templates/content/types.d.ts +++ b/packages/astro/templates/content/types.d.ts @@ -45,6 +45,10 @@ declare module 'astro:content' { collection: C; slug: E; }; + export type ReferenceLiveEntry = { + collection: C; + id: string; + }; /** @deprecated Use `getEntry` instead. */ export function getEntryBySlug< @@ -73,6 +77,11 @@ declare module 'astro:content' { filter?: (entry: CollectionEntry) => unknown, ): Promise[]>; + export function getCollection( + collection: C, + filter?: LiveLoaderCollectionFilterType, + ): Promise>>; + export function getEntry< C extends keyof ContentEntryMap, E extends ValidContentEntrySlug | (string & {}), @@ -109,6 +118,10 @@ declare module 'astro:content' { ? Promise | undefined : Promise : Promise | undefined>; + export function getEntry( + collection: C, + filter: string | LiveLoaderEntryFilterType, + ): Promise>>; /** Resolve an array of entry references from the same collection */ export function getEntries( @@ -117,6 +130,9 @@ declare module 'astro:content' { export function getEntries( entries: ReferenceDataEntry[], ): Promise[]>; + export function getEntries( + entries: ReferenceLiveEntry[], + ): Promise>[]>; export function render( entry: AnyEntryMap[C][string], @@ -152,5 +168,25 @@ declare module 'astro:content' { type AnyEntryMap = ContentEntryMap & DataEntryMap; + type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< + infer TData, + infer TEntryFilter, + infer TCollectionFilter + > + ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter } + : { data: never; entryFilter: never; collectionFilter: never }; + type ExtractDataType = ExtractLoaderTypes['data']; + type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; + type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; + + type LiveLoaderDataType = ExtractDataType< + LiveContentConfig['collections'][C]['loader'] + >; + type LiveLoaderEntryFilterType = + ExtractEntryFilterType; + type LiveLoaderCollectionFilterType = + ExtractCollectionFilterType; + export type ContentConfig = '@@CONTENT_CONFIG_TYPE@@'; + export type LiveContentConfig = '@@LIVE_CONTENT_CONFIG_TYPE@@'; } diff --git a/packages/astro/test/fixtures/live-loaders/astro.config.mjs b/packages/astro/test/fixtures/live-loaders/astro.config.mjs new file mode 100644 index 000000000000..30a8c2568a74 --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/astro.config.mjs @@ -0,0 +1,14 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + adapter: node({ + mode: 'standalone' + }), + experimental: { + liveContentLoaders: true + } +}); diff --git a/packages/astro/test/fixtures/live-loaders/package.json b/packages/astro/test/fixtures/live-loaders/package.json new file mode 100644 index 000000000000..6be0ee921d04 --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/package.json @@ -0,0 +1,15 @@ +{ + "name": "@test/live-loaders", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/live-loaders/src/content.config.ts b/packages/astro/test/fixtures/live-loaders/src/content.config.ts new file mode 100644 index 000000000000..f3a629cd1cf8 --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/src/content.config.ts @@ -0,0 +1,5 @@ +import { defineCollection } from "astro:content"; +const something = defineCollection({ + loader: () => ([]) +}) +export const collections = { something }; diff --git a/packages/astro/test/fixtures/live-loaders/src/live.config.ts b/packages/astro/test/fixtures/live-loaders/src/live.config.ts new file mode 100644 index 000000000000..09fb8736f91f --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/src/live.config.ts @@ -0,0 +1,31 @@ +import { defineCollection, z, type BaseSchema } from 'astro:content'; +import type { LiveLoader } from 'astro/loaders'; + +type Entry = { + title: string; +}; + +const entries = { + '123': { id: '123', data: { title: 'Page 123' } }, + '456': { id: '456', data: { title: 'Page 456' } }, + '789': { id: '789', data: { title: 'Page 789' } }, +}; + +const loader: LiveLoader = { + name: 'test-loader', + loadEntry: async (context) => { + return entries[context.filter.id] || null; + }, + loadCollection: async (context) => { + return { + entries: Object.values(entries), + }; + }, +}; + +const liveStuff = defineCollection({ + type: 'live', + loader, +}); + +export const collections = { liveStuff }; diff --git a/packages/astro/test/fixtures/live-loaders/src/pages/index.astro b/packages/astro/test/fixtures/live-loaders/src/pages/index.astro new file mode 100644 index 000000000000..83a72f143c7c --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/src/pages/index.astro @@ -0,0 +1,28 @@ +--- +import { getCollection } from "astro:content"; + + +const collection = await getCollection("liveStuff") +export const prerender = false; + +--- + + + + + + + + Astro + + +

Astro

+
    + {collection.entries.map((item) => ( +
  • + {item.data.title} +
  • + ))} +
+ + diff --git a/packages/astro/test/fixtures/live-loaders/src/pages/more.astro b/packages/astro/test/fixtures/live-loaders/src/pages/more.astro new file mode 100644 index 000000000000..e57b915157d5 --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/src/pages/more.astro @@ -0,0 +1,21 @@ +--- +import { getEntry } from "astro:content"; + +const data = await getEntry("liveStuff", "123") +export const prerender = false; + +--- + + + + + + + + Astro + + +

{data.data.title}

+ + + diff --git a/packages/astro/test/fixtures/live-loaders/tsconfig.json b/packages/astro/test/fixtures/live-loaders/tsconfig.json new file mode 100644 index 000000000000..8bf91d3bb997 --- /dev/null +++ b/packages/astro/test/fixtures/live-loaders/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/packages/astro/types/content.d.ts b/packages/astro/types/content.d.ts index 51516b8acf69..59495d28bf78 100644 --- a/packages/astro/types/content.d.ts +++ b/packages/astro/types/content.d.ts @@ -87,14 +87,25 @@ declare module 'astro:content' { loader?: never; }; - export type CollectionConfig = - | ContentCollectionConfig - | DataCollectionConfig - | ContentLayerConfig; - - export function defineCollection( - input: CollectionConfig, - ): CollectionConfig; + type LiveDataCollectionConfig< + S extends BaseSchema, + L extends import('astro/loaders').LiveLoader, + > = { + type: 'live'; + schema?: S; + loader: L; + }; + + export type CollectionConfig< + S extends BaseSchema, + TLiveLoader = never, + > = TLiveLoader extends import('astro/loaders').LiveLoader + ? LiveDataCollectionConfig + : ContentCollectionConfig | DataCollectionConfig | ContentLayerConfig; + + export function defineCollection( + input: CollectionConfig, + ): CollectionConfig; /** Run `astro dev` or `astro sync` to generate high fidelity types */ export const getEntryBySlug: (...args: any[]) => any; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ae548a3c05e..a6a59c37a74f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3366,6 +3366,15 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/live-loaders: + dependencies: + '@astrojs/node': + specifier: workspace:* + version: link:../../../../integrations/node + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/markdown: dependencies: '@astrojs/preact': diff --git a/tsconfig.base.json b/tsconfig.base.json index 432d3c35380d..2141875ed131 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,4 +14,4 @@ "noUnusedLocals": true, "noUnusedParameters": true } -} +} \ No newline at end of file