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

Data collections and references #6850

Merged
merged 135 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
335e516
feat: add generated lookup-map
bholmesdev Apr 26, 2023
46b192f
feat: wire up fast getEntryBySlug() lookup
bholmesdev Apr 26, 2023
2383345
fix: consider frontmatter slugs
bholmesdev Apr 26, 2023
e2bae4b
chore: changeset
bholmesdev Apr 26, 2023
ff9a44b
chore: lint no-shadow
bholmesdev Apr 26, 2023
dc7c88b
fix: revert bad rootRelativePath change
bholmesdev Apr 26, 2023
0518c79
chore: better var name
bholmesdev Apr 26, 2023
7695a98
refactor: generated `.json` to in-memory map
bholmesdev Apr 26, 2023
bacab79
chore: removed unneeded await
bholmesdev Apr 27, 2023
1b04242
chore: removed unneeded await
bholmesdev Apr 27, 2023
fea6da5
Revert "chore: removed unneeded await"
bholmesdev Apr 27, 2023
aa1b8ca
fix: bad `GetEntryImport` type
bholmesdev Apr 27, 2023
cde0830
chore: remove unused variable
bholmesdev Apr 27, 2023
5aa65d7
refactor: for -> Promise.all
bholmesdev Apr 27, 2023
6b498f3
refactor: replace duplicate parseSlug
bholmesdev Apr 27, 2023
1a076a7
refactor: add cache layer
bholmesdev Apr 27, 2023
1255449
Revert "refactor: add cache layer"
bholmesdev Apr 27, 2023
a561ed6
feat: json collection POC
bholmesdev Apr 3, 2023
3bcfa2e
wip: add test json file
bholmesdev Apr 3, 2023
c5cf656
wip: playing with api ideas
bholmesdev Apr 5, 2023
7395532
refactor: extract getCollectionName
bholmesdev Apr 7, 2023
aae33f7
feat: add defineDataCollection
bholmesdev Apr 7, 2023
2100ad0
refactor: variable destructure
bholmesdev Apr 7, 2023
bf09eda
wip: basic data entry pipeline
bholmesdev Apr 7, 2023
b68ebf0
chore: revert fixture playing
bholmesdev Apr 7, 2023
38d52bc
wip: basic entry array parser
bholmesdev Apr 7, 2023
3b0f9fc
feat: basic data type gen
bholmesdev Apr 10, 2023
1d1215b
chore: add with-data playground
bholmesdev Apr 10, 2023
3f8c3e6
feat: add error when `defineDataCollection()` isn't used
bholmesdev Apr 10, 2023
74f990c
fix: missing error message
bholmesdev Apr 10, 2023
96ac415
feat: data collections are here!
bholmesdev Apr 10, 2023
24a3a90
wip: play with data query APIs
bholmesdev Apr 10, 2023
7d301b6
feat: reference() util!
bholmesdev Apr 10, 2023
221e273
fix: Markdoc `$entry` variable
bholmesdev Apr 10, 2023
2639bb9
play: add reference util with markdoc
bholmesdev Apr 10, 2023
ec5600c
chore: delete console logs
bholmesdev Apr 10, 2023
d03d18a
feat: `src/data/`!
bholmesdev Apr 11, 2023
8682266
feat: reference() errors
bholmesdev Apr 12, 2023
84323c4
fix: handle hoisted schema parse errors
bholmesdev Apr 12, 2023
2f78268
fix: reload config and invalid on collection changes
bholmesdev Apr 13, 2023
6401362
feat: separate maps for content and data entries
bholmesdev Apr 14, 2023
fa2c387
feat: new `reference()` API that fixes type inference
bholmesdev Apr 14, 2023
62cd27a
feat: support `defineCollection()` for data config
bholmesdev Apr 14, 2023
e64622a
fix: defineCollection `type` inferenenceπinference
bholmesdev Apr 18, 2023
4d815b0
chore: lock
bholmesdev Apr 18, 2023
bd47ee5
feat: getCollection() for everything!
bholmesdev Apr 18, 2023
85b564f
feat: get full entry access from reference()
bholmesdev Apr 18, 2023
9faefcc
chore: changeset
bholmesdev Apr 18, 2023
3eb8c45
wip: type error on acorn?
bholmesdev Apr 18, 2023
d30e0c4
chore: lint
bholmesdev Apr 18, 2023
67b0386
chore: add slugger to data ID processing
bholmesdev Apr 18, 2023
6fbc54f
chore: astro/zod -> zod
bholmesdev Apr 18, 2023
b7929d2
chore: example version
bholmesdev Apr 18, 2023
03fbd41
chore: remove slugifier from data id
bholmesdev Apr 25, 2023
b136af5
chore: remove dead getDataCollection
bholmesdev Apr 27, 2023
38addf1
chore: remove dead defineDataCollection
bholmesdev Apr 27, 2023
54300e0
fix: bad collection import
bholmesdev Apr 27, 2023
11d8304
chore: lock
bholmesdev Apr 27, 2023
638643d
feat: add data collections to lookup map
bholmesdev Apr 28, 2023
7ef9c0f
refactor: stop resolving data from reference
bholmesdev Apr 28, 2023
6a0cb6f
feat: introduce getEntry and new reference()
bholmesdev Apr 28, 2023
ccf5219
fix: update config loader
bholmesdev Apr 28, 2023
0c0fe93
fix: reference() type
bholmesdev Apr 28, 2023
97e2fc9
feat: test self references (they work 🎉)
bholmesdev Apr 28, 2023
bf11a19
fix: use `slug` for content references
bholmesdev Apr 28, 2023
4a4f2e1
fix: bad getEntry content type
bholmesdev Apr 28, 2023
f868fba
chroe: remove console logs
bholmesdev Apr 28, 2023
b1e233d
fix: strict null checks on with-data
bholmesdev Apr 28, 2023
c35093b
feat: add getEntries for ref arrays
bholmesdev Apr 28, 2023
af96dde
chore: fix type hints for reference strings
bholmesdev Apr 28, 2023
07193f9
chore: change to type never for clarity
bholmesdev Apr 28, 2023
f7d8e5a
play: try getEntries
bholmesdev Apr 28, 2023
8ff1ab4
Return to "everything goes in `src/content/`
bholmesdev Apr 28, 2023
225e1df
fix: remove old function
bholmesdev Apr 28, 2023
132446b
chore: update to AstroErrors
bholmesdev Apr 28, 2023
4b510bf
chore: remove unused fixture files
bholmesdev Apr 28, 2023
0d1ff7c
play: names
bholmesdev May 2, 2023
11770e3
deps: js-yaml
bholmesdev May 8, 2023
1205234
feat: data collection YAML with error handling
bholmesdev May 8, 2023
e6ad792
refactor: remove console log
bholmesdev May 8, 2023
2c7f7e4
refactor: code cleanup
bholmesdev May 8, 2023
c661e70
fix: allow mixed content to pass through glob imports
bholmesdev May 8, 2023
bb7b827
chore: move lookupMap util to virtual-mod
bholmesdev May 8, 2023
71978fb
Merge branch 'main' into feat/data-collections
bholmesdev May 9, 2023
474a6fd
refactor: new lookupMap logic, better errors
bholmesdev May 9, 2023
3256851
chore: change MixedContent title
bholmesdev May 9, 2023
faede6d
refactor: remove unneeded try / catch
bholmesdev May 9, 2023
ef13323
fix: use `ws.send` for type gen errors
bholmesdev May 9, 2023
5ea9316
fix: bubble `ws.send` errors from astro sync
bholmesdev May 9, 2023
2772a48
refactor: revert verbose astroContentCollectionEntry
bholmesdev May 15, 2023
70f512d
Merge branch 'main' into feat/data-collections
bholmesdev May 15, 2023
56b562a
fix: bad with-data package name
bholmesdev May 15, 2023
d025ca9
fix: bad virtual mod flag
bholmesdev May 15, 2023
62b96cd
chore: remove with-data playground
bholmesdev May 15, 2023
3d674f1
test: data collection authors
bholmesdev May 15, 2023
b83fe29
test: translations data collection
bholmesdev May 15, 2023
7297a1f
fix: add `.yml` support
bholmesdev May 15, 2023
dc6ea7d
refactor: mix in `.yaml` just for fun
bholmesdev May 15, 2023
bcf30e4
refactor: i18n -> translations
bholmesdev May 15, 2023
2dbb445
chore: content-collection-references fixture
bholmesdev May 15, 2023
4f1ab81
chore: bad lockfile
bholmesdev May 15, 2023
717a5b6
fix: bad ContentLookupMap import
bholmesdev May 15, 2023
2a1815c
chore: revert back to astroContentCollectionEntry
bholmesdev May 15, 2023
4038f26
Merge branch 'main' into feat/data-collections
bholmesdev May 16, 2023
b9456d8
test: collection references
bholmesdev May 16, 2023
6f3c19b
fix: bad error code override
bholmesdev May 16, 2023
2b951b4
chore: remove unused asset
bholmesdev May 16, 2023
c8a4660
test: sync errors
bholmesdev May 16, 2023
91dfd4d
chore: remove stray console log
bholmesdev May 16, 2023
321943e
chore: lock
bholmesdev May 16, 2023
290ac01
chore: revert with-markdoc changes
bholmesdev May 16, 2023
3c415b7
chore: doc error states, remove bad merge code
bholmesdev May 16, 2023
6386ac9
chore: remove bad `as any`
bholmesdev May 16, 2023
6fa410d
chore: lint
bholmesdev May 16, 2023
c468a14
chore: inline ContentLookupMap comments
bholmesdev May 16, 2023
5b95d51
chore: settings -> config
bholmesdev May 16, 2023
5b5eb31
fix: put back `defineCollection()`
bholmesdev May 16, 2023
c35f0b8
fix: entry.slug for get content collection
bholmesdev May 16, 2023
637eca9
chore: update get-entry-type tests
bholmesdev May 16, 2023
65f37f6
docs: totally shorten "missing a `type`"
bholmesdev May 16, 2023
22ca645
docs: truncate share a `schema`
bholmesdev May 16, 2023
7aae335
Merge branch 'main' into feat/data-collections
bholmesdev May 17, 2023
b9d8a47
chore: add `test:unit` and `test:unit:match`to base
bholmesdev May 17, 2023
9c0c418
chore: update changeset
bholmesdev May 17, 2023
087485b
refactor: cleanup runtime types and inline comments
bholmesdev May 17, 2023
39b4740
nit: [0] instead of shift()
bholmesdev May 17, 2023
7aa0f3c
refactor: `getRelativeEntryPath()` util
bholmesdev May 17, 2023
4a8c90c
chore: capitalized Collections for test:match
bholmesdev May 17, 2023
afa743a
nit: ?? viteId on split
bholmesdev May 17, 2023
2cc3d20
nit: separate Params obj
bholmesdev May 17, 2023
26b7976
chore: add try / catch on readFile
bholmesdev May 17, 2023
1e31a4e
nit: `const data`
bholmesdev May 17, 2023
506944f
chore: clean up data collection exceptions
bholmesdev May 17, 2023
81fcc91
nit: `?? ''` for search params
bholmesdev May 17, 2023
c8a715a
chore: remove TODO on hoisted error
bholmesdev May 17, 2023
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
6 changes: 6 additions & 0 deletions .changeset/early-eyes-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': minor
'@astrojs/markdoc': minor
---

