This chapter introduces changes related to page entries when upgrading from Modern.js 2.0 to 3.0.
Modern.js 3.0 has optimized and simplified the entry mechanism. The main changes include:
index.[jt]sx to entry.[jt]sxcreateRoot and render APIsApp.config and config exports from routes/layout need to be migratedApp.init and init exports from routes/layout need to be changed to runtime pluginsBefore starting migration, first identify the entry type used in your project.
Modern.js scans directories and identifies entries that meet any of the following conditions:
routes/ directory → Convention-based routing entryApp.[jt]sx? file → Self-controlled routing entryindex.[jt]sx? file (2.0) or entry.[jt]sx? file (3.0) → Custom entrySingle Entry Application: Scans the src/ directory by default
src/
├── routes/ # or
├── App.tsx # or
└── index.tsx # 2.0 versionMultiple Entry Application: Scans first-level subdirectories under src/
src/
├── entry1/
│ └── routes/ # Each subdirectory is an entry
└── entry2/
└── App.tsxYou can modify the entry scanning directory through the source.entriesDir configuration.
The migration operations in this section only need to be performed when the corresponding usage actually exists in the project, such as bootstrap function, App.config/App.init, config/init functions in routes/layout.tsx, etc.
If your project uses a custom entry file (index.[jt]sx), you need to rename it to entry.[jt]sx.
2.0 Version:
src/
└── index.tsx3.0 Version:
src/
└── entry.tsxIf your entry file exports a function that receives App and bootstrap parameters, you need to use the new API instead.
2.0 Version:
export default (App: React.ComponentType, bootstrap: () => void) => {
// Perform initialization operations
initSomething().then(() => {
bootstrap();
});
};3.0 Version:
import { createRoot } from '@modern-js/runtime/react';
import { render } from '@modern-js/runtime/browser';
// Create root component
const ModernRoot = createRoot();
// Perform initialization operations
async function beforeRender() {
await initSomething();
}
// Render application
beforeRender().then(() => {
render(<ModernRoot />);
});createRoot() corresponds to the component generated by the routes/ directory or exported by App.tsxrender() function is used to handle rendering and mounting componentsIf you defined App.config in App.[tj]sx, you need to migrate it to the runtime configuration file.
2.0 Version:
const App = () => {
return <div>Hello</div>;
};
App.config = {
router: {
supportHtml5History: true,
},
};
export default App;3.0 Version:
Create modern.runtime.ts in the same directory as the entry:
import { defineRuntimeConfig } from '@modern-js/runtime';
export default defineRuntimeConfig({
router: {
supportHtml5History: true,
},
});
Modern.js 3.0 no longer supports configuring runtime in modern.config.ts, you must use the modern.runtime.ts file.
If you defined App.init in App.[tj]sx, you need to change it to a runtime plugin.
2.0 Version:
const App = () => {
return <div>Hello</div>;
};
App.init = context => {
context.store = createStore();
context.request = (url: string) => fetch(url);
};
export default App;3.0 Version:
import type { RuntimePlugin } from '@modern-js/runtime';
import { defineRuntimeConfig } from '@modern-js/runtime';
const initPlugin = (): RuntimePlugin => ({
name: 'init-plugin',
setup: api => {
return {
init({ context }) {
context.store = createStore();
context.request = (url: string) => fetch(url);
},
};
},
});
export default defineRuntimeConfig({
plugins: [initPlugin()],
});If you exported a config function in routes/layout.tsx, you need to migrate it to the runtime configuration file.
2.0 Version:
export const config = () => {
return {
router: {
supportHtml5History: true,
},
};
};
export default function Layout() {
return <Outlet />;
}3.0 Version:
export default function Layout() {
return <Outlet />;
}import { defineRuntimeConfig } from '@modern-js/runtime';
export default defineRuntimeConfig({
router: {
supportHtml5History: true,
},
});If you exported an init function in routes/layout.tsx, you need to change it to a runtime plugin.
2.0 Version:
export const init = context => {
context.request = (url: string) => fetch(url);
};
export default function Layout() {
return <Outlet />;
}3.0 Version:
export default function Layout() {
return <Outlet />;
}import type { RuntimePlugin } from '@modern-js/runtime';
import { defineRuntimeConfig } from '@modern-js/runtime';
const initPlugin = (): RuntimePlugin => ({
name: 'init-plugin',
setup: api => {
return {
init({ context }) {
context.request = (url: string) => fetch(url);
},
};
},
});
export default defineRuntimeConfig({
plugins: [initPlugin()],
});For multi-entry applications, you need to use function-form configuration in src/modern.runtime.ts, returning different runtime configurations based on entry names.
Directory Structure:
src/
├── modern.runtime.ts # Unified runtime configuration file
├── entry1/
│ └── routes/
└── entry2/
└── App.tsxConfiguration Example:
import { defineRuntimeConfig } from '@modern-js/runtime';
export default defineRuntimeConfig(entryName => {
// Common configuration
const commonConfig = {
plugins: [commonPlugin()],
};
// Return specific configuration based on entry name
if (entryName === 'entry1') {
return {
...commonConfig,
router: {
supportHtml5History: true,
},
plugins: [...commonConfig.plugins, entry1Plugin()],
};
}
if (entryName === 'entry2') {
return {
...commonConfig,
router: {
supportHtml5History: false,
},
plugins: [...commonConfig.plugins, entry2Plugin()],
};
}
// Default configuration
return commonConfig;
});entryName parameter corresponds to the entry directory namename in package.json): the directory name is passed inMerge configurations for the same entry: If both App.config/App.init and config/init from routes/layout.tsx exist for the same entry, you need to merge them into the corresponding entry configuration in the src/modern.runtime.ts file
Multiple plugins in parallel: Multiple runtime plugins can be configured in parallel in the plugins array
Clean up old code: After migration is complete, remember to delete from the original files:
App.config propertyApp.init methodconfig export from routes/layout.tsxinit export from routes/layout.tsxAssume you have a 2.0 version multi-entry application:
2.0 Version Directory Structure:
src/
├── main/
│ ├── routes/
│ │ └── layout.tsx # Contains config and init
│ └── App.tsx # Contains App.config and App.init
└── admin/
└── routes/
└── layout.tsx # Contains config and init2.0 Version Configuration:
const App = () => <div>Main App</div>;
App.config = {
router: { supportHtml5History: true },
};
App.init = context => {
context.mainData = 'main';
};export const config = () => ({
router: { supportHtml5History: false },
});
export const init = context => {
context.adminData = 'admin';
};3.0 Version After Migration:
src/
├── modern.runtime.ts # New unified configuration file
├── main/
│ ├── routes/
│ │ └── layout.tsx # Removed config and init
│ └── App.tsx # Removed App.config and App.init
└── admin/
└── routes/
└── layout.tsx # Removed config and initimport { defineRuntimeConfig } from '@modern-js/runtime';
import type { RuntimePlugin } from '@modern-js/runtime';
// Main entry initialization plugin
const mainInitPlugin = (): RuntimePlugin => ({
name: 'main-init-plugin',
setup: api => {
return {
init({ context }) {
context.mainData = 'main';
},
};
},
});
// Admin entry initialization plugin
const adminInitPlugin = (): RuntimePlugin => ({
name: 'admin-init-plugin',
setup: api => {
return {
init({ context }) {
context.adminData = 'admin';
},
};
},
});
export default defineRuntimeConfig(entryName => {
if (entryName === 'main') {
return {
router: {
supportHtml5History: true,
},
plugins: [mainInitPlugin()],
};
}
if (entryName === 'admin') {
return {
router: {
supportHtml5History: false,
},
plugins: [adminInitPlugin()],
};
}
return {};
});