From f246c55823bd2ceb94acd7c41da7ba307c407fb1 Mon Sep 17 00:00:00 2001 From: guitavano Date: Mon, 22 Jun 2026 23:14:20 -0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(schema):=20support=20array=20output=20t?= =?UTF-8?q?ypes=20in=20loader=E2=86=92section=20block-ref=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loaders returning array types (e.g. Product[]) had their output type resolved as "Array" by getSymbol().getName(), causing all array-returning loaders to collide under the same key. Section properties like `products: Product[] | null` also resolved to "Array", matching the wrong loaders or none at all — rendering as inline array forms instead of loader pickers in the CMS. Now extractLoaderOutputTypeName unwraps array element types and keys them as "Product[]", and typeToJsonSchema falls back to element-type lookup for array/nullable-array properties. Co-Authored-By: Claude Opus 4.6 --- scripts/generate-schema.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/generate-schema.ts b/scripts/generate-schema.ts index 62b3f1ea..6a686797 100644 --- a/scripts/generate-schema.ts +++ b/scripts/generate-schema.ts @@ -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; } @@ -415,9 +423,25 @@ function typeToJsonSchema(type: Type, visited = new Set(), 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: [ From 5c6401892fbdf0d9c441b94cd12934aeb93b8fb0 Mon Sep 17 00:00:00 2001 From: guitavano Date: Tue, 23 Jun 2026 00:05:51 -0300 Subject: [PATCH 2/2] feat(schema): scan app loaders from @decocms/apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a static mapping of CMS keys → inline-loader source files so the schema generator registers app loaders (productList, productListingPage, productDetailsPage) alongside site loaders. Each source file is parsed once and reused across alias keys. The output types feed into outputTypeToLoaderKeys so section block-ref props (e.g. ProductShelf.products, SearchResult.page) and commerce extension wrappers automatically include app loaders in their anyOf. Co-Authored-By: Claude Opus 4.6 --- scripts/generate-schema.ts | 113 +++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/scripts/generate-schema.ts b/scripts/generate-schema.ts index 6a686797..cc9535e5 100644 --- a/scripts/generate-schema.ts +++ b/scripts/generate-schema.ts @@ -838,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(); + + 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 }; // ---------------------------------------------------------------------------