diff --git a/packages/tools/kolibri-cli/package.json b/packages/tools/kolibri-cli/package.json index 7eb4d518cd3..20bb8aef4d6 100644 --- a/packages/tools/kolibri-cli/package.json +++ b/packages/tools/kolibri-cli/package.json @@ -30,8 +30,8 @@ "restart": "pnpm reset && pnpm start", "unused": "knip", "watch": "nodemon --ignore package.json src/index.ts migrate --ignore-uncommitted-changes --test-tasks test", - "test": "pnpm test:unit", - "test:unit": "TS_NODE_PROJECT=tsconfig.test.json mocha --require ts-node/register test/**/*.ts" + "test": "pnpm test:unit", + "test:unit": "TS_NODE_PROJECT=tsconfig.test.json mocha --require ts-node/register test/**/*.ts" }, "type": "commonjs", "dependencies": { diff --git a/packages/tools/kolibri-cli/src/index.ts b/packages/tools/kolibri-cli/src/index.ts index d5b60e09d5e..9d04a76f559 100644 --- a/packages/tools/kolibri-cli/src/index.ts +++ b/packages/tools/kolibri-cli/src/index.ts @@ -31,4 +31,4 @@ generateScss(program); info(program); migrate(program); -program.parse(); +void program.parseAsync(); diff --git a/packages/tools/kolibri-cli/src/info/index.ts b/packages/tools/kolibri-cli/src/info/index.ts index f028ed31f4f..e8241db74ea 100644 --- a/packages/tools/kolibri-cli/src/info/index.ts +++ b/packages/tools/kolibri-cli/src/info/index.ts @@ -8,6 +8,19 @@ import { PackageJson } from '../types'; // Function to get the binary version const getBinaryVersion = (command: string): string => { try { + // For yarn, use a temporary directory to prevent package.json modification + // Yarn with Corepack automatically adds packageManager field when executed + if (command === 'yarn') { + const originalCwd = process.cwd(); + const tmpDir = os.tmpdir(); + process.chdir(tmpDir); + try { + return execSync(`${command} --version`, { encoding: 'utf8' }).trim(); + } finally { + process.chdir(originalCwd); + } + } + return execSync(`${command} --version`, { encoding: 'utf8' }).trim(); } catch { return 'N/A'; diff --git a/packages/tools/kolibri-cli/src/migrate/index.ts b/packages/tools/kolibri-cli/src/migrate/index.ts index 1cb8c4583bd..17948841221 100644 --- a/packages/tools/kolibri-cli/src/migrate/index.ts +++ b/packages/tools/kolibri-cli/src/migrate/index.ts @@ -23,6 +23,7 @@ import { MODIFIED_FILES, POST_MESSAGES, setRemoveMode, + hasKoliBriTags, } from './shares/reuse'; import { REMOVE_MODE, RemoveMode } from './types'; @@ -91,6 +92,13 @@ Target version of @public-ui/*: ${options.overwriteTargetVersion} Source folder to migrate: ${baseDir} `); + if (!fs.existsSync(baseDir)) { + throw logAndCreateError(`The specified source folder "${baseDir}" does not exist or is inaccessible. Please check the path and try again.`); + } + if (!hasKoliBriTags(baseDir)) { + console.log(chalk.yellow(`No KoliBri components (web or React) found under "${baseDir}". Check the path or your task configuration.`)); + } + if (!options.ignoreGreaterVersion && semver.lt(options.overwriteTargetVersion, options.overwriteCurrentVersion)) { throw logAndCreateError( 'Your current version of @public-ui/components is greater than the version of @public-ui/kolibri-cli. Please update @public-ui/kolibri-cli or force the migration with --ignore-greater-version.', @@ -203,6 +211,10 @@ Modified files: ${MODIFIED_FILES.size}`); console.log(`- ${file}`); }); + if (MODIFIED_FILES.size === 0) { + console.log(chalk.yellow(`No files were modified. Verify the folder "${baseDir}" or check your .kolibri.config.json tasks.`)); + } + if (MODIFIED_FILES.size > 0 && options.format) { console.log(` We try to format the modified files with prettier...`); diff --git a/packages/tools/kolibri-cli/src/migrate/shares/reuse.ts b/packages/tools/kolibri-cli/src/migrate/shares/reuse.ts index db340ba9ce3..be62d6e0257 100644 --- a/packages/tools/kolibri-cli/src/migrate/shares/reuse.ts +++ b/packages/tools/kolibri-cli/src/migrate/shares/reuse.ts @@ -2,7 +2,7 @@ import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; -import { FileExtension, PackageJson } from '../../types'; +import { FileExtension, PackageJson, MARKUP_EXTENSIONS, WEB_TAG_REGEX, REACT_TAG_REGEX } from '../../types'; import { RemoveMode } from '../types'; /** @@ -61,6 +61,45 @@ export function filterFilesByExt(dir: string, ext: FileExtension | FileExtension return files; } +/** + * Checks if the specified directory contains any files with KoliBri tags. + * Files are streamed in chunks to avoid loading entire files into memory. + * @param {string} dir The directory to search in + * @returns {boolean} True if at least one file contains KoliBri component tags (web or React) + */ +export function hasKoliBriTags(dir: string): boolean { + const regexes = [WEB_TAG_REGEX, REACT_TAG_REGEX]; + const files = filterFilesByExt(dir, MARKUP_EXTENSIONS); + + for (const file of files) { + let fd: number | undefined; + try { + fd = fs.openSync(file, 'r'); + const buffer = Buffer.alloc(65536); + let bytesRead: number; + let content = ''; + while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) { + content += buffer.toString('utf8', 0, bytesRead); + if (regexes.some((regex) => regex.test(content))) { + fs.closeSync(fd); + return true; + } + if (content.length > 1024) { + content = content.slice(-1024); + } + } + } catch (err) { + console.error(`Error reading file ${file}, skipping file due to read error:`, err); + } finally { + if (fd !== undefined) { + fs.closeSync(fd); + } + } + } + + return false; +} + /** * This function is used to get the version of the package.json as string. * @param {string} offsetPath The offset path to the package.json diff --git a/packages/tools/kolibri-cli/src/types.ts b/packages/tools/kolibri-cli/src/types.ts index 135e54f1987..1dc01d13e85 100644 --- a/packages/tools/kolibri-cli/src/types.ts +++ b/packages/tools/kolibri-cli/src/types.ts @@ -5,6 +5,9 @@ export const COMPONENT_FILE_EXTENSIONS: FileExtension[] = ['jsx', 'tsx', 'vue']; export const CUSTOM_ELEMENT_FILE_EXTENSIONS: FileExtension[] = ['html', 'xhtml', 'jsx', 'tsx', 'vue']; export const MARKUP_EXTENSIONS: FileExtension[] = COMPONENT_FILE_EXTENSIONS.concat(CUSTOM_ELEMENT_FILE_EXTENSIONS); +export const WEB_TAG_REGEX = /\b { - it('runs generate-scss command', () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-')); - const cwd = process.cwd(); - process.chdir(tmpDir); + it('runs generate-scss command', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-')); + const cwd = process.cwd(); + process.chdir(tmpDir); - const typedBem = require('typed-bem/scss'); - const original = typedBem.generateBemScssFile; - const calls: string[] = []; - typedBem.generateBemScssFile = (_: unknown, name: string) => { calls.push(name); }; + const typedBem = require('typed-bem/scss'); + const original = typedBem.generateBemScssFile; + const calls: string[] = []; + typedBem.generateBemScssFile = (_: unknown, name: string) => { + calls.push(name); + }; - const program = new Command(); - generateScss(program); - program.parse(['node', 'cli', 'generate-scss']); + const program = new Command(); + generateScss(program); + await program.parseAsync(['node', 'cli', 'generate-scss']); - typedBem.generateBemScssFile = original; - process.chdir(cwd); + typedBem.generateBemScssFile = original; + process.chdir(cwd); - assert.deepStrictEqual(calls, ['alert', 'icon']); - }); + assert.deepStrictEqual(calls, ['alert', 'icon']); + }); - it('runs info command', () => { - const program = new Command(); - info(program); - let output = ''; - const original = console.log; - console.log = (str: string) => { output += str; }; - program.parse(['node', 'cli', 'info']); - console.log = original; - assert.ok(output.includes('Operating System')); - }); + it('runs info command', async () => { + const program = new Command(); + info(program); + let output = ''; + const original = console.log; + console.log = (str: string) => { + output += str; + }; + await program.parseAsync(['node', 'cli', 'info']); + console.log = original; + assert.ok(output.includes('Operating System')); + }); - it('runs migrate command with options', () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-')); - fs.writeFileSync( - path.join(tmpDir, 'package.json'), - JSON.stringify({ dependencies: { '@public-ui/components': '0.0.0' }, devDependencies: { '@public-ui/kolibri-cli': '0.0.0' } }), - ); - fs.writeFileSync(path.join(tmpDir, 'pnpm-lock.yaml'), ''); - const cwd = process.cwd(); - process.chdir(tmpDir); + it('runs migrate command with options', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-')); + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ dependencies: { '@public-ui/components': '0.0.0' }, devDependencies: { '@public-ui/kolibri-cli': '0.0.0' } }), + ); + fs.writeFileSync(path.join(tmpDir, 'pnpm-lock.yaml'), ''); + const cwd = process.cwd(); - - const childProc = require('child_process'); - const execOrig = childProc.exec; - (childProc as any).exec = (_: string, cb: (err: null, out: string) => void) => cb(null, ''); + try { + process.chdir(tmpDir); - let runCalled = false; - const runOrig = TaskRunner.prototype.run; - TaskRunner.prototype.run = function () { - runCalled = true; - }; - const getStatusOrig = TaskRunner.prototype.getStatus; - TaskRunner.prototype.getStatus = () => ({ total: 0, done: 0, pending: 0, nextVersion: '0.0.0', config: { migrate: { tasks: {} } } }); - const getPendingOrig = TaskRunner.prototype.getPendingMinVersion; - TaskRunner.prototype.getPendingMinVersion = () => '0.0.0'; + const childProc = require('child_process'); + const execOrig = childProc.exec; + (childProc as any).exec = (_: string, cb: (err: null, out: string) => void) => cb(null, ''); - const program = new Command(); - migrate(program); - program.parse([ - 'node', - 'cli', - 'migrate', - '.', - '--ignore-uncommitted-changes', - '--overwrite-current-version', - '0.0.0', - '--overwrite-target-version', - '0.0.0', - '--remove-mode', - 'delete', - '--test-tasks', - ]); + let runCalled = false; + const runOrig = TaskRunner.prototype.run; + TaskRunner.prototype.run = function () { + runCalled = true; + }; + const getStatusOrig = TaskRunner.prototype.getStatus; + TaskRunner.prototype.getStatus = () => ({ total: 0, done: 0, pending: 0, nextVersion: '0.0.0', config: { migrate: { tasks: {} } } }); + const getPendingOrig = TaskRunner.prototype.getPendingMinVersion; + TaskRunner.prototype.getPendingMinVersion = () => '0.0.0'; - (childProc as any).exec = execOrig; - TaskRunner.prototype.run = runOrig; - TaskRunner.prototype.getStatus = getStatusOrig; - TaskRunner.prototype.getPendingMinVersion = getPendingOrig; - process.chdir(cwd); + const program = new Command(); + migrate(program); + await program.parseAsync([ + 'node', + 'cli', + 'migrate', + '.', + '--ignore-uncommitted-changes', + '--overwrite-current-version', + '0.0.0', + '--overwrite-target-version', + '0.0.0', + '--remove-mode', + 'delete', + '--test-tasks', + ]); - assert.ok(runCalled); - assert.equal(getRemoveMode(), 'delete'); - setRemoveMode('prefix'); - }); + (childProc as any).exec = execOrig; + TaskRunner.prototype.run = runOrig; + TaskRunner.prototype.getStatus = getStatusOrig; + TaskRunner.prototype.getPendingMinVersion = getPendingOrig; + + assert.ok(runCalled); + assert.equal(getRemoveMode(), 'delete'); + setRemoveMode('prefix'); + } finally { + // Always restore working directory + process.chdir(cwd); + } + }); }); diff --git a/packages/tools/visual-tests/src/index.js b/packages/tools/visual-tests/src/index.js index 8aff691f6fe..0854a198b18 100755 --- a/packages/tools/visual-tests/src/index.js +++ b/packages/tools/visual-tests/src/index.js @@ -6,8 +6,9 @@ import { readFile } from 'fs/promises'; import * as fs from 'fs'; import portfinder from 'portfinder'; import * as process from 'process'; +import os from 'node:os'; -const tempDir = process.env.RUNNER_TEMP || process.env.TMPDIR; // TODO: Check on Windows +const tempDir = process.env.RUNNER_TEMP || process.env.TMPDIR || os.tmpdir(); // TODO: Check on Windows if (!process.env.THEME_MODULE) { throw new Error('Environment variable THEME_MODULE not specified.');