diff --git a/Gruntfile.js b/Gruntfile.js index ffae32ce35..c0af1ec0c0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -33,9 +33,9 @@ const fs = require('fs'); const cp = require('child_process'); const path = require('path'); const buildUtils = require('./js/build-utils'); -const BundlerGeneral = require("./js/bundler/bundler-general"); +const {TemplateBundler, Bundler} = require('espo-frontend-build-tools'); const LayoutTypeBundler = require('./js/layout-template-bundler'); -const TemplateBundler = require('./js/template-bundler/template-bundler'); + const bundleConfig = require('./frontend/bundle-config.json'); const libs = require('./frontend/libs.json'); @@ -277,7 +277,7 @@ module.exports = grunt => { }; grunt.registerTask('bundle', () => { - let bundler = new BundlerGeneral(bundleConfig, libs); + let bundler = new Bundler(bundleConfig, libs); let result = bundler.bundle(); diff --git a/js/bundler/bundler-general.js b/js/bundler/bundler-general.js deleted file mode 100644 index 80f8e245b1..0000000000 --- a/js/bundler/bundler-general.js +++ /dev/null @@ -1,240 +0,0 @@ -/************************************************************************ - * This file is part of EspoCRM. - * - * EspoCRM - Open Source CRM application. - * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko - * Website: https://www.espocrm.com - * - * EspoCRM is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EspoCRM is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EspoCRM. If not, see http://www.gnu.org/licenses/. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU General Public License version 3. - * - * In accordance with Section 7(b) of the GNU General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -const Bundler = require("./bundler"); -const Precompiler = require('./template-precompiler'); - -class BundlerGeneral { - - /** - * @param {{ - * basePath?: string, - * transpiledPath?: string, - * chunks: Object., - * modulePaths?: Record., - * lookupPatterns: string[], - * order: string[], - * }} config - * @param {{ - * src?: string, - * bundle?: boolean, - * key?: string, - * files?: { - * src: string, - * }[] - * }[]} libs - * @param {string} [filePattern] - */ - constructor(config, libs, filePattern) { - this.config = config; - this.libs = libs; - this.mainBundleFiles = []; - this.filePattern = filePattern || 'client/lib/espo-{*}.min.js'; - - if (!this.config.order.length) { - throw new Error(`No chunks specified in 'order' param.`); - } - } - - /** - * @return {Object.} - */ - bundle() { - let result = {}; - let mapping = {}; - let files = []; - let modules = []; - let templateFiles = []; - let mainName = this.config.order[0]; - - /** @var {Object.} */ - let notBundledMap = {}; - - this.config.order.forEach((name, i) => { - let data = this.#bundleChunk(name, i === 0, { - files: files, - templateFiles: templateFiles, - }); - - files = files.concat(data.files); - templateFiles = templateFiles.concat(data.templateFiles); - modules = modules.concat(data.modules); - notBundledMap[name] = data.notBundledModules; - result[name] = data.contents; - - console.log(` Chunk '${name}' done, ${data.files.length} files.`) - - if (i === 0 && this.config.order.length > 1) { - return; - } - - data.modules.forEach(item => mapping[item] = name); - - let bundleFile = this.filePattern.replace('{*}', name); - - let requires = [].concat(this.config.chunks[name].requires ?? []); - - if (this.config.chunks[name].mapDependencies) { - requires = requires.concat(data.dependencyModules); - } - - if (requires.length) { - let part = JSON.stringify(requires); - - result[mainName] += `Espo.loader.mapBundleDependencies('${name}', ${part});\n`; - } - - result[mainName] += `Espo.loader.mapBundleFile('${name}', '${bundleFile}');\n`; - }); - - let notBundledModules = []; - - this.config.order.forEach(name => { - notBundledMap[name] - .filter(item => !modules.includes(item)) - .filter(item => !notBundledModules.includes(item)) - .forEach(item => notBundledModules.push(item)); - }); - - if (notBundledModules.length) { - let part = notBundledModules - .map(item => ' ' + item) - .join('\n'); - - console.log(`\nNot bundled:\n${part}`); - } - - result[mainName] += `Espo.loader.addBundleMapping(${JSON.stringify(mapping)});` - - return result; - } - - /** - * @param {string} name - * @param {boolean} isMain - * @param {{files: [], templateFiles: []}} alreadyBundled - * @return {{ - * contents: string, - * modules: string[], - * files: string[], - * templateFiles: string[], - * notBundledModules: string[], - * dependencyModules: [], - * }} - */ - #bundleChunk(name, isMain, alreadyBundled) { - let contents = ''; - let modules = []; - let dependencyModules = []; - - let params = this.config.chunks[name]; - - let patterns = params.patterns; - let lookupPatterns = [] - .concat(this.config.lookupPatterns) - .concat(params.lookupPatterns || []); - - let bundledFiles = []; - let bundledTemplateFiles = []; - let notBundledModules = []; - - if (params.patterns) { - let bundler = new Bundler( - this.config.modulePaths, - this.config.basePath, - this.config.transpiledPath - ); - - // The main bundle is always loaded, duplicates are not needed. - let ignoreFiles = [].concat(this.mainBundleFiles); - - if (params.noDuplicates) { - ignoreFiles = ignoreFiles.concat(alreadyBundled.files); - } - - let data = bundler.bundle({ - name: name, - files: params.files, - patterns: patterns, - lookupPatterns: lookupPatterns, - libs: this.libs, - ignoreFullPathFiles: ignoreFiles, - ignoreFiles: params.ignoreFiles, - dependentOn: params.dependentOn, - }); - - contents += data.contents; - - if (isMain) { - this.mainBundleFiles = data.files; - } - - modules = data.modules; - bundledFiles = data.files; - - notBundledModules = data.notBundledModules; - dependencyModules = data.dependencyModules; - } - - // Pre-compiled templates turned out to be slower if too many are bundled. - // To be used sparingly. - if (params.templatePatterns) { - let ignoreFiles = params.noDuplicates ? [].concat(alreadyBundled.templateFiles) : []; - - let data = (new Precompiler()).precompile({ - patterns: params.templatePatterns, - modulePaths: this.config.modulePaths, - ignoreFiles: ignoreFiles, - }); - - contents += '\n' + data.contents; - bundledTemplateFiles = data.files; - } - - return { - contents: contents, - modules: modules, - files: bundledFiles, - templateFiles: bundledTemplateFiles, - notBundledModules: notBundledModules, - dependencyModules: dependencyModules, - }; - } -} - -module.exports = BundlerGeneral; diff --git a/js/bundler/bundler.js b/js/bundler/bundler.js deleted file mode 100644 index cccca6bb6b..0000000000 --- a/js/bundler/bundler.js +++ /dev/null @@ -1,654 +0,0 @@ -/************************************************************************ - * This file is part of EspoCRM. - * - * EspoCRM - Open Source CRM application. - * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko - * Website: https://www.espocrm.com - * - * EspoCRM is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EspoCRM is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EspoCRM. If not, see http://www.gnu.org/licenses/. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU General Public License version 3. - * - * In accordance with Section 7(b) of the GNU General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -const typescript = require('typescript'); -const fs = require('fs'); -const {globSync} = require('glob'); - -/** - * Normalizes and concatenates Espo modules. - * - * Modules dependent on not bundled libs are ignored. Modules dependent on such modules - * are ignored as well and so on. - */ -class Bundler { - - /** - * @param {Object.} modPaths - * @param {string} [basePath] - * @param {string} [transpiledPath] - */ - constructor(modPaths, basePath, transpiledPath) { - this.modPaths = modPaths; - this.basePath = basePath ?? 'client'; - this.transpiledPath = transpiledPath ?? 'client/lib/transpiled'; - - this.srcPath = this.basePath + '/src'; - } - - /** - * Bundles Espo js files into chunks. - * - * @param {{ - * name: string, - * files?: string[], - * patterns: string[], - * ignoreFiles?: string[], - * lookupPatterns?: string[], - * ignoreFullPathFiles?: string[], - * dependentOn?: string[], - * libs: { - * src?: string, - * bundle?: boolean, - * key?: string, - * }[], - * }} params - * @return {{ - * contents: string, - * files: string[], - * modules: string[], - * notBundledModules: string[], - * dependencyModules: string[], - * }} - */ - bundle(params) { - let ignoreFullPathFiles = params.ignoreFullPathFiles ?? []; - let files = params.files ?? []; - let ignoreFiles = params.ignoreFiles ?? []; - - let fullPathFiles = [] - .concat(this.#normalizePaths(params.files || [])) - .concat(this.#obtainFiles(params.patterns, [...files, ...ignoreFiles])) - // @todo Check if working. - .filter(file => !ignoreFullPathFiles.includes(file)); - - let allFiles = this.#obtainFiles(params.lookupPatterns || params.patterns); - - let ignoreLibs = params.libs - .filter(item => item.key && !item.bundle) - .map(item => 'lib!' + item.key) - .filter(item => !(params.dependentOn || []).includes(item)); - - let notBundledModules = []; - - let {files: sortedFiles, depModules} = this.#sortFiles( - params.name, - fullPathFiles, - allFiles, - ignoreLibs, - ignoreFullPathFiles, - notBundledModules, - params.dependentOn || null, - ); - - let contents = ''; - - this.#mapToTraspiledFiles(sortedFiles) - .forEach(file => contents += this.#normalizeSourceFile(file) + '\n'); - - let modules = sortedFiles.map(file => this.#obtainModuleName(file)); - - return { - contents: contents, - files: sortedFiles, - modules: modules, - notBundledModules: notBundledModules, - dependencyModules: depModules, - }; - } - - /** - * @param {string[]} files - * @return {string[]} - */ - #mapToTraspiledFiles(files) { - return files.map(file => { - return this.transpiledPath + '/' + file.slice(this.basePath.length + 1); - }); - } - - /** - * @param {string[]} patterns - * @param {string[]} [ignoreFiles] - * @return {string[]} - */ - #obtainFiles(patterns, ignoreFiles) { - let files = []; - ignoreFiles = this.#normalizePaths(ignoreFiles || []); - - this.#normalizePaths(patterns).forEach(pattern => { - let itemFiles = globSync(pattern, {}) - .map(file => file.replaceAll('\\', '/')) - .filter(file => !ignoreFiles.includes(file)); - - files = files.concat(itemFiles); - }); - - return files; - } - - /** - * @param {string[]} patterns - * @return {string[]} - */ - #normalizePaths(patterns) { - return patterns.map(item => this.basePath + '/' + item); - } - - /** - * @param {string} name - * @param {string[]} files - * @param {string[]} allFiles - * @param {string[]} ignoreLibs - * @param {string[]} ignoreFiles - * @param {string[]} notBundledModules - * @param {string[]|null} dependentOn - * @return {{ - * files: string[], - * depModules: string[], - * }} - */ - #sortFiles( - name, - files, - allFiles, - ignoreLibs, - ignoreFiles, - notBundledModules, - dependentOn - ) { - /** @var {Object.} */ - let map = {}; - let standalonePathList = []; - let modules = []; - let moduleFileMap = {}; - - let ignoreModules = ignoreFiles.map(file => this.#obtainModuleName(file)); - - allFiles.forEach(file => { - let data = this.#obtainModuleData(file); - - let isTarget = files.includes(file); - - if (!data) { - if (isTarget) { - standalonePathList.push(file); - } - - return; - } - - map[data.name] = data.deps; - moduleFileMap[data.name] = file; - - if (isTarget) { - modules.push(data.name); - } - }); - - let depModules = []; - let allDepModules = []; - - modules - .forEach(name => { - let deps = this.#obtainAllDeps(name, map); - - deps - .filter(item => !modules.includes(item)) - .filter(item => !allDepModules.includes(item)) - .forEach(item => allDepModules.push(item)); - - deps - .filter(item => !item.includes('!')) - .filter(item => !modules.includes(item)) - .filter(item => !depModules.includes(item)) - .forEach(item => depModules.push(item)); - }); - - modules = modules - .concat(depModules) - .filter(module => !ignoreModules.includes(module)); - - /** @var {string[]} */ - let discardedModules = []; - /** @var {Object.} */ - let depthMap = {}; - /** @var {string[]} */ - let pickedModules = []; - - for (let module of modules) { - this.#buildTreeItem( - module, - map, - depthMap, - ignoreLibs, - dependentOn, - discardedModules, - pickedModules - ); - } - - if (dependentOn) { - modules = pickedModules; - } - - modules.sort((v1, v2) => { - return depthMap[v2] - depthMap[v1]; - }); - - discardedModules.forEach(item => notBundledModules.push(item)); - - modules = modules.filter(item => !discardedModules.includes(item)); - - let modulePaths = modules.map(name => { - if (!moduleFileMap[name]) { - throw Error(`Can't obtain ${name}. Might be missing in lookupPatterns.`); - } - - return moduleFileMap[name]; - }); - - return { - files: standalonePathList.concat(modulePaths), - depModules: allDepModules, - }; - } - - /** - * @param {string} name - * @param {Object.} map - * @param {string[]} [list] - */ - #obtainAllDeps(name, map, list) { - if (!list) { - list = []; - } - - let deps = map[name] || []; - - deps.forEach(depName => { - if (!list.includes(depName)) { - list.push(depName); - } - - if (depName.includes('!')) { - return; - } - - this.#obtainAllDeps(depName, map, list); - }); - - return list; - } - - /** - * @param {string} module - * @param {Object.} map - * @param {Object.} depthMap - * @param {string[]} ignoreLibs - * @param {string[]} dependentOn - * @param {string[]} discardedModules - * @param {string[]} pickedModules - * @param {number} [depth] - * @param {string[]} [path] - */ - #buildTreeItem( - module, - map, - depthMap, - ignoreLibs, - dependentOn, - discardedModules, - pickedModules, - depth, - path - ) { - /** @var {string[]} */ - let deps = map[module] || []; - depth = depth || 0; - path = [].concat(path || []); - - path.push(module); - - if (!(module in depthMap)) { - depthMap[module] = depth; - } - else if (depth > depthMap[module]) { - depthMap[module] = depth; - } - - if (deps.length === 0) { - return; - } - - for (let depName of deps) { - if (ignoreLibs.includes(depName)) { - path - .filter(item => !discardedModules.includes(item)) - .forEach(item => discardedModules.push(item)); - - return; - } - - if (dependentOn && dependentOn.includes(depName)) { - path - .filter(item => !pickedModules.includes(item)) - .forEach(item => pickedModules.push(item)); - } - } - - deps.forEach(depName => { - if (depName.includes('!')) { - return; - } - - this.#buildTreeItem( - depName, - map, - depthMap, - ignoreLibs, - dependentOn, - discardedModules, - pickedModules, - depth + 1, - path - ); - }); - } - - /** - * @param {string} file - * @return string - */ - #obtainModuleName(file) { - for (let mod in this.modPaths) { - let part = this.basePath + '/' + this.modPaths[mod] + '/src/'; - - if (file.indexOf(part) === 0) { - return `modules/${mod}/` + file.substring(part.length, file.length - 3); - } - } - - return file.slice(this.#getSrcPath().length, -3); - } - - /** - * @param {string} path - * @return {{deps: string[], name: string}|null} - */ - #obtainModuleData(path) { - if (!this.#isClientJsFile(path)) { - return null; - } - - let moduleName = this.#obtainModuleName(path); - - const sourceCode = fs.readFileSync(path, 'utf-8'); - - let tsSourceFile = typescript.createSourceFile( - path, - sourceCode, - typescript.ScriptTarget.Latest - ); - - let rootStatement = tsSourceFile.statements[0]; - - if ( - !rootStatement.expression || - !rootStatement.expression.expression || - rootStatement.expression.expression.escapedText !== 'define' - ) { - if (!sourceCode.includes('export ')) { - return null; - } - - if (!sourceCode.includes('import ')) { - return { - name: moduleName, - deps: [], - }; - } - - return { - name: moduleName, - deps: this.#obtainModuleDeps(tsSourceFile, moduleName), - }; - } - - let deps = []; - - let argumentList = rootStatement.expression.arguments; - - for (let argument of argumentList.slice(0, 2)) { - if (argument.elements) { - argument.elements.forEach(node => { - if (!node.text) { - return; - } - - let dep = this.#normalizeModModuleId(node.text); - - deps.push(dep); - }); - } - } - - return { - name: moduleName, - deps: deps, - }; - } - - /** - * @param {string} sourceFile - * @param {string} subjectId - * @return {string[]} - */ - #obtainModuleDeps(sourceFile, subjectId) { - return sourceFile.statements - .filter(item => item.importClause && item.moduleSpecifier) - .map(item => item.moduleSpecifier.text) - .map(/** string */id => { - id = this.#normalizeIdPath(id, subjectId); - - return this.#normalizeModModuleId(id); - }); - } - - /** - * @param {string} id - * @param {string} subjectId - * @private - */ - #normalizeIdPath(id, subjectId) { - if (id.at(0) !== '.') { - return id; - } - - if (id.slice(0, 2) !== './' && id.slice(0, 3) !== '../') { - return id; - } - - let outputPath = id; - - let dirParts = subjectId.split('/').slice(0, -1); - - if (id.slice(0, 2) === './') { - outputPath = dirParts.join('/') + '/' + id.slice(2); - } - - let parts = outputPath.split('/'); - - let up = 0; - - for (let part of parts) { - if (part === '..') { - up++; - - continue; - } - - break; - } - - if (!up) { - return outputPath; - } - - if (up) { - outputPath = dirParts.slice(0, -up).join('/') + '/' + outputPath.slice(3 * up); - } - - return outputPath; - } - - /** - * @param {string} id - * @return {string} - */ - #normalizeModModuleId(id) { - if (!id.includes(':')) { - return id; - } - - let [mod, part] = id.split(':'); - - return `modules/${mod}/` + part; - } - - /** - * @param {string} path - * @return {boolean} - */ - #isClientJsFile(path) { - if (path.slice(-3) !== '.js') { - return false; - } - - let startParts = [this.#getSrcPath()]; - - for (let mod in this.modPaths) { - let modPath = this.basePath + '/' + this.modPaths[mod] + '/src/'; - - startParts.push(modPath); - } - - for (let starPart of startParts) { - if (path.indexOf(starPart) === 0) { - return true; - } - } - - return false; - } - - /** - * @private - * @param {string} path - * @return {string} - */ - #normalizeSourceFile(path) { - let sourceCode = fs.readFileSync(path, 'utf-8'); - let srcPath = this.#getSrcPath(); - - sourceCode = this.#stripSourceMappingUrl(sourceCode); - - if (!this.#isClientJsFile(path)) { - return sourceCode; - } - - if (!sourceCode.includes('define')) { - return sourceCode; - } - - let moduleName = path.slice(srcPath.length, -3); - - let tsSourceFile = typescript.createSourceFile( - path, - sourceCode, - typescript.ScriptTarget.Latest - ); - - let rootStatement = tsSourceFile.statements[0]; - - if ( - !rootStatement.expression || - !rootStatement.expression.expression || - rootStatement.expression.expression.escapedText !== 'define' - ) { - return sourceCode; - } - - let argumentList = rootStatement.expression.arguments; - - if (argumentList.length >= 3 || argumentList.length === 0) { - return sourceCode; - } - - let moduleNameNode = typescript.createStringLiteral(moduleName); - - if (argumentList.length === 1) { - argumentList.unshift( - typescript.createArrayLiteral([]) - ); - } - - argumentList.unshift(moduleNameNode); - - return typescript.createPrinter().printFile(tsSourceFile); - } - - /** - * @param {string} contents - * @return {string} - */ - #stripSourceMappingUrl(contents) { - let re = /^\/\/# sourceMappingURL.*/gm; - - if (!contents.match(re)) { - return contents; - } - - return contents.replaceAll(re, ''); - } - - /** - * @return {string} - */ - #getSrcPath() { - let path = this.srcPath; - - if (path.slice(-1) !== '/') { - path += '/'; - } - - return path; - } -} - -module.exports = Bundler; diff --git a/js/bundler/template-precompiler.js b/js/bundler/template-precompiler.js deleted file mode 100644 index 7081df3eb3..0000000000 --- a/js/bundler/template-precompiler.js +++ /dev/null @@ -1,123 +0,0 @@ -/************************************************************************ - * This file is part of EspoCRM. - * - * EspoCRM - Open Source CRM application. - * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko - * Website: https://www.espocrm.com - * - * EspoCRM is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EspoCRM is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EspoCRM. If not, see http://www.gnu.org/licenses/. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU General Public License version 3. - * - * In accordance with Section 7(b) of the GNU General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -const fs = require('fs'); -const {globSync} = require('glob'); -const Handlebars = require('handlebars'); - -class TemplatePrecompiler { - - defaultPath = 'client'; - - /** - * @param {{ - * patterns: string[], - * modulePaths: Record., - * ignoreFiles: string[], - * }} params - * @return {{contents: string, files: string[]}} - */ - precompile(params) { - const baseBase = 'client'; - - let files = []; - - this.#normalizePaths(params.patterns).forEach(pattern => { - let itemFiles = globSync(pattern) - .map(file => file.replaceAll('\\', '/')); - - files = files.concat(itemFiles); - }); - - let nameMap = {}; - let compiledFiles = []; - - files.forEach(file => { - let module = null; - - if (params.ignoreFiles.includes(file)) { - return; - } - - for (let itemModule in params.modulePaths) { - let path = baseBase + '/' + params.modulePaths[itemModule]; - - if (file.indexOf(path) === 0) { - module = itemModule; - - break; - } - } - - let path = module ? - baseBase + '/' + params.modulePaths[module] : - this.defaultPath; - - path += '/res/templates/'; - - let name = file.substring(path.length).slice(0, -4); - - if (module) { - name = module + ':' + name; - } - - nameMap[file] = name - compiledFiles.push(file); - }); - - let contents = - 'Espo.preCompiledTemplates = Espo.preCompiledTemplates || {};\n' + - 'Object.assign(Espo.preCompiledTemplates, {\n'; - - for (let file in nameMap) { - let name = nameMap[file]; - - let templateContent = fs.readFileSync(file, 'utf8'); - let compiled = Handlebars.precompile(templateContent); - - contents += `'${name}': Handlebars.template(\n${compiled}\n),\n`; - } - - contents += `\n});`; - - return { - files: compiledFiles, - contents: contents, - }; - } - - /** - * @param {string[]} patterns - * @return {string[]} - */ - #normalizePaths(patterns) { - return patterns.map(item => this.defaultPath + '/' + item); - } -} - -module.exports = TemplatePrecompiler; diff --git a/js/template-bundler/template-bundler.js b/js/template-bundler/template-bundler.js deleted file mode 100644 index d9b2452e90..0000000000 --- a/js/template-bundler/template-bundler.js +++ /dev/null @@ -1,111 +0,0 @@ -/************************************************************************ - * This file is part of EspoCRM. - * - * EspoCRM - Open Source CRM application. - * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko - * Website: https://www.espocrm.com - * - * EspoCRM is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EspoCRM is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EspoCRM. If not, see http://www.gnu.org/licenses/. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU General Public License version 3. - * - * In accordance with Section 7(b) of the GNU General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -const fs = require('fs'); -const {globSync} = require('glob'); - -class TemplateBundler { - - /** - * @param {{ - * dirs?: string[], - * dest?: string, - * clientDir?: string, - * }} config - */ - constructor(config) { - this.dirs = config.dirs ?? ['client/res/templates']; - this.dest = config.dest ?? 'client/lib/templates.tpl'; - this.clientDir = config.clientDir ?? 'client'; - } - - /** - * @return {string} - */ - process() { - /** @type {string[]} */ - let allFiles = []; - - this.dirs.forEach(dir => { - let files = globSync(dir + '/**/*.tpl') - .map(file => file.replaceAll('\\', '/')); - - allFiles.push(...files); - }); - - let contents = []; - - allFiles.forEach(file => { - let content = fs.readFileSync(file, 'utf-8'); - - if (file.indexOf(this.clientDir) !== 0) { - throw new Error(`File ${file} not in the client dir.`); - } - - let path = file.slice(this.clientDir.length + 1); - - content = path + '\n' + content; - - contents.push(content); - }); - - let delimiter = this.#generateDelimiter(contents); - - let result = delimiter + '\n' + contents.join('\n' + delimiter + '\n'); - - fs.writeFileSync(this.dest, result, 'utf8'); - - console.log(` ${contents.length} templates bundled in ${this.dest}.`); - } - - /** - * @param {string[]} contents - * @return {string} - */ - #generateDelimiter(contents) { - let delimiter = '_delimiter_' + Math.random().toString(36).slice(2); - - let retry = false; - - for (let content of contents) { - if (content.includes(delimiter)) { - retry = true; - - break; - } - } - - if (retry) { - return this.#generateDelimiter(contents); - } - - return delimiter; - } -} - -module.exports = TemplateBundler; diff --git a/js/transpile.js b/js/transpile.js index bcff3a7988..c1b149fb8d 100644 --- a/js/transpile.js +++ b/js/transpile.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -const Transpiler = require('./transpiler/transpiler'); +const {Transpiler} = require('espo-frontend-build-tools'); let file; diff --git a/js/transpiler/transpiler.js b/js/transpiler/transpiler.js deleted file mode 100644 index e2660a6d05..0000000000 --- a/js/transpiler/transpiler.js +++ /dev/null @@ -1,207 +0,0 @@ -/************************************************************************ - * This file is part of EspoCRM. - * - * EspoCRM - Open Source CRM application. - * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko - * Website: https://www.espocrm.com - * - * EspoCRM is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * EspoCRM is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with EspoCRM. If not, see http://www.gnu.org/licenses/. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU General Public License version 3. - * - * In accordance with Section 7(b) of the GNU General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -const babelCore = require("@babel/core"); -const fs = require('fs'); -const {globSync} = require('glob'); - -class Transpiler { - - /** - * @param {{ - * path?: string, - * destDir?: string, - * mod?: string, - * file?: string, - * }} config - */ - constructor(config) { - this.path = (config.path ?? 'client') + '/src'; - this.destDir = config.destDir || 'client/lib/transpiled'; - this.mod = config.mod; - this.file = config.file; - - this.contentsCache = {}; - } - - /** - * @return {{ - * transpiled: string[], - * copied: string[], - * }} - */ - process() { - let allFiles = globSync(this.path + '/**/*.js') - .map(file => file.replaceAll('\\', '/')); - - if (this.file) { - let file = this.file.replaceAll('\\', '/'); - - if (!allFiles.includes(file)) { - return { - transpiled: [], - copied: [], - }; - } - - allFiles = [file]; - } - - let files = allFiles.filter(file => this.#isToBeTranspiled(file)); - let otherFiles = allFiles.filter(file => !files.includes(file)); - - files.forEach(file => this.#processFile(file)); - otherFiles.forEach(file => this.#copyFile(file)); - - return { - transpiled: files, - copied: otherFiles, - }; - } - - /** - * @param {string} file - */ - #processFile(file) { - const module = this.#obtainModuleName(file); - - const result = babelCore.transformSync(this.#getContents(file), { - plugins: ['@babel/plugin-transform-modules-amd'], - moduleId: module, - sourceMaps: true, - }); - - let dir = this.#obtainTargetDir(module); - - fs.mkdirSync(dir, {recursive: true}); - - let part = module; - - if (part.includes(':')) { - part = part.split(':')[1]; - } - - let filePart = part.split('/').slice(-1)[0] + '.js'; - let destFile = dir + filePart; - - let resultContent = result.code + `\n//# sourceMappingURL=${filePart}.map ;`; - - fs.writeFileSync(destFile, resultContent, 'utf-8'); - fs.writeFileSync(destFile + '.map', result.map.toString(), 'utf-8'); - } - - /** - * @param {string} file - */ - #copyFile(file) { - let module = this.#obtainModuleName(file); - let dir = this.#obtainTargetDir(module); - - fs.mkdirSync(dir, {recursive: true}); - - let destFile = dir + file.split('/').slice(-1)[0]; - - fs.mkdirSync(dir, {recursive: true}); - fs.copyFileSync(file, destFile); - } - - /** - * @param {string} module - * @return {string} - */ - #obtainTargetDir(module) { - let destDir = this.destDir; - - let part = 'src'; - let path = module; - - if (module.includes(':')) { - let [mod, itemPath] = module.split(':'); - - part = 'modules/' + mod + '/' + part; - - path = itemPath; - } - else if (module.startsWith('modules/')) { - let items = module.split('/'); - - if (items.length < 2) { - throw new Error(`Bad module name ${module}.`); - } - - let mod = items[1]; - - part = 'modules/' + mod + '/' + part; - path = items.slice(2).join('/'); - } - - destDir += '/' + part + '/' + path.split('/').slice(0, -1).join('/'); - - if (destDir.slice(-1) !== '/') { - destDir += '/'; - } - - return destDir; - } - - /** - * @param {string} file - * @return {boolean} - */ - #isToBeTranspiled(file) { - let contents = this.#getContents(file); - - return !contents.includes("\ndefine(") && contents.includes("\nexport "); - } - - /** - * @param {string} file - * @return {string} - */ - #getContents(file) { - if (!(file in this.contentsCache)) { - this.contentsCache[file] = fs.readFileSync(file, 'utf-8'); - } - - return this.contentsCache[file]; - } - - /** - * @param {string} file - * @return string - */ - #obtainModuleName(file) { - if (this.mod) { - return `modules/${this.mod}/` + file.slice(this.path.length + 1, -3); - } - - return file.slice(this.path.length + 1, -3); - } -} - -module.exports = Transpiler; diff --git a/package-lock.json b/package-lock.json index 00d7dead93..a60ba2a5a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,11 +48,8 @@ "vis": "^4.19.1" }, "devDependencies": { - "@babel/cli": "^7.21.5", - "@babel/core": "^7.22.1", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/preset-env": "^7.22.4", "archiver": "^5.3.0", + "espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.0.1", "fstream": ">=1.0.12", "glob": "^10.2.6", "grunt": "^1.5.3", @@ -68,8 +65,7 @@ "jasmine-core": "^5.0.1", "js-yaml": "^3.13.1", "pofile": "^1.1.3", - "tar": "^6.1.11", - "typescript": "^4.7.4" + "tar": "^6.1.11" }, "engines": { "node": ">=15", @@ -3098,6 +3094,25 @@ "node": ">=0.8.0" } }, + "node_modules/espo-frontend-build-tools": { + "version": "0.0.1", + "resolved": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#226e3af9574a241d45e933327b72a5b41bc5bdb5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/cli": "^7.21.5", + "@babel/core": "^7.22.1", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/preset-env": "^7.22.4", + "glob": "^10.2.6", + "handlebars": "^4.7.7", + "typescript": "^4.7.4" + }, + "engines": { + "node": ">=15", + "npm": ">=7" + } + }, "node_modules/espo-funnel-chart": { "version": "1.1.0", "resolved": "git+ssh://git@github.com/yurikuzn/espo-funnel-chart.git#fe827dd8acc8cd65c1a16f71c47f742940d7ca24", @@ -9324,6 +9339,20 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "espo-frontend-build-tools": { + "version": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#226e3af9574a241d45e933327b72a5b41bc5bdb5", + "dev": true, + "from": "espo-frontend-build-tools@github:espocrm/frontend-build-tools#0.0.1", + "requires": { + "@babel/cli": "^7.21.5", + "@babel/core": "^7.22.1", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/preset-env": "^7.22.4", + "glob": "^10.2.6", + "handlebars": "^4.7.7", + "typescript": "^4.7.4" + } + }, "espo-funnel-chart": { "version": "git+ssh://git@github.com/yurikuzn/espo-funnel-chart.git#fe827dd8acc8cd65c1a16f71c47f742940d7ca24", "from": "espo-funnel-chart@github:yurikuzn/espo-funnel-chart#1.1.0", diff --git a/package.json b/package.json index ac36e674d0..adaaead068 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,8 @@ "author": "Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko", "license": "GPL-3.0", "devDependencies": { - "@babel/cli": "^7.21.5", - "@babel/core": "^7.22.1", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/preset-env": "^7.22.4", "archiver": "^5.3.0", + "espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.0.1", "fstream": ">=1.0.12", "glob": "^10.2.6", "grunt": "^1.5.3", @@ -36,8 +33,7 @@ "jasmine-core": "^5.0.1", "js-yaml": "^3.13.1", "pofile": "^1.1.3", - "tar": "^6.1.11", - "typescript": "^4.7.4" + "tar": "^6.1.11" }, "dependencies": { "ace-builds": "^1.4.12",