frontend build tools usage

This commit is contained in:
Yuri Kuznetsov
2023-06-22 12:49:28 +03:00
parent 00cf85abd4
commit 716976c078
9 changed files with 41 additions and 1351 deletions

View File

@@ -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.<string, {
* files?: string[],
* patterns?: string[],
* ignoreFiles?: string[],
* lookupPatterns?: string[],
* templatePatterns?: string[],
* noDuplicates?: boolean,
* dependentOn?: string[],
* requires?: string[],
* mapDependencies?: boolean,
* }>,
* modulePaths?: Record.<string, string>,
* 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.<string, string>}
*/
bundle() {
let result = {};
let mapping = {};
let files = [];
let modules = [];
let templateFiles = [];
let mainName = this.config.order[0];
/** @var {Object.<string, string[]>} */
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;

View File

@@ -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.<string, string>} 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.<string, string[]>} */
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.<string, number>} */
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.<string, string[]>} 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.<string, string[]>} map
* @param {Object.<string, number>} 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;

View File

@@ -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.<string, string>,
* 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;