UNPKG

@inox-tools/content-utils

Version:

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

150 lines (125 loc) 4.67 kB
import type { Plugin } from 'vite'; import { walk, type Node } from 'estree-walker'; import * as assert from 'node:assert'; import MagicString from 'magic-string'; import { AstroError } from 'astro/errors'; import type { IntegrationState } from './state.js'; import { fileURLToPath } from 'node:url'; import { dirname, resolve, join as joinPath } from 'node:path'; import { getDebug } from '../internal/debug.js'; const debug = getDebug('injector-plugin'); const thisFile = fileURLToPath(import.meta.url); const thisDir = dirname(thisFile); debug('Resolution base:', { thisFile, thisDir }); const INJECTOR_VIRTUAL_MODULE = '@it-astro:content/injector'; const RESOLVED_INJECTOR_VIRTUAL_MODULE = `\0${INJECTOR_VIRTUAL_MODULE}`; const CONTENT_VIRTUAL_MODULE = '@it-astro:content'; const RESOLVED_CONTENT_VIRTUAL_MODULE = `\0${CONTENT_VIRTUAL_MODULE}`; export const injectorPlugin = (state: IntegrationState): Plugin => { const { logger, injectedCollectionsEntrypoints: entrypoints, contentPaths: { configPath: configFile }, } = state; return { name: '@inox-tools/content-utils/injector', resolveId(id) { switch (id) { case INJECTOR_VIRTUAL_MODULE: return RESOLVED_INJECTOR_VIRTUAL_MODULE; case CONTENT_VIRTUAL_MODULE: return RESOLVED_CONTENT_VIRTUAL_MODULE; } }, load(id) { switch (id) { case RESOLVED_INJECTOR_VIRTUAL_MODULE: debug('Generating injected collection modole from:', entrypoints); return [ ...entrypoints.map( (entrypoint, index) => `import {collections as __collections${index}} from '${entrypoint}';` ), 'export const injectedCollections = {', ...entrypoints.map((_, index) => `...__collections${index},`), '};', ].join('\n'); case RESOLVED_CONTENT_VIRTUAL_MODULE: debug('Generating fancy content module'); return [ `export {defineCollection} from ${JSON.stringify(resolve(thisDir, 'runtime/fancyContent.js'))};`, 'export {z, reference} from "astro:content";', ].join('\n'); } }, transform(code, id) { if (id !== configFile) return; debug('Transforming config file'); const ast = this.parse(code); const s = new MagicString(code); function update(node: Node, updater: (code: string) => string) { assert.ok(isBryceNode(node), 'Ping Bryce, he lied!'); const { start, end } = node; const oldCode = s.slice(start, end); const newCode = updater(oldCode); if (oldCode === newCode) { debug('Code is unnafected by transformation.'); return; } s.update(start, end, newCode); } debug('Adding import for collection injection runtime'); // This imports the collection injection under a name unconditionally because the plugin is never injected // more than once. If this guarantee changes, this line would require some logic to ensure a unique identifier. s.prepend( `import {injectCollections as $$inox_tools__injectCollection} from ${JSON.stringify(resolve(thisDir, 'runtime/injector.js'))};` ); walk(ast, { enter(node, parent) { if (parent?.type !== 'ExportNamedDeclaration' || node.type !== 'VariableDeclaration') return; const collectionDeclaration = node.declarations.find((value) => { return value.id.type === 'Identifier' && value.id.name === 'collections'; }); if (collectionDeclaration?.init == null) { throw new AstroError( 'Exported collections is not initialized.', `Change your ${configFile} to initialize the value of "collections".` ); } if (node.kind !== 'const') { logger.warn( 'Exporting collections config using "let" may have unintended consequences. ' + `Prefer "export const collections" in your "${configFile}".` ); } const sourceInit = collectionDeclaration.init; debug('Wrapping collection definition with collection injection'); update(sourceInit, (code) => `$$inox_tools__injectCollection(${code})`); }, }); return { code: s.toString(), map: s.generateMap(), }; }, writeBundle(info, bundle) { if (!info.dir) return; for (const chunk of Object.values(bundle)) { if (chunk.type !== 'chunk') continue; if (chunk.moduleIds.length === 1 && chunk.moduleIds[0] === '\0astro:data-layer-content') { state.contentDataEntrypoint = joinPath(info.dir, chunk.fileName); break; } } }, }; }; type BryceNode = Node & { start: number; end: number; }; // Bryce said this always happens function isBryceNode(node: Node): node is BryceNode { return typeof (node as any).start === 'number' && typeof (node as any).end === 'number'; }