Content collections now support data formats including JSON and YAML. You can also create relationships, or references, between collections to pull information from one collection entry into another. Learn more on our [updated Content Collections docs](https://docs.astro.build/en/guides/content-collections/).
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"format:imports": "organize-imports-cli ./packages/*/tsconfig.json ./packages/*/*/tsconfig.json",
"test": "turbo run test --concurrency=1 --filter=astro --filter=create-astro --filter=\"@astrojs/*\"",
"test:match": "cd packages/astro && pnpm run test:match",
"test:unit": "cd packages/astro && pnpm run test:unit",
"test:unit:match": "cd packages/astro && pnpm run test:unit:match",
"test:smoke": "pnpm test:smoke:example && pnpm test:smoke:docs",
"test:smoke:example": "turbo run build --concurrency=100% --filter=\"@example/*\"",
"test:smoke:docs": "turbo run build --filter=docs",
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"html-escaper": "^3.0.3",
"js-yaml": "^4.1.0",
"kleur": "^4.1.4",
"magic-string": "^0.27.0",
"mime": "^3.0.0",
Expand Down Expand Up @@ -181,6 +182,7 @@
"@types/estree": "^0.0.51",
"@types/hast": "^2.3.4",
"@types/html-escaper": "^3.0.0",
"@types/js-yaml": "^4.0.5",
"@types/mime": "^2.0.3",
"@types/mocha": "^9.1.1",
"@types/prettier": "^2.6.3",
Expand Down
25 changes: 23 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,12 +1249,22 @@ export type ContentEntryModule = {
};
};

