build.js 8.56 KB
#!/usr/bin/env node

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

const fsExtra = require('fs-extra');
const fs = require('fs');
const {resolve} = require('path');
const config = require('./config.js');
const commander = require('commander');
const {build, watch, color} = require('zrender/build/helper');
const ecLangPlugin = require('./rollup-plugin-ec-lang');
const prePublish = require('./pre-publish');
const recheckDEV = require('zrender/build/babel-plugin-transform-remove-dev').recheckDEV;

function run() {

    /**
     * Tips for `commander`:
     * (1) If arg xxx not specified, `commander.xxx` is undefined.
     *     Otherwise:
     *      If '-x, --xxx', `commander.xxx` can only be true/false, even if '--xxx yyy' input.
     *      If '-x, --xxx <some>', the 'some' string is required, or otherwise error will be thrown.
     *      If '-x, --xxx [some]', the 'some' string is optional, that is, `commander.xxx` can be boolean or string.
     * (2) `node ./build/build.js --help` will print helper info and exit.
     */

    let descIndent = '                                 ';
    let egIndent = '    ';

    commander
        .usage('[options]')
        .description([
            'Build echarts and generate result files in directory `echarts/dist`.',
            '',
            '  For example:',
            '',
            egIndent + 'node build/build.js --release'
                + '\n' + descIndent + '# Build all to `dist` folder.',
            egIndent + 'node build/build.js --prepublish'
                + '\n' + descIndent + '# Only prepublish.',
            egIndent + 'node build/build.js --removedev'
                + '\n' + descIndent + '# Remove __DEV__ code. If --min, __DEV__ always be removed.',
            egIndent + 'node build/build.js --type ""'
                + '\n' + descIndent + '# Only generate `dist/echarts.js`.',
            egIndent + 'node build/build.js --type common --min'
                + '\n' + descIndent + '# Only generate `dist/echarts.common.min.js`.',
            egIndent + 'node build/build.js --type simple --min --lang en'
                + '\n' + descIndent + '# Only generate `dist/echarts-en.simple.min.js`.',
            egIndent + 'node build/build.js --lang "my/lang.js" -i "my/index.js" -o "my/bundle.js"'
                + '\n' + descIndent + '# Take `<cwd>/my/index.js` as input and generate `<cwd>/my/bundle.js`,'
                + '\n' + descIndent + 'where `<cwd>/my/lang.js` is used as language file.',
        ].join('\n'))
        .option(
            '-w, --watch', [
            'Watch modifications of files and auto-compile to dist file. For example,',
            descIndent + '`echarts/dist/echarts.js`.'
        ].join('\n'))
        .option(
            '--lang <language file path or shortcut>', [
            'Use the specified file instead of `echarts/src/lang.js`. For example:',
            descIndent + '`--lang en` will use `echarts/src/langEN.js`.',
            descIndent + '`--lang my/langDE.js` will use `<cwd>/my/langDE.js`. -o must be specified in this case.',
            descIndent + '`--lang /my/indexSW.js` will use `/my/indexSW.js`. -o must be specified in this case.'
        ].join('\n'))
        .option(
            '--release',
            'Build all for release'
        )
        .option(
            '--prepublish',
            'Build all for release'
        )
        .option(
            '--removedev',
            'Remove __DEV__ code. If --min, __DEV__ always be removed.'
        )
        .option(
            '--min',
            'Whether to compress the output file, and remove error-log-print code.'
        )
        .option(
            '--type <type name>', [
            'Can be "simple" or "common" or "" (default). For example,',
            descIndent + '`--type ""` or `--type "common"`.'
        ].join('\n'))
        .option(
            '--sourcemap',
            'Whether output sourcemap.'
        )
        .option(
            '--format <format>',
            'The format of output bundle. Can be "umd", "amd", "iife", "cjs", "es".'
        )
        .option(
            '-i, --input <input file path>',
            'If input file path is specified, output file path must be specified too.'
        )
        .option(
            '-o, --output <output file path>',
            'If output file path is specified, input file path must be specified too.'
        )
        .parse(process.argv);

    let isWatch = !!commander.watch;
    let isRelease = !!commander.release;
    let isPrePublish = !!commander.prepublish;

    let opt = {
        lang: commander.lang,
        min: commander.min,
        type: commander.type || '',
        input: commander.input,
        output: commander.output,
        format: commander.format,
        sourcemap: commander.sourcemap,
        removeDev: commander.removedev,
        addBundleVersion: isWatch
    };

    validateIO(opt.input, opt.output);
    validateLang(opt.lang, opt.output);

    normalizeParams(opt);

    // Clear `echarts/dist`
    if (isRelease) {
        fsExtra.removeSync(getPath('./dist'));
    }

    if (isWatch) {
        watch(config.createECharts(opt));
    }
    else if (isPrePublish) {
        prePublish();
    }
    else if (isRelease) {
        let configs = [];
        let configForCheck;

        [
            {min: false},
            {min: true},
            {min: false, lang: 'en'},
            {min: true, lang: 'en'}
        ].forEach(function (opt) {

            ['', 'simple', 'common'].forEach(function (type) {
                let singleOpt = Object.assign({type}, opt);
                normalizeParams(singleOpt);
                let singleConfig = config.createECharts(singleOpt);
                configs.push(singleConfig);

                if (singleOpt.min && singleOpt.type === '') {
                    configForCheck = singleConfig;
                }
            });
        });

        configs.push(
            config.createBMap(false),
            config.createBMap(true),
            config.createDataTool(false),
            config.createDataTool(true)
        );

        build(configs)
            .then(function () {
                checkCode(configForCheck);
                prePublish();
            }).catch(handleBuildError);
    }
    else {
        let cfg = config.createECharts(opt);
        build([cfg])
            .then(function () {
                if (opt.removeDev) {
                    checkCode(cfg);
                }
            })
            .catch(handleBuildError);
    }
}

function normalizeParams(opt) {
    if (opt.sourcemap == null) {
        opt.sourcemap = !(opt.min || opt.type);
    }
    if (opt.removeDev == null) {
        opt.removeDev = !!opt.min;
    }
}

function handleBuildError(err) {
    console.log(err);
}

function checkCode(singleConfig) {
    // Make sure __DEV__ is eliminated.
    let code = fs.readFileSync(singleConfig.output.file, {encoding: 'utf-8'});
    if (!code) {
        throw new Error(`${singleConfig.output.file} is empty`);
    }
    recheckDEV(code);
    console.log(color('fgGreen', 'dim')('Check code: correct.'));
}

function validateIO(input, output) {
    if ((input != null && output == null)
        || (input == null && output != null)
    ) {
        throw new Error('`input` and `output` must be both set.');
    }
}

function validateLang(lang, output) {
    if (!lang) {
        return;
    }

    let langInfo = ecLangPlugin.getLangFileInfo(lang);

    if (langInfo.isOuter && !output) {
        throw new Error('`-o` or `--output` must be specified if using a file path in `--lang`.');
    }
    if (!langInfo.absolutePath || !fs.statSync(langInfo.absolutePath).isFile()) {
        throw new Error(`File ${langInfo.absolutePath} does not exist yet. Contribution is welcome!`);
    }
}

/**
 * @param {string} relativePath Based on echarts directory.
 * @return {string} Absolute path.
 */
function getPath(relativePath) {
    return resolve(__dirname, '../', relativePath);
}

run();