-
Notifications
You must be signed in to change notification settings - Fork 13.3k
feat(migrate): add v8-to-v9 migration tool #31285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ShaneK
wants to merge
7
commits into
major-9.0
Choose a base branch
from
FW-7612
base: major-9.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
37c1e79
feat(migrate): add v9 migration CLI with doctor report and autofixes
ShaneK 92e18f1
feat(migrate): auto-fix zoneless, autocorrect, and react-router v6 ro…
ShaneK 49883bd
feat(migrate): add post-run prettier formatting and NgModule zoneless…
ShaneK 42e1c0c
docs(readme): adding migrator readme
ShaneK 0bcbc5c
Merge branch 'major-9.0' of github.com:ionic-team/ionic-framework int…
ShaneK dddbcd1
ci(migrate): add lint/test/build workflow with always-reporting gate
ShaneK ac3edf6
feat(migrate): add nav-router migration, gate re-runs
ShaneK File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| name: 'Ionic Migrate' | ||
|
|
||
| # Lint, test, and build the @ionic/migrate CLI. | ||
| # | ||
| # The workflow always triggers (no trigger-level `paths` filter) so the | ||
| # `verify-migrate` gate always reports a status and can be marked a required | ||
| # check: a path-filtered *trigger* would skip the whole workflow on unrelated | ||
| # PRs, and a required check that never runs blocks merges forever. Instead the | ||
| # `changes` job detects whether the package was touched and the heavy job runs | ||
| # only then; `verify-migrate` reports success either way (skipped == fine). | ||
| on: | ||
| pull_request: | ||
| branches: [ '**' ] | ||
| merge_group: | ||
| workflow_dispatch: | ||
|
|
||
| # Lint/test/build only reads the repo; keep the token least-privilege. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: migrate-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| changes: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| migrate: ${{ steps.filter.outputs.migrate }} | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| # Full history so the PR base commit is reachable for the diff below. | ||
| fetch-depth: 0 | ||
| - name: Detect migrate changes | ||
| id: filter | ||
| shell: bash | ||
| run: | | ||
| # Fail the step (not the else branch) if git diff itself errors, so a | ||
| # broken detector can't masquerade as "package untouched". | ||
| set -euo pipefail | ||
| # Only pull_request runs get the path optimization. merge_group and | ||
| # manual dispatch run the (fast) tests unconditionally, so the merge | ||
| # queue never blocks on a check that was skipped by a stale diff. | ||
| if [ "${{ github.event_name }}" != "pull_request" ]; then | ||
| echo "migrate=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| base="${{ github.event.pull_request.base.sha }}" | ||
| changed="$(git diff --name-only "$base" HEAD)" | ||
| if echo "$changed" \ | ||
| | grep -qE '^(packages/migrate/|\.github/workflows/migrate\.yml$)'; then | ||
| echo "migrate=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "migrate=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| test-migrate: | ||
| needs: changes | ||
| if: ${{ needs.changes.outputs.migrate == 'true' }} | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: ./packages/migrate | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | ||
| with: | ||
| node-version: 24.x | ||
| cache: 'npm' | ||
| cache-dependency-path: packages/migrate/package-lock.json | ||
| - name: Install Dependencies | ||
| run: npm ci | ||
| - name: Lint | ||
| run: npm run lint | ||
| - name: Test | ||
| run: npm test | ||
| - name: Build | ||
| run: npm run build | ||
|
|
||
| # Always-running required gate. See header comment for why this exists. | ||
| # Green when the package was untouched (test-migrate skipped) or tests pass; | ||
| # red when detection failed or the tests actually failed. | ||
| verify-migrate: | ||
| if: ${{ always() }} | ||
| needs: [ changes, test-migrate ] | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check results | ||
| # changes.result != 'success' catches a failed/cancelled detector, | ||
| # whose skipped test-migrate would otherwise slip through as green. | ||
| if: ${{ needs.changes.result != 'success' || needs.test-migrate.result == 'failure' || needs.test-migrate.result == 'cancelled' }} | ||
| run: exit 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # @ionic/migrate | ||
|
|
||
| Automates and reports the breaking changes between major versions of Ionic | ||
| Framework. It scans your app, applies the changes it can make safely, and prints | ||
| a checklist of the ones you need to do by hand, each with a file and line and a | ||
| link to the docs. | ||
|
|
||
| It handles Angular, React, and Vue (and vanilla) from one command. New breaking | ||
| changes and future majors are added as data, not new tooling. | ||
|
|
||
| ## Usage | ||
|
|
||
| Run it from the root of your app: | ||
|
|
||
| ```sh | ||
| npx @ionic/migrate | ||
| ``` | ||
|
|
||
| Commit first. The tool edits files in place and won't write to a dirty working | ||
| tree, so git is your undo. Then review the diff and the checklist it prints. | ||
|
|
||
| ### 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 | ||
| --no-format Skip running the project's Prettier over changed files | ||
| --from <major> Override the detected source major version | ||
| --to <major> Override the target major version | ||
| -h, --help Show this help | ||
| ``` | ||
|
|
||
| ## What it does | ||
|
|
||
| Every breaking change is one of two kinds: | ||
|
|
||
| - Auto-fix: a deterministic edit that preserves behavior, applied for you. | ||
| - Report-only: a change that needs judgement (semantic rework, a dialect | ||
| choice). The tool finds it and explains it, but won't rewrite it. | ||
|
|
||
| ### Coverage (v8 to v9) | ||
|
|
||
| | Change | Framework | Mode | | ||
| | --- | --- | --- | | ||
| | `@ionic/angular` -> `@ionic/angular/lazy`, `/standalone` -> `@ionic/angular` | Angular | auto | | ||
| | `@ionic/angular` package bump | Angular | auto | | ||
| | CSS `~` prefix removal in `@ionic/angular` imports | Angular | auto | | ||
| | Add `provideZoneChangeDetection()` to a standalone bootstrap (keep Zone.js) | Angular | auto | | ||
| | NgModule bootstrap zone provider | Angular | report | | ||
| | `@ionic/react` + React Router v6 bumps, drop `@types/react-router*` | React | auto | | ||
| | `<Route exact>` removal, `component={X}` -> `element={<X />}` | React | auto | | ||
| | React Router v6: removed imports, `IonRedirect`, `render`/non-identifier `component`, `history` prop, regex paths | React | report | | ||
| | `@ionic/vue` + Vue Router 5 + Vue 3.5 bumps | Vue | auto | | ||
| | `next()` in navigation guards | Vue | report | | ||
| | `autocorrect="off"` on `ion-input`/`ion-searchbar` | all | auto | | ||
| | Legacy picker (`ion-picker-legacy`, `pickerController`, removed types) | all | report | | ||
| | `ion-img` deprecation | all | report | | ||
| | `ion-nav` router removal (`setRouteId`/`getRouteId`/`updateURL`) | all | report | | ||
|
|
||
| ### What it can't detect | ||
|
|
||
| Some v9 breaks are runtime behavior changes with no reliable source signal, so | ||
| the tool leaves them out rather than guess. Check these by hand against the | ||
| [migration guide](https://ionicframework.com/docs/updating/9-0): | ||
|
|
||
| - `ion-modal`'s `handleBehavior` now defaults to `"cycle"`. A sheet modal with a | ||
| handle becomes focusable and cycles its breakpoints. Set `handleBehavior="none"` | ||
| to keep the handle inert. | ||
| - `ion-select`'s `ionChange` only fires on an actual change now, and the action | ||
| sheet's `selected` role is gone. Code that ran on every confirmation, or read | ||
| that role, needs a look. | ||
| - Platform detection no longer honors Capacitor 2's `isNative` flag, so a | ||
| Capacitor 2 app reports web from `isPlatform('capacitor')` and `'hybrid'`. | ||
| Upgrade to Capacitor 7 or later. | ||
| - In React and Vue the `swipeBackEnabled` config is read once when the outlet | ||
| mounts. If you toggle it at runtime, move to the `swipeGesture` prop on | ||
| `ion-router-outlet`. Setting it once at startup still works, so most apps need | ||
| no change, which is why we don't flag it. | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. Detect the installed framework and major version from `package.json`. | ||
| 2. Select the migrations whose version range applies. A project already on the | ||
| target major selects nothing, so a finished migration doesn't run again. | ||
| 3. Apply the auto-fixes and collect the report-only findings. | ||
| 4. Format the changed files with the project's own Prettier, so the AST-based | ||
| edits come out as clean diffs. Pass `--no-format` to skip it. | ||
| 5. Print a grouped summary of what was fixed and what's left for you. | ||
|
|
||
| Transforms use `ts-morph` to locate nodes for TypeScript and TSX, and a small | ||
| quote-aware scanner for HTML and Vue templates, so it doesn't touch string | ||
| literals, comments, or unrelated code. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - Single-shot. A project migrator isn't idempotent, and the standalone import | ||
| swap in particular will corrupt already-migrated code if you re-run it. The | ||
| version gate prevents that once the `@ionic/*` bump has landed. | ||
| - Angular zoneless is auto-fixed only for the standalone `bootstrapApplication` | ||
| shape. NgModule apps are flagged for manual migration instead. | ||
| - Angular inline templates (a `template:` string in a decorator) and `.js`/`.jsx` | ||
| files are report-only for template changes. The auto-fix covers external | ||
| `.html`, `.vue`, and `.tsx`. | ||
| - `ion-img` is report-only. It's a deprecation, not a v9 break. | ||
| - The template scanner is best-effort, not a full HTML parser. | ||
|
|
||
| ## Extending | ||
|
|
||
| Add a migration by dropping a file under `src/migrations/v<major>/` that exports | ||
| a `Migration` (see `src/types.ts`) and registering it in | ||
| `src/migrations/index.ts`. Give it a `detect()` and, when the change is safe to | ||
| automate, a `fix()`, plus `fromMajor`/`toMajor` for version scoping and a | ||
| fixture-backed test. The engine handles selection, ordering, git safety, | ||
| formatting, and reporting. | ||
|
|
||
| ## Development | ||
|
|
||
| ```sh | ||
| npm test # Vitest | ||
| npm run lint # tsc --noEmit | ||
| npm run build # emit dist/ | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this?