Skip to content

Commit eadc9dd

Browse files
authored
Fix file() loader JSON schema (#14221)
1 parent 77b18fb commit eadc9dd

9 files changed

Lines changed: 98 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes JSON schema support for content collections using the `file()` loader

packages/astro/src/content/loaders/file.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export function file(fileName: string, options?: FileOptions): Loader {
9090
logger.debug(`Found object with ${entries.length} entries in ${fileName}`);
9191
store.clear();
9292
for (const [id, rawItem] of entries) {
93+
if (id === '$schema' && typeof rawItem === 'string') {
94+
// Ignore JSON schema field.
95+
continue;
96+
}
9397
const parsedData = await parseData({ id, data: rawItem, filePath });
9498
store.set({ id, data: parsedData, filePath: normalizedFilePath });
9599
}

packages/astro/src/content/types-generator.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,11 +643,25 @@ async function generateJSONSchema(
643643
zodSchemaForJson = await getContentLayerSchema(collectionConfig, collectionKey);
644644
}
645645

646+
// The `file()` loader uses a schema which applies to every item in the file rather than a schema
647+
// for the whole file. We special case this to provide the correct JSON schema to users.
648+
// TODO: it would be nice if loaders could indicate this behavior so it wasn’t unique to the built-in loader.
649+
if (
650+
collectionConfig.type === CONTENT_LAYER_TYPE &&
651+
collectionConfig.loader.name === 'file-loader'
652+
) {
653+
// `file()` supports arrays of items, but you can’t set `$schema` when using a top-level array,
654+
// so we’re only handling the object case.
655+
// We use `z.object()` instead of `z.record()` for compatibility with the next `if` statement.
656+
zodSchemaForJson = z.object({}).catchall(zodSchemaForJson);
657+
}
658+
646659
if (zodSchemaForJson instanceof z.ZodObject) {
647660
zodSchemaForJson = zodSchemaForJson.extend({
648661
$schema: z.string().optional(),
649662
});
650663
}
664+
651665
try {
652666
await fsMod.promises.writeFile(
653667
new URL(`./${collectionKey.replace(/"/g, '')}.schema.json`, collectionSchemasDir),

packages/astro/test/content-intellisense.test.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,38 @@ describe('Content Intellisense', () => {
1212
/** @type {{collections: {hasSchema: boolean, name: string}[], entries: Record<string, string>}} */
1313
let manifest = undefined;
1414

15+
/** @type {Record<string, Array<{ id: string; data: any; filePath: string; collection: string }>>} */
16+
let collections;
17+
1518
before(async () => {
1619
fixture = await loadFixture({ root: './fixtures/content-intellisense/' });
1720
await fixture.build();
1821

1922
collectionsDir = await fixture.readdir('../.astro/collections');
2023
manifest = JSON.parse(await fixture.readFile('../.astro/collections/collections.json'));
24+
collections = JSON.parse(await fixture.readFile('index.json'));
2125
});
2226

2327
it('generate JSON schemas for content collections', async () => {
24-
assert.deepEqual(collectionsDir.includes('blog-cc.schema.json'), true);
28+
assert.equal(collectionsDir.includes('blog-cc.schema.json'), true);
2529
});
2630

2731
it('generate JSON schemas for content layer', async () => {
28-
assert.deepEqual(collectionsDir.includes('blog-cl.schema.json'), true);
32+
assert.equal(collectionsDir.includes('blog-cl.schema.json'), true);
33+
});
34+
35+
it('generate JSON schemas for file loader', async () => {
36+
assert.equal(collectionsDir.includes('data-cl.schema.json'), true);
37+
});
38+
39+
it('generates a record JSON schema for the file loader', async () => {
40+
const schema = JSON.parse(await fixture.readFile('../.astro/collections/data-cl.schema.json'));
41+
assert.equal(schema.definitions['data-cl'].type, 'object');
42+
assert.equal(schema.definitions['data-cl'].additionalProperties.type, 'object');
43+
assert.deepEqual(schema.definitions['data-cl'].additionalProperties.properties, {
44+
name: { type: 'string' },
45+
color: { type: 'string' },
46+
});
2947
});
3048

3149
it('manifest exists', async () => {
@@ -76,4 +94,15 @@ describe('Content Intellisense', () => {
7694
"Expected 3 entries for 'blog-cl' collection to have 'blog-cl' as collection name",
7795
);
7896
});
97+
98+
it('doesn’t generate a `$schema` entry for file loader if `$schema` value is a string', async () => {
99+
assert.equal(collections['data-cl-json'].map((entry) => entry.id).includes('$schema'), false);
100+
});
101+
102+
it('generates a `$schema` entry for file loader if `$schema` value isn’t a string', async () => {
103+
assert.equal(
104+
collections['data-schema-misuse'].map((entry) => entry.id).includes('$schema'),
105+
true,
106+
);
107+
});
79108
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"$schema": {
3+
"value": 1
4+
}
5+
}

packages/astro/test/fixtures/content-intellisense/src/content.config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { glob } from 'astro/loaders';
1+
import { glob, file } from 'astro/loaders';
22
import { defineCollection, z } from 'astro:content';
33

44
const blogCC = defineCollection({
@@ -18,7 +18,18 @@ const blogCL = defineCollection({
1818
}),
1919
});
2020

21+
const dataSchema = z.object({ name: z.string(), color: z.string() });
22+
const dataYML = defineCollection({ loader: file('src/data-cl.yml'), schema: dataSchema });
23+
const dataJSON = defineCollection({ loader: file('src/data-cl.json'), schema: dataSchema });
24+
const dataWithSchemaMisuse = defineCollection({
25+
loader: file('src/$schema-misuse.json'),
26+
schema: z.object({ value: z.number() }),
27+
});
28+
2129
export const collections = {
2230
"blog-cc": blogCC,
2331
"blog-cl": blogCL,
32+
"data-cl": dataYML,
33+
"data-cl-json": dataJSON,
34+
"data-schema-misuse": dataWithSchemaMisuse,
2435
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "../.astro/collections/data-cl-json.schema.json",
3+
"starlight": {
4+
"name": "Starlight",
5+
"color": "golden"
6+
},
7+
"astro": {
8+
"name": "Astro flame",
9+
"color": "red"
10+
}
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- id: starlight
2+
name: Starlight
3+
color: golden
4+
- id: astro
5+
name: Astro flame
6+
color: red
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { getCollection } from 'astro:content';
2+
3+
// Generates a JSON file containing content collection data for testing.
4+
export const GET = async () => {
5+
return new Response(JSON.stringify({
6+
'data-cl': await getCollection('data-cl'),
7+
'data-cl-json': await getCollection('data-cl-json'),
8+
'data-schema-misuse': await getCollection('data-schema-misuse'),
9+
}));
10+
};

0 commit comments

Comments
 (0)