Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement legacy collections using glob #11976

Merged
merged 58 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
6cfa42d
feat: support pattern arrays with glob
ascorbic Sep 9, 2024
c2f48aa
wip
ascorbic Sep 9, 2024
dd20f95
Merge branch 'next' into auto-glob
ascorbic Sep 12, 2024
0e9b54c
feat: emulate legacy content collections
ascorbic Sep 12, 2024
1ff97b4
Fixes
ascorbic Sep 12, 2024
305a756
Merge branch 'next' into auto-glob
ascorbic Sep 12, 2024
9585b5a
Lint
ascorbic Sep 12, 2024
181c950
Correctly handle legacy data
ascorbic Sep 12, 2024
f47c6ba
Fix tests
ascorbic Sep 12, 2024
b4f14ea
Merge branch 'next' into auto-glob
ascorbic Sep 13, 2024
3967012
Switch flag handling
ascorbic Sep 13, 2024
072dd7e
Fix warnings
ascorbic Sep 13, 2024
75c7181
Add layout warning
ascorbic Sep 13, 2024
9d371a1
Update fixtures
ascorbic Sep 13, 2024
1a27f35
More tests!
ascorbic Sep 13, 2024
e14090a
Handle empty md files
ascorbic Sep 13, 2024
77b7815
Merge branch 'next' into auto-glob
ascorbic Sep 13, 2024
bb15ef5
Lockfile
ascorbic Sep 13, 2024
e42c4b0
Dedupe name
ascorbic Sep 13, 2024
8b7dc59
Handle data ID unslug
ascorbic Sep 13, 2024
ab7918a
Fix e2e
ascorbic Sep 13, 2024
57127ff
Merge branch 'next' into auto-glob
ascorbic Sep 13, 2024
5d1684e
Clean build
ascorbic Sep 13, 2024
032ce4f
Clean builds in tests
ascorbic Sep 13, 2024
f48be22
Test fixes
ascorbic Sep 14, 2024
f337838
Fix test
ascorbic Sep 14, 2024
ec2fcb0
Fix typegen
ascorbic Sep 14, 2024
0aebf32
Fix tests
ascorbic Sep 14, 2024
b0cb038
Merge branch 'next' into auto-glob
ascorbic Sep 16, 2024
8f2e3fe
Fixture updates
ascorbic Sep 16, 2024
a7cccef
Test updates
ascorbic Sep 16, 2024
a030814
Update changeset
ascorbic Sep 16, 2024
67a4253
Test
ascorbic Sep 16, 2024
e1b9491
Remove wait in test
ascorbic Sep 16, 2024
5200a5d
Handle race condition
ascorbic Sep 16, 2024
7add932
Merge branch 'next' into auto-glob
ascorbic Sep 17, 2024
869dc3a
Lock
ascorbic Sep 17, 2024
505b426
Merge branch 'next' into auto-glob
ascorbic Sep 18, 2024
850c3e9
Merge branch 'next' into auto-glob
ascorbic Sep 20, 2024
81be579
Merge branch 'next' into auto-glob
ascorbic Oct 1, 2024
5339675
chore: changes from review
ascorbic Oct 1, 2024
8280450
Handle folders without config
ascorbic Oct 1, 2024
3da71b4
lint
ascorbic Oct 1, 2024
99c823a
Merge branch 'next' into auto-glob
ascorbic Oct 1, 2024
5c6e428
Fix test
ascorbic Oct 1, 2024
ce2c486
Update wording for auto-collections
ascorbic Oct 1, 2024
9d0d306
Delete legacyId
ascorbic Oct 1, 2024
c8e19ed
Sort another fixture
ascorbic Oct 1, 2024
6f4bab0
Rename flag to `legacy.collections`
ascorbic Oct 1, 2024
a8bf7aa
Merge branch 'next' into auto-glob
ascorbic Oct 1, 2024
0e310e9
Apply suggestions from code review
ascorbic Oct 2, 2024
5fb9247
Changes from review
ascorbic Oct 2, 2024
266da4d
Apply suggestions from code review
ascorbic Oct 2, 2024
e0b211e
Merge branch 'next' into auto-glob
ascorbic Oct 4, 2024
7ebcf58
lockfile
ascorbic Oct 4, 2024
7318f1c
Merge branch 'next' into auto-glob
ascorbic Oct 4, 2024
bd9a5f5
lock
ascorbic Oct 4, 2024
4eae23f
Merge branch 'next' into auto-glob
ascorbic Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .changeset/quick-onions-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
'astro': patch
---

Refactors legacy `content` and `data` collections to use the Content Layer API `glob()` loader for better performance and to support backwards compatibility. Also introduces the `legacy.collections` flag for projects that are unable to update to the new behavior immediately.

:warning: **BREAKING CHANGE FOR LEGACY CONTENT COLLECTIONS** :warning:

