From 582d141a000067aed6ded10779a2842c06020dba Mon Sep 17 00:00:00 2001 From: Yabo Hu Date: Sat, 8 Aug 2026 14:28:37 +1000 Subject: [PATCH] deep merge tspconfig --- packages/typespec-powershell/src/emitter.ts | 33 +--------- .../src/utils/configuration.ts | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 packages/typespec-powershell/src/utils/configuration.ts diff --git a/packages/typespec-powershell/src/emitter.ts b/packages/typespec-powershell/src/emitter.ts index 995e1512bb..226e0dd1fe 100644 --- a/packages/typespec-powershell/src/emitter.ts +++ b/packages/typespec-powershell/src/emitter.ts @@ -1,5 +1,5 @@ import { Program, EmitContext, listServices, emitFile, resolvePath } from "@typespec/compiler"; -import { serialize, deserialize } from '@azure-tools/codegen'; +import { serialize } from '@azure-tools/codegen'; import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; // Following files finally should be imported from @autorest/powershell // import { ModelState } from "./powershell/utils/model-state.js"; @@ -7,37 +7,8 @@ import { createSdkContext } from "@azure-tools/typespec-client-generator-core"; import { getClients } from "./utils/clientUtils.js"; import { transformPwshModel } from "./convertor/convertor.js"; import { PSOptions } from "./types/interfaces.js"; +import { loadConfiguration } from "./utils/configuration.js"; import { generatePwshModule } from "@autorest/powershell"; -import { readFileSync } from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; - -// Function to recursively interpolate placeholders in an object -function interpolatePlaceholders(obj: any, values: any) { - for (const key in obj) { - if (typeof obj[key] === 'string') { - obj[key] = obj[key].replace(/{([^}]+)}/g, (match: string, placeholder: string) => values[placeholder]); - } else if (typeof obj[key] === 'object') { - interpolatePlaceholders(obj[key], values); - } - } -} - -//load configuration from configuration.md -function loadConfiguration(emitterOptions: Record): Record { - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const configPath = path.join(__dirname, '../../configuration.yaml'); - const configuration = deserialize>(readFileSync(configPath, 'utf8'), configPath); - // Define the values for interpolation - const interpolationValues = { - 'module-name': emitterOptions['module-name'] ?? configuration['module-name'], - 'service-name': emitterOptions['service-name'] ?? configuration['service-name'] - }; - interpolatePlaceholders(configuration, interpolationValues); - // If there is overlap between the configuration and the emitter options, the emitter options will take precedence - return { ...configuration, ...emitterOptions }; -} export async function $onEmit(context: EmitContext) { const program: Program = context.program; diff --git a/packages/typespec-powershell/src/utils/configuration.ts b/packages/typespec-powershell/src/utils/configuration.ts new file mode 100644 index 0000000000..fd00fa9e90 --- /dev/null +++ b/packages/typespec-powershell/src/utils/configuration.ts @@ -0,0 +1,60 @@ +import { deserialize } from '@azure-tools/codegen'; +import { readFileSync } from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +function isPlainObject(value: any): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +// Recursively merges source into target. Plain objects merge by key; arrays, +// primitives and null in source replace the target value. Inputs are not mutated. +export function deepMerge(target: any, source: any): any { + if (!isPlainObject(target) || !isPlainObject(source)) { + return source === undefined ? target : source; + } + + const result: Record = { ...target }; + for (const key of Object.keys(source)) { + const sourceValue = source[key]; + if (sourceValue === undefined) { + continue; + } + result[key] = isPlainObject(result[key]) && isPlainObject(sourceValue) + ? deepMerge(result[key], sourceValue) + : sourceValue; + } + return result; +} + +// Recursively interpolate {placeholder} tokens in string values. +export function interpolatePlaceholders(obj: any, values: Record): void { + for (const key in obj) { + if (typeof obj[key] === 'string') { + obj[key] = obj[key].replace(/{([^}]+)}/g, (match: string, placeholder: string) => values[placeholder]); + } else if (obj[key] !== null && typeof obj[key] === 'object') { + interpolatePlaceholders(obj[key], values); + } + } +} + +// Load configuration.yaml and merge emitter options over it, with emitter +// options taking precedence at every level of nested mappings. +export function loadConfiguration(emitterOptions: Record): Record { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const configPath = path.join(__dirname, '../../../configuration.yaml'); + const configuration = deserialize>(readFileSync(configPath, 'utf8'), configPath); + + // Emitter options take precedence over the defaults, merged recursively so + // partial nested mappings (e.g. metadata) keep unspecified defaults. + const merged = deepMerge(configuration, emitterOptions); + + const interpolationValues = { + 'module-name': merged['module-name'], + 'service-name': merged['service-name'] + }; + interpolatePlaceholders(merged, interpolationValues); + + return merged; +}