0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-10 23:01:26 -05:00

Implement legacy collections using glob (#11976)

* feat: support pattern arrays with glob

* wip

* feat: emulate legacy content collections

* Fixes

* Lint

* Correctly handle legacy data

* Fix tests

* Switch flag handling

* Fix warnings

* Add layout warning

* Update fixtures

* More tests!

* Handle empty md files

* Lockfile

* Dedupe name

* Handle data ID unslug

* Fix e2e

* Clean build

* Clean builds in tests

* Test fixes

* Fix test

* Fix typegen

* Fix tests

* Fixture updates

* Test updates

* Update changeset

* Test

* Remove wait in test

* Handle race condition

* Lock

* chore: changes from review

* Handle folders without config

* lint

* Fix test

* Update wording for auto-collections

* Delete legacyId

* Sort another fixture

* Rename flag to `legacy.collections`

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Changes from review

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* lockfile

* lock

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Matt Kane 2024-10-04 16:10:34 +01:00
parent 953e6e0f23
commit abf9a89ac1
117 changed files with 2172 additions and 511 deletions

View file

@ -0,0 +1,47 @@
---
'astro': major
---
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
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.

View file

@ -0,0 +1,5 @@
import { defineCollection } from 'astro:content';
export const collections = {
docs: defineCollection({})
};

View file

@ -0,0 +1,6 @@
import { defineCollection } from "astro:content";
const posts = defineCollection({});
export const collections = { posts };

View file

@ -34,6 +34,8 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
*/
deferredRender?: boolean;
assetImports?: Array<string>;
/** @deprecated */
legacyId?: string;
}
/**

View file

@ -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,
@ -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(
@ -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;
}
@ -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);
@ -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);
@ -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
@ -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);
@ -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;
}
@ -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)}.`,

View file

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

View file

@ -94,7 +94,7 @@ export function createGetCollection({
if (hasFilter && !filter(entry)) {
continue;
}
result.push(entry);
result.push(entry.legacyId ? emulateLegacyEntry(entry) : entry);
}
return result;
} else {
@ -162,23 +162,31 @@ export function createGetEntryBySlug({
getEntryImport,
getRenderEntryImport,
collectionNames,
getEntry,
}: {
getEntryImport: GetEntryImport;
getRenderEntryImport: GetEntryImport;
collectionNames: Set<string>;
getEntry: ReturnType<typeof createGetEntry>;
}) {
return async function getEntryBySlug(collection: string, slug: string) {
const store = await globalDataStore.get();
if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
const entry = await getEntry(collection, slug);
if (entry && 'slug' in entry) {
return entry;
}
throw new AstroError({
...AstroErrorData.GetEntryDeprecationError,
message: AstroErrorData.GetEntryDeprecationError.message(collection, 'getEntryBySlug'),
});
}
// eslint-disable-next-line no-console
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`,
);
return undefined;
}
@ -207,22 +215,23 @@ export function createGetEntryBySlug({
export function createGetDataEntryById({
getEntryImport,
collectionNames,
getEntry,
}: {
getEntryImport: GetEntryImport;
collectionNames: Set<string>;
getEntry: ReturnType<typeof createGetEntry>;
}) {
return async function getDataEntryById(collection: string, id: string) {
const store = await globalDataStore.get();
if (!collectionNames.has(collection)) {
if (store.hasCollection(collection)) {
throw new AstroError({
...AstroErrorData.GetEntryDeprecationError,
message: AstroErrorData.GetEntryDeprecationError.message(collection, 'getDataEntryById'),
});
return getEntry(collection, id);
}
// eslint-disable-next-line no-console
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`,
);
return undefined;
}
@ -256,6 +265,21 @@ type DataEntryResult = {
type EntryLookupObject = { collection: string; id: string } | { collection: string; slug: string };
function emulateLegacyEntry(entry: DataEntry) {
// Define this first so it's in scope for the render function
const legacyEntry = {
...entry,
id: entry.legacyId!,
slug: entry.id,
};
delete legacyEntry.legacyId;
return {
...legacyEntry,
// Define separately so the render function isn't included in the object passed to `renderEntry()`
render: () => renderEntry(legacyEntry),
};
}
export function createGetEntry({
getEntryImport,
getRenderEntryImport,
@ -303,6 +327,9 @@ export function createGetEntry({
// @ts-expect-error virtual module
const { default: imageAssetMap } = await import('astro:asset-imports');
entry.data = updateImageReferencesInData(entry.data, entry.filePath, imageAssetMap);
if (entry.legacyId) {
return { ...emulateLegacyEntry(entry), collection } as ContentEntryResult;
}
return {
...entry,
collection,
@ -311,7 +338,9 @@ export function createGetEntry({
if (!collectionNames.has(collection)) {
// eslint-disable-next-line no-console
console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
console.warn(
`The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`,
);
return undefined;
}
@ -433,13 +462,16 @@ function updateImageReferencesInData<T extends Record<string, unknown>>(
}
export async function renderEntry(
entry: DataEntry | { render: () => Promise<{ Content: AstroComponentFactory }> },
entry:
| DataEntry
| { render: () => Promise<{ Content: AstroComponentFactory }> }
| (DataEntry & { render: () => Promise<{ Content: AstroComponentFactory }> }),
) {
if (!entry) {
throw new AstroError(AstroErrorData.RenderUndefinedEntryError);
}
if ('render' in entry) {
if ('render' in entry && !('legacyId' in entry)) {
// This is an old content collection entry, so we use its render method
return entry.render();
}
@ -619,6 +651,7 @@ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap })
}
return { id: lookup, collection };
}
// If the collection is not in the lookup map or store, it may be a content layer collection and the store may not yet be populated.
// If the store has 0 or 1 entries it probably means that the entries have not yet been loaded.
// The store may have a single entry even if the collections have not loaded, because the top-level metadata collection is generated early.
@ -627,7 +660,6 @@ export function createReference({ lookupMap }: { lookupMap: ContentLookupMap })
// later in the pipeline when we do have access to the store.
return { id: lookup, collection };
}
const { type, entries } = lookupMap[collection];
const entry = entries[lookup];

View file

@ -501,7 +501,10 @@ async function writeContentFiles({
contentTypesStr += `};\n`;
break;
case CONTENT_LAYER_TYPE:
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n collection: ${collectionKey};\n data: ${dataType};\n rendered?: RenderedContent;\n filePath?: string;\n body?: string \n}>;\n`;
const legacyTypes = (collectionConfig as any)?._legacy
? 'render(): Render[".md"];\n slug: string;\n body: string;\n'
: 'body?: string;\n';
dataTypesStr += `${collectionKey}: Record<string, {\n id: string;\n ${legacyTypes} collection: ${collectionKey};\n data: ${dataType};\n rendered?: RenderedContent;\n filePath?: string;\n}>;\n`;
break;
case 'data':
if (collectionEntryKeys.length === 0) {

View file

@ -10,6 +10,7 @@ import { z } from 'zod';
import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js';
import { isYAMLException } from '../core/errors/utils.js';
import type { Logger } from '../core/logger/core.js';
import { appendForwardSlash } from '../core/path.js';
import { normalizePath } from '../core/viteUtils.js';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
@ -22,7 +23,9 @@ import {
IMAGE_IMPORT_PREFIX,
PROPAGATED_ASSET_FLAG,
} from './consts.js';
import { glob } from './loaders/glob.js';
import { createImage } from './runtime-assets.js';
import { green } from 'kleur/colors';
/**
* Amap from a collection + slug to the local file path.
* This is used internally to resolve entry imports when using `getEntry()`.
@ -114,6 +117,8 @@ const collectionConfigParser = z.union([
render: z.function(z.tuple([z.any()], z.unknown())).optional(),
}),
]),
/** deprecated */
_legacy: z.boolean().optional(),
}),
]);
@ -162,7 +167,7 @@ export async function getEntryDataAndImages<
pluginContext?: PluginContext,
): Promise<{ data: TOutputData; imageImports: Array<string> }> {
let data: TOutputData;
if (collectionConfig.type === 'data' || collectionConfig.type === CONTENT_LAYER_TYPE) {
if (collectionConfig.type === 'data') {
data = entry.unvalidatedData as TOutputData;
} else {
const { slug, ...unvalidatedData } = entry.unvalidatedData;
@ -536,6 +541,97 @@ async function loadContentConfig({
}
}
export async function autogenerateCollections({
config,
settings,
fs,
}: {
config?: ContentConfig;
settings: AstroSettings;
fs: typeof fsMod;
}): Promise<ContentConfig | undefined> {
if (settings.config.legacy.collections) {
return config;
}
const contentDir = new URL('./content/', settings.config.srcDir);
const collections: Record<string, CollectionConfig> = config?.collections ?? {};
const contentExts = getContentEntryExts(settings);
const dataExts = getDataEntryExts(settings);
const contentPattern = globWithUnderscoresIgnored('', contentExts);
const dataPattern = globWithUnderscoresIgnored('', dataExts);
let usesContentLayer = false;
for (const collectionName of Object.keys(collections)) {
if (collections[collectionName]?.type === 'content_layer') {
usesContentLayer = true;
// This is already a content layer, skip
continue;
}
const isDataCollection = collections[collectionName]?.type === 'data';
const base = new URL(`${collectionName}/`, contentDir);
// Only "content" collections need special legacy handling
const _legacy = !isDataCollection || undefined;
collections[collectionName] = {
...collections[collectionName],
type: 'content_layer',
_legacy,
loader: glob({
base,
pattern: isDataCollection ? dataPattern : contentPattern,
_legacy,
// Legacy data collections IDs aren't slugified
generateId: isDataCollection
? ({ entry }) =>
getDataEntryId({
entry: new URL(entry, base),
collection: collectionName,
contentDir,
})
: undefined,
// Zod weirdness has trouble with typing the args to the load function
}) as any,
};
}
if (!usesContentLayer) {
// 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.
const orphanedCollections = [];
for (const entry of await fs.promises.readdir(contentDir, { withFileTypes: true })) {
const collectionName = entry.name;
if (['_', '.'].includes(collectionName.at(0) ?? '')) {
continue;
}
if (entry.isDirectory() && !(collectionName in collections)) {
orphanedCollections.push(collectionName);
const base = new URL(`${collectionName}/`, contentDir);
collections[collectionName] = {
type: 'content_layer',
loader: glob({
base,
pattern: contentPattern,
_legacy: true,
}) as any,
};
}
}
if (orphanedCollections.length > 0) {
console.warn(
`
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".
The following collections have been auto-generated: ${orphanedCollections
.map((name) => green(name))
.join(', ')}\n`,
);
}
}
return { ...config, collections };
}
export async function reloadContentConfigObserver({
observer = globalContentConfigObserver,
...loadContentConfigOpts
@ -547,7 +643,13 @@ export async function reloadContentConfigObserver({
}) {
observer.set({ status: 'loading' });
try {
const config = await loadContentConfig(loadContentConfigOpts);
let config = await loadContentConfig(loadContentConfigOpts);
config = await autogenerateCollections({
config,
...loadContentConfigOpts,
});
if (config) {
observer.set({ status: 'loaded', config });
} else {
@ -685,6 +787,16 @@ export function hasAssetPropagationFlag(id: string): boolean {
}
}
export function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): string[] {
const extGlob = getExtGlob(exts);
const contentDir = relContentDir.length > 0 ? appendForwardSlash(relContentDir) : relContentDir;
return [
`${contentDir}**/*${extGlob}`,
`!${contentDir}**/_*/**/*${extGlob}`,
`!${contentDir}**/_*${extGlob}`,
];
}
/**
* Convert a platform path to a posix path.
*/

View file

@ -6,7 +6,6 @@ import glob from 'fast-glob';
import pLimit from 'p-limit';
import type { Plugin } from 'vite';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { appendForwardSlash } from '../core/path.js';
import { rootRelativePath } from '../core/viteUtils.js';
import type { AstroSettings } from '../types/astro.js';
import type { AstroPluginMetadata } from '../vite-plugin-astro/index.js';
@ -38,6 +37,7 @@ import {
getEntrySlug,
getEntryType,
getExtGlob,
globWithUnderscoresIgnored,
isDeferredModule,
} from './utils.js';
@ -98,10 +98,12 @@ export function astroContentVirtualModPlugin({
},
async load(id, args) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
const lookupMap = await generateLookupMap({
settings,
fs,
});
const lookupMap = settings.config.legacy.collections
? await generateLookupMap({
settings,
fs,
})
: {};
const isClient = !args?.ssr;
const code = await generateContentEntryFile({
settings,
@ -201,26 +203,28 @@ export async function generateContentEntryFile({
const contentPaths = getContentPaths(settings.config);
const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);
let contentEntryGlobResult: string;
let dataEntryGlobResult: string;
let renderEntryGlobResult: string;
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
const contentEntryExts = [...contentEntryConfigByExt.keys()];
const dataEntryExts = getDataEntryExts(settings);
const createGlob = (value: string[], flag: string) =>
`import.meta.glob(${JSON.stringify(value)}, { query: { ${flag}: true } })`;
contentEntryGlobResult = createGlob(
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
CONTENT_FLAG,
);
dataEntryGlobResult = createGlob(
globWithUnderscoresIgnored(relContentDir, dataEntryExts),
DATA_FLAG,
);
renderEntryGlobResult = createGlob(
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
CONTENT_RENDER_FLAG,
);
let contentEntryGlobResult = '""';
let dataEntryGlobResult = '""';
let renderEntryGlobResult = '""';
if (settings.config.legacy.collections) {
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
const contentEntryExts = [...contentEntryConfigByExt.keys()];
const dataEntryExts = getDataEntryExts(settings);
const createGlob = (value: string[], flag: string) =>
`import.meta.glob(${JSON.stringify(value)}, { query: { ${flag}: true } })`;
contentEntryGlobResult = createGlob(
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
CONTENT_FLAG,
);
dataEntryGlobResult = createGlob(
globWithUnderscoresIgnored(relContentDir, dataEntryExts),
DATA_FLAG,
);
renderEntryGlobResult = createGlob(
globWithUnderscoresIgnored(relContentDir, contentEntryExts),
CONTENT_RENDER_FLAG,
);
}
let virtualModContents: string;
if (isClient) {
@ -354,16 +358,6 @@ export async function generateLookupMap({
return lookupMap;
}
function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): string[] {
const extGlob = getExtGlob(exts);
const contentDir = appendForwardSlash(relContentDir);
return [
`${contentDir}**/*${extGlob}`,
`!${contentDir}**/_*/**/*${extGlob}`,
`!${contentDir}**/_*${extGlob}`,
];
}
const UnexpectedLookupMapError = new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `Unexpected error while parsing content entry IDs and slugs.`,

View file

@ -80,7 +80,9 @@ export const ASTRO_CONFIG_DEFAULTS = {
integrations: [],
markdown: markdownConfigDefaults,
vite: {},
legacy: {},
legacy: {
collections: false,
},
redirects: {},
security: {
checkOrigin: true,
@ -522,7 +524,14 @@ export const AstroConfigSchema = z.object({
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`,
)
.default({}),
legacy: z.object({}).default({}),
legacy: z
.object({
collections: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.legacy.collections),
})
.default({}),
});
export type AstroConfigType = z.infer<typeof AstroConfigSchema>;

View file

@ -1541,7 +1541,49 @@ export interface AstroUserConfig {
* These flags allow you to opt in to some deprecated or otherwise outdated behavior of Astro
* in the latest version, so that you can continue to upgrade and take advantage of new Astro releases.
*/
legacy?: object;
legacy?: {
/**
* @docs
* @name legacy.collections
* @type {boolean}
* @default `false`
* @version 5.0.0
* @description
* Enable legacy behavior for content collections.
*
* ```js
* // astro.config.mjs
* import { defineConfig } from 'astro/config';
* export default defineConfig({
* legacy: {
* collections: true
* }
* });
* ```
*
* If enabled, `data` and `content` collections (only) are handled using the legacy content collections implementation. Collections with a `loader` (only) will continue to use the Content Layer API instead. Both kinds of collections may exist in the same project, each using their respective implementations.
*
* The following limitations continue to exist:
*
* - Any legacy (`type: 'content'` or `type: 'data'`) collections must continue to be located in the `src/content/` directory.
* - These legacy collections will not be transformed to implicitly use the `glob()` loader, and will instead be handled by legacy code.
* - Collections using the Content Layer API (with a `loader` defined) are forbidden in `src/content/`, but may exist anywhere else in your project.
*
* 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
* // src/content/config.ts
* import { defineCollection, z } from 'astro:content';
*
* const blog = defineCollection({ })
*
* export const collections = { blog };
* ```
*
*/
collections?: boolean;
};
/**
* @docs

View file

@ -58,23 +58,25 @@ export const getCollection = createGetCollection({
cacheEntriesByCollection,
});
export const getEntryBySlug = createGetEntryBySlug({
getEntryImport: createGlobLookup(contentCollectionToEntryMap),
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
collectionNames,
});
export const getDataEntryById = createGetDataEntryById({
getEntryImport: createGlobLookup(dataCollectionToEntryMap),
collectionNames,
});
export const getEntry = createGetEntry({
getEntryImport: createGlobLookup(collectionToEntryMap),
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
collectionNames,
});
export const getEntryBySlug = createGetEntryBySlug({
getEntryImport: createGlobLookup(contentCollectionToEntryMap),
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
collectionNames,
getEntry,
});
export const getDataEntryById = createGetDataEntryById({
getEntryImport: createGlobLookup(dataCollectionToEntryMap),
collectionNames,
getEntry,
});
export const getEntries = createGetEntries(getEntry);
export const reference = createReference({ lookupMap });

View file

@ -142,20 +142,24 @@ describe('astro sync', () => {
'.astro/content.d.ts',
`"blog": Record<string, {
id: string;
render(): Render[".md"];
slug: string;
body: string;
collection: "blog";
data: InferEntrySchema<"blog">;
render(): Render[".md"];
}>;`,
rendered?: RenderedContent;
filePath?: string;`,
'Types file does not include empty collection type',
);
fixture.thenFileContentShouldInclude(
'.astro/content.d.ts',
`"blogMeta": Record<string, {
id: string;
body?: string;
collection: "blogMeta";
data: InferEntrySchema<"blogMeta">;
rendered?: RenderedContent;
filePath?: string;
}>;`,
'Types file does not include empty collection type',
);

View file

@ -16,9 +16,12 @@ describe('Content Collections - references', () => {
describe(mode, () => {
before(async () => {
if (mode === 'prod') {
await fixture.build();
await fixture.build({ force: true });
} else if (mode === 'dev') {
devServer = await fixture.startDevServer();
devServer = await fixture.startDevServer({ force: true });
await fixture.onNextDataStoreChange(1000).catch(() => {
// Ignore timeout, because it may have saved before we get here.
});
}
});
@ -65,13 +68,9 @@ describe('Content Collections - references', () => {
it('Returns `author` data', () => {
const { author } = json;
assert.ok(author.hasOwnProperty('data'));
assert.deepEqual(author, {
id: 'nate-moore',
collection: 'authors',
data: {
name: 'Nate Something Moore',
twitter: 'https://twitter.com/n_moore',
},
assert.deepEqual(author.data, {
name: 'Nate Something Moore',
twitter: 'https://twitter.com/n_moore',
});
});
@ -82,20 +81,23 @@ describe('Content Collections - references', () => {
...meta,
body: fixLineEndings(body).trim(),
}));
assert.deepEqual(topLevelInfo, [
{
id: 'related-1.md',
slug: 'related-1',
body: '# Related post 1\n\nThis is related to the welcome post.',
collection: 'blog',
},
{
id: 'related-2.md',
slug: 'related-2',
body: '# Related post 2\n\nThis is related to the welcome post.',
collection: 'blog',
},
]);
assert.deepEqual(
topLevelInfo.map(({ id, slug, body, collection }) => ({ id, slug, body, collection })),
[
{
id: 'related-1.md',
slug: 'related-1',
body: '# Related post 1\n\nThis is related to the welcome post.',
collection: 'blog',
},
{
id: 'related-2.md',
slug: 'related-2',
body: '# Related post 2\n\nThis is related to the welcome post.',
collection: 'blog',
},
],
);
const postData = relatedPosts.map(({ data }) => data);
assert.deepEqual(postData, [
{

View file

@ -11,7 +11,7 @@ describe('Content Collections', () => {
let fixture;
before(async () => {
fixture = await loadFixture({ root: './fixtures/content-collections/' });
await fixture.build();
await fixture.build({ force: true });
});
describe('Collection', () => {
@ -26,13 +26,16 @@ describe('Content Collections', () => {
assert.equal(Array.isArray(json.withoutConfig), true);
const ids = json.withoutConfig.map((item) => item.id);
assert.deepEqual(ids, [
'columbia.md',
'endeavour.md',
'enterprise.md',
// Spaces allowed in IDs
'promo/launch week.mdx',
]);
assert.deepEqual(
ids.sort(),
[
'columbia.md',
'endeavour.md',
'enterprise.md',
// Spaces allowed in IDs
'promo/launch week.mdx',
].sort(),
);
});
it('Handles spaces in `without config` slugs', async () => {
@ -40,13 +43,16 @@ describe('Content Collections', () => {
assert.equal(Array.isArray(json.withoutConfig), true);
const slugs = json.withoutConfig.map((item) => item.slug);
assert.deepEqual(slugs, [
'columbia',
'endeavour',
'enterprise',
// "launch week.mdx" is converted to "launch-week.mdx"
'promo/launch-week',
]);
assert.deepEqual(
slugs.sort(),
[
'columbia',
'endeavour',
'enterprise',
// "launch week.mdx" is converted to "launch-week.mdx"
'promo/launch-week',
].sort(),
);
});
it('Returns `with schema` collection', async () => {
@ -55,20 +61,20 @@ describe('Content Collections', () => {
const ids = json.withSchemaConfig.map((item) => item.id);
const publishedDates = json.withSchemaConfig.map((item) => item.data.publishedAt);
assert.deepEqual(ids, ['four%.md', 'one.md', 'three.md', 'two.md']);
assert.deepEqual(ids.sort(), ['four%.md', 'one.md', 'three.md', 'two.md'].sort());
assert.equal(
publishedDates.every((date) => date instanceof Date),
true,
'Not all publishedAt dates are Date objects',
);
assert.deepEqual(
publishedDates.map((date) => date.toISOString()),
publishedDates.map((date) => date.toISOString()).sort(),
[
'2021-01-01T00:00:00.000Z',
'2021-01-01T00:00:00.000Z',
'2021-01-03T00:00:00.000Z',
'2021-01-02T00:00:00.000Z',
],
].sort(),
);
});
@ -77,7 +83,7 @@ describe('Content Collections', () => {
assert.equal(Array.isArray(json.withSlugConfig), true);
const slugs = json.withSlugConfig.map((item) => item.slug);
assert.deepEqual(slugs, ['fancy-one', 'excellent-three', 'interesting-two']);
assert.deepEqual(slugs.sort(), ['fancy-one', 'excellent-three', 'interesting-two'].sort());
});
it('Returns `with union schema` collection', async () => {
@ -102,10 +108,12 @@ describe('Content Collections', () => {
it('Handles symlinked content', async () => {
assert.ok(json.hasOwnProperty('withSymlinkedContent'));
assert.equal(Array.isArray(json.withSymlinkedContent), true);
const ids = json.withSymlinkedContent.map((item) => item.id);
assert.deepEqual(ids, ['first.md', 'second.md', 'third.md']);
assert.equal(json.withSymlinkedContent[0].data.title, 'First Blog');
assert.deepEqual(ids.sort(), ['first.md', 'second.md', 'third.md'].sort());
assert.equal(
json.withSymlinkedContent.find(({ id }) => id === 'first.md').data.title,
'First Blog',
);
});
it('Handles symlinked data', async () => {
@ -137,11 +145,6 @@ describe('Content Collections', () => {
json = devalue.parse(rawJson);
});
it('Returns `without config` collection entry', async () => {
assert.ok(json.hasOwnProperty('columbiaWithoutConfig'));
assert.equal(json.columbiaWithoutConfig.id, 'columbia.md');
});
it('Returns `with schema` collection entry', async () => {
assert.ok(json.hasOwnProperty('oneWithSchemaConfig'));
assert.equal(json.oneWithSchemaConfig.id, 'one.md');
@ -212,7 +215,7 @@ describe('Content Collections', () => {
before(async () => {
fixture = await loadFixture({ root: './fixtures/content-static-paths-integration/' });
await fixture.build();
await fixture.build({ force: true });
});
it('Generates expected pages', async () => {
@ -246,7 +249,7 @@ describe('Content Collections', () => {
const fixture = await loadFixture({ root: './fixtures/content with spaces in folder name/' });
let error = null;
try {
await fixture.build();
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
@ -260,7 +263,7 @@ describe('Content Collections', () => {
});
let error;
try {
await fixture.build();
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
@ -274,7 +277,7 @@ describe('Content Collections', () => {
});
let error;
try {
await fixture.build();
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
@ -289,7 +292,7 @@ describe('Content Collections', () => {
});
let error;
try {
await fixture.build();
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
@ -304,7 +307,7 @@ describe('Content Collections', () => {
});
let error;
try {
await fixture.build();
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
@ -330,7 +333,7 @@ describe('Content Collections', () => {
plugins: [preventNodeBuiltinDependencyPlugin()],
},
});
await fixture.build();
await fixture.build({ force: true });
app = await fixture.loadTestAdapterApp();
});
@ -373,7 +376,7 @@ describe('Content Collections', () => {
fixture = await loadFixture({
root: './fixtures/content-collections-base/',
});
await fixture.build();
await fixture.build({ force: true });
});
it('Includes base in links', async () => {
@ -396,7 +399,7 @@ describe('Content Collections', () => {
fixture = await loadFixture({
root: './fixtures/content-collections-mutation/',
});
await fixture.build();
await fixture.build({ force: true });
});
it('Does not mutate cached collection', async () => {

View file

@ -9,7 +9,7 @@ describe('Content Collections - data collections', () => {
let fixture;
before(async () => {
fixture = await loadFixture({ root: './fixtures/data-collections/' });
await fixture.build();
await fixture.build({ force: true });
});
describe('Authors Collection', () => {
@ -26,19 +26,25 @@ describe('Content Collections - data collections', () => {
it('Generates correct ids', async () => {
const ids = json.map((item) => item.id).sort();
assert.deepEqual(ids, ['Ben Holmes', 'Fred K Schott', 'Nate Moore']);
assert.deepEqual(ids.sort(), ['Ben Holmes', 'Fred K Schott', 'Nate Moore'].sort());
});
it('Generates correct data', async () => {
const names = json.map((item) => item.data.name);
assert.deepEqual(names, ['Ben J Holmes', 'Fred K Schott', 'Nate Something Moore']);
assert.deepEqual(
names.sort(),
['Ben J Holmes', 'Fred K Schott', 'Nate Something Moore'].sort(),
);
const twitterUrls = json.map((item) => item.data.twitter);
assert.deepEqual(twitterUrls, [
'https://twitter.com/bholmesdev',
'https://twitter.com/FredKSchott',
'https://twitter.com/n_moore',
]);
assert.deepEqual(
twitterUrls.sort(),
[
'https://twitter.com/bholmesdev',
'https://twitter.com/FredKSchott',
'https://twitter.com/n_moore',
].sort(),
);
});
});

View file

@ -9,5 +9,5 @@ export default defineConfig({
build: {
assetsInlineLimit: 0
}
}
},
});

View file

@ -5,6 +5,7 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
integrations: [mdx()],
experimental: {
contentIntellisense: true
contentIntellisense: true,
}
});

View file

@ -0,0 +1,14 @@
---
title: 'Enterprise'
description: 'Learn about the Enterprise NASA space shuttle.'
publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 70s]
---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.

View file

@ -0,0 +1,14 @@
---
title: 'Enterprise'
description: 'Learn about the Enterprise NASA space shuttle.'
publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 70s]
---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.

View file

@ -52,6 +52,8 @@ const withSymlinkedContent = defineCollection({
}),
});
const withScripts = defineCollection({});
export const collections = {
'with-data': withData,
'with-custom-slugs': withCustomSlugs,
@ -59,4 +61,5 @@ export const collections = {
'with-union-schema': withUnionSchema,
'with-symlinked-data': withSymlinkedData,
'with-symlinked-content': withSymlinkedContent,
'with-scripts': withScripts,
};

View file

@ -3,14 +3,14 @@ import * as devalue from 'devalue';
import { stripAllRenderFn } from '../utils.js';
export async function GET() {
const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
const withSchemaConfig = stripAllRenderFn(await getCollection('with-schema-config'));
const withSlugConfig = stripAllRenderFn(await getCollection('with-custom-slugs'));
const withUnionSchema = stripAllRenderFn(await getCollection('with-union-schema'));
const withSymlinkedContent = stripAllRenderFn(await getCollection('with-symlinked-content'));
const withSymlinkedData = stripAllRenderFn(await getCollection('with-symlinked-data'));
const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
return new Response(
devalue.stringify({ withoutConfig, withSchemaConfig, withSlugConfig, withUnionSchema, withSymlinkedContent, withSymlinkedData }),
devalue.stringify({ withSchemaConfig, withSlugConfig, withUnionSchema, withSymlinkedContent, withSymlinkedData, withoutConfig }),
);
}

View file

@ -3,7 +3,6 @@ import * as devalue from 'devalue';
import { stripRenderFn } from '../utils.js';
export async function GET() {
const columbiaWithoutConfig = stripRenderFn(await getEntryBySlug('without-config', 'columbia'));
const oneWithSchemaConfig = stripRenderFn(await getEntryBySlug('with-schema-config', 'one'));
const twoWithSlugConfig = stripRenderFn(
await getEntryBySlug('with-custom-slugs', 'interesting-two')
@ -12,7 +11,6 @@ export async function GET() {
return new Response(
devalue.stringify({
columbiaWithoutConfig,
oneWithSchemaConfig,
twoWithSlugConfig,
postWithUnionSchema,

View file

@ -1,4 +1,8 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});
export default defineConfig({
legacy: {
collections: true,
}
});

View file

@ -3,4 +3,8 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
integrations: [mdx()],
legacy: {
// Enable legacy content collections as we test layout fields
collections: true
}
});

View file

@ -11,7 +11,6 @@ export async function getStaticPaths() {
const { entry } = Astro.props;
const { Content } = await entry.render();
const myImage = await getImage(entry.data.image);
---
<html>
<head>

View file

@ -0,0 +1,9 @@
// @ts-check
import { defineConfig } from 'astro/config';
export default defineConfig({
legacy: {
// Needed because we're using image().refine()
collections: true,
},
});

View file

@ -0,0 +1,9 @@
// @ts-check
import { defineConfig } from 'astro/config';
export default defineConfig({
legacy: {
// Needed because we're using image().refine()
collections: true,
},
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
legacy: {
// Enable legacy content collections as we test layout fields
collections: true
}
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
legacy: {
// Enable legacy content collections as we test layout fields
collections: true
}
});

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
legacy: {
// Enable legacy content collections as we test layout fields
collections: true
}
});

View file

@ -38,4 +38,6 @@ const image = defineCollection({
}),
});
export const collections = { docs, func, image, i18n };
const authors = defineCollection({});
export const collections = { docs, func, image, i18n, authors };

View file

@ -9,7 +9,7 @@ export function getStaticPaths() {
/** @param {import('astro').APIContext} params */
export async function GET({ params }) {
const { id } = params;
const author = await getEntry('authors-without-config', id);
const author = await getEntry('authors', id);
if (!author) {
return Response.json({ error: `Author ${id} Not found` });
} else {

View file

@ -1,6 +1,6 @@
import { getCollection } from 'astro:content';
export async function GET() {
const authors = await getCollection('authors-without-config');
const authors = await getCollection('authors');
return Response.json(authors);
}

View file

@ -1,4 +1,6 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});
export default defineConfig({
});

View file

@ -17,4 +17,8 @@ const i18n = defineCollection({
}),
});
export const collections = { docs, i18n };
const authors = defineCollection({
type: 'data'
});
export const collections = { docs, i18n, authors };

View file

@ -1,15 +1,15 @@
import { getEntry } from 'astro:content';
import { getEntry, getCollection } from 'astro:content';
const ids = ['Ben Holmes', 'Fred K Schott', 'Nate Moore'];
export async function getStaticPaths() {
const collection = await getCollection('authors');
export function getStaticPaths() {
return ids.map((id) => ({ params: { id } }));
return collection.map(({ id }) => ({ params: { id } }));
}
/** @param {import('astro').APIContext} params */
export async function GET({ params }) {
const { id } = params;
const author = await getEntry('authors-without-config', id);
const author = await getEntry('authors', id);
if (!author) {
return Response.json({ error: `Author ${id} Not found` });
} else {

View file

@ -1,6 +1,6 @@
import { getCollection } from 'astro:content';
export async function GET() {
const authors = await getCollection('authors-without-config');
const authors = await getCollection('authors');
return Response.json(authors);
}

View file

@ -0,0 +1,13 @@
import mdx from '@astrojs/mdx';
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
integrations: [mdx()],
experimental: {
contentIntellisense: true,
},
legacy: {
collections: true,
},
});

View file

@ -0,0 +1,9 @@
{
"name": "@test/legacy-content-collections",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/mdx": "workspace:*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1 @@
<script>console.log('ScriptCompA')</script>

View file

@ -0,0 +1 @@
<script>console.log('ScriptCompB')</script>

View file

@ -0,0 +1,62 @@
import { defineCollection, z } from 'astro:content';
const withData = defineCollection({
type: 'data',
schema: z.object({
title: z.string(),
}),
});
const withCustomSlugs = defineCollection({
// Ensure schema passes even when `slug` is present
schema: z.object({}).strict(),
});
const withSchemaConfig = defineCollection({
schema: z.object({
title: z.string(),
isDraft: z.boolean().default(false),
lang: z.enum(['en', 'fr', 'es']).default('en'),
publishedAt: z.date().transform((val) => new Date(val)),
}),
});
const withUnionSchema = defineCollection({
schema: z.discriminatedUnion('type', [
z.object({
type: z.literal('post'),
title: z.string(),
description: z.string(),
}),
z.object({
type: z.literal('newsletter'),
subject: z.string(),
}),
]),
});
const withSymlinkedData = defineCollection({
type: 'data',
schema: ({ image }) =>
z.object({
alt: z.string(),
src: image(),
}),
});
const withSymlinkedContent = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.date(),
}),
});
export const collections = {
'with-data': withData,
'with-custom-slugs': withCustomSlugs,
'with-schema-config': withSchemaConfig,
'with-union-schema': withUnionSchema,
'with-symlinked-data': withSymlinkedData,
'with-symlinked-content': withSymlinkedContent,
};

View file

@ -0,0 +1,5 @@
---
slug: fancy-one
---
# It's the first page, fancy!

View file

@ -0,0 +1,5 @@
---
slug: excellent-three
---
# It's the third page, excellent!

View file

@ -0,0 +1,5 @@
---
slug: interesting-two
---
# It's the second page, interesting!

View file

@ -0,0 +1,3 @@
{
"title": "One"
}

View file

@ -0,0 +1,3 @@
{
"title": "Three"
}

View file

@ -0,0 +1,3 @@
{
"title": "Two"
}

View file

@ -0,0 +1,8 @@
---
title: Four
description: The forth page
lang: en
publishedAt: 2021-01-01
---
# It's the forth page, fancy!

View file

@ -0,0 +1,8 @@
---
title: One
description: The first page
lang: en
publishedAt: 2021-01-01
---
# It's the first page, fancy!

View file

@ -0,0 +1,8 @@
---
title: Three
description: The third page
lang: es
publishedAt: 2021-01-03
---
# It's the third page, excellent!

View file

@ -0,0 +1,8 @@
---
title: Two
description: The second page
lang: en
publishedAt: 2021-01-02
---
# It's the second page, interesting!

View file

@ -0,0 +1,7 @@
import ScriptCompA from '../../components/ScriptCompA.astro'
import ScriptCompB from '../../components/ScriptCompB.astro'
Both scripts should exist.
<ScriptCompA />
<ScriptCompB />

View file

@ -0,0 +1 @@
../../symlinked-collections/content-collection

View file

@ -0,0 +1 @@
../../symlinked-collections/data-collection

View file

@ -0,0 +1,6 @@
---
type: newsletter
subject: My Newsletter
---
# It's a newsletter!

View file

@ -0,0 +1,7 @@
---
type: post
title: My Post
description: This is my post
---
# It's a post!

View file

@ -0,0 +1,15 @@
---
title: Columbia
description: 'Learn about the Columbia NASA space shuttle.'
publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 90s]
---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.

View file

@ -0,0 +1,14 @@
---
title: Endeavour
description: 'Learn about the Endeavour NASA space shuttle.'
publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 90s]
---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.

View file

@ -0,0 +1,14 @@
---
title: 'Enterprise'
description: 'Learn about the Enterprise NASA space shuttle.'
publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 70s]
---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.

View file

@ -0,0 +1,3 @@
body {
font-family: 'Comic Sans MS', sans-serif;
}

View file

@ -0,0 +1,14 @@
---
title: 'Launch week!'
description: 'Join us for the exciting launch of SPACE BLOG'
publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: ['announcement']
---
import './_launch-week-styles.css';
Join us for the space blog launch!
- THIS THURSDAY
- Houston, TX
- Dress code: **interstellar casual** ✨

View file

@ -0,0 +1,16 @@
import { getCollection } from 'astro:content';
import * as devalue from 'devalue';
import { stripAllRenderFn } from '../utils.js';
export async function GET() {
const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
const withSchemaConfig = stripAllRenderFn(await getCollection('with-schema-config'));
const withSlugConfig = stripAllRenderFn(await getCollection('with-custom-slugs'));
const withUnionSchema = stripAllRenderFn(await getCollection('with-union-schema'));
const withSymlinkedContent = stripAllRenderFn(await getCollection('with-symlinked-content'));
const withSymlinkedData = stripAllRenderFn(await getCollection('with-symlinked-data'));
return new Response(
devalue.stringify({ withoutConfig, withSchemaConfig, withSlugConfig, withUnionSchema, withSymlinkedContent, withSymlinkedData }),
);
}

View file

@ -0,0 +1,21 @@
import { getEntryBySlug } from 'astro:content';
import * as devalue from 'devalue';
import { stripRenderFn } from '../utils.js';
export async function GET() {
const columbiaWithoutConfig = stripRenderFn(await getEntryBySlug('without-config', 'columbia'));
const oneWithSchemaConfig = stripRenderFn(await getEntryBySlug('with-schema-config', 'one'));
const twoWithSlugConfig = stripRenderFn(
await getEntryBySlug('with-custom-slugs', 'interesting-two')
);
const postWithUnionSchema = stripRenderFn(await getEntryBySlug('with-union-schema', 'post'));
return new Response(
devalue.stringify({
columbiaWithoutConfig,
oneWithSchemaConfig,
twoWithSlugConfig,
postWithUnionSchema,
})
);
}

View file

@ -0,0 +1,24 @@
---
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>It's content time!</title>
<style>
html,
body {
font-family: system-ui;
margin: 0;
}
body {
padding: 2rem;
}
</style>
</head>
<body>
<main>
</main>
</body>
</html>

View file

@ -0,0 +1,22 @@
---
import { getCollection } from "astro:content";
const posts = await getCollection("with-schema-config");
---
<html>
<body>
<div class="foo">
<div>Hello World</div>
<span>Styles?</span>
</div>
</body>
</html>
<style>
.foo {
background-color: blue;
}
span::after {
content: "works!";
}
</style>

View file

@ -0,0 +1,21 @@
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const blogEntries = await getCollection('with-scripts');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
const { title } = entry.data;
---
<article>
<h1>This is a content collection post</h1>
<h2>{title}</h2>
<Content />
</article>

View file

@ -0,0 +1,8 @@
export function stripRenderFn(entryWithRender) {
const { render, ...entry } = entryWithRender;
return entry;
}
export function stripAllRenderFn(collection = []) {
return collection.map(stripRenderFn);
}

View file

@ -0,0 +1,6 @@
---
title: "First Blog"
date: 2024-04-05
---
First blog content.

View file

@ -0,0 +1,6 @@
---
title: "Second Blog"
date: 2024-04-06
---
Second blog content.

View file

@ -0,0 +1,6 @@
---
title: "Third Blog"
date: 2024-04-07
---
Third blog content.

View file

@ -0,0 +1,4 @@
{
"alt": "Futuristic landscape with chrome buildings and blue skies",
"src": "../../assets/the-future.jpg"
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
legacy: {
collections: true,
},
});

View file

@ -0,0 +1,16 @@
{
"name": "@test/legacy-data-collections",
"type": "module",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,2 @@
name: Ben J Holmes
twitter: https://twitter.com/bholmesdev

View file

@ -0,0 +1,2 @@
name: Fred K Schott
twitter: https://twitter.com/FredKSchott

View file

@ -0,0 +1,2 @@
name: Nate Something Moore
twitter: https://twitter.com/n_moore

View file

@ -0,0 +1,20 @@
import { defineCollection, z } from 'astro:content';
const docs = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
})
});
const i18n = defineCollection({
type: 'data',
schema: z.object({
homepage: z.object({
greeting: z.string(),
preamble: z.string(),
})
}),
});
export const collections = { docs, i18n };

View file

@ -0,0 +1,3 @@
---
title: The future of content
---

View file

@ -0,0 +1,6 @@
{
"homepage": {
"greeting": "Hello World!",
"preamble": "Welcome to the future of content."
}
}

View file

@ -0,0 +1,6 @@
{
"homepage": {
"greeting": "¡Hola Mundo!",
"preamble": "Bienvenido al futuro del contenido."
}
}

View file

@ -0,0 +1,3 @@
homepage:
greeting: "Bonjour le monde!"
preamble: "Bienvenue dans le futur du contenu."

View file

@ -0,0 +1,18 @@
import { getEntry } from 'astro:content';
const ids = ['Ben Holmes', 'Fred K Schott', 'Nate Moore'];
export function getStaticPaths() {
return ids.map((id) => ({ params: { id } }));
}
/** @param {import('astro').APIContext} params */
export async function GET({ params }) {
const { id } = params;
const author = await getEntry('authors-without-config', id);
if (!author) {
return Response.json({ error: `Author ${id} Not found` });
} else {
return Response.json(author);
}
}

View file

@ -0,0 +1,6 @@
import { getCollection } from 'astro:content';
export async function GET() {
const authors = await getCollection('authors-without-config');
return Response.json(authors);
}

View file

@ -0,0 +1,18 @@
import { getEntry } from 'astro:content';
const langs = ['en', 'es', 'fr'];
export function getStaticPaths() {
return langs.map((lang) => ({ params: { lang } }));
}
/** @param {import('astro').APIContext} params */
export async function GET({ params }) {
const { lang } = params;
const translations = await getEntry('i18n', lang);
if (!translations) {
return Response.json({ error: `Translation ${lang} Not found` });
} else {
return Response.json(translations);
}
}

View file

@ -0,0 +1,6 @@
import { getCollection } from 'astro:content';
export async function GET() {
const translations = await getCollection('i18n');
return Response.json(translations);
}

View file

@ -0,0 +1,6 @@
import { getDataEntryById } from 'astro:content';
export async function GET() {
const item = await getDataEntryById('i18n', 'en');
return Response.json(item);
}

View file

@ -0,0 +1,450 @@
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import * as devalue from 'devalue';
import testAdapter from './test-adapter.js';
import { preventNodeBuiltinDependencyPlugin } from './test-plugins.js';
import { loadFixture } from './test-utils.js';
describe('Legacy Content Collections', () => {
describe('Query', () => {
let fixture;
before(async () => {
fixture = await loadFixture({ root: './fixtures/legacy-content-collections/' });
await fixture.build();
});
describe('Collection', () => {
let json;
before(async () => {
const rawJson = await fixture.readFile('/collections.json');
json = devalue.parse(rawJson);
});
it('Returns `without config` collection', async () => {
assert.ok(json.hasOwnProperty('withoutConfig'));
assert.equal(Array.isArray(json.withoutConfig), true);
const ids = json.withoutConfig.map((item) => item.id);
assert.deepEqual(
ids.sort(),
[
'columbia.md',
'endeavour.md',
'enterprise.md',
// Spaces allowed in IDs
'promo/launch week.mdx',
].sort(),
);
});
it('Handles spaces in `without config` slugs', async () => {
assert.ok(json.hasOwnProperty('withoutConfig'));
assert.equal(Array.isArray(json.withoutConfig), true);
const slugs = json.withoutConfig.map((item) => item.slug);
assert.deepEqual(
slugs.sort(),
[
'columbia',
'endeavour',
'enterprise',
// "launch week.mdx" is converted to "launch-week.mdx"
'promo/launch-week',
].sort(),
);
});
it('Returns `with schema` collection', async () => {
assert.ok(json.hasOwnProperty('withSchemaConfig'));
assert.equal(Array.isArray(json.withSchemaConfig), true);
const ids = json.withSchemaConfig.map((item) => item.id);
const publishedDates = json.withSchemaConfig.map((item) => item.data.publishedAt);
assert.deepEqual(ids.sort(), ['four%.md', 'one.md', 'three.md', 'two.md'].sort());
assert.equal(
publishedDates.every((date) => date instanceof Date),
true,
'Not all publishedAt dates are Date objects',
);
assert.deepEqual(
publishedDates.map((date) => date.toISOString()),
[
'2021-01-01T00:00:00.000Z',
'2021-01-01T00:00:00.000Z',
'2021-01-03T00:00:00.000Z',
'2021-01-02T00:00:00.000Z',
],
);
});
it('Returns `with custom slugs` collection', async () => {
assert.ok(json.hasOwnProperty('withSlugConfig'));
assert.equal(Array.isArray(json.withSlugConfig), true);
const slugs = json.withSlugConfig.map((item) => item.slug);
assert.deepEqual(slugs, ['fancy-one', 'excellent-three', 'interesting-two']);
});
it('Returns `with union schema` collection', async () => {
assert.ok(json.hasOwnProperty('withUnionSchema'));
assert.equal(Array.isArray(json.withUnionSchema), true);
const post = json.withUnionSchema.find((item) => item.id === 'post.md');
assert.notEqual(post, undefined);
assert.deepEqual(post.data, {
type: 'post',
title: 'My Post',
description: 'This is my post',
});
const newsletter = json.withUnionSchema.find((item) => item.id === 'newsletter.md');
assert.notEqual(newsletter, undefined);
assert.deepEqual(newsletter.data, {
type: 'newsletter',
subject: 'My Newsletter',
});
});
it('Handles symlinked content', async () => {
assert.ok(json.hasOwnProperty('withSymlinkedContent'));
assert.equal(Array.isArray(json.withSymlinkedContent), true);
const ids = json.withSymlinkedContent.map((item) => item.id);
assert.deepEqual(ids.sort(), ['first.md', 'second.md', 'third.md'].sort());
assert.equal(
json.withSymlinkedContent.find(({ id }) => id === 'first.md').data.title,
'First Blog',
);
});
it('Handles symlinked data', async () => {
assert.ok(json.hasOwnProperty('withSymlinkedData'));
assert.equal(Array.isArray(json.withSymlinkedData), true);
const ids = json.withSymlinkedData.map((item) => item.id);
assert.deepEqual(ids, ['welcome']);
assert.equal(
json.withSymlinkedData[0].data.alt,
'Futuristic landscape with chrome buildings and blue skies',
);
assert.notEqual(json.withSymlinkedData[0].data.src.src, undefined);
});
});
describe('Propagation', () => {
it('Applies styles', async () => {
const html = await fixture.readFile('/propagation/index.html');
const $ = cheerio.load(html);
assert.equal($('style').text().includes('content:"works!"'), true);
});
});
describe('Entry', () => {
let json;
before(async () => {
const rawJson = await fixture.readFile('/entries.json');
json = devalue.parse(rawJson);
});
it('Returns `without config` collection entry', async () => {
assert.ok(json.hasOwnProperty('columbiaWithoutConfig'));
assert.equal(json.columbiaWithoutConfig.id, 'columbia.md');
});
it('Returns `with schema` collection entry', async () => {
assert.ok(json.hasOwnProperty('oneWithSchemaConfig'));
assert.equal(json.oneWithSchemaConfig.id, 'one.md');
assert.equal(json.oneWithSchemaConfig.data.publishedAt instanceof Date, true);
assert.equal(
json.oneWithSchemaConfig.data.publishedAt.toISOString(),
'2021-01-01T00:00:00.000Z',
);
});
it('Returns `with custom slugs` collection entry', async () => {
assert.ok(json.hasOwnProperty('twoWithSlugConfig'));
assert.equal(json.twoWithSlugConfig.slug, 'interesting-two');
});
it('Returns `with union schema` collection entry', async () => {
assert.ok(json.hasOwnProperty('postWithUnionSchema'));
assert.equal(json.postWithUnionSchema.id, 'post.md');
assert.deepEqual(json.postWithUnionSchema.data, {
type: 'post',
title: 'My Post',
description: 'This is my post',
});
});
});
describe('Scripts', () => {
it('Contains all the scripts imported by components', async () => {
const html = await fixture.readFile('/with-scripts/one/index.html');
const $ = cheerio.load(html);
assert.equal($('script').length, 2);
// Read the scripts' content
const scriptsCode = $('script')
.map((_, el) => $(el).text())
.toArray()
.join('\n');
assert.match(scriptsCode, /ScriptCompA/);
assert.match(scriptsCode, /ScriptCompB/);
});
});
});
const blogSlugToContents = {
'first-post': {
title: 'First post',
element: 'blockquote',
content: 'First post loaded: yes!',
},
'second-post': {
title: 'Second post',
element: 'blockquote',
content: 'Second post loaded: yes!',
},
'third-post': {
title: 'Third post',
element: 'blockquote',
content: 'Third post loaded: yes!',
},
'using-mdx': {
title: 'Using MDX',
element: 'a[href="#"]',
content: 'Embedded component in MDX',
},
};
describe('Static paths integration', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/content-static-paths-integration/',
legacy: {
collections: true,
},
});
await fixture.build();
});
it('Generates expected pages', async () => {
for (const slug in blogSlugToContents) {
assert.equal(fixture.pathExists(`/posts/${slug}`), true);
}
});
it('Renders titles', async () => {
for (const slug in blogSlugToContents) {
const post = await fixture.readFile(`/posts/${slug}/index.html`);
const $ = cheerio.load(post);
assert.equal($('h1').text(), blogSlugToContents[slug].title);
}
});
it('Renders content', async () => {
for (const slug in blogSlugToContents) {
const post = await fixture.readFile(`/posts/${slug}/index.html`);
const $ = cheerio.load(post);
assert.equal(
$(blogSlugToContents[slug].element).text().trim(),
blogSlugToContents[slug].content,
);
}
});
});
describe('With spaces in path', () => {
it('Does not throw', async () => {
const fixture = await loadFixture({
root: './fixtures/content with spaces in folder name/',
legacy: {
collections: true,
},
});
let error = null;
try {
await fixture.build({ force: true });
} catch (e) {
error = e.message;
}
assert.equal(error, null);
});
});
describe('With config.mjs', () => {
it("Errors when frontmatter doesn't match schema", async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections-with-config-mjs/',
legacy: {
collections: true,
},
});
let error;
try {
await fixture.build();
} catch (e) {
error = e.message;
}
assert.equal(error.includes('**title**: Expected type `"string"`, received "number"'), true);
});
});
describe('With config.mts', () => {
it("Errors when frontmatter doesn't match schema", async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections-with-config-mts/',
legacy: {
collections: true,
},
});
let error;
try {
await fixture.build();
} catch (e) {
error = e.message;
}
assert.equal(error.includes('**title**: Expected type `"string"`, received "number"'), true);
});
});
describe('With empty markdown file', () => {
it('Throws the right error', async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections-empty-md-file/',
legacy: {
collections: true,
},
});
let error;
try {
await fixture.build();
} catch (e) {
error = e.message;
}
assert.equal(error.includes('**title**: Required'), true);
});
});
describe('With empty collections directory', () => {
it('Handles the empty directory correctly', async () => {
const fixture = await loadFixture({
root: './fixtures/content-collections-empty-dir/',
legacy: {
collections: true,
},
});
let error;
try {
await fixture.build();
} catch (e) {
error = e.message;
}
assert.equal(error, undefined);
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const h1 = $('h1');
assert.equal(h1.text(), 'Entries length: 0');
assert.equal(h1.attr('data-entries'), '[]');
});
});
describe('SSR integration', () => {
let app;
before(async () => {
const fixture = await loadFixture({
root: './fixtures/content-ssr-integration/',
output: 'server',
adapter: testAdapter(),
vite: {
plugins: [preventNodeBuiltinDependencyPlugin()],
},
legacy: {
collections: true,
},
});
await fixture.build();
app = await fixture.loadTestAdapterApp();
});
it('Responds 200 for expected pages', async () => {
for (const slug in blogSlugToContents) {
const request = new Request('http://example.com/posts/' + slug);
const response = await app.render(request);
assert.equal(response.status, 200);
}
});
it('Renders titles', async () => {
for (const slug in blogSlugToContents) {
const request = new Request('http://example.com/posts/' + slug);
const response = await app.render(request);
const body = await response.text();
const $ = cheerio.load(body);
assert.equal($('h1').text(), blogSlugToContents[slug].title);
}
});
it('Renders content', async () => {
for (const slug in blogSlugToContents) {
const request = new Request('http://example.com/posts/' + slug);
const response = await app.render(request);
const body = await response.text();
const $ = cheerio.load(body);
assert.equal(
$(blogSlugToContents[slug].element).text().trim(),
blogSlugToContents[slug].content,
);
}
});
});
describe('Base configuration', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/content-collections-base/',
legacy: {
collections: true,
},
});
await fixture.build();
});
it('Includes base in links', async () => {
const html = await fixture.readFile('/docs/index.html');
const $ = cheerio.load(html);
assert.equal($('link').attr('href').startsWith('/docs'), true);
});
it('Includes base in scripts', async () => {
const html = await fixture.readFile('/docs/index.html');
const $ = cheerio.load(html);
assert.equal($('script').attr('src').startsWith('/docs'), true);
});
});
describe('Mutation', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/content-collections-mutation/',
legacy: {
collections: true,
},
});
await fixture.build();
});
it('Does not mutate cached collection', async () => {
const html = await fixture.readFile('/index.html');
const index = cheerio.load(html)('h2:first').text();
const html2 = await fixture.readFile('/another_page/index.html');
const anotherPage = cheerio.load(html2)('h2:first').text();
assert.equal(index, anotherPage);
});
});
});

Some files were not shown because too many files have changed in this diff Show more