From 37c1e790c70cb448f7771853469a56eea5b313bc Mon Sep 17 00:00:00 2001 From: ShaneK Date: Mon, 20 Jul 2026 11:55:55 -0700 Subject: [PATCH 1/6] feat(migrate): add v9 migration CLI with doctor report and autofixes --- packages/migrate/package-lock.json | 1996 +++++++++++++++++ packages/migrate/package.json | 28 + packages/migrate/src/ast/deps-migration.ts | 135 ++ packages/migrate/src/ast/package-json.ts | 44 + packages/migrate/src/ast/text-scan.ts | 29 + packages/migrate/src/cli.ts | 156 ++ packages/migrate/src/context.ts | 120 + packages/migrate/src/detect.ts | 59 + packages/migrate/src/migrations/index.ts | 32 + .../src/migrations/v9/angular-css-tilde.ts | 46 + .../migrate/src/migrations/v9/angular-deps.ts | 21 + .../v9/angular-standalone-imports.ts | 58 + .../src/migrations/v9/angular-zoneless.ts | 31 + .../src/migrations/v9/core-autocorrect.ts | 28 + .../migrate/src/migrations/v9/core-ion-img.ts | 26 + .../src/migrations/v9/core-legacy-picker.ts | 37 + .../migrate/src/migrations/v9/react-deps.ts | 22 + .../src/migrations/v9/react-router-6-code.ts | 62 + .../migrate/src/migrations/v9/vue-deps.ts | 22 + .../migrations/v9/vue-router-next-guard.ts | 36 + packages/migrate/src/registry.ts | 31 + packages/migrate/src/report.ts | 47 + packages/migrate/src/runner.ts | 52 + packages/migrate/src/types.ts | 49 + packages/migrate/src/versions.ts | 14 + .../migrate/test/angular-css-tilde.test.ts | 37 + .../test/angular-standalone-imports.test.ts | 75 + packages/migrate/test/engine.test.ts | 183 ++ packages/migrate/test/migrations.test.ts | 230 ++ packages/migrate/tsconfig.build.json | 5 + packages/migrate/tsconfig.json | 18 + 31 files changed, 3729 insertions(+) create mode 100644 packages/migrate/package-lock.json create mode 100644 packages/migrate/package.json create mode 100644 packages/migrate/src/ast/deps-migration.ts create mode 100644 packages/migrate/src/ast/package-json.ts create mode 100644 packages/migrate/src/ast/text-scan.ts create mode 100644 packages/migrate/src/cli.ts create mode 100644 packages/migrate/src/context.ts create mode 100644 packages/migrate/src/detect.ts create mode 100644 packages/migrate/src/migrations/index.ts create mode 100644 packages/migrate/src/migrations/v9/angular-css-tilde.ts create mode 100644 packages/migrate/src/migrations/v9/angular-deps.ts create mode 100644 packages/migrate/src/migrations/v9/angular-standalone-imports.ts create mode 100644 packages/migrate/src/migrations/v9/angular-zoneless.ts create mode 100644 packages/migrate/src/migrations/v9/core-autocorrect.ts create mode 100644 packages/migrate/src/migrations/v9/core-ion-img.ts create mode 100644 packages/migrate/src/migrations/v9/core-legacy-picker.ts create mode 100644 packages/migrate/src/migrations/v9/react-deps.ts create mode 100644 packages/migrate/src/migrations/v9/react-router-6-code.ts create mode 100644 packages/migrate/src/migrations/v9/vue-deps.ts create mode 100644 packages/migrate/src/migrations/v9/vue-router-next-guard.ts create mode 100644 packages/migrate/src/registry.ts create mode 100644 packages/migrate/src/report.ts create mode 100644 packages/migrate/src/runner.ts create mode 100644 packages/migrate/src/types.ts create mode 100644 packages/migrate/src/versions.ts create mode 100644 packages/migrate/test/angular-css-tilde.test.ts create mode 100644 packages/migrate/test/angular-standalone-imports.test.ts create mode 100644 packages/migrate/test/engine.test.ts create mode 100644 packages/migrate/test/migrations.test.ts create mode 100644 packages/migrate/tsconfig.build.json create mode 100644 packages/migrate/tsconfig.json diff --git a/packages/migrate/package-lock.json b/packages/migrate/package-lock.json new file mode 100644 index 00000000000..b6ce50204b8 --- /dev/null +++ b/packages/migrate/package-lock.json @@ -0,0 +1,1996 @@ +{ + "name": "@ionic/migrate", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ionic/migrate", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "ts-morph": "^25.0.0" + }, + "bin": { + "ionic-migrate": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.20.1", + "typescript": "^5.7.3", + "vitest": "^3.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ts-morph/common": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.26.1.tgz", + "integrity": "sha512-Sn28TGl/4cFpcM+jwsH1wLncYq3FtN/BIpem+HOygfBWPT5pAeS5dB4VFVzV8FbnOKHpDLZmvAl4AjPEev5idA==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-morph": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz", + "integrity": "sha512-QJEiTdnz1YjrB3JFhd626gX4rKHDLSjSVMvGGG4v7ONc3RBwa0Eei98G9AT9uNFDMtV54JyuXsFeC+OH0n6bXQ==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.26.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/packages/migrate/package.json b/packages/migrate/package.json new file mode 100644 index 00000000000..1822d17a9a4 --- /dev/null +++ b/packages/migrate/package.json @@ -0,0 +1,28 @@ +{ + "name": "@ionic/migrate", + "version": "0.0.0", + "private": true, + "description": "Migration tool to automate and report on breaking changes across Ionic Framework major versions.", + "license": "MIT", + "type": "module", + "bin": { + "ionic-migrate": "./dist/cli.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "test": "vitest run", + "test.watch": "vitest", + "lint": "tsc --noEmit" + }, + "dependencies": { + "ts-morph": "^25.0.0" + }, + "devDependencies": { + "@types/node": "^22.20.1", + "typescript": "^5.7.3", + "vitest": "^3.0.0" + } +} diff --git a/packages/migrate/src/ast/deps-migration.ts b/packages/migrate/src/ast/deps-migration.ts new file mode 100644 index 00000000000..b382e8b7ac4 --- /dev/null +++ b/packages/migrate/src/ast/deps-migration.ts @@ -0,0 +1,135 @@ +import { parseMajor } from '../detect.js'; +import type { PackageJson } from './package-json.js'; +import type { Finding, Framework, Migration } from '../types.js'; +import { findDependency, readPackageJson, setRange, writePackageJson } from './package-json.js'; + +/** + * A bump target: either a major number (raised to `^{major}.0.0`) for normal + * published packages, or an explicit version string (used verbatim) for + * unpublished `@ionic/*` dev builds. + */ +export type BumpTarget = number | string; + +export interface DepsMigrationOptions { + id: string; + framework: Framework; + docsUrl: string; + /** `[packageName, target]` pairs. */ + bumps: [name: string, target: BumpTarget][]; + /** Packages to remove entirely (e.g. obsolete `@types/*`). */ + removes?: string[]; +} + +function targetRange(target: BumpTarget): string { + return typeof target === 'number' ? `^${target}.0.0` : target; +} + +/** + * Ranges that don't express a plain, comparable version and must never be + * rewritten: protocol/alias references (`workspace:`, `catalog:`, `npm:`, + * `file:`, `link:`, `portal:`), git/URL refs, and dist-tags (`latest`, `*`). + * These can embed a digit (`catalog:vue3`, `git+...#v3.4.0`), so a bare + * digit-scan would wrongly treat them as a version. + */ +function isPlainSemverRange(range: string): boolean { + if (/^(workspace|catalog|npm|file|link|portal|git|https?):/.test(range)) return false; + if (/^git\+|:\/\//.test(range)) return false; + // A plain range starts with an optional operator then a number. + return /^\s*[\^~>=<]*\s*\d/.test(range); +} + +/** + * Parse the floor of a plain semver range (`^3.4.1`, `~3.5`, `3`) into + * `[major, minor, patch]`, or `undefined` if it is not a plain range. The + * version must sit at the start (after an optional operator) so protocol/URL + * ranges are rejected rather than mined for a stray digit. + */ +function parseVersion(range: string): [number, number, number] | undefined { + if (!isPlainSemverRange(range)) return undefined; + const m = range.match(/^\s*[\^~>=<]*\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/); + if (!m) return undefined; + return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)]; +} + +/** Whether version `a` is strictly below `b`, compared major.minor.patch. */ +function isBelow(a: [number, number, number], b: [number, number, number]): boolean { + for (let i = 0; i < 3; i++) { + if (a[i] !== b[i]) return a[i] < b[i]; + } + return false; +} + +/** + * Whether a dependency needs changing: + * - numeric target: the installed major must be below it. + * - caret/tilde range target (a minimum floor, e.g. `^3.5.0`): bump only when + * the installed version is below the floor, so a higher pin is never + * downgraded. + * - explicit version string (an unpublished dev build): the range must simply + * differ - a major comparison won't work because a `8.8.x-dev` build of v9 + * still reads as major 8. + */ +function needsChange(pkg: PackageJson, name: string, target: BumpTarget): boolean { + const dep = findDependency(pkg, name); + if (!dep) return false; + // Protocol/alias/URL ranges are never rewritten, whatever the target shape. + if (!isPlainSemverRange(dep.range)) return false; + if (typeof target === 'number') { + return (parseMajor(dep.range) ?? Infinity) < target; + } + if (target.startsWith('^') || target.startsWith('~')) { + const current = parseVersion(dep.range); + return current !== undefined && isBelow(current, parseVersion(target)!); + } + return dep.range !== target; +} + +/** + * Build an auto-fixable migration that raises `package.json` dependency ranges + * and removes obsolete packages. Factoring this out keeps every framework's + * dependency migration to a short, declarative list and guarantees `detect` + * and `fix` stay in lockstep. + */ +export function createDepsMigration(opts: DepsMigrationOptions): Migration { + const { id, framework, docsUrl, bumps, removes = [] } = opts; + + return { + id, + framework, + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl, + + detect(ctx) { + const parsed = readPackageJson(ctx); + if (!parsed) return []; + const findings: Finding[] = []; + for (const [name, target] of bumps) { + if (needsChange(parsed.pkg, name, target)) { + findings.push({ filePath: 'package.json', line: 1, detail: `set ${name} to ${targetRange(target)}` }); + } + } + for (const name of removes) { + if (findDependency(parsed.pkg, name)) { + findings.push({ filePath: 'package.json', line: 1, detail: `remove ${name}` }); + } + } + return findings; + }, + + fix(ctx) { + const parsed = readPackageJson(ctx); + if (!parsed) return; + const { pkg } = parsed; + for (const [name, target] of bumps) { + if (needsChange(pkg, name, target)) setRange(pkg, name, targetRange(target)); + } + for (const name of removes) { + delete pkg.dependencies?.[name]; + delete pkg.devDependencies?.[name]; + } + writePackageJson(ctx, pkg); + }, + }; +} diff --git a/packages/migrate/src/ast/package-json.ts b/packages/migrate/src/ast/package-json.ts new file mode 100644 index 00000000000..75da7bb1054 --- /dev/null +++ b/packages/migrate/src/ast/package-json.ts @@ -0,0 +1,44 @@ +import type { MigrationContext } from '../context.js'; + +export interface PackageJson { + dependencies?: Record; + devDependencies?: Record; + [key: string]: unknown; +} + +/** Read and parse `package.json`, or `undefined` if it does not exist. */ +export function readPackageJson(ctx: MigrationContext): { pkg: PackageJson } | undefined { + const raw = ctx.readFile('package.json'); + if (!raw) return undefined; + try { + return { pkg: JSON.parse(raw) as PackageJson }; + } catch (e) { + throw new Error(`Could not parse package.json in ${ctx.rootDir}: ${(e as Error).message}`); + } +} + +/** Serialize and write `package.json`, keeping 2-space indent and a trailing newline. */ +export function writePackageJson(ctx: MigrationContext, pkg: PackageJson): void { + ctx.writeFile('package.json', `${JSON.stringify(pkg, null, 2)}\n`); +} + +/** Which dependency block a package lives in. */ +export function findDependency( + pkg: PackageJson, + name: string +): { block: 'dependencies' | 'devDependencies'; range: string } | undefined { + if (pkg.dependencies?.[name] !== undefined) { + return { block: 'dependencies', range: pkg.dependencies[name] }; + } + if (pkg.devDependencies?.[name] !== undefined) { + return { block: 'devDependencies', range: pkg.devDependencies[name] }; + } + return undefined; +} + +/** Set `name` to an explicit range/version string in place, if present. */ +export function setRange(pkg: PackageJson, name: string, range: string): void { + const dep = findDependency(pkg, name); + if (!dep) return; + pkg[dep.block]![name] = range; +} diff --git a/packages/migrate/src/ast/text-scan.ts b/packages/migrate/src/ast/text-scan.ts new file mode 100644 index 00000000000..c52c3e5ee8a --- /dev/null +++ b/packages/migrate/src/ast/text-scan.ts @@ -0,0 +1,29 @@ +import type { MigrationContext } from '../context.js'; +import type { Finding } from '../types.js'; + +/** + * Scan matching files line by line, emitting a {@link Finding} for every line + * the `matcher` returns a detail string for. Used by report-only migrations + * where precise locations matter more than AST fidelity. + */ +export function scanLines( + ctx: MigrationContext, + globs: string[], + matcher: (line: string) => string | undefined +): Finding[] { + const findings: Finding[] = []; + for (const filePath of ctx.glob(globs)) { + const text = ctx.readFile(filePath); + if (text === undefined) continue; + text.split('\n').forEach((line, i) => { + const detail = matcher(line); + if (detail !== undefined) { + findings.push({ filePath, line: i + 1, detail }); + } + }); + } + return findings; +} + +/** Common file globs for source that may contain templates/markup. */ +export const SOURCE_GLOBS = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.html', '**/*.vue']; diff --git a/packages/migrate/src/cli.ts b/packages/migrate/src/cli.ts new file mode 100644 index 00000000000..91898816a37 --- /dev/null +++ b/packages/migrate/src/cli.ts @@ -0,0 +1,156 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +import { createDiskContext } from './context.js'; +import { detectFrameworks } from './detect.js'; +import { selectMigrations } from './registry.js'; +import { run } from './runner.js'; +import { buildReport } from './report.js'; +import { allMigrations } from './migrations/index.js'; + +interface Args { + dir: string; + dryRun: boolean; + check: boolean; + experimental: boolean; + force: boolean; + help: boolean; + from?: number; + to?: number; +} + +const USAGE = `Usage: ionic-migrate [dir] [options] + +Detect and apply Ionic Framework breaking-change migrations. With no options, +edits are written in place (commit first - git is your undo). + +Options: + --dry-run Report what would change without writing anything + --check Report only; exit non-zero if any migration applies (for CI) + --experimental Include experimental migrations + --force Write even if the working tree is dirty or not a git repo + --from Override the detected source major version + --to Override the target major version + -h, --help Show this help`; + +/** Parse a `--from`/`--to` value into a finite integer, or exit on bad input. */ +function parseMajorArg(flag: string, value: string | undefined): number { + const n = Number(value); + if (value === undefined || !Number.isInteger(n)) { + console.error(`Invalid value for ${flag}: expected an integer, got ${value ?? '(missing)'}`); + process.exit(1); + } + return n; +} + +function parseArgs(argv: string[]): Args { + const args: Args = { dir: '.', dryRun: false, check: false, experimental: false, force: false, help: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--dry-run') args.dryRun = true; + else if (a === '--check') args.check = true; + else if (a === '--experimental') args.experimental = true; + else if (a === '--force') args.force = true; + else if (a === '--help' || a === '-h') args.help = true; + else if (a === '--from') args.from = parseMajorArg('--from', argv[++i]); + else if (a === '--to') args.to = parseMajorArg('--to', argv[++i]); + else if (a.startsWith('-')) { + console.error(`Unknown option: ${a}\n\n${USAGE}`); + process.exit(1); + } else args.dir = a; + } + return args; +} + +/** Returns true/false for clean/dirty, or null when the dir is not a git repo. */ +function isWorkingTreeClean(dir: string): boolean | null { + try { + const out = execSync('git status --porcelain', { + cwd: dir, + stdio: ['ignore', 'pipe', 'ignore'], + // A repo with very many changed files can exceed the 1 MB default and + // throw, which we'd otherwise misread as "not a git repo". + maxBuffer: 64 * 1024 * 1024, + }); + return out.toString().trim() === ''; + } catch { + return null; + } +} + +function main(): void { + const args = parseArgs(process.argv.slice(2)); + if (args.help) { + console.log(USAGE); + return; + } + const rootDir = resolve(args.dir); + const ctx = createDiskContext(rootDir); + + const detected = detectFrameworks(ctx); + if (detected.length === 0) { + console.log(`No @ionic/{angular,react,vue} dependency found in ${rootDir}. Nothing to do.`); + return; + } + + const detectedMajor = Math.min(...detected.map((d) => d.major)); + // A `--from` below the detected major re-selects migrations the project has + // already had applied. Some (e.g. angular-standalone-imports) are single-shot + // and corrupt already-migrated code if re-run, so require --force to override + // the version gate deliberately. + if (args.from !== undefined && args.from < detectedMajor && !args.force) { + console.error( + `Refusing to run: --from ${args.from} is below the detected major (${detectedMajor}).\n` + + 'Re-running migrations that were already applied can corrupt your code.\n' + + 'Re-run with --force if you are sure.' + ); + process.exit(1); + } + + const fromMajor = args.from ?? detectedMajor; + const toMajor = args.to ?? fromMajor + 1; + const frameworks = detected.map((d) => d.framework); + const migrations = selectMigrations(allMigrations, { + fromMajor, + toMajor, + frameworks, + includeExperimental: args.experimental, + }); + + const label = detected.map((d) => `${d.framework}@${d.major}`).join(', '); + console.log(`Ionic migrate: ${label} (v${fromMajor} -> v${toMajor}, ${migrations.length} migration(s))\n`); + + const writing = !args.dryRun && !args.check; + if (writing && !args.force) { + const clean = isWorkingTreeClean(rootDir); + if (clean === false) { + console.error( + 'Refusing to write: the working tree has uncommitted changes.\n' + + 'Commit or stash first (git is your undo), or re-run with --force.' + ); + process.exit(1); + } + if (clean === null) { + console.error( + `Refusing to write: ${rootDir} is not a git repository, so there is no undo.\n` + + 'Initialize git (or another backup) first, or re-run with --force.' + ); + process.exit(1); + } + } + + const result = run(ctx, migrations, { dryRun: !writing }); + console.log(buildReport(result)); + + if (args.check && result.entries.length > 0) { + process.exit(1); + } +} + +try { + main(); +} catch (e) { + console.error(e instanceof Error ? e.message : String(e)); + process.exit(1); +} diff --git a/packages/migrate/src/context.ts b/packages/migrate/src/context.ts new file mode 100644 index 00000000000..38c4456fd6f --- /dev/null +++ b/packages/migrate/src/context.ts @@ -0,0 +1,120 @@ +import { Project } from 'ts-morph'; + +/** Join a root dir and a relative path using posix separators. */ +function join(root: string, rel: string): string { + return `${root.replace(/\/$/, '')}/${rel.replace(/^\//, '')}`; +} + +/** Build/vendor directories that no glob or source load should descend into. */ +const EXCLUDE_DIRS = ['node_modules', 'dist', 'www', '.angular', '.git']; + +/** Matches a path under any {@link EXCLUDE_DIRS} entry, derived from that list. */ +const EXCLUDE_RE = new RegExp(`(^|/)(${EXCLUDE_DIRS.map((d) => d.replace(/\./g, '\\.')).join('|')})/`); + +/** Negative glob patterns (one per {@link EXCLUDE_DIRS} entry) rooted at `root`. */ +function EXCLUDE_GLOBS(root: string): string[] { + return EXCLUDE_DIRS.map((dir) => `!${join(root, `**/${dir}/**`)}`); +} + +/** + * The surface a migration operates on. TypeScript/TSX transforms use + * {@link project} (ts-morph); config, CSS, HTML, and Vue-SFC edits use + * {@link readFile}/{@link writeFile}. Both are backed by a single filesystem + * (in-memory for tests, real disk for the CLI). Note the two views are not + * auto-synced: ts-morph caches the text of files it has loaded, so a + * {@link writeFile} to a `.ts`/`.tsx` file ts-morph also holds will be clobbered + * by the next {@link save}. Today no migration edits a loaded file both ways, so + * a given file is only ever touched through one view. + */ +export interface MigrationContext { + /** Project root; all relative paths resolve against it. */ + readonly rootDir: string; + /** ts-morph project holding the loaded `.ts`/`.tsx` source files. */ + readonly project: Project; + /** Read a file's text, or `undefined` if it does not exist. */ + readFile(relPath: string): string | undefined; + /** Write a file's text, creating it if needed. */ + writeFile(relPath: string, content: string): void; + /** Return paths (relative to {@link rootDir}) matching the given glob patterns. */ + glob(patterns: string[]): string[]; + /** Convert an absolute path to one relative to {@link rootDir}. */ + relative(absPath: string): string; + /** Persist any pending ts-morph edits to the underlying filesystem. */ + save(): void; +} + +function buildContext(rootDir: string, project: Project): MigrationContext { + const fs = project.getFileSystem(); + return { + rootDir, + project, + readFile(relPath) { + const abs = join(rootDir, relPath); + return fs.fileExistsSync(abs) ? fs.readFileSync(abs) : undefined; + }, + writeFile(relPath, content) { + fs.writeFileSync(join(rootDir, relPath), content); + }, + glob(patterns) { + const prefix = `${rootDir.replace(/\/$/, '')}/`; + // Pass the excludes as negative patterns so the walk is pruned at the + // source (fast-glob honors `!` entries) rather than enumerating all of + // node_modules before discarding it. The post-filter stays as a backstop + // for filesystems whose glob ignores negative patterns (e.g. in-memory). + return fs + .globSync([...patterns.map((p) => join(rootDir, p)), ...EXCLUDE_GLOBS(rootDir)]) + .map((abs) => (abs.startsWith(prefix) ? abs.slice(prefix.length) : abs)) + .filter((rel) => !EXCLUDE_RE.test(rel)); + }, + relative(absPath) { + const prefix = `${rootDir.replace(/\/$/, '')}/`; + return absPath.startsWith(prefix) ? absPath.slice(prefix.length) : absPath; + }, + save() { + project.saveSync(); + }, + }; +} + +/** + * Build a context whose filesystem lives entirely in memory. `.ts`/`.tsx` + * entries are loaded as ts-morph source files; everything else is written as a + * plain file. Used by tests. + */ +export function createInMemoryContext(files: Record, rootDir = '/app'): MigrationContext { + const project = new Project({ useInMemoryFileSystem: true }); + const fs = project.getFileSystem(); + // Write everything to the filesystem so glob/readFile see it, mirroring how + // the disk context works, then load TS sources into ts-morph. + for (const [relPath, content] of Object.entries(files)) { + fs.writeFileSync(join(rootDir, relPath), content); + } + project.addSourceFilesAtPaths([ + join(rootDir, '**/*.ts'), + join(rootDir, '**/*.tsx'), + ...EXCLUDE_GLOBS(rootDir), + ]); + return buildContext(rootDir, project); +} + +/** + * Build a context backed by real files on disk, rooted at `rootDir`. Loads the + * project's TypeScript sources into ts-morph. + */ +export function createDiskContext(rootDir: string): MigrationContext { + const project = new Project({ + skipAddingFilesFromTsConfig: true, + compilerOptions: { allowJs: false }, + }); + // Load the whole tree (minus build/vendor dirs), not just `src/`, so AST + // migrations cover files outside `src/` (Angular multi-project workspaces and + // monorepos keep sources there too). AST migrations are scoped to `.ts`/`.tsx` + // by design (allowJs: false); JS/JSX React projects get the text-scan + // migrations but not the AST-based ones (e.g. react-router-6-code). + project.addSourceFilesAtPaths([ + join(rootDir, '**/*.ts'), + join(rootDir, '**/*.tsx'), + ...EXCLUDE_GLOBS(rootDir), + ]); + return buildContext(rootDir, project); +} diff --git a/packages/migrate/src/detect.ts b/packages/migrate/src/detect.ts new file mode 100644 index 00000000000..0cfce163375 --- /dev/null +++ b/packages/migrate/src/detect.ts @@ -0,0 +1,59 @@ +import type { MigrationContext } from './context.js'; +import type { Framework } from './types.js'; +import { IONIC_V9_VERSION } from './versions.js'; + +/** An Ionic framework binding found in the project, with its installed major. */ +export interface DetectedFramework { + framework: Framework; + major: number; +} + +const FRAMEWORK_PACKAGES: Record, string> = { + angular: '@ionic/angular', + react: '@ionic/react', + vue: '@ionic/vue', +}; + +/** Extract the major version from a semver range like `^8.4.1` or `~9.0.0-rc.1`. */ +export function parseMajor(range: string | undefined): number | undefined { + if (!range) return undefined; + const match = range.match(/(\d+)/); + return match ? Number(match[1]) : undefined; +} + +/** + * Determine which Ionic framework binding(s) a project depends on and the major + * version installed for each, by reading its `package.json`. + */ +export function detectFrameworks(ctx: MigrationContext): DetectedFramework[] { + const pkgText = ctx.readFile('package.json'); + if (!pkgText) return []; + + let pkg: { + dependencies?: Record; + devDependencies?: Record; + }; + try { + pkg = JSON.parse(pkgText); + } catch (e) { + throw new Error(`Could not parse package.json in ${ctx.rootDir}: ${(e as Error).message}`); + } + const deps = { ...pkg.dependencies, ...pkg.devDependencies }; + + const detected: DetectedFramework[] = []; + for (const [framework, pkgName] of Object.entries(FRAMEWORK_PACKAGES) as [ + Exclude, + string, + ][]) { + const range = deps[pkgName]; + // A project already pinned to the v9 dev build reads as major 8 via semver + // (the pin is versioned `8.8.x-dev`), so recognize it explicitly as v9. + // This closes the re-run gate: a migrated project detects as v9 and selects + // no v8->v9 migrations. Remove once the pin becomes `^9.0.0` at GA. + const major = range === IONIC_V9_VERSION ? 9 : parseMajor(range); + if (major !== undefined) { + detected.push({ framework, major }); + } + } + return detected; +} diff --git a/packages/migrate/src/migrations/index.ts b/packages/migrate/src/migrations/index.ts new file mode 100644 index 00000000000..5996a4551b9 --- /dev/null +++ b/packages/migrate/src/migrations/index.ts @@ -0,0 +1,32 @@ +import type { Migration } from '../types.js'; + +import { angularStandaloneImports } from './v9/angular-standalone-imports.js'; +import { angularCssTilde } from './v9/angular-css-tilde.js'; +import { angularZoneless } from './v9/angular-zoneless.js'; +import { angularDeps } from './v9/angular-deps.js'; +import { reactDeps } from './v9/react-deps.js'; +import { reactRouter6Code } from './v9/react-router-6-code.js'; +import { vueDeps } from './v9/vue-deps.js'; +import { vueRouterNextGuard } from './v9/vue-router-next-guard.js'; +import { coreLegacyPicker } from './v9/core-legacy-picker.js'; +import { coreAutocorrect } from './v9/core-autocorrect.js'; +import { coreIonImg } from './v9/core-ion-img.js'; + +/** + * Every migration the tool knows about, across all major versions. To add a + * migration, drop a file under `migrations/v/` and register it here; the + * engine filters by version range and framework at run time. + */ +export const allMigrations: Migration[] = [ + angularStandaloneImports, + angularCssTilde, + angularZoneless, + angularDeps, + reactDeps, + reactRouter6Code, + vueDeps, + vueRouterNextGuard, + coreLegacyPicker, + coreAutocorrect, + coreIonImg, +]; diff --git a/packages/migrate/src/migrations/v9/angular-css-tilde.ts b/packages/migrate/src/migrations/v9/angular-css-tilde.ts new file mode 100644 index 00000000000..973b75cdb6d --- /dev/null +++ b/packages/migrate/src/migrations/v9/angular-css-tilde.ts @@ -0,0 +1,46 @@ +import type { Finding, Migration } from '../../types.js'; + +/** + * Angular's build pipeline no longer supports the webpack-loader `~` prefix in + * CSS `@import` statements for `@ionic/angular` stylesheets: + * + * @import '~@ionic/angular/css/core.css'; -> @import '@ionic/angular/css/core.css'; + * + * See https://ionicframework.com/docs/updating/9-0#css-imports + */ +const TILDE_IONIC_IMPORT = /(@import\s+['"])~(@ionic\/angular\/)/g; + +export const angularCssTilde: Migration = { + id: 'angular-css-tilde', + framework: 'angular', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#css-imports', + + detect(ctx) { + const findings: Finding[] = []; + for (const filePath of ctx.glob(['**/*.css', '**/*.scss'])) { + const text = ctx.readFile(filePath); + if (!text) continue; + text.split('\n').forEach((line, i) => { + // Fresh non-global regex per line: TILDE_IONIC_IMPORT is /g (needed for + // fix's replace), and a global regex's stateful lastIndex makes repeated + // .test() calls alternate true/false and skip matching lines. + if (new RegExp(TILDE_IONIC_IMPORT.source).test(line)) { + findings.push({ filePath, line: i + 1, detail: 'remove `~` prefix from @ionic/angular import' }); + } + }); + } + return findings; + }, + + fix(ctx) { + for (const filePath of ctx.glob(['**/*.css', '**/*.scss'])) { + const text = ctx.readFile(filePath); + if (!text) continue; + const next = text.replace(TILDE_IONIC_IMPORT, '$1$2'); + if (next !== text) ctx.writeFile(filePath, next); + } + }, +}; diff --git a/packages/migrate/src/migrations/v9/angular-deps.ts b/packages/migrate/src/migrations/v9/angular-deps.ts new file mode 100644 index 00000000000..62b56147116 --- /dev/null +++ b/packages/migrate/src/migrations/v9/angular-deps.ts @@ -0,0 +1,21 @@ +import { createDepsMigration } from '../../ast/deps-migration.js'; +import { IONIC_V9_VERSION } from '../../versions.js'; + +/** + * Raise the `@ionic/angular` packages to v9. (The Angular framework version + * itself is the developer's choice via `ng update`, so it is left untouched.) + * Bumping the Ionic packages here also closes the engine's version gate: once + * `package.json` pins the v9 build, `detectFrameworks` reports v9 on a re-run + * and applies nothing. + * + * See https://ionicframework.com/docs/updating/9-0#angular + */ +export const angularDeps = createDepsMigration({ + id: 'angular-deps', + framework: 'angular', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#angular', + bumps: [ + ['@ionic/angular', IONIC_V9_VERSION], + ['@ionic/angular-server', IONIC_V9_VERSION], + ], +}); diff --git a/packages/migrate/src/migrations/v9/angular-standalone-imports.ts b/packages/migrate/src/migrations/v9/angular-standalone-imports.ts new file mode 100644 index 00000000000..a5aa0832b19 --- /dev/null +++ b/packages/migrate/src/migrations/v9/angular-standalone-imports.ts @@ -0,0 +1,58 @@ +import type { Finding, Migration } from '../../types.js'; + +/** Old import specifier -> new import specifier. */ +const REWRITES: Record = { + '@ionic/angular': '@ionic/angular/lazy', + '@ionic/angular/standalone': '@ionic/angular', +}; + +/** + * Ionic 9 makes standalone the default Angular import path: + * - `@ionic/angular` (was lazy) -> `@ionic/angular/lazy` + * - `@ionic/angular/standalone` (was standalone) -> `@ionic/angular` + * + * See https://ionicframework.com/docs/updating/9-0#component-imports + * + * Single-shot: `@ionic/angular` is a v8 lazy import but a v9 standalone import, + * so the two states are textually identical and this migration cannot be + * re-run safely. The migration assumes a v8 starting point; the engine's + * version gate (see `detectFrameworks`, which recognizes the v9 pin) guards + * against re-application after `angular-deps` has bumped `package.json`. + */ +export const angularStandaloneImports: Migration = { + id: 'angular-standalone-imports', + framework: 'angular', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#component-imports', + + detect(ctx) { + const findings: Finding[] = []; + for (const file of ctx.project.getSourceFiles()) { + for (const decl of file.getImportDeclarations()) { + const specifier = decl.getModuleSpecifierValue(); + const target = REWRITES[specifier]; + if (target !== undefined) { + findings.push({ + filePath: ctx.relative(file.getFilePath()), + line: decl.getStartLineNumber(), + detail: `'${specifier}' -> '${target}'`, + }); + } + } + } + return findings; + }, + + fix(ctx) { + for (const file of ctx.project.getSourceFiles()) { + for (const decl of file.getImportDeclarations()) { + const target = REWRITES[decl.getModuleSpecifierValue()]; + if (target !== undefined) { + decl.setModuleSpecifier(target); + } + } + } + }, +}; diff --git a/packages/migrate/src/migrations/v9/angular-zoneless.ts b/packages/migrate/src/migrations/v9/angular-zoneless.ts new file mode 100644 index 00000000000..92cda565203 --- /dev/null +++ b/packages/migrate/src/migrations/v9/angular-zoneless.ts @@ -0,0 +1,31 @@ +import type { Migration } from '../../types.js'; +import { scanLines } from '../../ast/text-scan.js'; + +/** + * Ionic 9 defaults to zoneless change detection (Angular 21+ bootstraps + * zoneless out of the box). Report-only: preserving Zone.js behavior requires + * coordinated, version-dependent edits (add `provideZoneChangeDetection()` at + * the bootstrap AND keep `zone.js` in `angular.json` polyfills). Adding the + * provider without the polyfill errors at runtime, so this flags the bootstrap + * for review rather than risking a half-applied auto-fix. + * + * See https://ionicframework.com/docs/updating/9-0#zoneless-change-detection + */ +const BOOTSTRAP = /\b(bootstrapApplication|bootstrapModule|platformBrowserDynamic)\b/; + +export const angularZoneless: Migration = { + id: 'angular-zoneless', + framework: 'angular', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#zoneless-change-detection', + + detect(ctx) { + return scanLines(ctx, ['**/main.ts', '**/main.tsx'], (line) => + BOOTSTRAP.test(line) + ? 'Ionic 9 is zoneless by default; to keep Zone.js add provideZoneChangeDetection() and keep zone.js in polyfills' + : undefined + ); + }, +}; diff --git a/packages/migrate/src/migrations/v9/core-autocorrect.ts b/packages/migrate/src/migrations/v9/core-autocorrect.ts new file mode 100644 index 00000000000..6b26aa99df4 --- /dev/null +++ b/packages/migrate/src/migrations/v9/core-autocorrect.ts @@ -0,0 +1,28 @@ +import type { Migration } from '../../types.js'; +import { scanLines, SOURCE_GLOBS } from '../../ast/text-scan.js'; + +/** + * `autocorrect` on `ion-input`/`ion-searchbar` is now a boolean defaulting to + * `false`. A string attribute like `autocorrect="off"` now coerces to `true` + * (autocorrect enabled) - the opposite of the intent. Report-only: the correct + * fix (remove the attribute, or use a framework-specific boolean binding) + * depends on the template dialect. + * + * See https://ionicframework.com/docs/updating/9-0#input + */ +export const coreAutocorrect: Migration = { + id: 'core-autocorrect', + framework: 'core', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#input', + + detect(ctx) { + return scanLines(ctx, SOURCE_GLOBS, (line) => + /autocorrect\s*=\s*["'](on|off)["']/.test(line) + ? 'autocorrect is now boolean; remove the attribute or bind a boolean' + : undefined + ); + }, +}; diff --git a/packages/migrate/src/migrations/v9/core-ion-img.ts b/packages/migrate/src/migrations/v9/core-ion-img.ts new file mode 100644 index 00000000000..d28c9471674 --- /dev/null +++ b/packages/migrate/src/migrations/v9/core-ion-img.ts @@ -0,0 +1,26 @@ +import type { Migration } from '../../types.js'; +import { scanLines, SOURCE_GLOBS } from '../../ast/text-scan.js'; + +/** + * `ion-img` is deprecated in Ionic 9 (removed in 10) in favor of a native + * ``. Report-only: event handlers + * (`ionImgDidLoad` etc.) and `::part(image)` styling need per-usage rework. + * + * See https://ionicframework.com/docs/updating/9-0#img + */ +export const coreIonImg: Migration = { + id: 'core-ion-img', + framework: 'core', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#img', + + detect(ctx) { + return scanLines(ctx, SOURCE_GLOBS, (line) => + /' + : undefined + ); + }, +}; diff --git a/packages/migrate/src/migrations/v9/core-legacy-picker.ts b/packages/migrate/src/migrations/v9/core-legacy-picker.ts new file mode 100644 index 00000000000..6351ab557ed --- /dev/null +++ b/packages/migrate/src/migrations/v9/core-legacy-picker.ts @@ -0,0 +1,37 @@ +import type { Migration } from '../../types.js'; +import { scanLines, SOURCE_GLOBS } from '../../ast/text-scan.js'; + +/** + * `ion-picker-legacy`/`IonPickerLegacy` and the `pickerController` / + * `useIonPicker` controller APIs were removed in Ionic 9. Report-only: the + * inline `ion-picker` replacement has a different API, so the rewrite needs + * developer judgement. + * + * See https://ionicframework.com/docs/updating/9-0#legacy-picker + */ +export const coreLegacyPicker: Migration = { + id: 'core-legacy-picker', + framework: 'core', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#legacy-picker', + + detect(ctx) { + return scanLines(ctx, SOURCE_GLOBS, (line) => { + if (/ion-picker-legacy|IonPickerLegacy/.test(line)) { + return 'replace legacy picker with ion-picker (inline)'; + } + if (/\bpickerController\b|\buseIonPicker\b/.test(line)) { + return 'remove pickerController/useIonPicker; use the inline ion-picker'; + } + // The legacy picker's type exports were removed alongside the component. + // (PickerColumn/PickerColumnOption are intentionally excluded: they are + // also valid v9 component names, so matching them bare would be noisy.) + if (/\b(PickerOptions|PickerButton)\b/.test(line)) { + return 'legacy picker type removed; use the inline ion-picker component API'; + } + return undefined; + }); + }, +}; diff --git a/packages/migrate/src/migrations/v9/react-deps.ts b/packages/migrate/src/migrations/v9/react-deps.ts new file mode 100644 index 00000000000..b52ab493c08 --- /dev/null +++ b/packages/migrate/src/migrations/v9/react-deps.ts @@ -0,0 +1,22 @@ +import { createDepsMigration } from '../../ast/deps-migration.js'; +import { IONIC_V9_VERSION } from '../../versions.js'; + +/** + * Raise the React Ionic packages to v9 and React Router to v6, and drop the + * now-bundled `@types/react-router*`. The accompanying code changes (Route + * `element`, `useNavigate`, etc.) are reported by `react-router-6-code`. + * + * See https://ionicframework.com/docs/updating/9-0#react + */ +export const reactDeps = createDepsMigration({ + id: 'react-deps', + framework: 'react', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#react', + bumps: [ + ['@ionic/react', IONIC_V9_VERSION], + ['@ionic/react-router', IONIC_V9_VERSION], + ['react-router', 6], + ['react-router-dom', 6], + ], + removes: ['@types/react-router', '@types/react-router-dom'], +}); diff --git a/packages/migrate/src/migrations/v9/react-router-6-code.ts b/packages/migrate/src/migrations/v9/react-router-6-code.ts new file mode 100644 index 00000000000..194b56138d7 --- /dev/null +++ b/packages/migrate/src/migrations/v9/react-router-6-code.ts @@ -0,0 +1,62 @@ +import { SyntaxKind } from 'ts-morph'; +import type { JsxAttribute } from 'ts-morph'; + +import type { Finding, Migration } from '../../types.js'; + +/** + * React Router v6 removes and renames a number of v5 APIs. These changes need + * semantic rework (hooks replacing render-prop patterns, route-graph-aware + * `/*` suffixes), so this migration is report-only: it pinpoints every v5 + * pattern with a file/line and a pointer to the docs. + * + * See https://ionicframework.com/docs/updating/9-0#react-router + */ +const REMOVED_IMPORTS: Record = { + Redirect: 'Redirect removed; use ', + useHistory: 'useHistory removed; use useNavigate() or useIonRouter()', + RouteComponentProps: 'RouteComponentProps removed; use useParams/useLocation/useNavigate hooks', +}; +const REMOVED_ROUTE_ATTRS: Record = { + component: `Route "component" prop removed; use "element" with JSX`, + render: `Route "render" prop removed; use "element" with JSX`, + exact: `Route "exact" prop removed; routes match exactly by default`, +}; +const ROUTER_MODULES = new Set(['react-router', 'react-router-dom']); +const ROUTE_TAGS = new Set(['Route', 'IonRoute']); + +export const reactRouter6Code: Migration = { + id: 'react-router-6-code', + framework: 'react', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#react-router', + + detect(ctx) { + const findings: Finding[] = []; + for (const file of ctx.project.getSourceFiles()) { + const filePath = ctx.relative(file.getFilePath()); + + for (const imp of file.getImportDeclarations()) { + if (!ROUTER_MODULES.has(imp.getModuleSpecifierValue())) continue; + for (const named of imp.getNamedImports()) { + const detail = REMOVED_IMPORTS[named.getName()]; + if (detail) findings.push({ filePath, line: named.getStartLineNumber(), detail }); + } + } + + for (const kind of [SyntaxKind.JsxOpeningElement, SyntaxKind.JsxSelfClosingElement] as const) { + for (const el of file.getDescendantsOfKind(kind)) { + if (!ROUTE_TAGS.has(el.getTagNameNode().getText())) continue; + for (const attr of el.getAttributes()) { + if (attr.getKind() !== SyntaxKind.JsxAttribute) continue; + const name = (attr as JsxAttribute).getNameNode().getText(); + const detail = REMOVED_ROUTE_ATTRS[name]; + if (detail) findings.push({ filePath, line: attr.getStartLineNumber(), detail }); + } + } + } + } + return findings; + }, +}; diff --git a/packages/migrate/src/migrations/v9/vue-deps.ts b/packages/migrate/src/migrations/v9/vue-deps.ts new file mode 100644 index 00000000000..cfb6d183d3f --- /dev/null +++ b/packages/migrate/src/migrations/v9/vue-deps.ts @@ -0,0 +1,22 @@ +import { createDepsMigration } from '../../ast/deps-migration.js'; +import { IONIC_V9_VERSION } from '../../versions.js'; + +/** + * Raise the Vue Ionic packages to v9, Vue Router to v5, and Vue to 3.5+ (the + * minimum v9 supports). Vue Router 5 is a transition release with no runtime + * breaking changes, so the dep bump is the only required change. Guard + * migrations are reported by `vue-router-next-guard`. + * + * See https://ionicframework.com/docs/updating/9-0#vue + */ +export const vueDeps = createDepsMigration({ + id: 'vue-deps', + framework: 'vue', + docsUrl: 'https://ionicframework.com/docs/updating/9-0#vue', + bumps: [ + ['@ionic/vue', IONIC_V9_VERSION], + ['@ionic/vue-router', IONIC_V9_VERSION], + ['vue', '^3.5.0'], + ['vue-router', 5], + ], +}); diff --git a/packages/migrate/src/migrations/v9/vue-router-next-guard.ts b/packages/migrate/src/migrations/v9/vue-router-next-guard.ts new file mode 100644 index 00000000000..1112a301cff --- /dev/null +++ b/packages/migrate/src/migrations/v9/vue-router-next-guard.ts @@ -0,0 +1,36 @@ +import type { Migration } from '../../types.js'; +import { scanLines } from '../../ast/text-scan.js'; + +/** + * Vue Router 5 deprecates calling `next()` inside navigation guards; the + * return-value form is preferred. Report-only: flags files that declare guards + * so the developer can migrate the callback style. The dep bump itself is + * handled by `vue-deps`. + * + * See https://ionicframework.com/docs/updating/9-0#deprecation-warning-for-next-in-navigation-guards + */ +// The global `beforeEach`/`beforeResolve` guards must be called on a router +// instance (`router.beforeEach(...)`); requiring a router-suffixed receiver +// avoids flagging both bare `beforeEach()` hooks and Playwright's +// `test.beforeEach()`. The component-guard names are Vue-specific and safe to +// match on their own. +const GUARD_HOOKS = + /(\b\w*[Rr]outer\s*\.\s*(beforeEach|beforeResolve)\b)|\b(beforeRouteEnter|beforeRouteLeave|beforeRouteUpdate|onBeforeRouteLeave|onBeforeRouteUpdate)\b/; + +export const vueRouterNextGuard: Migration = { + id: 'vue-router-next-guard', + framework: 'vue', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: + 'https://ionicframework.com/docs/updating/9-0#deprecation-warning-for-next-in-navigation-guards', + + detect(ctx) { + return scanLines(ctx, ['**/*.ts', '**/*.js', '**/*.vue'], (line) => + GUARD_HOOKS.test(line) + ? 'navigation guard: migrate next() calls to the return-value form (vue-router 5)' + : undefined + ); + }, +}; diff --git a/packages/migrate/src/registry.ts b/packages/migrate/src/registry.ts new file mode 100644 index 00000000000..a30b438776c --- /dev/null +++ b/packages/migrate/src/registry.ts @@ -0,0 +1,31 @@ +import type { Framework, Migration } from './types.js'; + +export interface SelectOptions { + /** Major version the project is currently on. */ + fromMajor: number; + /** Major version to migrate to. */ + toMajor: number; + /** Frameworks detected in the project. `core` migrations always apply. */ + frameworks: Framework[]; + /** Include `experimental` migrations. Defaults to `false`. */ + includeExperimental?: boolean; +} + +/** + * Choose the migrations that apply to a project, in the order they should run. + * + * A migration is selected when its version hop sits within the requested range + * (`fromMajor <= migration.fromMajor` and `migration.toMajor <= toMajor`) and + * its framework was detected (or it is a `core` migration). This is also the + * safety gate against re-running: a project already on the target major yields + * an empty range and therefore no migrations. + */ +export function selectMigrations(all: Migration[], opts: SelectOptions): Migration[] { + const { fromMajor, toMajor, frameworks, includeExperimental = false } = opts; + + return all + .filter((m) => m.fromMajor >= fromMajor && m.toMajor <= toMajor) + .filter((m) => m.framework === 'core' || frameworks.includes(m.framework)) + .filter((m) => includeExperimental || m.status === 'stable') + .sort((a, b) => a.fromMajor - b.fromMajor || a.id.localeCompare(b.id)); +} diff --git a/packages/migrate/src/report.ts b/packages/migrate/src/report.ts new file mode 100644 index 00000000000..92020942a3e --- /dev/null +++ b/packages/migrate/src/report.ts @@ -0,0 +1,47 @@ +import type { RunResult } from './runner.js'; + +/** + * Render a "doctor" report of a run: one section per migration that matched, + * grouped by whether it was auto-applied or needs manual work, each with its + * finding count and docs link. Plain ASCII so it renders cleanly in any + * terminal and copy-pastes without mangling. + */ +export function buildReport(result: RunResult): string { + if (result.entries.length === 0) { + return 'No applicable Ionic migrations found. Nothing to do.'; + } + + // Bucket by whether the migration can auto-fix, not by whether it ran, so a + // dry run still shows which changes are automatic vs. manual. + const auto = result.entries.filter((e) => e.migration.fix !== undefined); + const manual = result.entries.filter((e) => e.migration.fix === undefined); + const lines: string[] = []; + + if (auto.length > 0) { + const applied = auto.some((e) => e.applied); + lines.push( + applied + ? `Applied ${auto.length} automatic migration(s):` + : `${auto.length} migration(s) can be auto-fixed (dry run - nothing written):` + ); + for (const entry of auto) { + lines.push( + ` [${entry.applied ? 'fixed' : 'would-fix'}] ${entry.migration.id} (${entry.findings.length} change(s))` + ); + } + lines.push(''); + } + + if (manual.length > 0) { + lines.push(`${manual.length} migration(s) need manual review:`); + for (const entry of manual) { + lines.push(` [todo] ${entry.migration.id} (${entry.findings.length} location(s))`); + for (const finding of entry.findings) { + lines.push(` ${finding.filePath}:${finding.line} - ${finding.detail}`); + } + lines.push(` see ${entry.migration.docsUrl}`); + } + } + + return lines.join('\n').trimEnd(); +} diff --git a/packages/migrate/src/runner.ts b/packages/migrate/src/runner.ts new file mode 100644 index 00000000000..7abfa712ae3 --- /dev/null +++ b/packages/migrate/src/runner.ts @@ -0,0 +1,52 @@ +import type { MigrationContext } from './context.js'; +import type { Finding, Migration } from './types.js'; + +/** Outcome of running a single migration. */ +export interface RunEntry { + migration: Migration; + findings: Finding[]; + /** Whether {@link Migration.fix} was applied. `false` for report-only or dry runs. */ + applied: boolean; +} + +export interface RunResult { + entries: RunEntry[]; +} + +export interface RunOptions { + /** Report what would change without writing anything. */ + dryRun?: boolean; +} + +/** + * Run migrations against a context. Each migration is detected first; only + * migrations with findings are reported. Auto-fixable migrations are applied + * unless {@link RunOptions.dryRun} is set. Report-only migrations (no `fix`) + * are always reported but never applied. + */ +export function run( + ctx: MigrationContext, + migrations: Migration[], + { dryRun = false }: RunOptions = {} +): RunResult { + const entries: RunEntry[] = []; + let mutated = false; + + for (const migration of migrations) { + const findings = migration.detect(ctx); + if (findings.length === 0) continue; + + let applied = false; + if (!dryRun && migration.fix) { + migration.fix(ctx); + applied = true; + mutated = true; + } + entries.push({ migration, findings, applied }); + } + + if (mutated) { + ctx.save(); + } + return { entries }; +} diff --git a/packages/migrate/src/types.ts b/packages/migrate/src/types.ts new file mode 100644 index 00000000000..cfb06942b2b --- /dev/null +++ b/packages/migrate/src/types.ts @@ -0,0 +1,49 @@ +import type { MigrationContext } from './context.js'; + +/** + * A single location in the project where a migration applies. Findings drive + * the "doctor" report and are passed back into {@link Migration.fix}. + */ +export interface Finding { + /** Path of the file the finding was located in. */ + filePath: string; + /** 1-based line number of the finding. */ + line: number; + /** Human-readable description of what was found at this location. */ + detail: string; +} + +/** The framework a migration targets. `core` applies regardless of framework. */ +export type Framework = 'angular' | 'react' | 'vue' | 'core'; + +/** + * Status of a migration. `experimental` migrations are skipped unless the user + * explicitly opts in, letting us ship transforms for changes that are not yet + * settled. + */ +export type MigrationStatus = 'stable' | 'experimental'; + +/** + * A single breaking-change migration. Migrations are version-scoped via + * {@link fromMajor}/{@link toMajor} so the engine can chain major-version hops, + * and are otherwise self-contained: they know how to locate where they apply + * ({@link detect}) and, when safe, how to apply the change ({@link fix}). + */ +export interface Migration { + /** Stable identifier, e.g. `angular-standalone-imports`. */ + id: string; + /** Framework this migration targets. */ + framework: Framework; + /** Major version the project is migrating from. */ + fromMajor: number; + /** Major version the project is migrating to. */ + toMajor: number; + /** Release status; `experimental` migrations require explicit opt-in. */ + status: MigrationStatus; + /** URL of the docs section describing this change. */ + docsUrl: string; + /** Locate every place this migration applies. */ + detect(ctx: MigrationContext): Finding[]; + /** Apply the change in place. Omitted when the change is report-only. */ + fix?(ctx: MigrationContext): void; +} diff --git a/packages/migrate/src/versions.ts b/packages/migrate/src/versions.ts new file mode 100644 index 00000000000..db22378986f --- /dev/null +++ b/packages/migrate/src/versions.ts @@ -0,0 +1,14 @@ +/** + * The version the `@ionic/*` packages are pinned to when migrating to v9. + * + * v9 is not yet published under the `latest` dist-tag, so a caret range like + * `^9.0.0` will not resolve. Until v9 ships, this pins a specific `major-9.0` + * dev build. It is versioned `8.8.x-dev` (not `9.0.0-dev`) because the + * `major-9.0` branch has not bumped its base version yet, but the code is v9. + * + * We pin the exact version rather than the `dev` dist-tag because that tag is + * shared with `main`'s dev builds and moves whenever any dev build publishes. + * + * Swap this for `^9.0.0` (or `latest`) at GA. + */ +export const IONIC_V9_VERSION = '8.8.14-dev.11784563563.137a903a'; diff --git a/packages/migrate/test/angular-css-tilde.test.ts b/packages/migrate/test/angular-css-tilde.test.ts new file mode 100644 index 00000000000..b9d3d90c159 --- /dev/null +++ b/packages/migrate/test/angular-css-tilde.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { createInMemoryContext } from '../src/context.js'; +import { angularCssTilde as migration } from '../src/migrations/v9/angular-css-tilde.js'; + +describe('angular-css-tilde', () => { + it('removes the ~ prefix from an @ionic/angular css import', () => { + const ctx = createInMemoryContext({ + 'src/global.scss': `@import '~@ionic/angular/css/core.css';\n`, + }); + + migration.fix!(ctx); + + expect(ctx.readFile('src/global.scss')).toBe(`@import '@ionic/angular/css/core.css';\n`); + }); + + it('leaves non-ionic tilde imports alone', () => { + const input = `@import '~bootstrap/scss/bootstrap';\n`; + const ctx = createInMemoryContext({ 'src/global.scss': input }); + + migration.fix!(ctx); + + expect(ctx.readFile('src/global.scss')).toBe(input); + expect(migration.detect(ctx)).toEqual([]); + }); + + it('reports each offending import with its file and line', () => { + const ctx = createInMemoryContext({ + 'src/global.scss': + `@import '~bootstrap/scss/bootstrap';\n` + `@import '~@ionic/angular/css/core.css';\n`, + }); + + expect(migration.detect(ctx)).toEqual([ + { filePath: 'src/global.scss', line: 2, detail: 'remove `~` prefix from @ionic/angular import' }, + ]); + }); +}); diff --git a/packages/migrate/test/angular-standalone-imports.test.ts b/packages/migrate/test/angular-standalone-imports.test.ts new file mode 100644 index 00000000000..214abc588e2 --- /dev/null +++ b/packages/migrate/test/angular-standalone-imports.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; + +import { createInMemoryContext } from '../src/context.js'; +import { angularStandaloneImports } from '../src/migrations/v9/angular-standalone-imports.js'; + +const migration = angularStandaloneImports; + +/** Build a context with a single TS file and return the context + a text reader. */ +function withFile(text: string, name = 'app.component.ts') { + const ctx = createInMemoryContext({ [name]: text }); + const read = () => ctx.project.getSourceFileOrThrow(`${ctx.rootDir}/${name}`).getFullText(); + // Findings report project-relative paths. + return { ctx, read, filePath: name }; +} + +describe('angular-standalone-imports', () => { + it('rewrites a lazy `@ionic/angular` import to `@ionic/angular/lazy`', () => { + const { ctx, read } = withFile(`import { IonButton } from '@ionic/angular';\n`); + + migration.fix!(ctx); + + expect(read()).toBe(`import { IonButton } from '@ionic/angular/lazy';\n`); + }); + + it('rewrites a `@ionic/angular/standalone` import to `@ionic/angular`', () => { + const { ctx, read } = withFile(`import { IonButton } from '@ionic/angular/standalone';\n`); + + migration.fix!(ctx); + + expect(read()).toBe(`import { IonButton } from '@ionic/angular';\n`); + }); + + it('migrates both import paths in one file without double-rewriting', () => { + const { ctx, read } = withFile( + `import { IonButton } from '@ionic/angular/standalone';\n` + + `import { NavController } from '@ionic/angular';\n` + ); + + migration.fix!(ctx); + + // The standalone import must become bare `@ionic/angular` and NOT then be + // re-caught and pushed on to `/lazy`; the lazy import becomes `/lazy`. + expect(read()).toBe( + `import { IonButton } from '@ionic/angular';\n` + + `import { NavController } from '@ionic/angular/lazy';\n` + ); + }); + + it('leaves string literals, comments, and unrelated specifiers untouched', () => { + const input = + `// import { IonButton } from '@ionic/angular';\n` + + `import { AngularToolkit } from '@ionic/angular-toolkit';\n` + + `const pkg = '@ionic/angular';\n`; + const { ctx, read } = withFile(input); + + migration.fix!(ctx); + + expect(read()).toBe(input); + }); + + it('reports a finding, with line and detail, for each import it would rewrite', () => { + const { ctx, filePath } = withFile( + `import { IonButton } from '@ionic/angular/standalone';\n` + + `import { AngularToolkit } from '@ionic/angular-toolkit';\n` + + `import { NavController } from '@ionic/angular';\n` + ); + + const findings = migration.detect(ctx); + + expect(findings).toEqual([ + { filePath, line: 1, detail: `'@ionic/angular/standalone' -> '@ionic/angular'` }, + { filePath, line: 3, detail: `'@ionic/angular' -> '@ionic/angular/lazy'` }, + ]); + }); +}); diff --git a/packages/migrate/test/engine.test.ts b/packages/migrate/test/engine.test.ts new file mode 100644 index 00000000000..1b57cde3dce --- /dev/null +++ b/packages/migrate/test/engine.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from 'vitest'; + +import { createInMemoryContext } from '../src/context.js'; +import { detectFrameworks, parseMajor } from '../src/detect.js'; +import { IONIC_V9_VERSION } from '../src/versions.js'; +import { selectMigrations } from '../src/registry.js'; +import { run } from '../src/runner.js'; +import { buildReport } from '../src/report.js'; +import type { Migration } from '../src/types.js'; + +/** Minimal migration factory for engine tests (behavior is irrelevant here). */ +function fakeMigration(over: Partial & Pick): Migration { + return { + framework: 'core', + fromMajor: 8, + toMajor: 9, + status: 'stable', + docsUrl: 'https://example.test', + detect: () => [], + ...over, + }; +} + +describe('detectFrameworks', () => { + it('reads the installed Ionic framework and major from package.json', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify({ dependencies: { '@ionic/angular': '^8.4.1' } }), + }); + + expect(detectFrameworks(ctx)).toEqual([{ framework: 'angular', major: 8 }]); + }); + + it('detects multiple bindings across dependencies and devDependencies', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify({ + dependencies: { '@ionic/react': '8.0.0' }, + devDependencies: { '@ionic/vue': '~8.2.0' }, + }), + }); + + expect(detectFrameworks(ctx)).toEqual([ + { framework: 'react', major: 8 }, + { framework: 'vue', major: 8 }, + ]); + }); + + it('parses the major from assorted range syntaxes', () => { + expect(parseMajor('^8.4.1')).toBe(8); + expect(parseMajor('9.0.0-rc.1')).toBe(9); + expect(parseMajor(undefined)).toBeUndefined(); + }); + + it('treats the v9 dev pin as major 9, closing the re-run gate', () => { + // The pin is versioned `8.8.x-dev`, so a naive semver read reports major 8 + // and would re-select every v8->v9 migration on a second run. + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify({ dependencies: { '@ionic/angular': IONIC_V9_VERSION } }), + }); + + expect(detectFrameworks(ctx)).toEqual([{ framework: 'angular', major: 9 }]); + }); + + it('throws a clear error on a malformed package.json', () => { + const ctx = createInMemoryContext({ 'package.json': '{ not valid json' }); + + expect(() => detectFrameworks(ctx)).toThrow(/Could not parse package\.json/); + }); +}); + +describe('selectMigrations', () => { + const angular89 = fakeMigration({ id: 'a', framework: 'angular', fromMajor: 8, toMajor: 9 }); + const react89 = fakeMigration({ id: 'r', framework: 'react', fromMajor: 8, toMajor: 9 }); + const core89 = fakeMigration({ id: 'c', framework: 'core', fromMajor: 8, toMajor: 9 }); + const core910 = fakeMigration({ id: 'd', framework: 'core', fromMajor: 9, toMajor: 10 }); + const experimental = fakeMigration({ id: 'x', fromMajor: 8, toMajor: 9, status: 'experimental' }); + const all = [core910, react89, angular89, core89, experimental]; + + it('selects only the detected framework plus core, sorted deterministically', () => { + const selected = selectMigrations(all, { fromMajor: 8, toMajor: 9, frameworks: ['angular'] }); + expect(selected.map((m) => m.id)).toEqual(['a', 'c']); + }); + + it('is a version gate: a project already on the target major yields nothing', () => { + const selected = selectMigrations(all, { fromMajor: 9, toMajor: 9, frameworks: ['angular'] }); + expect(selected).toEqual([]); + }); + + it('chains multiple major hops in order', () => { + const selected = selectMigrations(all, { fromMajor: 8, toMajor: 10, frameworks: ['core'] }); + expect(selected.map((m) => m.id)).toEqual(['c', 'd']); + }); + + it('excludes experimental migrations unless opted in', () => { + const stable = selectMigrations(all, { fromMajor: 8, toMajor: 9, frameworks: ['core'] }); + const withExp = selectMigrations(all, { + fromMajor: 8, + toMajor: 9, + frameworks: ['core'], + includeExperimental: true, + }); + expect(stable.map((m) => m.id)).toEqual(['c']); + expect(withExp.map((m) => m.id)).toEqual(['c', 'x']); + }); +}); + +describe('run', () => { + const rewriteFoo = fakeMigration({ + id: 'rewrite-foo', + detect: (ctx) => (ctx.readFile('data.txt') === 'foo' ? [{ filePath: 'data.txt', line: 1, detail: 'foo->bar' }] : []), + fix: (ctx) => ctx.writeFile('data.txt', 'bar'), + }); + + it('applies auto-fixable migrations and persists the change', () => { + const ctx = createInMemoryContext({ 'data.txt': 'foo' }); + + const result = run(ctx, [rewriteFoo]); + + expect(result.entries[0].applied).toBe(true); + expect(ctx.readFile('data.txt')).toBe('bar'); + }); + + it('dry-run reports findings but writes nothing', () => { + const ctx = createInMemoryContext({ 'data.txt': 'foo' }); + + const result = run(ctx, [rewriteFoo], { dryRun: true }); + + expect(result.entries[0].findings).toHaveLength(1); + expect(result.entries[0].applied).toBe(false); + expect(ctx.readFile('data.txt')).toBe('foo'); + }); + + it('reports a report-only migration (no fix) without applying it', () => { + const reportOnly = fakeMigration({ + id: 'report-only', + detect: () => [{ filePath: 'x.ts', line: 3, detail: 'review this' }], + fix: undefined, + }); + const ctx = createInMemoryContext({}); + + const result = run(ctx, [reportOnly]); + + expect(result.entries[0].applied).toBe(false); + expect(result.entries[0].findings).toHaveLength(1); + }); + + it('skips migrations with no findings', () => { + const ctx = createInMemoryContext({ 'data.txt': 'already-bar' }); + + const result = run(ctx, [rewriteFoo]); + + expect(result.entries).toEqual([]); + }); +}); + +describe('buildReport', () => { + it('summarizes fixed and manual migrations', () => { + const ctx = createInMemoryContext({ 'data.txt': 'foo' }); + const result = run(ctx, [ + fakeMigration({ + id: 'auto', + detect: () => [{ filePath: 'data.txt', line: 1, detail: 'x' }], + fix: () => {}, + }), + fakeMigration({ + id: 'manual', + detect: () => [{ filePath: 'x.ts', line: 9, detail: 'do it yourself' }], + fix: undefined, + docsUrl: 'https://docs.test/manual', + }), + ]); + + const report = buildReport(result); + + expect(report).toContain('[fixed] auto'); + expect(report).toContain('[todo] manual'); + expect(report).toContain('x.ts:9 - do it yourself'); + expect(report).toContain('https://docs.test/manual'); + }); + + it('reports nothing-to-do on an empty result', () => { + expect(buildReport({ entries: [] })).toBe('No applicable Ionic migrations found. Nothing to do.'); + }); +}); diff --git a/packages/migrate/test/migrations.test.ts b/packages/migrate/test/migrations.test.ts new file mode 100644 index 00000000000..2541a2a0302 --- /dev/null +++ b/packages/migrate/test/migrations.test.ts @@ -0,0 +1,230 @@ +import { describe, expect, it } from 'vitest'; + +import { createInMemoryContext } from '../src/context.js'; +import { IONIC_V9_VERSION } from '../src/versions.js'; +import { angularDeps } from '../src/migrations/v9/angular-deps.js'; +import { reactDeps } from '../src/migrations/v9/react-deps.js'; +import { reactRouter6Code } from '../src/migrations/v9/react-router-6-code.js'; +import { vueDeps } from '../src/migrations/v9/vue-deps.js'; +import { vueRouterNextGuard } from '../src/migrations/v9/vue-router-next-guard.js'; +import { angularZoneless } from '../src/migrations/v9/angular-zoneless.js'; +import { coreLegacyPicker } from '../src/migrations/v9/core-legacy-picker.js'; +import { coreAutocorrect } from '../src/migrations/v9/core-autocorrect.js'; +import { coreIonImg } from '../src/migrations/v9/core-ion-img.js'; + +describe('react-deps', () => { + it('bumps @ionic/react + router deps and drops @types/react-router*', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify( + { + dependencies: { + '@ionic/react': '^8.4.0', + '@ionic/react-router': '^8.4.0', + 'react-router': '^5.3.0', + 'react-router-dom': '^5.3.0', + }, + devDependencies: { '@types/react-router-dom': '^5.3.0' }, + }, + null, + 2 + ), + }); + + reactDeps.fix!(ctx); + const pkg = JSON.parse(ctx.readFile('package.json')!); + + expect(pkg.dependencies['@ionic/react']).toBe(IONIC_V9_VERSION); + expect(pkg.dependencies['react-router']).toBe('^6.0.0'); + expect(pkg.dependencies['react-router-dom']).toBe('^6.0.0'); + expect(pkg.devDependencies['@types/react-router-dom']).toBeUndefined(); + }); + + it('does nothing when already on v9/v6 (version gate is closed)', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify( + { dependencies: { '@ionic/react': IONIC_V9_VERSION, 'react-router-dom': '^6.4.0' } }, + null, + 2 + ), + }); + + expect(reactDeps.detect(ctx)).toEqual([]); + }); +}); + +describe('angular-deps', () => { + it('bumps @ionic/angular to v9', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify({ dependencies: { '@ionic/angular': '^8.4.0' } }, null, 2), + }); + + angularDeps.fix!(ctx); + const pkg = JSON.parse(ctx.readFile('package.json')!); + + expect(pkg.dependencies['@ionic/angular']).toBe(IONIC_V9_VERSION); + }); +}); + +describe('vue-deps', () => { + it('bumps @ionic/vue and vue-router to their v9 targets', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify( + { dependencies: { '@ionic/vue': '^8.4.0', vue: '^3.4.0', 'vue-router': '^4.2.0' } }, + null, + 2 + ), + }); + + vueDeps.fix!(ctx); + const pkg = JSON.parse(ctx.readFile('package.json')!); + + expect(pkg.dependencies['@ionic/vue']).toBe(IONIC_V9_VERSION); + expect(pkg.dependencies['vue-router']).toBe('^5.0.0'); + // Vue must be raised to the 3.5+ floor v9 requires; a 3.4 pin is below it. + expect(pkg.dependencies['vue']).toBe('^3.5.0'); + }); + + it('does not downgrade a Vue pin already at or above the 3.5 floor', () => { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify( + { dependencies: { '@ionic/vue': IONIC_V9_VERSION, vue: '^3.6.0', 'vue-router': '^5.0.0' } }, + null, + 2 + ), + }); + + expect(vueDeps.detect(ctx)).toEqual([]); + }); + + it('leaves a non-semver Vue range (workspace/catalog/latest) untouched', () => { + for (const range of [ + 'workspace:*', + 'catalog:', + 'catalog:vue3', + 'workspace:~3.4.0', + 'npm:vue@3.4.0', + 'git+https://github.com/vuejs/core.git#v3.4.0', + 'latest', + '*', + ]) { + const ctx = createInMemoryContext({ + 'package.json': JSON.stringify({ dependencies: { '@ionic/vue': '^8.4.0', vue: range } }, null, 2), + }); + + const vueChange = vueDeps.detect(ctx).find((f) => f.detail.includes(' vue ')); + expect(vueChange, `range ${range} should not be rewritten`).toBeUndefined(); + } + }); +}); + +describe('react-router-6-code (report-only)', () => { + it('reports removed imports and Route props with locations', () => { + const ctx = createInMemoryContext({ + 'App.tsx': + `import { Redirect, useHistory } from 'react-router-dom';\n` + + `export const App = () => ;\n`, + }); + + const details = reactRouter6Code.detect(ctx).map((f) => f.detail); + + expect(details.some((d) => d.includes('Redirect removed'))).toBe(true); + expect(details.some((d) => d.includes('useHistory removed'))).toBe(true); + expect(details.some((d) => d.includes('"component" prop removed'))).toBe(true); + expect(details.some((d) => d.includes('"exact" prop removed'))).toBe(true); + }); + + it('ignores non-router imports and non-Route elements', () => { + const ctx = createInMemoryContext({ + 'App.tsx': + `import { useState } from 'react';\n` + `export const App = () => ;\n`, + }); + + expect(reactRouter6Code.detect(ctx)).toEqual([]); + }); +}); + +describe('core report-only migrations', () => { + it('core-legacy-picker flags legacy picker and controller usage', () => { + const ctx = createInMemoryContext({ + 'page.html': `\n`, + 'page.ts': `const p = await pickerController.create();\n`, + }); + + expect(coreLegacyPicker.detect(ctx)).toHaveLength(2); + }); + + it('core-legacy-picker flags removed picker type imports', () => { + const ctx = createInMemoryContext({ + 'page.ts': `import type { PickerOptions, PickerButton } from '@ionic/angular';\n`, + }); + + expect(coreLegacyPicker.detect(ctx)).toHaveLength(1); + }); + + it('core-autocorrect flags string autocorrect attributes', () => { + const ctx = createInMemoryContext({ + 'page.html': `\n\n`, + }); + + const findings = coreAutocorrect.detect(ctx); + expect(findings).toHaveLength(1); + expect(findings[0].line).toBe(1); + }); + + it('core-ion-img flags ion-img usage', () => { + const ctx = createInMemoryContext({ 'page.html': `\n` }); + + expect(coreIonImg.detect(ctx)).toHaveLength(1); + }); + + it('does not flag when nothing matches', () => { + const ctx = createInMemoryContext({ 'page.html': `Hi\n` }); + + expect(coreLegacyPicker.detect(ctx)).toEqual([]); + expect(coreAutocorrect.detect(ctx)).toEqual([]); + expect(coreIonImg.detect(ctx)).toEqual([]); + }); +}); + +describe('angular-zoneless (report-only)', () => { + it('flags the bootstrap call in main.ts', () => { + const ctx = createInMemoryContext({ + 'src/main.ts': `bootstrapApplication(AppComponent, { providers: [] });\n`, + }); + + expect(angularZoneless.detect(ctx)).toHaveLength(1); + }); + + it('ignores files without a bootstrap call', () => { + const ctx = createInMemoryContext({ 'src/main.ts': `console.log('hello');\n` }); + + expect(angularZoneless.detect(ctx)).toEqual([]); + }); +}); + +describe('vue-router-next-guard (report-only)', () => { + it('flags router-qualified navigation guards', () => { + const ctx = createInMemoryContext({ + 'router.ts': `router.beforeEach((to, from, next) => { next(); });\n`, + }); + + expect(vueRouterNextGuard.detect(ctx)).toHaveLength(1); + }); + + it('does not flag bare or Playwright beforeEach hooks', () => { + const ctx = createInMemoryContext({ + 'component.spec.ts': `beforeEach(() => { setup(); });\n`, + 'e2e.spec.ts': `test.beforeEach(async ({ page }) => { await page.goto('/'); });\n`, + }); + + expect(vueRouterNextGuard.detect(ctx)).toEqual([]); + }); + + it('flags Vue-specific component navigation guards on their own', () => { + const ctx = createInMemoryContext({ + 'view.ts': `onBeforeRouteLeave((to, from, next) => next());\n`, + }); + + expect(vueRouterNextGuard.detect(ctx)).toHaveLength(1); + }); +}); diff --git a/packages/migrate/tsconfig.build.json b/packages/migrate/tsconfig.build.json new file mode 100644 index 00000000000..5714944a2bf --- /dev/null +++ b/packages/migrate/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/migrate/tsconfig.json b/packages/migrate/tsconfig.json new file mode 100644 index 00000000000..5a4c36ef461 --- /dev/null +++ b/packages/migrate/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "dist"] +} From 92e18f15c6b394492747e2b348c54bccabeb8321 Mon Sep 17 00:00:00 2001 From: ShaneK Date: Mon, 20 Jul 2026 13:25:20 -0700 Subject: [PATCH 2/6] feat(migrate): auto-fix zoneless, autocorrect, and react-router v6 route migrations --- packages/migrate/src/ast/markup.ts | 59 +++++++++ packages/migrate/src/context.ts | 11 +- packages/migrate/src/migrations/index.ts | 4 + .../src/migrations/v9/angular-zoneless.ts | 72 +++++++++-- .../migrations/v9/core-autocorrect-manual.ts | 46 +++++++ .../src/migrations/v9/core-autocorrect.ts | 98 ++++++++++++-- .../src/migrations/v9/react-router-6-code.ts | 27 +++- .../migrations/v9/react-router-6-routes.ts | 121 ++++++++++++++++++ .../migrate/test/angular-zoneless.test.ts | 86 +++++++++++++ .../test/core-autocorrect-manual.test.ts | 49 +++++++ .../migrate/test/core-autocorrect.test.ts | 96 ++++++++++++++ packages/migrate/test/markup.test.ts | 30 +++++ packages/migrate/test/migrations.test.ts | 51 +++----- .../test/react-router-6-routes.test.ts | 85 ++++++++++++ 14 files changed, 771 insertions(+), 64 deletions(-) create mode 100644 packages/migrate/src/ast/markup.ts create mode 100644 packages/migrate/src/migrations/v9/core-autocorrect-manual.ts create mode 100644 packages/migrate/src/migrations/v9/react-router-6-routes.ts create mode 100644 packages/migrate/test/angular-zoneless.test.ts create mode 100644 packages/migrate/test/core-autocorrect-manual.test.ts create mode 100644 packages/migrate/test/core-autocorrect.test.ts create mode 100644 packages/migrate/test/markup.test.ts create mode 100644 packages/migrate/test/react-router-6-routes.test.ts diff --git a/packages/migrate/src/ast/markup.ts b/packages/migrate/src/ast/markup.ts new file mode 100644 index 00000000000..4877967b0f9 --- /dev/null +++ b/packages/migrate/src/ast/markup.ts @@ -0,0 +1,59 @@ +/** An opening tag located in template/markup text. */ +export interface OpeningTag { + /** Lower-cased tag name. */ + name: string; + /** Offset of the leading `<`. */ + start: number; + /** Offset just past the closing `>`. */ + end: number; + /** The full opening-tag text, e.g. ``. */ + text: string; +} + +/** + * Find opening tags of the given names in template text (HTML, Vue `