/**
* @description get code using standard dataSource format
* @NOTE getd files structure is as below:
* - library (contains class library code)
* - interfaces (contains interfaces code)
* - api.d.ts (contains interfaces and library definitions)
* - api-lock.json (contains local code state)
*/
import * as _ from 'lodash';
import * as fs from 'fs-extra';
import * as path from 'path';
import { StandardDataSource, Interface, Mod, BaseClass, StandardDataType } from '../standard';
import { format, reviseModName, getFileName, getTemplatesDirFile, judgeTemplatesDirFileExists } from '../utils';
import { info } from '../debugLog';
import { templateRegistion } from '../templates';
import { Surrounding } from '../../types/pontConfig';
export class FileStructures {
constructor(
public generators: CodeGenerator[],
public usingMultipleOrigins: boolean,
private surrounding = Surrounding.typeScript,
private baseDir = 'src/service',
private templateType = '',
public spiltApiLock: boolean
) {}
getMultipleOriginsFileStructures() {
const files = {};
this.generators
.filter((generator) => generator.outDir === this.baseDir)
.forEach((generator) => {
const dsName = generator.dataSource.name;
const dsFiles = this.getOriginFileStructures(generator, true);
files[dsName] = dsFiles;
});
const fileStructures = {
...files,
[getFileName('index', this.surrounding)]: this.getDataSourcesTs.bind(this),
'api.d.ts': this.getDataSourcesDeclarationTs.bind(this)
};
return this.spiltApiLock
? fileStructures
: {
...fileStructures,
'api-lock.json': this.getLockContent()
};
}
getBaseClassesInDeclaration(originCode: string | (() => string), usingMultipleOrigins: boolean) {
const codeStr = typeof originCode === 'function' ? originCode() : originCode;
if (usingMultipleOrigins) {
return `
declare namespace defs {
export ${codeStr}
};
`;
}
return `
declare ${codeStr}
`;
}
getModsDeclaration(originCode: string | (() => string), usingMultipleOrigins: boolean) {
const codeStr = typeof originCode === 'function' ? originCode() : originCode;
if (usingMultipleOrigins) {
return `
declare namespace API {
export ${codeStr}
};
`;
}
return `
declare ${codeStr}
`;
}
getOriginFileStructures(generator: CodeGenerator, usingMultipleOrigins = false) {
let mods = {};
const dataSource = generator.dataSource;
const indexFileName = getFileName('index', this.surrounding);
dataSource.mods.forEach((mod) => {
const currMod = {};
mod.interfaces.forEach((inter) => {
currMod[getFileName(inter.name, this.surrounding)] = generator.getInterfaceContent.bind(generator, inter);
currMod[indexFileName] = generator.getModIndex.bind(generator, mod);
});
const modName = reviseModName(mod.name);
mods[modName] = currMod;
mods[indexFileName] = generator.getModsIndex.bind(generator);
});
if (!generator.hasContextBund) {
generator.getBaseClassesInDeclaration = this.getBaseClassesInDeclaration.bind(
this,
generator.getBaseClassesInDeclaration.bind(generator),
usingMultipleOrigins
);
generator.getModsDeclaration = this.getModsDeclaration.bind(
this,
generator.getModsDeclaration.bind(generator),
usingMultipleOrigins
);
generator.hasContextBund = true;
}
const fileStructures = {
[getFileName('baseClass', this.surrounding)]: generator.getBaseClassesIndex.bind(generator),
mods: mods,
[indexFileName]: generator.getIndex.bind(generator),
'api.d.ts': generator.getDeclaration.bind(generator)
};
if (this.spiltApiLock && usingMultipleOrigins) {
fileStructures[generator.lockFilename] = this.getLockContent(generator);
} else if (!usingMultipleOrigins) {
fileStructures[generator.lockFilename] = this.getLockContent();
}
return fileStructures;
}
getFileStructures() {
const result =
this.usingMultipleOrigins || this.generators.length > 1
? this.getMultipleOriginsFileStructures()
: this.getOriginFileStructures(this.generators[0]);
// js环境时,默认为新用户,生成pontCore文件
if (this.surrounding === Surrounding.javaScript) {
if (!fs.existsSync(this.baseDir + '/pontCore.js')) {
result['pontCore.js'] = getTemplatesDirFile('pontCore.js', 'pontCore/');
result['pontCore.d.ts'] = getTemplatesDirFile('pontCore.d.ts', 'pontCore/');
}
if (this.templateType && this.checkHasTemplateFetch()) {
result[`${this.templateType}.js`] = getTemplatesDirFile(`${this.templateType}.js`, 'pontCore/');
result[`${this.templateType}.d.ts`] = getTemplatesDirFile(`${this.templateType}.d.ts`, 'pontCore/');
}
} else if (this.surrounding === Surrounding.typeScript) {
if (!fs.existsSync(this.baseDir + '/pontCore.ts')) {
result['pontCore.ts'] = getTemplatesDirFile('pontCore.ts', 'pontCore/');
}
if (this.templateType && this.checkHasTemplateFetch()) {
result[`${this.templateType}.ts`] = getTemplatesDirFile(`${this.templateType}.ts`, 'pontCore/');
}
}
return result;
}
private checkHasTemplateFetch() {
const templateTypesWithOutFetch = templateRegistion
.map((item) => item.templateType)
.filter((item) => item !== 'fetch');
const ext = this.surrounding === Surrounding.typeScript ? 'ts' : 'js';
if (
templateTypesWithOutFetch.includes(this.templateType) &&
judgeTemplatesDirFileExists(`${this.templateType}.${ext}`, 'pontCore/')
) {
return true;
}
return false;
}
getMultipleOriginsDataSourceName() {
const dsNames = this.generators.map((ge) => ge.dataSource.name);
if (this.judgeHasMultipleFilesName()) {
const generate = this.generators.find((ge) => ge.outDir === this.baseDir);
if (generate) {
return [generate.dataSource.name];
}
}
return dsNames;
}
judgeHasMultipleFilesName(): boolean {
return this.generators.some((generate) => {
return generate.outDir !== this.baseDir;
});
}
getDataSourcesTs() {
const dsNames = this.getMultipleOriginsDataSourceName();
const generatedCode = this.surrounding === Surrounding.typeScript ? '(window as any)' : 'window';
return `
${dsNames
.map((name) => {
return `import { defs as ${name}Defs, ${name} } from './${name}';
`;
})
.join('\n')}
${generatedCode}.defs = {
${dsNames.map((name) => `${name}: ${name}Defs,`).join('\n')}
};
${generatedCode}.API = {
${dsNames.join(',\n')}
};
`;
}
getDataSourcesDeclarationTs() {
const dsNames = this.getMultipleOriginsDataSourceName();
return `
${dsNames
.map((name) => {
return `/// `;
})
.join('\n')}
`;
}
/** 不指定generate时生成全量数据源的LockContent */
getLockContent(generate?: CodeGenerator): string {
if (generate) {
const dataSource = this.usingMultipleOrigins ? generate.dataSource : [generate.dataSource];
return JSON.stringify(dataSource, null, 2);
} else {
const dataSource = this.generators.map((ge) => ge.dataSource);
return JSON.stringify(dataSource, null, 2);
}
}
/** API 使用case,用于scan扫描接口 */
getApiUseCases = (inter: Interface): Array => {
const context = inter.getContext();
return [`API${this.usingMultipleOrigins ? `.${context.dataSource.name}` : ''}.${context.mod.name}.${inter.name}`];
};
}
export class CodeGenerator {
usingMultipleOrigins = false;
dataSource: StandardDataSource;
hasContextBund = false;
readonly lockFilename: string;
constructor(public surrounding = Surrounding.typeScript, public outDir = '', lockFilename = 'api-lock.json') {
this.lockFilename = lockFilename;
}
setDataSource(dataSource: StandardDataSource) {
this.dataSource = dataSource;
// 将basic-resource这种命名转化成合法命名
this.dataSource.name = _.camelCase(this.dataSource.name);
}
/** 获取某个基类的类型定义代码 */
getBaseClassInDeclaration(base: BaseClass) {
if (base.templateArgs && base.templateArgs.length) {
return `class ${base.name}<${base.templateArgs.map((_, index) => `T${index} = any`).join(', ')}> {
${base.properties
.map((prop) => {
const index = base.templateArgs.findIndex((ele) => ele.typeName === prop.dataType.typeName);
if (index !== -1) {
// 复写这个部分
// 直接判断是否相等 相等然后找到匹配的泛型,放进去
let fieldTypeDeclaration = `: T${index}`;
return `
/** ${prop.description || prop.name} */
${prop.name}${fieldTypeDeclaration};`;
}
// 等于-1可能是array套嵌
const deepArgs = function (deepProp: StandardDataType) {
const index = base.templateArgs.findIndex((ele) => ele.typeName === deepProp.typeName);
if (index !== -1) {
return `Array`;
}
if (deepProp.typeName === 'Array') {
const len = deepProp.typeArgs.length;
for (let i = 0; i < len; i++) {
const arg = deepProp.typeArgs[i];
const result = deepArgs(arg);
if (result) {
return result;
}
}
}
return false;
};
if (prop.dataType.typeName === 'Array') {
let len = prop.dataType.typeArgs.length;
for (let i = 0; i < len; i++) {
const arg = prop.dataType.typeArgs[i];
const result = deepArgs(arg);
if (result) {
return `
/** ${prop.description || prop.name} */
${prop.name}: ${result};`;
}
}
}
return prop.toPropertyCode(Surrounding.typeScript, true);
})
.join('\n')}
}
`;
}
return `class ${base.name} {
${base.properties.map((prop) => prop.toPropertyCode(Surrounding.typeScript, true)).join('\n')}
}
`;
}
/** 获取所有基类的类型定义代码,一个 namespace
* surrounding, 优先级高于this.surrounding,用于生成api.d.ts时强制保留类型
*/
getBaseClassesInDeclaration() {
const content = `namespace ${this.dataSource.name || 'defs'} {
${this.dataSource.baseClasses
.map(
(base) => `
export ${this.getBaseClassInDeclaration(base)}
`
)
.join('\n')}
}
`;
return content;
}
getBaseClassesInDeclarationWithMultipleOrigins() {
return `
declare namespace defs {
export ${this.getBaseClassesInDeclaration()}
}
`;
}
getBaseClassesInDeclarationWithSingleOrigin() {
return `
declare ${this.getBaseClassesInDeclaration()}
`;
}
/** 获取接口内容的类型定义代码 */
getInterfaceContentInDeclaration(inter: Interface) {
const bodyParams = inter.getBodyParamsCode();
const requestParams = bodyParams ? `params: Params, bodyParams: ${bodyParams}` : `params: Params`;
return `
export ${inter.getParamsCode('Params', this.surrounding)}
export type Response = ${inter.responseType};
export const init: Response;
export function request(${requestParams}): Promise<${inter.responseType}>;
`;
}
private getInterfaceInDeclaration(inter: Interface) {
return `
/**
* ${inter.description}
* ${inter.path}
*/
export namespace ${inter.name} {
${this.getInterfaceContentInDeclaration(inter)}
}
`;
}
/** 获取模块的类型定义代码,一个 namespace ,一般不需要覆盖 */
getModsDeclaration() {
const mods = this.dataSource.mods;
const content = `namespace ${this.dataSource.name || 'API'} {
${mods
.map(
(mod) => `
/**
* ${mod.description}
*/
export namespace ${reviseModName(mod.name)} {
${mod.interfaces.map(this.getInterfaceInDeclaration.bind(this)).join('\n')}
}
`
)
.join('\n\n')}
}
`;
return content;
}
getModsDeclarationWithMultipleOrigins() {}
getModsDeclarationWithSingleOrigin() {}
/** 获取公共的类型定义代码 */
getCommonDeclaration() {
return '';
}
/** 获取总的类型定义代码 */
getDeclaration() {
return `
type ObjectMap = {
[key in Key]: Value;
}
${this.getCommonDeclaration()}
${this.getBaseClassesInDeclaration()}
${this.getModsDeclaration()}
`;
}
/** 获取接口类和基类的总的 index 入口文件代码 */
getIndex() {
let conclusion = `
import * as defs from './baseClass';
import './mods/';
${this.surrounding === Surrounding.typeScript ? '(window as any)' : 'window'}.defs = defs;
`;
// dataSource name means multiple dataSource
if (this.dataSource.name) {
conclusion = `
import { ${this.dataSource.name} as defs } from './baseClass';
export { ${this.dataSource.name} } from './mods/';
export { defs };
`;
}
return conclusion;
}
/** 获取所有基类文件代码 */
getBaseClassesIndex() {
const clsCodes = this.dataSource.baseClasses.map(
(base) => `
class ${base.name} {
${base.properties
.map((prop) => {
return prop.toPropertyCodeWithInitValue(base.name);
})
.filter((id) => id)
.join('\n')}
}
`
);
if (this.dataSource.name) {
return `
${clsCodes.join('\n')}
export const ${this.dataSource.name} = {
${this.dataSource.baseClasses.map((bs) => bs.name).join(',\n')}
}
`;
}
return clsCodes.map((cls) => `export ${cls}`).join('\n');
}
/** 获取接口实现内容的代码 */
getInterfaceContent(inter: Interface) {
const method = inter.method.toUpperCase();
const bodyParams = inter.getBodyParamsCode();
return `
/**
* @desc ${inter.description}
*/
import * as defs from '../../baseClass';
import { pontCore } from '../../pontCore';
export ${inter.getParamsCode('Params', this.surrounding)}
export const init = ${inter.response.getInitialValue()};
export function request(${bodyParams ? `params = {}, bodyParams = null` : 'params = {}'}) {
return pontCore.fetch(pontCore.getUrl("${inter.path}", params, "${method}"), {
method: "${method}",
body: ${bodyParams ? 'bodyParams' : 'null'},
});
}
`;
}
/** 获取单个模块的 index 入口文件 */
getModIndex(mod: Mod) {
return `
/**
* @description ${mod.description}
*/
${mod.interfaces
.map((inter) => {
return `import * as ${inter.name} from './${inter.name}';`;
})
.join('\n')}
export {
${mod.interfaces.map((inter) => inter.name).join(', \n')}
}
`;
}
/** 获取所有模块的 index 入口文件 */
getModsIndex() {
let conclusion = `
${this.surrounding === Surrounding.typeScript ? '(window as any)' : 'window'}.API = {
${this.dataSource.mods.map((mod) => reviseModName(mod.name)).join(', \n')}
};
`;
// dataSource name means multiple dataSource
if (this.dataSource.name) {
conclusion = `
export const ${this.dataSource.name} = {
${this.dataSource.mods.map((mod) => reviseModName(mod.name)).join(', \n')}
};
`;
}
return `
${this.dataSource.mods
.map((mod) => {
const modName = reviseModName(mod.name);
return `import * as ${modName} from './${modName}';`;
})
.join('\n')}
${conclusion}
`;
}
/**
* 获取中间态数据结构
* @param dataSource
*/
getDataSourceCallback(dataSource?: StandardDataSource): void {
// 空实现, 用于对外暴露文件数据结构
if (dataSource) {
return;
}
}
codeSnippet(inter: Interface): string {
const context = inter.getContext();
return `API${this.usingMultipleOrigins ? `.${context.dataSource.name}` : ''}.${context.mod.name}.${inter.name}`;
}
}
export class FilesManager {
// todo: report 可以更改为单例,防止每个地方都注入。
report = info;
prettierConfig: {};
constructor(public fileStructures: FileStructures, private baseDir: string) {}
/** 初始化清空路径 */
private initPath(path: string) {
if (!fs.existsSync(path)) {
fs.mkdirpSync(path);
}
}
async regenerate(files: {}, oldFiles?: {}) {
this.initPath(this.baseDir);
if (oldFiles && Object.keys(oldFiles || {}).length) {
const updateTask = this.diffFiles(files, oldFiles);
if (updateTask.deletes && updateTask.deletes.length) {
this.report(`删除${updateTask.deletes.length}个文件及文件夹`);
await Promise.all(
updateTask.deletes.map((filePath) => {
fs.unlink(filePath);
})
);
}
if (updateTask.updateCnt) {
this.report(`更新${updateTask.updateCnt}个文件`);
console.time(`更新${updateTask.updateCnt}个文件`);
await this.updateFiles(updateTask.files);
console.timeEnd(`更新${updateTask.updateCnt}个文件`);
}
} else {
await this.generateFiles(files);
}
}
async saveLock(originName?: string) {
const setLockFile = async (generator: CodeGenerator) => {
const lockFilePath = path.join(
generator.outDir,
this.fileStructures.spiltApiLock && this.fileStructures.usingMultipleOrigins ? generator.dataSource.name : '',
generator.lockFilename
);
const lockContent = await fs.readFile(lockFilePath, 'utf8');
const newLockContent = this.fileStructures.getLockContent(
this.fileStructures.spiltApiLock && this.fileStructures.usingMultipleOrigins ? generator : null
);
if (lockContent !== newLockContent) {
await fs.ensureFile(lockFilePath);
await fs.writeFile(lockFilePath, newLockContent);
}
};
if (this.fileStructures.usingMultipleOrigins) {
if (originName) {
const targetOrigin = this.fileStructures.generators.find(
(generator) => generator.dataSource.name === originName
);
targetOrigin && setLockFile(targetOrigin);
} else {
this.fileStructures.generators.forEach(setLockFile);
}
} else {
const targetOrigin = this.fileStructures.generators[0];
targetOrigin && setLockFile(targetOrigin);
}
}
diffFiles(newFiles: {}, lastFiles: {}, dir = this.baseDir) {
const task = {
deletes: [] as string[],
files: {},
updateCnt: 0
};
// 待删除、待更新
_.map(lastFiles, (lastValue: string | {}, name) => {
const currPath = `${dir}/${name}`;
const newValue = newFiles[name];
// 待删除
if (!newValue) {
task.deletes.push(currPath);
return;
}
// 文件转文件夹
if (typeof newValue === 'object' && typeof lastValue === 'string') {
task.deletes.push(currPath);
const fileTask = this.diffFiles(newValue, {}, currPath);
if (fileTask.updateCnt) {
task.files = { ...task.files, [currPath]: undefined, ...fileTask.files };
task.updateCnt += fileTask.updateCnt + 1;
}
return;
}
// 文件夹转文件
if (typeof newValue === 'string' && typeof lastValue === 'object') {
task.deletes.push(currPath);
return;
}
// 待更新
if (typeof lastValue === 'string') {
// 文件更新
if (newValue !== lastValue) {
task.files[currPath] = newValue;
task.updateCnt++;
}
} else {
// 文件夹更新
const fileTask = this.diffFiles(newValue, lastValue, currPath);
task.deletes.push(...fileTask.deletes);
if (fileTask.updateCnt) {
task.updateCnt += fileTask.updateCnt;
task.files = { ...task.files, ...fileTask.files };
}
}
});
// 待增加
_.map(newFiles, (newValue: string | {}, name) => {
const currPath = `${dir}/${name}`;
const lastValue = lastFiles[name];
if (!lastValue) {
if (typeof newValue === 'string') {
task.files[currPath] = newValue;
task.updateCnt += 1;
} else {
const fileTask = this.diffFiles(newValue, {}, currPath);
if (fileTask.updateCnt) {
task.updateCnt += fileTask.updateCnt + 1;
task.files = { ...task.files, [currPath]: undefined, ...fileTask.files };
}
}
}
});
return task;
}
public formatFile(code: string, name = '') {
if (name && name.endsWith('.json')) {
return code;
}
return format(code, this.prettierConfig);
}
async updateFiles(files: {}) {
await Promise.all(
_.map(files, async (value: string, filePath) => {
if (value === undefined) {
return fs.ensureDir(filePath);
}
await fs.ensureFile(filePath);
if (filePath.endsWith('.json')) {
return fs.writeFile(filePath, value);
}
return fs.writeFile(filePath, this.formatFile(value));
})
);
}
/** 根据 Codegenerator 配置生成目录和文件 */
async generateFiles(files: {}, dir = this.baseDir) {
const currFiles = await fs.readdir(dir);
const promises = _.map(files, async (value: string | {}, name) => {
const currPath = `${dir}/${name}`;
if (typeof value === 'string') {
if (currFiles.includes(name)) {
const state = await fs.lstat(currPath);
if (state.isDirectory()) {
await fs.unlink(currPath);
await fs.ensureFile(currPath);
return fs.writeFile(currPath, this.formatFile(value, name));
} else {
const newValue = this.formatFile(value, name);
const currValue = await fs.readFile(currPath, 'utf8');
if (newValue !== currValue) {
await fs.ensureFile(currPath);
return fs.writeFile(currPath, this.formatFile(value, name));
}
return;
}
} else {
await fs.ensureFile(currPath);
return fs.writeFile(currPath, this.formatFile(value, name));
}
}
// 新路径为文件夹
if (currFiles.includes(name)) {
const state = await fs.lstat(currPath);
if (state.isDirectory()) {
return this.generateFiles(files[name], currPath);
} else {
await fs.unlink(currPath);
await fs.ensureDir(currPath);
return this.generateFiles(files[name], currPath);
}
} else {
await fs.ensureDir(currPath);
return this.generateFiles(files[name], currPath);
}
});
await Promise.all(promises);
}
}