0
Fork 0
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:
Matt Kane 2024-11-21 10:45:02 +00:00 committed by GitHub
parent 18a04c008a
commit 3f02d5f12b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 79 additions and 37 deletions

View 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.

View file

@ -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;

View file

@ -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(

View file

@ -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: {

View file

@ -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 };

View 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 = {

View file

@ -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,

View file

@ -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({ })

View file

@ -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();

View file

@ -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();

View file

@ -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({

View file

@ -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 }),

View file

@ -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({

View file

@ -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),

View file

@ -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({

View file

@ -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({