Skip to content

Commit 486af8a

Browse files
committed
feat: add source.enableAsyncPreEntry for async entry
1 parent cb33dae commit 486af8a

File tree

16 files changed

+301
-4
lines changed

16 files changed

+301
-4
lines changed

.changeset/friendly-planes-help.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modern-js/app-tools': minor
3+
'@modern-js/runtime': minor
4+
---
5+
6+
feat: add `source.enableAsyncPreEntry` for async entry scenarios
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
title: enableAsyncPreEntry
3+
---
4+
5+
# source.enableAsyncPreEntry
6+
7+
- **Type:** `boolean`
8+
- **Default:** `false`
9+
10+
This option only takes effect when `source.enableAsyncEntry` is enabled.
11+
12+
When enabled, Modern.js injects the modules configured in `source.preEntry` at the top of the auto-generated entry file (`index.jsx`), so `preEntry` still runs before the real entry logic in async entry scenarios.
13+
14+
Meanwhile, when both `source.enableAsyncEntry` and `source.enableAsyncPreEntry` are enabled, Modern.js will not pass `source.preEntry` into builder config to avoid duplicate injection or injection into an unexpected entry.
15+
16+
## Example
17+
18+
```ts title="modern.config.ts"
19+
export default defineConfig({
20+
source: {
21+
enableAsyncEntry: true,
22+
enableAsyncPreEntry: true,
23+
preEntry: ['./src/pre-a.ts', './src/pre-b.ts'],
24+
},
25+
});
26+
```
27+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
title: enableAsyncPreEntry
3+
---
4+
5+
# source.enableAsyncPreEntry
6+
7+
- **类型:** `boolean`
8+
- **默认值:** `false`
9+
10+
该配置仅在 `source.enableAsyncEntry` 开启时生效。
11+
12+
开启后,Modern.js 会在自动生成的入口文件(`index.jsx`)最上方依次插入 `source.preEntry` 中配置的模块 import,确保异步入口场景下 `preEntry` 也会在真正的入口代码之前执行。
13+
14+
同时,当 `source.enableAsyncEntry``source.enableAsyncPreEntry` 同时开启时,Modern.js 不会将 `source.preEntry` 传递给构建器配置,避免重复注入或注入到不期望的 entry 中。
15+
16+
## 示例
17+
18+
```ts title="modern.config.ts"
19+
export default defineConfig({
20+
source: {
21+
enableAsyncEntry: true,
22+
enableAsyncPreEntry: true,
23+
preEntry: ['./src/pre-a.ts', './src/pre-b.ts'],
24+
},
25+
});
26+
```
27+

packages/runtime/plugin-runtime/src/cli/code.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/// <reference types="node" />
2+
13
import path from 'path';
24
import type {
35
AppNormalizedConfig,
@@ -6,7 +8,7 @@ import type {
68
AppToolsNormalizedConfig,
79
} from '@modern-js/app-tools';
810
import type { Entrypoint } from '@modern-js/types';
9-
import { fs } from '@modern-js/utils';
11+
import { fs, formatImportPath } from '@modern-js/utils';
1012
import {
1113
ENTRY_BOOTSTRAP_FILE_NAME,
1214
ENTRY_POINT_FILE_NAME,
@@ -45,15 +47,54 @@ function getSSRMode(
4547
}
4648
}
4749

50+
const normalizePreEntry = (preEntry: unknown): string[] => {
51+
if (!preEntry) {
52+
return [];
53+
}
54+
if (Array.isArray(preEntry)) {
55+
return preEntry.filter(
56+
(v): v is string => typeof v === 'string' && v.length > 0,
57+
);
58+
}
59+
if (typeof preEntry === 'string') {
60+
return preEntry ? [preEntry] : [];
61+
}
62+
return [];
63+
};
64+
65+
const resolvePreEntryImportPath = ({
66+
preEntry,
67+
appDirectory,
68+
srcDirectory,
69+
internalSrcAlias,
70+
}: {
71+
preEntry: string;
72+
appDirectory: string;
73+
srcDirectory: string;
74+
internalSrcAlias: string;
75+
}) => {
76+
const absPath = path.isAbsolute(preEntry)
77+
? preEntry
78+
: path.resolve(appDirectory, preEntry);
79+
80+
// Prefer importing via the internal src alias to keep imports shorter and stable.
81+
if (absPath.startsWith(srcDirectory)) {
82+
return formatImportPath(absPath.replace(srcDirectory, internalSrcAlias));
83+
}
84+
return formatImportPath(absPath);
85+
};
86+
4887
export const generateCode = async (
4988
entrypoints: Entrypoint[],
5089
appContext: AppToolsContext<'shared'>,
5190
config: AppToolsNormalizedConfig,
5291
hooks: AppToolsFeatureHooks<'shared'>,
5392
) => {
5493
const { mountId } = config.html;
55-
const { enableAsyncEntry } = config.source;
94+
const { enableAsyncEntry, enableAsyncPreEntry, preEntry } = config.source;
95+
const shouldInjectAsyncPreEntry = !!enableAsyncEntry && !!enableAsyncPreEntry;
5696
const {
97+
appDirectory,
5798
runtimeConfigFile,
5899
internalDirectory,
59100
internalSrcAlias,
@@ -85,8 +126,10 @@ export const generateCode = async (
85126
metaName,
86127
entryName,
87128
mountId,
88-
urlPath: serverRoutes.find(route => route.entryName === entryName)
89-
?.urlPath,
129+
urlPath: serverRoutes.find(
130+
(route: { entryName: string; urlPath?: string }) =>
131+
route.entryName === entryName,
132+
)?.urlPath,
90133
isNestedRouter: entrypoint.nestedRoutesEntry,
91134
});
92135
} else {
@@ -104,6 +147,28 @@ export const generateCode = async (
104147
});
105148
}
106149

