From fbb4fb1a8669fdb15a11904aa1de1aff360f5603 Mon Sep 17 00:00:00 2001 From: Dmitriy Stepanenko Date: Sat, 29 Jun 2024 18:51:48 +0300 Subject: [PATCH 1/3] feat: add migrate-v2 command --- packages/qwik/package.json | 3 +- .../src/cli/migrate-v2/replace-package.ts | 91 ++++++ .../qwik/src/cli/migrate-v2/run-migration.ts | 32 ++ .../cli/migrate-v2/tools/binary-extensions.ts | 277 ++++++++++++++++++ .../tools/visit-not-ignored-files.spec.ts | 56 ++++ .../tools/visit-not-ignored-files.ts | 34 +++ packages/qwik/src/cli/run.ts | 13 + pnpm-lock.yaml | 3 + 8 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 packages/qwik/src/cli/migrate-v2/replace-package.ts create mode 100644 packages/qwik/src/cli/migrate-v2/run-migration.ts create mode 100644 packages/qwik/src/cli/migrate-v2/tools/binary-extensions.ts create mode 100644 packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts create mode 100644 packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts diff --git a/packages/qwik/package.json b/packages/qwik/package.json index 24c8e4f9d8f..0fb922b85a7 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -17,7 +17,8 @@ "image-size": "1.1.1", "kleur": "4.1.5", "prettier": "3.3.3", - "vitest": "2.1.4" + "vitest": "2.1.4", + "ignore": "^5.3.1" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" diff --git a/packages/qwik/src/cli/migrate-v2/replace-package.ts b/packages/qwik/src/cli/migrate-v2/replace-package.ts new file mode 100644 index 00000000000..1f9b9d04ca7 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/replace-package.ts @@ -0,0 +1,91 @@ +import { basename } from 'path'; +import { isBinaryPath } from './tools/binary-extensions'; +import { visitNotIgnoredFiles } from './tools/visit-not-ignored-files'; +import { readFileSync } from 'fs'; +import { log } from '@clack/prompts'; + +function writeFileSync(path: string, content: string) { + log.info(`"${path}" has been updated`); +} + +export function replacePackage( + oldPackageName: string, + newPackageName: string +): void { + replacePackageInDependencies(oldPackageName, newPackageName); + + replaceMentions(oldPackageName, newPackageName); +} + +function replacePackageInDependencies( + oldPackageName: string, + newPackageName: string +) { + visitNotIgnoredFiles('.', (path) => { + if (basename(path) !== 'package.json') { + return; + } + + try { + const packageJson = JSON.parse(readFileSync(path, 'utf-8')); + for (const deps of [ + packageJson.dependencies ?? {}, + packageJson.devDependencies ?? {}, + packageJson.peerDependencies ?? {}, + packageJson.optionalDependencies ?? {}, + ]) { + if (oldPackageName in deps) { + deps[newPackageName] = deps[oldPackageName]; + delete deps[oldPackageName]; + } + } + writeFileSync(path, JSON.stringify(packageJson, null, 2)); + } catch (e) { + console.warn( + `Could not replace ${oldPackageName} with ${newPackageName} in ${path}.` + ); + } + }); +} + + +function replaceMentions( + oldPackageName: string, + newPackageName: string +) { + visitNotIgnoredFiles('.', (path) => { + if (isBinaryPath(path)) { + return; + } + + const ignoredFiles = [ + 'yarn.lock', + 'package-lock.json', + 'pnpm-lock.yaml', + 'bun.lockb', + 'CHANGELOG.md', + ]; + if (ignoredFiles.includes(basename(path))) { + return; + } + + try { + const contents = readFileSync(path, 'utf-8'); + + if (!contents.includes(oldPackageName)) { + return; + } + + writeFileSync( + path, + contents.replace(new RegExp(oldPackageName, 'g'), newPackageName) + ); + } catch { + // Its **probably** ok, contents can be null if the file is too large or + // there was an access exception. + log.warn( + `An error was thrown when trying to update ${path}. If you believe the migration should have updated it, be sure to review the file and open an issue.` + ); + } + }); +} diff --git a/packages/qwik/src/cli/migrate-v2/run-migration.ts b/packages/qwik/src/cli/migrate-v2/run-migration.ts new file mode 100644 index 00000000000..cecebd59383 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/run-migration.ts @@ -0,0 +1,32 @@ +import { confirm, intro, isCancel } from '@clack/prompts'; +import type { AppCommand } from '../utils/app-command'; +import { bgMagenta } from 'kleur/colors'; +import { bye } from '../utils/utils'; +import { replacePackage } from './replace-package'; + +export async function runV2Migration(app: AppCommand) { + intro( + `✨ ${bgMagenta(' This command will migrate your Qwik application from v1 to v2 \n')}` + + `This includes the following: \n` + + // TODO: package names + ` - "@builder.io/qwik", "@builder.io/qwik-city" packages will be rescoped to "@qwik.dev/core" and "@qwik.dev/qwik-city" \n` + + ` - related dependencies will be updated \n` + ); + const proceed = await confirm({ + message: 'Do you want to proceed?', + initialValue: true, + }); + + if (isCancel(proceed) || !proceed) { + bye(); + } + + try { + replacePackage('@builder.io/qwik', '@qwik.dev/qwik'); + replacePackage('@builder.io/qwik-city', '@qwik.dev/city'); + } catch (error) { + console.log(error); + throw error + } + +} diff --git a/packages/qwik/src/cli/migrate-v2/tools/binary-extensions.ts b/packages/qwik/src/cli/migrate-v2/tools/binary-extensions.ts new file mode 100644 index 00000000000..d4aa5d3401b --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/tools/binary-extensions.ts @@ -0,0 +1,277 @@ +import { extname } from 'path'; + +const binaryExtensions = new Set([ + // types originally from https://github.com/sindresorhus/binary-extensions/blob/40e44b510d87a63dcf42300bc8fbcb105f45a61c/binary-extensions.json + '.3dm', + '.3ds', + '.3g2', + '.3gp', + '.7z', + '.a', + '.aac', + '.adp', + '.ai', + '.aif', + '.aiff', + '.als', + '.alz', + '.ape', + '.apk', + '.appimage', + '.ar', + '.arj', + '.asf', + '.au', + '.avi', + '.bak', + '.baml', + '.bh', + '.bin', + '.bk', + '.bmp', + '.btif', + '.bz2', + '.bzip2', + '.cab', + '.caf', + '.cgm', + '.class', + '.cmx', + '.cpio', + '.cr2', + '.cur', + '.dat', + '.dcm', + '.deb', + '.dex', + '.djvu', + '.dll', + '.dmg', + '.dng', + '.doc', + '.docm', + '.docx', + '.dot', + '.dotm', + '.dra', + '.DS_Store', + '.dsk', + '.dts', + '.dtshd', + '.dvb', + '.dwg', + '.dxf', + '.ecelp4800', + '.ecelp7470', + '.ecelp9600', + '.egg', + '.eol', + '.eot', + '.epub', + '.exe', + '.f4v', + '.fbs', + '.fh', + '.fla', + '.flac', + '.flatpak', + '.fli', + '.flv', + '.fpx', + '.fst', + '.fvt', + '.g3', + '.gh', + '.gif', + '.glb', + '.graffle', + '.gz', + '.gzip', + '.h261', + '.h263', + '.h264', + '.icns', + '.ico', + '.ief', + '.img', + '.ipa', + '.iso', + '.jar', + '.jpeg', + '.jpg', + '.jpgv', + '.jpm', + '.jxr', + '.key', + '.keystore', + '.ktx', + '.lha', + '.lib', + '.lvp', + '.lz', + '.lzh', + '.lzma', + '.lzo', + '.m3u', + '.m4a', + '.m4v', + '.mar', + '.mdi', + '.mht', + '.mid', + '.midi', + '.mj2', + '.mka', + '.mkv', + '.mmr', + '.mng', + '.mobi', + '.mov', + '.movie', + '.mp3', + '.mp4', + '.mp4a', + '.mpeg', + '.mpg', + '.mpga', + '.msi', + '.mxu', + '.nef', + '.npx', + '.npy', + '.numbers', + '.nupkg', + '.o', + '.odp', + '.ods', + '.odt', + '.oga', + '.ogg', + '.ogv', + '.otf', + '.ott', + '.pages', + '.pbm', + '.pbf', + '.pcx', + '.pdb', + '.pdf', + '.pea', + '.pgm', + '.pic', + '.pkg', + '.plist', + '.png', + '.pnm', + '.pot', + '.potm', + '.potx', + '.ppa', + '.ppam', + '.ppm', + '.pps', + '.ppsm', + '.ppsx', + '.ppt', + '.pptm', + '.pptx', + '.psd', + '.pxd', + '.pxz', + '.pya', + '.pyc', + '.pyo', + '.pyv', + '.qt', + '.rar', + '.ras', + '.raw', + '.resources', + '.rgb', + '.rip', + '.rlc', + '.rmf', + '.rmvb', + '.rpm', + '.rtf', + '.rz', + '.s3m', + '.s7z', + '.scpt', + '.sgi', + '.shar', + '.snap', + '.sil', + '.sketch', + '.slk', + '.smv', + '.snk', + '.so', + '.stl', + '.suo', + '.sub', + '.swf', + '.tar', + '.tbz', + '.tbz2', + '.tga', + '.tgz', + '.thmx', + '.tif', + '.tiff', + '.tlz', + '.ttc', + '.ttf', + '.txz', + '.udf', + '.uvh', + '.uvi', + '.uvm', + '.uvp', + '.uvs', + '.uvu', + '.viv', + '.vob', + '.war', + '.wav', + '.wax', + '.wbmp', + '.wdp', + '.weba', + '.webm', + '.webp', + '.whl', + '.wim', + '.wm', + '.wma', + '.wmv', + '.wmx', + '.woff', + '.woff2', + '.wrm', + '.wvx', + '.xbm', + '.xif', + '.xla', + '.xlam', + '.xls', + '.xlsb', + '.xlsm', + '.xlsx', + '.xlt', + '.xltm', + '.xltx', + '.xm', + '.xmind', + '.xpi', + '.xpm', + '.xwd', + '.xz', + '.z', + '.zip', + '.zipx', +]); + +export function isBinaryPath(path: string): boolean { + return binaryExtensions.has(extname(path).toLowerCase()); +} diff --git a/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts new file mode 100644 index 00000000000..8c9f85310c5 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts @@ -0,0 +1,56 @@ +import { createTree } from 'nx/src/generators/testing-utils/create-tree'; +import type { Tree } from 'nx/src/generators/tree'; +import { visitNotIgnoredFiles } from './visit-not-ignored-files'; + +describe('visitNotIgnoredFiles', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTree(); + }); + + it('should visit files recursively in a directory', () => { + tree.write('dir/file1.ts', ''); + tree.write('dir/dir2/file2.ts', ''); + + const visitor = jest.fn(); + visitNotIgnoredFiles(tree, 'dir', visitor); + + expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); + expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); + }); + + it('should not visit ignored files in a directory', () => { + tree.write('.gitignore', 'node_modules'); + + tree.write('dir/file1.ts', ''); + tree.write('dir/node_modules/file1.ts', ''); + tree.write('dir/dir2/file2.ts', ''); + + const visitor = jest.fn(); + visitNotIgnoredFiles(tree, 'dir', visitor); + + expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); + expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); + expect(visitor).not.toHaveBeenCalledWith('dir/node_modules/file1.ts'); + }); + + it.each(['', '.', '/', './'])( + 'should be able to visit the root path "%s"', + (dirPath) => { + tree.write('.gitignore', 'node_modules'); + + tree.write('dir/file1.ts', ''); + tree.write('dir/node_modules/file1.ts', ''); + tree.write('dir/dir2/file2.ts', ''); + + const visitor = jest.fn(); + visitNotIgnoredFiles(tree, dirPath, visitor); + + expect(visitor).toHaveBeenCalledWith('.gitignore'); + expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); + expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); + expect(visitor).not.toHaveBeenCalledWith('dir/node_modules/file1.ts'); + } + ); +}); diff --git a/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts new file mode 100644 index 00000000000..eadcd32baa2 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts @@ -0,0 +1,34 @@ +import { existsSync, lstatSync, readFileSync, readdirSync } from 'fs'; +import ignore from 'ignore'; +import { join, relative } from 'path'; + +/** + * Utility to act on all files in a tree that are not ignored by git. + */ +export function visitNotIgnoredFiles( + dirPath: string, + visitor: (path: string) => void +): void { + let ig: ReturnType | undefined; + if (existsSync('.gitignore')) { + ig = ignore(); + ig.add('.git'); + ig.add(readFileSync('.gitignore', 'utf-8')); + } + // TODO: test how it works if invoked not from the workspace root + dirPath = relative(process.cwd(), dirPath); + if (dirPath !== '' && ig?.ignores(dirPath)) { + return; + } + for (const child of readdirSync(dirPath)) { + const fullPath = join(dirPath, child); + if (ig?.ignores(fullPath)) { + continue; + } + if (lstatSync(fullPath).isFile()) { + visitor(fullPath); + } else { + visitNotIgnoredFiles(fullPath, visitor); + } + } +} diff --git a/packages/qwik/src/cli/run.ts b/packages/qwik/src/cli/run.ts index fe9ed7f2f9b..507aae9a2cd 100644 --- a/packages/qwik/src/cli/run.ts +++ b/packages/qwik/src/cli/run.ts @@ -7,6 +7,7 @@ import { runJokeCommand } from './joke/run-joke-command'; import { note, panic, pmRunCmd, printHeader, bye } from './utils/utils'; import { runBuildCommand } from './utils/run-build-command'; import { intro, isCancel, select, confirm } from '@clack/prompts'; +import { runV2Migration } from './migrate-v2/run-migration'; const SPACE_TO_HINT = 18; const COMMANDS = [ @@ -45,6 +46,14 @@ const COMMANDS = [ run: () => runJokeCommand(), showInHelp: true, }, + { + value: 'migrate-v2', + label: 'migrate-v2', + // TODO: package names + hint: 'Rescopes the application from @builder.io/* namespace to @qwik.dev/*', + run: (app: AppCommand) => runV2Migration(app), + showInHelp: true, + }, { value: 'help', label: 'help', @@ -98,6 +107,10 @@ async function runCommand(app: AppCommand) { await runJokeCommand(); return; } + case 'migrate-v2': { + await runV2Migration(app); + return; + } case 'version': { printVersion(); return; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f12e1f851ac..d70347fe321 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -565,6 +565,9 @@ importers: '@qwik.dev/dom': specifier: workspace:* version: link:../qwik-dom + ignore: + specifier: ^5.3.1 + version: 5.3.1 image-size: specifier: 1.1.1 version: 1.1.1 From 954f94c361769a8627a764de7333f03b28146b27 Mon Sep 17 00:00:00 2001 From: Dmitriy Stepanenko Date: Sun, 8 Sep 2024 13:12:12 +0300 Subject: [PATCH 2/3] feat: update dependencies --- packages/qwik/package.json | 2 +- .../src/cli/migrate-v2/replace-package.ts | 32 +++++------ .../qwik/src/cli/migrate-v2/run-migration.ts | 21 ++++--- .../tools/visit-not-ignored-files.spec.ts | 56 ------------------- .../tools/visit-not-ignored-files.ts | 12 +--- .../src/cli/migrate-v2/update-dependencies.ts | 26 +++++++++ packages/qwik/src/cli/migrate-v2/versions.ts | 5 ++ packages/qwik/src/cli/run.ts | 4 +- pnpm-lock.yaml | 2 +- 9 files changed, 63 insertions(+), 97 deletions(-) delete mode 100644 packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts create mode 100644 packages/qwik/src/cli/migrate-v2/update-dependencies.ts create mode 100644 packages/qwik/src/cli/migrate-v2/versions.ts diff --git a/packages/qwik/package.json b/packages/qwik/package.json index 0fb922b85a7..9c1b1ca4d01 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -18,7 +18,7 @@ "kleur": "4.1.5", "prettier": "3.3.3", "vitest": "2.1.4", - "ignore": "^5.3.1" + "ignore": "5.3.1" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" diff --git a/packages/qwik/src/cli/migrate-v2/replace-package.ts b/packages/qwik/src/cli/migrate-v2/replace-package.ts index 1f9b9d04ca7..86826e52794 100644 --- a/packages/qwik/src/cli/migrate-v2/replace-package.ts +++ b/packages/qwik/src/cli/migrate-v2/replace-package.ts @@ -1,25 +1,28 @@ import { basename } from 'path'; import { isBinaryPath } from './tools/binary-extensions'; import { visitNotIgnoredFiles } from './tools/visit-not-ignored-files'; -import { readFileSync } from 'fs'; +import { readFileSync, writeFileSync } from 'fs'; import { log } from '@clack/prompts'; -function writeFileSync(path: string, content: string) { +function updateFileContent(path: string, content: string) { + writeFileSync(path, content); log.info(`"${path}" has been updated`); } export function replacePackage( oldPackageName: string, - newPackageName: string + newPackageName: string, + newPackageVersion: string ): void { - replacePackageInDependencies(oldPackageName, newPackageName); + replacePackageInDependencies(oldPackageName, newPackageName, newPackageVersion); replaceMentions(oldPackageName, newPackageName); } function replacePackageInDependencies( oldPackageName: string, - newPackageName: string + newPackageName: string, + newPackageVersion: string ) { visitNotIgnoredFiles('.', (path) => { if (basename(path) !== 'package.json') { @@ -35,24 +38,18 @@ function replacePackageInDependencies( packageJson.optionalDependencies ?? {}, ]) { if (oldPackageName in deps) { - deps[newPackageName] = deps[oldPackageName]; + deps[newPackageName] = newPackageVersion; delete deps[oldPackageName]; } } - writeFileSync(path, JSON.stringify(packageJson, null, 2)); + updateFileContent(path, JSON.stringify(packageJson, null, 2)); } catch (e) { - console.warn( - `Could not replace ${oldPackageName} with ${newPackageName} in ${path}.` - ); + console.warn(`Could not replace ${oldPackageName} with ${newPackageName} in ${path}.`); } }); } - -function replaceMentions( - oldPackageName: string, - newPackageName: string -) { +function replaceMentions(oldPackageName: string, newPackageName: string) { visitNotIgnoredFiles('.', (path) => { if (isBinaryPath(path)) { return; @@ -76,10 +73,7 @@ function replaceMentions( return; } - writeFileSync( - path, - contents.replace(new RegExp(oldPackageName, 'g'), newPackageName) - ); + updateFileContent(path, contents.replace(new RegExp(oldPackageName, 'g'), newPackageName)); } catch { // Its **probably** ok, contents can be null if the file is too large or // there was an access exception. diff --git a/packages/qwik/src/cli/migrate-v2/run-migration.ts b/packages/qwik/src/cli/migrate-v2/run-migration.ts index cecebd59383..f1bbd736a38 100644 --- a/packages/qwik/src/cli/migrate-v2/run-migration.ts +++ b/packages/qwik/src/cli/migrate-v2/run-migration.ts @@ -1,15 +1,17 @@ -import { confirm, intro, isCancel } from '@clack/prompts'; +import { confirm, intro, isCancel, log } from '@clack/prompts'; import type { AppCommand } from '../utils/app-command'; -import { bgMagenta } from 'kleur/colors'; +import { bgMagenta, green } from 'kleur/colors'; import { bye } from '../utils/utils'; import { replacePackage } from './replace-package'; +import { updateDependencies } from './update-dependencies'; +import { versions } from './versions'; export async function runV2Migration(app: AppCommand) { intro( `✨ ${bgMagenta(' This command will migrate your Qwik application from v1 to v2 \n')}` + `This includes the following: \n` + - // TODO: package names - ` - "@builder.io/qwik", "@builder.io/qwik-city" packages will be rescoped to "@qwik.dev/core" and "@qwik.dev/qwik-city" \n` + + // TODO(migrate-v2): package names + ` - "@builder.io/qwik", "@builder.io/qwik-city" packages will be rescoped to "@qwik.dev/core" and "@qwik.dev/city" \n` + ` - related dependencies will be updated \n` ); const proceed = await confirm({ @@ -22,11 +24,12 @@ export async function runV2Migration(app: AppCommand) { } try { - replacePackage('@builder.io/qwik', '@qwik.dev/qwik'); - replacePackage('@builder.io/qwik-city', '@qwik.dev/city'); + replacePackage('@builder.io/qwik-city', '@qwik.dev/city', versions['@qwik.dev/city']); + replacePackage('@builder.io/qwik', '@qwik.dev/qwik', versions['@qwik.dev/qwik']); + await updateDependencies(); + log.success(`${green(`Your application has been successfully migrated to v2!`)}`); } catch (error) { - console.log(error); - throw error + console.error(error); + throw error; } - } diff --git a/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts deleted file mode 100644 index 8c9f85310c5..00000000000 --- a/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createTree } from 'nx/src/generators/testing-utils/create-tree'; -import type { Tree } from 'nx/src/generators/tree'; -import { visitNotIgnoredFiles } from './visit-not-ignored-files'; - -describe('visitNotIgnoredFiles', () => { - let tree: Tree; - - beforeEach(() => { - tree = createTree(); - }); - - it('should visit files recursively in a directory', () => { - tree.write('dir/file1.ts', ''); - tree.write('dir/dir2/file2.ts', ''); - - const visitor = jest.fn(); - visitNotIgnoredFiles(tree, 'dir', visitor); - - expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); - expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); - }); - - it('should not visit ignored files in a directory', () => { - tree.write('.gitignore', 'node_modules'); - - tree.write('dir/file1.ts', ''); - tree.write('dir/node_modules/file1.ts', ''); - tree.write('dir/dir2/file2.ts', ''); - - const visitor = jest.fn(); - visitNotIgnoredFiles(tree, 'dir', visitor); - - expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); - expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); - expect(visitor).not.toHaveBeenCalledWith('dir/node_modules/file1.ts'); - }); - - it.each(['', '.', '/', './'])( - 'should be able to visit the root path "%s"', - (dirPath) => { - tree.write('.gitignore', 'node_modules'); - - tree.write('dir/file1.ts', ''); - tree.write('dir/node_modules/file1.ts', ''); - tree.write('dir/dir2/file2.ts', ''); - - const visitor = jest.fn(); - visitNotIgnoredFiles(tree, dirPath, visitor); - - expect(visitor).toHaveBeenCalledWith('.gitignore'); - expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); - expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); - expect(visitor).not.toHaveBeenCalledWith('dir/node_modules/file1.ts'); - } - ); -}); diff --git a/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts index eadcd32baa2..6312e8f1a1f 100644 --- a/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts +++ b/packages/qwik/src/cli/migrate-v2/tools/visit-not-ignored-files.ts @@ -2,25 +2,19 @@ import { existsSync, lstatSync, readFileSync, readdirSync } from 'fs'; import ignore from 'ignore'; import { join, relative } from 'path'; -/** - * Utility to act on all files in a tree that are not ignored by git. - */ -export function visitNotIgnoredFiles( - dirPath: string, - visitor: (path: string) => void -): void { +/** Utility to act on all files in a tree that are not ignored by git. */ +export function visitNotIgnoredFiles(dirPath: string, visitor: (path: string) => void): void { let ig: ReturnType | undefined; if (existsSync('.gitignore')) { ig = ignore(); ig.add('.git'); ig.add(readFileSync('.gitignore', 'utf-8')); } - // TODO: test how it works if invoked not from the workspace root dirPath = relative(process.cwd(), dirPath); if (dirPath !== '' && ig?.ignores(dirPath)) { return; } - for (const child of readdirSync(dirPath)) { + for (const child of readdirSync(join(process.cwd(), dirPath))) { const fullPath = join(dirPath, child); if (ig?.ignores(fullPath)) { continue; diff --git a/packages/qwik/src/cli/migrate-v2/update-dependencies.ts b/packages/qwik/src/cli/migrate-v2/update-dependencies.ts new file mode 100644 index 00000000000..5312ccd8eb5 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/update-dependencies.ts @@ -0,0 +1,26 @@ +import { readPackageJson, writePackageJson } from './../utils/utils'; +// import { getPackageManager } from './../utils/utils'; +// import { installDeps } from '../utils/install-deps'; +import { versions } from './versions'; + +export async function updateDependencies() { + // TODO(migrate-v2): rely on workspaceRoot instead? + const packageJson = await readPackageJson(process.cwd()); + const devDependencies = (packageJson.devDependencies ??= {}); + + // TODO: this logic should be enhanced to check in both dependencies and devDependencies + for (const key of Object.keys(devDependencies)) { + if (Object.prototype.hasOwnProperty.call(versions, key)) { + // for now only updating existing dependencies if they exist in root package.json + devDependencies[key] = versions[key as unknown as keyof typeof versions]; + } + } + + await writePackageJson(process.cwd(), packageJson); + // TODO(migrate-v2): not installing dependencies because we don't have correct versions set + // const { install } = installDeps(getPackageManager(), process.cwd()); + // const passed = await install; + // if (!passed) { + // throw new Error('Failed to install dependencies'); + // } +} diff --git a/packages/qwik/src/cli/migrate-v2/versions.ts b/packages/qwik/src/cli/migrate-v2/versions.ts new file mode 100644 index 00000000000..80d758c2c5c --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/versions.ts @@ -0,0 +1,5 @@ +export const versions = { + '@qwik.dev/qwik': '2.0.0', + '@qwik.dev/city': '2.0.0', + 'eslint-plugin-qwik': '2.0.0', +}; diff --git a/packages/qwik/src/cli/run.ts b/packages/qwik/src/cli/run.ts index 507aae9a2cd..48ecbedf2c8 100644 --- a/packages/qwik/src/cli/run.ts +++ b/packages/qwik/src/cli/run.ts @@ -49,10 +49,10 @@ const COMMANDS = [ { value: 'migrate-v2', label: 'migrate-v2', - // TODO: package names + // TODO(migrate-v2): package names hint: 'Rescopes the application from @builder.io/* namespace to @qwik.dev/*', run: (app: AppCommand) => runV2Migration(app), - showInHelp: true, + showInHelp: false, }, { value: 'help', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d70347fe321..46dd8352a31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -566,7 +566,7 @@ importers: specifier: workspace:* version: link:../qwik-dom ignore: - specifier: ^5.3.1 + specifier: 5.3.1 version: 5.3.1 image-size: specifier: 1.1.1 From 8b80fe91b3b4000447d4f8ef166b826c0e13f257 Mon Sep 17 00:00:00 2001 From: Dmitriy Stepanenko Date: Sun, 3 Nov 2024 19:32:03 +0200 Subject: [PATCH 3/3] feat: updated package names, added additional migrations --- packages/qwik/package.json | 3 +- .../qwik/src/cli/migrate-v2/rename-import.ts | 49 +++++++++++++++++++ .../qwik/src/cli/migrate-v2/run-migration.ts | 35 +++++++++++-- .../src/cli/migrate-v2/update-dependencies.ts | 44 ++++++++++++----- packages/qwik/src/cli/migrate-v2/versions.ts | 5 +- pnpm-lock.yaml | 33 +++++++++++++ 6 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 packages/qwik/src/cli/migrate-v2/rename-import.ts diff --git a/packages/qwik/package.json b/packages/qwik/package.json index 9c1b1ca4d01..37084ba22a2 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -18,7 +18,8 @@ "kleur": "4.1.5", "prettier": "3.3.3", "vitest": "2.1.4", - "ignore": "5.3.1" + "ignore": "5.3.1", + "ts-morph": "23.0.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" diff --git a/packages/qwik/src/cli/migrate-v2/rename-import.ts b/packages/qwik/src/cli/migrate-v2/rename-import.ts new file mode 100644 index 00000000000..d75d1ba8367 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/rename-import.ts @@ -0,0 +1,49 @@ +import { Project, ts } from 'ts-morph'; +import { visitNotIgnoredFiles } from './tools/visit-not-ignored-files'; +import { log } from '@clack/prompts'; + +export function replaceImportInFiles( + changes: [oldImport: string, newImport: string][], + library: string +) { + const project = new Project(); + + visitNotIgnoredFiles('.', (path) => { + if (!path.endsWith('.ts') && !path.endsWith('.tsx')) { + return; + } + project.addSourceFileAtPath(path); + }); + + project.getSourceFiles().forEach((sourceFile) => { + let hasChanges = false; + + sourceFile.getImportDeclarations().forEach((importDeclaration) => { + // startsWith is used in order to handle nested imports + if (importDeclaration.getModuleSpecifierValue().startsWith(library)) { + for (const [oldImport, newImport] of changes) { + importDeclaration.getNamedImports().forEach((namedImport) => { + if (namedImport.getName() === oldImport) { + namedImport.setName(newImport); + hasChanges = true; + } + }); + } + } + }); + + sourceFile.getDescendantsOfKind(ts.SyntaxKind.Identifier).forEach((identifier) => { + for (const [oldImport, newImport] of changes) { + if (identifier.getText() === oldImport) { + identifier.replaceWithText(newImport); + hasChanges = true; + } + } + }); + + if (hasChanges) { + sourceFile.saveSync(); + log.info(`Updated imports in ${sourceFile.getFilePath()}`); + } + }); +} diff --git a/packages/qwik/src/cli/migrate-v2/run-migration.ts b/packages/qwik/src/cli/migrate-v2/run-migration.ts index f1bbd736a38..7dcdb6c0591 100644 --- a/packages/qwik/src/cli/migrate-v2/run-migration.ts +++ b/packages/qwik/src/cli/migrate-v2/run-migration.ts @@ -3,15 +3,19 @@ import type { AppCommand } from '../utils/app-command'; import { bgMagenta, green } from 'kleur/colors'; import { bye } from '../utils/utils'; import { replacePackage } from './replace-package'; -import { updateDependencies } from './update-dependencies'; +import { + installTsMorph, + removeTsMorphFromPackageJson, + updateDependencies, +} from './update-dependencies'; import { versions } from './versions'; +import { replaceImportInFiles } from './rename-import'; export async function runV2Migration(app: AppCommand) { intro( `✨ ${bgMagenta(' This command will migrate your Qwik application from v1 to v2 \n')}` + `This includes the following: \n` + - // TODO(migrate-v2): package names - ` - "@builder.io/qwik", "@builder.io/qwik-city" packages will be rescoped to "@qwik.dev/core" and "@qwik.dev/city" \n` + + ` - "@builder.io/qwik", "@builder.io/qwik-city" and "@builder.io/qwik-react" packages will be rescoped to "@qwik.dev/core", "@qwik.dev/router" and "@qwik.dev/react" respectively \n` + ` - related dependencies will be updated \n` ); const proceed = await confirm({ @@ -24,8 +28,29 @@ export async function runV2Migration(app: AppCommand) { } try { - replacePackage('@builder.io/qwik-city', '@qwik.dev/city', versions['@qwik.dev/city']); - replacePackage('@builder.io/qwik', '@qwik.dev/qwik', versions['@qwik.dev/qwik']); + const installedTsMorph = await installTsMorph(); + + replaceImportInFiles( + [ + ['QwikCityProvider', 'QwikRouterProvider'], + ['qwikCity', 'qwikRouter'], + ['QwikCityVitePluginOptions', 'QwikRouterVitePluginOptions'], + ['QwikCityPlugin', 'QwikRouterPlugin'], + ['createQwikCity', 'createQwikRouter'], + ['QwikCityNodeRequestOptions', 'QwikRouterNodeRequestOptions'], + ], + '@builder.io/qwik-city' + ); + + replacePackage('@builder.io/qwik-city', '@qwik.dev/router', versions['@qwik.dev/router']); + replacePackage('@builder.io/qwik-react', '@qwik.dev/react', versions['@qwik.dev/react']); + // "@builder.io/qwik" should be the last one because it's name is a substring of the package names above + replacePackage('@builder.io/qwik', '@qwik.dev/core', versions['@qwik.dev/core']); + + if (installedTsMorph) { + await removeTsMorphFromPackageJson(); + } + await updateDependencies(); log.success(`${green(`Your application has been successfully migrated to v2!`)}`); } catch (error) { diff --git a/packages/qwik/src/cli/migrate-v2/update-dependencies.ts b/packages/qwik/src/cli/migrate-v2/update-dependencies.ts index 5312ccd8eb5..7ae96782127 100644 --- a/packages/qwik/src/cli/migrate-v2/update-dependencies.ts +++ b/packages/qwik/src/cli/migrate-v2/update-dependencies.ts @@ -1,26 +1,48 @@ -import { readPackageJson, writePackageJson } from './../utils/utils'; -// import { getPackageManager } from './../utils/utils'; -// import { installDeps } from '../utils/install-deps'; +import { installDeps } from '../utils/install-deps'; +import { getPackageManager, readPackageJson, writePackageJson } from './../utils/utils'; import { versions } from './versions'; export async function updateDependencies() { // TODO(migrate-v2): rely on workspaceRoot instead? const packageJson = await readPackageJson(process.cwd()); const devDependencies = (packageJson.devDependencies ??= {}); + const dependencies = (packageJson.dependencies ??= {}); - // TODO: this logic should be enhanced to check in both dependencies and devDependencies for (const key of Object.keys(devDependencies)) { if (Object.prototype.hasOwnProperty.call(versions, key)) { - // for now only updating existing dependencies if they exist in root package.json devDependencies[key] = versions[key as unknown as keyof typeof versions]; } } + for (const key of Object.keys(dependencies)) { + if (Object.prototype.hasOwnProperty.call(versions, key)) { + dependencies[key] = versions[key as unknown as keyof typeof versions]; + } + } await writePackageJson(process.cwd(), packageJson); - // TODO(migrate-v2): not installing dependencies because we don't have correct versions set - // const { install } = installDeps(getPackageManager(), process.cwd()); - // const passed = await install; - // if (!passed) { - // throw new Error('Failed to install dependencies'); - // } + runInstall(); +} + +export async function installTsMorph() { + const packageJson = await readPackageJson(process.cwd()); + if (packageJson.dependencies?.['ts-morph'] || packageJson.devDependencies?.['ts-morph']) { + return false; + } + (packageJson.devDependencies ??= {})['ts-morph'] = 'latest'; + await runInstall(); + return true; +} + +async function runInstall() { + const { install } = installDeps(getPackageManager(), process.cwd()); + const passed = await install; + if (!passed) { + throw new Error('Failed to install dependencies'); + } +} + +export async function removeTsMorphFromPackageJson() { + const packageJson = await readPackageJson(process.cwd()); + delete packageJson.dependencies?.['ts-morph']; + delete packageJson.devDependencies?.['ts-morph']; } diff --git a/packages/qwik/src/cli/migrate-v2/versions.ts b/packages/qwik/src/cli/migrate-v2/versions.ts index 80d758c2c5c..817643e9ec0 100644 --- a/packages/qwik/src/cli/migrate-v2/versions.ts +++ b/packages/qwik/src/cli/migrate-v2/versions.ts @@ -1,5 +1,6 @@ export const versions = { - '@qwik.dev/qwik': '2.0.0', - '@qwik.dev/city': '2.0.0', + '@qwik.dev/core': '2.0.0', + '@qwik.dev/router': '2.0.0', + '@qwik.dev/react': '2.0.0', 'eslint-plugin-qwik': '2.0.0', }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46dd8352a31..082d115acf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -577,6 +577,9 @@ importers: prettier: specifier: 3.3.3 version: 3.3.3 + ts-morph: + specifier: 23.0.0 + version: 23.0.0 vitest: specifier: 2.1.4 version: 2.1.4(@types/node@20.14.11)(terser@5.36.0) @@ -3136,6 +3139,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@ts-morph/common@0.24.0': + resolution: {integrity: sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -4187,6 +4193,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -7019,6 +7028,11 @@ packages: engines: {node: '>=10'} hasBin: true + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.7.0: resolution: {integrity: sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==} @@ -8880,6 +8894,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@23.0.0: + resolution: {integrity: sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -11970,6 +11987,13 @@ snapshots: '@trysound/sax@0.2.0': {} + '@ts-morph/common@0.24.0': + dependencies: + fast-glob: 3.3.2 + minimatch: 9.0.4 + mkdirp: 3.0.1 + path-browserify: 1.0.1 + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -13245,6 +13269,8 @@ snapshots: clsx@2.1.1: {} + code-block-writer@13.0.3: {} + collapse-white-space@2.1.0: {} color-convert@1.9.3: @@ -16682,6 +16708,8 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mlly@1.7.0: dependencies: acorn: 8.11.3 @@ -18832,6 +18860,11 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@23.0.0: + dependencies: + '@ts-morph/common': 0.24.0 + code-block-writer: 13.0.3 + ts-node@10.9.2(@types/node@20.14.11)(typescript@5.4.5): dependencies: '@cspotcode/source-map-support': 0.8.1