mirror of
https://github.com/withastro/astro.git
synced 2025-01-06 22:10:10 -05:00
c6d7ebefdd
* feat: add generated lookup-map * feat: wire up fast getEntryBySlug() lookup * fix: consider frontmatter slugs * chore: changeset * chore: lint no-shadow * fix: revert bad rootRelativePath change * chore: better var name * refactor: generated `.json` to in-memory map * chore: removed unneeded await Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> * chore: removed unneeded await Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> * Revert "chore: removed unneeded await" This reverts commit1b0a8b00c2
. * fix: bad `GetEntryImport` type * chore: remove unused variable * refactor: for -> Promise.all * refactor: replace duplicate parseSlug * refactor: add cache layer * Revert "refactor: add cache layer" This reverts commit1c3bfdc6b3
. * feat: json collection POC * wip: add test json file * wip: playing with api ideas * refactor: extract getCollectionName * feat: add defineDataCollection * refactor: variable destructure * wip: basic data entry pipeline * chore: revert fixture playing * wip: basic entry array parser * feat: basic data type gen * chore: add with-data playground * feat: add error when `defineDataCollection()` isn't used * fix: missing error message * feat: data collections are here! * wip: play with data query APIs * feat: reference() util! * fix: Markdoc `$entry` variable * play: add reference util with markdoc * chore: delete console logs * feat: `src/data/`! * feat: reference() errors * fix: handle hoisted schema parse errors * fix: reload config and invalid on collection changes * feat: separate maps for content and data entries * feat: new `reference()` API that fixes type inference * feat: support `defineCollection()` for data config * fix: defineCollection `type` inferenenceπinference * chore: lock * feat: getCollection() for everything! * feat: get full entry access from reference() * chore: changeset * wip: type error on acorn? * chore: lint * chore: add slugger to data ID processing * chore: astro/zod -> zod * chore: example version * chore: remove slugifier from data id * chore: remove dead getDataCollection * chore: remove dead defineDataCollection * fix: bad collection import * chore: lock * feat: add data collections to lookup map * refactor: stop resolving data from reference * feat: introduce getEntry and new reference() * fix: update config loader * fix: reference() type * feat: test self references (they work 🎉) * fix: use `slug` for content references * fix: bad getEntry content type * chroe: remove console logs * fix: strict null checks on with-data * feat: add getEntries for ref arrays * chore: fix type hints for reference strings * chore: change to type never for clarity * play: try getEntries * Return to "everything goes in `src/content/` This reverts commit cc637ec6db4fc23afab585df5f240b7f7c0abc8a. * fix: remove old function * chore: update to AstroErrors * chore: remove unused fixture files * play: names * deps: js-yaml * feat: data collection YAML with error handling * refactor: remove console log * refactor: code cleanup * fix: allow mixed content to pass through glob imports * chore: move lookupMap util to virtual-mod * refactor: new lookupMap logic, better errors * chore: change MixedContent title * refactor: remove unneeded try / catch * fix: use `ws.send` for type gen errors * fix: bubble `ws.send` errors from astro sync * refactor: revert verbose astroContentCollectionEntry * fix: bad with-data package name * fix: bad virtual mod flag * chore: remove with-data playground * test: data collection authors * test: translations data collection * fix: add `.yml` support * refactor: mix in `.yaml` just for fun * refactor: i18n -> translations * chore: content-collection-references fixture * chore: bad lockfile * fix: bad ContentLookupMap import * chore: revert back to astroContentCollectionEntry * test: collection references * fix: bad error code override * chore: remove unused asset * test: sync errors * chore: remove stray console log * chore: lock * chore: revert with-markdoc changes * chore: doc error states, remove bad merge code * chore: remove bad `as any` * chore: lint * chore: inline ContentLookupMap comments * chore: settings -> config * fix: put back `defineCollection()` * fix: entry.slug for get content collection * chore: update get-entry-type tests * docs: totally shorten "missing a `type`" Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * docs: truncate share a `schema` Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * chore: add `test:unit` and `test:unit:match`to base * chore: update changeset * refactor: cleanup runtime types and inline comments * nit: [0] instead of shift() * refactor: `getRelativeEntryPath()` util * chore: capitalized Collections for test:match * nit: ?? viteId on split * nit: separate Params obj * chore: add try / catch on readFile * nit: `const data` * chore: clean up data collection exceptions * nit: `?? ''` for search params * chore: remove TODO on hoisted error --------- Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
200 lines
7.2 KiB
TypeScript
200 lines
7.2 KiB
TypeScript
/* eslint-disable no-console */
|
|
import type { Node } from '@markdoc/markdoc';
|
|
import Markdoc from '@markdoc/markdoc';
|
|
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
|
import fs from 'node:fs';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { isValidUrl, MarkdocError, parseFrontmatter, prependForwardSlash } from './utils.js';
|
|
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
|
import { emitESMImage } from 'astro/assets';
|
|
import { bold, red, yellow } from 'kleur/colors';
|
|
import type * as rollup from 'rollup';
|
|
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';
|
|
import { applyDefaultConfig } from './runtime.js';
|
|
|
|
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
// `contentEntryType` is not a public API
|
|
// Add type defs here
|
|
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
|
};
|
|
|
|
export default function markdocIntegration(legacyConfig?: any): AstroIntegration {
|
|
if (legacyConfig) {
|
|
console.log(
|
|
`${red(
|
|
bold('[Markdoc]')
|
|
)} Passing Markdoc config from your \`astro.config\` is no longer supported. Configuration should be exported from a \`markdoc.config.mjs\` file. See the configuration docs for more: https://docs.astro.build/en/guides/integrations-guide/markdoc/#configuration`
|
|
);
|
|
process.exit(0);
|
|
}
|
|
let markdocConfigResult: MarkdocConfigResult | undefined;
|
|
return {
|
|
name: '@astrojs/markdoc',
|
|
hooks: {
|
|
'astro:config:setup': async (params) => {
|
|
const { config: astroConfig, addContentEntryType } = params as SetupHookParams;
|
|
|
|
markdocConfigResult = await loadMarkdocConfig(astroConfig);
|
|
const userMarkdocConfig = markdocConfigResult?.config ?? {};
|
|
|
|
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
|
return {
|
|
data: parsed.data,
|
|
body: parsed.content,
|
|
slug: parsed.data.slug,
|
|
rawData: parsed.matter,
|
|
};
|
|
}
|
|
addContentEntryType({
|
|
extensions: ['.mdoc'],
|
|
getEntryInfo,
|
|
async getRenderModule({ entry, viteId }) {
|
|
const ast = Markdoc.parse(entry.body);
|
|
const pluginContext = this;
|
|
const markdocConfig = applyDefaultConfig(userMarkdocConfig, entry);
|
|
|
|
const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
|
|
return (
|
|
// Ignore `variable-undefined` errors.
|
|
// Variables can be configured at runtime,
|
|
// so we cannot validate them at build time.
|
|
e.error.id !== 'variable-undefined' &&
|
|
(e.error.level === 'error' || e.error.level === 'critical')
|
|
);
|
|
});
|
|
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;
|
|
throw new MarkdocError({
|
|
message: [
|
|
`**${String(entry.collection)} → ${String(entry.id)}** contains invalid content:`,
|
|
...validationErrors.map((e) => `- ${e.error.message}`),
|
|
].join('\n'),
|
|
location: {
|
|
// Error overlay does not support multi-line or ranges.
|
|
// Just point to the first line.
|
|
line: frontmatterBlockOffset + validationErrors[0].lines[0],
|
|
file: viteId,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (astroConfig.experimental.assets) {
|
|
await emitOptimizedImages(ast.children, {
|
|
astroConfig,
|
|
pluginContext,
|
|
filePath: entry._internal.filePath,
|
|
});
|
|
}
|
|
|
|
const res = `import { jsx as h } from 'astro/jsx-runtime';
|
|
import { Renderer } from '@astrojs/markdoc/components';
|
|
import { collectHeadings, applyDefaultConfig, Markdoc, headingSlugger } from '@astrojs/markdoc/runtime';
|
|
import * as entry from ${JSON.stringify(viteId + '?astroContentCollectionEntry')};
|
|
${
|
|
markdocConfigResult
|
|
? `import _userConfig from ${JSON.stringify(
|
|
markdocConfigResult.fileUrl.pathname
|
|
)};\nconst userConfig = _userConfig ?? {};`
|
|
: 'const userConfig = {};'
|
|
}${
|
|
astroConfig.experimental.assets
|
|
? `\nimport { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';\nuserConfig.nodes = { ...experimentalAssetsConfig.nodes, ...userConfig.nodes };`
|
|
: ''
|
|
}
|
|
const stringifiedAst = ${JSON.stringify(
|
|
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
|
|
)};
|
|
export function getHeadings() {
|
|
${
|
|
/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
|
|
TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself,
|
|
instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
|
|
''
|
|
}
|
|
headingSlugger.reset();
|
|
const headingConfig = userConfig.nodes?.heading;
|
|
const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
|
|
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
|
|
const content = Markdoc.transform(ast, config);
|
|
return collectHeadings(Array.isArray(content) ? content : content.children);
|
|
}
|
|
export async function Content (props) {
|
|
headingSlugger.reset();
|
|
const config = applyDefaultConfig({
|
|
...userConfig,
|
|
variables: { ...userConfig.variables, ...props },
|
|
}, entry);
|
|
|
|
return h(Renderer, { config, stringifiedAst });
|
|
}`;
|
|
return { code: res };
|
|
},
|
|
contentModuleTypes: await fs.promises.readFile(
|
|
new URL('../template/content-module-types.d.ts', import.meta.url),
|
|
'utf-8'
|
|
),
|
|
});
|
|
},
|
|
'astro:server:setup': async ({ server }) => {
|
|
server.watcher.on('all', (event, entry) => {
|
|
if (pathToFileURL(entry).pathname === markdocConfigResult?.fileUrl.pathname) {
|
|
console.log(
|
|
yellow(
|
|
`${bold('[Markdoc]')} Restart the dev server for config changes to take effect.`
|
|
)
|
|
);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Emits optimized images, and appends the generated `src` to each AST node
|
|
* via the `__optimizedSrc` attribute.
|
|
*/
|
|
async function emitOptimizedImages(
|
|
nodeChildren: Node[],
|
|
ctx: {
|
|
pluginContext: rollup.PluginContext;
|
|
filePath: string;
|
|
astroConfig: AstroConfig;
|
|
}
|
|
) {
|
|
for (const node of nodeChildren) {
|
|
if (
|
|
node.type === 'image' &&
|
|
typeof node.attributes.src === 'string' &&
|
|
shouldOptimizeImage(node.attributes.src)
|
|
) {
|
|
// Attempt to resolve source with Vite.
|
|
// This handles relative paths and configured aliases
|
|
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
|
|
|
|
if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) {
|
|
const src = await emitESMImage(
|
|
resolved.id,
|
|
ctx.pluginContext.meta.watchMode,
|
|
ctx.pluginContext.emitFile,
|
|
{ config: ctx.astroConfig }
|
|
);
|
|
node.attributes.__optimizedSrc = src;
|
|
} else {
|
|
throw new MarkdocError({
|
|
message: `Could not resolve image ${JSON.stringify(
|
|
node.attributes.src
|
|
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`,
|
|
});
|
|
}
|
|
}
|
|
await emitOptimizedImages(node.children, ctx);
|
|
}
|
|
}
|
|
|
|
function shouldOptimizeImage(src: string) {
|
|
// Optimize anything that is NOT external or an absolute path to `public/`
|
|
return !isValidUrl(src) && !src.startsWith('/');
|
|
}
|