150+
// Only works with `enableAsyncEntry`:
151+
// inject `source.preEntry` to the top of the generated entry file
152+
// (`index.jsx`), so it runs before the real entry code even when the
153+
// build entry is `bootstrap.jsx`.
154+
if (shouldInjectAsyncPreEntry) {
155+
const preEntries = normalizePreEntry(preEntry);
156+
if (preEntries.length > 0) {
157+
const injected = preEntries
158+
.map(item => {
159+
const importPath = resolvePreEntryImportPath({
160+
preEntry: item,
161+
appDirectory,
162+
srcDirectory,
163+
internalSrcAlias,
164+
});
165+
return `import '${importPath}';`;
166+
})
167+
.join('\n');
168+
indexCode = `${injected}\n${indexCode}`;
169+
}
170+
}
171+
107172
const indexFile = path.resolve(
108173
internalDirectory,
109174
`./${entryName}/${ENTRY_POINT_FILE_NAME}`,

packages/solutions/app-tools/src/builder/generator/createBuilderProviderConfig.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ export function createBuilderProviderConfig<B extends Bundler>(
6767
},
6868
};
6969

70+
// Only works with `enableAsyncEntry`:
71+
// when both are enabled, `preEntry` should be handled by Modern.js entry code
72+
// generation (inject into `index.jsx`), not by builder.
73+
if (config.source?.enableAsyncEntry && config.source?.enableAsyncPreEntry) {
74+
delete (config.source as any).preEntry;
75+
}
76+
7077
modifyOutputConfig(config, appContext);
7178

7279
return config as AppNormalizedConfig<B>;

packages/solutions/app-tools/src/config/default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function createDefaultConfig(
4141
mainEntryName: MAIN_ENTRY_NAME,
4242
enableAsyncEntry: false,
4343
enableCustomEntry: false,
44+
enableAsyncPreEntry: false,
4445
disableDefaultEntries: false,
4546
entriesDir: './src',
4647
configDir: './config',

packages/solutions/app-tools/src/types/config/source.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export type Entries = Record<string, Entry>;
2727

2828
export interface SourceUserConfig
2929
extends NonNullable<UniBuilderConfig['source']> {
30+
/**
31+
* Add code before each page entry. It will be executed before the page code.
32+
* @default []
33+
*/
34+
preEntry?: string | string[];
3035
/**
3136
* Used to configure custom page entries.
3237
*/
@@ -47,6 +52,13 @@ export interface SourceUserConfig
4752
* @default false
4853
*/
4954
enableCustomEntry?: boolean;
55+
/**
56+
* When enabled, framework will inject `source.preEntry` into the top of the
57+
* auto-generated entry file (`index.jsx`) in async entry scenarios.
58+
* This option only takes effect when `source.enableAsyncEntry` is enabled.
59+
* @default false
60+
*/
61+
enableAsyncPreEntry?: boolean;
5062
/**
5163
* Used to disable the functionality of automatically identifying page entry points based on directory structure.
5264
* @default false
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { applyBaseConfig } from '../../../../utils/applyBaseConfig';
2+
3+
export default applyBaseConfig({
4+
server: {
5+
ssr: {
6+
mode: 'string',
7+
},
8+
},
9+
source: {
10+
enableAsyncEntry: true,
11+
enableAsyncPreEntry: true,
12+
preEntry: ['./src/pre.ts'],
13+
},
14+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "ssr-base-async-pre-entry-test",
3+
"version": "2.66.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "modern dev"
7+
},
8+
"dependencies": {
9+
"@modern-js/runtime": "workspace:*",
10+
"react": "^19.2.4",
11+
"react-dom": "^19.2.4"
12+
},
13+
"devDependencies": {
14+
"@modern-js/app-tools": "workspace:*",
15+
"@modern-js/tsconfig": "workspace:*",
16+
"@types/react": "^19.2.14",
17+
"@types/react-dom": "^19.2.3",
18+
"typescript": "^5"
19+
}
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types='@modern-js/app-tools/types' />

0 commit comments

Comments
 (0)