UNPKG

@bitrix/cli

Version:
697 lines (664 loc) 23.5 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var Mocha = _interopDefault(require('mocha')); var Logger = _interopDefault(require('@bitrix/logger')); var minimist = _interopDefault(require('minimist')); var os = _interopDefault(require('os')); var glob = _interopDefault(require('fast-glob')); var Ora = _interopDefault(require('ora')); var EventEmitter = _interopDefault(require('events')); var chokidar = _interopDefault(require('chokidar')); var fs = _interopDefault(require('fs')); var slash = _interopDefault(require('slash')); var path = _interopDefault(require('path')); require('colors'); var alias = { w: 'watch', p: 'path', m: 'modules', t: 'test', h: 'help', v: 'version', c: 'create', n: 'name', e: 'extensions' }; var argv = minimist(process.argv.slice(2), { alias }); function invalidateModuleCache(module, recursive, store = []) { if (typeof module === 'string') { const resolvedModule = require.resolve(module); if (require.cache[resolvedModule] && !store.includes(resolvedModule)) { store.push(resolvedModule); if (Array.isArray(require.cache[resolvedModule].children) && recursive) { require.cache[resolvedModule].children.forEach(currentModule => { invalidateModuleCache(currentModule.id, recursive, store); }); } delete require.cache[resolvedModule]; } } } const appRoot = path.resolve(__dirname, '../'); const lockFile = path.resolve(os.homedir(), '.bitrix.lock'); function makeIterable(value) { if (Array.isArray(value)) { return value; } if (typeof value !== 'undefined' && value !== null) { return [value]; } return []; } function prepareConcatConfig(files, context) { if (typeof files !== 'object') { return {}; } const result = {}; Object.keys(files).forEach(key => { if (Array.isArray(files[key])) { result[key] = files[key].map(filePath => path.resolve(context, filePath)); } }); return result; } function isEs6File(path$$1) { return typeof path$$1 === 'string' && path$$1.endsWith('script.es6.js'); } function loadSourceBundleConfig(configPath) { if (isEs6File(configPath)) { const context = configPath.replace('script.es6.js', ''); return { input: path.resolve(context, 'script.es6.js'), output: { js: path.resolve(context, 'script.js'), css: path.resolve(context, 'style.css') } }; } // eslint-disable-next-line return require(configPath); } const rcFileName = '.browserslistrc'; function getTargets(context) { if (typeof context === 'string' && context !== '') { const rcFilePath = path.resolve(context, rcFileName); if (fs.existsSync(rcFilePath)) { const content = fs.readFileSync(rcFilePath, 'utf-8'); if (typeof content === 'string') { return content.split('\n').map(rule => { return rule.trim(); }); } } else { if (context !== path.sep && !/^[A-Z]:\\$/.test(context)) { return getTargets(path.dirname(context)); } } } return ['IE >= 11', 'last 4 version']; } function getConfigs(directory) { const normalizedDirectory = `${slash(directory)}`; const pattern = [path.resolve(normalizedDirectory, '**/bundle.config.js'), path.resolve(normalizedDirectory, '**/script.es6.js')]; const options = { dot: true, cache: true, unique: false }; return glob.sync(pattern, options).reduce((acc, file) => { const context = slash(path.dirname(file)); const config = loadSourceBundleConfig(file); const configs = makeIterable(config); configs.forEach(currentConfig => { var _currentConfig$tests$, _currentConfig$tests, _currentConfig$tests$2, _currentConfig$tests$3, _currentConfig$tests2, _currentConfig$tests3; let { plugins } = currentConfig; if (currentConfig.protected && context !== normalizedDirectory) { return; } if (typeof plugins !== 'object') { plugins = { resolve: false }; } const output = (() => { const changeExt = (filePath, ext) => { const pos = filePath.lastIndexOf('.'); if (pos > 0) { return `${filePath.substr(0, pos)}.${ext}`; } return filePath; }; if (typeof currentConfig.output === 'object') { const { js } = currentConfig.output; let { css } = currentConfig.output; if (typeof css !== 'string') { css = changeExt(js, 'css'); } return { js: path.resolve(context, js), css: path.resolve(context, css) }; } return { js: path.resolve(context, currentConfig.output), css: path.resolve(context, changeExt(currentConfig.output, 'css')) }; })(); acc.push({ input: path.resolve(context, currentConfig.input), output, name: currentConfig.namespace || '', treeshake: currentConfig.treeshake !== false, adjustConfigPhp: currentConfig.adjustConfigPhp !== false, protected: currentConfig.protected === true, rel: makeIterable(currentConfig.rel), plugins, context: path.resolve(context), concat: prepareConcatConfig(currentConfig.concat, path.resolve(context)), cssImages: currentConfig.cssImages || {}, resolveFilesImport: currentConfig.resolveFilesImport || {}, targets: (() => { if (Array.isArray(currentConfig.browserslist)) { return currentConfig.browserslist.map(rule => { return rule.trim(); }); } if (typeof currentConfig.browserslist === 'string') { return currentConfig.browserslist.split(',').map(rule => { return rule.trim(); }); } if (currentConfig.browserslist === true) { return getTargets(context); } return getTargets(null); })(), transformClasses: currentConfig.transformClasses === true, minification: (() => { if (currentConfig.minification !== null && typeof currentConfig.minification === 'object') { return currentConfig.minification; } return currentConfig.minification === true; })(), sourceMaps: currentConfig.sourceMaps !== false, tests: { localization: { autoLoad: (_currentConfig$tests$ = currentConfig === null || currentConfig === void 0 ? void 0 : (_currentConfig$tests = currentConfig.tests) === null || _currentConfig$tests === void 0 ? void 0 : (_currentConfig$tests$2 = _currentConfig$tests.localization) === null || _currentConfig$tests$2 === void 0 ? void 0 : _currentConfig$tests$2.autoLoad) !== null && _currentConfig$tests$ !== void 0 ? _currentConfig$tests$ : true, languageId: (_currentConfig$tests$3 = currentConfig === null || currentConfig === void 0 ? void 0 : (_currentConfig$tests2 = currentConfig.tests) === null || _currentConfig$tests2 === void 0 ? void 0 : (_currentConfig$tests3 = _currentConfig$tests2.localization) === null || _currentConfig$tests3 === void 0 ? void 0 : _currentConfig$tests3.languageId) !== null && _currentConfig$tests$3 !== void 0 ? _currentConfig$tests$3 : 'en' } } }); }); return acc; }, []); } class Directory { constructor(dir) { this.location = dir; } getConfigs(recursive = true) { if (!Directory.configs.has(this.location)) { const configs = getConfigs(this.location).filter(config => { if (config.protected) { return config.context === this.location; } return config; }); Directory.configs.set(this.location, configs); } const configs = Directory.configs.get(this.location); if (recursive) { return configs; } const parentConfig = configs.reduce((prevConfig, config) => { if (prevConfig) { const prevContext = prevConfig.context; const currContext = config.context; if (prevContext.length < currContext.length) { return prevConfig; } } return config; }, null); if (parentConfig) { return configs.filter(config => config.context === parentConfig.context); } return configs; } } Directory.configs = new Map(); function resolveRootDirectoryByCwd(cwd) { if (typeof cwd === 'string' && cwd.length > 0) { if (cwd.includes('modules')) { const [modulesPath] = /.*modules/.exec(cwd); if (typeof modulesPath === 'string' && fs.existsSync(path.join(modulesPath, 'main')) && fs.existsSync(path.join(modulesPath, 'ui'))) { return { rootPath: modulesPath, type: 'modules' }; } } if (cwd.includes('local')) { const localPath = path.dirname(/.*local/.exec(cwd)[0]); if (typeof localPath === 'string' && fs.existsSync(path.join(localPath, 'bitrix'))) { return { rootPath: localPath, type: 'product' }; } } if (cwd.includes('bitrix')) { const productPath = path.dirname(/.*bitrix/.exec(cwd)[0]); if (typeof productPath === 'string' && fs.existsSync(path.join(productPath, 'bitrix')) && fs.existsSync(path.join(productPath, 'bitrix', 'js')) && fs.existsSync(path.join(productPath, 'bitrix', 'modules'))) { return { rootPath: productPath, type: 'product' }; } } const productPath = path.dirname(cwd); if (fs.existsSync(path.join(productPath, 'bitrix')) && fs.existsSync(path.join(productPath, 'bitrix', 'js')) && fs.existsSync(path.join(productPath, 'bitrix', 'modules'))) { return { rootPath: productPath, type: 'product' }; } if (productPath !== path.sep && !/^[A-Z]:\\$/.test(productPath)) { return resolveRootDirectoryByCwd(productPath); } } return null; } function resolveExtension(options) { const extensionPath = (() => { const rootDirectory = (() => { if (options.cwd) { return resolveRootDirectoryByCwd(options.cwd); } return resolveRootDirectoryByCwd(path.dirname(options.sourcePath)); })(); if (rootDirectory) { const nameSegments = options.name.split('.'); const [moduleName] = nameSegments; if (rootDirectory.type === 'modules') { return path.join(rootDirectory.rootPath, moduleName, 'install', 'js', ...nameSegments); } if (rootDirectory.type === 'product') { const localExtension = path.join(rootDirectory.rootPath, 'local', 'js', ...nameSegments); if (fs.existsSync(localExtension)) { return localExtension; } const productExtension = path.join(rootDirectory.rootPath, 'bitrix', 'js', ...nameSegments); if (fs.existsSync(productExtension)) { return productExtension; } } } return null; })(); if (typeof extensionPath === 'string') { const configPath = path.join(extensionPath, 'bundle.config.js'); if (fs.existsSync(configPath)) { const config = require(configPath); if (config) { return { context: extensionPath, input: path.join(extensionPath, config.input), bundleConfig: configPath }; } } } return null; } function fetchMessages(filePath) { const result = {}; if (fs.existsSync(filePath)) { const contents = fs.readFileSync(filePath, 'ascii'); const regex = /\$MESS\[['"](?<code>.+?)['"]]\s*=\s*['"](?<phrase>.*?)['"]/gm; let match; while ((match = regex.exec(contents)) !== null) { if (match.index === regex.lastIndex) { regex.lastIndex++; } result[match.groups.code] = match.groups.phrase; } } return result; } function loadMessages(options = {}) { if (Object.hasOwn(options, 'extension')) { const extensions = [options.extension].flat(); extensions.forEach(extension => { const resolverResult = resolveExtension(extension); if (resolverResult) { loadMessages({ langFile: path.join(resolverResult.context, 'lang', extension.lang, 'config.php') }); } }); } if (typeof options.langFile === 'string') { const messages = fetchMessages(options.langFile); const setMessage = (() => { var _global$window, _global$window$BX, _global$window$BX$Loc; if ((_global$window = global.window) !== null && _global$window !== void 0 && (_global$window$BX = _global$window.BX) !== null && _global$window$BX !== void 0 && (_global$window$BX$Loc = _global$window$BX.Loc) !== null && _global$window$BX$Loc !== void 0 && _global$window$BX$Loc.setMessage) { var _global$window2, _global$window2$BX, _global$window2$BX$Lo; return (_global$window2 = global.window) === null || _global$window2 === void 0 ? void 0 : (_global$window2$BX = _global$window2.BX) === null || _global$window2$BX === void 0 ? void 0 : (_global$window2$BX$Lo = _global$window2$BX.Loc) === null || _global$window2$BX$Lo === void 0 ? void 0 : _global$window2$BX$Lo.setMessage; } if (!global.window.BX) { global.window.BX = {}; } if (!global.window.BX.message) { global.window.BX.message = messages => { if (typeof messages === 'object' && messages !== null) { Object.assign(global.window.BX.message, messages); } }; } return global.window.BX.message; })(); if (setMessage) { setMessage(messages); } } if (Array.isArray(options.langFile)) { options.langFile.forEach(filePath => { loadMessages({ langFile: filePath }); }); } } function buildExtensionName(filePath, context) { const moduleExp = new RegExp('/(.[a-z0-9_-]+)/install/js/(.[a-z0-9_-]+)/'); const moduleRes = `${slash(filePath)}`.match(moduleExp); if (Array.isArray(moduleRes)) { const fragments = `${slash(context)}`.split(`${moduleRes[1]}/install/js/${moduleRes[2]}/`); return `${moduleRes[2]}.${fragments[fragments.length - 1].replace(/\/$/, '').split('/').join('.')}`; } const localExp = new RegExp('/local/js/(.[a-z0-9_-]+)/(.[a-z0-9_-]+)/'); const localRes = `${slash(filePath)}`.match(localExp); if (!Array.isArray(localRes)) { return path.basename(context); } const fragments = `${slash(context)}`.split(`/local/js/${localRes[1]}/`); return `${localRes[1]}.${fragments[fragments.length - 1].replace(/\/$/, '').split('/').join('.')}`; } /* eslint "no-restricted-syntax": "off", "no-await-in-loop": "off" */ function reporterStub() {} function appendBootstrap() { const bootstrapPath = path.resolve(appRoot, 'dist/test.bootstrap.js'); invalidateModuleCache(bootstrapPath); // eslint-disable-next-line require(bootstrapPath); } async function testDirectory(dir, report = true) { const directory = new Directory(dir); const configs = directory.getConfigs(); const result = []; if (!report) { global.currentDirectory = path.resolve(dir); } const mocha = new Mocha({ globals: Object.keys(global), reporter: argv.test || argv.t || !report ? reporterStub : 'spec', checkLeaks: true, timeout: 10000 }); appendBootstrap(); configs.forEach(config => { if (fs.existsSync(path.resolve(config.context, 'test'))) { const extensionTests = glob.sync(path.resolve(config.context, 'test/**/*.js')); if (extensionTests.length > 0) { var _config$tests, _config$tests$localiz; if ((config === null || config === void 0 ? void 0 : (_config$tests = config.tests) === null || _config$tests === void 0 ? void 0 : (_config$tests$localiz = _config$tests.localization) === null || _config$tests$localiz === void 0 ? void 0 : _config$tests$localiz.autoLoad) !== false) { loadMessages({ extension: { name: buildExtensionName(config.input, config.context), lang: config.tests.localization.languageId, cwd: config.context } }); } extensionTests.forEach(testFile => { const recursive = true; invalidateModuleCache(testFile, recursive); mocha.addFile(testFile); }); } } }); await new Promise(resolve => { mocha.run(failures => { result.push(failures ? 'failure' : 'passed'); }).on('end', () => resolve()); }); if (result.every(res => res === 'no-tests')) { return 'no-tests'; } if (result.some(res => res === 'passed') && result.every(res => res !== 'failure')) { return 'passed'; } return 'failed'; } async function test(dir, report = true) { if (Array.isArray(dir)) { for (const item of dir) { const testStatus = await testDirectory(item, report); let testResult = ''; if (testStatus === 'passed') { testResult = 'passed'.green; } if (testStatus === 'failure') { testResult = 'failed'.red; } if (testStatus === 'notests') { testResult = 'no tests'.grey; } // eslint-disable-next-line Logger.log(`Test module ${item}`.bold, `${testResult}`); } } else if (typeof dir === 'string') { return testDirectory(dir, report); } else { throw new Error('dir not string or array'); } return ''; } function getDirectories(dir) { if (fs.existsSync(path.resolve(dir))) { const pattern = slash(path.resolve(dir, '**')); const options = { onlyDirectories: true, deep: 0 }; return glob.sync(pattern, options).map(dirPath => path.basename(dirPath)); } return []; } function isRepositoryRoot(dirPath) { const dirs = getDirectories(dirPath); return dirs.includes('main') && dirs.includes('fileman') && dirs.includes('iblock') && dirs.includes('ui') && dirs.includes('translate'); } var params = { get path() { return path.resolve(argv.path || process.cwd()); }, get modules() { const modules = (argv.modules || '').split(',').map(module => module.trim()).filter(module => !!module).map(module => path.resolve(this.path, module)); if (isRepositoryRoot(this.path) && modules.length === 0) { return getDirectories(this.path); } return modules; }, get extensions() { if (typeof argv.extensions === 'string') { return argv.extensions.split(',').map(module => module.trim()); } return []; }, get name() { return argv.name || argv._[1]; } }; class Repository { constructor(path$$1) { this.path = path$$1; if (!fs.existsSync(path$$1)) { fs.writeFileSync(path$$1, ''); } } isLocked(filePath) { return fs.readFileSync(this.path, 'utf-8').split('\n').some(repoPath => !!repoPath && filePath.startsWith(repoPath)); } } var repository = new Repository(lockFile); const defaultExtensions = ['.js', '.jsx', '.vue', '.css', '.scss']; function getTrackedExtensions() { if (typeof argv.watch === 'string' && argv.watch.length > 0) { return argv.watch.split(',').map(extName => { return String(extName).trim(); }).reduce((acc, extName) => { if (typeof extName === 'string' && extName.length > 0) { if (extName === 'defaults') { return [...acc, ...defaultExtensions]; } const preparedName = (() => { if (!extName.startsWith('.')) { return `.${extName}`; } return extName; })(); if (!acc.includes(preparedName)) { acc.push(preparedName); } } return acc; }, []); } return [...defaultExtensions]; } function isAllowed(fileName) { if (typeof fileName !== 'string') { return false; } const normalizedFileName = slash(fileName); if (new RegExp('/components/(.*)/style.js').test(normalizedFileName) || new RegExp('/components/(.*)/style.css').test(normalizedFileName) && !new RegExp('/components/(.*)/src/(.*)style.css').test(normalizedFileName)) { return false; } return getTrackedExtensions().includes(path.extname(normalizedFileName)); } function isInput(dir, fileName) { return new Directory(dir).getConfigs().every(config => { return !fileName.includes(path.normalize(config.output.js)) && !fileName.includes(path.normalize(config.output.css)); }); } function isAllowedChanges(directories, file) { return directories.every(dir => isAllowed(file) && isInput(dir, file)); } const trackedExtensions = getTrackedExtensions(); function createPattern(directories) { return directories.reduce((acc, dir) => { const directory = new Directory(dir); const directoryConfigs = directory.getConfigs(); directoryConfigs.forEach(currentConfig => { trackedExtensions.forEach(extName => { acc.push(slash(path.resolve(currentConfig.context, `**/*${extName}`))); }); }); return acc; }, []); } function watch(directories) { const preparedDirectories = Array.isArray(directories) ? directories : [directories]; const pattern = createPattern(preparedDirectories); const emitter = new EventEmitter(); const watcher = chokidar.watch(pattern).on('ready', () => emitter.emit('ready', watcher)).on('change', file => { if (repository.isLocked(file)) { return; } if (!isAllowedChanges(preparedDirectories, file)) { return; } const changedConfig = preparedDirectories.reduce((acc, dir) => acc.concat(new Directory(dir).getConfigs()), []).filter(config => path.resolve(file).includes(config.context)).reduce((prevConfig, config) => { if (prevConfig && prevConfig.context.length > config.context.length) { return prevConfig; } return config; }, null); if (changedConfig) { emitter.emit('change', changedConfig); } }); process.nextTick(() => { emitter.emit('start', watcher); }); return emitter; } async function bitrixTest({ path: path$$1, extensions, modules = [] } = params) { if (Array.isArray(extensions) && extensions.length > 0) { for (const extensionName of extensions) { const resolverResult = resolveExtension({ name: extensionName, cwd: path$$1 }); if (resolverResult) { await test(resolverResult.context); } } } else { await test(modules.length ? modules : path$$1); } if (argv.watch) { return await new Promise(resolve => { const progressbar = new Ora(); const directories = (() => { if (modules.length > 0 && (!Array.isArray(extensions) || extensions.length === 0)) { return modules; } if (Array.isArray(extensions) && extensions.length > 0) { return extensions.reduce((acc, extensionName) => { const resolverResult = resolveExtension({ name: extensionName, cwd: path$$1 }); if (resolverResult) { acc.push(resolverResult.context); } return acc; }, []); } return [path$$1]; })(); const emitter = watch(directories).on('start', watcher => { progressbar.start('Run test watcher'); resolve({ watcher, emitter }); }).on('ready', () => { progressbar.succeed(`Test watcher is ready`.green.bold); }).on('change', config => { void test(config.context); }); }); } } module.exports = bitrixTest;