@public-ui/visual-tests 1.7.0-rc.6 → 1.7.0-rc.8

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.
Files changed (58) hide show
  1. package/README.md +144 -23
  2. package/package.json +17 -19
  3. package/src/index.ts +33 -0
  4. package/src/migrate/index.ts +127 -0
  5. package/src/migrate/runner/abstract-task.ts +64 -0
  6. package/src/migrate/runner/task-runner.ts +163 -0
  7. package/src/migrate/runner/tasks/common/GenericRenamePropertyTask.ts +85 -0
  8. package/src/migrate/runner/tasks/common/LabelExpertSlot.ts +96 -0
  9. package/src/migrate/runner/tasks/common/RemovePropertyNameTask.ts +104 -0
  10. package/src/migrate/runner/tasks/common/RenamePropertyNameTask.ts +31 -0
  11. package/src/migrate/runner/tasks/test/index.ts +16 -0
  12. package/src/migrate/runner/tasks/test/test-dummy.ts +20 -0
  13. package/src/migrate/runner/tasks/test/test-version-1.3.ts +7 -0
  14. package/src/migrate/runner/tasks/test/test-version-current.ts +7 -0
  15. package/src/migrate/runner/tasks/test/test-version-next-2.ts +7 -0
  16. package/src/migrate/runner/tasks/test/test-version-next-3.ts +7 -0
  17. package/src/migrate/runner/tasks/test/test-version-zero.ts +7 -0
  18. package/src/migrate/runner/tasks/v1/abbr.ts +3 -0
  19. package/src/migrate/runner/tasks/v1/accordion.ts +3 -0
  20. package/src/migrate/runner/tasks/v1/badge.ts +6 -0
  21. package/src/migrate/runner/tasks/v1/breadcrumb.ts +3 -0
  22. package/src/migrate/runner/tasks/v1/button-link.ts +6 -0
  23. package/src/migrate/runner/tasks/v1/button.ts +8 -0
  24. package/src/migrate/runner/tasks/v1/card.ts +4 -0
  25. package/src/migrate/runner/tasks/v1/details.ts +3 -0
  26. package/src/migrate/runner/tasks/v1/icon.ts +5 -0
  27. package/src/migrate/runner/tasks/v1/index.ts +151 -0
  28. package/src/migrate/runner/tasks/v1/input-checkbox.ts +3 -0
  29. package/src/migrate/runner/tasks/v1/input-color.ts +3 -0
  30. package/src/migrate/runner/tasks/v1/input-date.ts +3 -0
  31. package/src/migrate/runner/tasks/v1/input-email.ts +3 -0
  32. package/src/migrate/runner/tasks/v1/input-number.ts +4 -0
  33. package/src/migrate/runner/tasks/v1/input-radio.ts +3 -0
  34. package/src/migrate/runner/tasks/v1/input-range.ts +3 -0
  35. package/src/migrate/runner/tasks/v1/input-text.ts +3 -0
  36. package/src/migrate/runner/tasks/v1/link-button.ts +15 -0
  37. package/src/migrate/runner/tasks/v1/link-group.ts +7 -0
  38. package/src/migrate/runner/tasks/v1/link.ts +15 -0
  39. package/src/migrate/runner/tasks/v1/logo.ts +3 -0
  40. package/src/migrate/runner/tasks/v1/modal.ts +3 -0
  41. package/src/migrate/runner/tasks/v1/nav.ts +7 -0
  42. package/src/migrate/runner/tasks/v1/pagination.ts +3 -0
  43. package/src/migrate/runner/tasks/v1/progress.ts +3 -0
  44. package/src/migrate/runner/tasks/v1/quote.ts +3 -0
  45. package/src/migrate/runner/tasks/v1/select.ts +4 -0
  46. package/src/migrate/runner/tasks/v1/skip-nav.ts +3 -0
  47. package/src/migrate/runner/tasks/v1/span.ts +3 -0
  48. package/src/migrate/runner/tasks/v1/split-button.ts +3 -0
  49. package/src/migrate/runner/tasks/v1/table.ts +3 -0
  50. package/src/migrate/runner/tasks/v1/tabs.ts +4 -0
  51. package/src/migrate/runner/tasks/v1/toast.ts +3 -0
  52. package/src/migrate/runner/tasks/v1/version.ts +3 -0
  53. package/src/migrate/runner/types.ts +2 -0
  54. package/src/migrate/shares/reuse.ts +182 -0
  55. package/src/migrate/types.ts +2 -0
  56. package/src/types.ts +23 -0
  57. package/dist/index.js +0 -43
  58. package/kolibri-visual-test.sh +0 -3
