This skill is used for the v2→v3 migration of a single Modern.js application. Goal: complete the safely rewritable parts, and narrow the remaining risks into a clear manual checklist. Rules and examples are based on the repository's guides/upgrade/* real documentation.
Usage principles
- The caller first determines
projectDir, all modifications are limited toprojectDir - Do not read all of
references/at the start; only load on demand when a manual item is hit - Commit once after each successful step ends (see
references/commit-changes.md)
Output requirements
- Progress brief:
[X/6] 开始/完成/跳过/失败 - Record non-blocking issues and continue; for blocking issues stop immediately and explain the step, reason, and recovery method
Pre-checks
git -C <projectDir> status --porcelain
Stop when the workspace is non-empty, prompt to first git commit/git stash. It is recommended to migrate on a clean branch or worktree, to facilitate rollback.
Execution steps
Step 1: Scan the project and generate the migration context
node scripts/scan-project.mjs <projectDir>
Produce <projectDir>/.agents/runs/modernjs-migrate/context.json: determine v2/v3, Node version, entry type, hit features and v2Signals. When the script fails (not v2/v3, Node too low), stop directly and show the reason.migrationState=v3 Treat as continued migration.
monorepo / workspace projects:
@modern-js/app-toolsWhen using protocols such asworkspace:*/link:/catalog:, the major version cannot be determined from the version number. At this point only when hittingv2-only structural signals(top-levelruntime、appTools({ bundler })、applyBaseConfig、@modern-js/plugin-tailwindcss、@modern-js/runtime/bff|serverimport、App.config/init、src/pages, customserver/index) is it judged as v2;if there is no signal at all, treat it as ambiguous and block(non-zero exit, do not write context), to avoid mistakenly migrating a workspace application that is already v3.routes/src/modern.runtime.ts/appTools()is not counted as a v2 signal (v3 also has it).
Convention-based routing red lines:
entryType=routesWhensrc/routes/page.*andsrc/routes/layout.*are v3 standard structures, they must be preserved. Do not renamepage.tsxtoindex.tsx, do not movelayout.tsxtolegacy-*, do not generatesrc/entry.*orsrc/legacy-app/src/legacy-routesout of thin air. If you need to handleroutes/layout'sconfig/initexport, only migrate the runtime configuration logic, do not change the routing file naming.
Step 2: Perform safely automatable rewrites
node scripts/migrate.mjs <projectDir>
Automatically complete (based on guides/upgrade/*):
- Pre-protection:
workspace/link/catalogprotocol + no v2-only signals → abort directly (exit 1, do not modify any files), without relying on scan - Target version: when
--tois not passed, the latest stable v3 version of@modern-js/app-tools@3will be detected via npm as the target version; only pass--to=<version>explicitly when the version needs to be pinned. If the current environment cannot access the npm registry, the script will abort and prompt to explicitly pass in the target version,it will not fall back to3.0.0。 - Dependencies: for pinned versions (
^2.x) of@modern-js/*, upgrade to the detected latest v3 target version;workspace/link/catalogprotocol dependencies are kept unchanged(upgraded together with the monorepo as a whole, put into the manual checklist); remove@modern-js/plugin-tailwindcss - Configuration entry form:
defineConfig({})/defineConfig<'rspack'>({})/export default {}/module.exports = {}(JS static config) are all handled as the main path; functional/dynamicdefineConfig(() => ({}))if it contains runtime, goes into the manual checklist - import paths:
@modern-js/runtime/bff→@modern-js/plugin-bff/client、@modern-js/runtime/server→@modern-js/server-runtime, andadd the corresponding dependencies(@modern-js/plugin-bff/@modern-js/server-runtime; version protocol: for regular semver use the target version;workspace:/catalog:(name-independent) reuse the existingapp-tools/runtimespec;link:/file:/portal:/npm:(pointing to a specific package path/alias)do not copy verbatim, goes into the manual checklist); when BFF is matched, givemodern.configtop-level plugins append at the endbffPlugin()(keep[appTools(), bffPlugin()]order; when necessary, on the@modern-js/app-toolsimport addappTools; if it cannot be added, it goes into the manual checklist, do not write a half-finished product) - Robustness (comments/strings): import'sdetection and rewriting share the same real specifier scanner(
eachModuleSpecifier, coveringimport/export-from/side-effect import/import()/require(), includingimport(/* magic */ '...')dynamic imports with comments like this);dev/serveronly at theconfig object top levelmigrate (nestedtools.dev.portis not touched); tailwind only deletes real import lines and realtailwindcssPlugin()calls (by real specifier offset, without harming ordinary strings/comment examples, and cleaning up dangling commas after removal);defineConfig/runtime/appTools({ bundler })/applyBaseConfig/dev: { port }/@modern-js/runtime/bff|serverin comments/ordinary strings will not be mistaken for real config, v2 signals, or imports; only judgments that need a string "value" (such asssr.mode) keep the string - Config:
appTools({ bundler })→appTools()(v3 defaults to Rspack, only delete thebundlerparameter);modern.configtop-levelruntimeblock → merge intosrc/modern.runtime.ts(v3 no longer supports configuring runtime in config; only merge intoemptydefineRuntimeConfig({}), non-empty/functional goes into the manual checklist without overwriting);dev.port→server.port(only move top-levelport, keep the remaining fields of the dev block; nested ones such asdev.client.portare not touched); remove the tailwind plugin and writepostcss.config.cjs - v3 config item renaming(based on
guides/upgrade/config.mdx, structured item by item):output.cssModuleLocalIdentName→output.cssModules.localIdentName、disableCssExtract→injectStyles、disableFilenameHash/disableMinimize/disableSourceMap→filenameHash/minify/sourceMap(boolean negated),enableInlineScripts/enableInlineStyles→inlineScripts/inlineStyles;source.moduleScopes/enableCustomEntry/disableEntryDirsremoved,resolveMainFields/resolveExtensionPrefix、output.enableLatestDecorators=truegoes to manual;html.appIconstring→{ icons:[{src,size:180}] }、disableHtmlFolder→outputStructure、xxxByEntriesgoes to manual;tools.webpack→rspack、webpackChain→bundlerChain、devServergoes to manual (splitdev.*). Non-boolean/non-literal values are not silently changed, they go to manual - Custom Server: remove the v3-deprecated
@modern-js/plugin-serverdependency +serverPlugin()call + import;server/index.*when there isunstableMiddleware/afterRender→ generate abuildableofserver/modern.server.*skeleton (emptydefineServerConfig, original logic preserved as comments), Hono Context semantic migration goes into the manual checklist - Entry:
src/index.*→src/entry.*(bootstrap arrow or function declarationboth forms are rewritten tocreateRoot()/render(); unrecognized bootstrap forms go to manual, do not pretend success);App.configextract tosrc/modern.runtime.ts(if it already exists, do not overwrite, goes into the manual checklist) - Runtime:
useRuntimeContext()→ for React 19+ useuse(RuntimeContext), for <19 useuseContext(RuntimeContext)(keep react default/namespace import;useRuntimeContext as 别名goes into the manual checklist without fake rewriting) - Routing:
src/pages→src/routes(when there are no routes), and per the v3 convention mappages/index.*toroutes/page.*、pages/foo/index.*map toroutes/foo/page.*, synchronously rewrite relative import references, and put any leftovers into the manual checklist; the migration output must not containroutes/**/index.*as a page file;when the rootroutes/layout.*is missing, automatically generate a minimalOutletlayout(the v3 conventional routing requires a root layout) - Scripts: delete the scripts in
package.jsonthat callmodern new/modern upgrade(v3 has removed these two commands)
applyBaseConfig(...)configuration wrapped by(repository integration test helper / non-standard user configuration):runtime/plugins/dev.port/appTools bundleretc.structural migrations always go into the manual checklist(the report is marked "structural migration incomplete"); only perform file-level safe rewrites such as dependency upgrades / import paths / tailwind removal, and do not semi-automatically break the configuration inside the wrapper.package.jsonthemodernConfig.runtimealso goes into the manual checklist.
After completion, view the .agents/runs/modernjs-migrate/report.json of changed / manual. After this step succeeds, execute references/commit-changes.md。
Step 3: Handle each item in the manual checklist one by one (read references as needed)
Based on the report's manual list, read whichever document matches the item hit:
| Manual item | Reference |
|---|---|
App.init / routes/layout the config/init export,modernConfig.runtime, non-empty/functional runtime |
references/migrate-entry.md |
custom Web Server (unstableMiddleware / afterRender) |
references/migrate-custom-server.md |
html.appIcon string,server.ssr.mode, webpack custom configuration,applyBaseConfig(...) structural migration |
references/migrate-config.md |
useRuntimeContext as 别名 call |
references/migrate-entry.md |
After handling each item, execute references/commit-changes.md。
Step 4: Install dependencies
bash scripts/install-deps.sh <projectDir>
After the lock file changes, execute references/commit-changes.md(with --include-lockfiles)。
Step 5: Lint auto-fix
bash scripts/run-lint.sh <projectDir>
After recording a failure, continue; do not treat it as a migration failure.
Step 6: Build verification + final report
Run modern build(key route smoke if necessary). Final report: successful steps, skipped items, failed items, manual handling items, follow-up suggestions.
Safety red lines
- Prefer structural rewrites; use plain text replacement only for unambiguous items (import paths).
- Do not manually modify
pnpm-lock.yaml/dist/CHANGELOG/node_modules/ secret。 - Complex or uncertain items always go into the manual checklist; do not blindly modify.