This repository has been archived by the owner on Mar 28, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add doctor command to check potential problems (#56)
* feat: add basic doctor command * feat: doctor support regular checkup * feat: add regular checkup rules * feat: doctor support source checkup * feat: add source checkup rules * feat: doctor support source imports checkup * feat: add source imports checkup rules * test: add cases for doctor command & rules * test: correct case * docs: add simple doctor command description
- Loading branch information
1 parent
e60eef0
commit f746159
Showing
29 changed files
with
647 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { chalk } from '@umijs/utils'; | ||
import doctor, { registerRules } from '../doctor'; | ||
import { IApi } from '../types'; | ||
|
||
function getSummaryLog(summary: { error: number; warn: number }) { | ||
const color = summary.error ? chalk.red : chalk.yellow; | ||
const total = summary.error + summary.warn; | ||
|
||
return color.bold(` | ||
💊 ${total} problems (${summary.error} errors ${summary.warn} warnings)`); | ||
} | ||
|
||
export default (api: IApi) => { | ||
registerRules(api); | ||
|
||
api.registerCommand({ | ||
name: 'doctor', | ||
description: 'check your project for potential problems', | ||
async fn() { | ||
const report = await doctor(api); | ||
const summary = { error: 0, warn: 0 }; | ||
|
||
report | ||
.sort((p) => (p.type === 'error' ? -1 : 1)) | ||
.forEach((item) => { | ||
summary[item.type] += 1; | ||
console.log(` | ||
${chalk[item.type === 'error' ? 'red' : 'yellow']( | ||
`${item.type.toUpperCase()}`.padStart(8), | ||
)} ${item.problem} | ||
${chalk.green('SOLUTION')} ${item.solution}`); | ||
}); | ||
|
||
if (summary.error || summary.warn) { | ||
console.log(getSummaryLog(summary)); | ||
|
||
if (summary.error) { | ||
process.exit(1); | ||
} | ||
} else { | ||
console.log(chalk.bold.green('🎉 This project looks fine!')); | ||
} | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { glob, lodash } from '@umijs/utils'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import { | ||
IBundleConfig, | ||
IBundlessConfig, | ||
normalizeUserConfig as getBuildConfig, | ||
} from '../builder/config'; | ||
import { DEFAULT_BUNDLESS_IGNORES } from '../constants'; | ||
import { getConfig as getPreBundleConfig } from '../prebundler/config'; | ||
import { IFatherBuildTypes, type IApi } from '../types'; | ||
import sourceParser from './parser'; | ||
|
||
export type IDoctorReport = { | ||
type: 'error' | 'warn'; | ||
problem: string; | ||
solution: string; | ||
}[]; | ||
|
||
/** | ||
* register all built-in rules | ||
*/ | ||
export function registerRules(api: IApi) { | ||
const ruleDir = path.join(__dirname, 'rules'); | ||
const rules = fs | ||
.readdirSync(ruleDir, { withFileTypes: true }) | ||
.filter((f) => f.isFile() && /(?<!\.d)\.(j|t)s$/.test(f.name)) | ||
.map((f) => path.join(ruleDir, f.name)); | ||
|
||
api.registerPlugins(rules); | ||
} | ||
|
||
/** | ||
* get top-level source dirs from configs | ||
*/ | ||
export function getSourceDirs( | ||
bundleConfigs: IBundleConfig[], | ||
bundlessConfigs: IBundlessConfig[], | ||
) { | ||
const configDirs = lodash.uniq([ | ||
...bundleConfigs.map((c) => path.dirname(c.entry)), | ||
...bundlessConfigs.map((c) => c.input), | ||
]); | ||
|
||
return [...configDirs].filter((d, i) => | ||
configDirs.every((dir, j) => i === j || !d.startsWith(dir)), | ||
); | ||
} | ||
|
||
export default async (api: IApi): Promise<IDoctorReport> => { | ||
// generate configs | ||
const [bundleConfigs, bundlessConfigs] = getBuildConfig( | ||
api.config, | ||
api.pkg, | ||
).reduce<[IBundleConfig[], IBundlessConfig[]]>( | ||
(ret, config) => { | ||
if (config.type === IFatherBuildTypes.BUNDLE) { | ||
ret[0].push(config); | ||
} else { | ||
ret[1].push(config); | ||
} | ||
|
||
return ret; | ||
}, | ||
[[], []], | ||
); | ||
const preBundleConfig = getPreBundleConfig({ | ||
userConfig: api.config.prebundle || {}, | ||
pkg: api.pkg, | ||
cwd: api.cwd, | ||
}); | ||
|
||
// collect all source files | ||
const sourceDirs = getSourceDirs(bundleConfigs, bundlessConfigs); | ||
const sourceFiles = sourceDirs.reduce<string[]>( | ||
(ret, dir) => | ||
ret.concat( | ||
glob.sync(`${dir}/**`, { | ||
cwd: api.cwd, | ||
ignore: DEFAULT_BUNDLESS_IGNORES, | ||
nodir: true, | ||
}), | ||
), | ||
[], | ||
); | ||
|
||
// collect all alias & externals | ||
// TODO: split bundle & bundless checkup, because externals not work for bundle | ||
const mergedAlias: Record<string, string[]> = {}; | ||
const mergedExternals: Record<string, true> = {}; | ||
|
||
[...bundleConfigs, ...bundlessConfigs].forEach((c) => { | ||
Object.entries(c.alias || {}).forEach(([k, v]) => { | ||
mergedAlias[k] ??= []; | ||
mergedAlias[k].push(v); | ||
}); | ||
|
||
if ('externals' in c) { | ||
Object.entries(c.externals || {}).forEach(([k, v]) => { | ||
mergedExternals[k] = true; | ||
}); | ||
} | ||
}); | ||
|
||
// regular checkup | ||
const regularReport: IDoctorReport = await api.applyPlugins({ | ||
key: 'addRegularCheckup', | ||
args: { bundleConfigs, bundlessConfigs, preBundleConfig }, | ||
}); | ||
|
||
// source checkup | ||
const sourceReport: IDoctorReport = []; | ||
|
||
for (const file of sourceFiles) { | ||
sourceReport.push( | ||
...(await api.applyPlugins({ | ||
key: 'addSourceCheckup', | ||
args: { | ||
file, | ||
content: fs.readFileSync(path.join(api.cwd, file), 'utf-8'), | ||
}, | ||
})), | ||
); | ||
} | ||
|
||
// imports checkup | ||
const importsReport: IDoctorReport = []; | ||
|
||
for (const file of sourceFiles) { | ||
importsReport.push( | ||
...(await api.applyPlugins({ | ||
key: 'addImportsCheckup', | ||
args: { | ||
file, | ||
imports: (await sourceParser(path.join(api.cwd, file))).imports, | ||
mergedAlias, | ||
mergedExternals, | ||
}, | ||
})), | ||
); | ||
} | ||
|
||
return [ | ||
...regularReport.filter(Boolean), | ||
...sourceReport.filter(Boolean), | ||
...importsReport.filter(Boolean), | ||
]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { | ||
build, | ||
type OnResolveArgs, | ||
} from '@umijs/bundler-utils/compiled/esbuild'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import { getCache } from '../utils'; | ||
|
||
export type IDoctorSourceParseResult = { | ||
imports: Omit<OnResolveArgs, 'pluginData'>[]; | ||
}; | ||
|
||
export default async ( | ||
fileAbsPath: string, | ||
): Promise<IDoctorSourceParseResult> => { | ||
const cache = getCache('doctor-parser'); | ||
// format: {path:mtime} | ||
const cacheKey = [fileAbsPath, fs.statSync(fileAbsPath).mtimeMs].join(':'); | ||
const cacheRet = cache.getSync(cacheKey, ''); | ||
const ret: IDoctorSourceParseResult = { imports: [] }; | ||
|
||
if (cacheRet) return cacheRet; | ||
|
||
await build({ | ||
// do not emit file | ||
write: false, | ||
// enable bundle for trigger onResolve hook, but all deps will be externalized | ||
bundle: true, | ||
logLevel: 'silent', | ||
format: 'esm', | ||
target: 'esnext', | ||
// esbuild need relative entry path | ||
entryPoints: [path.basename(fileAbsPath)], | ||
absWorkingDir: path.dirname(fileAbsPath), | ||
plugins: [ | ||
{ | ||
name: 'plugin-father-doctor', | ||
setup: (builder) => { | ||
builder.onResolve({ filter: /.*/ }, ({ pluginData, ...args }) => { | ||
if (args.kind !== 'entry-point') { | ||
ret.imports.push(args); | ||
|
||
return { | ||
path: args.path, | ||
// make all deps external | ||
external: true, | ||
}; | ||
} | ||
}); | ||
}, | ||
}, | ||
], | ||
}); | ||
|
||
cache.set(cacheKey, ret); | ||
|
||
return ret; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { IDoctorReport } from '..'; | ||
import type { IApi } from '../../types'; | ||
|
||
export default (api: IApi) => { | ||
api.addRegularCheckup(() => { | ||
const warns: IDoctorReport = []; | ||
|
||
if (api.pkg.peerDependencies && api.pkg.dependencies) { | ||
Object.keys(api.pkg.peerDependencies).forEach((pkg) => { | ||
if (api.pkg.dependencies![pkg]) { | ||
warns.push({ | ||
type: 'warn', | ||
problem: `The package \`${pkg}\` is both a peerDependency and a dependency`, | ||
solution: | ||
'Remove one from the package.json file base on project requirements', | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
return warns; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import type { IDoctorReport } from '..'; | ||
import type { IApi } from '../../types'; | ||
|
||
export default (api: IApi) => { | ||
let hasStyleProblem = false; | ||
|
||
api.addSourceCheckup(({ file, content }) => { | ||
if ( | ||
api.pkg.sideEffects === false && | ||
!hasStyleProblem && | ||
/\.(j|t)sx?$/.test(file) && | ||
/\simport\s+['"]\.[^'"]+\.(less|css|sass|scss)/.test(content) | ||
) { | ||
hasStyleProblem = true; | ||
|
||
return { | ||
type: 'error', | ||
problem: | ||
'Source file contains style imports, and the `"sideEffects": false` will causes styles lost after tree-shaking', | ||
solution: | ||
'Correct `sideEffects` config in the package.json file, such as `"sideEffects": ["**/*.less"]`', | ||
}; | ||
} | ||
}); | ||
|
||
api.addRegularCheckup(() => { | ||
if (Array.isArray(api.pkg.sideEffects)) { | ||
const result: IDoctorReport = []; | ||
|
||
api.pkg.sideEffects.forEach((s) => { | ||
if (s.startsWith('*.')) { | ||
result.push({ | ||
type: 'warn', | ||
problem: `The \`${s}\` sideEffect syntax only match top-level files in Rollup.js, but match all in Webpack`, | ||
solution: | ||
'Prefix `**/` for this sideEffect value in the package.json file', | ||
}); | ||
} | ||
}); | ||
|
||
return result; | ||
} | ||
}); | ||
}; |
Oops, something went wrong.