blob: b4f1097b6dcabc9fb5129bb3f9c165439defee4d [file] [log] [blame]
/*
* 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.
*/
/**
* [Create CommonJS files]:
* Compatible with prevoius folder structure: `echarts/lib` exists in `node_modules`
* (1) Build all files to CommonJS to `echarts/lib`.
* (2) Remove __DEV__.
* (3) Mount `echarts/src/export.js` to `echarts/lib/echarts.js`.
*
* [Create ESModule files]:
* Build all files to CommonJS to `echarts/esm`.
*/
const nodePath = require('path');
const assert = require('assert');
const fs = require('fs');
const fsExtra = require('fs-extra');
const chalk = require('chalk');
const ts = require('typescript');
const globby = require('globby');
const transformDEVUtil = require('./transform-dev');
const preamble = require('./preamble');
const dts = require('@lang/rollup-plugin-dts').default;
const rollup = require('rollup');
const ecDir = nodePath.resolve(__dirname, '..');
const tmpDir = nodePath.resolve(ecDir, 'pre-publish-tmp');
const tsConfig = readTSConfig();
const autoGeneratedFileAlert = `
/**
* AUTO-GENERATED FILE. DO NOT MODIFY.
*/
`;
const mainSrcGlobby = {
patterns: [
'src/**/*.ts'
],
cwd: ecDir
};
const extensionSrcGlobby = {
patterns: [
'extension-src/**/*.ts'
],
cwd: ecDir
};
const extensionSrcDir = nodePath.resolve(ecDir, 'extension-src');
const extensionESMDir = nodePath.resolve(ecDir, 'extension');
const typesDir = nodePath.resolve(ecDir, 'types');
const esmDir = 'lib';
const compileWorkList = [
{
logLabel: 'main ts -> js-esm',
compilerOptionsOverride: {
module: 'ES2015',
rootDir: ecDir,
outDir: tmpDir,
// Generate types when buidling esm
declaration: true,
declarationDir: typesDir
},
srcGlobby: mainSrcGlobby,
transformOptions: {
filesGlobby: {patterns: ['**/*.js'], cwd: tmpDir},
preamble: preamble.js,
transformDEV: true
},
before: async function () {
fsExtra.removeSync(tmpDir);
fsExtra.removeSync(nodePath.resolve(ecDir, 'types'));
fsExtra.removeSync(nodePath.resolve(ecDir, esmDir));
fsExtra.removeSync(nodePath.resolve(ecDir, 'index.js'));
fsExtra.removeSync(nodePath.resolve(ecDir, 'index.blank.js'));
fsExtra.removeSync(nodePath.resolve(ecDir, 'index.common.js'));
fsExtra.removeSync(nodePath.resolve(ecDir, 'index.simple.js'));
},
after: async function () {
fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.all.js'), nodePath.resolve(ecDir, 'index.js'));
fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.blank.js'), nodePath.resolve(ecDir, 'index.blank.js'));
fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.common.js'), nodePath.resolve(ecDir, 'index.common.js'));
fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.simple.js'), nodePath.resolve(ecDir, 'index.simple.js'));
fs.renameSync(nodePath.resolve(tmpDir, 'src'), nodePath.resolve(ecDir, esmDir));
transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.js'), esmDir);
transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.blank.js'), esmDir);
transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.common.js'), esmDir);
transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.simple.js'), esmDir);
await transformDistributionFiles(nodePath.resolve(ecDir, esmDir), esmDir);
await transformDistributionFiles(nodePath.resolve(ecDir, 'types'), esmDir);
fsExtra.removeSync(tmpDir);
}
},
{
logLabel: 'extension ts -> js-esm',
compilerOptionsOverride: {
module: 'ES2015',
rootDir: extensionSrcDir,
outDir: extensionESMDir
},
srcGlobby: extensionSrcGlobby,
transformOptions: {
filesGlobby: {patterns: ['**/*.js'], cwd: extensionESMDir},
preamble: preamble.js,
transformDEV: true
},
before: async function () {
fsExtra.removeSync(extensionESMDir);
},
after: async function () {
await transformDistributionFiles(extensionESMDir, 'lib');
}
}
];
/**
* @public
*/
module.exports = async function () {
for (let {
logLabel, compilerOptionsOverride, srcGlobby,
transformOptions, before, after
} of compileWorkList) {
process.stdout.write(chalk.green.dim(`[${logLabel}]: compiling ...`));
before && await before();
let srcPathList = await readFilePaths(srcGlobby);
await tsCompile(compilerOptionsOverride, srcPathList);
process.stdout.write(chalk.green.dim(` done \n`));
process.stdout.write(chalk.green.dim(`[${logLabel}]: transforming ...`));
await transformCode(transformOptions);
after && await after();
process.stdout.write(chalk.green.dim(` done \n`));
}
process.stdout.write(chalk.green.dim(`Generating entries ...`));
generateEntries();
process.stdout.write(chalk.green.dim(`Bundling DTS ...`));
await bundleDTS();
console.log(chalk.green.dim('All done.'));
};
async function runTsCompile(localTs, compilerOptions, srcPathList) {
// Must do it. becuase the value in tsconfig.json might be different from the inner representation.
// For example: moduleResolution: "NODE" => moduleResolution: 2
const {options, errors} = localTs.convertCompilerOptionsFromJson(compilerOptions, ecDir);
if (errors.length) {
let errMsg = 'tsconfig parse failed: '
+ errors.map(error => error.messageText).join('. ')
+ '\n compilerOptions: \n' + JSON.stringify(compilerOptions, null, 4);
assert(false, errMsg);
}
// See: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API
let program = localTs.createProgram(srcPathList, options);
let emitResult = program.emit();
let allDiagnostics = localTs
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
allDiagnostics.forEach(diagnostic => {
if (diagnostic.file) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
let message = localTs.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
console.log(chalk.red(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`));
}
else {
console.log(chalk.red(localTs.flattenDiagnosticMessageText(diagnostic.messageText, '\n')));
}
});
assert(!emitResult.emitSkipped, 'ts compile failed.');
}
module.exports.runTsCompile = runTsCompile;
async function tsCompile(compilerOptionsOverride, srcPathList) {
assert(
compilerOptionsOverride
&& compilerOptionsOverride.module
&& compilerOptionsOverride.rootDir
&& compilerOptionsOverride.outDir
);
let compilerOptions = {
...tsConfig.compilerOptions,
...compilerOptionsOverride,
sourceMap: false
};
runTsCompile(ts, compilerOptions, srcPathList);
}
/**
* Transform import/require path in the entry file to `esm` or `lib`.
*/
function transformRootFolderInEntry(entryFile, replacement) {
let code = fs.readFileSync(entryFile, 'utf-8');
// Simple regex replacement
// TODO More robust way?
assert(
!/(import\s+|from\s+|require\(\s*)["']\.\/echarts\./.test(code)
&& !/(import\s+|from\s+|require\(\s*)["']echarts\./.test(code),
'Import echarts.xxx.ts is not supported.'
);
code = code.replace(/((import\s+|from\s+|require\(\s*)["'])\.\//g, `$1./${replacement}/`);
fs.writeFileSync(
entryFile,
// Also transform zrender.
singleTransformZRRootFolder(code, replacement),
'utf-8'
);
}
/**
* Transform `zrender/src` to `zrender/esm` in all files
*/
async function transformDistributionFiles(rooltFolder, replacement) {
const files = await readFilePaths({
patterns: ['**/*.js', '**/*.d.ts'],
cwd: rooltFolder
});
// Simple regex replacement
// TODO More robust way?
for (let fileName of files) {
let code = fs.readFileSync(fileName, 'utf-8');
code = singleTransformZRRootFolder(code, replacement);
// For lower ts version, not use import type
// TODO Use https://github.com/sandersn/downlevel-dts ?
// if (fileName.endsWith('.d.ts')) {
// code = singleTransformImportType(code);
// }
fs.writeFileSync(fileName, code, 'utf-8');
}
}
/**
* Remove __esModule mark.
*
* In the 4.x version. The exported CJS don't have __esModule mark.
* Developers can use `import echarts from 'echarts/lib/echarts' instead of
* `import * as echarts from 'echarts/lib/echarts'` to import all the exported methods.
* It's not recommand but developers may still have the chance to do it.
* But in the tsc export with __esModule mark. This will get an undefined export.
* To avoid breaking this kind of code. We remove __esModule mark manually here.
*/
function removeESmoduleMark() {
const filePath = nodePath.resolve(ecDir, 'lib/echarts.js');
const code = fs.readFileSync(filePath, 'utf-8')
.replace('\nexports.__esModule = true;\n', '');
if (code.indexOf('__esModule') >= 0) {
throw new Error('Seems still has __esModule mark');
}
fs.writeFileSync(filePath, code, 'utf-8');
}
function singleTransformZRRootFolder(code, replacement) {
return code.replace(/([\"\'])zrender\/src\//g, `$1zrender/${replacement}/`);
}
// function singleTransformImportType(code) {
// return code.replace(/import\s+type\s+/g, 'import ');
// }
/**
* @param {Object} transformOptions
* @param {Object} transformOptions.filesGlobby {patterns: string[], cwd: string}
* @param {string} [transformOptions.preamble] See './preamble.js'
* @param {boolean} [transformOptions.transformDEV]
*/
async function transformCode({filesGlobby, preamble, transformDEV}) {
let filePaths = await readFilePaths(filesGlobby);
filePaths.map(filePath => {
let code = fs.readFileSync(filePath, 'utf8');
if (transformDEV) {
let result = transformDEVUtil.transform(code, false);
code = result.code;
}
code = autoGeneratedFileAlert + code;
if (preamble) {
code = preamble + code;
}
fs.writeFileSync(filePath, code, 'utf8');
});
}
async function readFilePaths({patterns, cwd}) {
assert(patterns && cwd);
return (
await globby(patterns, {cwd})
).map(
srcPath => nodePath.resolve(cwd, srcPath)
);
}
async function bundleDTS() {
const outDir = nodePath.resolve(__dirname, '../types/dist');
const commonConfig = {
onwarn(warning, rollupWarn) {
// Not warn circular dependency
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
rollupWarn(warning);
}
},
plugins: [
dts({
respectExternal: true
})
// {
// generateBundle(options, bundle) {
// for (let chunk of Object.values(bundle)) {
// chunk.code = `
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
// ${chunk.code}`
// }
// }
// }
]
};
// Bundle chunks.
const parts = [
'core', 'charts', 'components', 'renderers', 'option'
];
const inputs = {};
parts.forEach(partName => {
inputs[partName] = nodePath.resolve(__dirname, `../types/src/export/${partName}.d.ts`)
});
const bundle = await rollup.rollup({
input: inputs,
...commonConfig
});
let idx = 1;
await bundle.write({
dir: outDir,
minifyInternalExports: false,
manualChunks: (id) => {
// Only create one chunk.
return 'shared';
},
chunkFileNames: 'shared.d.ts'
});
// Bundle all in one
const bundleAllInOne = await rollup.rollup({
input: nodePath.resolve(__dirname, `../types/src/export/all.d.ts`),
...commonConfig
});
await bundleAllInOne.write({
file: nodePath.resolve(outDir, 'echarts.d.ts')
});
}
function readTSConfig() {
// tsconfig.json may have comment string, which is invalid if
// using `require('tsconfig.json'). So we use a loose parser.
let filePath = nodePath.resolve(ecDir, 'tsconfig.json');
const tsConfigText = fs.readFileSync(filePath, {encoding: 'utf8'});
return (new Function(`return ( ${tsConfigText} )`))();
}
function generateEntries() {
['charts', 'components', 'renderers', 'core'].forEach(entryName => {
if (entryName !== 'option') {
const jsCode = fs.readFileSync(nodePath.join(__dirname, `template/${entryName}.js`), 'utf-8');
fs.writeFileSync(nodePath.join(__dirname, `../${entryName}.js`), jsCode, 'utf-8');
}
const dtsCode = fs.readFileSync(nodePath.join(__dirname, `/template/${entryName}.d.ts`), 'utf-8');
fs.writeFileSync(nodePath.join(__dirname, `../${entryName}.d.ts`), dtsCode, 'utf-8');
});
}
module.exports.readTSConfig = readTSConfig;