Skip to content

Commit bef2690

Browse files
committed
feat: addContentEntryType integration hook
1 parent 508568f commit bef2690

12 files changed

Lines changed: 135 additions & 43 deletions

File tree

packages/astro/src/@types/astro.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,12 +977,27 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
977977
integrations: AstroIntegration[];
978978
}
979979

980+
export interface ContentEntryType {
981+
extensions: string[];
982+
getEntryInfo(params: { fileUrl: URL }): Promise<{
983+
data: Record<string, unknown>;
984+
/**
985+
* Used for error hints to point to correct line and location
986+
* Should be the untouched data as read from the file,
987+
* including newlines
988+
*/
989+
rawData: string;
990+
body: string;
991+
slug: string;
992+
}>;
993+
}
994+
980995
export interface AstroSettings {
981996
config: AstroConfig;
982-
983997
adapter: AstroAdapter | undefined;
984998
injectedRoutes: InjectedRoute[];
985999
pageExtensions: string[];
1000+
contentEntryTypes: ContentEntryType[];
9861001
renderers: AstroRenderer[];
9871002
scripts: {
9881003
stage: InjectedScriptStage;

packages/astro/src/content/consts.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
/** TODO as const*/
2-
export const defaultContentFileExts = ['.md', '.mdx'];
1+
export const defaultContentEntryExts = ['.md', '.mdx'] as const;
32
export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets';
43
export const CONTENT_FLAG = 'astroContent';
54
export const VIRTUAL_MODULE_ID = 'astro:content';

packages/astro/src/content/types-generator.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import type { AstroSettings } from '../@types/astro.js';
88
import { AstroError, AstroErrorData } from '../core/errors/index.js';
99
import { info, LogOptions, warn } from '../core/logger/core.js';
1010
import { isRelativePath } from '../core/path.js';
11-
import { CONTENT_TYPES_FILE, defaultContentFileExts } from './consts.js';
11+
import { CONTENT_TYPES_FILE } from './consts.js';
1212
import {
1313
ContentConfig,
1414
ContentObservable,
1515
ContentPaths,
1616
EntryInfo,
17+
getContentEntryExts,
1718
getContentPaths,
1819
getEntryInfo,
1920
getEntrySlug,
@@ -22,7 +23,6 @@ import {
2223
NoCollectionError,
2324
parseFrontmatter,
2425
} from './utils.js';
25-
import { contentEntryTypes } from './~dream.js';
2626

2727
type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
2828
type RawContentEvent = { name: ChokidarEvent; entry: string };
@@ -53,10 +53,7 @@ export async function createContentTypesGenerator({
5353
}: CreateContentGeneratorParams) {
5454
const contentTypes: ContentTypes = {};
5555
const contentPaths = getContentPaths(settings.config, fs);
56-
const contentFileExts = [
57-
...defaultContentFileExts,
58-
...contentEntryTypes.map((t) => t.extensions).flat(),
59-
];
56+
const contentEntryExts = getContentEntryExts(settings);
6057

6158
let events: Promise<{ shouldGenerateTypes: boolean; error?: Error }>[] = [];
6259
let debounceTimeout: NodeJS.Timeout | undefined;
@@ -117,7 +114,7 @@ export async function createContentTypesGenerator({
117114
}
118115
return { shouldGenerateTypes: true };
119116
}
120-
const fileType = getEntryType(fileURLToPath(event.entry), contentPaths, contentFileExts);
117+
const fileType = getEntryType(fileURLToPath(event.entry), contentPaths, contentEntryExts);
121118
if (fileType === 'ignored') {
122119
return { shouldGenerateTypes: false };
123120
}

packages/astro/src/content/utils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from '
77
import { z } from 'zod';
88
import { AstroConfig, AstroSettings } from '../@types/astro.js';
99
import { AstroError, AstroErrorData } from '../core/errors/index.js';
10-
import { appendForwardSlash } from '../core/path.js';
11-
import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js';
10+
import { CONTENT_TYPES_FILE, defaultContentEntryExts } from './consts.js';
1211

1312
export const collectionConfigParser = z.object({
1413
schema: z.any().optional(),
@@ -119,6 +118,14 @@ export async function getEntryData(
119118
return data;
120119
}
121120

121+
export function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryTypes'>) {
122+
return [
123+
// TODO: roll defaults into settings
124+
...defaultContentEntryExts,
125+
...settings.contentEntryTypes.map((t) => t.extensions).flat(),
126+
];
127+
}
128+
122129
export class NoCollectionError extends Error {}
123130

124131
export function getEntryInfo(

packages/astro/src/content/vite-plugin-content-assets.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import npath from 'node:path';
22
import { pathToFileURL } from 'url';
33
import type { Plugin } from 'vite';
4+
import { AstroSettings } from '../@types/astro.js';
45
import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js';
56
import { BuildInternals, getPageDataByViteID } from '../core/build/internal.js';
67
import { AstroBuildPlugin } from '../core/build/plugin.js';
@@ -11,23 +12,30 @@ import { prependForwardSlash } from '../core/path.js';
1112
import { getStylesForURL } from '../core/render/dev/css.js';
1213
import { getScriptsForURL } from '../core/render/dev/scripts.js';
1314
import {
14-
defaultContentFileExts,
1515
LINKS_PLACEHOLDER,
1616
PROPAGATED_ASSET_FLAG,
1717
SCRIPTS_PLACEHOLDER,
1818
STYLES_PLACEHOLDER,
1919
} from './consts.js';
20+
import { getContentEntryExts } from './utils.js';
2021

21-
function isPropagatedAsset(viteId: string): boolean {
22+
function isPropagatedAsset(viteId: string, contentEntryExts: string[]): boolean {
2223
const url = new URL(viteId, 'file://');
2324
return (
2425
url.searchParams.has(PROPAGATED_ASSET_FLAG) &&
25-
defaultContentFileExts.some((ext) => url.pathname.endsWith(ext))
26+
contentEntryExts.some((ext) => url.pathname.endsWith(ext))
2627
);
2728
}
2829

29-
export function astroContentAssetPropagationPlugin({ mode }: { mode: string }): Plugin {
30+
export function astroContentAssetPropagationPlugin({
31+
mode,
32+
settings,
33+
}: {
34+
mode: string;
35+
settings: AstroSettings;
36+
}): Plugin {
3037
let devModuleLoader: ModuleLoader;
38+
const contentEntryExts = getContentEntryExts(settings);
3139
return {
3240
name: 'astro:content-asset-propagation',
3341
enforce: 'pre',
@@ -37,7 +45,7 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }):
3745
}
3846
},
3947
load(id) {
40-
if (isPropagatedAsset(id)) {
48+
if (isPropagatedAsset(id, contentEntryExts)) {
4149
const basePath = id.split('?')[0];
4250
const code = `
4351
export async function getMod() {
@@ -52,7 +60,7 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }):
5260
},
5361
async transform(code, id, options) {
5462
if (!options?.ssr) return;
55-
if (devModuleLoader && isPropagatedAsset(id)) {
63+
if (devModuleLoader && isPropagatedAsset(id, contentEntryExts)) {
5664
const basePath = id.split('?')[0];
5765
if (!devModuleLoader.getModuleById(basePath)?.ssrModule) {
5866
await devModuleLoader.import(basePath);

packages/astro/src/content/vite-plugin-content-imports.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { contentEntryTypes } from './~dream.js';
21
import * as devalue from 'devalue';
32
import type fsMod from 'node:fs';
43
import { pathToFileURL } from 'url';
@@ -7,9 +6,10 @@ import { AstroSettings } from '../@types/astro.js';
76
import { AstroErrorData } from '../core/errors/errors-data.js';
87
import { AstroError } from '../core/errors/errors.js';
98
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
10-
import { defaultContentFileExts, CONTENT_FLAG } from './consts.js';
9+
import { CONTENT_FLAG } from './consts.js';
1110
import {
1211
ContentConfig,
12+
getContentEntryExts,
1313
getContentPaths,
1414
getEntryData,
1515
getEntryInfo,
@@ -19,9 +19,9 @@ import {
1919
parseFrontmatter,
2020
} from './utils.js';
2121

22-
function isContentFlagImport(viteId: string) {
23-
const { searchParams } = new URL(viteId, 'file://');
24-
return searchParams.has(CONTENT_FLAG);
22+
function isContentFlagImport(viteId: string, contentEntryExts: string[]) {
23+
const { searchParams, pathname } = new URL(viteId, 'file://');
24+
return searchParams.has(CONTENT_FLAG) && contentEntryExts.some((ext) => pathname.endsWith(ext));
2525
}
2626

2727
export function astroContentImportPlugin({
@@ -32,16 +32,13 @@ export function astroContentImportPlugin({
3232
settings: AstroSettings;
3333
}): Plugin {
3434
const contentPaths = getContentPaths(settings.config, fs);
35-
const contentFileExts = [
36-
...defaultContentFileExts,
37-
...contentEntryTypes.map((t) => t.extensions).flat(),
38-
];
35+
const contentEntryExts = getContentEntryExts(settings);
3936

4037
return {
4138
name: 'astro:content-imports',
4239
async load(id) {
4340
const { fileId } = getFileInfo(id, settings.config);
44-
if (isContentFlagImport(id)) {
41+
if (isContentFlagImport(id, contentEntryExts)) {
4542
const observable = globalContentConfigObserver.get();
4643

4744
// Content config should be loaded before this plugin is used
@@ -74,7 +71,7 @@ export function astroContentImportPlugin({
7471
});
7572
}
7673
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
77-
const contentEntryType = contentEntryTypes.find((entryType) =>
74+
const contentEntryType = settings.contentEntryTypes.find((entryType) =>
7875
entryType.extensions.some((ext) => fileId.endsWith(ext))
7976
);
8077
let body: string,
@@ -129,11 +126,11 @@ export const _internal = {
129126
viteServer.watcher.on('all', async (event, entry) => {
130127
if (
131128
['add', 'unlink', 'change'].includes(event) &&
132-
getEntryType(entry, contentPaths, contentFileExts) === 'config'
129+
getEntryType(entry, contentPaths, contentEntryExts) === 'config'
133130
) {
134131
// Content modules depend on config, so we need to invalidate them.
135132
for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) {
136-
if (isContentFlagImport(modUrl)) {
133+
if (isContentFlagImport(modUrl, contentEntryExts)) {
137134
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
138135
if (mod) {
139136
viteServer.moduleGraph.invalidateModule(mod);
@@ -144,7 +141,7 @@ export const _internal = {
144141
});
145142
},
146143
async transform(code, id) {
147-
if (isContentFlagImport(id)) {
144+
if (isContentFlagImport(id, contentEntryExts)) {
148145
// Escape before Rollup internal transform.
149146
// Base on MUCH trial-and-error, inspired by MDX integration 2-step transform.
150147
return { code: escapeViteEnvReferences(code) };

packages/astro/src/content/vite-plugin-content-virtual-mod.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import type { Plugin } from 'vite';
44
import { normalizePath } from 'vite';
55
import type { AstroSettings } from '../@types/astro.js';
66
import { appendForwardSlash, prependForwardSlash } from '../core/path.js';
7-
import { defaultContentFileExts, VIRTUAL_MODULE_ID } from './consts.js';
8-
import { getContentPaths } from './utils.js';
9-
import { contentEntryTypes } from './~dream.js';
7+
import { VIRTUAL_MODULE_ID } from './consts.js';
8+
import { getContentEntryExts, getContentPaths } from './utils.js';
109

1110
interface AstroContentVirtualModPluginParams {
1211
settings: AstroSettings;
@@ -23,12 +22,9 @@ export function astroContentVirtualModPlugin({
2322
)
2423
)
2524
);
26-
const contentFileExts = [
27-
...defaultContentFileExts,
28-
...contentEntryTypes.map((t) => t.extensions).flat(),
29-
];
25+
const contentEntryExts = getContentEntryExts(settings);
3026

31-
const entryGlob = `${relContentDir}**/*{${contentFileExts.join(',')}}`;
27+
const entryGlob = `${relContentDir}**/*{${contentEntryExts.join(',')}}`;
3228
const virtualModContents = fsMod
3329
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
3430
.replace('@@CONTENT_DIR@@', relContentDir)

packages/astro/src/core/config/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
1515
adapter: undefined,
1616
injectedRoutes: [],
1717
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
18+
/** TODO: default Markdown entry type */
19+
contentEntryTypes: [],
1820
renderers: [jsxRenderer],
1921
scripts: [],
2022
watchFiles: [],

packages/astro/src/core/create-vite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export async function createVite(
112112
astroInjectEnvTsPlugin({ settings, logging, fs }),
113113
astroContentVirtualModPlugin({ settings }),
114114
astroContentImportPlugin({ fs, settings }),
115-
astroContentAssetPropagationPlugin({ mode }),
115+
astroContentAssetPropagationPlugin({ mode, settings }),
116116
],
117117
publicDir: fileURLToPath(settings.config.publicDir),
118118
root: fileURLToPath(settings.config.root),

packages/astro/src/integrations/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
AstroRenderer,
99
AstroSettings,
1010
BuildConfig,
11+
ContentEntryType,
1112
HookParameters,
1213
RouteData,
1314
} from '../@types/astro.js';
@@ -100,11 +101,22 @@ export async function runHookConfigSetup({
100101
const exts = (input.flat(Infinity) as string[]).map((ext) => `.${ext.replace(/^\./, '')}`);
101102
updatedSettings.pageExtensions.push(...exts);
102103
}
104+
105+
// Semi-private `addContentEntryType` hook
106+
function addContentEntryType(contentEntryType: ContentEntryType) {
107+
updatedSettings.contentEntryTypes.push(contentEntryType);
108+
}
109+
103110
Object.defineProperty(hooks, 'addPageExtension', {
104111
value: addPageExtension,
105112
writable: false,
106113
enumerable: false,
107114
});
115+
Object.defineProperty(hooks, 'addContentEntryType', {
116+
value: addContentEntryType,
117+
writable: false,
118+
enumerable: false,
119+
});
108120
await withTakingALongTimeMsg({
109121
name: integration.name,
110122
hookResult: integration.hooks['astro:config:setup'](hooks),

0 commit comments

Comments
 (0)