UNPKG

@ckeditor/ckeditor5-dev-utils

Version:
756 lines (724 loc) 25 kB
import { styleText } from 'node:util'; import path from 'node:path'; import { createRequire } from 'node:module'; 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'; /** * @license Copyright (c) 2003-2026, 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-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const require$1 = 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$1.resolve(loaderName); } /** * @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] // 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-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 } }; } /** * @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) }; } /** * @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')] }; } /** * @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')] }; } /** * @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) }; } /** * @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) }; } /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ var index$4 = /*#__PURE__*/Object.freeze({ __proto__: null, 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. */ 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-2026, 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-2026, 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-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)); }); } /** * @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 { 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-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'); } /** * @license Copyright (c) 2003-2026, 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-2026, 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-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 }); } }; } /** * @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(() => { // 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-2026, 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-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; } const paths = await glob(pattern, globOptions); return paths.map(path => upath.normalize(path)); } /** * @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); } /** * @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) { 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-2026, 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 loaders, logger, index$1 as npm, index$3 as stream, index$2 as tools, index as workspaces };