@ckeditor/ckeditor5-dev-utils
Version:
Utils for CKEditor 5 development tools packages.
1,143 lines (1,099 loc) • 38.7 kB
JavaScript
import { styleText } from 'node:util';
import fs, { readFileSync } from 'node:fs';
import path from 'node:path';
import { CKEditorTranslationsPlugin } from '@ckeditor/ckeditor5-dev-translations';
import { createRequire } from 'node:module';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import postCssImport from 'postcss-import';
import postCssMixins from 'postcss-mixins';
import postCssNesting from 'postcss-nesting';
import cssnano from 'cssnano';
import postcss from 'postcss';
import { PassThrough } from 'node:stream';
import through from 'through2';
import readline from 'node:readline';
import isInteractive from 'is-interactive';
import cliSpinners from 'cli-spinners';
import cliCursor from 'cli-cursor';
import sh from 'shelljs';
import { simpleGit } from 'simple-git';
import upath from 'upath';
import fs$1, { readFile } from 'node:fs/promises';
import os from 'node:os';
import { randomUUID } from 'node:crypto';
import pacote from 'pacote';
import { glob } from 'glob';
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const levels = new Map();
levels.set('silent', new Set([]));
levels.set('info', new Set(['info']));
levels.set('warning', new Set(['info', 'warning']));
levels.set('error', new Set(['info', 'warning', 'error']));
/**
* Logger module which allows configuring the verbosity level.
*
* There are three levels of verbosity:
* 1. `info` - all messages will be logged,
* 2. `warning` - warning and errors will be logged,
* 3. `error` - only errors will be logged.
*
* Usage:
*
* import { logger } from '@ckeditor/ckeditor5-dev-utils';
*
* const infoLog = logger( 'info' );
* infoLog.info( 'Message.' ); // This message will be always displayed.
* infoLog.warning( 'Message.' ); // This message will be always displayed.
* infoLog.error( 'Message.' ); // This message will be always displayed.
*
* const warningLog = logger( 'warning' );
* warningLog.info( 'Message.' ); // This message won't be displayed.
* warningLog.warning( 'Message.' ); // This message will be always displayed.
* warningLog.error( 'Message.' ); // This message will be always displayed.
*
* const errorLog = logger( 'error' );
* errorLog.info( 'Message.' ); // This message won't be displayed.
* errorLog.warning( 'Message.' ); // This message won't be displayed.
* errorLog.error( 'Message.' ); // This message will be always displayed.
*
* Additionally, the `logger#error()` method prints the error instance if provided as the second argument.
*/
function logger(moduleVerbosity = 'info') {
return {
info(message) {
this._log('info', message);
},
warning(message) {
this._log('warning', styleText('yellow', message));
},
error(message, error) {
this._log('error', styleText('red', message), error);
},
_log(messageVerbosity, message, error) {
if (!levels.get(messageVerbosity).has(moduleVerbosity)) {
return;
}
console.log(message);
if (error) {
console.dir(error, { depth: null });
}
}
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getLicenseBanner() {
const date = new Date();
// License banner starts with `!`. That combines with uglifyjs' `comments` /^!/ option
// make webpack preserve that banner while cleaning code from others comments during the build task.
// It's because UglifyJsWebpackPlugin minification takes place after adding a banner.
/* eslint-disable @stylistic/indent */
return (`/*!
* @license Copyright (c) 2003-${date.getFullYear()}, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/`);
/* eslint-enable @stylistic/indent */
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$7 = /*#__PURE__*/Object.freeze({
__proto__: null,
getLicenseBanner: getLicenseBanner
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const require = createRequire(import.meta.url);
/**
* This can be replaced with `fileURLToPath( import.meta.resolve( '<NAME>' ) )`
* once Vitest 4 releases and we update to it.
*
* In Vitest 3 and earlier, `import.meta.resolve` results in the following error:
*
* ```
* __vite_ssr_import_meta__.resolve is not a function
* ```
*/
function resolveLoader(loaderName) {
return require.resolve(loaderName);
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const escapedPathSep = path.sep == '/' ? '/' : '\\\\';
function getCoverageLoader({ files }) {
return {
test: /\.[jt]s$/,
use: [
{
loader: resolveLoader('babel-loader'),
options: {
plugins: [
'babel-plugin-istanbul'
]
}
}
],
include: getPathsToIncludeForCoverage(files),
exclude: [
new RegExp(`${escapedPathSep}(lib)${escapedPathSep}`)
]
};
}
/**
* Returns an array of `/ckeditor5-name\/src\//` regexps based on passed globs.
* E.g., `ckeditor5-utils/**\/*.js` will be converted to `/ckeditor5-utils\/src/`.
*
* This loose way of matching packages for CC works with packages under various paths.
* E.g., `workspace/ckeditor5-utils` and `ckeditor5/node_modules/ckeditor5-utils` and every other path.
*/
function getPathsToIncludeForCoverage(globs) {
const values = globs
.reduce((returnedPatterns, globPatterns) => {
returnedPatterns.push(...globPatterns);
return returnedPatterns;
}, [])
.map(glob => {
const matchCKEditor5 = glob.match(/\/(ckeditor5-[^/]+)\/(?!.*ckeditor5-)/);
if (matchCKEditor5) {
const packageName = matchCKEditor5[1]
// A special case when --files='!engine' or --files='!engine|ui' was passed.
// Convert it to /ckeditor5-(?!engine)[^/]\/src\//.
.replace(/ckeditor5-!\(([^)]+)\)\*/, 'ckeditor5-(?!$1)[^' + escapedPathSep + ']+')
.replace('ckeditor5-*', 'ckeditor5-[a-z]+');
return new RegExp(packageName + escapedPathSep + 'src' + escapedPathSep);
}
})
// Filter undefined ones.
.filter(path => path);
return [...new Set(values)];
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @param {Array.<string>} debugFlags
* @returns {object}
*/
function getDebugLoader(debugFlags) {
return {
loader: path.join(import.meta.dirname, 'ck-debug-loader.js'),
options: { debugFlags }
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getTypeScriptLoader(options = {}) {
const { configFile = 'tsconfig.json', debugFlags = [], includeDebugLoader = false } = options;
return {
test: /\.ts$/,
use: [
{
loader: resolveLoader('esbuild-loader'),
options: {
target: 'es2022',
tsconfig: configFile
}
},
includeDebugLoader ? getDebugLoader(debugFlags) : null
].filter(Boolean)
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getIconsLoader({ matchExtensionOnly = false } = {}) {
return {
test: matchExtensionOnly ? /\.svg$/ : /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
use: [resolveLoader('raw-loader')]
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getFormattedTextLoader() {
return {
test: /\.(txt|html|rtf)$/,
use: [resolveLoader('raw-loader')]
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getJavaScriptLoader({ debugFlags }) {
return {
test: /\.js$/,
...getDebugLoader(debugFlags)
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* A plugin that prepends a path to the file in the comment for each file
* processed by PostCSS.
*/
function themeLogger() {
return {
postcssPlugin: 'postcss-ckeditor5-theme-logger',
Once(root) {
root.prepend(`/* ${root.source.input.file} */ \n`);
}
};
}
themeLogger.postcss = true;
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Returns a (CKEditor 5) package name the file belongs to.
*
* E.g., for the path to the file:
*
* "/foo/ckeditor5/packages/ckeditor5-bar/baz.css"
*
* it outputs
*
* "ckeditor5-bar"
*
* It always returns the last found package. Sometimes the whole project can be located
* under path which starts with `ckeditor5-`. In this case it isn't a package and it doesn't make
* sense to return the directory name. See #381.
*
* E.g., for the path from the package directory to the file:
*
* "/foo/ckeditor5/packages/ckeditor5-editor-classic/node_modules/@ckeditor/ckeditor5-bar/baz.css"
*
* it outputs
*
* "ckeditor5-bar"
*/
function getPackageName(inputFilePath) {
const match = inputFilePath.match(/^.+[/\\](ckeditor5-[^/\\]+)/);
if (match) {
return match.pop();
}
else {
return null;
}
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const log = logger();
/**
* A PostCSS plugin that loads a theme files from specified path.
*
* For any CSS file processed by the PostCSS, this plugin tries to find a complementary
* theme file and load it (knowing the path to the theme). Theme files must be organized
* to reflect the structure of the CSS files in editor packages,
*
* E.g., if the path to the theme is:
* `/foo/bar/ckeditor5-theme-foo/theme/theme.css`
*
* and the CSS to be themed is:
* `/baz/qux/ckeditor5-qux/theme/components/button.css`
*
* the theme file for `button.css` should be located under:
* `/foo/bar/ckeditor5-theme-foo/ckeditor5-qux/theme/components/button.css`
*
* See the `ThemeImporterOptions` to learn about importer options.
*
* To learn more about PostCSS plugins, please refer to the API
* [documentation](http://api.postcss.org/postcss.html#.plugin) of the project.
*/
function themeImporter(pluginOptions = {}) {
return {
postcssPlugin: 'postcss-ckeditor5-theme-importer',
Once(root, { result }) {
// Clone the options, don't alter the original options object.
const options = Object.assign({}, pluginOptions, {
debug: pluginOptions.debug || false,
postCssOptions: {
plugins: [
postCssImport(),
themeLogger()
]
},
root,
result
});
return importThemeFile(options);
}
};
}
themeImporter.postcss = true;
/**
* Imports a complementary theme file corresponding with a CSS file being processed by
* PostCSS, if such a theme file exists.
*/
function importThemeFile(options) {
const inputFilePath = options.root.source.input.file;
// A corresponding theme file e.g. "/foo/bar/ckeditor5-theme-baz/theme/ckeditor5-qux/components/button.css".
const themeFilePath = getThemeFilePath(options.themePath, inputFilePath);
if (themeFilePath) {
if (options.debug) {
log.info(`[ThemeImporter] Loading for "${styleText('cyan', inputFilePath)}".`);
}
options.fileToImport = themeFilePath;
options.fileToImportParent = inputFilePath;
return importFile(options);
}
}
/**
* Imports a CSS file specified in the options using the postcss-import
* plugin and appends its content to the css tree (root).
*/
function importFile(options) {
const { root, result, sourceMap } = options;
const file = options.fileToImport;
const parent = options.fileToImportParent;
const processingOptions = {
from: file,
to: file,
map: sourceMap ? { inline: true } : false
};
if (!fs.existsSync(file)) {
if (options.debug) {
log.info(`[ThemeImporter] Failed to find "${styleText('yellow', file)}".`);
}
return;
}
return postcss(options.postCssOptions)
.process(`@import "${file}";`, processingOptions)
.then(importResult => {
// Merge the CSS trees.
root.append(importResult.root.nodes);
// Let the watcher know that the theme file should be observed too.
result.messages.push({
file, parent,
type: 'dependency'
});
// `importResult` contains references to all dependencies that were used.
// We need to inform the base file (the file which imports the *.css file) that these dependencies should be watched too.
importResult.messages.forEach(message => {
result.messages.push(message);
});
if (options.debug) {
log.info(`[ThemeImporter] Loaded "${styleText('green', file)}".`);
}
})
.catch(error => {
throw error;
});
}
/**
* For a given path to the theme, and a path to the CSS file processed by
* PostCSS, it returns a path to the complementary file in the theme.
*
* E.g., if the path to the theme is:
* `/foo/bar/ckeditor5-theme-foo/theme/theme.css`
*
* and the CSS to be themed is:
* `/baz/qux/ckeditor5-qux/theme/components/button.css`
*
* this helper will return:
* `/foo/bar/ckeditor5-theme-foo/ckeditor5-qux/theme/components/button.css`
*
* @param themePath Path to the theme.
* @param inputFilePath Path to the CSS file which is to be themed.
*/
function getThemeFilePath(themePath, inputFilePath) {
// ckeditor5-theme-foo/theme/theme.css -> ckeditor5-theme-foo/theme
themePath = path.dirname(themePath);
// "ckeditor5-qux"
const packageName = getPackageName(inputFilePath);
// Don't load theme file for files not belonging to a "ckeditor5-*" package.
if (!packageName) {
return;
}
// "components/button.css"
const inputFileName = inputFilePath.split(path.join(packageName, 'theme', path.sep))[1];
// Don't load theme file for files not belonging to "ckeditor5-*/theme" folder.
if (!inputFileName) {
return;
}
// A corresponding theme file e.g. "/foo/bar/ckeditor5-theme-baz/theme/ckeditor5-qux/components/button.css".
return path.resolve(themePath, packageName, inputFileName);
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Returns a PostCSS configuration to build the editor styles (e.g., used by postcss-loader).
*/
function getPostCssConfig(options = {}) {
const config = {
plugins: [
postCssImport(),
themeImporter(options.themeImporter),
postCssMixins(),
postCssNesting({
// https://github.com/ckeditor/ckeditor5/issues/11730
noIsPseudoSelector: true,
edition: '2021'
}),
themeLogger()
]
};
if (options.sourceMap) {
config.sourceMap = 'inline';
}
if (options.minify) {
config.plugins.push(cssnano({
preset: 'default'
}));
}
return config;
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$6 = /*#__PURE__*/Object.freeze({
__proto__: null,
getPostCssConfig: getPostCssConfig,
themeImporter: themeImporter
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getStylesLoader(options) {
const { themePath, minify = false, sourceMap = false, extractToSeparateFile = false, skipPostCssLoader = false } = options;
const getBundledLoader = () => ({
loader: resolveLoader('style-loader'),
options: {
injectType: 'singletonStyleTag',
attributes: {
'data-cke': true
}
}
});
const getExtractedLoader = () => {
return MiniCssExtractPlugin.loader;
};
return {
test: /\.css$/,
use: [
extractToSeparateFile ? getExtractedLoader() : getBundledLoader(),
resolveLoader('css-loader'),
skipPostCssLoader ? null : {
loader: resolveLoader('postcss-loader'),
options: {
postcssOptions: getPostCssConfig({
themeImporter: { themePath },
minify,
sourceMap
})
}
}
].filter(Boolean)
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$5 = /*#__PURE__*/Object.freeze({
__proto__: null,
getCoverageLoader: getCoverageLoader,
getDebugLoader: getDebugLoader,
getFormattedTextLoader: getFormattedTextLoader,
getIconsLoader: getIconsLoader,
getJavaScriptLoader: getJavaScriptLoader,
getStylesLoader: getStylesLoader,
getTypeScriptLoader: getTypeScriptLoader
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Returns a webpack configuration that creates a bundle file for the specified package. Thanks to that, plugins exported
* by the package can be added to DLL builds.
* @returns {object}
*/
async function getDllPluginWebpackConfig(webpack, options) {
// Terser requires webpack. However, it's needed in runtime. To avoid the "Cannot find module 'webpack'" error,
// let's load the Terser dependency when `getDllPluginWebpackConfig()` is executed.
// See: https://github.com/ckeditor/ckeditor5/issues/13136.
const TerserPlugin = (await import('terser-webpack-plugin')).default;
const { name: packageName } = JSON.parse(fs.readFileSync(path.join(options.packagePath, 'package.json'), 'utf-8'));
const langDirExists = fs.existsSync(path.join(options.packagePath, 'lang'));
const indexJsExists = fs.existsSync(path.join(options.packagePath, 'src', 'index.js'));
const webpackConfig = {
mode: options.isDevelopmentMode ? 'development' : 'production',
performance: { hints: false },
// Use the `index.js` file to prepare the build to avoid potential issues if a source code differs from the published one.
entry: path.join(options.packagePath, 'src', indexJsExists ? 'index.js' : 'index.ts'),
output: {
library: ['CKEditor5', getGlobalKeyForPackage(packageName)],
path: path.join(options.packagePath, 'build'),
filename: getIndexFileName(packageName),
libraryTarget: 'window'
},
optimization: {
minimize: false
},
plugins: [
new webpack.BannerPlugin({
banner: getLicenseBanner(),
raw: true
}),
new webpack.DllReferencePlugin({
manifest: JSON.parse(fs.readFileSync(options.manifestPath, 'utf-8')),
scope: 'ckeditor5/src',
name: 'CKEditor5.dll'
})
],
resolve: {
extensions: ['.ts', '.js', '.json'],
extensionAlias: {
'.js': ['.js', '.ts']
}
},
module: {
rules: [
getIconsLoader({ matchExtensionOnly: true }),
getStylesLoader({
themePath: options.themePath,
minify: true
}),
getTypeScriptLoader({
configFile: options.tsconfigPath || 'tsconfig.json'
})
]
}
};
// Force loading JS files first if the `index.js` file exists.
if (indexJsExists) {
webpackConfig.resolve.extensions = moveArrayItem(webpackConfig.resolve.extensions, webpackConfig.resolve.extensions.indexOf('.js'), 0);
}
if (langDirExists) {
webpackConfig.plugins.push(new CKEditorTranslationsPlugin({
// UI language. Language codes follow the https://en.wikipedia.org/wiki/ISO_639-1 format.
language: 'en',
additionalLanguages: 'all',
sourceFilesPattern: /^src[/\\].+\.[jt]s$/,
skipPluralFormFunction: true
}));
}
if (options.isDevelopmentMode) {
webpackConfig.devtool = 'source-map';
}
else {
webpackConfig.optimization.minimize = true;
webpackConfig.optimization.minimizer = [
new TerserPlugin({
terserOptions: {
output: {
// Preserve CKEditor 5 license comments.
comments: /^!/
}
},
extractComments: false
})
];
}
return webpackConfig;
}
/**
* Transforms the package name (`@ckeditor/ckeditor5-foo-bar`) to the name that will be used while
* exporting the library into the global scope.
*/
function getGlobalKeyForPackage(packageName) {
return packageName
.replace(/^@ckeditor\/ckeditor5?-/, '')
.replace(/-([a-z])/g, (match, p1) => p1.toUpperCase());
}
/**
* Extracts the main file name from the package name.
*/
function getIndexFileName(packageName) {
return packageName.replace(/^@ckeditor\/ckeditor5?-/, '') + '.js';
}
function moveArrayItem(source, indexFrom, indexTo) {
const tmp = source.slice();
tmp.splice(indexTo, 0, ...tmp.splice(indexFrom, 1));
return tmp;
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$4 = /*#__PURE__*/Object.freeze({
__proto__: null,
getDllPluginWebpackConfig: getDllPluginWebpackConfig
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function noop(callback) {
if (!callback) {
return new PassThrough({ objectMode: true });
}
return through({ objectMode: true }, (chunk, encoding, throughCallback) => {
const callbackResult = callback(chunk);
if (callbackResult instanceof Promise) {
callbackResult
.then(() => {
throughCallback(null, chunk);
})
.catch(err => {
throughCallback(err);
});
}
else {
throughCallback(null, chunk);
}
});
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$3 = /*#__PURE__*/Object.freeze({
__proto__: null,
noop: noop
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
// A size of default indent for a log.
const INDENT_SIZE = 3;
/**
* A factory function that creates an instance of a CLI spinner. It supports both a spinner CLI and a spinner with a counter.
*
* The spinner improves UX when processing a time-consuming task. A developer does not have to consider whether the process hanged on.
*
* @param title Description of the current processed task.
* @param [options={}]
*/
function createSpinner(title, options = {}) {
const isEnabled = !options.isDisabled && isInteractive();
const indentLevel = options.indentLevel || 0;
const indent = ' '.repeat(indentLevel * INDENT_SIZE);
const emoji = options.emoji || '📍';
const status = options.status || '[title] Status: [current]/[total].';
const spinnerType = typeof options.total === 'number' ? 'counter' : 'spinner';
let timerId;
let counter = 0;
return {
start() {
if (!isEnabled) {
console.log(`${emoji} ${title}`);
return;
}
const { frames } = cliSpinners.dots12;
const getMessage = () => {
if (spinnerType === 'spinner') {
return title;
}
if (typeof options.status === 'function') {
return options.status(title, counter, options.total);
}
return `${status}`
.replace('[title]', title)
.replace('[current]', String(counter))
.replace('[total]', options.total.toString());
};
let index = 0;
let shouldClearLastLine = false;
cliCursor.hide();
timerId = setInterval(() => {
if (index === frames.length) {
index = 0;
}
if (shouldClearLastLine) {
clearLastLine();
}
process.stdout.write(`${indent}${frames[index++]} ${getMessage()}`);
shouldClearLastLine = true;
}, cliSpinners.dots12.interval);
},
increase() {
if (spinnerType === 'spinner') {
throw new Error('The \'#increase()\' method is available only when using the counter spinner.');
}
counter += 1;
},
finish(options = {}) {
const finishEmoji = options.emoji || emoji;
if (!isEnabled) {
return;
}
clearInterval(timerId);
clearLastLine();
if (spinnerType === 'counter') {
clearLastLine();
}
cliCursor.show();
console.log(`${indent}${finishEmoji} ${title}`);
}
};
function clearLastLine() {
readline.clearLine(process.stdout, 1);
readline.cursorTo(process.stdout, 0);
}
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Returns array with all directories under the specified path.
*/
function getDirectories(directoryPath) {
const isDirectory = (directoryPath) => {
try {
return fs.statSync(directoryPath).isDirectory();
}
catch {
return false;
}
};
return fs.readdirSync(directoryPath)
.filter(item => {
return isDirectory(path.join(directoryPath, item));
});
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function shExec(command, options = {}) {
const { verbosity = 'info', cwd = process.cwd(), async = false } = options;
sh.config.silent = true;
const execOptions = { cwd };
if (async) {
return new Promise((resolve, reject) => {
sh.exec(command, execOptions, (code, stdout, stderr) => {
try {
const result = execHandler({ code, stdout, stderr, verbosity, command });
resolve(result);
}
catch (err) {
reject(err);
}
});
});
}
const { code, stdout, stderr } = sh.exec(command, execOptions);
return execHandler({ code, stdout, stderr, verbosity, command });
}
function execHandler({ code, stdout, stderr, verbosity, command }) {
const log = logger(verbosity);
const grey = (text) => styleText('grey', text);
if (code) {
if (stdout) {
log.error(grey(stdout));
}
if (stderr) {
log.error(grey(stderr));
}
throw new Error(`Error while executing ${command}: ${stderr}`);
}
if (stdout) {
log.info(grey(stdout));
}
if (stderr) {
log.info(grey(stderr));
}
return stdout;
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Updates JSON file under a specified path.
*
* @param filePath Path to a file on disk.
* @param updateFunction Function that will be called with a parsed JSON object. It should return the modified JSON object to save.
*/
function updateJSONFile(filePath, updateFunction) {
const contents = fs.readFileSync(filePath, 'utf-8');
let json = JSON.parse(contents);
json = updateFunction(json);
fs.writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const CHUNK_LENGTH_LIMIT = 4000;
async function commit({ cwd, message, files, dryRun = false }) {
cwd = upath.normalize(cwd);
const git = simpleGit({ baseDir: cwd });
const filteredFiles = await getFilesToCommit(cwd, files, git);
// To avoid an error when trying to commit a non-existing path.
if (!filteredFiles.length) {
return;
}
if (dryRun) {
const lastCommit = await git.log(['-1']);
await makeCommit(git, message, filteredFiles);
await git.reset([lastCommit.latest.hash]);
}
else {
await makeCommit(git, message, filteredFiles);
}
}
async function makeCommit(git, message, filteredFiles) {
for (const chunk of splitPathsIntoChunks(filteredFiles)) {
await git.add(chunk);
}
const status = await git.status();
if (!status.isClean()) {
await git.commit(message);
}
}
function splitPathsIntoChunks(filePaths) {
return filePaths.reduce((chunks, singlePath) => {
const lastChunk = chunks.at(-1);
const newLength = [...lastChunk, singlePath].join(' ').length;
if (newLength < CHUNK_LENGTH_LIMIT) {
lastChunk.push(singlePath);
}
else {
chunks.push([singlePath]);
}
return chunks;
}, [[]]);
}
/**
* Returns a set of Git-tracked file paths by parsing `git ls-files --stage`.
* Supports file names with spaces using tab-splitting.
*/
async function getFilesToCommit(cwd, files, git) {
const gitTracked = await getTrackedFiles(git);
const filePromises = files
.map(filePath => {
const normalized = upath.normalize(filePath);
// `upath` and Unix environment may fail on detecting a Windows-like path.
// Hence, let's use `isAbsolute` from both systems.
const isAbsolute = upath.win32.isAbsolute(normalized) || upath.posix.isAbsolute(normalized);
return isAbsolute ? upath.relative(cwd, normalized) : normalized;
})
.map(async (itemPath) => {
if (gitTracked.has(itemPath)) {
return itemPath;
}
const fullPath = upath.join(cwd, itemPath);
try {
await fs$1.access(fullPath);
return itemPath;
}
catch {
return null;
}
});
return (await Promise.all(filePromises))
.filter((pathOrNull) => pathOrNull !== null);
}
/**
* Returns a set of Git-tracked files in a current repository.
*/
async function getTrackedFiles(git) {
const gitTrackedOutput = await git.raw(['ls-files', '--stage']);
const gitTracked = gitTrackedOutput
.split('\n')
// <mode> <object> <stage>\t<file>
// Split by tab and take the last part, which is the file path that could contain spaces.
.map(line => line.trim().split('\t').pop())
.filter(Boolean)
.map(p => upath.normalize(p));
return new Set(gitTracked);
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$2 = /*#__PURE__*/Object.freeze({
__proto__: null,
commit: commit,
createSpinner: createSpinner,
getDirectories: getDirectories,
shExec: shExec,
updateJSONFile: updateJSONFile
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const manifest = cacheLessPacoteFactory(pacote.manifest);
const packument = cacheLessPacoteFactory(pacote.packument);
/**
* Creates a version of a `pacote` function that doesn't use caching.
*/
function cacheLessPacoteFactory(callback) {
return async (...args) => {
const [description, options = {}] = args;
const uuid = randomUUID();
const cacheDir = upath.join(os.tmpdir(), `pacote--${uuid}`);
await fs$1.mkdir(cacheDir, { recursive: true });
try {
return await callback(description, {
...options,
cache: cacheDir,
memoize: false,
preferOnline: true
});
}
finally {
await fs$1.rm(cacheDir, { recursive: true, force: true });
}
};
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Checks if a specific version of a package is available in the npm registry.
*/
async function checkVersionAvailability(version, packageName) {
return manifest(`${packageName}@${version}`)
.then(() => {
// If `manifest` resolves, a package with the given version exists.
return false;
})
.catch(() => {
// When throws, the package does not exist.
return true;
});
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
checkVersionAvailability: checkVersionAvailability,
manifest: manifest,
packument: packument
});
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* This function locates package.json files for all packages located in `packagesDirectory` in the repository structure.
*/
async function findPathsToPackages(cwd, packagesDirectory, options = {}) {
const { includePackageJson = false, includeCwd = false, packagesDirectoryFilter = null } = options;
const packagePaths = await getPackages(cwd, packagesDirectory, includePackageJson);
if (includeCwd) {
if (includePackageJson) {
packagePaths.push(upath.join(cwd, 'package.json'));
}
else {
packagePaths.push(cwd);
}
}
const normalizedPaths = packagePaths.map(item => upath.normalize(item));
if (packagesDirectoryFilter) {
return normalizedPaths.filter(item => packagesDirectoryFilter(item));
}
return normalizedPaths;
}
async function getPackages(cwd, packagesDirectory, includePackageJson) {
if (!packagesDirectory) {
return Promise.resolve([]);
}
const globOptions = {
cwd: upath.join(cwd, packagesDirectory),
absolute: true
};
let pattern = '*/';
if (includePackageJson) {
pattern += 'package.json';
globOptions.nodir = true;
}
const paths = await glob(pattern, globOptions);
return paths.map(path => upath.normalize(path));
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* Reads and returns the contents of the package.json file.
*/
function getPackageJson(cwd = process.cwd(), { async = false } = {}) {
const path = upath.join(cwd, 'package.json');
if (async) {
return readFile(path, 'utf-8').then(data => JSON.parse(data));
}
const data = readFileSync(path, 'utf-8');
return JSON.parse(data);
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* This function extracts the repository URL for generating links in the changelog.
*/
function getRepositoryUrl(cwd, { async = false } = {}) {
if (!async) {
const packageJson = getPackageJson(cwd);
return findRepositoryUrl(packageJson);
}
return getPackageJson(cwd, { async: true }).then(findRepositoryUrl);
}
function findRepositoryUrl(packageJson) {
// Due to merging our issue trackers, `packageJson.bugs` will point to the same place for every package.
// We cannot rely on this value anymore. See: https://github.com/ckeditor/ckeditor5/issues/1988.
// Instead of we can take a value from `packageJson.repository` and adjust it to match to our requirements.
let repositoryUrl = (typeof packageJson.repository === 'object') ? packageJson.repository.url : packageJson.repository;
if (!repositoryUrl) {
throw new Error(`The package.json for "${packageJson.name}" must contain the "repository" property.`);
}
if (repositoryUrl.startsWith('git+')) {
repositoryUrl = repositoryUrl.slice(4);
}
const match = repositoryUrl.match(/^(?:https?:\/\/|git@)github\.com[:/](?<owner>[^/\s]+)\/(?<repo>[^/\s]+?)(?:\.git)?(?:[/?#].*)?$/);
if (match) {
const { owner, repo } = match.groups;
return `https://github.com/${owner}/${repo}`;
}
// Short notation: `owner/repo`.
if (/^[^/\s]+\/[^/\s]+$/.test(repositoryUrl)) {
return `https://github.com/${repositoryUrl}`;
}
throw new Error(`The repository URL "${repositoryUrl}" is not supported.`);
}
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
var index = /*#__PURE__*/Object.freeze({
__proto__: null,
findPathsToPackages: findPathsToPackages,
getPackageJson: getPackageJson,
getRepositoryUrl: getRepositoryUrl
});
export { index$4 as builds, index$7 as bundler, index$5 as loaders, logger, index$1 as npm, index$3 as stream, index$6 as styles, index$2 as tools, index as workspaces };