packages/ros-cdk-codegen/lib/resource-codegen.ts (608 lines of code) (raw):

import { schema } from '@alicloud/ros-cdk-spec'; import { CodeMaker } from 'codemaker'; import * as genspec from './genspec'; import { itemTypeNames, scalarTypeNames, SpecName } from './spec-utils'; import { upcaseFirst } from './util'; import * as fs from "fs-extra"; import * as path from "path"; import * as util from "./util"; const CORE = genspec.CORE_NAMESPACE; const RESOURCE_CLASS_PREFIX = genspec.RESOURCE_CLASS_PREFIX; const RESOURCE_BASE_CLASS = `${CORE}.Resource`; // base class for all resources const CONSTRUCT_CLASS = `${CORE}.Construct`; const TAG_MANAGER = `${CORE}.TagManager`; interface Dictionary<T> { [key: string]: T; } export interface ResourceCodeGeneratorOptions { /** * How to import the core library. * * @default '@alicloud/ros-cdk-core' */ readonly coreImport?: string; } /** * Emits classes for all resource types */ export default class ResourceCodeGenerator { public readonly outputFile: string; private code = new CodeMaker(); /** * Creates the code generator. * @param moduleName the name of the module (used to determine the file name). * @param spec resource specification */ constructor( moduleName: string, private readonly spec: schema.Specification, private readonly affix: string, options: ResourceCodeGeneratorOptions = {}, ) { this.outputFile = `${moduleName}`; this.code.openFile(this.outputFile); const coreImport = options.coreImport ?? '@alicloud/ros-cdk-core'; this.code.line(`import * as ${CORE} from '${coreImport}';`); } public async emitCode(resourceName: string) { // SpecName { module: 'ALIYUN::ECS', resourceName: 'VPC' } // CodeName { // packageName: 'ecs', // namespace: '', // className: 'VPC', // specName: SpecName { module: 'ALIYUN::ECS', resourceName: 'VPC' }, // methodName: undefined // } const resourceSpec = this.spec.ResourceTypes[resourceName]; const rosName = SpecName.parse(resourceName); const rosReourceName = genspec.CodeName.forRosResource(rosName, this.affix); const resourceCodeName = genspec.CodeName.forResource(rosName, this.affix); if (resourceCodeName.packageName === 'eip' || resourceCodeName.packageName === 'eipanycast') { // 处理DATASOURCE::EIP::Addresses 这个原属于VPC的特殊资源 this.code.line( `import { ${RESOURCE_CLASS_PREFIX}${rosName.resourceName} } from './vpc.generated';`, ); } else { this.code.line( `import { ${RESOURCE_CLASS_PREFIX}${rosName.resourceName} } from './${resourceCodeName.packageName}.generated';`, ); } this.code.line('// Generated from the AliCloud ROS Resource Specification'); this.code.line(`export { ${RESOURCE_CLASS_PREFIX}${rosName.resourceName} as ${rosName.resourceName}Property };`); this.code.line(); this.emitResourceType(resourceCodeName, rosReourceName, resourceSpec); } /** * Saves the generated file. */ public async save(dir: string): Promise<string[]> { this.code.closeFile(this.outputFile); return await this.code.save(dir); } private openClass(name: genspec.CodeName, superClasses?: string, superInterfaces?: string): string { const extendsPostfix = superClasses ? ` extends ${superClasses}` : ''; const implementsPostfix = superInterfaces ? ` implements ${superInterfaces}` : ''; // handle special case whose letters are all capital, like VPC // in jsii type name must be CamelCased. let className: string = name.className; if(!/[a-z]/.test(className)) { let lowClassName: string = className.toLowerCase(); className = lowClassName.charAt(0).toUpperCase() + lowClassName.substring(1); } this.code.openBlock(`export class ${className}${extendsPostfix}${implementsPostfix}`); return name.className; } private emitPropsType( resourceContext: genspec.CodeName, rosResourceContext: genspec.CodeName, spec: schema.ResourceType, ): genspec.CodeName | undefined { if (!spec.Properties) { return; } const name = genspec.CodeName.forResourceProperties(resourceContext); this.docLink(spec.Documentation, `Properties for defining a \`${resourceContext.className}\`. `); this.code.openBlock(`export interface ${name.className}`); this.emitPropsTypeProperties(rosResourceContext, spec.Properties, Container.Interface); this.code.closeBlock(); return name; } /** * Emit TypeScript for each of the properties, while renaming * * Return a mapping of { originalName -> newName }. */ private emitPropsTypeProperties( resource: genspec.CodeName, propertiesSpec: { [name: string]: schema.Property }, container: Container, ): Dictionary<string> { const propertyMap: Dictionary<string> = {}; Object.keys(propertiesSpec) .sort(propertyComparator) .forEach((propName) => { this.code.line(); const propSpec = propertiesSpec[propName]; const additionalDocs = resource.specName!.relativeName(propName).fqn; propertyMap[propName] = this.emitProperty( { context: resource, propName, spec: propSpec, additionalDocs: quoteCode(additionalDocs), }, container, ); }); return propertyMap; /** * A comparator that places required properties before optional properties, * and sorts properties alphabetically. * @param l the left property name. * @param r the right property name. */ function propertyComparator(l: string, r: string): number { const lp = propertiesSpec[l]; const rp = propertiesSpec[r]; if (lp.Required === rp.Required) { return l.localeCompare(r); } else if (lp.Required) { return -1; } return 1; } } private emitResourceInterface( resourceContext: genspec.CodeName, propsType: genspec.CodeName | undefined, spec: schema.ResourceType, ): genspec.CodeName { const name = genspec.CodeName.forResourceInterface(resourceContext); this.docLink(undefined, `Represents a \`${resourceContext.className}\`. `); this.code.openBlock(`export interface ${name.className} extends ros.IResource`); if (propsType) { this.code.line(`readonly props: ${propsType.className};`); } if (spec.Attributes) { for (const attributeName of Object.keys(spec.Attributes).sort()) { if ( !(attributeName[0] >= 'a' && attributeName[0] <= 'z') && !(attributeName[0] >= 'A' && attributeName[0] <= 'Z') ) continue; const attributeSpec = spec.Attributes![attributeName]; this.code.line(); this.docLink(undefined, `Attribute ${attributeName}: ${(attributeSpec as schema.Description).Description}`); const attr = genspec.attributeDefinition(attributeName); this.code.line(`readonly ${attr.propertyName}: ${CORE}.IResolvable | string;`); } } this.code.closeBlock(); return name; } private emitResourceType( resourceName: genspec.CodeName, rosResourceName: genspec.CodeName, spec: schema.ResourceType, ): void { this.beginNamespace(resourceName); const rosName = resourceName.specName!.fqn; // // Props Bag for this Resource // const propsType = this.emitPropsType(resourceName, rosResourceName, spec); if (propsType) { this.code.line(); } // // Interface for this Resource // const interfaceName = this.emitResourceInterface(resourceName, propsType, spec); // // The class declaration representing this Resource // let specificDescription = `This class encapsulates and extends the ROS resource type \`${rosName}\`.`; if (spec.Description) { specificDescription = `${specificDescription.replace('.', spec.Description.replace(rosName, ', which'))}`; } const note = `@Note This class may have some new functions to facilitate development, so it is recommended to use this class instead of \`Ros${resourceName.className}\`for a more convenient development experience.`; this.docLink(spec.Documentation, specificDescription, note); this.openClass(resourceName, RESOURCE_BASE_CLASS, interfaceName.className); this.code.line(`protected scope: ros.Construct;`); this.code.line(`protected id: string;`); if (propsType) { this.code.line(`public readonly props: ${propsType.className};`); } this.code.line(`protected enableResourcePropertyConstraint: boolean;`); // // Attributes // const attributes = new Array<genspec.Attribute>(); if (spec.Attributes) { for (const attributeName of Object.keys(spec.Attributes).sort()) { if ( !(attributeName[0] >= 'a' && attributeName[0] <= 'z') && !(attributeName[0] >= 'A' && attributeName[0] <= 'Z') ) continue; const attributeSpec = spec.Attributes![attributeName]; this.code.line(); this.docLink(undefined, `Attribute ${attributeName}: ${(attributeSpec as schema.Description).Description}`); const attr = genspec.attributeDefinition(attributeName); this.code.line(`public readonly ${attr.propertyName}: ${CORE}.IResolvable | string;`); attributes.push(attr); } } // // Constructor // this.code.line(); this.code.line('/**'); this.code.line(' * Param scope - scope in which this resource is defined'); this.code.line(' * Param id - scoped id of the resource'); this.code.line(' * Param props - resource properties'); this.code.line(' */'); const optionalProps = spec.Properties && !Object.values(spec.Properties).some((p) => p.Required || false); const propsArgument = propsType ? `, props: ${propsType.className}${optionalProps ? ' = {}' : ''}` : ''; this.code.openBlock( `constructor(scope: ${CONSTRUCT_CLASS}, id: string${propsArgument}, enableResourcePropertyConstraint:boolean = true)`, ); this.code.line(`super(scope, id);`); this.code.line(`this.scope = scope;`); this.code.line(`this.id = id;`); if (propsType) { this.code.line(`this.props = props;`); } this.code.line(`this.enableResourcePropertyConstraint = enableResourcePropertyConstraint;`); // initialize all property class members if (propsType) { this.code.line(); this.code.openBlock(`const ${CORE}${resourceName.className} = new ${rosResourceName.className}(this, id, `); if (spec.Properties) { for (const propName of Object.keys(spec.Properties)) { const prop = spec.Properties[propName]; const propCodeName = genspec.rosTemplateToScriptName(propName); if (schema.isTagPropertyName(upcaseFirst(propCodeName)) && schema.isTaggableResource(spec)) { this.code.line(`tags: ${CORE}.tagFactory(props.${propCodeName}),`); } else { let codeDefault: string = ''; if (typeof prop.Default === 'number' || typeof prop.Default === 'boolean') { codeDefault = ` === undefined || props.${propCodeName} === null ? ${prop.Default} : props.${propCodeName}`; } else if (typeof prop.Default === 'string') { codeDefault = ` === undefined || props.${propCodeName} === null ? '${prop.Default}' : props.${propCodeName}`; } this.code.line(`${propCodeName}: props.${propCodeName}${codeDefault},`); } } } this.code.closeBlockFormatter = (s) => `}${s}`; this.code.closeBlock(`, enableResourcePropertyConstraint && this.stack.enableResourcePropertyConstraint);`); if (spec.Properties && Object.keys(spec.Properties).length === 0) { this.code.line(`props;`); } } this.code.line(`this.resource = ${CORE}${resourceName.className};`); // initialize all attribute properties for (const at of attributes) { // this.code.line(`this.${at.propertyName} = this.getResourceNameAttribute(${rosResourceName}.${at.propertyName});`); this.code.line(`this.${at.propertyName} = ${CORE}${resourceName.className}.${at.propertyName};`); } this.code.closeBlockFormatter = () => `}`; this.code.closeBlock(); this.code.closeBlock(); } private emitInterfaceProperty(props: EmitPropertyProps): string { const javascriptPropertyName = genspec.rosTemplateToScriptName(props.propName); this.docLink( undefined, `Property ${javascriptPropertyName}: ${props.spec.Description?.replace(new RegExp('\n', 'gm'), '\n * ').replace(new RegExp('/', 'gm'), '\\/')}`, ); const line = props.propName === 'Tags' && (props.spec as schema.ComplexListProperty).ItemType === 'Tag' ? ': { [key: string]: any }[];' : `: ${this.findNativeType(props.context, props.spec, props.propName)};`; const question = props.spec.Required ? '' : '?'; this.code.line(`readonly ${javascriptPropertyName}${question}${line}`); return javascriptPropertyName; } private emitClassProperty(props: EmitPropertyProps): string { const javascriptPropertyName = genspec.rosTemplateToScriptName(props.propName); this.docLink(props.spec.Documentation, props.additionalDocs); const question = props.spec.Required ? ';' : ' | undefined;'; const line = `: ${`${this.findNativeType(props.context, props.spec, props.propName)}`}${question}`; if (schema.isTagPropertyName(props.propName) && schema.isTagProperty(props.spec)) { this.code.line(`public readonly tags: ${TAG_MANAGER};`); } else { this.code.line(`public ${javascriptPropertyName}${line}`); } return javascriptPropertyName; } private emitProperty(props: EmitPropertyProps, container: Container): string { switch (container) { case Container.Class: return this.emitClassProperty(props); case Container.Interface: return this.emitInterfaceProperty(props); default: throw new Error(`Unsupported container ${container}`); } } private beginNamespace(type: genspec.CodeName): void { if (type.namespace) { const parts = type.namespace.split('.'); for (const part of parts) { this.code.openBlock(`export namespace ${part}`); } } } /** * Return the native type expression for the given propSpec */ private findNativeType(resourceContext: genspec.CodeName, propSpec: schema.Property, propName?: string): string { // switch (propSpec.Type) { // case 'list': // return `Array<any>`; // case 'map': // return `{ [key: string]: any }`; // case 'integer': // case 'number': // return 'number'; // default: // return `${propSpec.Type}`; // } const alternatives: string[] = []; // render the union of all item types if (schema.isCollectionProperty(propSpec)) { // render the union of all item types const itemTypes = genspec.specTypesToCodeTypes(resourceContext, itemTypeNames(propSpec)); // 'tokenizableType' operates at the level of rendered type names in TypeScript, so stringify // the objects. const renderedTypes = itemTypes.map((t) => this.renderCodeName(resourceContext, t)); if (!tokenizableType(renderedTypes) && !schema.isTagPropertyName(propName)) { // Always accept a token in place of any list element (unless the list elements are tokenizable) itemTypes.push(genspec.TOKEN_NAME); } const union = this.renderTypeUnion(resourceContext, itemTypes); if (schema.isMapProperty(propSpec)) { alternatives.push(`{ [key: string]: (${union}) }`); } else if (schema.isListProperty(propSpec) && (propSpec as schema.PrimitiveListProperty).PrimitiveItemType === schema.PrimitiveType.AnyDict) { alternatives.push(`Array<{ [key: string]: any }>`); } else { // To make TSLint happy, we have to either emit: SingleType[] or Array<Alt1 | Alt2> if (union.indexOf('|') !== -1) { alternatives.push(`Array<${union}>`); } else { alternatives.push(`${union}[]`); } } } // Yes, some types can be both collection and scalar. Looking at you, SAM. if (schema.isScalarProperty(propSpec)) { // Scalar type const typeNames = scalarTypeNames(propSpec); const types = genspec.specTypesToCodeTypes(resourceContext, typeNames); alternatives.push(this.renderTypeUnion(resourceContext, types)); } // Only if this property is not of a "tokenizable type" (string, string[], // number in the future) we add a type union for `cdk.Token`. We rather // everything to be tokenizable because there are languages that do not // support union types (i.e. Java, .NET), so we lose type safety if we have // a union. if (!tokenizableType(alternatives) && !schema.isTagPropertyName(propName)) { alternatives.push(genspec.TOKEN_NAME.fqn); } return alternatives.join(' | '); } /** * Render a CodeName to a string representation of it in TypeScript */ private renderCodeName(context: genspec.CodeName, type: genspec.CodeName): string { const rel = type.relativeTo(context); const specType = rel.specName && this.spec.PropertyTypes[rel.specName.fqn]; if (!specType || schema.isRecordType(specType)) { return rel.fqn; } return this.findNativeType(context, specType); } private renderTypeUnion(context: genspec.CodeName, types: genspec.CodeName[]): string { return types.map((t) => this.renderCodeName(context, t)).join(' | '); } private docLink(link: string | undefined, ...before: string[]): void { if (!link && before.length === 0) { return; } this.code.line('/**'); before.forEach((line) => this.code.line(` * ${line}`.trimRight())); if (link) { this.code.line(` * See ${link}`); } this.code.line(' */'); return; } } export class ExtensionCodeGenerator { public async getFilesWithoutNodeModules(dir: string): Promise<string[]> { let files: string[] = []; const items = await fs.readdir(dir); for (const item of items) { const itemPath = path.join(dir, item); const stat = await fs.stat(itemPath); if (stat.isDirectory()) { if (!itemPath.includes('node_modules') && !itemPath.includes('\.idea')) { const subFiles = await this.getFilesWithoutNodeModules(itemPath); files = files.concat(subFiles); } } else { if (!itemPath.includes('node_modules') && !itemPath.includes('\.idea')) { files.push(itemPath); } } } return files; } public async emitCode(outPath : string) { const extensionFilePaths = await this.getFilesWithoutNodeModules('../@alicloud/extension'); const pkgTemplate = fs.readFileSync('./lib/pkg-template/package.json', 'utf8'); for (const extensionFilePath of extensionFilePaths) { const filePathRegex = /@alicloud\/extension\/(.*?)\//; const filePathMatch = extensionFilePath.match(filePathRegex); let packageName = undefined; if (filePathMatch && filePathMatch.length > 1) { packageName = filePathMatch[1]; } else { continue; } const serviceName = packageName.split('-')[2]; const fileName = extensionFilePath.split('/')[extensionFilePath.split('/').length-1]; const pathSuffixIndex = extensionFilePath.indexOf("@alicloud/extension/") + "@alicloud/extension/".length; const pathSuffix = extensionFilePath.substring(pathSuffixIndex); const resourceFilePath = `${outPath}/${pathSuffix}`; // console.log(extensionFilePath); let data = fs.readFileSync(extensionFilePath, 'utf8'); let resourceCodes :string[] = []; if (fs.existsSync(resourceFilePath)) { const resourceData = fs.readFileSync(resourceFilePath, 'utf8'); resourceCodes = resourceData.split('\n'); if (fileName === 'package.json') { console.log('merge package.json: ', extensionFilePath); const mergedPackageData = util.mergeObjects(JSON.parse(resourceData), JSON.parse(data)); const mergedPackageJson = JSON.stringify(mergedPackageData, null, 2); fs.writeFileSync(resourceFilePath, mergedPackageJson, 'utf8'); continue; } else if (fileName === 'index.ts') { console.log('merge extension index: ', extensionFilePath); const extensionCodes = data.split('\n'); resourceCodes = resourceCodes.concat(extensionCodes); fs.writeFile(resourceFilePath, resourceCodes.join('\n'), 'utf8', (err) => { if (err) { console.error(`Error writing to ${resourceFilePath}: ${err}`); } }); continue; } console.log('merge extension resource: ', extensionFilePath); const resourceClassRegex = /class\s+Extension(\w+)\s+extends/; const resourceClassMatch = resourceClassRegex.exec(data); let resourceName: string; if (resourceClassMatch && resourceClassMatch.length > 1) { resourceName = resourceClassMatch[1]; data = data.replace(new RegExp(`Extension${resourceName}`, 'g'), `${resourceName}`); } else { throw new Error(`Extension class name is not correct: ${extensionFilePath}`); } const matchingImports = [ resourceName, `${resourceName}Props`, `Ros${resourceName}`, `I${resourceName}` ]; const extensionCodes = data.split('\n'); let resourceCodeIndex = resourceCodes.findIndex((str) => str.includes(`export { Ros${resourceName} as ${resourceName}Property };`)) + 1; let generateImportIndex = resourceCodes.findIndex((str) => str.includes(`import { Ros${resourceName} } from './${serviceName}.generated';`)) let extensionLeftBracketCounts = 0; let constructorBeginIndex = 0; let constructorEndIndex = 0; let meetExtensionConstructor = false; let meetSuperConstructor = false; for (let codeLine of extensionCodes) { if (codeLine === 'import * as ros from "@alicloud/ros-cdk-core";') { continue; } if (codeLine && codeLine.includes(`@alicloud/${packageName}`)) { const matches = codeLine.match(/{(.*?)}/); if (matches && matches[1]) { const importResourceNames = matches[1].split(',').map((element) => element.trim()); let generateImports: string[] = []; for (const importResourceName of importResourceNames) { if (matchingImports.includes(importResourceName)) { continue; } if (importResourceName.startsWith('Ros')) { generateImports.push(importResourceName); continue; } resourceCodes.splice(resourceCodeIndex, 0, `import { ${importResourceName} } from './${importResourceName.toLowerCase()}';`); resourceCodeIndex++; } if (generateImports.length > 0) { resourceCodes.splice(generateImportIndex, 1, `import { Ros${resourceName}, ${generateImports.join(', ')} } from './${serviceName}.generated';`); } } } else if (codeLine && codeLine.includes(`import`)) { resourceCodes.splice(resourceCodeIndex, 0, codeLine); resourceCodeIndex++; } else if (codeLine && codeLine.includes(`class ${resourceName}`)) { // 合并拓展继承和实现 const extensionExtracted = extractExtendsImplements(codeLine); extensionExtracted.extends = extensionExtracted.extends.filter((element) => element !== resourceName); const resourceClassIndex = resourceCodes.findIndex((str) => str.includes(`export class ${resourceName}`)); const resourceExtracted = extractExtendsImplements(resourceCodes[resourceClassIndex]); resourceExtracted.extends = resourceExtracted.extends.concat(extensionExtracted.extends); resourceExtracted.implements = resourceExtracted.implements.concat(extensionExtracted.implements); const newResourceClass = `export class ${resourceName} ${resourceExtracted.extends.length > 0 ? `extends ${resourceExtracted.extends.join(', ')}` : ''}${resourceExtracted.implements.length > 0 ? ` implements ${resourceExtracted.implements.join(', ')}` : ''} \{`; resourceCodes.splice(resourceClassIndex, 1, newResourceClass); const subArray = resourceCodes.slice(resourceClassIndex + 1); const relativeConstructorIndex = subArray.findIndex((str) => str.includes("constructor(scope: ros.Construct,")); if (relativeConstructorIndex !== -1) { constructorBeginIndex = resourceClassIndex + 1 + relativeConstructorIndex; for (let i = constructorBeginIndex; i > 0; i--) { if (resourceCodes[i] && resourceCodes[i].includes('/**')) { resourceCodes.splice(i, 0, ''); constructorBeginIndex++; resourceCodeIndex = i; break; } } let leftBracketCounts = 0; for (let i = constructorBeginIndex; i < resourceCodes.length; i++) { leftBracketCounts += (resourceCodes[i].split('{').length - 1); leftBracketCounts -= (resourceCodes[i].split('}').length - 1); if (leftBracketCounts === 0) { constructorEndIndex = i; break; } } } else { throw new Error(`Class ${resourceName} has no constuctor`); } } else if (codeLine && codeLine.includes(`constructor(scope: ros.Construct, id: string, props: `) && constructorBeginIndex != 0) { extensionLeftBracketCounts += (codeLine.split('{').length - 1); extensionLeftBracketCounts -= (codeLine.split('}').length - 1); meetExtensionConstructor = true; } else { if (codeLine && codeLine.includes(`super(scope, id, props, enableResourcePropertyConstraint);`)) { meetSuperConstructor = true; continue; } extensionLeftBracketCounts += (codeLine.split('{').length - 1); extensionLeftBracketCounts -= (codeLine.split('}').length - 1); if (meetExtensionConstructor) { if (extensionLeftBracketCounts === 0) { meetExtensionConstructor = false; } else { if (meetSuperConstructor) { resourceCodes.splice(constructorEndIndex, 0, codeLine); constructorEndIndex++; } else { resourceCodes.splice(constructorBeginIndex + 1, 0, codeLine); } constructorBeginIndex++; } continue; } if (extensionLeftBracketCounts === -1) { resourceCodeIndex = resourceCodes.length - 1; continue; } if (!codeLine && resourceCodeIndex > 0 && !resourceCodes[resourceCodeIndex-1]) { continue; } resourceCodes.splice(resourceCodeIndex, 0, codeLine); resourceCodeIndex++; constructorBeginIndex++; constructorEndIndex++; } } } else { const directoryPath = path.dirname(resourceFilePath); await fs.ensureDir(directoryPath); console.log('add extension file: ', extensionFilePath); if (fileName !== 'package.json' && !fileName.endsWith('.ts')) { fs.copyFileSync(extensionFilePath, resourceFilePath); continue; } if (fileName === 'package.json') { const mergedPackageData = util.mergeObjects(JSON.parse(pkgTemplate), JSON.parse(data)); const mergedPackageJson = JSON.stringify(mergedPackageData, null, 2); fs.writeFileSync(resourceFilePath, mergedPackageJson, 'utf8'); continue; } const codes = data.split('\n'); // fs.mkdirSync(resourceFilePath, { recursive: true }); for (let codeLine of codes) { // const codeLine = codes.shift(); if (codeLine && codeLine.includes(`@alicloud/${packageName}`)) { const matches = codeLine.match(/{(.*?)}/); if (matches && matches[1]) { const importResourceNames = matches[1].split(',').map((element) => element.trim()); let generateImports: string[] = []; for (const importResourceName of importResourceNames) { if (importResourceName.startsWith('Ros')) { generateImports.push(importResourceName); continue; } resourceCodes.push( `import { ${importResourceName} } from './${importResourceName.toLowerCase()}';`, ); } if (generateImports.length > 0) { resourceCodes.push( `import { ${generateImports.join(', ')} } from './${serviceName}.generated';`); } } } else { resourceCodes.push(codeLine); } } } fs.writeFile(resourceFilePath, resourceCodes.join('\n'), 'utf8', (err) => { if (err) { console.error(`Error writing to ${resourceFilePath}: ${err}`); } }); } } } /** * Quotes a code name for inclusion in a JSDoc comment, so it will render properly * in the Markdown output. * * @param code a code name to be quoted. * * @returns the code name surrounded by double backticks. */ function quoteCode(code: string): string { return '`' + code + '`'; } function tokenizableType(alternatives: string[]): boolean { if (alternatives.length > 1) { return false; } // 支持IResolvable隐式转换 // const type = alternatives[0]; // if (type === 'string') { // return true; // } // // if (type === 'string[]') { // return true; // } // // if (type === 'number') { // return true; // } return false; } function extractExtendsImplements(classDeclaration: string): { extends: string[], implements: string[] } { const result: { extends: string[], implements: string[] } = { extends: [], implements: [] }; // 匹配 extends 和 implements 的位置 const extendsIndex = classDeclaration.indexOf('extends'); const implementsIndex = classDeclaration.indexOf('implements'); if (extendsIndex !== -1) { // 提取 extends 后的内容 let extendsEndIndex = implementsIndex !== -1 ? implementsIndex : classDeclaration.length; const extendsMatch = classDeclaration.substring(extendsIndex + 'extends'.length, extendsEndIndex).match(/([\w\d\s,.<>]+)/); if (extendsMatch) { result.extends = extendsMatch[1].split(',').map(s => s.trim()).filter(s => s.length > 0); } } if (implementsIndex !== -1) { // 提取 implements 后的内容 const implementsMatch = classDeclaration.substring(implementsIndex + 'implements'.length).match(/([\w\d\s,.<>]+)/); if (implementsMatch) { result.implements = implementsMatch[1].split(',').map(s => s.trim()).filter(s => s.length > 0); } } return result; } enum Container { Interface = 'INTERFACE', Class = 'CLASS', } interface EmitPropertyProps { context: genspec.CodeName; propName: string; spec: schema.Property; additionalDocs: string; }