@rancher/create-extension 1.0.0-rc.1 → 1.0.0
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/init +49 -21
- package/migrate/config.js +7 -0
- package/migrate/ignore.js +14 -0
- package/migrate/init +35 -0
- package/migrate/package.json +23 -0
- package/migrate/params.js +7 -0
- package/migrate/stats.js +13 -0
- package/migrate/tasks/eslintUpdates.js +70 -0
- package/migrate/tasks/index.js +19 -0
- package/migrate/tasks/nvmUpdates.js +51 -0
- package/migrate/tasks/packageUpdates.js +242 -0
- package/migrate/tasks/routerUpdates.js +23 -0
- package/migrate/tasks/stylesUpdates.js +19 -0
- package/migrate/tasks/tsUpdates.js +19 -0
- package/migrate/tasks/vueConfigUpdates.js +30 -0
- package/migrate/tasks/vueSyntaxUpdates.js +148 -0
- package/migrate/utils/content.js +165 -0
- package/migrate/utils/index.js +4 -0
- package/migrate/utils/vueSyntax.js +126 -0
- package/package.json +7 -3
package/init
CHANGED
|
@@ -9,20 +9,34 @@ const creatorPkg = require('./package.json');
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
let extensionName = '';
|
|
11
11
|
let appName = '';
|
|
12
|
+
let migrate = false;
|
|
12
13
|
let updateOnly = false;
|
|
13
14
|
let skeletonOnly = false;
|
|
14
|
-
let ignoreShellDepCheck = false;
|
|
15
|
+
let ignoreShellDepCheck = false; // Ignore the check for @rancher/shell dependency (Only used for testing)
|
|
15
16
|
let tagUsed = ''; // To store the inferred tag
|
|
16
17
|
|
|
17
18
|
args.forEach((arg, index) => {
|
|
18
19
|
switch (arg) {
|
|
20
|
+
case '--help':
|
|
21
|
+
case '-h':
|
|
22
|
+
console.log(`Usage: ${ creatorPkg.name } [options] [extension-name]`);
|
|
23
|
+
console.log('\nOptions:');
|
|
24
|
+
console.log(' --help, -h Show help');
|
|
25
|
+
console.log(' --migrate Migrate an existing extension to Vue 3');
|
|
26
|
+
console.log(' --update, -u Update applications');
|
|
27
|
+
console.log(' --app-name, -a Specify the name of the application');
|
|
28
|
+
console.log(' --skeleton-only, -s Create only the skeleton application');
|
|
29
|
+
process.exit(0);
|
|
30
|
+
case '--migrate':
|
|
31
|
+
migrate = true;
|
|
32
|
+
break;
|
|
19
33
|
case '--update':
|
|
20
34
|
case '-u':
|
|
21
35
|
updateOnly = true;
|
|
22
36
|
break;
|
|
23
37
|
case '--app-name':
|
|
24
38
|
case '-a':
|
|
25
|
-
if (
|
|
39
|
+
if (args[index + 1] && !args[index + 1].startsWith('-')) {
|
|
26
40
|
appName = args[index + 1];
|
|
27
41
|
} else {
|
|
28
42
|
console.error('Error: Missing value for --app-name or -a option.');
|
|
@@ -37,14 +51,14 @@ args.forEach((arg, index) => {
|
|
|
37
51
|
ignoreShellDepCheck = true;
|
|
38
52
|
break;
|
|
39
53
|
default:
|
|
40
|
-
if (
|
|
54
|
+
if (!arg.startsWith('-') && extensionName === '') {
|
|
41
55
|
extensionName = arg;
|
|
42
56
|
appName = appName || extensionName;
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
});
|
|
46
60
|
|
|
47
|
-
if (
|
|
61
|
+
if (!extensionName && !updateOnly && !skeletonOnly && !migrate) {
|
|
48
62
|
console.error('Please provide an extension name.');
|
|
49
63
|
process.exit(1);
|
|
50
64
|
}
|
|
@@ -55,7 +69,7 @@ try {
|
|
|
55
69
|
const currentVersion = creatorPkg.version;
|
|
56
70
|
|
|
57
71
|
// Fetch the dist-tags from npm
|
|
58
|
-
const distTags = JSON.parse(execSync(`npm view ${ packageName } dist-tags --json
|
|
72
|
+
const distTags = JSON.parse(execSync(`npm view ${ packageName } dist-tags --json`, { stdio: 'pipe' }).toString());
|
|
59
73
|
|
|
60
74
|
// Find the tag matching the current version
|
|
61
75
|
tagUsed = Object.keys(distTags).find((tag) => distTags[tag] === currentVersion) || 'latest';
|
|
@@ -72,12 +86,12 @@ const shellPackageName = '@rancher/shell';
|
|
|
72
86
|
|
|
73
87
|
try {
|
|
74
88
|
// Fetch the version of the `@rancher/shell` package that corresponds to the inferred tag
|
|
75
|
-
const tagVersion = execSync(`npm view ${ shellPackageName } dist-tags.${ tagUsed }
|
|
89
|
+
const tagVersion = execSync(`npm view ${ shellPackageName } dist-tags.${ tagUsed }`, { stdio: 'pipe' }).toString().trim();
|
|
76
90
|
|
|
77
91
|
if (tagVersion) {
|
|
78
92
|
shellVersion = tagVersion;
|
|
79
93
|
} else {
|
|
80
|
-
const latestVersion = execSync(`npm view ${ shellPackageName } version
|
|
94
|
+
const latestVersion = execSync(`npm view ${ shellPackageName } version`, { stdio: 'pipe' }).toString().trim();
|
|
81
95
|
|
|
82
96
|
shellVersion = latestVersion;
|
|
83
97
|
}
|
|
@@ -86,7 +100,9 @@ try {
|
|
|
86
100
|
process.exit(1);
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
if (!migrate) {
|
|
104
|
+
console.log(` Using version ${ shellVersion } for ${ shellPackageName }`);
|
|
105
|
+
}
|
|
90
106
|
|
|
91
107
|
const basePath = process.cwd();
|
|
92
108
|
let skeletonPath;
|
|
@@ -94,12 +110,12 @@ let isInsideSkeleton = false;
|
|
|
94
110
|
let directoryExists = false;
|
|
95
111
|
|
|
96
112
|
// Check if we are inside a skeleton application by looking for package.json
|
|
97
|
-
if (
|
|
113
|
+
if (fs.existsSync(path.join(basePath, 'package.json'))) {
|
|
98
114
|
// Check for @rancher/shell dependency
|
|
99
115
|
const packageJsonPath = path.join(basePath, 'package.json');
|
|
100
116
|
const packageJson = require(packageJsonPath);
|
|
101
117
|
|
|
102
|
-
if (
|
|
118
|
+
if (!ignoreShellDepCheck && (!packageJson.dependencies || !packageJson.dependencies['@rancher/shell'])) {
|
|
103
119
|
throw new Error('@rancher/shell dependency is missing in package.json.');
|
|
104
120
|
} else {
|
|
105
121
|
isInsideSkeleton = true;
|
|
@@ -109,7 +125,7 @@ if ( fs.existsSync(path.join(basePath, 'package.json')) ) {
|
|
|
109
125
|
// If not inside a skeleton, check if a directory with the appName already exists
|
|
110
126
|
skeletonPath = path.join(basePath, appName);
|
|
111
127
|
|
|
112
|
-
if (
|
|
128
|
+
if (fs.existsSync(skeletonPath)) {
|
|
113
129
|
directoryExists = true;
|
|
114
130
|
}
|
|
115
131
|
}
|
|
@@ -121,7 +137,19 @@ const skeletonExists = fs.existsSync(skeletonPath);
|
|
|
121
137
|
const pkgExists = fs.existsSync(pkgPath);
|
|
122
138
|
|
|
123
139
|
try {
|
|
124
|
-
if (
|
|
140
|
+
if (migrate) {
|
|
141
|
+
if (!isInsideSkeleton) {
|
|
142
|
+
throw new Error('Migrate option can only be used inside a skeleton application.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`Migrating extension to Vue 3...`);
|
|
146
|
+
execSync(`node ${ path.join(__dirname, 'migrate', 'init') } ${ args.join(' ') }`, { stdio: 'inherit' });
|
|
147
|
+
|
|
148
|
+
console.log('Migration completed successfully.');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (updateOnly) {
|
|
125
153
|
// Run the update script directly
|
|
126
154
|
console.log('Updating applications...');
|
|
127
155
|
execSync(`node ${ path.join(updatePath, 'init') }`, { stdio: 'inherit' });
|
|
@@ -131,44 +159,44 @@ try {
|
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
// If the directory exists but we're not inside a skeleton, we should exit to prevent overwriting
|
|
134
|
-
if (
|
|
162
|
+
if (directoryExists && !isInsideSkeleton) {
|
|
135
163
|
throw new Error(`A directory named "${ appName }" already exists. Aborting.`);
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
// Create skeleton application if it doesn't exist
|
|
139
|
-
if (
|
|
167
|
+
if (!isInsideSkeleton && !skeletonExists) {
|
|
140
168
|
console.log(`Creating skeleton application: ${ appName }...`);
|
|
141
169
|
// Pass all arguments to the app/init script
|
|
142
170
|
execSync(`node ${ path.join(__dirname, 'app', 'init') } ${ appName } ${ shellVersion } ${ args.join(' ') }`, { stdio: 'inherit' });
|
|
143
171
|
|
|
144
172
|
// Ensure the skeleton path directory is created before attempting to change directory
|
|
145
|
-
if (
|
|
173
|
+
if (!fs.existsSync(skeletonPath)) {
|
|
146
174
|
throw new Error(`Failed to create skeleton application directory: ${ skeletonPath }`);
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
// Change working directory to the newly created skeleton app
|
|
150
178
|
process.chdir(skeletonPath);
|
|
151
|
-
} else if (
|
|
179
|
+
} else if (isInsideSkeleton) {
|
|
152
180
|
// If skeleton exists, ensure the working directory is set correctly
|
|
153
181
|
process.chdir(skeletonPath);
|
|
154
182
|
}
|
|
155
183
|
|
|
156
|
-
if (
|
|
184
|
+
if (skeletonOnly) {
|
|
157
185
|
console.log('Skeleton application created successfully. No additional packages will be installed.');
|
|
158
186
|
process.exit(0);
|
|
159
187
|
}
|
|
160
188
|
|
|
161
|
-
if (
|
|
189
|
+
if (pkgExists) {
|
|
162
190
|
throw new Error(`A package directory for "${ extensionName }" already exists.`);
|
|
163
191
|
}
|
|
164
192
|
|
|
165
193
|
// Check for package existence and create it if necessary
|
|
166
|
-
if (
|
|
194
|
+
if (!pkgExists) {
|
|
167
195
|
console.log(`Creating package: ${ extensionName }...`);
|
|
168
196
|
execSync(`node ${ path.join(__dirname, 'pkg', 'init') } ${ extensionName } ${ shellVersion } ${ args.join(' ') }`, { stdio: 'inherit' });
|
|
169
197
|
}
|
|
170
198
|
|
|
171
|
-
if (
|
|
199
|
+
if (args.includes('--update') || args.includes('-u')) {
|
|
172
200
|
// Run the update script
|
|
173
201
|
console.log('Updating applications...');
|
|
174
202
|
execSync(`node ${ path.join(updatePath, 'init') } ${ extensionName }`, { stdio: 'inherit' });
|
|
@@ -176,7 +204,7 @@ try {
|
|
|
176
204
|
|
|
177
205
|
console.log('Extension created successfully.');
|
|
178
206
|
|
|
179
|
-
if (
|
|
207
|
+
if (skeletonOnly || !isInsideSkeleton) {
|
|
180
208
|
console.log(`To begin, run: \n\n\tcd ${ appName } && yarn install\n`);
|
|
181
209
|
}
|
|
182
210
|
} catch (error) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = [
|
|
2
|
+
'**/node_modules/**',
|
|
3
|
+
'**/dist/**',
|
|
4
|
+
'**/dist-pkg/**',
|
|
5
|
+
'**/.vscode/**',
|
|
6
|
+
'**/assets/**',
|
|
7
|
+
'**/charts/**',
|
|
8
|
+
'**/extensions/**',
|
|
9
|
+
'**/scripts/vue-migrate.js',
|
|
10
|
+
'docusaurus/**',
|
|
11
|
+
'storybook-static/**',
|
|
12
|
+
'storybook/**',
|
|
13
|
+
'migration/**',
|
|
14
|
+
];
|
package/migrate/init
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { setParams, printLog, printUsage } = require('./utils/content');
|
|
4
|
+
const {
|
|
5
|
+
packageUpdates,
|
|
6
|
+
nvmUpdates,
|
|
7
|
+
vueConfigUpdates,
|
|
8
|
+
vueSyntaxUpdates,
|
|
9
|
+
routerUpdates,
|
|
10
|
+
eslintUpdates,
|
|
11
|
+
tsUpdates,
|
|
12
|
+
stylesUpdates,
|
|
13
|
+
} = require('./tasks');
|
|
14
|
+
const params = require('./params');
|
|
15
|
+
|
|
16
|
+
(function main() {
|
|
17
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
18
|
+
printUsage();
|
|
19
|
+
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setParams(params);
|
|
24
|
+
|
|
25
|
+
packageUpdates(params);
|
|
26
|
+
nvmUpdates(params);
|
|
27
|
+
vueConfigUpdates(params);
|
|
28
|
+
vueSyntaxUpdates(params);
|
|
29
|
+
routerUpdates(params);
|
|
30
|
+
eslintUpdates(params);
|
|
31
|
+
tsUpdates(params);
|
|
32
|
+
stylesUpdates(params);
|
|
33
|
+
|
|
34
|
+
printLog();
|
|
35
|
+
})();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rancher/create-migration",
|
|
3
|
+
"description": "Rancher UI Vue3 migration helper",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "SUSE",
|
|
7
|
+
"private": false,
|
|
8
|
+
"bin": "./init",
|
|
9
|
+
"files": [
|
|
10
|
+
"**/*.*",
|
|
11
|
+
"init"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"diff": "^7.0.0",
|
|
18
|
+
"fs-extra": "^10.0.0",
|
|
19
|
+
"glob": "^11.0.0",
|
|
20
|
+
"path": "^0.12.7",
|
|
21
|
+
"semver": "^7.6.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/migrate/stats.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const glob = require('glob');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const stats = require('../stats');
|
|
5
|
+
const { writeContent, printContent } = require('../utils/content');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ESLint Updates
|
|
9
|
+
* Files: .eslintrc.js, .eslintrc.json, .eslintrc.yml
|
|
10
|
+
*/
|
|
11
|
+
const eslintUpdates = (params) => {
|
|
12
|
+
const files = glob.sync(params.paths || '**/.eslintrc.*{js,json,yml}', { ignore: params.ignore });
|
|
13
|
+
const replacePlugins = [
|
|
14
|
+
['plugin:vue/essential', 'plugin:vue/vue3-essential'],
|
|
15
|
+
['plugin:vue/strongly-recommended', 'plugin:vue/vue3-strongly-recommended'],
|
|
16
|
+
['plugin:vue/recommended', 'plugin:vue/vue3-recommended']
|
|
17
|
+
];
|
|
18
|
+
const newRules = {
|
|
19
|
+
'vue/one-component-per-file': 'off',
|
|
20
|
+
'vue/no-deprecated-slot-attribute': 'off',
|
|
21
|
+
'vue/require-explicit-emits': 'off',
|
|
22
|
+
'vue/v-on-event-hyphenation': 'off',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
files.forEach((file) => {
|
|
26
|
+
const originalContent = fs.readFileSync(file, 'utf8');
|
|
27
|
+
let content = originalContent;
|
|
28
|
+
const matchedCases = [];
|
|
29
|
+
|
|
30
|
+
replacePlugins.forEach(([text, replacement]) => {
|
|
31
|
+
if (content.includes(text)) {
|
|
32
|
+
content = content.replaceAll(text, replacement);
|
|
33
|
+
matchedCases.push([text, replacement]);
|
|
34
|
+
|
|
35
|
+
writeContent(file, content, originalContent);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const eslintConfigPath = path.join(process.cwd(), `${ file }`);
|
|
40
|
+
let eslintConfig;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
eslintConfig = require(eslintConfigPath);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
eslintConfig = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!eslintConfig.rules) {
|
|
49
|
+
eslintConfig.rules = {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Object.keys(newRules).forEach((rule) => {
|
|
53
|
+
if (!eslintConfig.rules[rule]) {
|
|
54
|
+
eslintConfig.rules[rule] = newRules[rule];
|
|
55
|
+
matchedCases.push(rule);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (matchedCases.length) {
|
|
60
|
+
const updatedConfig = `module.exports = ${ JSON.stringify(eslintConfig, null, 2) }`;
|
|
61
|
+
|
|
62
|
+
writeContent(file, updatedConfig, originalContent);
|
|
63
|
+
printContent(file, `Updating ESLint`, matchedCases);
|
|
64
|
+
stats.eslint.push(file);
|
|
65
|
+
stats.total.push(file);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
module.exports = eslintUpdates;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const packageUpdates = require('./packageUpdates');
|
|
2
|
+
const nvmUpdates = require('./nvmUpdates');
|
|
3
|
+
const vueConfigUpdates = require('./vueConfigUpdates');
|
|
4
|
+
const vueSyntaxUpdates = require('./vueSyntaxUpdates');
|
|
5
|
+
const routerUpdates = require('./routerUpdates');
|
|
6
|
+
const eslintUpdates = require('./eslintUpdates');
|
|
7
|
+
const tsUpdates = require('./tsUpdates');
|
|
8
|
+
const stylesUpdates = require('./stylesUpdates');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
packageUpdates,
|
|
12
|
+
nvmUpdates,
|
|
13
|
+
vueConfigUpdates,
|
|
14
|
+
vueSyntaxUpdates,
|
|
15
|
+
routerUpdates,
|
|
16
|
+
eslintUpdates,
|
|
17
|
+
tsUpdates,
|
|
18
|
+
stylesUpdates,
|
|
19
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const glob = require('glob');
|
|
3
|
+
const semver = require('semver');
|
|
4
|
+
const stats = require('../stats');
|
|
5
|
+
const { printContent, writeContent } = require('../utils/content');
|
|
6
|
+
const { nodeRequirement, isDry, isSuggest } = require('../config');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* NVM updates
|
|
10
|
+
* Files: .nvmrc
|
|
11
|
+
*
|
|
12
|
+
* Verify presence of .nvmrc, create one if none, update if any
|
|
13
|
+
*/
|
|
14
|
+
const nvmUpdates = (params) => {
|
|
15
|
+
const files = glob.sync(params.paths || '**/.nvmrc', { ignore: params.ignore });
|
|
16
|
+
const nvmRequirement = 20;
|
|
17
|
+
|
|
18
|
+
if (files.length === 0) {
|
|
19
|
+
// If no .nvmrc files found, create one
|
|
20
|
+
const newFilePath = '.nvmrc';
|
|
21
|
+
|
|
22
|
+
if (!isDry && !isSuggest) {
|
|
23
|
+
fs.writeFileSync(newFilePath, nvmRequirement.toString());
|
|
24
|
+
printContent(newFilePath, `Created ${ newFilePath } with Node version ${ nvmRequirement }`, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
writeContent(newFilePath, nvmRequirement.toString(), '');
|
|
28
|
+
stats.nvmrc.push(newFilePath);
|
|
29
|
+
stats.total.push(newFilePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
files.forEach((file) => {
|
|
33
|
+
if (file) {
|
|
34
|
+
const originalContent = fs.readFileSync(file, 'utf8');
|
|
35
|
+
let content = originalContent;
|
|
36
|
+
const nodeVersionMatch = content.match(/([0-9.x]+)/g);
|
|
37
|
+
const nodeVersion = semver.coerce(nodeVersionMatch[0]);
|
|
38
|
+
|
|
39
|
+
// Ensure node version is up to date
|
|
40
|
+
if (nodeVersion && semver.lt(nodeVersion, semver.coerce(nodeRequirement))) {
|
|
41
|
+
printContent(file, `Updating node ${ [nodeVersionMatch[0], nvmRequirement] }`);
|
|
42
|
+
content = content.replaceAll(nodeVersionMatch[0], nvmRequirement);
|
|
43
|
+
writeContent(file, content, originalContent);
|
|
44
|
+
stats.nvmrc.push(file);
|
|
45
|
+
stats.total.push(file);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = nvmUpdates;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('glob');
|
|
4
|
+
const semver = require('semver');
|
|
5
|
+
const stats = require('../stats');
|
|
6
|
+
const { nodeRequirement, removePlaceholder } = require('../config');
|
|
7
|
+
const { writeContent, printContent } = require('../utils/content');
|
|
8
|
+
|
|
9
|
+
function packageUpdates(params) {
|
|
10
|
+
const files = glob.sync(params.paths || '**/package.json', { ignore: params.ignore });
|
|
11
|
+
|
|
12
|
+
files.forEach((file) => {
|
|
13
|
+
const originalContent = fs.readFileSync(file, 'utf8');
|
|
14
|
+
let content = originalContent;
|
|
15
|
+
|
|
16
|
+
const [librariesContent, replaceLibraries] = packageUpdatesLibraries(file, content);
|
|
17
|
+
|
|
18
|
+
if (replaceLibraries.length) {
|
|
19
|
+
content = librariesContent;
|
|
20
|
+
printContent(file, `Updating libraries`, replaceLibraries);
|
|
21
|
+
stats.libraries.push(file);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [nodeContent, replaceNode] = packageUpdatesEngine(file, content);
|
|
25
|
+
|
|
26
|
+
if (replaceNode.length) {
|
|
27
|
+
content = nodeContent;
|
|
28
|
+
printContent(file, `Updating node engine`, replaceNode);
|
|
29
|
+
stats.node.push(file);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [resolutionContent, replaceResolution] = packageUpdatesResolution(file, content);
|
|
33
|
+
|
|
34
|
+
if (replaceResolution.length) {
|
|
35
|
+
content = resolutionContent;
|
|
36
|
+
printContent(file, `Updating resolutions`, replaceResolution);
|
|
37
|
+
stats.libraries.push(file);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [annotationsContent, annotationsChanges] = packageUpdatesAnnotations(file, content);
|
|
41
|
+
|
|
42
|
+
if (annotationsChanges.length) {
|
|
43
|
+
content = annotationsContent;
|
|
44
|
+
printContent(file, `Updating annotations`, annotationsChanges);
|
|
45
|
+
stats.annotations = stats.annotations || [];
|
|
46
|
+
stats.annotations.push(file);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
replaceLibraries.length ||
|
|
51
|
+
replaceNode.length ||
|
|
52
|
+
replaceResolution.length ||
|
|
53
|
+
annotationsChanges.length
|
|
54
|
+
) {
|
|
55
|
+
writeContent(file, content, originalContent);
|
|
56
|
+
stats.total.push(file);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function packageUpdatesLibraries(file, oldContent) {
|
|
62
|
+
let content = oldContent;
|
|
63
|
+
let parsedJson = JSON.parse(content);
|
|
64
|
+
const replaceLibraries = [];
|
|
65
|
+
const types = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
66
|
+
|
|
67
|
+
// [Library name, new version or new library, new library version]
|
|
68
|
+
const librariesUpdates = [
|
|
69
|
+
['@rancher/shell', '^3.0.0'],
|
|
70
|
+
['@rancher/components', '^0.3.0-alpha.1'],
|
|
71
|
+
['@nuxt/babel-preset-app', removePlaceholder],
|
|
72
|
+
['@types/jest', '^29.5.2'],
|
|
73
|
+
['@typescript-eslint/eslint-plugin', '~5.4.0'],
|
|
74
|
+
['@typescript-eslint/parser', '~5.4.0'],
|
|
75
|
+
['@vue/cli-plugin-babel', '~5.0.0'],
|
|
76
|
+
['@vue/cli-plugin-e2e-cypress', '~5.0.0'],
|
|
77
|
+
['@vue/cli-plugin-eslint', '~5.0.0'],
|
|
78
|
+
['@vue/cli-plugin-router', '~5.0.0'],
|
|
79
|
+
['@vue/cli-plugin-typescript', '~5.0.0'],
|
|
80
|
+
['@vue/cli-plugin-unit-jest', '~5.0.0'],
|
|
81
|
+
['@vue/cli-plugin-vuex', '~5.0.0'],
|
|
82
|
+
['@vue/cli-service', '~5.0.0'],
|
|
83
|
+
['@vue/eslint-config-typescript', '~9.1.0'],
|
|
84
|
+
['@vue/vue2-jest', '@vue/vue3-jest', '^27.0.0-alpha.1'],
|
|
85
|
+
['@vue/test-utils', '~2.0.0-0'],
|
|
86
|
+
['core-js', '3.25.3'],
|
|
87
|
+
['cache-loader', '^4.1.0'],
|
|
88
|
+
['node-polyfill-webpack-plugin', '^3.0.0'],
|
|
89
|
+
['portal-vue', '~3.0.0'],
|
|
90
|
+
['require-extension-hooks-babel', '1.0.0'],
|
|
91
|
+
['require-extension-hooks-vue', '3.0.0'],
|
|
92
|
+
['require-extension-hooks', '0.3.3'],
|
|
93
|
+
['sass-loader', '~12.0.0'],
|
|
94
|
+
['typescript', '~4.5.5'],
|
|
95
|
+
['vue-router', '~4.0.3'],
|
|
96
|
+
['vue-virtual-scroll-list', 'vue3-virtual-scroll-list', '0.2.1'],
|
|
97
|
+
['vue', '~3.2.13'],
|
|
98
|
+
['vuex', '~4.0.0'],
|
|
99
|
+
['xterm', '5.2.1'],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
types.forEach((type) => {
|
|
103
|
+
if (parsedJson[type]) {
|
|
104
|
+
librariesUpdates.forEach(([library, newVersion, newLibraryVersion]) => {
|
|
105
|
+
if (parsedJson[type][library]) {
|
|
106
|
+
const version = semver.coerce(parsedJson[type][library]);
|
|
107
|
+
|
|
108
|
+
if (newVersion === removePlaceholder) {
|
|
109
|
+
// Remove library
|
|
110
|
+
replaceLibraries.push([library, [parsedJson[type][library], removePlaceholder]]);
|
|
111
|
+
delete parsedJson[type][library];
|
|
112
|
+
content = JSON.stringify(parsedJson, null, 2);
|
|
113
|
+
} else if (newLibraryVersion) {
|
|
114
|
+
// Replace with a new library
|
|
115
|
+
replaceLibraries.push([library, [parsedJson[type][library], newVersion, newLibraryVersion]]);
|
|
116
|
+
content = content.replace(
|
|
117
|
+
`"${ library }": "${ parsedJson[type][library] }"`,
|
|
118
|
+
`"${ newVersion }": "${ newLibraryVersion }"`
|
|
119
|
+
);
|
|
120
|
+
parsedJson = JSON.parse(content);
|
|
121
|
+
} else if (version && semver.lt(version, semver.coerce(newVersion))) {
|
|
122
|
+
// Update library version if outdated
|
|
123
|
+
replaceLibraries.push([library, [parsedJson[type][library], newVersion]]);
|
|
124
|
+
content = content.replace(
|
|
125
|
+
`"${ library }": "${ parsedJson[type][library] }"`,
|
|
126
|
+
`"${ library }": "${ newVersion }"`
|
|
127
|
+
);
|
|
128
|
+
parsedJson = JSON.parse(content);
|
|
129
|
+
}
|
|
130
|
+
} else if (newLibraryVersion && library === newVersion) {
|
|
131
|
+
// Add new library if it doesn't exist
|
|
132
|
+
parsedJson[type][library] = newLibraryVersion;
|
|
133
|
+
replaceLibraries.push([library, [null, newLibraryVersion]]);
|
|
134
|
+
content = JSON.stringify(parsedJson, null, 2);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return [content, replaceLibraries];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function packageUpdatesEngine(file, oldContent) {
|
|
144
|
+
let content = oldContent;
|
|
145
|
+
let parsedJson = JSON.parse(content);
|
|
146
|
+
const replaceNode = [];
|
|
147
|
+
|
|
148
|
+
if (parsedJson.engines) {
|
|
149
|
+
const outdated = semver.lt(semver.coerce(parsedJson.engines.node), semver.coerce(nodeRequirement));
|
|
150
|
+
|
|
151
|
+
if (outdated) {
|
|
152
|
+
replaceNode.push([parsedJson.engines.node, nodeRequirement]);
|
|
153
|
+
content = content.replace(
|
|
154
|
+
`"node": "${ parsedJson.engines.node }"`,
|
|
155
|
+
`"node": ">=${ nodeRequirement }"`
|
|
156
|
+
);
|
|
157
|
+
parsedJson = JSON.parse(content);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return [content, replaceNode];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function packageUpdatesResolution(file, oldContent) {
|
|
165
|
+
let content = oldContent;
|
|
166
|
+
let parsedJson = JSON.parse(content);
|
|
167
|
+
const replaceResolution = [];
|
|
168
|
+
const resolutions = [
|
|
169
|
+
['@vue/cli-service/html-webpack-plugin', '^5.0.0'],
|
|
170
|
+
['**/webpack', removePlaceholder],
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
if (parsedJson.resolutions) {
|
|
174
|
+
resolutions.forEach(([library, newVersion]) => {
|
|
175
|
+
if (newVersion === removePlaceholder) {
|
|
176
|
+
delete parsedJson.resolutions[library];
|
|
177
|
+
content = JSON.stringify(parsedJson, null, 2);
|
|
178
|
+
parsedJson = JSON.parse(content);
|
|
179
|
+
} else if (!parsedJson.resolutions[library]) {
|
|
180
|
+
parsedJson.resolutions[library] = newVersion;
|
|
181
|
+
content = JSON.stringify(parsedJson, null, 2);
|
|
182
|
+
parsedJson = JSON.parse(content);
|
|
183
|
+
} else {
|
|
184
|
+
const outdated = semver.lt(
|
|
185
|
+
semver.coerce(parsedJson.resolutions[library]),
|
|
186
|
+
semver.coerce(newVersion)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (outdated) {
|
|
190
|
+
replaceResolution.push([parsedJson.engines.node, nodeRequirement]);
|
|
191
|
+
content = content.replace(
|
|
192
|
+
`"${ library }": "${ parsedJson.resolutions[library] }"`,
|
|
193
|
+
`"${ library }": "${ newVersion }"`
|
|
194
|
+
);
|
|
195
|
+
parsedJson = JSON.parse(content);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return [content, replaceResolution];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function packageUpdatesAnnotations(file, oldContent) {
|
|
205
|
+
let content = oldContent;
|
|
206
|
+
const parsedJson = JSON.parse(content);
|
|
207
|
+
const changesMade = [];
|
|
208
|
+
|
|
209
|
+
// Check if the file is in pkg/*/package.json
|
|
210
|
+
const dirName = path.dirname(file); // e.g., 'pkg/extension-name'
|
|
211
|
+
const parentDirName = path.basename(path.dirname(dirName)); // Should be 'pkg'
|
|
212
|
+
|
|
213
|
+
if (parentDirName === 'pkg') {
|
|
214
|
+
// The file is in pkg/<extension-name>/package.json
|
|
215
|
+
const annotations = {
|
|
216
|
+
'catalog.cattle.io/rancher-version': '>= 2.10.0-0',
|
|
217
|
+
'catalog.cattle.io/ui-extension-version': '>= 3.0.0',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (!parsedJson.rancher) {
|
|
221
|
+
parsedJson.rancher = { annotations };
|
|
222
|
+
changesMade.push('Added rancher annotations');
|
|
223
|
+
} else if (!parsedJson.rancher.annotations) {
|
|
224
|
+
parsedJson.rancher.annotations = annotations;
|
|
225
|
+
changesMade.push('Added rancher annotations');
|
|
226
|
+
} else {
|
|
227
|
+
// Merge existing annotations with the new ones
|
|
228
|
+
parsedJson.rancher.annotations = {
|
|
229
|
+
...parsedJson.rancher.annotations,
|
|
230
|
+
...annotations,
|
|
231
|
+
};
|
|
232
|
+
changesMade.push('Updated rancher annotations');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Update content
|
|
236
|
+
content = JSON.stringify(parsedJson, null, 2);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return [content, changesMade];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = packageUpdates;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const glob = require('glob');
|
|
2
|
+
const { replaceCases } = require('../utils/content');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vue Router
|
|
6
|
+
* Files: .vue, .js, .ts
|
|
7
|
+
*/
|
|
8
|
+
const routerUpdates = (params) => {
|
|
9
|
+
const files = glob.sync(params.paths || '**/*.{vue,js,ts}', { ignore: params.ignore });
|
|
10
|
+
const replacementCases = [
|
|
11
|
+
[`import Router from 'vue-router'`, `import { createRouter } from 'vue-router'`, 'Ensure correct import of createRouter'],
|
|
12
|
+
[`vueApp.use(Router)`, `const router = createRouter({})`, 'Update router initialization'],
|
|
13
|
+
[`Vue.use(Router)`, `const router = createRouter({})`, 'Update router initialization'],
|
|
14
|
+
|
|
15
|
+
[/import\s*\{([^}]*)\s* RouteConfig\s*([^}]*)\}\s*from\s*'vue-router'/g, (match, before, after) => `import {${ before.trim() } RouteRecordRaw ${ after.trim() }} from 'vue-router'`, 'Update RouteConfig to RouteRecordRaw'],
|
|
16
|
+
[/import\s*\{([^}]*)\s* Location\s*([^}]*)\}\s*from\s*'vue-router'/g, (match, before, after) => `import {${ before.trim() } RouteLocation ${ after.trim() }} from 'vue-router'`, 'Update Location to RouteLocation'],
|
|
17
|
+
['mode: \'history\'', 'history: createWebHistory()', 'Update router mode to history'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
replaceCases('router', files, replacementCases, `Updating Vue Router`);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = routerUpdates;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const glob = require('glob');
|
|
2
|
+
const { replaceCases } = require('../utils/content');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Update styles (e.g., replace ::v-deep with :deep)
|
|
6
|
+
*/
|
|
7
|
+
const stylesUpdates = (params) => {
|
|
8
|
+
const files = glob.sync(params.paths || '**/*.{vue,scss,css}', { ignore: params.ignore });
|
|
9
|
+
const cases = [
|
|
10
|
+
// Replace '::v-deep' without parentheses with ':deep()'
|
|
11
|
+
[/::v-deep(?!\()/g, ':deep()', 'Replace ::v-deep with :deep()'],
|
|
12
|
+
// Replace '::v-deep(' with ':deep('
|
|
13
|
+
[/::v-deep\(/g, ':deep(', 'Replace ::v-deep( with :deep('],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
replaceCases('style', files, cases, `Updating styles`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = stylesUpdates;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { isSuggest } = require('../config');
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
/**
|
|
5
|
+
* TS Updates
|
|
6
|
+
* Files: tsconfig*.json
|
|
7
|
+
*
|
|
8
|
+
* Add information about TS issues, recommend @ts-nocheck as temporary solution
|
|
9
|
+
*/
|
|
10
|
+
const tsUpdates = (params) => {
|
|
11
|
+
if (!isSuggest) {
|
|
12
|
+
console.warn('TS checks are stricter and may require to be fixed manually.',
|
|
13
|
+
'Use @ts-nocheck to give you time to fix them.',
|
|
14
|
+
'Add exception to your ESLint config to avoid linting errors.');
|
|
15
|
+
}
|
|
16
|
+
// TODO: Add case
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = tsUpdates;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const glob = require('glob');
|
|
3
|
+
const stats = require('../stats');
|
|
4
|
+
const { writeContent } = require('../utils/content');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vue config update
|
|
8
|
+
* Files: vue.config.js
|
|
9
|
+
*
|
|
10
|
+
* Verify vue.config presence of deprecated Webpack5 options
|
|
11
|
+
* - devServer.public: 'path' -> client: { webSocketURL: 'path' }
|
|
12
|
+
*/
|
|
13
|
+
const vueConfigUpdates = (params) => {
|
|
14
|
+
const files = glob.sync(params.paths || 'vue.config**.js', { ignore: params.ignore });
|
|
15
|
+
|
|
16
|
+
files.forEach((file) => {
|
|
17
|
+
const originalContent = fs.readFileSync(file, 'utf8');
|
|
18
|
+
const content = originalContent;
|
|
19
|
+
|
|
20
|
+
// Verify vue.config presence of deprecated Webpack5 options
|
|
21
|
+
if (content.includes('devServer.public: \'path\'')) {
|
|
22
|
+
writeContent(file, content, originalContent);
|
|
23
|
+
stats.webpack.push(file);
|
|
24
|
+
stats.total.push(file);
|
|
25
|
+
// TODO: Add replacement
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = vueConfigUpdates;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const glob = require('glob');
|
|
2
|
+
const { replaceCases } = require('../utils/content');
|
|
3
|
+
const {
|
|
4
|
+
vueSetReplacement, vueDeleteReplacement, vueKeyReplacement, vueTemplateKeyReplacement, vueTemplateKeyRemoval
|
|
5
|
+
} = require('../utils/vueSyntax');
|
|
6
|
+
|
|
7
|
+
function vueSyntaxUpdates(params) {
|
|
8
|
+
const files = glob.sync(
|
|
9
|
+
params.paths || '**/*.{vue,js,ts}',
|
|
10
|
+
{
|
|
11
|
+
ignore: [
|
|
12
|
+
...params.ignore,
|
|
13
|
+
'**/*.spec.ts',
|
|
14
|
+
'**/*.spec.js',
|
|
15
|
+
'**/__tests__/**',
|
|
16
|
+
'**/*.test.ts',
|
|
17
|
+
'jest.setup.js',
|
|
18
|
+
'**/*.d.ts',
|
|
19
|
+
'**/vue-shim.ts',
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const replacementCases = [
|
|
25
|
+
// Handle Vue.set and this.$set
|
|
26
|
+
[/\bVue\.set\(([^,]+),\s*([^,]+),\s*([^)]+)\)/g, (match, obj, prop, val) => vueSetReplacement(match, obj, prop, val), 'Replace Vue.set with direct assignment - https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
27
|
+
[/\bthis\.\$set\(([^,]+),\s*([^,]+),\s*([^)]+)\)/g, (match, obj, prop, val) => vueSetReplacement(match, obj, prop, val), 'Replace this.$set with direct assignment - https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
28
|
+
|
|
29
|
+
// Handle Vue.delete and this.$delete
|
|
30
|
+
[/\bVue\.delete\(([^,]+),\s*([^)]+)\)/g, (match, obj, prop) => vueDeleteReplacement(match, obj, prop), 'Replace Vue.delete with delete operator - https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
31
|
+
[/\bthis\.\$delete\(([^,]+),\s*([^)]+)\)/g, (match, obj, prop) => vueDeleteReplacement(match, obj, prop), 'Replace this.$delete with delete operator - https://vuejs.org/guide/extras/reactivity-in-depth.html'],
|
|
32
|
+
|
|
33
|
+
// Replace Vue import with createApp and initialize vueApp
|
|
34
|
+
[/import Vue from 'vue';?/g, `import { createApp } from 'vue';\nconst vueApp = createApp({});`, 'Replace Vue import with createApp - https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp'],
|
|
35
|
+
|
|
36
|
+
// Replace new Vue({}) with createApp({})
|
|
37
|
+
[/new Vue\(/g, 'createApp(', 'Replace new Vue with createApp - https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp'],
|
|
38
|
+
|
|
39
|
+
// Replace Vue global methods with vueApp methods
|
|
40
|
+
[/\bVue\.(config|directive|filter|mixin|component|use|prototype)\b/g, (match, method) => `vueApp.${ method }`, 'Replace Vue global methods with vueApp methods - https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp'],
|
|
41
|
+
|
|
42
|
+
// Update Vue.prototype to vueApp.config.globalProperties
|
|
43
|
+
[/Vue\.prototype/g, 'vueApp.config.globalProperties', 'Update Vue.prototype to vueApp.config.globalProperties - https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp'],
|
|
44
|
+
|
|
45
|
+
// Remove Vue.util as it's no longer available
|
|
46
|
+
[/Vue\.util/g, '', 'Vue.util is private and no longer available - https://v3-migration.vuejs.org/migration-build.html#partially-compatible-with-caveats'],
|
|
47
|
+
|
|
48
|
+
// Replace vue-virtual-scroll-list with vue3-virtual-scroll-list
|
|
49
|
+
[`vue-virtual-scroll-list`, `vue3-virtual-scroll-list`, 'library update'],
|
|
50
|
+
|
|
51
|
+
// Update Vue.nextTick
|
|
52
|
+
[/\bVue\.nextTick\b/g, 'nextTick', 'Update Vue.nextTick to nextTick - https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html#global-api-treeshaking'],
|
|
53
|
+
[/\bthis\.nextTick\b/g, 'nextTick', 'Update this.nextTick to nextTick - https://v3-migration.vuejs.org/breaking-changes/global-api-treeshaking.html#global-api-treeshaking'],
|
|
54
|
+
// Note: You may need to import nextTick from 'vue' where used.
|
|
55
|
+
|
|
56
|
+
// Update props default function context
|
|
57
|
+
[/(default)\(\)\s*\{([\s\S]*?)return\s+([\s\S]*?)\}/g, (match, def, middle, retVal) => `${ def }(props) {${ middle }return ${ retVal }}`, 'Update props default function context - https://v3-migration.vuejs.org/breaking-changes/props-default-this.html'],
|
|
58
|
+
|
|
59
|
+
// Replace @input with @update:value (excluding plainInputEvent)
|
|
60
|
+
[/@input="((?!.*plainInputEvent).+?)"/g, (_, handler) => `@update:value="${ handler }"`, 'Update @input to @update:value'],
|
|
61
|
+
|
|
62
|
+
// Update v-model syntax
|
|
63
|
+
[/v-model=/g, 'v-model:value=', 'Update v-model to v-model:value'],
|
|
64
|
+
|
|
65
|
+
// Replace .sync modifier with v-model
|
|
66
|
+
[/:([a-zA-Z0-9_-]+)\.sync=/g, 'v-model:$1=', 'Update .sync modifier to v-model'],
|
|
67
|
+
|
|
68
|
+
// Replace click.native with click
|
|
69
|
+
[/(\b@click)\.native/g, '$1', 'Remove .native modifier from @click'],
|
|
70
|
+
|
|
71
|
+
// Remove v-on="$listeners"
|
|
72
|
+
[/v-on="\$listeners"/g, '', 'Remove v-on="$listeners" as it is no longer needed - https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html'],
|
|
73
|
+
|
|
74
|
+
// Update :listeners="$listeners" to v-bind="$attrs"
|
|
75
|
+
[/:listeners="\$listeners"/g, 'v-bind="$attrs"', 'Update :listeners="$listeners" to v-bind="$attrs" - https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html'],
|
|
76
|
+
|
|
77
|
+
// Update $scopedSlots to $slots
|
|
78
|
+
[/\$scopedSlots/g, '$slots', 'Update $scopedSlots to $slots - https://v3-migration.vuejs.org/breaking-changes/slots-unification.html'],
|
|
79
|
+
|
|
80
|
+
// Update slot-scope to v-slot
|
|
81
|
+
[/slot-scope="([^"]+)"/g, 'v-slot="$1"', 'Update slot-scope to v-slot - https://vuejs.org/guide/components/slots.html#scoped-slots'],
|
|
82
|
+
|
|
83
|
+
// Update this.$slots['name'] to this.$slots.name()
|
|
84
|
+
[/this\.\$slots\['([^']+)'\]/g, `this.$slots[\'$1\']()`, `Update this.$slots['name'] to this.$slots.name() - https://vuejs.org/guide/components/slots.html#scoped-slots`],
|
|
85
|
+
|
|
86
|
+
// Remove portal-vue components (now use Teleport)
|
|
87
|
+
[/<\/?portal(-target)?\b[^>]*>/g, '', 'Remove portal components (use Teleport instead) - https://v3.vuejs.org/guide/teleport.html'],
|
|
88
|
+
|
|
89
|
+
// Add :key to <template v-for> elements if missing
|
|
90
|
+
[
|
|
91
|
+
/(<template\b[^>]*v-for="([^"]*)"[^>]*)(>)/g,
|
|
92
|
+
(match, beforeTagEnd, vForContent, tagClose) => vueKeyReplacement(match, beforeTagEnd, vForContent, tagClose),
|
|
93
|
+
'Add :key to <template v-for> elements if missing, using existing variables',
|
|
94
|
+
],
|
|
95
|
+
|
|
96
|
+
// Move :key from child elements to <template v-for>
|
|
97
|
+
[
|
|
98
|
+
/(<template\b[^>]*v-for="[^"]*"[^>]*>)([\s\S]*?)(<\/template>)/g,
|
|
99
|
+
(match, templateStart, templateContent, templateEnd) => vueTemplateKeyReplacement(match, templateStart, templateContent, templateEnd),
|
|
100
|
+
'Move :key from child elements to <template v-for>',
|
|
101
|
+
],
|
|
102
|
+
|
|
103
|
+
// Remove any remaining :key from child elements within <template v-for>
|
|
104
|
+
[
|
|
105
|
+
/(<template\b[^>]*v-for="[^"]*"[^>]*>)([\s\S]*?)(<\/template>)/g,
|
|
106
|
+
(match, templateStart, templateContent, templateEnd) => vueTemplateKeyRemoval(match, templateStart, templateContent, templateEnd),
|
|
107
|
+
'Remove any remaining :key from child elements within <template v-for>',
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
// For other elements with v-for (excluding <template>), ensure :key is present
|
|
111
|
+
[
|
|
112
|
+
/(<(?!template\b)\w+[^>]*v-for="([^"]*)"[^>]*)(>)/g,
|
|
113
|
+
(match, beforeTagEnd, vForContent, tagClose) => vueKeyReplacement(match, beforeTagEnd, vForContent, tagClose),
|
|
114
|
+
'Add :key to elements with v-for that lack it, using existing variables (excluding <template>)',
|
|
115
|
+
],
|
|
116
|
+
|
|
117
|
+
// Update custom directives hooks
|
|
118
|
+
[/\binserted\s*\(/g, 'mounted(', 'Update inserted hook to mounted - https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
119
|
+
[/\bcomponentUpdated\s*\(/g, 'updated(', 'Update componentUpdated hook to updated - https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
120
|
+
[/\bunbind\b/g, 'unmounted', 'Update unbind hook to unmounted - https://v3-migration.vuejs.org/breaking-changes/custom-directives.html'],
|
|
121
|
+
|
|
122
|
+
// Update hook events
|
|
123
|
+
[/@hook:/g, '@vue:', 'Update @hook: events to @vue: - https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html'],
|
|
124
|
+
|
|
125
|
+
// Remove events API ($on, $off, $once)
|
|
126
|
+
[/\$on\(/g, '', 'Remove $on as the events API has been removed - https://v3-migration.vuejs.org/breaking-changes/events-api.html'],
|
|
127
|
+
[/\$off\(/g, '', 'Remove $off as the events API has been removed - https://v3-migration.vuejs.org/breaking-changes/events-api.html'],
|
|
128
|
+
[/\$once\(/g, '', 'Remove $once as the events API has been removed - https://v3-migration.vuejs.org/breaking-changes/events-api.html'],
|
|
129
|
+
|
|
130
|
+
// Update Vuex store creation
|
|
131
|
+
[/new Vuex\.Store\(/g, 'createStore(', 'Update Vuex store creation - https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#installation-process'],
|
|
132
|
+
[/import Vuex from 'vuex'/g, `import { createStore } from 'vuex'`, 'Update Vuex import - https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#installation-process'],
|
|
133
|
+
|
|
134
|
+
// Replace n-link with router-link
|
|
135
|
+
[/<\/?n-link(\s|>)/g, (match) => match.replace('n-link', 'router-link'), 'Replace n-link with router-link'],
|
|
136
|
+
|
|
137
|
+
// Replace v-popover with VDropdown
|
|
138
|
+
[/\bv-popover\b/g, 'VDropdown', 'Replace v-popover with VDropdown'],
|
|
139
|
+
[/<template\s+#popover>/g, '<template #popper>', 'Update slot name from #popover to #popper'],
|
|
140
|
+
|
|
141
|
+
// Extra cases TBD (it seems like we already use the suggested way for arrays)
|
|
142
|
+
// watch option used on arrays not triggered by mutations - https://v3-migration.vuejs.org/breaking-changes/watch.html
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
replaceCases('vueSyntax', files, replacementCases, 'Updating Vue syntax', params);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = vueSyntaxUpdates;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { createPatch } = require('diff');
|
|
4
|
+
const stats = require('../stats');
|
|
5
|
+
const {
|
|
6
|
+
isDry, isSuggest, isVerbose, removePlaceholder
|
|
7
|
+
} = require('../config');
|
|
8
|
+
|
|
9
|
+
const diffOutput = [];
|
|
10
|
+
|
|
11
|
+
function printUsage() {
|
|
12
|
+
console.log(`
|
|
13
|
+
Usage: node index.js [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
|
|
17
|
+
--dry Dry Run Mode: Run the script without making any changes to your files.
|
|
18
|
+
--verbose Verbose Output: Enable detailed logging.
|
|
19
|
+
--suggest Suggest Mode: Generate a 'suggested_changes.diff' file with proposed changes.
|
|
20
|
+
--paths=<path> Specify Paths: Limit migration to specific paths or files (accepts glob patterns).
|
|
21
|
+
--ignore=<patterns> Ignore Patterns: Exclude specific files or directories (accepts comma-separated glob patterns).
|
|
22
|
+
--files Output Modified Files: List all files modified during the migration.
|
|
23
|
+
--log Generate Log File: Write detailed migration statistics to 'stats.json'.
|
|
24
|
+
--help, -h Display this help message and exit.
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeContent(file, content, originalContent) {
|
|
29
|
+
if (!isDry && !isSuggest) {
|
|
30
|
+
fs.writeFileSync(file, content);
|
|
31
|
+
} else if (isSuggest) {
|
|
32
|
+
if (typeof originalContent === 'undefined') {
|
|
33
|
+
originalContent = fs.readFileSync(file, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
const diff = createPatch(file, originalContent, content, '', '', { context: 3 });
|
|
36
|
+
|
|
37
|
+
if (diff.trim()) {
|
|
38
|
+
diffOutput.push({ file, diff });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printContent(...args) {
|
|
44
|
+
if (isVerbose) {
|
|
45
|
+
console.log(...args);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function escapeRegExp(string) {
|
|
50
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setParams(params) {
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
const paramKeys = ['paths', 'ignore'];
|
|
56
|
+
|
|
57
|
+
args.forEach((val) => {
|
|
58
|
+
paramKeys.forEach((key) => {
|
|
59
|
+
if (val.startsWith(`--${ key }=`)) {
|
|
60
|
+
const value = val.split('=')[1];
|
|
61
|
+
|
|
62
|
+
if (key === 'ignore') {
|
|
63
|
+
params.ignorePatterns = value.split(',').map((pattern) => pattern.trim());
|
|
64
|
+
} else {
|
|
65
|
+
params[key] = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Add user-specified ignore patterns
|
|
72
|
+
if (params.ignorePatterns.length > 0) {
|
|
73
|
+
params.ignore = params.ignore.concat(params.ignorePatterns);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printLog() {
|
|
78
|
+
if (process.argv.includes('--files')) {
|
|
79
|
+
console.dir(stats, { compact: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const statsCount = Object.entries(stats).reduce(
|
|
83
|
+
(acc, [key, value]) => ({
|
|
84
|
+
...acc,
|
|
85
|
+
[key]: value.length,
|
|
86
|
+
}),
|
|
87
|
+
{}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
console.table(statsCount);
|
|
91
|
+
|
|
92
|
+
if (isSuggest && diffOutput.length > 0) {
|
|
93
|
+
const diffFile = 'suggested_changes.diff';
|
|
94
|
+
let diffContent = '';
|
|
95
|
+
|
|
96
|
+
diffOutput.forEach(({ file, diff }) => {
|
|
97
|
+
diffContent += `--- ${ file }\n+++ ${ file }\n${ diff }\n`;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
fs.writeFileSync(diffFile, diffContent);
|
|
101
|
+
console.log(`\nSuggested changes have been written to ${ diffFile }`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (process.argv.includes('--log')) {
|
|
105
|
+
fs.writeFileSync('stats.json', JSON.stringify(stats, null, 2));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function replaceCases(fileType, files, replacementCases, printText, params) {
|
|
110
|
+
files.forEach((file) => {
|
|
111
|
+
const originalContent = fs.readFileSync(file, 'utf8');
|
|
112
|
+
let content = originalContent;
|
|
113
|
+
const matchedCases = [];
|
|
114
|
+
|
|
115
|
+
replacementCases.forEach(([pattern, replacement, notes]) => {
|
|
116
|
+
let matches = false;
|
|
117
|
+
|
|
118
|
+
if (typeof pattern === 'string') {
|
|
119
|
+
const regex = new RegExp(escapeRegExp(pattern), 'g');
|
|
120
|
+
|
|
121
|
+
if (regex.test(content)) {
|
|
122
|
+
matches = true;
|
|
123
|
+
|
|
124
|
+
// Exclude cases without replacement
|
|
125
|
+
if (replacement) {
|
|
126
|
+
// Remove discontinued functionalities which do not break
|
|
127
|
+
content = content.replace(regex, replacement === removePlaceholder ? '' : replacement);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (pattern.test(content)) {
|
|
131
|
+
matches = true;
|
|
132
|
+
|
|
133
|
+
// Exclude cases without replacement
|
|
134
|
+
if (replacement) {
|
|
135
|
+
content = content.replace(pattern, replacement === removePlaceholder ? '' : replacement);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (matches) {
|
|
140
|
+
matchedCases.push({
|
|
141
|
+
pattern: pattern.toString(),
|
|
142
|
+
replacement,
|
|
143
|
+
notes,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (matchedCases.length) {
|
|
149
|
+
writeContent(file, content, originalContent);
|
|
150
|
+
printContent(file, printText, matchedCases);
|
|
151
|
+
stats[fileType].push(file);
|
|
152
|
+
stats.total.push(file);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
printUsage,
|
|
159
|
+
writeContent,
|
|
160
|
+
printContent,
|
|
161
|
+
escapeRegExp,
|
|
162
|
+
setParams,
|
|
163
|
+
printLog,
|
|
164
|
+
replaceCases
|
|
165
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vue syntax specific utilities
|
|
3
|
+
*/
|
|
4
|
+
const isSimpleIdentifier = (str) => /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(str.trim());
|
|
5
|
+
const isBracketedExpression = (str) => str.trim().startsWith('[') && str.trim().endsWith(']');
|
|
6
|
+
const isStringLiteral = (str) => /^['"].*['"]$/.test(str.trim());
|
|
7
|
+
// Extracts the key expression from a v-for directive.
|
|
8
|
+
const extractKeyExpression = (vForContent) => {
|
|
9
|
+
const vForMatch = vForContent.match(/^\s*\(([^,]+),\s*([^)]+)\)\s+in\s+(.*)$/);
|
|
10
|
+
let keyExpression = null;
|
|
11
|
+
|
|
12
|
+
if (vForMatch) {
|
|
13
|
+
// v-for="(item, key) in items"
|
|
14
|
+
keyExpression = vForMatch[2].trim();
|
|
15
|
+
} else {
|
|
16
|
+
const simpleVForMatch = vForContent.match(/^\s*([^\s]+)\s+in\s+(.*)$/);
|
|
17
|
+
|
|
18
|
+
if (simpleVForMatch) {
|
|
19
|
+
// v-for="item in items"
|
|
20
|
+
// Use 'item' as key if it's a simple identifier
|
|
21
|
+
keyExpression = isSimpleIdentifier(simpleVForMatch[1].trim()) ? simpleVForMatch[1].trim() : null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return keyExpression;
|
|
26
|
+
};
|
|
27
|
+
// Adds the :key attribute to a tag if it doesn't already have one.
|
|
28
|
+
const addKeyAttribute = (tag, keyExpression) => {
|
|
29
|
+
// Add space if necessary
|
|
30
|
+
const space = tag.endsWith(' ') ? '' : ' ';
|
|
31
|
+
|
|
32
|
+
return `${ tag }${ space }:key="${ keyExpression }"`;
|
|
33
|
+
};
|
|
34
|
+
const vueSetReplacement = (match, obj, prop, val) => {
|
|
35
|
+
prop = prop.trim();
|
|
36
|
+
obj = obj.trim();
|
|
37
|
+
val = val.trim();
|
|
38
|
+
|
|
39
|
+
if (isBracketedExpression(prop)) {
|
|
40
|
+
return `${ obj }${ prop } = ${ val }`;
|
|
41
|
+
} else if (isStringLiteral(prop)) {
|
|
42
|
+
return `${ obj }[${ prop }] = ${ val }`;
|
|
43
|
+
} else if (isSimpleIdentifier(prop)) {
|
|
44
|
+
return `${ obj }.${ prop } = ${ val }`;
|
|
45
|
+
} else {
|
|
46
|
+
return `${ obj }[${ prop }] = ${ val }`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const vueDeleteReplacement = (match, obj, prop) => {
|
|
50
|
+
prop = prop.trim();
|
|
51
|
+
obj = obj.trim();
|
|
52
|
+
|
|
53
|
+
if (isBracketedExpression(prop)) {
|
|
54
|
+
return `delete ${ obj }${ prop }`;
|
|
55
|
+
} else if (isStringLiteral(prop)) {
|
|
56
|
+
return `delete ${ obj }[${ prop }]`;
|
|
57
|
+
} else if (isSimpleIdentifier(prop)) {
|
|
58
|
+
return `delete ${ obj }.${ prop }`;
|
|
59
|
+
} else {
|
|
60
|
+
return `delete ${ obj }[${ prop }]`;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const vueKeyReplacement = (match, beforeTagEnd, vForContent, tagClose) => {
|
|
64
|
+
// Check if :key exists in the tag
|
|
65
|
+
if (beforeTagEnd.includes(':key=')) {
|
|
66
|
+
return match; // :key already exists, do not modify
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const keyExpression = extractKeyExpression(vForContent);
|
|
70
|
+
|
|
71
|
+
if (keyExpression) {
|
|
72
|
+
const updatedTag = addKeyAttribute(beforeTagEnd, keyExpression);
|
|
73
|
+
|
|
74
|
+
return `${ updatedTag }${ tagClose }`;
|
|
75
|
+
} else {
|
|
76
|
+
// Cannot safely determine a key, so do not add one
|
|
77
|
+
return match;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const vueTemplateKeyReplacement = (match, templateStart, templateContent, templateEnd) => {
|
|
81
|
+
// Check if :key is on the <template> tag
|
|
82
|
+
const hasKeyOnTemplate = /:key=/.test(templateStart);
|
|
83
|
+
|
|
84
|
+
// Find any :key on direct child elements
|
|
85
|
+
const childWithKeyRegex = /(<\w+[^>]*)(\s+:key="([^"]+)")([^>]*>)/g;
|
|
86
|
+
let updatedContent = templateContent;
|
|
87
|
+
let movedKey = null;
|
|
88
|
+
|
|
89
|
+
updatedContent = updatedContent.replace(childWithKeyRegex, (childMatch, beforeKey, keyAttr, keyValue, afterKey) => {
|
|
90
|
+
if (!hasKeyOnTemplate && !movedKey) {
|
|
91
|
+
// Move the first encountered :key to the template
|
|
92
|
+
movedKey = keyValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Remove :key from child element
|
|
96
|
+
return `${ beforeKey }${ afterKey }`;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!hasKeyOnTemplate && movedKey) {
|
|
100
|
+
// Add :key to the <template> tag
|
|
101
|
+
const updatedTemplateStart = `${ addKeyAttribute(templateStart.replace(/>$/, ''), movedKey) }>`;
|
|
102
|
+
|
|
103
|
+
return `${ updatedTemplateStart }${ updatedContent }${ templateEnd }`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `${ templateStart }${ updatedContent }${ templateEnd }`;
|
|
107
|
+
};
|
|
108
|
+
const vueTemplateKeyRemoval = (match, templateStart, templateContent, templateEnd) => {
|
|
109
|
+
const childWithKeyRegex = /(<\w+[^>]*)(\s+:key="[^"]+")([^>]*>)/g;
|
|
110
|
+
const updatedContent = templateContent.replace(childWithKeyRegex, '$1$3');
|
|
111
|
+
|
|
112
|
+
return `${ templateStart }${ updatedContent }${ templateEnd }`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
isSimpleIdentifier,
|
|
117
|
+
isBracketedExpression,
|
|
118
|
+
isStringLiteral,
|
|
119
|
+
extractKeyExpression,
|
|
120
|
+
addKeyAttribute,
|
|
121
|
+
vueSetReplacement,
|
|
122
|
+
vueDeleteReplacement,
|
|
123
|
+
vueKeyReplacement,
|
|
124
|
+
vueTemplateKeyReplacement,
|
|
125
|
+
vueTemplateKeyRemoval
|
|
126
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rancher/create-extension",
|
|
3
3
|
"description": "Rancher UI Extension generator",
|
|
4
|
-
"version": "1.0.0
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SUSE",
|
|
7
|
-
"packageManager": "yarn@4.
|
|
7
|
+
"packageManager": "yarn@4.5.0",
|
|
8
8
|
"bin": {
|
|
9
9
|
"create-extension": "./init"
|
|
10
10
|
},
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"
|
|
19
|
+
"diff": "^7.0.0",
|
|
20
|
+
"fs-extra": "^10.0.0",
|
|
21
|
+
"glob": "^11.0.0",
|
|
22
|
+
"path": "^0.12.7",
|
|
23
|
+
"semver": "^7.6.3"
|
|
20
24
|
},
|
|
21
25
|
"_pkgs": {
|
|
22
26
|
"core-js": "3.25.3",
|