Skip to content
Open
Changes from all commits
Commits
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
139 changes: 138 additions & 1 deletion scripts/generate-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ function extractLoaderOutputTypeName(sourceFile: SourceFile): string | null {
const nonNull = ret.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
if (nonNull.length === 1) ret = nonNull[0];
}
// Unwrap array element type — Product[] → "Product[]" (keyed as array)
if (ret.isArray()) {
const elType = ret.getArrayElementType();
if (elType) {
const elName = elType.getSymbol()?.getName() ?? elType.getAliasSymbol()?.getName() ?? null;
if (elName) return `${elName}[]`;
}
}
return ret.getSymbol()?.getName() ?? ret.getAliasSymbol()?.getName() ?? null;
}

Expand Down Expand Up @@ -415,9 +423,25 @@ function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?: Generat
if (ctx?.outputTypeToLoaderKeys) {
const typeSym = propType.getSymbol() ?? propType.getAliasSymbol();
const outputTypeName = typeSym?.getName() ?? baseHint;
const matchingLoaders =
let matchingLoaders =
ctx.outputTypeToLoaderKeys.get(outputTypeName) ??
(outputTypeName !== baseHint ? ctx.outputTypeToLoaderKeys.get(baseHint) : undefined);
// If no match yet and the prop type is an array, try element type name + "[]"
if (!matchingLoaders?.length) {
let arrayElementType = propType.isArray() ? propType.getArrayElementType() : null;
if (!arrayElementType && propType.isUnion()) {
const nonNull = propType.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
if (nonNull.length === 1 && nonNull[0].isArray()) {
arrayElementType = nonNull[0].getArrayElementType();
}
}
if (arrayElementType) {
const elName = arrayElementType.getSymbol()?.getName() ?? arrayElementType.getAliasSymbol()?.getName();
if (elName) {
matchingLoaders = ctx.outputTypeToLoaderKeys.get(`${elName}[]`);
}
}
}
if (matchingLoaders?.length) {
const blockRefSchema: any = {
anyOf: [
Expand Down Expand Up @@ -814,6 +838,119 @@ function generateMeta(): MetaResponse {
}
}

// ---------------------------------------------------------------------------
// App loaders pass: scan @decocms/apps inline-loaders and register them
// under CMS keys that match the runtime routing (e.g.
// "vtex/loaders/intelligentSearch/productList.ts").
// ---------------------------------------------------------------------------
const APP_LOADERS: Array<{
cmsKey: string;
sourceFile: string;
namespace: string;
}> = [
// Intelligent Search loaders
{ cmsKey: "vtex/loaders/intelligentSearch/productList.ts",
sourceFile: "vtex/inline-loaders/productListShelf.ts",
namespace: "vtex" },
{ cmsKey: "vtex/loaders/intelligentSearch/productListingPage.ts",
sourceFile: "vtex/inline-loaders/productListingPage.ts",
namespace: "vtex" },
{ cmsKey: "vtex/loaders/intelligentSearch/productDetailsPage.ts",
sourceFile: "vtex/inline-loaders/productDetailsPage.ts",
namespace: "vtex" },
// Legacy aliases (same source files)
{ cmsKey: "vtex/loaders/legacy/productList.ts",
sourceFile: "vtex/inline-loaders/productListShelf.ts",
namespace: "vtex" },
{ cmsKey: "vtex/loaders/legacy/productDetailsPage.ts",
sourceFile: "vtex/inline-loaders/productDetailsPage.ts",
namespace: "vtex" },
// Top-level aliases
{ cmsKey: "vtex/loaders/ProductList.ts",
sourceFile: "vtex/inline-loaders/productListShelf.ts",
namespace: "vtex" },
{ cmsKey: "vtex/loaders/ProductListingPage.ts",
sourceFile: "vtex/inline-loaders/productListingPage.ts",
namespace: "vtex" },
{ cmsKey: "vtex/loaders/ProductDetailsPage.ts",
sourceFile: "vtex/inline-loaders/productDetailsPage.ts",
namespace: "vtex" },
];

// De-duplicate: parse each source file once and reuse schema + output type
const appLoaderCache = new Map<string, {
propsSchema: any;
outputTypeName: string | null;
}>();

console.log(`Scanning app loaders from @decocms/apps...`);
for (const appLoader of APP_LOADERS) {
const absSourceFile = path.resolve(root, "node_modules/@decocms/apps", appLoader.sourceFile);
if (!fs.existsSync(absSourceFile)) {
console.warn(` ✗ app loader ${appLoader.cmsKey}: source not found (${appLoader.sourceFile})`);
continue;
}

try {
let cached = appLoaderCache.get(absSourceFile);
if (!cached) {
const sourceFile = getSourceFile(project, absSourceFile, sourceFileCache);

// Extract Props (input schema)
let propsSchema: any = null;
const propsInterface = sourceFile.getInterface("Props");
if (propsInterface) propsSchema = typeToJsonSchema(propsInterface.getType());

const propsTypeAlias = sourceFile.getTypeAlias("Props");
if (!propsSchema && propsTypeAlias) propsSchema = typeToJsonSchema(propsTypeAlias.getType());

if (!propsSchema) {
const localPropsType = extractDefaultExportPropsType(sourceFile);
if (localPropsType) propsSchema = typeToJsonSchema(localPropsType);
}

if (!propsSchema) propsSchema = { type: "object", properties: {} };

const outputTypeName = extractLoaderOutputTypeName(sourceFile);
cached = { propsSchema, outputTypeName };
appLoaderCache.set(absSourceFile, cached);
}

const { propsSchema, outputTypeName } = cached;
const loaderDefKey = toBase64(appLoader.cmsKey);

definitions[loaderDefKey] = {
title: appLoader.cmsKey,
type: "object",
required: ["__resolveType", ...(propsSchema?.required || [])],
properties: {
__resolveType: { type: "string", enum: [appLoader.cmsKey], default: appLoader.cmsKey },
...(propsSchema?.properties || {}),
},
};

loaderBlocks[appLoader.cmsKey] = {
$ref: `#/definitions/${loaderDefKey}`,
namespace: appLoader.namespace,
};
loaderRootAnyOf.push({ $ref: `#/definitions/${loaderDefKey}` });

// Register output type for block-ref resolution in sections
if (outputTypeName) {
const existing = outputTypeToLoaderKeys.get(outputTypeName) ?? [];
existing.push(appLoader.cmsKey);
outputTypeToLoaderKeys.set(outputTypeName, existing);
}

const propCount = Object.keys(propsSchema.properties || {}).length;
console.log(
` ✓ app loader ${appLoader.cmsKey} (${propCount} props${outputTypeName ? ` → ${outputTypeName}` : ""})`,
);
} catch (e) {
console.warn(` ✗ app loader ${appLoader.cmsKey}: ${(e as Error).message}`);
}
}

const ctx: GenerationContext = { outputTypeToLoaderKeys };

// ---------------------------------------------------------------------------
Expand Down