diff --git a/packages/doctor/CHANGELOG.md b/packages/doctor/CHANGELOG.md index 98520628c..d4a55b716 100644 --- a/packages/doctor/CHANGELOG.md +++ b/packages/doctor/CHANGELOG.md @@ -1,10 +1,10 @@ # changelog -## 0.4.8 -- update eslint to version 7.0 +## 0.2.0 -## 0.4.7 -- support scan `unknown` framework project +- Remove codemod CLI usage. +- Add codemod check and fix reports to `scan`. -## 0.4.6 -- fix can't catch timeout error \ No newline at end of file +## 0.1.0 + +- Init diff --git a/packages/doctor/README.md b/packages/doctor/README.md index cab419bac..271972d48 100644 --- a/packages/doctor/README.md +++ b/packages/doctor/README.md @@ -51,12 +51,13 @@ $ appworks-doctor -s ./ --ignore types mock * fix?: boolean, whether fix ESLint fixable problems. * framework?: string, target project framework, default is `react`. +* transforms?: string[], you want to run code transform keys, from [@appworks/codemod](https://github.com/appworks-lab/codemod/) * languageType?: 'js'|'ts', target project languageType, default is `js`. * tempFileDir?: string, set temp reporters file directory, default is `node_modules/@appworks/doctor/tmp/`. * disableESLint?: boolean, whether disable ESLint part reports. * disableMaintainability?: boolean, whether disable maintainability part reports. * disableRepeatability?: boolean, whether disable repeatability part reports. - +* disableCodemod?: boolean, whether disable codemod part reports. ### Result #### ESLint @@ -84,39 +85,8 @@ Use [typhonjs-escomplex](https://www.npmjs.com/package/typhonjs-escomplex) calcu Use [jscpd](https://www.npmjs.com/package/jscpd) calculate repeatability reports. -## 2. Codemod - -This repository also contains a collection of codemod scripts that help update React([ICE](https://ice.work/)) and [Rax](https://rax.js.org/) APIs. - -### Installation - -Install [jscodeshift](https://www.npmjs.com/package/jscodeshift) and @appworks/doctor. - -```shell -$ npm i -g jscodeshift @appworks/doctor -``` - -### Usage (CLI) +#### Codemod -`appworks-doctor -c [...options?]` - - * `transform` - name of transform, see available transforms below. - * `path?` - files or directory to transform. - * `options?` - some rule's options. - -This will start an interactive wizard, and then run the specified transform. - -PS: You can also clone this repository then use [jscodeshift](https://www.npmjs.com/package/jscodeshift)'s CLI API. - -### Included Transforms - -#### `plugin-rax-component-to-component` - -Update `plugin-rax-component` to `plugin-component`. -See: https://rax.js.org/docs/guide/com-migration - -```shell -appworks-doctor -c plugin-rax-component-to-component ./ -``` +Use [@appworks/codemod](https://www.npmjs.com/package/@appworks/codemod) check and update Rax and React deprecated usages. Enjoy! diff --git a/packages/doctor/bin/appworks-doctor b/packages/doctor/bin/appworks-doctor index 4df6e7d8f..db85e0b3a 100755 --- a/packages/doctor/bin/appworks-doctor +++ b/packages/doctor/bin/appworks-doctor @@ -3,17 +3,12 @@ 'use strict'; const argv = require('argv-parse'); -const fs = require('fs'); -const path = require('path'); -const { spawnSync } = require('child_process'); const options = require('./options.json'); const pkg = require('../package.json'); const { Doctor } = require('../lib/index'); const args = argv(options); -const transformerDirectory = path.join(__dirname, '../lib/', 'transforms'); - // Show version if (args.version) { console.log(pkg.version); @@ -50,7 +45,7 @@ if (args.scan) { }, score: result.score || 0, }; - ['ESLint', 'maintainability', 'repeatability'].forEach(key => { + ['ESLint', 'maintainability', 'repeatability'].forEach((key) => { if (result[key]) { report[key] = result[key].score; } @@ -62,33 +57,3 @@ if (args.scan) { console.error(err); }); } - -// codemod -if (args.codemod) { - args.supportExts = (args.supportExts || []).concat(['json']); - const [transformer, targetPath = process.cwd()] = args.codemod; - - const doctor = new Doctor(args); - doctor.ignore = doctor.ignore.filter(i => i !== 'demo'); - const files = doctor.getFiles(targetPath).map(file => file.path); - - let transformerArgs = ['-p']; - const transformerPath = path.join(transformerDirectory, `${transformer}.js`); - - if (!files.length) { - console.log(`No files found matching ${targetPath}`); - } else if (!fs.existsSync(transformerPath)) { - console.log(`No transformer found matching ${transformer}`); - } else { - transformerArgs = transformerArgs.concat(['--transform', transformerPath]); - transformerArgs = transformerArgs.concat(files); - - // jscodeshift has too large size as node dependencies and better use in CLI env. - // Suggest user to install jscodeshift in codemod case. - spawnSync('jscodeshift', transformerArgs, - { - stdio: 'inherit', - stripEof: false, - }); - } -} diff --git a/packages/doctor/bin/options.json b/packages/doctor/bin/options.json index 99e941c2d..edc987dbe 100644 --- a/packages/doctor/bin/options.json +++ b/packages/doctor/bin/options.json @@ -14,11 +14,6 @@ "desc": "Scan target project directory.", "type": "string" }, - "codemod":{ - "alias": "c", - "desc": "codemod scripts that help update React(ICE) and Rax APIs.", - "type": "array" - }, "ignore": { "desc": "Ignore directories when scan.", "type": "array" diff --git a/packages/doctor/package.json b/packages/doctor/package.json index f4c6fce24..c7512df08 100644 --- a/packages/doctor/package.json +++ b/packages/doctor/package.json @@ -1,7 +1,7 @@ { "name": "@appworks/doctor", "description": "Analyse and running codemods over react/rax projects, troubleshooting and automatically fixing errors", - "version": "0.1.0", + "version": "0.2.0", "keywords": [ "doctor", "analysis", @@ -25,6 +25,7 @@ "prepublishOnly": "npm run compile" }, "dependencies": { + "@appworks/codemod": "^0.1.0", "@babel/parser": "^7.12.5", "@babel/traverse": "^7.12.5", "@iceworks/spec": "^1.0.2", diff --git a/packages/doctor/src/Analyzer.ts b/packages/doctor/src/Analyzer.ts index 4d9e64c64..52fe11890 100644 --- a/packages/doctor/src/Analyzer.ts +++ b/packages/doctor/src/Analyzer.ts @@ -20,13 +20,13 @@ const LANGUAGE_MAP = { const UNKNOWN_LANGUAGE = 'Other'; export default class Analyzer { - public options: IAnalyzerOptions; + options: IAnalyzerOptions; constructor(options: IAnalyzerOptions) { this.options = options; } - public analyse(directory: string): IAnalyzerReport { + analyse(directory: string): IAnalyzerReport { const report = { languages: [] } as IAnalyzerReport; const languageCache = {}; const files: IFileInfo[] = getFiles(directory, this.options.ignore); diff --git a/packages/doctor/src/Scanner.ts b/packages/doctor/src/Scanner.ts index c5a5ef975..4b693eb15 100644 --- a/packages/doctor/src/Scanner.ts +++ b/packages/doctor/src/Scanner.ts @@ -13,14 +13,14 @@ import getFinalScore from './getFinalScore'; const tempDir = path.join(__dirname, 'tmp/'); export default class Scanner { - public options: IScannerOptions; + options: IScannerOptions; constructor(options: IScannerOptions) { this.options = options; } // Entry - public async scan(directory: string, options?: IScanOptions): Promise { + async scan(directory: string, options?: IScanOptions): Promise { const timer = new Timer(); const reports = {} as IScannerReports; const tempFileDir = options?.tempFileDir || tempDir; @@ -67,6 +67,14 @@ export default class Scanner { }); } + // Run Codemod + if (!options || options.disableCodemod !== true) { + subprocessList.push(execa.node(path.join(__dirname, './workers/codemod/index.js'), [`${directory} ${tempFileDir} ${options?.transforms}`])); + processReportList.push(async () => { + reports.codemod = await fs.readJSON(path.join(tempFileDir, config.tmpFiles.report.codemod)); + }); + } + async function process() { // Check await Promise.all(subprocessList); @@ -79,6 +87,7 @@ export default class Scanner { (reports.ESLint || {}).score, (reports.maintainability || {}).score, (reports.repeatability || {}).score, + (reports.codemod || {}).score, ].filter((score) => !isNaN(score)), ); diff --git a/packages/doctor/src/Scorer.ts b/packages/doctor/src/Scorer.ts index e05e53c9b..70ab8181c 100644 --- a/packages/doctor/src/Scorer.ts +++ b/packages/doctor/src/Scorer.ts @@ -17,17 +17,17 @@ export default class Scanner { this.currentScore = options.start || this.highestScore; } - public plus(score: number): number { + plus(score: number): number { this.currentScore += score; return this.getScore(); } - public minus(score: number): number { + minus(score: number): number { this.currentScore -= score; return this.getScore(); } - public getAverage(list: number[]): number { + getAverage(list: number[]): number { if (list.length) { let sum = 0; @@ -46,7 +46,7 @@ export default class Scanner { return this.getScore(); } - public getScore(): number { + getScore(): number { if (this.currentScore > this.highestScore) { return this.highestScore; } else if (this.currentScore < this.lowestScore) { diff --git a/packages/doctor/src/Timer.ts b/packages/doctor/src/Timer.ts index c4b0387bd..5009b79b5 100644 --- a/packages/doctor/src/Timer.ts +++ b/packages/doctor/src/Timer.ts @@ -11,14 +11,14 @@ export default class Timer { this.startTime = Date.now(); } - public async raceTimeout(ms: number) { - await new Promise(resolve => { + async raceTimeout(ms: number) { + await new Promise((resolve) => { this.timer = setTimeout(resolve, ms); }); throw new Error('@appworks/doctor time out!'); } - public duration(): number { + duration(): number { if (this.timer) { clearTimeout(this.timer); } diff --git a/packages/doctor/src/config.ts b/packages/doctor/src/config.ts index cc4dd4147..bb389e04b 100644 --- a/packages/doctor/src/config.ts +++ b/packages/doctor/src/config.ts @@ -5,6 +5,7 @@ export default { eslint: 'eslint-result.json', escomplex: 'escomplex-result.json', jscpd: 'jscpd-result.json', + codemod: 'codemod-result.json', }, }, }; diff --git a/packages/doctor/src/index.ts b/packages/doctor/src/index.ts index 79a497e8d..452d742a2 100644 --- a/packages/doctor/src/index.ts +++ b/packages/doctor/src/index.ts @@ -8,9 +8,9 @@ import { IFileInfo } from './types/File'; // Ignore directories const defaultignore = ['.faas_debug_tmp', '.ice', '.rax', 'build', 'es', 'dist', 'lib', 'mocks', 'logs', 'coverage', 'node_modules', 'demo', 'examples', 'public', 'test', '__tests__']; class Doctor { - public options: any; + options: any; - public ignore: string[]; + ignore: string[]; private scanner: any; diff --git a/packages/doctor/src/transforms/plugin-rax-component-to-component.js b/packages/doctor/src/transforms/plugin-rax-component-to-component.js deleted file mode 100644 index ab682fb13..000000000 --- a/packages/doctor/src/transforms/plugin-rax-component-to-component.js +++ /dev/null @@ -1,113 +0,0 @@ -// Update `plugin-rax-component` to `plugin-component` -// https://rax.js.org/docs/guide/com-migration -const fs = require('fs'); -const path = require('path'); - -let demoOrder = 1; -// glob to reg: demo/*.{js,jsx,ts,tsx} -const DEMO_FILE_REG = /demo\/.*\.(js|jsx|ts|tsx)$/; - -const changeFileExt = (file, ext) => { - return path.join(path.dirname(file), path.basename(file, path.extname(file)) + ext); -}; - -module.exports = (fileInfo, api) => { - const j = api.jscodeshift; - const basename = path.basename(fileInfo.path); - if (basename === 'build.json') { - // { - // + "type": "rax", - // + "targets": ["web"], - // + "inlineStyle": true, - // "plugins": [ - // - ["build-plugin-rax-component", { - // - "type": "rax", - // - "targets": ["web"], - // - "forceInline": true - // - }] - // + "build-plugin-component" - // ] - // } - let config = JSON.parse(fileInfo.source); - const pluginList = config.plugins || []; - - for (let i = 0; i < pluginList.length; i++) { - if (pluginList[0][0] === 'build-plugin-rax-component') { - config = Object.assign({}, config, pluginList[0][1] || {}); - if (config.forceInline !== undefined) { - config.inlineStyle = config.forceInline; - delete config.forceInline; - } - config.plugins.splice(i, 1, 'build-plugin-component'); - } - } - return JSON.stringify(config, null, ' '); - } else if (basename === 'package.json') { - // { - // - "build-plugin-rax-component": "^0.2.14", - // + "build-plugin-component": "^1.0.0" - // } - const config = JSON.parse(fileInfo.source); - const { devDependencies = {} } = config; - if (devDependencies['build-plugin-rax-component']) { - devDependencies['build-plugin-component'] = '^1.0.0'; - delete devDependencies['build-plugin-rax-component']; - } - return JSON.stringify(config, null, ' '); - } else if (DEMO_FILE_REG.test(fileInfo.path)) { - // demo/xxx.jsx -> demo/xxx.md - // + --- - // + title: Baisc - // + order: 1 - // + --- - // + ```jsx - // + import { createElement } from 'rax'; - // - import { createElement, render } from 'rax'; - // - import DriverUniversal from 'driver-universal'; - // - import MyComponent from '../src/index'; - // + import MyComponent from 'rax-example'; - // - render(, document.body, { driver: DriverUniversal }); - // + function App(){ - // + return ; - // + } - // + export default App; - // + ``` - const transform = j(fileInfo.source); - // render -> export function - transform.find(j.ExpressionStatement).forEach(p => { - const { expression } = p.node; - if (expression.callee.name === 'render' && expression.arguments[0].type === 'JSXElement') { - const demoFnName = 'App'; - const demoJSXElement = expression.arguments[0]; - j(p).replaceWith( - j.functionDeclaration(j.identifier(demoFnName), [], j.blockStatement([j.returnStatement(demoJSXElement)])), - ).insertAfter( - j.exportDeclaration(true, { type: 'Identifier', name: demoFnName }), - ); - } - }); - // process import - transform.find(j.ImportDeclaration).forEach(p => { - const { node } = p; - // remove driver import like driver-universal - if (node.source.value.indexOf('driver-') > -1) { - j(p).remove(); - } - }); - // write xx.md file - fs.writeFileSync(changeFileExt(fileInfo.path, '.md'), - `${'---\n' + - 'title: Baisc\n' + - `order: ${demoOrder++}\n` + - '---\n' + - '\n' + - `${fileInfo.path} usage\n` + - '```jsx\n'}${ - transform.toSource() - }\`\`\`` - , 'utf8'); - return fileInfo.source; - } else { - return fileInfo.source; - } -}; diff --git a/packages/doctor/src/types/Analyzer.ts b/packages/doctor/src/types/Analyzer.ts index cee535048..d42476dd9 100644 --- a/packages/doctor/src/types/Analyzer.ts +++ b/packages/doctor/src/types/Analyzer.ts @@ -6,6 +6,6 @@ export interface IAnalyzerReport { filesInfo: { count: number; lines: number; - }, - languages: Array<{ language: string, count: number }> + }; + languages: Array<{ language: string; count: number }>; } diff --git a/packages/doctor/src/types/Scanner.ts b/packages/doctor/src/types/Scanner.ts index 74a886577..38f78187b 100644 --- a/packages/doctor/src/types/Scanner.ts +++ b/packages/doctor/src/types/Scanner.ts @@ -1,4 +1,5 @@ import { IClone } from '@jscpd/core'; +import { IResult } from '@appworks/codemod'; export interface IScannerOptions { ignore: string[]; @@ -7,12 +8,14 @@ export interface IScannerOptions { export interface IScanOptions { fix?: boolean; framework?: string; + transforms?: string[]; languageType?: 'js' | 'ts'; tempFileDir?: string; timeout?: number; disableESLint?: boolean; disableMaintainability?: boolean; disableRepeatability?: boolean; + disableCodemod?: boolean; maxRepeatabilityCheckLines?: number; } @@ -51,6 +54,11 @@ export interface IEslintReports { customConfig: any; } +export interface ICodemodReports { + score: number; + reports: IResult[]; +} + export interface IScannerReports { filesInfo: { count: number; @@ -61,4 +69,5 @@ export interface IScannerReports { ESLint?: IEslintReports; maintainability?: IMaintainabilityReports; repeatability?: IRepeatabilityReports; + codemod?: ICodemodReports; } diff --git a/packages/doctor/src/workers/codemod/getCodemodReports.ts b/packages/doctor/src/workers/codemod/getCodemodReports.ts new file mode 100644 index 000000000..32269a872 --- /dev/null +++ b/packages/doctor/src/workers/codemod/getCodemodReports.ts @@ -0,0 +1,47 @@ + +import { check, run, IResult } from '@appworks/codemod'; +import Scorer from '../../Scorer'; +import { ICodemodReports } from '../../types/Scanner'; +import { IFileInfo } from '../../types/File'; + +// level waring minus 2 point +const WARNING_WEIGHT = -2; +// level error minus 5 point +const ERROR_WEIGHT = -5; + +export default async function getCodemodReports(directory: string, files: IFileInfo[], transforms: string[]): Promise { + let reports: IResult[] = []; + const runResults: IResult[] = []; + + const scorer = new Scorer(); + const filesPathArr = files.map((file) => file.path); + + try { + // Run codemod first + for (let i = 0, l = transforms.length; i < l; i++) { + runResults.push(await run(directory, filesPathArr, transforms[i])); + } + + // Check recommended codemod + reports = await check(directory, filesPathArr); + + reports.forEach((report) => { + if (report.severity === 1) { + scorer.plus(WARNING_WEIGHT); + } else if (report.severity === 2) { + scorer.plus(ERROR_WEIGHT); + } + }); + + // concat `run` results + reports = reports.concat(runResults); + } catch (e) { + // ignore + console.log(e); + } + + return { + score: scorer.getScore(), + reports, + }; +} diff --git a/packages/doctor/src/workers/codemod/index.ts b/packages/doctor/src/workers/codemod/index.ts new file mode 100644 index 000000000..46efd0942 --- /dev/null +++ b/packages/doctor/src/workers/codemod/index.ts @@ -0,0 +1,19 @@ +import * as fs from 'fs-extra'; +import { join } from 'path'; +import getCodemodReports from './getCodemodReports'; +import config from '../../config'; + +const [directory, tempFileDir, transforms] = process.argv.slice(2)[0].split(' '); + +async function run() { + const files = fs.readJSONSync(join(tempFileDir, config.tmpFiles.files)); + const result = await getCodemodReports( + directory, + files, + transforms && transforms !== 'undefined' ? transforms.split(',') : [], + ); + + fs.writeFileSync(join(tempFileDir, config.tmpFiles.report.codemod), JSON.stringify(result)); +} + +run();