By default, collections that use the old types (`content` or `data`) and do not define a `loader` are now implemented under the hood using the Content Layer API's built-in `glob()` loader, with extra backward-compatibility handling.

In order to achieve backwards compatibility with existing `content` collections, the following have been implemented:

- a `glob` loader collection is defined, with patterns that match the previous handling (matches `src/content/<collection name>/**/*.md` and other content extensions depending on installed integrations, with underscore-prefixed files and folders ignored)
- When used in the runtime, the entries have an ID based on the filename in the same format as legacy collections
- A `slug` field is added with the same format as before
- A `render()` method is added to the entry, so they can be called using `entry.render()`
- `getEntryBySlug` is supported

In order to achieve backwards compatibility with existing `data` collections, the following have been implemented:

- a `glob` loader collection is defined, with patterns that match the previous handling (matches `src/content/<collection name>/**/*{.json,.yaml}` and other data extensions, with underscore-prefixed files and folders ignored)
- Entries have an ID that is not slugified
- `getDataEntryById` is supported
ascorbic marked this conversation as resolved.
Show resolved Hide resolved

While this backwards compatibility implementation is able to emulate most of the features of legacy collections, **there are some differences and limitations that may cause breaking changes to existing collections**:

- In previous versions of Astro, collections would be generated for all folders in `src/content/`, even if they were not defined in `src/content/config.ts`. This behavior is now deprecated, and collections should always be defined in `src/content/config.ts`. For existing collections, these can just be empty declarations (e.g. `const blog = defineCollection({})`) and Astro will implicitly define your legacy collection for you in a way that is compatible with the new loading behavior.
- The special `layout` field is not supported in Markdown collection entries. This property is intended only for standalone page files located in `src/pages/` and not likely to be in your collection entries. However, if you were using this property, you must now create dynamic routes that include your page styling.
- Sort order of generated collections is non-deterministic and platform-dependent. This means that if you are calling `getCollection()`, the order in which entries are returned may be different than before. If you need a specific order, you should sort the collection entries yourself.
- `image().refine()` is not supported. If you need to validate the properties of an image you will need to do this at runtime in your page or component.
- the `key` argument of `getEntry(collection, key)` is typed as `string`, rather than having types for every entry.

