Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add migrate-v2 command #6810

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/qwik/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"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",
"ts-morph": "23.0.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
Expand Down
49 changes: 49 additions & 0 deletions packages/qwik/src/cli/migrate-v2/rename-import.ts
Original file line number Diff line number Diff line change
@@ -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()}`);
}
});
}
85 changes: 85 additions & 0 deletions packages/qwik/src/cli/migrate-v2/replace-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { basename } from 'path';
import { isBinaryPath } from './tools/binary-extensions';
import { visitNotIgnoredFiles } from './tools/visit-not-ignored-files';
import { readFileSync, writeFileSync } from 'fs';
import { log } from '@clack/prompts';

function updateFileContent(path: string, content: string) {
writeFileSync(path, content);
log.info(`"${path}" has been updated`);
}

export function replacePackage(
oldPackageName: string,
newPackageName: string,
newPackageVersion: string
): void {
replacePackageInDependencies(oldPackageName, newPackageName, newPackageVersion);

replaceMentions(oldPackageName, newPackageName);
}

function replacePackageInDependencies(
oldPackageName: string,
newPackageName: string,
newPackageVersion: 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] = newPackageVersion;
delete deps[oldPackageName];
}
}
updateFileContent(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;
}

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.
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.`
);
}
});
}
60 changes: 60 additions & 0 deletions packages/qwik/src/cli/migrate-v2/run-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { confirm, intro, isCancel, log } from '@clack/prompts';
import type { AppCommand } from '../utils/app-command';
import { bgMagenta, green } from 'kleur/colors';
import { bye } from '../utils/utils';
import { replacePackage } from './replace-package';
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` +
` - "@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({
message: 'Do you want to proceed?',
initialValue: true,
});

if (isCancel(proceed) || !proceed) {
bye();
}

try {
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) {
console.error(error);
throw error;
}
}
Loading