UNPKG

@inox-tools/content-utils

Version:

Utilities to work with content collections on an Astro project from an integration or library.

172 lines (149 loc) 5.52 kB
import { withApi, onHook, registerGlobalHooks } from '@inox-tools/modular-station'; import { emptyState, type IntegrationState } from './state.js'; import { resolveContentPaths } from '../internal/resolver.js'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { addVitePlugin, defineIntegration } from 'astro-integration-kit'; import { injectorPlugin } from './injectorPlugin.js'; import { seedCollections, type SeedCollectionsOptions } from './seedCollections.js'; import { gitBuildPlugin, gitDevPlugin } from './gitPlugin.js'; import { debug } from '../internal/debug.js'; import * as devalue from 'devalue'; import { z } from 'astro/zod'; export type InjectCollectionOptions = { /** * Module to be imported with the configured collections. * * This module should be resolvable from the root of the Astro project and must export a `collections` object. */ entrypoint: string; /** * Seed collections using this template if they are not present. * * @see {seedCollections} */ seedTemplateDirectory?: string; }; export const integration = withApi( defineIntegration({ name: '@inox-tools/content-utils', optionsSchema: z .object({ staticOnlyCollections: z.array(z.string()).optional().default([]), }) .optional() .default({}), setup: ({ options: { staticOnlyCollections } }) => { debug('Generating empty state'); const state = emptyState(); state.staticOnlyCollections.push(...staticOnlyCollections); const collectionSeedBuffer: SeedCollectionsOptions[] = []; const api = { /** * Inject a content collection definition alongside the project. * * Collections defined here can be overriden by the project. */ injectCollection: onHook( ['astro:config:setup', 'astro:config:done', 'astro:build:start', 'astro:server:setup'], (options: InjectCollectionOptions) => { debug('Injecting collection:', options); state.injectedCollectionsEntrypoints.push(options.entrypoint); if (options.seedTemplateDirectory) { api.seedCollections({ templateDirectory: options.seedTemplateDirectory, }); } } ), seedCollections: onHook( ['astro:config:setup', 'astro:config:done', 'astro:build:start', 'astro:server:setup'], (options: SeedCollectionsOptions) => { debug('Requesting collection seeding:', options); if (state.contentPaths === undefined) { collectionSeedBuffer.push(options); } else { seedCollections(state, options); } } ), }; return { hooks: { 'astro:config:setup': (params) => { state.logger = params.logger; registerGlobalHooks(params); state.contentPaths = resolveContentPaths(params.config); debug('Resolved content paths:', state.contentPaths); if (!state.contentPaths.configExists) { // Create the `<srcDir>/content/config.ts` file if it doesn't exist, // otherwise there is no module to modify in the Vite lifecycle. debug('Creating minimal content config file:', state.contentPaths.configPath); mkdirSync(state.contentPaths.contentPath, { recursive: true }); writeFileSync(state.contentPaths.configPath, 'export const collections = {};'); } debug('Adding content collection injector Vite plugin'); addVitePlugin(params, { plugin: injectorPlugin(state), warnDuplicated: true, }); debug('Adding Git time Vite plugin'); addVitePlugin(params, { plugin: params.command === 'dev' ? gitDevPlugin(state) : gitBuildPlugin(state), warnDuplicated: true, }); debug('Seeding collections from buffer'); for (const seedOptions of collectionSeedBuffer) { seedCollections(state, seedOptions); } }, 'astro:build:done': async () => { await clearStaticCollections(state); for (const cleanup of state.cleanups) { await cleanup(); } }, }, ...api, }; }, }) ); async function clearStaticCollections(state: IntegrationState) { // After the build is done, if there was such a chunk and there are collections // that should only be present during static build, clean them. if ( !( state.staticOnlyCollections.length > 0 && state.contentDataEntrypoint && existsSync(state.contentDataEntrypoint) ) ) return; const originalContent = readFileSync(state.contentDataEntrypoint, 'utf-8'); // Content was already cleared by Astro. Collections are not used anywhere on server bundle if (!originalContent.includes('export')) return; // Import the chunk, which exports a devalue flattened map as the default export const { default: value } = await import(/*@vite-ignore*/ state.contentDataEntrypoint); // Unflatten the map const map: Map<string, Map<string, unknown>> = devalue.unflatten(value); // Remove all the collections we promise we won't use on the server for (const collection of state.staticOnlyCollections) { map.delete(collection); } // Build the source code with the new map flattened const newContent = [ `const _astro_dataLayerContent = ${devalue.stringify(map)}`, '\nexport { _astro_dataLayerContent as default }', ].join('\n'); // Write it back writeFileSync(state.contentDataEntrypoint, newContent, 'utf-8'); } const triggers = Symbol.for('@inox-tools/content-utils:triggers/gitTrackedListResolved'); /** * @internal */ declare global { const globalThis: typeof global & { [triggers]: string; }; }