A new legacy configuration flag `legacy.collections` is added for users that want to keep their current legacy (content and data) collections behavior (available in Astro v2 - v4), or who are not yet ready to update their projects:

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
legacy: {
collections: true
}
});
```

When set, no changes to your existing collections are necessary, and the restrictions on storing both new and old collections continue to exist: legacy collections (only) must continue to remain in `src/content/`, while new collections using a loader from the Content Layer API are forbidden in that folder.

5 changes: 5 additions & 0 deletions examples/with-markdoc/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineCollection } from 'astro:content';

export const collections = {
docs: defineCollection({})
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineCollection } from "astro:content";


const posts = defineCollection({});

export const collections = { posts };
2 changes: 2 additions & 0 deletions packages/astro/src/content/data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
*/
deferredRender?: boolean;
assetImports?: Array<string>;
/** @deprecated */
legacyId?: string;
}

/**
Expand Down
46 changes: 39 additions & 7 deletions packages/astro/src/content/loaders/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function generateIdDefault({ entry, base, data }: GenerateIdOptions): string {
if (data.slug) {
return data.slug as string;
}
const entryURL = new URL(entry, base);
const entryURL = new URL(encodeURI(entry), base);
const { slug } = getContentEntryIdAndSlug({
entry: entryURL,
contentDir: base,
Expand All @@ -55,6 +55,15 @@ function checkPrefix(pattern: string | Array<string>, prefix: string) {
* Loads multiple entries, using a glob pattern to match files.
* @param pattern A glob pattern to match files, relative to the content directory.
*/
export function glob(globOptions: GlobOptions): Loader;
/** @private */
export function glob(
globOptions: GlobOptions & {
/** @deprecated */
_legacy?: true;
},
): Loader;

export function glob(globOptions: GlobOptions): Loader {
if (checkPrefix(globOptions.pattern, '../')) {
throw new Error(
Expand All @@ -80,19 +89,21 @@ export function glob(globOptions: GlobOptions): Loader {
>();

const untouchedEntries = new Set(store.keys());

const isLegacy = (globOptions as any)._legacy;
// If global legacy collection handling flag is *not* enabled then this loader is used to emulate them instead
const emulateLegacyCollections = !config.legacy.collections;
async function syncData(entry: string, base: URL, entryType?: ContentEntryType) {
if (!entryType) {
logger.warn(`No entry type found for ${entry}`);
return;
}
const fileUrl = new URL(entry, base);
const fileUrl = new URL(encodeURI(entry), base);
const contents = await fs.readFile(fileUrl, 'utf-8').catch((err) => {
logger.error(`Error reading ${entry}: ${err.message}`);
return;
});

if (!contents) {
if (!contents && contents !== '') {
logger.warn(`No contents found for ${entry}`);
return;
}
Expand All @@ -103,6 +114,17 @@ export function glob(globOptions: GlobOptions): Loader {
});

const id = generateId({ entry, base, data });
let legacyId: string | undefined;

if (isLegacy) {
const entryURL = new URL(encodeURI(entry), base);
const legacyOptions = getContentEntryIdAndSlug({
entry: entryURL,
contentDir: base,
collection: '',
});
legacyId = legacyOptions.id;
}
untouchedEntries.delete(id);

const existingEntry = store.get(id);
Expand Down Expand Up @@ -132,6 +154,12 @@ export function glob(globOptions: GlobOptions): Loader {
filePath,
});
if (entryType.getRenderFunction) {
if (isLegacy && data.layout) {
logger.error(
`The Markdown "layout" field is not supported in content collections in Astro 5. Ignoring layout for ${JSON.stringify(entry)}. Enable "legacy.collections" if you need to use the layout field.`,
);
}

let render = renderFunctionByContentType.get(entryType);
if (!render) {
render = await entryType.getRenderFunction(config);
Expand Down Expand Up @@ -160,6 +188,7 @@ export function glob(globOptions: GlobOptions): Loader {
digest,
rendered,
assetImports: rendered?.metadata?.imagePaths,
legacyId,
});

// todo: add an explicit way to opt in to deferred rendering
Expand All @@ -171,9 +200,10 @@ export function glob(globOptions: GlobOptions): Loader {
filePath: relativePath,
digest,
deferredRender: true,
legacyId,
});
} else {
store.set({ id, data: parsedData, body, filePath: relativePath, digest });
store.set({ id, data: parsedData, body, filePath: relativePath, digest, legacyId });
}

fileToIdMap.set(filePath, id);
Expand Down Expand Up @@ -222,7 +252,7 @@ export function glob(globOptions: GlobOptions): Loader {
if (isConfigFile(entry)) {
return;
}
if (isInContentDir(entry)) {
if (!emulateLegacyCollections && isInContentDir(entry)) {
skippedFiles.push(entry);
return;
}
Expand All @@ -240,7 +270,9 @@ export function glob(globOptions: GlobOptions): Loader {
? globOptions.pattern.join(', ')
: globOptions.pattern;

logger.warn(`The glob() loader cannot be used for files in ${bold('src/content')}.`);
logger.warn(
`The glob() loader cannot be used for files in ${bold('src/content')} when legacy mode is enabled.`,
);
if (skipCount > 10) {
logger.warn(
`Skipped ${green(skippedFiles.length)} files that matched ${green(patternList)}.`,
Expand Down
42 changes: 16 additions & 26 deletions packages/astro/src/content/mutable-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Traverse } from 'neotraverse/modern';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { IMAGE_IMPORT_PREFIX } from './consts.js';
import { type DataEntry, ImmutableDataStore, type RenderedContent } from './data-store.js';
import { type DataEntry, ImmutableDataStore } from './data-store.js';
import { contentModuleToId } from './utils.js';

const SAVE_DEBOUNCE_MS = 500;
Expand Down Expand Up @@ -197,7 +197,17 @@ export default new Map([\n${lines.join(',\n')}]);
entries: () => this.entries(collectionName),
values: () => this.values(collectionName),
keys: () => this.keys(collectionName),
set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
set: ({
id: key,
data,
body,
filePath,
deferredRender,
digest,
rendered,
assetImports,
legacyId,
}) => {
if (!key) {
throw new Error(`ID must be a non-empty string`);
}
Expand Down Expand Up @@ -244,6 +254,9 @@ export default new Map([\n${lines.join(',\n')}]);
if (rendered) {
entry.rendered = rendered;
}
if (legacyId) {
entry.legacyId = legacyId;
}
if (deferredRender) {
entry.deferredRender = deferredRender;
if (filePath) {
Expand Down Expand Up @@ -335,30 +348,7 @@ export interface DataStore {
key: string,
) => DataEntry<TData> | undefined;
entries: () => Array<[id: string, DataEntry]>;
set: <TData extends Record<string, unknown>>(opts: {
/** The ID of the entry. Must be unique per collection. */
id: string;
/** The data to store. */
data: TData;
/** The raw body of the content, if applicable. */
body?: string;
/** The file path of the content, if applicable. Relative to the site root. */
filePath?: string;
/** A content digest, to check if the content has changed. */
digest?: number | string;
/** The rendered content, if applicable. */
rendered?: RenderedContent;
/**
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase.
*/
deferredRender?: boolean;
/**
* Assets such as images to process during the build. These should be files on disk, with a path relative to filePath.
* Any values that use image() in the schema will already be added automatically.
* @internal
*/
assetImports?: Array<string>;
}) => boolean;
set: <TData extends Record<string, unknown>>(opts: DataEntry<TData>) => boolean;
values: () => Array<DataEntry>;
keys: () => Array<string>;
delete: (key: string) => void;
Expand Down
Loading
Loading