@bitrix/cli
Version:
Bitrix CLI tools
697 lines (664 loc) • 23.5 kB
JavaScript
;
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;