Files
vscode-front-matter/ssg-scripts/astro.collections.mjs

235 lines
6.1 KiB
JavaScript

import { writeFileSync } from 'fs';
import { join } from 'path';
import { createServer } from 'vite';
import zod from 'astro/zod';
const { ZodDefault, ZodObject, ZodOptional, ZodString, ZodEffects, ZodEnum, ZodUnion } = zod;
/**
* Process the Zod field
* @param {ZodTypeAny} field
* @param {boolean} isOptional
* @param {string} defaultValue
* @returns
*/
function getField(field, isOptional = false, defaultValue = undefined) {
// Handle various type transformations and assignments
if (field instanceof ZodOptional) {
isOptional = true;
const type = field.unwrap();
return getField(type, isOptional, defaultValue);
}
if (field instanceof ZodEffects) {
const type = field.sourceType();
return getField(type, isOptional, defaultValue);
}
if (field instanceof ZodUnion) {
const types = field._def.options;
const type = types[0];
return getField(type, isOptional, defaultValue);
}
if (field instanceof ZodDefault) {
// https://github.com/colinhacks/zod/blob/master/README.md#default
isOptional = true;
defaultValue = field.parse(undefined);
// https://github.com/sachinraja/zod-to-ts/blob/main/src/index.ts
const type = field._def.innerType;
return getField(type, isOptional, defaultValue);
}
return {
type: field,
isOptional,
defaultValue
};
}
/**
* Generate the field information
* @param {string} name
* @param {ZodTypeAny} type
* @returns
*/
function generateFieldInfo(name, type) {
let description = type.description;
let defaultValue = undefined;
const {
type: fieldType,
isOptional: isFieldOptional,
defaultValue: fieldDefaultValue
} = getField(type, false, defaultValue);
const fieldInfo = {
name: name,
description: description,
defaultValue: fieldDefaultValue,
type: fieldType._def.typeName,
required: !isFieldOptional
};
if (fieldType instanceof ZodObject) {
const subFields = extractFieldInfoFromShape(fieldType);
fieldInfo.fields = subFields;
}
if (fieldType instanceof ZodEffects) {
fieldInfo.type = fieldType.sourceType().fmFieldType;
}
if (fieldInfo.name.toLowerCase().includes('image')) {
fieldInfo.type = 'image';
}
if (fieldType instanceof ZodEnum) {
fieldInfo.options = fieldType.options;
}
if (fieldType instanceof ZodString) {
// https://github.com/StefanTerdell/zod-to-json-schema/blob/master/src/parsers/string.ts#L45
if (fieldType._def.checks && fieldType._def.checks.length > 0) {
const check = fieldType._def.checks.pop();
fieldInfo.type = check.kind;
}
}
return fieldInfo;
}
/**
* Parse the scheme into an array of fields
* @param {ZodTypeAny} type
* @returns
*/
function extractFieldInfoFromShape(type) {
if (type instanceof ZodOptional) {
type = type.unwrap();
}
if (!(type instanceof ZodObject)) {
// Return an empty array if the type is not of the expected type
return [];
}
// Iterate through the shape properties
// https://github.com/sachinraja/zod-to-ts/blob/1389b33557bcca8a02da66cd5c48efbe7579720c/src/index.ts#L134
const properties = Object.entries(type._def.shape());
const fieldInfoList = properties.map(([fieldName, fieldShape]) => {
return generateFieldInfo(fieldName, fieldShape);
});
return fieldInfoList;
}
/**
* Process each content collection
* @param {*} collections
* @returns
*/
function processCollection(collections) {
if (!Array.isArray(collections)) {
return [];
}
return collections.map(([name, collection]) => {
const schema =
typeof collection.schema === 'function'
? collection.schema({
image() {
const field = zod.string();
field.fmFieldType = 'image';
return field;
}
})
: collection.schema;
return {
name,
type: collection.type || 'content',
fields: extractFieldInfoFromShape(schema)
};
});
}
/**
* More info: https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention
* @returns
*/
const astroContentModulePlugin = () => {
const astroContent = 'astro:content';
const astroContentMarker = `\0${astroContent}`;
return {
name: 'astro-content-collections',
resolveId(importee) {
if (importee === astroContent) {
return astroContentMarker;
}
},
load(id) {
if (id === astroContentMarker) {
// https://github.com/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/content-module.template.mjs#L12
return `
export { z } from 'astro/zod';
// Reference: /node_modules/astro/content-module.template.mjs
export function defineCollection(config) {
if (!config.type) config.type = 'content';
return config;
};
`;
}
}
};
};
/**
* Start of the Astro Collections script
*/
(async () => {
const configPath = process.argv[2];
if (!configPath || typeof configPath !== 'string') {
console.log('No config path provided');
process.exit(1);
}
// https://vitejs.dev/guide/ssr.html#setting-up-the-dev-server
// https://github.com/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/src/core/config/vite-load.ts#L8
const server = await createServer({
server: {
middlewareMode: true,
hmr: false
},
optimizeDeps: {
disabled: true
},
appType: 'custom',
plugins: [astroContentModulePlugin()]
});
try {
// https://github.dev/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/src/content/utils.ts#L334
const contentModule = await server.ssrLoadModule(configPath);
const collections = Object.entries(contentModule.collections ?? {});
const processedCollections = processCollection(collections);
writeFileSync(
join(process.cwd(), `./.frontmatter/temp/astro.collections.json`),
JSON.stringify(processedCollections, null, 2)
);
console.log('Collections generated successfully');
process.exit(0);
} catch (error) {
console.log(error.message);
process.exit(1);
} finally {
await server.close();
}
})();