@chameleonmind/vue-migrate
Version:
A CLI tool to help with migrating projects from Vue2 with Vue CLI to Vue2/Vue3 with Vite
514 lines (496 loc) • 15.2 kB
JavaScript
// src/index.ts
import * as p from "@clack/prompts";
// src/helpers/fileHelpers.ts
import * as fs from "fs";
async function getFile(fileName) {
return await new Promise((resolve, reject) => {
try {
const file = fs.readFileSync(process.cwd() + fileName, "utf8");
resolve(file);
} catch (e) {
reject(new Error("File not found"));
}
});
}
async function saveFile(fileName, fileContents) {
return await new Promise((resolve, reject) => {
try {
fs.writeFileSync(process.cwd() + fileName, fileContents);
resolve("OK");
} catch (e) {
reject(e);
}
});
}
async function removeFile(fileName) {
return await new Promise((resolve, reject) => {
try {
if (fs.existsSync(process.cwd() + fileName)) {
fs.unlinkSync(process.cwd() + fileName);
resolve("OK");
}
} catch (e) {
reject(e);
}
});
}
async function copyFile(source, destination) {
return await new Promise((resolve, reject) => {
try {
fs.copyFileSync(process.cwd() + source, process.cwd() + destination);
resolve("OK");
} catch (e) {
reject(e);
}
});
}
// src/helpers/removeOldDependencies.ts
function cleanupDependencies(dependencies) {
const toExclude = ["@vue/cli", "webpack", "webpack-cli", "vue-template-compiler", "sass-loader", "node-sass", "babel-eslint", "core-js"];
return Object.keys(dependencies).reduce((acc, key) => {
if (!toExclude.some((excluded) => key.includes(excluded))) {
acc[key] = dependencies[key];
}
return acc;
}, {});
}
async function removeOldDependencies() {
const file = await getFile("/package.json");
const parsedPackage = JSON.parse(file);
const newPackage = { ...parsedPackage };
newPackage.dependencies = cleanupDependencies(parsedPackage.dependencies);
newPackage.devDependencies = cleanupDependencies(parsedPackage.devDependencies);
if (parsedPackage?.eslintConfig?.parserOptions?.parser === "babel-eslint") {
delete newPackage.eslintConfig.parserOptions.parser;
newPackage.eslintConfig.env.es2022 = true;
}
await saveFile("/package.json", JSON.stringify(newPackage, null, 2));
return {
ok: true
};
}
// src/helpers/addVue2Deps.ts
async function addVue2Deps() {
const file = await getFile("/package.json");
const parsedPackage = JSON.parse(file);
const newPackage = { ...parsedPackage };
newPackage.dependencies = {
...parsedPackage.dependencies,
"@vitejs/plugin-vue": "^1.6.1",
vite: "^2.5.4",
"vite-plugin-vue2": "^2.0.3",
eslint: "^8.37.0",
"eslint-plugin-vue": "^8.7.1"
};
await saveFile("/package.json", JSON.stringify(newPackage, null, 2));
return {
ok: true
};
}
// src/index.ts
import pc from "picocolors";
// src/helpers/dependencyDictionary.ts
var dependencyDictionary = {
vuex: {
name: "vuex",
version: "^4.0.2"
},
"@vue/test-utils": {
name: "@vue/test-utils",
version: "^2.3.2"
},
"vee-validate": {
name: "vee-validate",
version: "^4.8.4"
},
"vue2-datepicker": {
name: "vue-datepicker-next",
version: "^1.0.3"
},
"@vue/vue2-jest": {
name: "@vue/vue3-jest",
version: "^29.2.3"
},
"element-ui": {
name: "element-plus",
version: "^2.3.2"
},
"vue-multiselect": {
name: "vue-multiselect",
version: "^3.0.0-beta.1"
},
"vue-router": {
name: "vue-router",
version: "^4.1.6"
}
};
function replaceDependencies(existingDependencies) {
return Object.entries(existingDependencies).reduce((acc, [key, value]) => {
if (dependencyDictionary[key] !== void 0) {
acc[dependencyDictionary[key].name] = dependencyDictionary[key].version;
} else {
acc[key] = value;
}
return acc;
}, {});
}
// src/helpers/addVueCompatDeps.ts
async function addVueCompatDeps() {
const file = await getFile("/package.json");
const parsedPackage = JSON.parse(file);
const newPackage = { ...parsedPackage };
newPackage.dependencies = {
...parsedPackage.dependencies,
vue: "^3.2.25",
"@vue/compat": "^3.2.47"
};
newPackage.devDependencies = {
...parsedPackage.devDependencies,
"@vitejs/plugin-vue": "^4.0.0",
eslint: "^8.37.0",
"eslint-plugin-vue": "^8.7.1"
// '@vue/compiler-sfc': '^3.1.0'
};
newPackage.dependencies = replaceDependencies(newPackage.dependencies);
newPackage.devDependencies = replaceDependencies(newPackage.devDependencies);
await saveFile("/package.json", JSON.stringify(newPackage, null, 2));
return {
ok: true
};
}
// src/helpers/addViteConfig.ts
import * as fs2 from "fs";
// src/helpers/fileContents.ts
var viteConfigJsVue2 = `
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import { createVuePlugin as vue } from "vite-plugin-vue2";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
}
})
`;
var viteConfigVueCompat = `
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import createVuePlugin from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
vue: '@vue/compat',
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
plugins: [
createVuePlugin({
template: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
]
})
`;
var viteConfigVue3 = `
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
`;
// src/helpers/utils.ts
async function sleep(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
// src/helpers/addViteConfig.ts
async function addViteConfig(mode = "vue2Vite", language = "js") {
const existingViteConfig = fs2.existsSync(`${process.cwd()}/vite.config.${language}`);
if (!existingViteConfig) {
if (mode === "vue2Vite") {
await saveFile(`/vite.config.${language}`, viteConfigJsVue2);
} else if (mode === "vue3CompatVite") {
await saveFile(`/vite.config.${language}`, viteConfigVueCompat);
} else if (mode === "vue3Vite") {
await saveFile(`/vite.config.${language}`, viteConfigVue3);
} else {
await sleep(1e3);
}
await removeFile(`/vue.config.${language}`);
return {
ok: true
};
} else {
await sleep(1e3);
return {
ok: false
};
}
}
// src/helpers/handleIndexFile.ts
import * as fs3 from "fs";
async function handleIndexFile(language = "js") {
if (fs3.existsSync(process.cwd() + "/public/index.html")) {
await copyFile("/public/index.html", "/public/index.html.bak");
await copyFile("/public/index.html", "/index.html");
await removeFile("/public/index.html");
const fileContents = await getFile("/index.html");
const newFileContent = fileContents.replace(/<%= BASE_URL %>/g, "/").replace(/<%=.%>/g, "Put your content here").replace(/<\/body>/g, `<script type="module" src="/src/main.${language}"></script>
</body>`);
await saveFile("/index.html", newFileContent);
return {
ok: true
};
} else {
await sleep(1e3);
return {
ok: false
};
}
}
// src/helpers/handleRunScripts.ts
async function handleRunScripts() {
const packageJson = await getFile("/package.json");
const parsedPackage = JSON.parse(packageJson);
const newPackage = { ...parsedPackage };
const scripts = { ...parsedPackage.scripts };
const newScripts = Object.entries(scripts).reduce((acc, [key, value]) => {
if (value.includes("vue-cli-service test:unit")) {
acc[key] = value.replace("vue-cli-service test:unit", "jest");
} else if (value.includes("vue-cli-service")) {
acc[key] = value.replace("vue-cli-service", "vite");
} else {
acc[key] = value;
}
return acc;
}, {});
newScripts.dev = "vite";
newPackage.scripts = newScripts;
await saveFile("/package.json", JSON.stringify(newPackage, null, 2));
return {
ok: true
};
}
// src/helpers/handleEnvFiles.ts
import * as fs4 from "fs";
async function handleEnvFiles() {
const envFiles = fs4.readdirSync(process.cwd()).filter((file) => file.startsWith(".env"));
if (envFiles.length > 0) {
for (const element of envFiles) {
const fileContents = await getFile(`/${element}`);
const newFileContent = fileContents.replace(/VUE_APP_/g, "VITE_");
await saveFile(`/${element}`, newFileContent);
}
return {
ok: true
};
}
return {
ok: false
};
}
// src/helpers/addVue3Deps.ts
async function addVue3Deps() {
const file = await getFile("/package.json");
const parsedPackage = JSON.parse(file);
const newPackage = { ...parsedPackage };
newPackage.dependencies = {
...parsedPackage.dependencies,
vue: "^3.2.25"
};
newPackage.devDependencies = {
...parsedPackage.devDependencies,
"@vitejs/plugin-vue": "^4.0.0",
eslint: "^8.37.0",
"eslint-plugin-vue": "^8.7.1"
};
newPackage.dependencies = replaceDependencies(newPackage.dependencies);
newPackage.devDependencies = replaceDependencies(newPackage.devDependencies);
await saveFile("/package.json", JSON.stringify(newPackage, null, 2));
return {
ok: true
};
}
// src/index.ts
var answers = {
mode: "vue2Vite",
language: "js",
packageManager: "npm",
installChoice: false
};
var jobsDone = {
removeOldDependencies: false,
addVue2Deps: false,
addVue3Deps: false,
addVueCompatDeps: false,
addViteConfig: false,
handleIndexFile: false,
handleRunScripts: false,
handleEnvFiles: false
};
function abortMigration() {
p.outro(pc.bgRed("Migration aborted..."));
process.exit(0);
}
await p.group(
{
intro: () => {
console.clear();
p.intro(pc.bgGreen("Welcome to the Vue migration tool"));
},
mode: async () => {
const response = await p.select({
message: "Select a migration mode",
options: [
{
value: "vue2Vite",
label: "Vue 2 with Vue CLI -> Vue 2 + Vite",
hint: "Play safe! Stay on Vue 2, replace Webpack with Vite."
},
{
value: "vue3CompatVite",
label: "Vue 2 with Vue CLI -> Vue 3 Migration build + Vite",
hint: "Recommended. Add Vue 3 migration build and slowly migrate the project to Vue 3 later."
},
{
value: "vue3Vite",
label: "Vue 2 with Vue CLI -> Vue 3 + Vite",
hint: "Feeling adventurous? Migrate to Vue 3 right away!"
}
]
});
answers.mode = response;
if (p.isCancel(response)) {
abortMigration();
}
},
language: async () => {
const response = await p.select({
message: "Do you use JavaScript or TypeScript?",
options: [
{
value: "ts",
label: "TypeScript"
},
{
value: "js",
label: "JavaScript"
}
]
});
answers.language = response;
if (p.isCancel(response)) {
abortMigration();
}
},
// packageManager: async () => {
// const response = await p.select({
// message: 'Which package manager do you use?',
// options: [
// {
// value: 'npm',
// label: 'npm'
// },
// {
// value: 'yarn',
// label: 'yarn'
// },
// {
// value: 'pnpm',
// label: 'pnpm'
// }
// ]
// })
//
// answers.packageManager = response as PackageManager
// if (p.isCancel(response)) {
// abortMigration()
// }
// },
// installChoice: async () => {
// const response = await p.select({
// message: 'Do you want vue-migrate to automatically install new dependencies after?',
// options: [
// {
// value: true,
// label: 'Yes',
// hint: `vue-migrate will run '${answers.packageManager} install' for you.`
// },
// {
// value: false,
// label: 'No',
// hint: 'You can install the dependencies yourself later.'
// }
// ]
// })
//
// answers.installChoice = response as boolean
// },
wait: async () => {
if (Object.values(answers).every((a) => typeof a !== "symbol" && a !== "")) {
const s = p.spinner();
s.start("Updating files and dependencies...");
const { ok: depsStatus } = await removeOldDependencies();
jobsDone.removeOldDependencies = depsStatus;
if (answers.mode === "vue2Vite") {
const { ok: depsStatus2 } = await addVue2Deps();
jobsDone.addVue2Deps = depsStatus2;
} else if (answers.mode === "vue3CompatVite") {
const { ok: depsStatus2 } = await addVueCompatDeps();
jobsDone.addVueCompatDeps = depsStatus2;
} else if (answers.mode === "vue3Vite") {
const { ok: depsStatus2 } = await addVue3Deps();
jobsDone.addVue3Deps = depsStatus2;
}
const { ok: configStatus } = await addViteConfig(answers.mode, answers.language);
jobsDone.addViteConfig = configStatus;
const { ok: indexStatus } = await handleIndexFile(answers.language);
jobsDone.handleIndexFile = indexStatus;
const { ok: scriptsStatus } = await handleRunScripts();
jobsDone.handleRunScripts = scriptsStatus;
const { ok: envStatus } = await handleEnvFiles();
jobsDone.handleEnvFiles = envStatus;
s.stop("Updated files and added new dependencies.");
p.note(
`All done. Don't forget to delete node_modules and package.json and run install command.
You may need to update some dependencies manually.
Here's what was changed:
- Removed old dependencies: ${jobsDone.removeOldDependencies ? pc.green("\u2713") : pc.red("\u2717")}
- Added new dependencies: ${jobsDone.addVue2Deps || jobsDone.addVueCompatDeps || jobsDone.addVue3Deps ? pc.green("\u2713") : pc.red("\u2717")}
- Added vite.config.js: ${jobsDone.addViteConfig ? pc.green("\u2713") : pc.red("\u2717")}
- Updated index.html: ${jobsDone.handleIndexFile ? pc.green("\u2713") : pc.red("\u2717")}
- Updated scripts in package.json: ${jobsDone.handleRunScripts ? pc.green("\u2713") : pc.red("\u2717")}
- Updated environment variables: ${jobsDone.handleEnvFiles ? pc.green("\u2713") : pc.red("\u2717")}
Also, check your scripts in package.json and make sure they work as expected.`
);
p.outro("The boring part of migration complete. Please check the migration guide for further steps: https://v3-migration.vuejs.org/");
}
}
},
{
// On Cancel callback that wraps the group
// So if the user cancels one of the prompts in the group this function will be called
onCancel: () => {
abortMigration();
}
}
);
//# sourceMappingURL=index.js.map