@ckeditor/ckeditor5-dev-utils
Version:
Utils for CKEditor 5 development tools packages.
675 lines (674 loc) • 22.4 kB
JavaScript
import { createRequire } from "node:module";
import { styleText } from "node:util";
import path from "node:path";
import { Features } from "lightningcss";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
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 fs, { readFileSync } from "node:fs";
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";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
//#endregion
//#region src/logger/index.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const levels = /* @__PURE__ */ new Map();
levels.set("silent", /* @__PURE__ */ 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 });
}
};
}
//#endregion
//#region src/loaders/resolve-loader.ts
/**
* @license Copyright (c) 2003-2026, 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);
}
//#endregion
//#region src/loaders/getcoverageloader.ts
/**
* @license Copyright (c) 2003-2026, 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].replace(/ckeditor5-!\(([^)]+)\)\*/, "ckeditor5-(?!$1)[^" + escapedPathSep + "]+").replace("ckeditor5-*", "ckeditor5-[a-z]+");
return new RegExp(packageName + escapedPathSep + "src" + escapedPathSep);
}
}).filter((path) => path);
return [...new Set(values)];
}
//#endregion
//#region src/loaders/getdebugloader.ts
/**
* @license Copyright (c) 2003-2026, 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 }
};
}
//#endregion
//#region src/loaders/gettypescriptloader.ts
/**
* @license Copyright (c) 2003-2026, 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)
};
}
//#endregion
//#region src/loaders/geticonsloader.ts
/**
* @license Copyright (c) 2003-2026, 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")]
};
}
//#endregion
//#region src/loaders/getformattedtextloader.ts
/**
* @license Copyright (c) 2003-2026, 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")]
};
}
//#endregion
//#region src/loaders/getjavascriptloader.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getJavaScriptLoader({ debugFlags }) {
return {
test: /\.js$/,
...getDebugLoader(debugFlags)
};
}
//#endregion
//#region src/loaders/getstylesloader.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
function getStylesLoader(options) {
const { minify = false, sourceMap = false, extractToSeparateFile = false } = options;
const getBundledLoader = () => ({
loader: resolveLoader("style-loader"),
options: {
injectType: "singletonStyleTag",
attributes: { "data-cke": true }
}
});
const getExtractedLoader = () => {
return MiniCssExtractPlugin.loader;
};
const getCssLoader = () => ({
loader: resolveLoader("css-loader"),
options: {
importLoaders: 1,
sourceMap
}
});
const getLightningCssLoader = () => ({
loader: path.join(import.meta.dirname, "ck-lightningcss-loader.js"),
options: { lightningCssOptions: {
minify,
sourceMap,
include: Features.Nesting
} }
});
return {
test: /\.css$/,
use: [
extractToSeparateFile ? getExtractedLoader() : getBundledLoader(),
getCssLoader(),
getLightningCssLoader()
].filter(Boolean)
};
}
//#endregion
//#region src/loaders/index.ts
var loaders_exports = /* @__PURE__ */ __exportAll({
getCoverageLoader: () => getCoverageLoader,
getDebugLoader: () => getDebugLoader,
getFormattedTextLoader: () => getFormattedTextLoader,
getIconsLoader: () => getIconsLoader,
getJavaScriptLoader: () => getJavaScriptLoader,
getStylesLoader: () => getStylesLoader,
getTypeScriptLoader: () => getTypeScriptLoader
});
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
//#endregion
//#region src/stream/noop.ts
/**
* @license Copyright (c) 2003-2026, 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);
});
}
//#endregion
//#region src/stream/index.ts
var stream_exports = /* @__PURE__ */ __exportAll({ noop: () => noop });
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
//#endregion
//#region src/tools/createspinner.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
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);
}
}
//#endregion
//#region src/tools/getdirectories.ts
/**
* @license Copyright (c) 2003-2026, 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));
});
}
//#endregion
//#region src/tools/shexec.ts
/**
* @license Copyright (c) 2003-2026, 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 {
resolve(execHandler({
code,
stdout,
stderr,
verbosity,
command
}));
} 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;
}
//#endregion
//#region src/tools/updatejsonfile.ts
/**
* @license Copyright (c) 2003-2026, 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");
}
//#endregion
//#region src/tools/commit.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
const CHUNK_LENGTH_LIMIT = 4e3;
async function commit({ cwd, message, files, dryRun = false }) {
cwd = upath.normalize(cwd);
const git = simpleGit({ baseDir: cwd });
const filteredFiles = await getFilesToCommit(cwd, files, git);
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);
if (!(await git.status()).isClean()) await git.commit(message);
}
function splitPathsIntoChunks(filePaths) {
return filePaths.reduce((chunks, singlePath) => {
const lastChunk = chunks.at(-1);
if ([...lastChunk, singlePath].join(" ").length < 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);
return upath.win32.isAbsolute(normalized) || upath.posix.isAbsolute(normalized) ? 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 gitTracked = (await git.raw(["ls-files", "--stage"])).split("\n").map((line) => line.trim().split(" ").pop()).filter(Boolean).map((p) => upath.normalize(p));
return new Set(gitTracked);
}
//#endregion
//#region src/tools/index.ts
var tools_exports = /* @__PURE__ */ __exportAll({
commit: () => commit,
createSpinner: () => createSpinner,
getDirectories: () => getDirectories,
shExec: () => shExec,
updateJSONFile: () => updateJSONFile
});
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
//#endregion
//#region src/npm/pacotecacheless.ts
/**
* @license Copyright (c) 2003-2026, 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
});
}
};
}
//#endregion
//#region src/npm/checkversionavailability.ts
/**
* @license Copyright (c) 2003-2026, 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(() => {
return false;
}).catch(() => {
return true;
});
}
//#endregion
//#region src/npm/index.ts
var npm_exports = /* @__PURE__ */ __exportAll({
checkVersionAvailability: () => checkVersionAvailability,
manifest: () => manifest,
packument: () => packument
});
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
//#endregion
//#region src/workspaces/findpathstopackages.ts
/**
* @license Copyright (c) 2003-2026, 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;
}
return (await glob(pattern, globOptions)).map((path) => upath.normalize(path));
}
//#endregion
//#region src/workspaces/getpackagejson.ts
/**
* @license Copyright (c) 2003-2026, 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);
}
//#endregion
//#region src/workspaces/getrepositoryurl.ts
/**
* @license Copyright (c) 2003-2026, 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) return findRepositoryUrl(getPackageJson(cwd));
return getPackageJson(cwd, { async: true }).then(findRepositoryUrl);
}
function findRepositoryUrl(packageJson) {
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}`;
}
if (/^[^/\s]+\/[^/\s]+$/.test(repositoryUrl)) return `https://github.com/${repositoryUrl}`;
throw new Error(`The repository URL "${repositoryUrl}" is not supported.`);
}
//#endregion
//#region src/workspaces/index.ts
var workspaces_exports = /* @__PURE__ */ __exportAll({
findPathsToPackages: () => findPathsToPackages,
getPackageJson: () => getPackageJson,
getRepositoryUrl: () => getRepositoryUrl
});
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
//#endregion
//#region src/index.ts
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
//#endregion
export { loaders_exports as loaders, logger, npm_exports as npm, stream_exports as stream, tools_exports as tools, workspaces_exports as workspaces };