Skip to content

Commit 7616bed

Browse files
authored
Add helpful warnings to migration tool (#8037)
2 parents 3a45545 + d4b7dd6 commit 7616bed

File tree

8 files changed

+153
-77
lines changed

8 files changed

+153
-77
lines changed

packages/tools/kolibri-cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"restart": "pnpm reset && pnpm start",
3131
"unused": "knip",
3232
"watch": "nodemon --ignore package.json src/index.ts migrate --ignore-uncommitted-changes --test-tasks test",
33-
"test": "pnpm test:unit",
34-
"test:unit": "TS_NODE_PROJECT=tsconfig.test.json mocha --require ts-node/register test/**/*.ts"
33+
"test": "pnpm test:unit",
34+
"test:unit": "TS_NODE_PROJECT=tsconfig.test.json mocha --require ts-node/register test/**/*.ts"
3535
},
3636
"type": "commonjs",
3737
"dependencies": {

packages/tools/kolibri-cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ generateScss(program);
3131
info(program);
3232
migrate(program);
3333

34-
program.parse();
34+
void program.parseAsync();

packages/tools/kolibri-cli/src/info/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ import { PackageJson } from '../types';
88
// Function to get the binary version
99
const getBinaryVersion = (command: string): string => {
1010
try {
11+
// For yarn, use a temporary directory to prevent package.json modification
12+
// Yarn with Corepack automatically adds packageManager field when executed
13+
if (command === 'yarn') {
14+
const originalCwd = process.cwd();
15+
const tmpDir = os.tmpdir();
16+
process.chdir(tmpDir);
17+
try {
18+
return execSync(`${command} --version`, { encoding: 'utf8' }).trim();
19+
} finally {
20+
process.chdir(originalCwd);
21+
}
22+
}
23+
1124
return execSync(`${command} --version`, { encoding: 'utf8' }).trim();
1225
} catch {
1326
return 'N/A';

packages/tools/kolibri-cli/src/migrate/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
MODIFIED_FILES,
2424
POST_MESSAGES,
2525
setRemoveMode,
26+
hasKoliBriTags,
2627
} from './shares/reuse';
2728
import { REMOVE_MODE, RemoveMode } from './types';
2829

@@ -91,6 +92,13 @@ Target version of @public-ui/*: ${options.overwriteTargetVersion}
9192
Source folder to migrate: ${baseDir}
9293
`);
9394

95+
if (!fs.existsSync(baseDir)) {
96+
throw logAndCreateError(`The specified source folder "${baseDir}" does not exist or is inaccessible. Please check the path and try again.`);
97+
}
98+
if (!hasKoliBriTags(baseDir)) {
99+
console.log(chalk.yellow(`No KoliBri components (web or React) found under "${baseDir}". Check the path or your task configuration.`));
100+
}
101+
94102
if (!options.ignoreGreaterVersion && semver.lt(options.overwriteTargetVersion, options.overwriteCurrentVersion)) {
95103
throw logAndCreateError(
96104
'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}`);
203211
console.log(`- ${file}`);
204212
});
205213

214+
if (MODIFIED_FILES.size === 0) {
215+
console.log(chalk.yellow(`No files were modified. Verify the folder "${baseDir}" or check your .kolibri.config.json tasks.`));
216+
}
217+
206218
if (MODIFIED_FILES.size > 0 && options.format) {
207219
console.log(`
208220
We try to format the modified files with prettier...`);

packages/tools/kolibri-cli/src/migrate/shares/reuse.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import chalk from 'chalk';
22
import fs from 'fs';
33
import path from 'path';
44

5-
import { FileExtension, PackageJson } from '../../types';
5+
import { FileExtension, PackageJson, MARKUP_EXTENSIONS, WEB_TAG_REGEX, REACT_TAG_REGEX } from '../../types';
66
import { RemoveMode } from '../types';
77

88
/**
@@ -61,6 +61,45 @@ export function filterFilesByExt(dir: string, ext: FileExtension | FileExtension
6161
return files;
6262
}
6363

64+
/**
65+
* Checks if the specified directory contains any files with KoliBri tags.
66+
* Files are streamed in chunks to avoid loading entire files into memory.
67+
* @param {string} dir The directory to search in
68+
* @returns {boolean} True if at least one file contains KoliBri component tags (web or React)
69+
*/
70+
export function hasKoliBriTags(dir: string): boolean {
71+
const regexes = [WEB_TAG_REGEX, REACT_TAG_REGEX];
72+
const files = filterFilesByExt(dir, MARKUP_EXTENSIONS);
73+
74+
for (const file of files) {
75+
let fd: number | undefined;
76+
try {
77+
fd = fs.openSync(file, 'r');
78+
const buffer = Buffer.alloc(65536);
79+
let bytesRead: number;
80+
let content = '';
81+
while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) {
82+
content += buffer.toString('utf8', 0, bytesRead);
83+
if (regexes.some((regex) => regex.test(content))) {
84+
fs.closeSync(fd);
85+
return true;
86+
}
87+
if (content.length > 1024) {
88+
content = content.slice(-1024);
89+
}
90+
}
91+
} catch (err) {
92+
console.error(`Error reading file ${file}, skipping file due to read error:`, err);
93+
} finally {
94+
if (fd !== undefined) {
95+
fs.closeSync(fd);
96+
}
97+
}
98+
}
99+
100+
return false;
101+
}
102+
64103
/**
65104
* This function is used to get the version of the package.json as string.
66105
* @param {string} offsetPath The offset path to the package.json

packages/tools/kolibri-cli/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export const COMPONENT_FILE_EXTENSIONS: FileExtension[] = ['jsx', 'tsx', 'vue'];
55
export const CUSTOM_ELEMENT_FILE_EXTENSIONS: FileExtension[] = ['html', 'xhtml', 'jsx', 'tsx', 'vue'];
66
export const MARKUP_EXTENSIONS: FileExtension[] = COMPONENT_FILE_EXTENSIONS.concat(CUSTOM_ELEMENT_FILE_EXTENSIONS);
77

8+
export const WEB_TAG_REGEX = /\b<kol-[a-z-]+/i;
9+
export const REACT_TAG_REGEX = /\b<Kol[A-Z][A-Za-z]*/;
10+
811
export type PackageJson = {
912
name: string;
1013
version: string;

packages/tools/kolibri-cli/test/cli-interface.spec.ts

Lines changed: 80 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -10,87 +10,95 @@ import { getRemoveMode, setRemoveMode } from '../src/migrate/shares/reuse';
1010
import { TaskRunner } from '../src/migrate/runner/task-runner';
1111

1212
describe('CLI interface', () => {
13-
it('runs generate-scss command', () => {
14-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-'));
15-
const cwd = process.cwd();
16-
process.chdir(tmpDir);
13+
it('runs generate-scss command', async () => {
14+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-'));
15+
const cwd = process.cwd();
16+
process.chdir(tmpDir);
1717

18-
const typedBem = require('typed-bem/scss');
19-
const original = typedBem.generateBemScssFile;
20-
const calls: string[] = [];
21-
typedBem.generateBemScssFile = (_: unknown, name: string) => { calls.push(name); };
18+
const typedBem = require('typed-bem/scss');
19+
const original = typedBem.generateBemScssFile;
20+
const calls: string[] = [];
21+
typedBem.generateBemScssFile = (_: unknown, name: string) => {
22+
calls.push(name);
23+
};
2224

23-
const program = new Command();
24-
generateScss(program);
25-
program.parse(['node', 'cli', 'generate-scss']);
25+
const program = new Command();
26+
generateScss(program);
27+
await program.parseAsync(['node', 'cli', 'generate-scss']);
2628

27-
typedBem.generateBemScssFile = original;
28-
process.chdir(cwd);
29+
typedBem.generateBemScssFile = original;
30+
process.chdir(cwd);
2931

30-
assert.deepStrictEqual(calls, ['alert', 'icon']);
31-
});
32+
assert.deepStrictEqual(calls, ['alert', 'icon']);
33+
});
3234

33-
it('runs info command', () => {
34-
const program = new Command();
35-
info(program);
36-
let output = '';
37-
const original = console.log;
38-
console.log = (str: string) => { output += str; };
39-
program.parse(['node', 'cli', 'info']);
40-
console.log = original;
41-
assert.ok(output.includes('Operating System'));
42-
});
35+
it('runs info command', async () => {
36+
const program = new Command();
37+
info(program);
38+
let output = '';
39+
const original = console.log;
40+
console.log = (str: string) => {
41+
output += str;
42+
};
43+
await program.parseAsync(['node', 'cli', 'info']);
44+
console.log = original;
45+
assert.ok(output.includes('Operating System'));
46+
});
4347

44-
it('runs migrate command with options', () => {
45-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-'));
46-
fs.writeFileSync(
47-
path.join(tmpDir, 'package.json'),
48-
JSON.stringify({ dependencies: { '@public-ui/components': '0.0.0' }, devDependencies: { '@public-ui/kolibri-cli': '0.0.0' } }),
49-
);
50-
fs.writeFileSync(path.join(tmpDir, 'pnpm-lock.yaml'), '');
51-
const cwd = process.cwd();
52-
process.chdir(tmpDir);
48+
it('runs migrate command with options', async () => {
49+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kolibri-cli-'));
50+
fs.writeFileSync(
51+
path.join(tmpDir, 'package.json'),
52+
JSON.stringify({ dependencies: { '@public-ui/components': '0.0.0' }, devDependencies: { '@public-ui/kolibri-cli': '0.0.0' } }),
53+
);
54+
fs.writeFileSync(path.join(tmpDir, 'pnpm-lock.yaml'), '');
55+
const cwd = process.cwd();
5356

54-
55-
const childProc = require('child_process');
56-
const execOrig = childProc.exec;
57-
(childProc as any).exec = (_: string, cb: (err: null, out: string) => void) => cb(null, '');
57+
try {
58+
process.chdir(tmpDir);
5859

59-
let runCalled = false;
60-
const runOrig = TaskRunner.prototype.run;
61-
TaskRunner.prototype.run = function () {
62-
runCalled = true;
63-
};
64-
const getStatusOrig = TaskRunner.prototype.getStatus;
65-
TaskRunner.prototype.getStatus = () => ({ total: 0, done: 0, pending: 0, nextVersion: '0.0.0', config: { migrate: { tasks: {} } } });
66-
const getPendingOrig = TaskRunner.prototype.getPendingMinVersion;
67-
TaskRunner.prototype.getPendingMinVersion = () => '0.0.0';
60+
const childProc = require('child_process');
61+
const execOrig = childProc.exec;
62+
(childProc as any).exec = (_: string, cb: (err: null, out: string) => void) => cb(null, '');
6863

69-
const program = new Command();
70-
migrate(program);
71-
program.parse([
72-
'node',
73-
'cli',
74-
'migrate',
75-
'.',
76-
'--ignore-uncommitted-changes',
77-
'--overwrite-current-version',
78-
'0.0.0',
79-
'--overwrite-target-version',
80-
'0.0.0',
81-
'--remove-mode',
82-
'delete',
83-
'--test-tasks',
84-
]);
64+
let runCalled = false;
65+
const runOrig = TaskRunner.prototype.run;
66+
TaskRunner.prototype.run = function () {
67+
runCalled = true;
68+
};
69+
const getStatusOrig = TaskRunner.prototype.getStatus;
70+
TaskRunner.prototype.getStatus = () => ({ total: 0, done: 0, pending: 0, nextVersion: '0.0.0', config: { migrate: { tasks: {} } } });
71+
const getPendingOrig = TaskRunner.prototype.getPendingMinVersion;
72+
TaskRunner.prototype.getPendingMinVersion = () => '0.0.0';
8573

86-
(childProc as any).exec = execOrig;
87-
TaskRunner.prototype.run = runOrig;
88-
TaskRunner.prototype.getStatus = getStatusOrig;
89-
TaskRunner.prototype.getPendingMinVersion = getPendingOrig;
90-
process.chdir(cwd);
74+
const program = new Command();
75+
migrate(program);
76+
await program.parseAsync([
77+
'node',
78+
'cli',
79+
'migrate',
80+
'.',
81+
'--ignore-uncommitted-changes',
82+
'--overwrite-current-version',
83+
'0.0.0',
84+
'--overwrite-target-version',
85+
'0.0.0',
86+
'--remove-mode',
87+
'delete',
88+
'--test-tasks',
89+
]);
9190

92-
assert.ok(runCalled);
93-
assert.equal(getRemoveMode(), 'delete');
94-
setRemoveMode('prefix');
95-
});
91+
(childProc as any).exec = execOrig;
92+
TaskRunner.prototype.run = runOrig;
93+
TaskRunner.prototype.getStatus = getStatusOrig;
94+
TaskRunner.prototype.getPendingMinVersion = getPendingOrig;
95+
96+
assert.ok(runCalled);
97+
assert.equal(getRemoveMode(), 'delete');
98+
setRemoveMode('prefix');
99+
} finally {
100+
// Always restore working directory
101+
process.chdir(cwd);
102+
}
103+
});
96104
});

packages/tools/visual-tests/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { readFile } from 'fs/promises';
66
import * as fs from 'fs';
77
import portfinder from 'portfinder';
88
import * as process from 'process';
9+
import os from 'node:os';
910

10-
const tempDir = process.env.RUNNER_TEMP || process.env.TMPDIR; // TODO: Check on Windows
11+
const tempDir = process.env.RUNNER_TEMP || process.env.TMPDIR || os.tmpdir(); // TODO: Check on Windows
1112

1213
if (!process.env.THEME_MODULE) {
1314
throw new Error('Environment variable THEME_MODULE not specified.');

0 commit comments

Comments
 (0)