export type DataEntryModule = {
id: string;
collection: string;
data: Record<string, unknown>;
_internal: {
rawData: string;
filePath: string;
};
};

export interface ContentEntryType {
extensions: string[];
getEntryInfo(params: {
fileUrl: URL;
contents: string;
}): GetEntryInfoReturnType | Promise<GetEntryInfoReturnType>;
}): GetContentEntryInfoReturnType | Promise<GetContentEntryInfoReturnType>;
getRenderModule?(
this: rollup.PluginContext,
params: {
Expand All @@ -1265,7 +1275,7 @@ export interface ContentEntryType {
contentModuleTypes?: string;
}

type GetEntryInfoReturnType = {
type GetContentEntryInfoReturnType = {
data: Record<string, unknown>;
/**
* Used for error hints to point to correct line and location
Expand All @@ -1277,12 +1287,23 @@ type GetEntryInfoReturnType = {
slug: string;
};

export interface DataEntryType {
extensions: string[];
getEntryInfo(params: {
fileUrl: URL;
contents: string;
}): GetDataEntryInfoReturnType | Promise<GetDataEntryInfoReturnType>;
}

export type GetDataEntryInfoReturnType = { data: Record<string, unknown>; rawData?: string };

export interface AstroSettings {
config: AstroConfig;
adapter: AstroAdapter | undefined;
injectedRoutes: InjectedRoute[];
pageExtensions: string[];
contentEntryTypes: ContentEntryType[];
dataEntryTypes: DataEntryType[];
renderers: AstroRenderer[];
scripts: {
stage: InjectedScriptStage;
Expand Down
5 changes: 4 additions & 1 deletion packages/astro/src/content/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets';
export const CONTENT_FLAG = 'astroContent';
export const CONTENT_FLAG = 'astroContentCollectionEntry';
export const DATA_FLAG = 'astroDataCollectionEntry';
export const CONTENT_FLAGS = [CONTENT_FLAG, DATA_FLAG, PROPAGATED_ASSET_FLAG] as const;

export const VIRTUAL_MODULE_ID = 'astro:content';
export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@';
export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@';
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/content/runtime-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { AstroSettings } from '../@types/astro.js';
import { emitESMImage } from '../assets/index.js';

export function createImage(
settings: AstroSettings,
settings: Pick<AstroSettings, 'config'>,
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved
pluginContext: PluginContext,
entryFilePath: string
) {
Expand Down
210 changes: 193 additions & 17 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { prependForwardSlash } from '../core/path.js';

import { ZodIssueCode, string as zodString, type z } from 'zod';
import {
createComponent,
createHeadAndContent,
Expand All @@ -9,7 +9,10 @@ import {
renderTemplate,
renderUniqueStylesheet,
unescapeHTML,
type AstroComponentFactory,
} from '../runtime/server/index.js';
import type { ContentLookupMap } from './utils.js';
import type { MarkdownHeading } from '@astrojs/markdown-remark';

type LazyImport = () => Promise<any>;
type GlobResult = Record<string, LazyImport>;
Expand Down Expand Up @@ -37,14 +40,31 @@ export function createCollectionToGlobResultMap({

const cacheEntriesByCollection = new Map<string, any[]>();
export function createGetCollection({
collectionToEntryMap,
contentCollectionToEntryMap,
dataCollectionToEntryMap,
getRenderEntryImport,
}: {
collectionToEntryMap: CollectionToEntryMap;
contentCollectionToEntryMap: CollectionToEntryMap;
dataCollectionToEntryMap: CollectionToEntryMap;
getRenderEntryImport: GetEntryImport;
}) {
return async function getCollection(collection: string, filter?: (entry: any) => unknown) {
const lazyImports = Object.values(collectionToEntryMap[collection] ?? {});
let type: 'content' | 'data';
if (collection in contentCollectionToEntryMap) {
type = 'content';
} else if (collection in dataCollectionToEntryMap) {
type = 'data';
} else {
throw new AstroError({
...AstroErrorData.CollectionDoesNotExistError,
message: AstroErrorData.CollectionDoesNotExistError.message(collection),
});
}
const lazyImports = Object.values(
type === 'content'
? contentCollectionToEntryMap[collection]
: dataCollectionToEntryMap[collection]
);
let entries: any[] = [];
// Cache `getCollection()` calls in production only
// prevents stale cache in development
Expand All @@ -54,20 +74,26 @@ export function createGetCollection({
entries = await Promise.all(
lazyImports.map(async (lazyImport) => {
const entry = await lazyImport();
return {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
return type === 'content'
? {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
collection: entry.collection,
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, entry.slug),
});
},
}
: {
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, entry.slug),
});
},
};
collection: entry.collection,
data: entry.data,
};
})
);
cacheEntriesByCollection.set(collection, entries);
Expand Down Expand Up @@ -110,6 +136,121 @@ export function createGetEntryBySlug({
};
}

export function createGetDataEntryById({
dataCollectionToEntryMap,
}: {
dataCollectionToEntryMap: CollectionToEntryMap;
}) {
return async function getDataEntryById(collection: string, id: string) {
const lazyImport =
dataCollectionToEntryMap[collection]?.[/*TODO: filePathToIdMap*/ id + '.json'];

// TODO: AstroError
if (!lazyImport) throw new Error(`Entry ${collection} → ${id} was not found.`);
const entry = await lazyImport();

return {
id: entry.id,
collection: entry.collection,
data: entry.data,
};
};
}

type ContentEntryResult = {
id: string;
slug: string;
body: string;
collection: string;
data: Record<string, any>;
render(): Promise<RenderResult>;
};

type DataEntryResult = {
id: string;
collection: string;
data: Record<string, any>;
};

type EntryLookupObject = { collection: string; id: string } | { collection: string; slug: string };

export function createGetEntry({
getEntryImport,
getRenderEntryImport,
}: {
getEntryImport: GetEntryImport;
getRenderEntryImport: GetEntryImport;
}) {
return async function getEntry(
// Can either pass collection and identifier as 2 positional args,
// Or pass a single object with the collection and identifier as properties.
// This means the first positional arg can have different shapes.
collectionOrLookupObject: string | EntryLookupObject,
_lookupId?: string
): Promise<ContentEntryResult | DataEntryResult | undefined> {
let collection: string, lookupId: string;
if (typeof collectionOrLookupObject === 'string') {
collection = collectionOrLookupObject;
if (!_lookupId)
throw new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: '`getEntry()` requires an entry identifier as the second argument.',
});
lookupId = _lookupId;
} else {
collection = collectionOrLookupObject.collection;
// Identifier could be `slug` for content entries, or `id` for data entries
lookupId =
'id' in collectionOrLookupObject
? collectionOrLookupObject.id
: collectionOrLookupObject.slug;
}

const entryImport = await getEntryImport(collection, lookupId);
if (typeof entryImport !== 'function') return undefined;

const entry = await entryImport();

if (entry._internal.type === 'content') {
return {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
collection: entry.collection,
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, lookupId),
});
},
};
} else if (entry._internal.type === 'data') {
return {
id: entry.id,
collection: entry.collection,
data: entry.data,
};
}
return undefined;
};
}

