mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
feat: support new location for content config (#12475)
* feat: support new location for content config * Test fixes * Handle missing dir * Handle missing content dir * chore: changes from review * Revert legacy fixtures * Clarify changeset
This commit is contained in:
parent
18a04c008a
commit
3f02d5f12b
39 changed files with 79 additions and 37 deletions
7
.changeset/thirty-clocks-jump.md
Normal file
7
.changeset/thirty-clocks-jump.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Changes the default content config location from `src/content/config.*` to `src/content.config.*`.
|
||||||
|
|
||||||
|
The previous location is still supported, and is required if the `legacy.collections` flag is enabled.
|
|
@ -26,12 +26,22 @@ type GlobResult = Record<string, LazyImport>;
|
||||||
type CollectionToEntryMap = Record<string, GlobResult>;
|
type CollectionToEntryMap = Record<string, GlobResult>;
|
||||||
type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;
|
type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;
|
||||||
|
|
||||||
|
export function getImporterFilename() {
|
||||||
|
// The 4th line in the stack trace should be the importer filename
|
||||||
|
const stackLine = new Error().stack?.split('\n')?.[3];
|
||||||
|
if (!stackLine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Extract the relative path from the stack line
|
||||||
|
const match = /\/(src\/.*?):\d+:\d+/.exec(stackLine);
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function defineCollection(config: any) {
|
export function defineCollection(config: any) {
|
||||||
if ('loader' in config) {
|
if ('loader' in config) {
|
||||||
if (config.type && config.type !== CONTENT_LAYER_TYPE) {
|
if (config.type && config.type !== CONTENT_LAYER_TYPE) {
|
||||||
throw new AstroUserError(
|
throw new AstroUserError(
|
||||||
'Collections that use the Content Layer API must have a `loader` defined and no `type` set.',
|
`Collections that use the Content Layer API must have a \`loader\` defined and no \`type\` set. Check your collection definitions in ${getImporterFilename() ?? 'your content config file'}.`,
|
||||||
"Check your collection definitions in `src/content/config.*`.'",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
config.type = CONTENT_LAYER_TYPE;
|
config.type = CONTENT_LAYER_TYPE;
|
||||||
|
|
|
@ -24,8 +24,16 @@ export async function attachContentServerListeners({
|
||||||
settings,
|
settings,
|
||||||
}: ContentServerListenerParams) {
|
}: ContentServerListenerParams) {
|
||||||
const contentPaths = getContentPaths(settings.config, fs);
|
const contentPaths = getContentPaths(settings.config, fs);
|
||||||
|
if (!settings.config.legacy?.collections) {
|
||||||
if (fs.existsSync(contentPaths.contentDir)) {
|
const contentGenerator = await createContentTypesGenerator({
|
||||||
|
fs,
|
||||||
|
settings,
|
||||||
|
logger,
|
||||||
|
viteServer,
|
||||||
|
contentConfigObserver: globalContentConfigObserver,
|
||||||
|
});
|
||||||
|
await contentGenerator.init();
|
||||||
|
} else if (fs.existsSync(contentPaths.contentDir)) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'content',
|
'content',
|
||||||
`Watching ${cyan(
|
`Watching ${cyan(
|
||||||
|
|
|
@ -86,13 +86,12 @@ export async function createContentTypesGenerator({
|
||||||
async function init(): Promise<
|
async function init(): Promise<
|
||||||
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
|
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
|
||||||
> {
|
> {
|
||||||
if (!fs.existsSync(contentPaths.contentDir)) {
|
|
||||||
return { typesGenerated: false, reason: 'no-content-dir' };
|
|
||||||
}
|
|
||||||
|
|
||||||
events.push({ name: 'add', entry: contentPaths.config.url });
|
events.push({ name: 'add', entry: contentPaths.config.url });
|
||||||
|
|
||||||
if (settings.config.legacy.collections) {
|
if (settings.config.legacy.collections) {
|
||||||
|
if (!fs.existsSync(contentPaths.contentDir)) {
|
||||||
|
return { typesGenerated: false, reason: 'no-content-dir' };
|
||||||
|
}
|
||||||
const globResult = await glob('**', {
|
const globResult = await glob('**', {
|
||||||
cwd: fileURLToPath(contentPaths.contentDir),
|
cwd: fileURLToPath(contentPaths.contentDir),
|
||||||
fs: {
|
fs: {
|
||||||
|
|
|
@ -597,7 +597,7 @@ export async function autogenerateCollections({
|
||||||
}) as any,
|
}) as any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!usesContentLayer) {
|
if (!usesContentLayer && fs.existsSync(contentDir)) {
|
||||||
// If the user hasn't defined any collections using the content layer, we'll try and help out by checking for
|
// If the user hasn't defined any collections using the content layer, we'll try and help out by checking for
|
||||||
// any orphaned folders in the content directory and creating collections for them.
|
// any orphaned folders in the content directory and creating collections for them.
|
||||||
const orphanedCollections = [];
|
const orphanedCollections = [];
|
||||||
|
@ -623,7 +623,7 @@ export async function autogenerateCollections({
|
||||||
console.warn(
|
console.warn(
|
||||||
`
|
`
|
||||||
Auto-generating collections for folders in "src/content/" that are not defined as collections.
|
Auto-generating collections for folders in "src/content/" that are not defined as collections.
|
||||||
This is deprecated, so you should define these collections yourself in "src/content/config.ts".
|
This is deprecated, so you should define these collections yourself in "src/content.config.ts".
|
||||||
The following collections have been auto-generated: ${orphanedCollections
|
The following collections have been auto-generated: ${orphanedCollections
|
||||||
.map((name) => green(name))
|
.map((name) => green(name))
|
||||||
.join(', ')}\n`,
|
.join(', ')}\n`,
|
||||||
|
@ -715,10 +715,10 @@ export type ContentPaths = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getContentPaths(
|
export function getContentPaths(
|
||||||
{ srcDir }: Pick<AstroConfig, 'root' | 'srcDir'>,
|
{ srcDir, legacy }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy'>,
|
||||||
fs: typeof fsMod = fsMod,
|
fs: typeof fsMod = fsMod,
|
||||||
): ContentPaths {
|
): ContentPaths {
|
||||||
const configStats = search(fs, srcDir);
|
const configStats = search(fs, srcDir, legacy?.collections);
|
||||||
const pkgBase = new URL('../../', import.meta.url);
|
const pkgBase = new URL('../../', import.meta.url);
|
||||||
return {
|
return {
|
||||||
contentDir: new URL('./content/', srcDir),
|
contentDir: new URL('./content/', srcDir),
|
||||||
|
@ -728,10 +728,16 @@ export function getContentPaths(
|
||||||
config: configStats,
|
config: configStats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function search(fs: typeof fsMod, srcDir: URL) {
|
function search(fs: typeof fsMod, srcDir: URL, legacy?: boolean) {
|
||||||
const paths = ['config.mjs', 'config.js', 'config.mts', 'config.ts'].map(
|
const paths = [
|
||||||
(p) => new URL(`./content/${p}`, srcDir),
|
...(legacy
|
||||||
);
|
? []
|
||||||
|
: ['content.config.mjs', 'content.config.js', 'content.config.mts', 'content.config.ts']),
|
||||||
|
'content/config.mjs',
|
||||||
|
'content/config.js',
|
||||||
|
'content/config.mts',
|
||||||
|
'content/config.ts',
|
||||||
|
].map((p) => new URL(`./${p}`, srcDir));
|
||||||
for (const file of paths) {
|
for (const file of paths) {
|
||||||
if (fs.existsSync(file)) {
|
if (fs.existsSync(file)) {
|
||||||
return { exists: true, url: file };
|
return { exists: true, url: file };
|
||||||
|
|
|
@ -1446,7 +1446,8 @@ export const GenerateContentTypesError = {
|
||||||
title: 'Failed to generate content types.',
|
title: 'Failed to generate content types.',
|
||||||
message: (errorMessage: string) =>
|
message: (errorMessage: string) =>
|
||||||
`\`astro sync\` command failed to generate content collection types: ${errorMessage}`,
|
`\`astro sync\` command failed to generate content collection types: ${errorMessage}`,
|
||||||
hint: 'This error is often caused by a syntax error inside your content, or your content configuration file. Check your `src/content/config.*` file for typos.',
|
hint: (fileName?: string) =>
|
||||||
|
`This error is often caused by a syntax error inside your content, or your content configuration file. Check your ${fileName ?? 'content config'} file for typos.`,
|
||||||
} satisfies ErrorData;
|
} satisfies ErrorData;
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
|
@ -1458,7 +1459,7 @@ export const GenerateContentTypesError = {
|
||||||
* @docs
|
* @docs
|
||||||
* @description
|
* @description
|
||||||
* Astro encountered an unknown error loading your content collections.
|
* Astro encountered an unknown error loading your content collections.
|
||||||
* This can be caused by certain errors inside your `src/content/config.ts` file or some internal errors.
|
* This can be caused by certain errors inside your `src/content.config.ts` file or some internal errors.
|
||||||
*
|
*
|
||||||
* If you can reliably cause this error to happen, we'd appreciate if you could [open an issue](https://astro.build/issues/)
|
* If you can reliably cause this error to happen, we'd appreciate if you could [open an issue](https://astro.build/issues/)
|
||||||
*/
|
*/
|
||||||
|
@ -1501,7 +1502,7 @@ export const GetEntryDeprecationError = {
|
||||||
* @description
|
* @description
|
||||||
* A Markdown or MDX entry does not match its collection schema.
|
* A Markdown or MDX entry does not match its collection schema.
|
||||||
* Make sure that all required fields are present, and that all fields are of the correct type.
|
* Make sure that all required fields are present, and that all fields are of the correct type.
|
||||||
* You can check against the collection schema in your `src/content/config.*` file.
|
* You can check against the collection schema in your `src/content.config.*` file.
|
||||||
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
|
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
|
||||||
*/
|
*/
|
||||||
export const InvalidContentEntryFrontmatterError = {
|
export const InvalidContentEntryFrontmatterError = {
|
||||||
|
@ -1528,7 +1529,7 @@ export const InvalidContentEntryFrontmatterError = {
|
||||||
* @description
|
* @description
|
||||||
* A content entry does not match its collection schema.
|
* A content entry does not match its collection schema.
|
||||||
* Make sure that all required fields are present, and that all fields are of the correct type.
|
* Make sure that all required fields are present, and that all fields are of the correct type.
|
||||||
* You can check against the collection schema in your `src/content/config.*` file.
|
* You can check against the collection schema in your `src/content.config.*` file.
|
||||||
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
|
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
|
||||||
*/
|
*/
|
||||||
export const InvalidContentEntryDataError = {
|
export const InvalidContentEntryDataError = {
|
||||||
|
@ -1553,7 +1554,7 @@ export const InvalidContentEntryDataError = {
|
||||||
* @description
|
* @description
|
||||||
* A content entry does not match its collection schema.
|
* A content entry does not match its collection schema.
|
||||||
* Make sure that all required fields are present, and that all fields are of the correct type.
|
* Make sure that all required fields are present, and that all fields are of the correct type.
|
||||||
* You can check against the collection schema in your `src/content/config.*` file.
|
* You can check against the collection schema in your `src/content.config.*` file.
|
||||||
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
|
* See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
|
||||||
*/
|
*/
|
||||||
export const ContentEntryDataError = {
|
export const ContentEntryDataError = {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { resolveConfig } from '../config/config.js';
|
||||||
import { createNodeLogger } from '../config/logging.js';
|
import { createNodeLogger } from '../config/logging.js';
|
||||||
import { createSettings } from '../config/settings.js';
|
import { createSettings } from '../config/settings.js';
|
||||||
import { createVite } from '../create-vite.js';
|
import { createVite } from '../create-vite.js';
|
||||||
import { collectErrorMetadata } from '../errors/dev/utils.js';
|
|
||||||
import {
|
import {
|
||||||
AstroError,
|
AstroError,
|
||||||
AstroErrorData,
|
AstroErrorData,
|
||||||
|
@ -31,7 +30,6 @@ import {
|
||||||
isAstroError,
|
isAstroError,
|
||||||
} from '../errors/index.js';
|
} from '../errors/index.js';
|
||||||
import type { Logger } from '../logger/core.js';
|
import type { Logger } from '../logger/core.js';
|
||||||
import { formatErrorMessage } from '../messages.js';
|
|
||||||
import { createRouteManifest } from '../routing/index.js';
|
import { createRouteManifest } from '../routing/index.js';
|
||||||
import { ensureProcessNodeEnv } from '../util.js';
|
import { ensureProcessNodeEnv } from '../util.js';
|
||||||
|
|
||||||
|
@ -255,7 +253,20 @@ async function syncContentCollections(
|
||||||
if (isAstroError(e)) {
|
if (isAstroError(e)) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const hint = AstroUserError.is(e) ? e.hint : AstroErrorData.GenerateContentTypesError.hint;
|
let configFile
|
||||||
|
try {
|
||||||
|
const contentPaths = getContentPaths(settings.config, fs);
|
||||||
|
if(contentPaths.config.exists) {
|
||||||
|
const matches = /\/(src\/.+)/.exec(contentPaths.config.url.href);
|
||||||
|
if (matches) {
|
||||||
|
configFile = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const hint = AstroUserError.is(e) ? e.hint : AstroErrorData.GenerateContentTypesError.hint(configFile);
|
||||||
throw new AstroError(
|
throw new AstroError(
|
||||||
{
|
{
|
||||||
...AstroErrorData.GenerateContentTypesError,
|
...AstroErrorData.GenerateContentTypesError,
|
||||||
|
|
|
@ -1707,7 +1707,7 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
|
||||||
* When you are ready to remove this flag and migrate to the new Content Layer API for your legacy collections, you must define a collection for any directories in `src/content/` that you want to continue to use as a collection. It is sufficient to declare an empty collection, and Astro will implicitly generate an appropriate definition for your legacy collections:
|
* When you are ready to remove this flag and migrate to the new Content Layer API for your legacy collections, you must define a collection for any directories in `src/content/` that you want to continue to use as a collection. It is sufficient to declare an empty collection, and Astro will implicitly generate an appropriate definition for your legacy collections:
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* // src/content/config.ts
|
* // src/content.config.ts
|
||||||
* import { defineCollection, z } from 'astro:content';
|
* import { defineCollection, z } from 'astro:content';
|
||||||
*
|
*
|
||||||
* const blog = defineCollection({ })
|
* const blog = defineCollection({ })
|
||||||
|
|
|
@ -209,7 +209,7 @@ describe('astro sync', () => {
|
||||||
assert.fail();
|
assert.fail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('Does not throw if a virtual module is imported in content/config.ts', async () => {
|
it('Does not throw if a virtual module is imported in content.config.ts', async () => {
|
||||||
try {
|
try {
|
||||||
await fixture.load('./fixtures/astro-env-content-collections/');
|
await fixture.load('./fixtures/astro-env-content-collections/');
|
||||||
fixture.clean();
|
fixture.clean();
|
||||||
|
|
|
@ -285,7 +285,7 @@ describe('Content Layer', () => {
|
||||||
it('clears the store on new build if the config has changed', async () => {
|
it('clears the store on new build if the config has changed', async () => {
|
||||||
let newJson = devalue.parse(await fixture.readFile('/collections.json'));
|
let newJson = devalue.parse(await fixture.readFile('/collections.json'));
|
||||||
assert.equal(newJson.increment.data.lastValue, 1);
|
assert.equal(newJson.increment.data.lastValue, 1);
|
||||||
await fixture.editFile('src/content/config.ts', (prev) => {
|
await fixture.editFile('src/content.config.ts', (prev) => {
|
||||||
return `${prev}\nexport const foo = 'bar';`;
|
return `${prev}\nexport const foo = 'bar';`;
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { glob } from 'astro/loaders';
|
||||||
const reptiles = defineCollection({
|
const reptiles = defineCollection({
|
||||||
loader: glob({
|
loader: glob({
|
||||||
pattern: '*.mdx',
|
pattern: '*.mdx',
|
||||||
base: new URL('../../content-outside-src-mdx', import.meta.url),
|
base: new URL('../content-outside-src-mdx', import.meta.url),
|
||||||
}),
|
}),
|
||||||
schema: () =>
|
schema: () =>
|
||||||
z.object({
|
z.object({
|
|
@ -1,6 +1,6 @@
|
||||||
import { defineCollection, z, reference } from 'astro:content';
|
import { defineCollection, z, reference } from 'astro:content';
|
||||||
import { file, glob } from 'astro/loaders';
|
import { file, glob } from 'astro/loaders';
|
||||||
import { loader } from '../loaders/post-loader.js';
|
import { loader } from './loaders/post-loader.js';
|
||||||
import { parse as parseToml } from 'toml';
|
import { parse as parseToml } from 'toml';
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
|
@ -141,7 +141,7 @@ const birds = defineCollection({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Absolute paths should also work
|
// Absolute paths should also work
|
||||||
const absoluteRoot = new URL('space', import.meta.url);
|
const absoluteRoot = new URL('content/space', import.meta.url);
|
||||||
|
|
||||||
const spacecraft = defineCollection({
|
const spacecraft = defineCollection({
|
||||||
loader: glob({ pattern: '*.md', base: absoluteRoot }),
|
loader: glob({ pattern: '*.md', base: absoluteRoot }),
|
|
@ -10,7 +10,7 @@ describe('frontmatter', () => {
|
||||||
title: One
|
title: One
|
||||||
---
|
---
|
||||||
`,
|
`,
|
||||||
'/src/content/config.ts': `\
|
'/src/content.config.ts': `\
|
||||||
import { defineCollection, z } from 'astro:content';
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
const posts = defineCollection({
|
const posts = defineCollection({
|
||||||
|
|
|
@ -8,7 +8,7 @@ const fixtures = [
|
||||||
title: 'Without any underscore above the content directory tree',
|
title: 'Without any underscore above the content directory tree',
|
||||||
contentPaths: {
|
contentPaths: {
|
||||||
config: {
|
config: {
|
||||||
url: new URL('src/content/config.ts', import.meta.url),
|
url: new URL('src/content.config.ts', import.meta.url),
|
||||||
exists: true,
|
exists: true,
|
||||||
},
|
},
|
||||||
contentDir: new URL('src/content/', import.meta.url),
|
contentDir: new URL('src/content/', import.meta.url),
|
||||||
|
|
|
@ -57,7 +57,7 @@ name: Ben
|
||||||
# Ben
|
# Ben
|
||||||
`,
|
`,
|
||||||
'/src/content/authors/tony.json': `{ "name": "Tony" }`,
|
'/src/content/authors/tony.json': `{ "name": "Tony" }`,
|
||||||
'/src/content/config.ts': `\
|
'/src/content.config.ts': `\
|
||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
const authors = defineCollection({
|
const authors = defineCollection({
|
||||||
|
@ -85,7 +85,7 @@ title: Post
|
||||||
# Post
|
# Post
|
||||||
`,
|
`,
|
||||||
'/src/content/blog/post.yaml': `title: YAML Post`,
|
'/src/content/blog/post.yaml': `title: YAML Post`,
|
||||||
'/src/content/config.ts': `\
|
'/src/content.config.ts': `\
|
||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
|
@ -128,7 +128,7 @@ export const collections = { banners };
|
||||||
...baseFileTree,
|
...baseFileTree,
|
||||||
// Add placeholder to ensure directory exists
|
// Add placeholder to ensure directory exists
|
||||||
'/src/content/i18n/_placeholder.txt': 'Need content here',
|
'/src/content/i18n/_placeholder.txt': 'Need content here',
|
||||||
'/src/content/config.ts': `\
|
'/src/content.config.ts': `\
|
||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
const i18n = defineCollection({
|
const i18n = defineCollection({
|
||||||
|
|
|
@ -166,7 +166,7 @@ describe('Content Collections - render()', () => {
|
||||||
it('can be used in a slot', async () => {
|
it('can be used in a slot', async () => {
|
||||||
const fixture = await createFixture({
|
const fixture = await createFixture({
|
||||||
...baseFileTree,
|
...baseFileTree,
|
||||||
'/src/content/config.ts': `
|
'/src/content.config.ts': `
|
||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
|
@ -233,7 +233,7 @@ describe('Content Collections - render()', () => {
|
||||||
it('can be called from any js/ts file', async () => {
|
it('can be called from any js/ts file', async () => {
|
||||||
const fixture = await createFixture({
|
const fixture = await createFixture({
|
||||||
...baseFileTree,
|
...baseFileTree,
|
||||||
'/src/content/config.ts': `
|
'/src/content.config.ts': `
|
||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
|
|
Loading…
Reference in a new issue