/* * 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 previous 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 { transformImport } = require('zrender/build/transformImport.js'); 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 ssrClientGlobby = { patterns: [ 'ssr/client/src/**/*.ts' ], cwd: ecDir }; const ssrClientSrcDir = nodePath.resolve(ecDir, 'ssr/client/src'); const ssrClientESMDir = nodePath.resolve(ecDir, 'ssr/client/lib'); const ssrClientTypeDir = nodePath.resolve(ecDir, 'ssr/client/types'); 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 building 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 transformLibFiles(nodePath.resolve(ecDir, esmDir), esmDir); await transformLibFiles(nodePath.resolve(ecDir, 'types'), esmDir); fsExtra.removeSync(tmpDir); } }, { logLabel: 'extension ts -> js-esm', compilerOptionsOverride: { module: 'ES2015', declaration: false, 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 transformLibFiles(extensionESMDir, 'lib'); } }, { logLabel: 'ssr client ts -> js-esm', compilerOptionsOverride: { module: 'ES2015', declaration: true, rootDir: ssrClientSrcDir, outDir: ssrClientESMDir, declarationDir: ssrClientTypeDir }, srcGlobby: ssrClientGlobby, transformOptions: { filesGlobby: {patterns: ['**/*.js'], cwd: ssrClientESMDir}, transformDEV: true }, before: async function () { fsExtra.removeSync(ssrClientESMDir); }, after: async function () { await transformLibFiles(ssrClientESMDir, '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, because 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'))); } }); if (allDiagnostics.length > 0) { throw new Error('TypeScript 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. singleTransformImport(code, replacement), 'utf-8' ); } /** * Transform `zrender/src` to `zrender/lib` in all files */ async function transformLibFiles(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 = singleTransformImport(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'); } } /** * 1. Transform zrender/src to zrender/lib * 2. Add .js extensions */ function singleTransformImport(code, replacement) { return transformImport( code.replace(/([\"\'])zrender\/src\//g, `$1zrender/${replacement}/`), (moduleName) => { // Ignore 'tslib' and 'echarts' in the extensions. if (moduleName === 'tslib' || moduleName === 'echarts') { return moduleName; } else if (moduleName === 'zrender/lib/export') { throw new Error('Should not import the whole zrender library.'); } else if (moduleName.endsWith('.ts')) { // Replace ts with js return moduleName.replace(/\.ts$/, '.js'); } else if (moduleName.endsWith('.js')) { return moduleName; } else { return moduleName + '.js' } } ); } // 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) ); } // Bundle can be used in echarts-examples. 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 = Pick>; // ${chunk.code}` // } // } // } ] }; // Bundle chunks. const parts = [ 'core', 'charts', 'components', 'renderers', 'option', 'features' ]; 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', 'features', 'ssr/client/index'].forEach(entryPath => { if (entryPath !== 'option') { const jsCode = fs.readFileSync(nodePath.join(__dirname, `template/${entryPath}.js`), 'utf-8'); fs.writeFileSync(nodePath.join(__dirname, `../${entryPath}.js`), jsCode, 'utf-8'); } // Make the d.ts in the same dir as .js, so that the can be found by tsc. // package.json "types" in "exports" does not always seam to work. const dtsCode = fs.readFileSync(nodePath.join(__dirname, `/template/${entryPath}.d.ts`), 'utf-8'); fs.writeFileSync(nodePath.join(__dirname, `../${entryPath}.d.ts`), dtsCode, 'utf-8'); }); } module.exports.readTSConfig = readTSConfig;