@public-ui/kolibri-cli 3.0.4-rc.1 → 3.0.4-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,6 +91,53 @@ Actually the following migration tasks from version 1 to version 2 are available
91
91
  - Detection of `_iconOnly` in TSX is not stable ([#5404](https://github.com/public-ui/kolibri/issues/5404)) ⏰
92
92
  - Handle the remove `@public-ui/core` package ([#???](https://github.com/public-ui/kolibri/issues/????)) ⏰
93
93
 
94
+ #### Theme Migration
95
+
96
+ The CLI also supports migrating KoliBri themes alongside component updates to ensure theme packages remain synchronized with breaking changes in the component library.
97
+
98
+ ##### Goal
99
+
100
+ Synchronies breaking changes in components with the styling of each theme package so that upgrades require minimal manual work.
101
+
102
+ ##### Requirements
103
+
104
+ - Components and themes follow the BEM naming convention via `typed-bem`.
105
+ - Each theme lives under `packages/themes` and keeps its SCSS sources in `src/`.
106
+ - The CLI migration tool can read and modify `.scss` files.
107
+
108
+ ##### Concept
109
+
110
+ 1. **Central BEM schemas**
111
+ - Every component exports its BEM schema. These schemas are used by the CLI to generate SCSS files and by themes to reference selectors.
112
+ - When a selector changes, the same schema information allows the migration to update all theme packages consistently.
113
+
114
+ 2. **SCSS migration tasks**
115
+ The CLI runner is extended with task types operating on SCSS. They behave like the existing property tasks and are idempotent.
116
+ - `RenameBlockTask` – rename a block selector everywhere in a theme.
117
+ - `RenameElementTask` – rename an element within a block.
118
+ - `RenameModifierTask` – rename or replace a modifier.
119
+ - `AddSelectorTask` and `RemoveSelectorTask` – insert or remove entire rule sets.
120
+ - `UpdateTokenTask` – adjust variable names or values when tokens change.
121
+ - `MoveRulesTask` – move declarations from one selector to another if the DOM structure changes.
122
+
123
+ Tasks scan the SCSS with regular expressions or an AST parser. If a pattern is missing the task logs a warning instead of failing.
124
+
125
+ 3. **Safe execution**
126
+ - Migrations abort when uncommitted changes are detected unless `--ignore-uncommitted-changes` is specified.
127
+ - Each task logs the files it touched. After completion Prettier can format them automatically.
128
+ - Because tasks are idempotent, rerunning the migration produces no new changes. To undo a run, reset the Git state.
129
+
130
+ ##### Theme Migration Workflow Example
131
+
132
+ ```bash
133
+ pnpm i -g @public-ui/kolibri-cli@2 # install the CLI matching the next version
134
+ kolibri migrate path/to/project # run all tasks for the upgrade
135
+ pnpm format # format changed files
136
+ git diff # review results and commit
137
+ ```
138
+
139
+ Using these tasks, theme packages remain aligned with component updates. Future breaking changes can be scripted and applied via the CLI so that projects can upgrade in a controlled and reproducible way.
140
+
94
141
  #### How does it work?
95
142
 
96
143
  1. The migration command will check your project for clear `git history` and the `installed version` of `KoliBri`. Now it loads all available migration tasks.
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScssAddSelectorTask = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const reuse_1 = require("../../../shares/reuse");
10
+ const abstract_task_1 = require("../../abstract-task");
11
+ /**
12
+ * Simple, fast hash function for string identifier generation.
13
+ * Based on djb2 algorithm - much faster than cryptographic hashes for non-security purposes.
14
+ * @param {string} str String to hash
15
+ * @returns {string} Hexadecimal hash string
16
+ */
17
+ function simpleHash(str) {
18
+ let hash = 5381;
19
+ for (let i = 0; i < str.length; i++) {
20
+ hash = (hash << 5) + hash + str.charCodeAt(i);
21
+ }
22
+ return (hash >>> 0).toString(16).substring(0, 8);
23
+ }
24
+ /**
25
+ * Escapes special characters for use in a regular expression.
26
+ * @param {string} str String to escape
27
+ * @returns {string} Escaped string
28
+ */
29
+ function escapeRegExp(str) {
30
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ }
32
+ /**
33
+ * Analyzes the content to determine formatting preferences
34
+ * @param {string} content The CSS content to analyze
35
+ * @returns {object} Formatting preferences object
36
+ */
37
+ function analyzeFormatting(content) {
38
+ const lines = content.split('\n');
39
+ // Detect indentation
40
+ let tabCount = 0;
41
+ let spaceCount = 0;
42
+ const indentSizes = [];
43
+ for (const line of lines) {
44
+ if (line.trim() === '')
45
+ continue;
46
+ const leadingWhitespace = line.match(/^(\s*)/)?.[1] || '';
47
+ if (leadingWhitespace.includes('\t')) {
48
+ tabCount++;
49
+ }
50
+ else if (leadingWhitespace.length > 0) {
51
+ spaceCount++;
52
+ indentSizes.push(leadingWhitespace.length);
53
+ }
54
+ }
55
+ const usesTabs = tabCount > spaceCount;
56
+ const averageSpaceIndent = indentSizes.length > 0 ? Math.round(indentSizes.reduce((sum, size) => sum + size, 0) / indentSizes.length) : 2;
57
+ // Detect brace formatting patterns
58
+ let newlineBeforeOpenBrace = false;
59
+ let newlineAfterOpenBrace = true; // Default to true for readability
60
+ let newlineBeforeCloseBrace = true; // Default to true for readability
61
+ let newlineAfterCloseBrace = true; // Default to true for separation
62
+ // Look for existing CSS rules to determine formatting style
63
+ const cssRulePattern = /[^{]*\{[^}]*\}/g;
64
+ const matches = content.match(cssRulePattern);
65
+ if (matches && matches.length > 0) {
66
+ let beforeOpenCount = 0;
67
+ let afterOpenCount = 0;
68
+ let beforeCloseCount = 0;
69
+ let afterCloseCount = 0;
70
+ for (const match of matches) {
71
+ // Check for newline before opening brace
72
+ if (/\n\s*\{/.test(match))
73
+ beforeOpenCount++;
74
+ // Check for newline after opening brace
75
+ if (/\{\s*\n/.test(match))
76
+ afterOpenCount++;
77
+ // Check for newline before closing brace
78
+ if (/\n\s*\}/.test(match))
79
+ beforeCloseCount++;
80
+ // Check for newline after closing brace (look at the context)
81
+ if (/\}\s*\n/.test(match))
82
+ afterCloseCount++;
83
+ }
84
+ // Use majority rule for formatting decisions
85
+ const totalMatches = matches.length;
86
+ newlineBeforeOpenBrace = beforeOpenCount > totalMatches / 2;
87
+ newlineAfterOpenBrace = afterOpenCount > totalMatches / 2;
88
+ newlineBeforeCloseBrace = beforeCloseCount > totalMatches / 2;
89
+ newlineAfterCloseBrace = afterCloseCount > totalMatches / 2;
90
+ }
91
+ return {
92
+ indentChar: usesTabs ? '\t' : ' ',
93
+ indentSize: usesTabs ? 1 : averageSpaceIndent,
94
+ newlineBeforeOpenBrace,
95
+ newlineAfterOpenBrace,
96
+ newlineBeforeCloseBrace,
97
+ newlineAfterCloseBrace,
98
+ };
99
+ }
100
+ /**
101
+ * Formats a CSS rule according to the detected formatting style
102
+ * @param {string} selector The CSS selector
103
+ * @param {string} rules The CSS rules
104
+ * @param {object} formatting The formatting preferences
105
+ * @param {string} formatting.indentChar The character used for indentation (tab or space)
106
+ * @param {number} formatting.indentSize The number of indent characters per level
107
+ * @param {boolean} formatting.newlineBeforeOpenBrace Whether to add newline before opening brace
108
+ * @param {boolean} formatting.newlineAfterOpenBrace Whether to add newline after opening brace
109
+ * @param {boolean} formatting.newlineBeforeCloseBrace Whether to add newline before closing brace
110
+ * @param {boolean} formatting.newlineAfterCloseBrace Whether to add newline after closing brace
111
+ * @returns {string} The formatted CSS rule
112
+ */
113
+ function formatCssRule(selector, rules, formatting) {
114
+ const indent = formatting.indentChar.repeat(formatting.indentSize);
115
+ // Ensure rules are properly indented and trimmed
116
+ const formattedRules = rules
117
+ .split('\n')
118
+ .map((line) => line.trim())
119
+ .filter((line) => line.length > 0)
120
+ .map((line) => indent + line)
121
+ .join('\n');
122
+ let result = '';
123
+ // Add selector
124
+ result += selector;
125
+ // Add space or newline before opening brace
126
+ if (formatting.newlineBeforeOpenBrace) {
127
+ result += '\n';
128
+ }
129
+ else {
130
+ result += ' ';
131
+ }
132
+ // Add opening brace
133
+ result += '{';
134
+ // Add newline after opening brace if needed
135
+ if (formatting.newlineAfterOpenBrace) {
136
+ result += '\n';
137
+ }
138
+ // Add rules
139
+ if (formattedRules.trim()) {
140
+ if (!formatting.newlineAfterOpenBrace) {
141
+ result += ' ';
142
+ }
143
+ result += formattedRules;
144
+ if (!formatting.newlineBeforeCloseBrace) {
145
+ result += ' ';
146
+ }
147
+ }
148
+ // Add newline before closing brace if needed
149
+ if (formatting.newlineBeforeCloseBrace && formattedRules.trim()) {
150
+ result += '\n';
151
+ }
152
+ // Add closing brace
153
+ result += '}';
154
+ // Add newline after closing brace if needed
155
+ if (formatting.newlineAfterCloseBrace) {
156
+ result += '\n';
157
+ }
158
+ return result;
159
+ }
160
+ class ScssAddSelectorTask extends abstract_task_1.AbstractTask {
161
+ selector;
162
+ rules;
163
+ regExp;
164
+ constructor(identifier, selector, rules, versionRange, dependentTasks = [], options = {}) {
165
+ super(identifier, `Add selector "${selector}"`, types_1.SCSS_FILE_EXTENSIONS, versionRange, dependentTasks, options);
166
+ this.selector = selector;
167
+ this.rules = rules;
168
+ if (!selector.startsWith('.')) {
169
+ throw (0, reuse_1.logAndCreateError)(`Selector "${selector}" must start with a dot.`);
170
+ }
171
+ this.regExp = new RegExp(escapeRegExp(selector) + '\\s*{');
172
+ }
173
+ static getInstance(selector, rules, versionRange, dependentTasks = [], options = {}) {
174
+ // Include rules in identifier to ensure unique instances for different rule sets
175
+ // Use simple hash to create shorter, more predictable identifiers
176
+ const rulesHash = simpleHash(rules);
177
+ const identifier = `add-selector-${selector}-${rulesHash}`;
178
+ if (!this.instances.has(identifier)) {
179
+ this.instances.set(identifier, new ScssAddSelectorTask(identifier, selector, rules, versionRange, dependentTasks, options));
180
+ }
181
+ return this.instances.get(identifier);
182
+ }
183
+ run(baseDir) {
184
+ (0, reuse_1.filterFilesByExt)(baseDir, types_1.SCSS_FILE_EXTENSIONS).forEach((file) => {
185
+ let content = fs_1.default.readFileSync(file, 'utf8');
186
+ if (!this.regExp.test(content)) {
187
+ const formatting = analyzeFormatting(content);
188
+ const newRule = formatCssRule(this.selector, this.rules, formatting);
189
+ // Add appropriate spacing before the new rule
190
+ if (content.trim() && !content.endsWith('\n')) {
191
+ content += '\n';
192
+ }
193
+ if (content.trim()) {
194
+ content += '\n';
195
+ }
196
+ content += newRule;
197
+ reuse_1.MODIFIED_FILES.add(file);
198
+ fs_1.default.writeFileSync(file, content);
199
+ }
200
+ });
201
+ }
202
+ }
203
+ exports.ScssAddSelectorTask = ScssAddSelectorTask;
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScssRemoveSelectorTask = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const reuse_1 = require("../../../shares/reuse");
10
+ const abstract_task_1 = require("../../abstract-task");
11
+ /**
12
+ * Finds and removes a CSS selector and its complete rule block, handling nested braces correctly.
13
+ * Also handles comma-separated selector lists.
14
+ * @param {string} content The CSS content to process
15
+ * @param {string} selector The selector to remove (must start with a dot)
16
+ * @returns {string} The content with the selector removed
17
+ */
18
+ function removeSelectorWithNestedBraces(content, selector) {
19
+ let result = content;
20
+ let offset = 0;
21
+ let currentIndex = 0;
22
+ while (currentIndex < content.length) {
23
+ // Find the next opening brace while properly handling strings and comments
24
+ const ruleStart = findNextRuleBlock(content, currentIndex);
25
+ if (ruleStart === -1)
26
+ break;
27
+ const openBraceIndex = ruleStart.openBraceIndex;
28
+ const selectorGroup = content.substring(ruleStart.selectorStart, openBraceIndex).trim();
29
+ // Check if this selector group contains our target selector
30
+ const selectors = selectorGroup.split(',').map((s) => s.trim());
31
+ let targetSelectorIndex = -1;
32
+ // First try exact match (for comma-separated lists)
33
+ targetSelectorIndex = selectors.findIndex((s) => s === selector);
34
+ // If no exact match, check if any selector contains our target as a class
35
+ if (targetSelectorIndex === -1) {
36
+ targetSelectorIndex = selectors.findIndex((s) => {
37
+ // Split by spaces to get individual parts of compound selectors
38
+ const parts = s.split(/\s+/);
39
+ return parts.includes(selector);
40
+ });
41
+ }
42
+ if (targetSelectorIndex === -1) {
43
+ // Target selector not found in this group, advance past this rule
44
+ currentIndex = findMatchingCloseBrace(content, openBraceIndex);
45
+ if (currentIndex === -1)
46
+ break;
47
+ currentIndex++;
48
+ continue;
49
+ }
50
+ // Find the matching closing brace using the existing brace counting logic
51
+ const closeBraceIndex = findMatchingCloseBrace(content, openBraceIndex);
52
+ if (closeBraceIndex === -1) {
53
+ // Malformed CSS, skip this rule
54
+ break;
55
+ }
56
+ let replacement;
57
+ if (selectors.length === 1) {
58
+ // Only one selector in the list, remove the entire rule block
59
+ replacement = `/* removed ${selector} */`;
60
+ }
61
+ else {
62
+ // Multiple selectors, remove only the target selector
63
+ const remainingSelectors = selectors.filter((_, index) => index !== targetSelectorIndex);
64
+ const ruleContent = content.substring(openBraceIndex, closeBraceIndex + 1);
65
+ replacement = `${remainingSelectors.join(', ')} ${ruleContent}`;
66
+ }
67
+ // Adjust for previous replacements
68
+ const adjustedStart = ruleStart.selectorStart - offset;
69
+ const adjustedEnd = closeBraceIndex + 1 - offset;
70
+ result = result.substring(0, adjustedStart) + replacement + result.substring(adjustedEnd);
71
+ const originalLength = closeBraceIndex + 1 - ruleStart.selectorStart;
72
+ offset += originalLength - replacement.length;
73
+ // Continue searching after this rule
74
+ currentIndex = closeBraceIndex + 1;
75
+ }
76
+ return result;
77
+ }
78
+ /**
79
+ * Finds the next CSS rule block while properly handling strings and comments.
80
+ * @param {string} content The CSS content to search
81
+ * @param {number} startIndex The index to start searching from
82
+ * @returns {object|number} Object with selectorStart and openBraceIndex, or -1 if no rule found
83
+ */
84
+ function findNextRuleBlock(content, startIndex) {
85
+ let currentIndex = startIndex;
86
+ let inString = false;
87
+ let stringChar = '';
88
+ let inComment = false;
89
+ let inSingleLineComment = false;
90
+ let potentialSelectorStart = -1;
91
+ while (currentIndex < content.length) {
92
+ const char = content[currentIndex];
93
+ const nextChar = content[currentIndex + 1];
94
+ // Handle single-line comments
95
+ if (!inString && !inComment && char === '/' && nextChar === '/') {
96
+ inSingleLineComment = true;
97
+ currentIndex += 2;
98
+ continue;
99
+ }
100
+ if (inSingleLineComment) {
101
+ if (char === '\n' || char === '\r') {
102
+ inSingleLineComment = false;
103
+ }
104
+ currentIndex++;
105
+ continue;
106
+ }
107
+ // Handle multi-line comments
108
+ if (!inString && !inSingleLineComment && char === '/' && nextChar === '*') {
109
+ inComment = true;
110
+ currentIndex += 2;
111
+ continue;
112
+ }
113
+ if (inComment) {
114
+ if (char === '*' && nextChar === '/') {
115
+ inComment = false;
116
+ currentIndex += 2;
117
+ continue;
118
+ }
119
+ currentIndex++;
120
+ continue;
121
+ }
122
+ // Handle strings
123
+ if (!inComment && !inSingleLineComment && (char === '"' || char === "'")) {
124
+ if (!inString) {
125
+ inString = true;
126
+ stringChar = char;
127
+ }
128
+ else if (char === stringChar && content[currentIndex - 1] !== '\\') {
129
+ inString = false;
130
+ stringChar = '';
131
+ }
132
+ }
133
+ // Look for rule blocks only when not in strings or comments
134
+ if (!inString && !inComment && !inSingleLineComment) {
135
+ if (char === '{') {
136
+ // Found opening brace, find the start of this selector
137
+ if (potentialSelectorStart === -1) {
138
+ // Find the start of the selector by looking backwards for the previous rule end or start of content
139
+ potentialSelectorStart = findSelectorStart(content, currentIndex);
140
+ }
141
+ return {
142
+ selectorStart: potentialSelectorStart,
143
+ openBraceIndex: currentIndex,
144
+ };
145
+ }
146
+ else if (char === '}') {
147
+ // End of a rule, reset potential selector start
148
+ potentialSelectorStart = -1;
149
+ }
150
+ else if (potentialSelectorStart === -1 && /\S/.test(char)) {
151
+ // First non-whitespace character, potential start of a selector
152
+ potentialSelectorStart = currentIndex;
153
+ }
154
+ }
155
+ currentIndex++;
156
+ }
157
+ return -1;
158
+ }
159
+ /**
160
+ * Finds the start of a selector by looking backwards from an opening brace.
161
+ * @param {string} content The CSS content
162
+ * @param {number} openBraceIndex The index of the opening brace
163
+ * @returns {number} The index where the selector starts
164
+ */
165
+ function findSelectorStart(content, openBraceIndex) {
166
+ let index = openBraceIndex - 1;
167
+ // Skip whitespace before the opening brace
168
+ while (index >= 0 && /\s/.test(content[index])) {
169
+ index--;
170
+ }
171
+ // Find the start of the selector (after previous '}' or at beginning)
172
+ while (index >= 0) {
173
+ if (content[index] === '}') {
174
+ return index + 1;
175
+ }
176
+ index--;
177
+ }
178
+ return 0; // Start of content
179
+ }
180
+ /**
181
+ * Finds the matching closing brace for an opening brace, handling nested braces correctly.
182
+ * @param {string} content The CSS content
183
+ * @param {number} openBraceIndex The index of the opening brace
184
+ * @returns {number} The index of the matching closing brace, or -1 if not found
185
+ */
186
+ function findMatchingCloseBrace(content, openBraceIndex) {
187
+ let braceCount = 1;
188
+ let currentIndex = openBraceIndex + 1;
189
+ let inString = false;
190
+ let stringChar = '';
191
+ let inComment = false;
192
+ let inSingleLineComment = false;
193
+ while (currentIndex < content.length && braceCount > 0) {
194
+ const char = content[currentIndex];
195
+ const nextChar = content[currentIndex + 1];
196
+ // Handle single-line comments
197
+ if (!inString && !inComment && char === '/' && nextChar === '/') {
198
+ inSingleLineComment = true;
199
+ currentIndex += 2;
200
+ continue;
201
+ }
202
+ if (inSingleLineComment) {
203
+ if (char === '\n' || char === '\r') {
204
+ inSingleLineComment = false;
205
+ }
206
+ currentIndex++;
207
+ continue;
208
+ }
209
+ // Handle multi-line comments
210
+ if (!inString && !inSingleLineComment && char === '/' && nextChar === '*') {
211
+ inComment = true;
212
+ currentIndex += 2;
213
+ continue;
214
+ }
215
+ if (inComment) {
216
+ if (char === '*' && nextChar === '/') {
217
+ inComment = false;
218
+ currentIndex += 2;
219
+ continue;
220
+ }
221
+ currentIndex++;
222
+ continue;
223
+ }
224
+ // Handle strings
225
+ if (!inComment && !inSingleLineComment && (char === '"' || char === "'")) {
226
+ if (!inString) {
227
+ inString = true;
228
+ stringChar = char;
229
+ }
230
+ else if (char === stringChar && content[currentIndex - 1] !== '\\') {
231
+ inString = false;
232
+ stringChar = '';
233
+ }
234
+ }
235
+ // Count braces only when not in strings or comments
236
+ if (!inString && !inComment && !inSingleLineComment) {
237
+ if (char === '{') {
238
+ braceCount++;
239
+ }
240
+ else if (char === '}') {
241
+ braceCount--;
242
+ }
243
+ }
244
+ currentIndex++;
245
+ }
246
+ return braceCount === 0 ? currentIndex - 1 : -1;
247
+ }
248
+ class ScssRemoveSelectorTask extends abstract_task_1.AbstractTask {
249
+ selector;
250
+ constructor(identifier, selector, versionRange, dependentTasks = [], options = {}) {
251
+ super(identifier, `Remove selector "${selector}"`, types_1.SCSS_FILE_EXTENSIONS, versionRange, dependentTasks, options);
252
+ this.selector = selector;
253
+ if (!selector.startsWith('.')) {
254
+ throw (0, reuse_1.logAndCreateError)(`Selector "${selector}" must start with a dot.`);
255
+ }
256
+ }
257
+ static getInstance(selector, versionRange, dependentTasks = [], options = {}) {
258
+ const identifier = `remove-selector-${selector}`;
259
+ if (!this.instances.has(identifier)) {
260
+ this.instances.set(identifier, new ScssRemoveSelectorTask(identifier, selector, versionRange, dependentTasks, options));
261
+ }
262
+ return this.instances.get(identifier);
263
+ }
264
+ run(baseDir) {
265
+ (0, reuse_1.filterFilesByExt)(baseDir, types_1.SCSS_FILE_EXTENSIONS).forEach((file) => {
266
+ const content = fs_1.default.readFileSync(file, 'utf8');
267
+ const newContent = removeSelectorWithNestedBraces(content, this.selector);
268
+ if (content !== newContent) {
269
+ reuse_1.MODIFIED_FILES.add(file);
270
+ fs_1.default.writeFileSync(file, newContent);
271
+ }
272
+ });
273
+ }
274
+ }
275
+ exports.ScssRemoveSelectorTask = ScssRemoveSelectorTask;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScssRenameBlockTask = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const reuse_1 = require("../../../shares/reuse");
10
+ const abstract_task_1 = require("../../abstract-task");
11
+ class ScssRenameBlockTask extends abstract_task_1.AbstractTask {
12
+ newBlock;
13
+ regExp;
14
+ constructor(identifier, block, newBlock, versionRange, dependentTasks = [], options = {}) {
15
+ super(identifier, `Rename block selector "${block}" to "${newBlock}"`, types_1.SCSS_FILE_EXTENSIONS, versionRange, dependentTasks, options);
16
+ this.newBlock = newBlock;
17
+ if (!reuse_1.isKebabCaseRegExp.test(block)) {
18
+ throw (0, reuse_1.logAndCreateError)(`Block "${block}" is not in kebab case.`);
19
+ }
20
+ if (!reuse_1.isKebabCaseRegExp.test(newBlock)) {
21
+ throw (0, reuse_1.logAndCreateError)(`Block "${newBlock}" is not in kebab case.`);
22
+ }
23
+ this.regExp = new RegExp(`\\.${block}(?=(?:__|--|\\b))`, 'g');
24
+ }
25
+ static getInstance(block, newBlock, versionRange, dependentTasks = [], options = {}) {
26
+ const identifier = `${block}-rename-block-${newBlock}`;
27
+ if (!this.instances.has(identifier)) {
28
+ this.instances.set(identifier, new ScssRenameBlockTask(identifier, block, newBlock, versionRange, dependentTasks, options));
29
+ }
30
+ return this.instances.get(identifier);
31
+ }
32
+ run(baseDir) {
33
+ (0, reuse_1.filterFilesByExt)(baseDir, types_1.SCSS_FILE_EXTENSIONS).forEach((file) => {
34
+ const content = fs_1.default.readFileSync(file, 'utf8');
35
+ const newContent = content.replace(this.regExp, `.${this.newBlock}`);
36
+ if (content !== newContent) {
37
+ reuse_1.MODIFIED_FILES.add(file);
38
+ fs_1.default.writeFileSync(file, newContent);
39
+ }
40
+ });
41
+ }
42
+ }
43
+ exports.ScssRenameBlockTask = ScssRenameBlockTask;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScssRenameElementTask = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const reuse_1 = require("../../../shares/reuse");
10
+ const abstract_task_1 = require("../../abstract-task");
11
+ class ScssRenameElementTask extends abstract_task_1.AbstractTask {
12
+ block;
13
+ newElement;
14
+ regExp;
15
+ constructor(identifier, block, element, newElement, versionRange, dependentTasks = [], options = {}) {
16
+ super(identifier, `Rename element selector "${block}__${element}" to "${block}__${newElement}"`, types_1.SCSS_FILE_EXTENSIONS, versionRange, dependentTasks, options);
17
+ this.block = block;
18
+ this.newElement = newElement;
19
+ if (!reuse_1.isKebabCaseRegExp.test(block)) {
20
+ throw (0, reuse_1.logAndCreateError)(`Block "${block}" is not in kebab case.`);
21
+ }
22
+ if (!reuse_1.isKebabCaseRegExp.test(element)) {
23
+ throw (0, reuse_1.logAndCreateError)(`Element "${element}" is not in kebab case.`);
24
+ }
25
+ if (!reuse_1.isKebabCaseRegExp.test(newElement)) {
26
+ throw (0, reuse_1.logAndCreateError)(`Element "${newElement}" is not in kebab case.`);
27
+ }
28
+ this.regExp = new RegExp(`\\.${block}__${element}(?=(?:--|\\b))`, 'g');
29
+ }
30
+ static getInstance(block, element, newElement, versionRange, dependentTasks = [], options = {}) {
31
+ const identifier = `${block}-rename-element-${element}-to-${newElement}`;
32
+ if (!this.instances.has(identifier)) {
33
+ this.instances.set(identifier, new ScssRenameElementTask(identifier, block, element, newElement, versionRange, dependentTasks, options));
34
+ }
35
+ return this.instances.get(identifier);
36
+ }
37
+ run(baseDir) {
38
+ (0, reuse_1.filterFilesByExt)(baseDir, types_1.SCSS_FILE_EXTENSIONS).forEach((file) => {
39
+ const content = fs_1.default.readFileSync(file, 'utf8');
40
+ const newContent = content.replace(this.regExp, `.${this.block}__${this.newElement}`);
41
+ if (content !== newContent) {
42
+ reuse_1.MODIFIED_FILES.add(file);
43
+ fs_1.default.writeFileSync(file, newContent);
44
+ }
45
+ });
46
+ }
47
+ }
48
+ exports.ScssRenameElementTask = ScssRenameElementTask;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScssRenameModifierTask = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const reuse_1 = require("../../../shares/reuse");
10
+ const abstract_task_1 = require("../../abstract-task");
11
+ class ScssRenameModifierTask extends abstract_task_1.AbstractTask {
12
+ base;
13
+ newModifier;
14
+ regExp;
15
+ constructor(identifier, base, modifier, newModifier, versionRange, dependentTasks = [], options = {}) {
16
+ super(identifier, `Rename modifier "${modifier}" of "${base}" selector`, types_1.SCSS_FILE_EXTENSIONS, versionRange, dependentTasks, options);
17
+ this.base = base;
18
+ this.newModifier = newModifier;
19
+ if (!reuse_1.isKebabCaseRegExp.test(base)) {
20
+ throw (0, reuse_1.logAndCreateError)(`Base selector "${base}" is not in kebab case.`);
21
+ }
22
+ if (!reuse_1.isKebabCaseRegExp.test(modifier)) {
23
+ throw (0, reuse_1.logAndCreateError)(`Modifier "${modifier}" is not in kebab case.`);
24
+ }
25
+ if (!reuse_1.isKebabCaseRegExp.test(newModifier)) {
26
+ throw (0, reuse_1.logAndCreateError)(`Modifier "${newModifier}" is not in kebab case.`);
27
+ }
28
+ this.regExp = new RegExp(`\\.${base}--${modifier}(?=\\b)`, 'g');
29
+ }
30
+ static getInstance(base, modifier, newModifier, versionRange, dependentTasks = [], options = {}) {
31
+ const identifier = `${base}-rename-modifier-${modifier}-to-${newModifier}`;
32
+ if (!this.instances.has(identifier)) {
33
+ this.instances.set(identifier, new ScssRenameModifierTask(identifier, base, modifier, newModifier, versionRange, dependentTasks, options));
34
+ }
35
+ return this.instances.get(identifier);
36
+ }
37
+ run(baseDir) {
38
+ (0, reuse_1.filterFilesByExt)(baseDir, types_1.SCSS_FILE_EXTENSIONS).forEach((file) => {
39
+ const content = fs_1.default.readFileSync(file, 'utf8');
40
+ const newContent = content.replace(this.regExp, `.${this.base}--${this.newModifier}`);
41
+ if (content !== newContent) {
42
+ reuse_1.MODIFIED_FILES.add(file);
43
+ fs_1.default.writeFileSync(file, newContent);
44
+ }
45
+ });
46
+ }
47
+ }
48
+ exports.ScssRenameModifierTask = ScssRenameModifierTask;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScssUpdateTokenTask = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const reuse_1 = require("../../../shares/reuse");
10
+ const abstract_task_1 = require("../../abstract-task");
11
+ /**
12
+ * Escapes special characters for use in a regular expression.
13
+ * @param {string} str String to escape
14
+ * @returns {string} Escaped string
15
+ */
16
+ function escapeRegExp(str) {
17
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ }
19
+ class ScssUpdateTokenTask extends abstract_task_1.AbstractTask {
20
+ newToken;
21
+ regExp;
22
+ constructor(identifier, token, newToken, versionRange, dependentTasks = [], options = {}) {
23
+ super(identifier, `Update token "${token}" to "${newToken}"`, types_1.SCSS_FILE_EXTENSIONS, versionRange, dependentTasks, options);
24
+ this.newToken = newToken;
25
+ if (!token.startsWith('$')) {
26
+ throw (0, reuse_1.logAndCreateError)(`Token "${token}" must start with "$".`);
27
+ }
28
+ if (!newToken.startsWith('$')) {
29
+ throw (0, reuse_1.logAndCreateError)(`Token "${newToken}" must start with "$".`);
30
+ }
31
+ this.regExp = new RegExp(escapeRegExp(token) + '(?=\\s|:|;|,|\\)|\\}|$)', 'g');
32
+ }
33
+ static getInstance(token, newToken, versionRange, dependentTasks = [], options = {}) {
34
+ const identifier = `update-token-${token}-to-${newToken}`;
35
+ if (!this.instances.has(identifier)) {
36
+ this.instances.set(identifier, new ScssUpdateTokenTask(identifier, token, newToken, versionRange, dependentTasks, options));
37
+ }
38
+ return this.instances.get(identifier);
39
+ }
40
+ run(baseDir) {
41
+ (0, reuse_1.filterFilesByExt)(baseDir, types_1.SCSS_FILE_EXTENSIONS).forEach((file) => {
42
+ const content = fs_1.default.readFileSync(file, 'utf8');
43
+ const newContent = content.replace(this.regExp, this.newToken);
44
+ if (content !== newContent) {
45
+ reuse_1.MODIFIED_FILES.add(file);
46
+ fs_1.default.writeFileSync(file, newContent);
47
+ }
48
+ });
49
+ }
50
+ }
51
+ exports.ScssUpdateTokenTask = ScssUpdateTokenTask;
@@ -6,6 +6,7 @@ const abbr_1 = require("./abbr");
6
6
  const modal_1 = require("./modal");
7
7
  const input_file_1 = require("./input-file");
8
8
  const all_input_1 = require("./all-input");
9
+ const toaster_1 = require("./toaster");
9
10
  exports.v3Tasks = [];
10
11
  exports.v3Tasks.push(textarea_1.TextareaUpdatePropertyValue_Resize_Both);
11
12
  exports.v3Tasks.push(textarea_1.TextareaUpdatePropertyValue_Resize_Horizontal);
@@ -13,3 +14,4 @@ exports.v3Tasks.push(abbr_1.AbbrRemovePropertyTooltipAlign);
13
14
  exports.v3Tasks.push(modal_1.ModalRemovePropertyActiveElement);
14
15
  exports.v3Tasks.push(input_file_1.InputFileRemovePropertyValue);
15
16
  exports.v3Tasks.push(...all_input_1.AllInputTasks);
17
+ exports.v3Tasks.push(toaster_1.ToasterRenameProperties);
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ToasterRenameProperties = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const types_1 = require("../../../../types");
9
+ const fileExtensions = [...types_1.FILE_EXTENSIONS];
10
+ const reuse_1 = require("../../../shares/reuse");
11
+ const abstract_task_1 = require("../../abstract-task");
12
+ class ToasterRenamePropertiesTask extends abstract_task_1.AbstractTask {
13
+ constructor() {
14
+ super('toaster-rename-properties', 'Rename Toaster properties `alertVariant` to `variant` and `defaultAlertType` to `defaultVariant`', fileExtensions, '>=2 <4');
15
+ }
16
+ static getInstance() {
17
+ const identifier = 'toaster-rename-properties';
18
+ if (!this.instances.has(identifier)) {
19
+ this.instances.set(identifier, new ToasterRenamePropertiesTask());
20
+ }
21
+ return this.instances.get(identifier);
22
+ }
23
+ run(baseDir) {
24
+ (0, reuse_1.filterFilesByExt)(baseDir, fileExtensions).forEach((file) => {
25
+ const content = fs_1.default.readFileSync(file, 'utf8');
26
+ const newContent = content.replace(/alertVariant(?=\s*:)/g, 'variant').replace(/defaultAlertType(?=\s*:)/g, 'defaultVariant');
27
+ if (content !== newContent) {
28
+ reuse_1.MODIFIED_FILES.add(file);
29
+ fs_1.default.writeFileSync(file, newContent);
30
+ }
31
+ });
32
+ }
33
+ }
34
+ exports.ToasterRenameProperties = ToasterRenamePropertiesTask.getInstance();
package/dist/types.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.REACT_TAG_REGEX = exports.WEB_TAG_REGEX = exports.MARKUP_EXTENSIONS = exports.CUSTOM_ELEMENT_FILE_EXTENSIONS = exports.COMPONENT_FILE_EXTENSIONS = exports.FILE_EXTENSIONS = void 0;
4
- exports.FILE_EXTENSIONS = ['html', 'xhtml', 'js', 'json', 'jsx', 'ts', 'tsx', 'vue'];
3
+ exports.REACT_TAG_REGEX = exports.WEB_TAG_REGEX = exports.SCSS_FILE_EXTENSIONS = exports.MARKUP_EXTENSIONS = exports.CUSTOM_ELEMENT_FILE_EXTENSIONS = exports.COMPONENT_FILE_EXTENSIONS = exports.FILE_EXTENSIONS = void 0;
4
+ exports.FILE_EXTENSIONS = ['html', 'xhtml', 'js', 'json', 'jsx', 'ts', 'tsx', 'vue', 'css', 'sass', 'scss'];
5
5
  exports.COMPONENT_FILE_EXTENSIONS = ['jsx', 'tsx', 'vue'];
6
6
  exports.CUSTOM_ELEMENT_FILE_EXTENSIONS = ['html', 'xhtml', 'jsx', 'tsx', 'vue'];
7
7
  exports.MARKUP_EXTENSIONS = exports.COMPONENT_FILE_EXTENSIONS.concat(exports.CUSTOM_ELEMENT_FILE_EXTENSIONS);
8
+ exports.SCSS_FILE_EXTENSIONS = ['css', 'sass', 'scss'];
8
9
  exports.WEB_TAG_REGEX = /\b<kol-[a-z-]+/i;
9
10
  exports.REACT_TAG_REGEX = /\b<Kol[A-Z][A-Za-z]*/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@public-ui/kolibri-cli",
3
- "version": "3.0.4-rc.1",
3
+ "version": "3.0.4-rc.2",
4
4
  "license": "EUPL-1.2",
5
5
  "homepage": "https://public-ui.github.io",
6
6
  "repository": {
@@ -21,35 +21,35 @@
21
21
  "description": "CLI for executing some helpful commands for KoliBri projects.",
22
22
  "type": "commonjs",
23
23
  "dependencies": {
24
- "chalk": "5.4.1",
24
+ "chalk": "5.6.0",
25
25
  "commander": "14.0.0",
26
26
  "deepmerge": "4.3.1",
27
27
  "gradient-string": "3.0.0",
28
28
  "loglevel": "1.9.2",
29
- "prettier": "3.5.3",
29
+ "prettier": "3.6.2",
30
30
  "semver": "7.7.2",
31
31
  "typed-bem": "1.0.0-rc.7",
32
- "@public-ui/components": "3.0.4-rc.1"
32
+ "@public-ui/components": "3.0.4-rc.2"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "24.0.1",
35
+ "@types/node": "24.3.0",
36
36
  "@typescript-eslint/eslint-plugin": "7.18.0",
37
37
  "@typescript-eslint/parser": "7.18.0",
38
- "cpy-cli": "5.0.0",
39
- "cross-env": "7.0.3",
38
+ "cpy-cli": "6.0.0",
39
+ "cross-env": "10.0.0",
40
40
  "eslint": "8.57.1",
41
- "eslint-config-prettier": "9.1.0",
41
+ "eslint-config-prettier": "9.1.2",
42
42
  "eslint-plugin-html": "8.1.3",
43
43
  "eslint-plugin-jsdoc": "50.8.0",
44
44
  "eslint-plugin-json": "3.1.0",
45
45
  "eslint-plugin-jsx-a11y": "6.10.2",
46
46
  "eslint-plugin-react": "7.37.5",
47
- "knip": "5.61.0",
48
- "mocha": "11.6.0",
47
+ "knip": "5.63.0",
48
+ "mocha": "11.7.1",
49
49
  "nodemon": "3.1.10",
50
50
  "rimraf": "6.0.1",
51
51
  "ts-node": "10.9.2",
52
- "typescript": "5.8.3"
52
+ "typescript": "5.9.2"
53
53
  },
54
54
  "files": [
55
55
  "dist"