midway-bin
Version:
Midway Command Tools
290 lines (259 loc) • 8.53 kB
JavaScript
;
const Command = require('egg-bin').Command;
const path = require('path');
const fs = require('fs');
const rimraf = require('mz-modules/rimraf');
const fse = require('fs-extra');
const globby = require('globby');
const ncc = require('@midwayjs/ncc');
const terser = require('terser');
let typescript;
const shebangRegEx = /^#![^\n\r]*[\r\n]/;
const inlineSourceMapRegEx = /\/\/# sourceMappingURL=data:application\/json;base64,(.*)/;
class BuildCommand extends Command {
constructor(rawArgv) {
super(rawArgv);
this.usage = 'Usage: midway-bin build [options]';
this.options = {
clean: {
description: 'clean build target dir',
type: 'boolean',
alias: 'c',
},
project: {
description: 'project file location',
type: 'string',
alias: 'p',
default: 'tsconfig.json',
},
srcDir: {
description: 'source code path',
type: 'string',
default: 'src',
},
entrypoint: {
description: 'bundle the source with the file given as entrypoint',
type: 'string',
default: '',
},
minify: {
type: 'boolean',
},
mode: {
description: 'bundle mode, "debug" or "release" (default)',
type: 'string',
default: 'release',
},
};
}
get description() {
return 'build application automatically';
}
async run(context) {
const { cwd, argv } = context;
const tscCli = require.resolve('typescript/bin/tsc');
const projectFile = path.join(cwd, argv.project);
if (!fs.existsSync(projectFile)) {
console.log(`[midway-bin] tsconfig.json not found in ${cwd}\n`);
return;
}
const tsConfig = require(projectFile);
const projectDir = this.projectDir = path.dirname(projectFile);
let outDir = this.inferCompilerOptions(tsConfig, 'outDir');
let outDirAbsolute;
if (outDir) {
outDir = path.resolve(projectDir, outDir);
outDir = path.relative(projectDir, outDir);
outDirAbsolute = path.join(projectDir, outDir);
}
if (argv.clean) {
await this.cleanDir(outDirAbsolute);
}
await this.copyFiles(projectDir, outDir, argv);
if (argv.mode !== 'release') {
argv.mode = 'debug';
}
if (argv.entrypoint) {
outDir = outDirAbsolute || path.resolve(projectDir, 'dist');
await this.bundle(path.resolve(cwd, argv.entrypoint), outDir,
{
sourceMap: this.inferCompilerOptions(tsConfig, 'sourceMap', { projectDir }),
mode: argv.mode,
});
return;
}
const args = [];
if (argv.project) {
args.push('-p');
args.push(argv.project);
}
await this.helper.forkNode(tscCli, args, { cwd, execArgv: [] });
if (argv.minify) {
await this.minify(tsConfig, outDirAbsolute);
}
}
async bundle(entry, outDir, { sourceMap = false, mode } = {}) {
// Assets is an object of asset file names to { source, permissions, symlinks }
// expected relative to the output code (if any)
const options = {
// provide a custom cache path or disable caching
cache: false,
// externals to leave as requires of the build
externals: [],
// directory outside of which never to emit assets
filterAssetBase: process.cwd(),
minify: false,
sourceMap,
// default treats sources as output-relative
sourceMapBasePrefix: path.relative(outDir, process.cwd()) + '/',
// Node.js v12 comes with builtin source map support.
sourceMapRegister: false,
watch: false,
v8cache: false,
quiet: false,
debugLog: false,
};
if (mode === 'release') {
options.minify = true;
}
const { error, code, map, assets, symlinks } = await ncc(entry, options);
if (error) {
console.error(error);
return;
}
fse.mkdirpSync(outDir);
let basename = path.basename(entry, '.ts');
if (!basename.endsWith('.js')) {
basename += '.js';
}
fs.writeFileSync(outDir + '/' + basename, code, { mode: code.match(shebangRegEx) ? 0o777 : 0o666 });
if (map) fs.writeFileSync(outDir + '/index.js.map', map);
for (const asset of Object.keys(assets)) {
const assetPath = outDir + '/' + asset;
fse.mkdirpSync(path.dirname(assetPath));
fs.writeFileSync(assetPath, assets[asset].source, { mode: assets[asset].permissions });
}
for (const symlink of Object.keys(symlinks)) {
const symlinkPath = outDir + '/' + symlink;
fs.symlinkSync(symlinks[symlink], symlinkPath);
}
}
async cleanDir(dir) {
if (dir) {
await rimraf(dir);
}
}
async copyFiles(from, outDir, argv) {
if (outDir && fs.existsSync(path.join(from, 'package.json'))) {
const pkg = require(path.join(from, 'package.json'));
if (pkg['midway-bin-build'] && pkg['midway-bin-build'].include) {
for (const file of pkg['midway-bin-build'].include) {
if (typeof file === 'string' && !/\*/.test(file)) {
const srcDir = path.join(argv.srcDir, file);
const targetDir = path.join(outDir, file);
// 目录,或者不含通配符的普通文件
this.copyFile(srcDir, targetDir, from);
} else {
// 通配符的情况
const paths = await globby([].concat(file), {
cwd: path.join(from, argv.srcDir),
});
for (const p of paths) {
this.copyFile(
path.join(argv.srcDir, p),
path.join(outDir, p),
from
);
}
}
}
}
}
}
copyFile(srcFile, targetFile, from) {
if (!fs.existsSync(srcFile)) {
console.warn(`[midway-bin] can't find ${srcFile} and skip it`);
} else {
fse.copySync(path.join(from, srcFile), path.join(from, targetFile));
console.log(`[midway-bin] copy ${srcFile} to ${targetFile} success!`);
}
}
inferCompilerOptions(tsConfig, optionKeyPath, { projectDir = process.cwd() } = {}) {
// if projectFile extended and without the option,
// get setting from its parent
if (tsConfig && tsConfig.extends) {
if (
!tsConfig.compilerOptions ||
(tsConfig.compilerOptions && !tsConfig.compilerOptions[optionKeyPath])
) {
return this.inferCompilerOptions(require(path.join(projectDir, tsConfig.extends)), optionKeyPath, { projectDir });
}
}
if (tsConfig && tsConfig.compilerOptions) {
return tsConfig.compilerOptions[optionKeyPath];
}
}
async minify(tsConfig, outDir) {
if (typescript == null) {
typescript = require('typescript');
}
const inlineSourceMap = !!tsConfig.compilerOptions.inlineSourceMap;
const sourceMap = inlineSourceMap || tsConfig.compilerOptions.sourceMap;
if (!sourceMap) {
return;
}
let files;
if (outDir) {
files = globby.sync([ '**/*.js' ], {
cwd: outDir,
ignore: [ '**/node_modules' ],
});
files = files.map(it => path.join(outDir, it));
} else {
const host = typescript.createCompilerHost(tsConfig.compilerOptions);
files = host.readDirectory(__dirname, [ '.ts' ], tsConfig.exclude, tsConfig.include);
files = files.map(it => {
return path.join(path.dirname(it), path.basename(it, '.js'));
});
}
for (const file of files) {
let code = await fse.readFile(file, 'utf8');
let map;
if (inlineSourceMap) {
map = this.parseInlineSourceMap(code);
} else {
map = await fse.readFile(file + '.map', 'utf8');
}
map = JSON.parse(map);
const result = terser.minify(code, {
compress: false,
mangle: {
keep_classnames: true,
keep_fnames: true,
},
sourceMap: {
content: map,
filename: path.basename(file),
url: inlineSourceMap ? 'inline' : `${path.basename(file)}.map`,
},
});
({ code, map } = result);
if (code == null) {
break;
}
if (!inlineSourceMap) {
await fse.writeFile(file + '.map', map, 'utf8');
}
await fse.writeFile(file, code, 'utf8');
}
}
parseInlineSourceMap(code) {
const match = inlineSourceMapRegEx.exec(code);
if (match == null) {
return;
}
const map = Buffer.from(match[1], 'base64').toString('utf8');
return map;
}
}
module.exports = BuildCommand;