/************************************************************************ * This file is part of EspoCRM. * * EspoCRM – Open Source CRM application. * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko * Website: https://www.espocrm.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * 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 Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ /** * * `grunt` - full build; * * `grunt dev` - build for development; * * `grunt offline` - build but skip *composer install*; * * `grunt internal` - build only libs and css; * * `grunt release` - full build zipped with upgrade packages`; * * `grunt test` - build for tests running; * * `grunt run-tests` - build and run unit and integration tests. */ const fs = require('fs'); const cp = require('child_process'); const path = require('path'); const buildUtils = require('./js/build-utils'); const {TemplateBundler, Bundler} = require('espo-frontend-build-tools'); const LayoutTypeBundler = require('./js/layout-template-bundler'); const bundleConfig = require('./frontend/bundle-config.json'); const libs = require('./frontend/libs.json'); module.exports = grunt => { const pkg = grunt.file.readJSON('package.json'); const originalLibDir = 'client/lib/original'; const libsBundleFileList = [ 'client/src/namespace.js', 'client/src/loader.js', ...buildUtils.getPreparedBundleLibList(libs), ]; const bundleFileMap = {'client/lib/espo.js': libsBundleFileList}; for (const name in bundleConfig.chunks) { const namePart = 'espo-' + name; bundleFileMap[`client/lib/${namePart}.js`] = originalLibDir + `/${namePart}.js` } const copyJsFileList = buildUtils.getCopyLibDataList(libs); const minifyLibFileList = copyJsFileList .filter(item => item.minify) .map(item => { return { dest: item.dest, src: item.originalDest, }; }); const currentPath = path.dirname(fs.realpathSync(__filename)); const themeList = []; fs.readdirSync('application/Espo/Resources/metadata/themes').forEach(file => { themeList.push(file.substring(0, file.length - 5)); }); const cssminFilesData = {}; const lessData = {}; themeList.forEach(theme => { const name = buildUtils.camelCaseToHyphen(theme); const files = {}; files['client/css/espo/'+name+'.css'] = 'frontend/less/'+name+'/main.less'; files['client/css/espo/'+name+'-iframe.css'] = 'frontend/less/'+name+'/iframe/main.less'; cssminFilesData['client/css/espo/'+name+'.css'] = 'client/css/espo/'+name+'.css'; cssminFilesData['client/css/espo/'+name+'-iframe.css'] = 'client/css/espo/'+name+'-iframe.css'; lessData[theme] = { options: { yuicompress: true, }, files: files, }; }); grunt.initConfig({ pkg: pkg, mkdir: { tmp: { options: { mode: 0o755, create: [ 'build/tmp', ], } } }, clean: { start: [ 'build/EspoCRM-*', 'client/lib/*', 'client/modules/crm/lib/*', 'client/css/espo/*', ], final: ['build/tmp'], release: ['build/EspoCRM-' + pkg.version], beforeFinal: { src: [ 'build/tmp/custom/Espo/Custom/*', '!build/tmp/custom/Espo/Custom/.htaccess', 'build/tmp/install/config.php', 'build/tmp/vendor/*/*/.git', 'build/tmp/custom/Espo/Custom/*', 'build/tmp/client/custom/*', '!build/tmp/client/custom/modules', 'build/tmp/client/custom/modules/*', '!build/tmp/client/custom/modules/dummy.txt', 'build/tmp/client/lib/original/espo.js', 'build/tmp/client/lib/original/espo-*.js', '!build/tmp/client/lib/original/espo-funnel-chart.js', 'build/tmp/client/lib/transpiled', ] }, }, less: lessData, cssmin: { themes: { files: cssminFilesData, }, }, uglify: { options: { sourceMap: true, output: { comments: /^!/, }, beautify: false, mangle: true, compress: true }, bundle: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n', }, files: bundleFileMap, }, lib: { files: minifyLibFileList, }, }, copy: { options: { mode: true, }, frontend: { expand: true, cwd: 'client', src: [ 'src/**', 'res/**', 'fonts/**', 'modules/**', 'img/**', 'css/**', 'sounds/**', 'custom/**', 'lib/**', ], dest: 'build/tmp/client', }, frontendLib: { files: copyJsFileList, }, backend: { expand: true, dot: true, src: [ 'application/**', 'custom/**', 'data/.data', 'vendor/**', 'html/**', 'public/**', 'install/**', 'bin/**', 'bootstrap.php', 'cron.php', 'daemon.php', 'rebuild.php', 'clear_cache.php', 'preload.php', 'upgrade.php', 'extension.php', 'websocket.php', 'command.php', 'index.php', 'LICENSE.txt', '.htaccess', 'web.config', ], dest: 'build/tmp/', }, final: { expand: true, dot: true, src: '**', cwd: 'build/tmp', dest: 'build/EspoCRM-<%= pkg.version %>/', }, }, replace: { version: { options: { patterns: [ { match: 'version', replacement: '<%= pkg.version %>', } ] }, files: [ { src: 'build/tmp/application/Espo/Resources/defaults/config.php', dest: 'build/tmp/application/Espo/Resources/defaults/config.php', } ], }, dev: { options: { patterns: [ { match: /# \{#dev}(.*)\{\/dev}/gs, replacement: '', } ] }, files: [ { src: 'build/tmp/.htaccess', dest: 'build/tmp/.htaccess', } ], }, }, }); const writeOriginalLib = (name, contents) => { if (!fs.existsSync(originalLibDir)) { fs.mkdirSync(originalLibDir); } const file = originalLibDir + `/${name}.js`; fs.writeFileSync(file, contents, 'utf8'); }; grunt.registerTask('bundle', () => { const bundler = new Bundler(bundleConfig, libs); const result = bundler.bundle(); for (const name in result) { let contents = result[name]; const key = 'espo-' + name; if (name === 'main') { contents += '\n' + (new LayoutTypeBundler()).bundle(); } writeOriginalLib(key, contents); } }); grunt.registerTask('bundle-templates', () => { const templateBundler = new TemplateBundler({ dirs: [ 'client/res/templates', 'client/modules/crm/res/templates', ], }); templateBundler.process(); }); grunt.registerTask('prepare-lib-original', () => { // Even though `npm ci` runs the same script, 'clean:start' deletes files. cp.execSync("node js/scripts/prepare-lib-original"); }); grunt.registerTask('prepare-lib', () => { cp.execSync("node js/scripts/prepare-lib"); }); grunt.registerTask('transpile', () => { cp.execSync("node js/transpile"); }); grunt.registerTask('chmod-folders', () => { cp.execSync( "find . -type d -exec chmod 755 {} +", {cwd: 'build/EspoCRM-' + pkg.version} ); }); grunt.registerTask('chmod-multiple', () => { const dirPath = 'build/EspoCRM-' + pkg.version; const fileList = [ { name: '*.php', }, { name: '*.json', }, { name: '*.config', }, { name: '.htaccess', }, { name: '*.html', }, { name: '*.txt', }, { name: '*.js', folder: 'client', }, { name: '*.css', folder: 'client', }, { name: '*.tpl', folder: 'client', }, ]; const dirReadableList = [ 'public/install', 'public/portal', 'public/api', 'public/api/v1', 'public/api/v1/portal-access', '.', ]; const dirWritableList = [ 'data', 'custom', 'custom/Espo', 'custom/Espo/Custom', 'client/custom', 'client/modules', 'application/Espo/Modules', ]; fileList.forEach(item => { const path = item.folder || '.'; const name = item.name; cp.execSync( `find ${path} -type f -iname "${name}" -exec chmod 644 {} +`, { cwd: dirPath, } ); }); dirReadableList.forEach(item => { cp.execSync( `chmod 755 ${item}`, { cwd: dirPath, } ); }); dirWritableList.forEach(item => { cp.execSync( `chmod 775 ${item}`, { cwd: dirPath, } ); }); cp.execSync( `find bin -type f -exec chmod 754 {} +`, { cwd: dirPath, } ); }); grunt.registerTask('composer-install', () => { cp.execSync("composer install --no-dev", {stdio: 'ignore'}); }); grunt.registerTask('composer-install-dev', () => { cp.execSync("composer install", {stdio: 'ignore'}); }); grunt.registerTask('upgrade', () => { cp.execSync("node diff --closest", {stdio: 'inherit'}); }); grunt.registerTask('unit-tests-run', () => { cp.execSync("vendor/bin/phpunit ./tests/unit", {stdio: 'inherit'}); }); grunt.registerTask('integration-tests-run', () => { cp.execSync("vendor/bin/phpunit ./tests/integration", {stdio: 'inherit'}); }); grunt.registerTask('set-config-params', () => { cp.execSync("composer run-script setConfigParams", {stdio: 'ignore'}); }); grunt.registerTask('zip', function () { // Don't change to arrow-function. const archiver = require('archiver'); const resolve = this.async(); const folder = 'EspoCRM-' + pkg.version; const zipPath = 'build/' + folder + '.zip'; if (fs.existsSync(zipPath)) { fs.unlinkSync(zipPath); } const archive = archiver('zip'); archive.on('error', err => { grunt.fail.warn(err); }); const zipOutput = fs.createWriteStream(zipPath); zipOutput.on('close', () => { console.log("EspoCRM package has been built."); resolve(); }); archive .directory(currentPath + '/build/' + folder, folder) .pipe(zipOutput); archive.finalize(); }); grunt.registerTask('npm-install', () => { cp.execSync("npm ci", {stdio: 'ignore'}); }); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-mkdir'); grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-replace'); grunt.registerTask('internal', [ 'less', 'cssmin', 'prepare-lib-original', 'transpile', 'bundle', 'bundle-templates', 'uglify:bundle', 'copy:frontendLib', 'prepare-lib', 'uglify:lib', ]); grunt.registerTask('offline', [ 'clean:start', 'mkdir:tmp', 'internal', 'copy:frontend', 'copy:backend', 'replace', 'clean:beforeFinal', 'copy:final', 'chmod-folders', 'chmod-multiple', 'clean:final', ]); grunt.registerTask('build', [ 'composer-install', 'npm-install', 'set-config-params', 'offline', ]); grunt.registerTask('default', [ 'build', ]); grunt.registerTask('release', [ 'build', 'upgrade', 'zip', 'clean:release', ]); grunt.registerTask('run-tests', [ 'test', 'unit-tests-run', 'integration-tests-run', ]); grunt.registerTask('run-unit-tests', [ 'composer-install-dev', 'unit-tests-run', ]); grunt.registerTask('run-integration-tests', [ 'test', 'integration-tests-run', ]); grunt.registerTask('dev', [ 'composer-install-dev', 'npm-install', 'internal', ]); grunt.registerTask('test', [ 'composer-install-dev', 'npm-install', 'offline', ]); };