diff --git a/.travis.yml b/.travis.yml index eae5997..630c519 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,5 @@ os: - linux script: - npm run cov -after_script: "npm install coveralls && cat ./coverage/lcov.info | coveralls" +after_script: + - npminstall codecov && codecov \ No newline at end of file diff --git a/README.md b/README.md index ac8e422..246236b 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ In `package.json` Generator is the core of `egg-ts-helper`. ( build-in generator: https://github.com/whxaxes/egg-ts-helper/tree/master/src/generators ) -In startup, `egg-ts-helper` executes all watcher's generator, the generator will traverse the directory and collect modules, then return fields `dist`( d.ts file path ) and `content`( import these modules and defined to interface of egg. ) to `egg-ts-helper`. `egg-ts-helper` will create `d.ts` by `dist` and `content` fields. +On `egg-ts-helper` startup, it will executes all watcher's generator, and the generator will traverse the directory and collect modules, then return fields `dist`( d.ts file path ) and `content`( import these modules and defined to interface of egg. ) to `egg-ts-helper`. `egg-ts-helper` will create `d.ts` by `dist` and `content` fields. You can configure watcher in option `watchDirs` ( see `getDefaultWatchDirs` method in https://github.com/whxaxes/egg-ts-helper/blob/master/src/index.ts to know default config of watcher ). `egg-ts-helper` watch these directories `app/extend`,`app/controller`,`app/service`, `app/config`, `app/middleware`, `app/model` by default. When the files under these folders is changed, the `d.ts` will be created ( config.watch should set to true ) . @@ -254,9 +254,11 @@ Should set `declareTo` if without `interface`. #### generator `string` -The name of generator, ( the generator will be executed and recreate `d.ts` when the file is changed. ) but I recommend to use `class` `function` `object` only, because the other generator is not suitable for custom loader. +The name of generator, ( the generator will be executed and recreate `d.ts` when the file is changed. ) but I recommend to use `class` `function` `object` `auto` only, because the other generator is not suitable for custom loader. -`generator` set to `class`. +##### | `generator` set to `class` + +the types created by `class` generator like below ```typescript interface IModel { @@ -264,7 +266,15 @@ interface IModel { } ``` -`generator` set to `function`. ( Support since `1.16.0` ) +suitable for module like this + +```typescript +export default class XXXController extends Controller { } +``` + +##### | `generator` set to `function` ( Support since `1.16.0` ) + +the types created by `function` generator like below ```typescript interface IModel { @@ -272,7 +282,17 @@ interface IModel { } ``` -`generator` set to `object`. ( Support since `1.16.0` ) +suitable for module like this + +```typescript +export default () => { + return {}; +} +``` + +##### | `generator` set to `object` ( Support since `1.16.0` ) + +the types created by `object` generator like below. ```typescript interface IModel { @@ -280,6 +300,26 @@ interface IModel { } ``` +suitable for module like this + +```typescript +export default {} +``` + +##### | `generator` set to `auto` ( Support since `1.19.0` ) + +the types created by `auto` generator like below. It will check types automatically. + +```typescript +type AutoInstanceType any ? ReturnType : T> = U extends { new (...args: any[]): any } ? InstanceType : U; + +interface IModel { + Station: AutoInstanceType; +} +``` + +suitable for every module in above. + #### interfaceHandle `function|string` ```js @@ -374,6 +414,8 @@ function myGenerator(config, baseConfig) { console.info(config); console.info(baseConfig); + // return type can be object or array { dist: string; content: string } | Array<{ dist: string; content: string }> + // egg-ts-helper will remove dist file when content is undefined. return { dist: 'd.ts file url', content: 'd.ts content' @@ -391,6 +433,45 @@ module.exports = { } ``` +or define generator to other js. + +```javascript +// ./my-generator.js + +// custom generator +module.exports = (config, baseConfig) => { + // config.dir dir + // config.dtsDir d.ts dir + // config.file changed file + // config.fileList file list + console.info(config); + console.info(baseConfig); + + // return type can be object or array { dist: string; content: string } | Array<{ dist: string; content: string }> + // egg-ts-helper will remove dist file when content is undefined. + return { + dist: 'd.ts file url', + content: 'd.ts content' + } +} +``` + +configure in `tshelper.js` or `package.json` + +```js +// ./tshelper.js + +module.exports = { + watchDirs: { + model: { + path: 'app/model', + generator: './my-generator', + trigger: ['add', 'unlink'], + } + } +} +``` + ## Register `egg-ts-helper` offers a `register.js` for easier to use with [egg-bin](https://github.com/eggjs/egg-bin). diff --git a/README.zh-CN.md b/README.zh-CN.md index 98bebb6..eb7bfd5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -253,9 +253,11 @@ interface T100 { #### generator `string` -生成器名称,watcher 监听到文件改动的时候会执行该生成器用来重新生成 d.ts,建议只使用 `class` `function` `object` 这三个生成器,因为其他几个比较定制化,不太适用于 custom loader。 +生成器名称,watcher 监听到文件改动的时候会执行该生成器用来重新生成 d.ts,建议只使用 `class` `function` `object` `auto` 这几个生成器,因为其他几个比较定制化,不太适用于 custom loader。 -`generator` 设置为 `class`. +##### | `generator` 设置为 `class`. + +生成的声明如下 ```typescript interface IModel { @@ -263,7 +265,15 @@ interface IModel { } ``` -`generator` 设置为 `function`. ( `1.16.0` 开始支持 ) +适合这样写的模块 + +```typescript +export default class XXXController extends Controller { } +``` + +##### | `generator` 设置为 `function`. ( `1.16.0` 开始支持 ) + +生成的声明如下 ```typescript interface IModel { @@ -271,7 +281,17 @@ interface IModel { } ``` -`generator` 设置为 `object`. ( `1.16.0` 开始支持 ) +适合这样写的模块 + +```typescript +export default () => { + return {}; +} +``` + +##### | `generator` 设置为 `object`. ( `1.16.0` 开始支持 ) + +生成的声明如下 ```typescript interface IModel { @@ -279,6 +299,26 @@ interface IModel { } ``` +适合这样写的模块 + +```typescript +export default {} +``` + +##### | `generator` 设置为 `auto`. ( `1.19.0` 开始支持 ) + +生成的声明如下,会自动判断 import 的类型是方法还是对象还是类。 + +```typescript +type AutoInstanceType any ? ReturnType : T> = U extends { new (...args: any[]): any } ? InstanceType : U; + +interface IModel { + Station: AutoInstanceType; +} +``` + +适合上面描述的所有模块。 + #### interfaceHandle `function|string` 如果需要自定义类型,就可以用该配置 @@ -375,12 +415,13 @@ function myGenerator(config, baseConfig) { console.info(config); console.info(baseConfig); + // 返回值可以是对象或者数组 { dist: string; content: string } | Array<{ dist: string; content: string }> + // 如果返回的 content 是 undefined,egg-ts-helper 会删除 dist 指向的文件 return { dist: 'd.ts file url', content: 'd.ts content' } } - module.exports = { watchDirs: { model: { @@ -392,6 +433,45 @@ module.exports = { } ``` +或者将自定义生成器定义到其他 js 中 + +```javascript +// ./my-generator.js + +// custom generator +module.exports = (config, baseConfig) => { + // config.dir dir + // config.dtsDir d.ts dir + // config.file changed file + // config.fileList file list + console.info(config); + console.info(baseConfig); + + // 返回值可以是对象或者数组 { dist: string; content: string } | Array<{ dist: string; content: string }> + // 如果返回的 content 是 undefined,egg-ts-helper 会删除 dist 指向的文件 + return { + dist: 'd.ts file url', + content: 'd.ts content' + } +} +``` + +配置一下 + +```js +// ./tshelper.js + +module.exports = { + watchDirs: { + model: { + path: 'app/model', + generator: './my-generator', + trigger: ['add', 'unlink'], + } + } +} +``` + ## 注册器 `egg-ts-helper` 提供了 `register.js` 来更方便的搭配 [egg-bin](https://github.com/eggjs/egg-bin) 使用. diff --git a/src/generators/auto.ts b/src/generators/auto.ts new file mode 100644 index 0000000..eddac92 --- /dev/null +++ b/src/generators/auto.ts @@ -0,0 +1,21 @@ +import { TsGenConfig, TsHelperConfig } from '..'; +import classGen from './class'; +import * as utils from '../utils'; + +export default function(config: TsGenConfig, baseConfig: TsHelperConfig) { + config.interfaceHandle = utils.preWrapHandle( + val => `AutoInstanceType`, + utils.strToFn(config.interfaceHandle), + ); + + const result = classGen(config, baseConfig); + /* istanbul ignore else */ + if (result.content) { + result.content = [ + 'type AutoInstanceType any ? ReturnType : T> = U extends { new (...args: any[]): any } ? InstanceType : U;', + result.content, + ].join('\n'); + } + + return result; +} diff --git a/src/generators/config.ts b/src/generators/config.ts index a9cac45..b0c5c95 100644 --- a/src/generators/config.ts +++ b/src/generators/config.ts @@ -85,10 +85,6 @@ export default function(config: TsGenConfig, baseConfig: TsHelperConfig) { // check config return type. export function checkConfigReturnType(f: string) { const result = utils.findExportNode(fs.readFileSync(f, 'utf-8')); - if (!result) { - return; - } - if (result.exportDefaultNode) { return ts.isFunctionLike(result.exportDefaultNode) ? EXPORT_DEFAULT_FUNCTION diff --git a/src/index.ts b/src/index.ts index 1f7440d..e6ad6cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,11 +3,12 @@ import d from 'debug'; import { EventEmitter } from 'events'; import fs from 'fs'; import path from 'path'; +import Watcher, { BaseWatchItem, WatchItem } from './watcher'; import * as utils from './utils'; const debug = d('egg-ts-helper#index'); const dtsComment = '// This file is created by egg-ts-helper\n' + - '// Do not modify this file!!!!!!!!!\n\n'; + '// Do not modify this file!!!!!!!!!\n'; declare global { interface PlainObject { @@ -15,16 +16,6 @@ declare global { } } -export interface BaseWatchItem { - path: string; - generator: string; - enabled: boolean; - trigger?: string[]; - pattern?: string; -} - -export interface WatchItem extends PlainObject, BaseWatchItem { } - export interface TsHelperOption { cwd?: string; framework?: string; @@ -39,6 +30,7 @@ export interface TsHelperOption { configFile?: string; } +export type WatchItem = WatchItem; export type TsHelperConfig = typeof defaultConfig; export type TsGenConfig = { dir: string; @@ -46,15 +38,17 @@ export type TsGenConfig = { fileList: string[], file?: string; } & WatchItem; + export interface GeneratorResult { dist: string; content?: string; } -export type TsGenerator = ( - config: T, + +export type TsGenerator = ( + config: TsGenConfig, baseConfig: TsHelperConfig, tsHelper: TsHelper, -) => U; +) => T; // partial and exclude some properties type PartialExclude = { [P in K]: T[P]; } & { [U in Exclude]?: T[U]; }; @@ -169,27 +163,9 @@ export function getDefaultWatchDirs(opt?: TsHelperOption) { return watchConfig; } -// preload generators -const gd = path.resolve(__dirname, './generators'); -const generators = fs - .readdirSync(gd) - .filter(f => f.endsWith('.js')) - .map(f => { - const name = f.substring(0, f.lastIndexOf('.')); - return { - name, - genFn: require(path.resolve(gd, name)).default, - }; - }); - export default class TsHelper extends EventEmitter { - readonly config: TsHelperConfig; - readonly watchDirs: string[]; - readonly watchNameList: string[]; - readonly generators: { [key: string]: TsGenerator } = {}; - private watchers: chokidar.FSWatcher[] = []; - private tickerMap: PlainObject = {}; - private watched: boolean = false; + config: TsHelperConfig; + watcherList: Watcher[]; private cacheDist: PlainObject = {}; private dtsFileList: string[] = []; @@ -199,89 +175,29 @@ export default class TsHelper extends EventEmitter { constructor(options: TsHelperOption = {}) { super(); - const config = (this.config = this.configure(options)); - - debug('framework is %s', config.framework); - - // add build-in generators - generators.forEach(({ name, genFn }) => this.register(name, genFn)); + // configure ets + this.configure(options); - // cached watching name list - this.watchNameList = Object.keys(config.watchDirs).filter(key => { - const dir = config.watchDirs[key]; - return dir.hasOwnProperty('enabled') - ? dir.enabled - : true; - }); - - // format watching dirs - this.watchDirs = this.watchNameList.map(key => { - const item = config.watchDirs[key]; - const p = item.path.replace(/\/|\\/, path.sep); - return getAbsoluteUrlByCwd(p, config.cwd); - }); + // init watcher + this.initWatcher(); // generate d.ts at init - if (config.execAtInit) { + if (this.config.execAtInit) { debug('exec at init'); this.build(); } - - // start watching dirs - if (config.watch) { - this.watch(); - } - } - - // register d.ts generator - register( - name: string, - tsGen: TsGenerator, - ) { - this.generators[name] = tsGen; } // build all dirs build() { - this.watchDirs.map((_, i) => this.generateTs(i)); - } - - // init watcher - watch() { - if (this.watched) { - return; - } - - // create watcher for each dir - this.watchDirs.forEach((item, index) => { - const conf = this.config.watchDirs[this.watchNameList[index]]; - - // glob only works with / in windows - const watchGlob = path - .join(item, conf.pattern || '**/*.(js|ts)') - .replace(/\/|\\/g, '/'); - - const watcher = chokidar.watch(watchGlob, this.config.watchOptions); - - // listen watcher event - watcher.on('all', (event, p) => this.onChange(p, event, index)); - - // auto remove js while ts was deleted - if (this.config.autoRemoveJs) { - watcher.on('unlink', utils.removeSameNameJs); - } - - this.watchers.push(watcher); - }); - - this.watched = true; + this.watcherList.forEach(watcher => watcher.execute()); } // destroy destroy() { this.removeAllListeners(); - this.watchers.forEach(watcher => watcher.close()); - this.watchers.length = 0; + this.watcherList.forEach(item => item.destroy()); + this.watcherList.length = 0; } // create oneForAll file @@ -305,9 +221,35 @@ export default class TsHelper extends EventEmitter { utils.writeFileSync(oneForAllDist, distContent); } + // init watcher + private initWatcher() { + const config = this.config; + // format watching dirs + this.watcherList = []; + Object.keys(config.watchDirs).forEach(key => { + const conf = config.watchDirs[key] as WatchItem; + if (!conf.enabled) { + return; + } + + const options = { + ...config.watchDirs[key] as WatchItem, + name: key, + }; + + const watcher = new Watcher(options, this); + this.watcherList.push(watcher); + watcher.on('update', this.generateTs.bind(this)); + + if (config.watch) { + watcher.watch(); + } + }); + } + // configure // options > configFile > package.json - private configure(options: TsHelperOption): TsHelperConfig { + private configure(options: TsHelperOption) { // base config const config = { ...defaultConfig, watchDirs: getDefaultWatchDirs(options) }; const cwd = options.cwd || config.cwd; @@ -321,7 +263,7 @@ export default class TsHelper extends EventEmitter { } // read from local file - mergeConfig(config, utils.requireFile(getAbsoluteUrlByCwd(configFile, cwd))); + mergeConfig(config, utils.requireFile(utils.getAbsoluteUrlByCwd(configFile, cwd))); debug('%o', config); // merge local config and options to config @@ -329,59 +271,15 @@ export default class TsHelper extends EventEmitter { debug('%o', options); // resolve config.typings to absolute url - config.typings = getAbsoluteUrlByCwd(config.typings, cwd); + config.typings = utils.getAbsoluteUrlByCwd(config.typings, cwd); - return config as TsHelperConfig; + this.config = config as TsHelperConfig; } - private generateTs(index: number, event?: string, file?: string) { + private generateTs(result: GeneratorResult | GeneratorResult[], file?: string) { const config = this.config; - const dir = this.watchDirs[index]; - const watchName = this.watchNameList[index]; - const generatorConfig = config.watchDirs[watchName] as WatchItem; - - if ( - !generatorConfig.trigger || - (event && !generatorConfig.trigger.includes(event)) - ) { - // check whether need to regenerate ts - return; - } - - const generator = - typeof generatorConfig.generator === 'string' - ? this.generators[generatorConfig.generator] - : generatorConfig.generator; - - if (typeof generator !== 'function') { - throw new Error(`ts generator: ${generatorConfig.generator} not exist!!`); - } - - const dtsDir = path.resolve(config.typings, path.relative(config.cwd, dir)); - let _fileList: string[] | undefined; - const newConfig = { - ...generatorConfig, - dir, - file, - dtsDir, - - get fileList() { - if (!_fileList) { - _fileList = utils.loadFiles(dir, generatorConfig.pattern); - } - return _fileList; - }, - }; - - // execute generator - const result = generator(newConfig, config, this); - debug('generate ts file result : %o', result); - if (!result) { - return; - } - const resultList = Array.isArray(result) ? result : [ result ]; - resultList.map(item => { + resultList.forEach(item => { // check cache if (this.isCached(item.dist, item.content)) { return; @@ -390,7 +288,12 @@ export default class TsHelper extends EventEmitter { let isRemove = false; if (item.content) { // create file - const dtsContent = `${dtsComment}import '${config.framework}';\n${item.content}`; + const dtsContent = [ + dtsComment, + `import '${config.framework}';`, + item.content, + ].join('\n'); + debug('created d.ts : %s', item.dist); utils.writeFileSync(item.dist, dtsContent); this.emit('update', item.dist, file); @@ -432,39 +335,12 @@ export default class TsHelper extends EventEmitter { this.cacheDist[fileUrl] = content; return false; } - - // trigger while file changed - private onChange(p: string, event: string, index: number) { - debug('%s trigger change', p); - - this.throttleFn(p, () => { - debug('trigger change event in %s', index); - this.emit('change', p); - this.generateTs(index, event, p); - }); - } - - // throttling execution - private throttleFn(key, fn) { - if (this.tickerMap[key]) { - return; - } - - this.tickerMap[key] = setTimeout(() => { - fn(); - this.tickerMap[key] = null; - }, this.config.throttle); - } } export function createTsHelperInstance(options?: TsHelperOption) { return new TsHelper(options); } -function getAbsoluteUrlByCwd(p: string, cwd: string) { - return path.isAbsolute(p) ? p : path.resolve(cwd, p); -} - // merge ts helper options function mergeConfig(base: TsHelperConfig, ...args: TsHelperOption[]) { args.forEach(opt => { @@ -473,26 +349,26 @@ function mergeConfig(base: TsHelperConfig, ...args: TsHelperOption[]) { } Object.keys(opt).forEach(key => { - if (key === 'watchDirs') { - const watchDirs = opt.watchDirs || {}; - - Object.keys(watchDirs).forEach(k => { - const item = watchDirs[k]; - if (typeof item === 'boolean') { - if (base.watchDirs[k]) { - base.watchDirs[k].enabled = item; - } - } else if (item) { - if (base.watchDirs[k]) { - Object.assign(base.watchDirs[k], item); - } else { - base.watchDirs[k] = formatWatchItem(item); - } - } - }); - } else { + if (key !== 'watchDirs') { base[key] = opt[key] === undefined ? base[key] : opt[key]; + return; } + + const watchDirs = opt.watchDirs || {}; + Object.keys(watchDirs).forEach(k => { + const item = watchDirs[k]; + if (typeof item === 'boolean') { + if (base.watchDirs[k]) { + base.watchDirs[k].enabled = item; + } + } else if (item) { + if (base.watchDirs[k]) { + Object.assign(base.watchDirs[k], item); + } else { + base.watchDirs[k] = formatWatchItem(item); + } + } + }); }); }); } diff --git a/src/utils.ts b/src/utils.ts index 689fda5..dc01f77 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,6 +34,10 @@ export function preWrapHandle(wrapper, fn) { }; } +export function getAbsoluteUrlByCwd(p: string, cwd: string) { + return path.isAbsolute(p) ? p : path.resolve(cwd, p); +} + // get import context export function getImportStr( from: string, @@ -102,14 +106,7 @@ export function removeSameNameJs(f: string) { // find export node from sourcefile. export function findExportNode(code: string) { - let sourceFile; - try { - sourceFile = ts.createSourceFile('file.ts', code, ts.ScriptTarget.ES2017, true); - } catch (e) { - console.error(e); - return; - } - + const sourceFile = ts.createSourceFile('file.ts', code, ts.ScriptTarget.ES2017, true); const cache: Map = new Map(); const exportNodeList: ts.Node[] = []; let exportDefaultNode: ts.Node | undefined; @@ -225,10 +222,6 @@ export function requireFile(url) { exp = exp.default; } - if (typeof exp === 'function') { - exp = exp(); - } - return exp; } diff --git a/src/watcher.ts b/src/watcher.ts new file mode 100644 index 0000000..142cb2f --- /dev/null +++ b/src/watcher.ts @@ -0,0 +1,169 @@ +import fs from 'fs'; +import path from 'path'; +import chokidar from 'chokidar'; +import { EventEmitter } from 'events'; +import { TsGenerator, TsGenConfig, TsHelperConfig, default as TsHelper } from './'; +import * as utils from './utils'; +import d from 'debug'; +const debug = d('egg-ts-helper#watcher'); +export interface BaseWatchItem { + path: string; + generator: string; + enabled: boolean; + trigger: Array<'add' | 'unlink' | 'change'>; + pattern: string; +} + +export interface WatchItem extends PlainObject, BaseWatchItem { } + +// preload build-in generators +const gd = path.resolve(__dirname, './generators'); +const generators: PlainObject = {}; +fs + .readdirSync(gd) + .filter(f => f.endsWith('.js')) + .map(f => { + const name = f.substring(0, f.lastIndexOf('.')); + generators[name] = require(path.resolve(gd, name)).default; + }); + +export default class Watcher extends EventEmitter { + name: string; + dir: string; + dtsDir: string; + config: TsHelperConfig; + generator: TsGenerator; + fsWatcher?: chokidar.FSWatcher; + throttleTick: any = null; + throttleStack: string[] = []; + + constructor( + public options: WatchItem & { name: string; }, + public helper: TsHelper, + ) { + super(); + this.init(); + } + + public init() { + this.config = this.helper.config; + this.name = this.options.name; + const p = this.options.path.replace(/\/|\\/, path.sep); + this.generator = this.getGenerator(this.options); + this.dir = utils.getAbsoluteUrlByCwd(p, this.config.cwd); + this.dtsDir = path.resolve( + this.config.typings, + path.relative(this.config.cwd, this.dir), + ); + } + + public destroy() { + if (this.fsWatcher) { + this.fsWatcher.close(); + } + + clearTimeout(this.throttleTick); + this.throttleTick = null; + this.throttleStack.length = 0; + this.removeAllListeners(); + } + + // watch file change + public watch() { + if (this.fsWatcher) { + this.fsWatcher.close(); + } + + // glob only works with / in windows + const watchGlob = path + .join(this.dir, this.options.pattern || '**/*.(js|ts)') + .replace(/\/|\\/g, '/'); + + const watcher = chokidar.watch(watchGlob, this.config.watchOptions); + + // listen watcher event + this.options.trigger.forEach(evt => { + watcher.on(evt, this.onChange.bind(this)); + }); + + // auto remove js while ts was deleted + if (this.config.autoRemoveJs) { + watcher.on('unlink', utils.removeSameNameJs); + } + + this.fsWatcher = watcher; + } + + // execute generator + public execute(file?: string): any { + debug('execution %s', file); + const options = this.options; + let _fileList: string[] | undefined; + const newConfig: TsGenConfig = { + ...this.options, + file, + dir: this.dir, + dtsDir: this.dtsDir, + + get fileList() { + if (!_fileList) { + _fileList = utils.loadFiles(this.dir, options.pattern); + } + return _fileList; + }, + }; + + const result = this.generator(newConfig, this.config, this.helper); + if (result) { + this.emit('update', result, file); + } + + return result; + } + + // on file change + private onChange(filePath: string) { + debug('file changed %s %o', filePath, this.throttleStack); + if (!this.throttleStack.includes(filePath)) { + this.throttleStack.push(filePath); + } + + if (this.throttleTick) { + return; + } + + this.throttleTick = setTimeout(() => { + while (this.throttleStack.length) { + this.execute(this.throttleStack.pop()!); + } + + this.throttleTick = null; + }, this.config.throttle); + } + + // get generator + private getGenerator(genConfig: WatchItem): TsGenerator { + const type = typeof genConfig.generator; + const typeIsString = type === 'string'; + let generator = typeIsString ? generators[genConfig.generator] : genConfig.generator; + + if (!generator && typeIsString) { + try { + // try to load generator as module path + const generatorPath = genConfig.generator.startsWith('.') + ? path.join(this.config.cwd, genConfig.generator) + : genConfig.generator; + + generator = require(generatorPath); + } catch (e) { + // do nothing + } + } + + if (typeof generator !== 'function') { + throw new Error(`generator: ${genConfig.generator} not exist!!`); + } + + return generator; + } +} diff --git a/test/fixtures/app/custom.js b/test/fixtures/app/custom.js new file mode 100644 index 0000000..6fbe578 --- /dev/null +++ b/test/fixtures/app/custom.js @@ -0,0 +1,8 @@ +const path = require('path'); + +module.exports = () => { + return { + dist: path.resolve(__dirname, './typings/custom2.d.ts'), + content: 'export const a: string;' + } +} diff --git a/test/fixtures/app/tshelper.js b/test/fixtures/app/tshelper.js index dd37f3b..b5bf938 100644 --- a/test/fixtures/app/tshelper.js +++ b/test/fixtures/app/tshelper.js @@ -2,6 +2,13 @@ const path = require('path'); module.exports = { framework: 'larva', watchDirs: { + notLegal: { + path: 'app/custom', + trigger: ['add', 'unlink'], + generator() { + return null; + } + }, custom: { path: 'app/custom', trigger: ['add', 'unlink'], @@ -12,6 +19,11 @@ module.exports = { } } }, + custom2: { + path: 'app/custom', + trigger: ['add', 'unlink'], + generator: './custom' + }, casestyle: { path: 'app/casestyle', interface: 'schema', diff --git a/test/fixtures/real-js/package.json b/test/fixtures/real-js/package.json new file mode 100644 index 0000000..7d6e8fc --- /dev/null +++ b/test/fixtures/real-js/package.json @@ -0,0 +1,6 @@ +{ + "name": "real-js-app", + "dependencies": { + "egg-ts-helper": "../../../" + } +} \ No newline at end of file diff --git a/test/fixtures/real/app.ts b/test/fixtures/real/app.ts new file mode 100644 index 0000000..f21e1d3 --- /dev/null +++ b/test/fixtures/real/app.ts @@ -0,0 +1,16 @@ +import { Application } from 'egg'; +import * as path from 'path'; + +export default (app: Application) => { + let directory = path.resolve(app.baseDir, './app/model'); + app.loader.loadToApp(directory, 'model', { + caseStyle: 'upper', + directory, + }); + + directory = path.resolve(app.baseDir, './app/custom'); + app.loader.loadToApp(directory, 'custom', { + caseStyle: 'lower', + directory, + }); +}; diff --git a/test/fixtures/real/app/custom/test.ts b/test/fixtures/real/app/custom/test.ts new file mode 100644 index 0000000..16e9b85 --- /dev/null +++ b/test/fixtures/real/app/custom/test.ts @@ -0,0 +1,5 @@ +export default () => { + return { + abc: '123', + }; +}; diff --git a/test/fixtures/real/app/router.ts b/test/fixtures/real/app/router.ts index 5cdff16..d64e067 100644 --- a/test/fixtures/real/app/router.ts +++ b/test/fixtures/real/app/router.ts @@ -3,5 +3,6 @@ import { Application } from 'egg'; export default function(app: Application) { const { router, controller } = app; + console.info(app.custom.test.abc); router.get('/', controller.home.index); } diff --git a/test/fixtures/real/tshelper.js b/test/fixtures/real/tshelper.js index 8976bca..62ab9fd 100644 --- a/test/fixtures/real/tshelper.js +++ b/test/fixtures/real/tshelper.js @@ -7,5 +7,11 @@ module.exports = { declareTo: 'Application.model', // declare to this interface interfaceHandle: val => `ReturnType`, // interfaceHandle }, + + custom: { + path: 'app/custom', // dir path + generator: 'auto', // generator name + declareTo: 'Application.custom', // declare to this interface + } } }; diff --git a/test/fixtures/real/typings/app/custom/index.d.ts b/test/fixtures/real/typings/app/custom/index.d.ts new file mode 100644 index 0000000..895708c --- /dev/null +++ b/test/fixtures/real/typings/app/custom/index.d.ts @@ -0,0 +1,16 @@ +// This file is created by egg-ts-helper +// Do not modify this file!!!!!!!!! + +import 'egg'; +type AutoInstanceType any ? ReturnType : T> = U extends { new (...args: any[]): any } ? InstanceType : U; +import ExportTest from '../../../app/custom/test'; + +declare module 'egg' { + interface Application { + custom: TC100; + } + + interface TC100 { + test: AutoInstanceType; + } +} diff --git a/test/generators/class.test.ts b/test/generators/class.test.ts index e7566fe..b13f0d3 100644 --- a/test/generators/class.test.ts +++ b/test/generators/class.test.ts @@ -53,6 +53,7 @@ describe('generators/class.test.ts', () => { it('should support interfaceHandle with model without error', () => { const result = triggerGenerator('model', appDir, undefined, { + generator: 'function', declareTo: 'Context.model', interfaceHandle: 'InstanceOf<{{ 0 }}>', }); diff --git a/test/generators/config.test.ts b/test/generators/config.test.ts index b4244ab..86eba2b 100644 --- a/test/generators/config.test.ts +++ b/test/generators/config.test.ts @@ -64,7 +64,7 @@ describe('generators/config.test.ts', () => { assert(result.content!.includes('Config')); }); - it('should works without empty config.ts', () => { + it('should works without error with empty config.ts', () => { const result = triggerGenerator('config', path.resolve(__dirname, '../fixtures/app4')); assert(!result.content); }); diff --git a/test/generators/utils.ts b/test/generators/utils.ts index d0019ce..348a62b 100644 --- a/test/generators/utils.ts +++ b/test/generators/utils.ts @@ -1,12 +1,6 @@ import path from 'path'; -import { - default as TsHelper, - GeneratorResult, - getDefaultWatchDirs, - TsGenerator, - WatchItem, -} from '../../dist/'; -import { loadFiles } from '../../dist/utils'; +import TsHelper, { GeneratorResult } from '../../dist/'; +import assert = require('assert'); export function triggerGenerator( name: string, @@ -14,31 +8,20 @@ export function triggerGenerator; - const dir = path.resolve(appDir, watchDir.path); - const dtsDir = path.resolve(tsHelper.config.typings, path.relative(tsHelper.config.cwd, dir)); - const config = { - ...watchDir, + const watcher = tsHelper.watcherList.find(watcher => watcher.name === name)!; + assert(watcher, 'watcher is not exist'); + const dir = path.resolve(appDir, watcher.options.path); + watcher.options = { + ...watcher.options, ...extra, }; + watcher.init(); - return generator( - { - ...config, - dir, - file: file ? path.resolve(dir, file) : '', - fileList: loadFiles(dir, config.pattern), - dtsDir, - }, - tsHelper.config, - tsHelper, - ); + return watcher.execute(file ? path.resolve(dir, file) : '') as any as T; } diff --git a/test/index.test.ts b/test/index.test.ts index 3be1105..3522c2e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -52,6 +52,7 @@ describe('index.test.ts', () => { assert(fs.existsSync(path.resolve(__dirname, './fixtures/app/typings/config/index.d.ts'))); assert(fs.existsSync(path.resolve(__dirname, './fixtures/app/typings/config/plugin.d.ts'))); assert(fs.existsSync(path.resolve(__dirname, './fixtures/app/typings/custom.d.ts'))); + assert(fs.existsSync(path.resolve(__dirname, './fixtures/app/typings/custom2.d.ts'))); // caseStyle check const caseStylePath = path.resolve(__dirname, './fixtures/app/typings/app/casestyle/index.d.ts'); @@ -271,9 +272,8 @@ describe('index.test.ts', () => { watchDirs, }); - debug('watchDirs : %o', tsHelper.watchDirs); - assert(tsHelper.watchNameList.length === 1); - assert(tsHelper.watchDirs[0].includes('proxy')); + assert(tsHelper.watcherList.length === 1); + assert(!!tsHelper.watcherList.find(watcher => watcher.name === 'proxy')); }); it('should support read framework by package.json', () => { @@ -300,8 +300,8 @@ describe('index.test.ts', () => { cwd: path.resolve(__dirname, './fixtures/app4'), }); const len = Object.keys(watchDirs).filter(k => (watchDirs[k] as any).enabled).length; - assert(tsHelper.watchNameList.length === len - 2); - assert(tsHelper.watchDirs[0].includes('controller')); + assert(tsHelper.watcherList.length === len - 2); + assert(!!tsHelper.watcherList.find(watcher => watcher.name === 'controller')); }); it('should works without error in real app', async () => { diff --git a/test/register.test.ts b/test/register.test.ts index 95947e6..c69568a 100644 --- a/test/register.test.ts +++ b/test/register.test.ts @@ -34,7 +34,7 @@ describe('register.test.ts', () => { ps.stdout.on('data', data => { str += data.toString(); clearTimeout(tick); - tick = setTimeout(resolve, 1000); + tick = setTimeout(resolve, 2000); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index b08797f..2733af7 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -19,8 +19,8 @@ describe('utils.test.ts', () => { const exp2 = utils.requireFile(path.resolve(appDir, './test2.js')); const exp3 = utils.requireFile(path.resolve(appDir, './test3.js')); assert(exp.hello); - assert(exp2.hello); - assert(exp3.hello); + assert(typeof exp2 === 'function'); + assert(typeof exp3 === 'function'); }); it('should removeSameNameJs without error', () => { diff --git a/test/watcher.test.ts b/test/watcher.test.ts new file mode 100644 index 0000000..4c44b82 --- /dev/null +++ b/test/watcher.test.ts @@ -0,0 +1,81 @@ +import { createTsHelperInstance, getDefaultWatchDirs } from '../dist'; +import Watcher, { WatchItem } from '../dist/watcher'; +import path from 'path'; +import assert = require('assert'); + +describe('watcher.test.ts', () => { + let watcher: Watcher; + const tsHelper = createTsHelperInstance({ + cwd: path.resolve(__dirname, './fixtures/app'), + watch: false, + execAtInit: true, + autoRemoveJs: false, + }); + + const defaultWatchDir = getDefaultWatchDirs(); + + afterEach(() => { + watcher.destroy(); + }); + + it('should works without error', () => { + watcher = new Watcher( + { + ...defaultWatchDir.model as WatchItem, + name: 'xxx', + }, + tsHelper, + ); + + assert(!!watcher.execute().dist); + assert(!!watcher.execute().content); + }); + + it('should watch multiple times without error', () => { + watcher = new Watcher( + { + ...defaultWatchDir.model as WatchItem, + name: 'xxx', + }, + tsHelper, + ); + + watcher.watch(); + const oldWatcher = watcher.fsWatcher!; + watcher.watch(); + assert(oldWatcher !== watcher.fsWatcher); + }); + + it('should throttle without error', () => { + watcher = new Watcher( + { + ...defaultWatchDir.model as WatchItem, + name: 'xxx', + }, + tsHelper, + ); + + watcher.watch(); + watcher.fsWatcher!.emit('add', 'fff'); + watcher.fsWatcher!.emit('add', 'fff'); + watcher.fsWatcher!.emit('add', 'fff2'); + assert(watcher.throttleStack.length === 2); + }); + + it('should throw error if generator is not exist', () => { + try { + watcher = new Watcher( + { + ...defaultWatchDir.model as WatchItem, + name: 'xxx', + generator: '666', + }, + tsHelper, + ); + } catch (e) { + return; + } + + throw new Error('should throw error'); + }); +});