mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-03 02:47:03 +00:00
frontend build tools usage
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user