UNPKG

@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
#! /usr/bin/env node // 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