export function createGetEntries(getEntry: ReturnType<typeof createGetEntry>) {
return async function getEntries(
entries: { collection: string; id: string }[] | { collection: string; slug: string }[]
) {
return Promise.all(entries.map((e) => getEntry(e)));
};
}

type RenderResult = {
Content: AstroComponentFactory;
headings: MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
};

async function render({
collection,
id,
Expand All @@ -118,7 +259,7 @@ async function render({
collection: string;
id: string;
renderEntryImport?: LazyImport;
}) {
}): Promise<RenderResult> {
const UnexpectedRenderError = new AstroError({
...AstroErrorData.UnknownContentCollectionError,
message: `Unexpected error while rendering ${String(collection)} → ${String(id)}.`,
Expand Down Expand Up @@ -186,3 +327,38 @@ async function render({
remarkPluginFrontmatter: mod.frontmatter ?? {},
};
}

export function createReference({ lookupMap }: { lookupMap: ContentLookupMap }) {
return function reference(collection: string) {
return zodString().transform((lookupId: string, ctx) => {
const flattenedErrorPath = ctx.path.join('.');
if (!lookupMap[collection]) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`,
});
return;
}

const { type, entries } = lookupMap[collection];
const entry = entries[lookupId];

if (!entry) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Expected ${Object.keys(
entries
)
.map((c) => JSON.stringify(c))
.join(' | ')}. Received ${JSON.stringify(lookupId)}.`,
});
return;
}
// Content is still identified by slugs, so map to a `slug` key for consistency.
if (type === 'content') {
return { slug: lookupId, collection };
}
return { id: lookupId, collection };
});
};
}
Loading