package/README.md CHANGED
@@ -1,47 +1,168 @@
1
- # KoliBri - Visual Tests
1
+ # KoliBri - CLI-Tools
2
2
 
3
3
  ## Motivation
4
4
 
5
- The `KoliBri` Visual Tests provide a way to add visual regression testing to **theme** modules.
6
- It takes screenshots of every component defined in the [React Sample App](https://github.com/public-ui/kolibri/tree/develop/packages/samples/react) with the theme applied and compares them to their references.
5
+ The `KoliBri` CLI-Tools are a collection of tools to support the development with `KoliBri` components.
7
6
 
8
7
  ## Installation
9
8
 
10
- You can install the `KoliBri` Visual Tests with `npm`, `pnpm` or `yarn`:
9
+ You can install the `KoliBri` CLI-Tools with `npm`, `pnpm` or `yarn`.
11
10
 
12
11
  ```bash
13
- npm i -D @public-ui/visual-tests
14
- pnpm i -D @public-ui/visual-tests
15
- yarn add -D @public-ui/visual-tests
12
+ npm i -g @public-ui/kolibri-cli
13
+ pnpm i -g @public-ui/kolibri-cli
14
+ yarn add -g @public-ui/kolibri-cli
16
15
  ```
17
16
 
18
17
  ## Usage
19
18
 
20
- Add the following npm scripts to the theme's `package.json`:
19
+ The `KoliBri` CLI is intended to be executed in your project root directory. Use the `kolibri` command to start the CLI.
21
20
 
22
- ```json
23
- {
24
- "scripts": {
25
- "test": "THEME_MODULE=src/index THEME_EXPORT=THEME_NAME kolibri-visual-test",
26
- "test-update": "THEME_MODULE=src/index THEME_EXPORT=THEME_NAME kolibri-visual-test --update-snapshots"
27
- }
28
- }
21
+ ```bash
22
+ kolibri --help
23
+ ```
24
+
25
+ ```bash
26
+ ,--. ,--. ,--. ,--. ,-----. ,--.
27
+ | .' / ,---. | | `--' | |) /_ ,--.--. `--'
28
+ | . ' | .-. | | | ,--. | .-. \ | .--' ,--.
29
+ | |\ \ | '-' | | | | | | '--' / | | | |
30
+ `--' `--´ `---´ `--' `--' `------´ `--' `--'
31
+ 🚹 The accessible HTML-Standard | 👉 https://public-ui.github.io
32
+
33
+ Usage: kolibri [options] [command]
34
+
35
+ CLI for executing some helpful commands for KoliBri projects.
36
+
37
+ Options:
38
+ -V, --version output the version number
39
+ -h, --help display help for command
40
+
41
+ Commands:
42
+ migrate [options] <string> This command migrates KoliBri code to the current version.
43
+ help [command] display help for command
44
+ ```
45
+
46
+ ### Migrate
47
+
48
+ With the `migrate` command you can migrate your project to the latest version of `KoliBri`.
49
+
50
+ Actually the following migrations are available:
51
+
52
+ - Component renaming ✓
53
+ - Component removal ⏰
54
+ - Property renaming ✓
55
+ - Property removal ✓
56
+ - Property type change ⏰
57
+ - Logic refactoring ⏰
58
+ - Expert-Slot refactoring ⏰
59
+ - `.vscode/settings.json` add IntelliSense for HTML ⏰
60
+ - `.gitignore` exclude `.kolibri.migrate.json` ⏰
61
+ - `.tsconfig` add `@public-ui/components` to `types` array ⏰
62
+
63
+ #### How does it work?
64
+
65
+ 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.
66
+ 2. Tasks in the correct version range will be executed one by one. Otherwise they will be skipped.
67
+ 3. After that the `package.json` will be updated with the new version of `KoliBri` and execute the `npm install` command.
68
+ 4. If there are any pending tasks, the migration command will be executed again. Otherwise the migration is finished.
69
+ 5. Now you can check the result and commit the changes.
70
+
71
+ > **Note:** You can reset the migration with `git reset --hard HEAD~1` or by discarding the affected files.
72
+
73
+ #### Help
74
+
75
+ ```bash
76
+ kolibri migrate --help
77
+ ```
78
+
79
+ #### Execute
80
+
81
+ ```bash
82
+ kolibri migrate <path>
29
83
  ```
30
84
 
31
- - `THEME_MODULE` defines the relative path to the TypeScript module containing the the theme definitions. Without file extension.
32
- - `THEME_EXPERT` defines the name of the export within the the module. (e.g., `export const THEME_NAME = {/**/};`) Defaults to `default`.
85
+ #### Options
33
86
 
34
- Run the tests with `npm test`. The first time, this will create a new folder `snapshots` which is supposed to be committed to the repository.
35
- In the following runs, new screenshots will be compared to this reference.
87
+ | Option | Description | Type | Default |
88
+ | ------------------------------ | ----------------------------------------- | :------------------: | :------: |
89
+ | `--ignore-uncommitted-changes` | Allows execution with uncommitted changes | boolean | false |
90
+ | `--remove-mode` | Prefix property name or delete property | `delete` \| `prefix` | `prefix` |
36
91
 
37
- To update the reference screenshots call `npm run test-update`.
92
+ #### Configuration
38
93
 
39
- It's also recommended to automatically run the tests before packing/publishing the module:
94
+ You can configure the migration with the `.kolibri.config.json` file in your project root folder. This file will be created automatically after the first migration.
95
+
96
+ **Troubleshooting:** If you have problems with migration, you can exclude some tasks with the configuration by setting the `false` flag (see `kol-select`).
40
97
 
41
98
  ```json
42
99
  {
43
- "scripts": {
44
- "prepack": "npm test"
100
+ "migrate": {
101
+ "tasks": {
102
+ "kol-abbr-rename-property-_title-to-_label": true,
103
+ "kol-accordion-rename-property-_heading-to-_label": true,
104
+ "kol-badge-rename-property-_icon-only-to-_hide-label": true,
105
+ "kol-badge-remove-property-_hide-label": true,
106
+ "kol-badge-remove-property-_icon-only": true,
107
+ "kol-breadcrumb-rename-property-_aria-label-to-_label": true,
108
+ "kol-button-link-remove-property-_aria-current": true,
109
+ "kol-button-link-remove-property-_aria-label": true,
110
+ "kol-button-link-rename-property-_icon-only-to-_hide-label": true,
111
+ "kol-button-remove-property-_aria-current": true,
112
+ "kol-button-remove-property-_aria-label": true,
113
+ "kol-button-rename-property-_icon-only-to-_hide-label": true,
114
+ "kol-card-rename-property-_heading-to-_label": true,
115
+ "kol-card-rename-property-_headline-to-_label": true,
116
+ "kol-details-rename-property-_summary-to-_label": true,
117
+ "kol-icon-remove-property-_part": true,
118
+ "kol-icon-rename-property-_aria-label-to-_label": true,
119
+ "kol-checkbox-rename-property-_type-to-_variant": true,
120
+ "kol-color-rename-property-_list-to-_suggestions": true,
121
+ "kol-date-rename-property-_list-to-_suggestions": true,
122
+ "kol-input-email-rename-property-_list-to-_suggestions": true,
123
+ "kol-input-number-rename-property-_list-to-_suggestions": true,
124
+ "kol-input-radio-rename-property-_list-to-_options": true,
125
+ "kol-input-range-rename-property-_list-to-_suggestions": true,
126
+ "kol-input-text-rename-property-_list-to-_suggestions": true,
127
+ "kol-link-group-rename-property-_aria-label-to-_label": true,
128
+ "kol-link-group-remove-property-_ordered": true,
129
+ "kol-nav-rename-property-_aria-label-to-_label": true,
130
+ "kol-nav-rename-property-_compact-to-_hide-label": true,
131
+ "kol-nav-remove-property-_has-compact-button": true,
132
+ "kol-pagination-rename-property-_count-to-_total": true,
133
+ "kol-progress-rename-property-_type-to-_variant": true,
134
+ "kol-quote-rename-property-_caption-to-_label": true,
135
+ "kol-select-rename-property-_height-to-_rows": false,
136
+ "kol-select-rename-property-_list-to-_options": false,
137
+ "kol-skip-nav-rename-property-_aria-label-to-_label": true,
138
+ "kol-span-rename-property-_icon-only-to-_hide-label": true,
139
+ "kol-split-button-remove-property-_aria-label": true,
140
+ "kol-table-rename-property-_caption-to-_label": true,
141
+ "kol-tabs-rename-property-_aria-label-to-_label": true,
142
+ "kol-tabs-rename-property-_tab-align-to-_align": true,
143
+ "kol-toast-rename-property-_heading-to-_label": true,
144
+ "kol-version-rename-property-_version-to-_label": true
145
+ }
45
146
  }
46
147
  }
47
148
  ```
149
+
150
+ ## Troubleshooting
151
+
152
+ If the migration failed, you can reset the migration with `git reset --hard HEAD~1`.
153
+
154
+ Use the configuration (`.kolibri.config.json`) to exclude some tasks.
155
+
156
+ If there are multiple obsolete properties that have been migrated to just one new property, the new property may appear multiple times in the tag. You can then decide which variant to use and remove all other variants accordingly.
157
+
158
+ Maybe it can help to prepare your code in the tricky places for migration.
159
+
160
+ Please give us feedback, if you have problems with the migration: [GitHub Issues](https://github.com/public-ui/kolibri/issues/new?assignees=&labels=useful+hint&projects=&template=7_feedback.md&title=%F0%9F%92%A1+CLI%3A+)
161
+
162
+ ## Dry run
163
+
164
+ You have always the possibility of a dry run. Because before the migration will be executed, you need a clean git history of you project.
165
+
166
+ After the migration you can check the result with `git status` and `git diff`.
167
+
168
+ Is anything wrong, you can reset the migration with `git reset --hard HEAD~1` or by discarding the affected files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@public-ui/visual-tests",
3
- "version": "1.7.0-rc.6",
3
+ "version": "1.7.0-rc.8",
4
4
  "license": "EUPL-1.2",
5
5
  "homepage": "https://public-ui.github.io",
6
6
  "repository": "https://github.com/public-ui/kolibri",
@@ -16,33 +16,31 @@
16
16
  "sideEffects": false,
17
17
  "description": "Provides utility to run visual regression tests for themes.",
18
18
  "scripts": {
19
- "build": "tsc --outDir dist",
19
+ "depcheck": "depcheck --ignores=@public-ui/sample-react,http-server,tslib",
20
20
  "format": "prettier --check src",
21
- "lint": "eslint src && tsc --noemit",
22
- "unused": "knip",
23
- "prepare": "npm run build"
21
+ "lint": "eslint src",
22
+ "unused": "knip"
24
23
  },
25
24
  "bin": {
26
- "kolibri-visual-test": "kolibri-visual-test.sh"
27
- },
28
- "devDependencies": {
29
- "@types/node": "20.6.0",
30
- "@typescript-eslint/eslint-plugin": "6.7.0",
31
- "eslint": "8.49.0",
32
- "eslint-plugin-no-loops": "0.3.0",
33
- "knip": "2.23.0",
34
- "npm-check-updates": "16.13.3",
35
- "prettier": "3.0.3",
36
- "tslib": "2.6.2",
37
- "typescript": "5.2.2"
25
+ "kolibri-visual-test": "src/index.js"
38
26
  },
39
27
  "dependencies": {
40
28
  "@playwright/test": "1.37.1",
41
- "@public-ui/sample-react": "1.7.0-rc.6",
29
+ "@public-ui/sample-react": "1.7.0-rc.8",
42
30
  "http-server": "14.1.1",
43
31
  "portfinder": "1.0.32"
44
32
  },
33
+ "devDependencies": {
34
+ "@babel/eslint-parser": "7.22.15",
35
+ "depcheck": "1.4.6",
36
+ "eslint": "8.49.0",
37
+ "eslint-plugin-no-loops": "0.3.0",
38
+ "knip": "2.24.0",
39
+ "prettier": "3.0.3"
40
+ },
45
41
  "files": [
46
- "dist"
42
+ "playwright.config.js",
43
+ "src",
44
+ "tests"
47
45
  ]
48
46
  }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import gradient from 'gradient-string';
5
+
6
+ import migrate from './migrate';
7
+ import { getVersionOfPublicUiKoliBriCli } from './migrate/shares/reuse';
8
+
9
+ const versionOfPublicUiKoliBriCli = getVersionOfPublicUiKoliBriCli();
10
+
11
+ const banner = gradient.atlas.multiline(
12
+ `
13
+ ,--. ,--. ,--. ,--. ,-----. ,--.
14
+ | .' / ,---. | | \`--' | |) /_ ,--.--. \`--'
15
+ | . ' | .-. | | | ,--. | .-. \\ | .--' ,--.
16
+ | |\\ \\ | '-' | | | | | | '--' / | | | |
17
+ \`--' \`--´ \`---´ \`--' \`--' \`------´ \`--' \`--'
18
+ 🚹 The accessible HTML-Standard | 👉 https://public-ui.github.io | ${versionOfPublicUiKoliBriCli}
19
+ `,
20
+ {
21
+ interpolation: 'hsv',
22
+ },
23
+ );
24
+ console.log(banner);
25
+
26
+ const program = new Command();
27
+
28
+ program.name('kolibri').description('CLI for executing some helpful commands for KoliBri projects.').version(versionOfPublicUiKoliBriCli);
29
+
30
+ // Add commands
31
+ migrate(program);
32
+
33
+ program.parse();
@@ -0,0 +1,127 @@
1
+ import { exec } from 'child_process';
2
+ import { Command, Option } from 'commander';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ import { Configuration } from '../types';
7
+ import { TaskRunner } from './runner/task-runner';
8
+ import { testTasks } from './runner/tasks/test';
9
+ import { v1Tasks } from './runner/tasks/v1';
10
+ import {
11
+ getContentOfProjectPkgJson,
12
+ getPackageManagerInstallCommand,
13
+ getVersionOfPublicUiComponents,
14
+ getVersionOfPublicUiKoliBriCli,
15
+ logAndCreateError,
16
+ MODIFIED_FILES,
17
+ setRemoveMode,
18
+ } from './shares/reuse';
19
+ import { REMOVE_MODE, RemoveMode } from './types';
20
+
21
+ type MigrateOption = {
22
+ ignoreUncommittedChanges: boolean;
23
+ removeMode: RemoveMode;
24
+ testTasks: boolean;
25
+ };
26
+
27
+ /**
28
+ * This function is used to register the migrate command.
29
+ * @param {Command} program The program object to register the command
30
+ */
31
+ export default function (program: Command): void {
32
+ program
33
+ .command('migrate')
34
+ .description('This command migrates KoliBri code to the current version.')
35
+ .argument('[string]', 'Source code folder to migrate', 'src')
36
+ .addOption(new Option('--ignore-uncommitted-changes', 'Allows execution with uncommitted changes').default(false))
37
+ .addOption(new Option('--remove-mode <mode>', 'Prefix property name or delete property').choices(REMOVE_MODE).default('prefix'))
38
+ .addOption(new Option('--test-tasks', 'Run additional test tasks').default(false).hideHelp())
39
+ .action((baseDir: string, options: MigrateOption) => {
40
+ exec('git status --porcelain', (err, stdout) => {
41
+ if (err) {
42
+ console.error(`exec error: ${err.message}`);
43
+ return;
44
+ }
45
+
46
+ if (!options.ignoreUncommittedChanges && stdout) {
47
+ throw logAndCreateError('There are uncommitted changes');
48
+ }
49
+
50
+ setRemoveMode(options.removeMode);
51
+
52
+ const versionOfPublicUiComponents = getVersionOfPublicUiComponents();
53
+ const versionOfPublicUiKoliBriCli = getVersionOfPublicUiKoliBriCli();
54
+
55
+ console.log(`
56
+ Current version of @public-ui/components: ${versionOfPublicUiComponents}
57
+ Source folder to migrate: ${baseDir}
58
+ `);
59
+
60
+ const configFile = path.resolve(process.cwd(), '.kolibri.config.json');
61
+ let config: Configuration = {};
62
+ if (fs.existsSync(configFile)) {
63
+ try {
64
+ config = JSON.parse(fs.readFileSync(configFile, 'utf8')) as Configuration;
65
+ } catch (e) {
66
+ // ignore
67
+ }
68
+ }
69
+
70
+ const runner = new TaskRunner(baseDir, versionOfPublicUiKoliBriCli, versionOfPublicUiComponents, config);
71
+ runner.registerTasks(v1Tasks);
72
+
73
+ if (options.testTasks) {
74
+ runner.registerTasks(testTasks);
75
+ }
76
+
77
+ let version = versionOfPublicUiComponents;
78
+
79
+ /**
80
+ * Runs the task runner in a loop until all tasks are completed.
81
+ */
82
+ function runLoop() {
83
+ runner.run();
84
+ if (version !== runner.getPendingMinVersion()) {
85
+ version = runner.getPendingMinVersion();
86
+ let packageJson = getContentOfProjectPkgJson();
87
+ packageJson = packageJson.replace(/"(@public-ui\/[^"]+)":\s*".*"/g, `"$1": "${version}"`);
88
+ fs.writeFileSync(path.resolve(process.cwd(), 'package.json'), packageJson);
89
+ runner.setProjectVersion(version);
90
+
91
+ console.log(`- Update @public-ui/* to version ${version}`);
92
+ exec(getPackageManagerInstallCommand(), (err) => {
93
+ if (err) {
94
+ console.error(`exec error: ${err.message}`);
95
+ return;
96
+ }
97
+ runLoop();
98
+ });
99
+ } else {
100
+ console.log(`
101
+ Status of all executed Tasks:`);
102
+
103
+ const status = runner.getStatus(true);
104
+ fs.writeFileSync(configFile, JSON.stringify(status.config, null, 2));
105
+
106
+ console.log(`
107
+ Modified files: ${MODIFIED_FILES.size}`);
108
+ MODIFIED_FILES.forEach((file) => {
109
+ console.log(`- ${file}`);
110
+ });
111
+
112
+ console.log(`
113
+ After the code migration has gone through, the code formatting may no longer be as desired. Therefore, please reformat your code afterwards if necessary.
114
+
115
+ Afterwards, it may be that functions or themes in newer major versions have changed or are no longer included. This should be checked finally and corrected manually if necessary.
116
+
117
+ Is anything wrong, you can reset the migration with "git reset --hard HEAD~1" or by discarding the affected files. For more information read the troubleshooting section in the README.`);
118
+ }
119
+ }
120
+
121
+ const status = runner.getStatus();
122
+ console.log(`
123
+ Execute ${status.total} registered tasks...`);
124
+ runLoop();
125
+ });
126
+ });
127
+ }
@@ -0,0 +1,64 @@
1
+ import semver from 'semver';
2
+
3
+ import { FILE_EXTENSIONS, FileExtension } from '../../types';
4
+ import { logAndCreateError } from '../shares/reuse';
5
+ import { TaskStatus } from './types';
6
+
7
+ export type TaskOptions = {
8
+ dependentTasks?: AbstractTask[];
9
+ description?: string;
10
+ };
11
+
12
+ export abstract class AbstractTask {
13
+ private status: TaskStatus = 'pending';
14
+
15
+ protected static readonly instances: Map<string, AbstractTask> = new Map();
16
+
17
+ protected readonly description?: string;
18
+
19
+ protected constructor(
20
+ protected readonly identifier: string,
21
+ protected readonly title: string,
22
+ protected readonly extensions: FileExtension[],
23
+ protected readonly versionRange: string,
24
+ protected readonly dependentTasks: AbstractTask[] = [],
25
+ options: TaskOptions = {},
26
+ ) {
27
+ this.description = options.description;
28
+ this.extensions = this.extensions.filter((ext) => FILE_EXTENSIONS.includes(ext));
29
+
30
+ if (!semver.validRange(this.versionRange)) {
31
+ throw logAndCreateError(`[${this.identifier}] Invalid semver range version: ${this.versionRange}`);
32
+ }
33
+ }
34
+
35
+ public getDependentTasks(): AbstractTask[] {
36
+ return this.dependentTasks;
37
+ }
38
+
39
+ public getDescription(): string | undefined {
40
+ return this.description;
41
+ }
42
+
43
+ public getIdentifier(): string {
44
+ return this.identifier;
45
+ }
46
+
47
+ public setStatus(status: TaskStatus): void {
48
+ this.status = status;
49
+ }
50
+
51
+ public getStatus(): TaskStatus {
52
+ return this.status;
53
+ }
54
+
55
+ public getTitle(): string {
56
+ return this.title;
57
+ }
58
+
59
+ public getVersionRange(): string {
60
+ return this.versionRange;
61
+ }
62
+
63
+ public abstract run(baseDir: string): void;
64
+ }
@@ -0,0 +1,163 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import semver from 'semver';
5
+
6
+ import { Configuration } from '../../types';
7
+ import { logAndCreateError } from '../shares/reuse';
8
+ import { AbstractTask } from './abstract-task';
9
+
10
+ export class TaskRunner {
11
+ private readonly tasks: Map<string, AbstractTask> = new Map();
12
+ private baseDir: string = '/';
13
+ private cliVersion: string = '0.0.0';
14
+ private projectVersion: string = '0.0.0';
15
+ private readonly config: Configuration = {
16
+ migrate: {
17
+ tasks: {},
18
+ },
19
+ };
20
+
21
+ public constructor(baseDir: string, cliVersion: string, projectVersion: string, config: Configuration) {
22
+ this.setBaseDir(baseDir);
23
+ this.setCliVersion(cliVersion);
24
+ this.setProjectVersion(projectVersion);
25
+ this.setConfig(config);
26
+ }
27
+
28
+ private setBaseDir(baseDir: string): void {
29
+ if (!fs.existsSync(path.resolve(process.cwd(), baseDir))) {
30
+ throw logAndCreateError(`Base directory "${baseDir}" does not exist`);
31
+ }
32
+ this.baseDir = baseDir;
33
+ }
34
+
35
+ private setCliVersion(version: string): void {
36
+ if (semver.valid(version) === null) {
37
+ throw logAndCreateError(`Invalid CLI version: ${version}`);
38
+ }
39
+ this.cliVersion = version;
40
+ }
41
+
42
+ public setProjectVersion(version: string): void {
43
+ if (semver.valid(version) === null) {
44
+ throw logAndCreateError(`Invalid project version: ${version}`);
45
+ }
46
+ if (this.projectVersion !== version) {
47
+ this.projectVersion = version;
48
+ }
49
+ }
50
+
51
+ private setConfig(config: Configuration): void {
52
+ if (config.migrate?.tasks) {
53
+ this.config.migrate!.tasks = {
54
+ ...this.config.migrate!.tasks,
55
+ ...config.migrate?.tasks,
56
+ };
57
+ }
58
+ }
59
+
60
+ public registerTasks(tasks: AbstractTask[]): void {
61
+ tasks.forEach((task) => {
62
+ if (
63
+ semver.gtr(this.projectVersion, task.getVersionRange(), {
64
+ includePrerelease: true,
65
+ })
66
+ ) {
67
+ console.log(
68
+ `Task "${task.getTitle()}" will be excluded. The current version (${
69
+ this.projectVersion
70
+ }) is greater than the task version range (${task.getVersionRange()}).`,
71
+ );
72
+ this.config.migrate!.tasks[task.getIdentifier()] = false;
73
+ } else {
74
+ this.tasks.set(task.getIdentifier(), task);
75
+ }
76
+ });
77
+ }
78
+
79
+ private registerTask(task: AbstractTask): void {
80
+ this.registerTasks([task]);
81
+ }
82
+
83
+ private runTask(task: AbstractTask): void {
84
+ if (this.config.migrate?.tasks[task.getIdentifier()] === false) {
85
+ task.setStatus('skipped');
86
+ } else {
87
+ this.config.migrate!.tasks[task.getIdentifier()] = true;
88
+ if (
89
+ task.getStatus() === 'pending' &&
90
+ semver.satisfies(this.projectVersion, task.getVersionRange(), {
91
+ includePrerelease: true,
92
+ })
93
+ ) {
94
+ // task.setStatus('running'); only of the task is async
95
+ if (!this.tasks.has(task.getIdentifier())) {
96
+ this.registerTask(task);
97
+ }
98
+ task.run(this.baseDir);
99
+ task.setStatus('done');
100
+ }
101
+ }
102
+ }
103
+
104
+ private dependentTaskRun(task: AbstractTask, dependentTasks: AbstractTask[]) {
105
+ dependentTasks.forEach((dependentTask) => {
106
+ this.dependentTaskRun(dependentTask, dependentTask.getDependentTasks());
107
+ });
108
+ if (dependentTasks.every((dependentTask) => dependentTask.getStatus() === 'done')) {
109
+ this.runTask(task);
110
+ }
111
+ }
112
+
113
+ public run(): void {
114
+ this.tasks.forEach((task) => {
115
+ this.dependentTaskRun(task, task.getDependentTasks());
116
+ });
117
+ }
118
+
119
+ public getPendingMinVersion(): string {
120
+ let version: string = this.cliVersion;
121
+ this.tasks.forEach((task) => {
122
+ if (task.getStatus() === 'pending') {
123
+ const minVersion = semver.minVersion(task.getVersionRange())?.raw ?? this.cliVersion;
124
+ if (semver.gt(version, minVersion)) {
125
+ version = minVersion;
126
+ }
127
+ }
128
+ });
129
+ return version;
130
+ }
131
+
132
+ public getStatus(outline = false): {
133
+ done: number;
134
+ pending: number;
135
+ total: number;
136
+ nextVersion: string | null;
137
+ config: Configuration;
138
+ } {
139
+ let done = 0;
140
+ let pending = 0;
141
+ this.tasks.forEach((task) => {
142
+ switch (task.getStatus()) {
143
+ case 'done':
144
+ done++;
145
+ break;
146
+ case 'pending':
147
+ pending++;
148
+ break;
149
+ }
150
+ if (outline) {
151
+ const status = task.getStatus();
152
+ console.log(`- ${task.getTitle()}:`, status === 'done' ? chalk.green(status) : chalk.yellow(status));
153
+ }
154
+ });
155
+ return {
156
+ done: done,
157
+ pending: pending,
158
+ total: this.tasks.size,
159
+ nextVersion: this.getPendingMinVersion(),
160
+ config: this.config,
161
+ };
162
+ }
163
+ }