Skip to content

Commit b43988e

Browse files
authored
feat: add indep bff doc (#6803)
1 parent a28e69d commit b43988e

File tree

24 files changed

+482
-124
lines changed

24 files changed

+482
-124
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@modern-js/create-request': patch
3+
'@modern-js/main-doc': patch
4+
'@modern-js/plugin-bff': patch
5+
---
6+
7+
feat: BFF 跨项目调用支持配置域名,补充文档
8+
feat: BFF cross-project-invocation supports configuration of domain, add doc

packages/cli/plugin-bff/src/cli.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const bffPlugin = (): CliPlugin<AppTools> => ({
117117

118118
const handleCrossProjectInvocation = async (isBuild = false) => {
119119
const { bff } = api.useResolvedConfigContext();
120-
if (bff?.enableCrossProjectInvocation) {
120+
if (bff?.crossProject) {
121121
if (!isBuild) {
122122
await compileApi();
123123
}
@@ -242,7 +242,7 @@ export const bffPlugin = (): CliPlugin<AppTools> => ({
242242
const appContext = api.useAppContext();
243243
const config = api.useResolvedConfigContext();
244244

245-
if (config?.bff?.enableCrossProjectInvocation) {
245+
if (config?.bff?.crossProject) {
246246
return [appContext.apiDirectory];
247247
} else {
248248
return [];
@@ -260,7 +260,8 @@ export const bffPlugin = (): CliPlugin<AppTools> => ({
260260
if (
261261
!isPrivate &&
262262
(eventType === 'change' || eventType === 'unlink') &&
263-
filename.startsWith(`${relativeApiPath}/`)
263+
filename.startsWith(`${relativeApiPath}/`) &&
264+
(filename.endsWith('.ts') || filename.endsWith('.js'))
264265
) {
265266
await handleCrossProjectInvocation();
266267
}

packages/cli/plugin-bff/src/utils/clientGenerator.ts

Lines changed: 89 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ interface FileDetails {
2525
relativeTargetDistDir: string;
2626
exportKey: string;
2727
}
28+
const API_DIR = 'api';
29+
const PLUGIN_DIR = 'plugin';
30+
const RUNTIME_DIR = 'runtime';
31+
const CLIENT_DIR = 'client';
32+
33+
const EXPORT_PREFIX = `./${API_DIR}/`;
34+
const TYPE_PREFIX = `${API_DIR}/`;
35+
2836
export async function readDirectoryFiles(
2937
appDirectory: string,
3038
directory: string,
@@ -48,7 +56,7 @@ export async function readDirectoryFiles(
4856
const parsedPath = path.parse(relativePath);
4957

5058
const targetDir = path.join(
51-
`./${relativeDistPath}/client`,
59+
`./${relativeDistPath}/${CLIENT_DIR}`,
5260
parsedPath.dir,
5361
`${parsedPath.name}.js`,
5462
);
@@ -83,6 +91,31 @@ export async function readDirectoryFiles(
8391
return filesList;
8492
}
8593

94+
function mergePackageJson(
95+
packageJson: any,
96+
files: string[],
97+
typesVersion: Record<string, any>,
98+
exports: Record<string, any>,
99+
) {
100+
packageJson.files = [...new Set([...(packageJson.files || []), ...files])];
101+
102+
packageJson.typesVersions ??= {};
103+
const starTypes = packageJson.typesVersions['*'] || {};
104+
Object.keys(starTypes).forEach(
105+
k => k.startsWith(TYPE_PREFIX) && delete starTypes[k],
106+
);
107+
packageJson.typesVersions['*'] = {
108+
...starTypes,
109+
...(typesVersion['*'] || {}),
110+
};
111+
112+
packageJson.exports ??= {};
113+
Object.keys(packageJson.exports).forEach(
114+
k => k.startsWith(EXPORT_PREFIX) && delete packageJson.exports[k],
115+
);
116+
Object.assign(packageJson.exports, exports);
117+
}
118+
86119
async function writeTargetFile(absTargetDir: string, content: string) {
87120
await fs.mkdir(path.dirname(absTargetDir), { recursive: true });
88121
await fs.writeFile(absTargetDir, content);
@@ -96,51 +129,60 @@ async function setPackage(
96129
}[],
97130
appDirectory: string,
98131
relativeDistPath: string,
99-
relativeApiPath: string,
100132
) {
101133
try {
102134
const packagePath = path.resolve(appDirectory, './package.json');
103135
const packageContent = await fs.readFile(packagePath, 'utf8');
104136
const packageJson = JSON.parse(packageContent);
105137

106-
packageJson.exports = packageJson.exports || {};
107-
packageJson.typesVersions = packageJson.typesVersions || { '*': {} };
108-
109-
files.forEach(file => {
110-
const exportKey = `./api/${file.exportKey}`;
111-
const jsFilePath = `./${file.targetDir}`;
112-
const typePath = file.relativeTargetDistDir;
113-
114-
packageJson.exports[exportKey] = {
115-
import: jsFilePath,
116-
types: typePath,
117-
};
118-
119-
packageJson.typesVersions['*'][`api/${file.exportKey}`] = [typePath];
120-
});
138+
const addFiles = [
139+
`${relativeDistPath}/${CLIENT_DIR}/**/*`,
140+
`${relativeDistPath}/${RUNTIME_DIR}/**/*`,
141+
`${relativeDistPath}/${PLUGIN_DIR}/**/*`,
142+
];
121143

122-
packageJson.exports['./plugin'] = {
123-
require: `./${relativeDistPath}/plugin/index.js`,
124-
types: `./${relativeDistPath}/plugin/index.d.ts`,
144+
const typesVersions = {
145+
'*': files.reduce(
146+
(acc, file) => {
147+
const typeFilePath = `./${file.targetDir}`.replace('js', 'd.ts');
148+
return {
149+
...acc,
150+
[`${TYPE_PREFIX}${file.exportKey}`]: [typeFilePath],
151+
};
152+
},
153+
{
154+
[RUNTIME_DIR]: [`./${relativeDistPath}/${RUNTIME_DIR}/index.d.ts`],
155+
[PLUGIN_DIR]: [`./${relativeDistPath}/${PLUGIN_DIR}/index.d.ts`],
156+
},
157+
),
125158
};
126159

127-
packageJson.exports['./runtime'] = {
128-
import: `./${relativeDistPath}/runtime/index.js`,
129-
types: `./${relativeDistPath}/runtime/index.d.ts`,
130-
};
131-
packageJson.typesVersions['*'].runtime = [
132-
`./${relativeDistPath}/runtime/index.d.ts`,
133-
];
134-
packageJson.typesVersions['*'].plugin = [
135-
`./${relativeDistPath}/plugin/index.d.ts`,
136-
];
160+
const exports = files.reduce(
161+
(acc, file) => {
162+
const exportKey = `${EXPORT_PREFIX}${file.exportKey}`;
163+
const jsFilePath = `./${file.targetDir}`;
137164

138-
packageJson.files = [
139-
`./${relativeDistPath}/client/**/*`,
140-
`./${relativeDistPath}/${relativeApiPath}/**/*`,
141-
`./${relativeDistPath}/runtime/**/*`,
142-
`./${relativeDistPath}/plugin/**/*`,
143-
];
165+
return {
166+
...acc,
167+
[exportKey]: {
168+
import: jsFilePath,
169+
types: jsFilePath.replace(/\.js$/, '.d.ts'),
170+
},
171+
};
172+
},
173+
{
174+
[`./${PLUGIN_DIR}`]: {
175+
require: `./${relativeDistPath}/${PLUGIN_DIR}/index.js`,
176+
types: `./${relativeDistPath}/${PLUGIN_DIR}/index.d.ts`,
177+
},
178+
[`./${RUNTIME_DIR}`]: {
179+
import: `./${relativeDistPath}/${RUNTIME_DIR}/index.js`,
180+
types: `./${relativeDistPath}/${RUNTIME_DIR}/index.d.ts`,
181+
},
182+
},
183+
);
184+
185+
mergePackageJson(packageJson, addFiles, typesVersions, exports);
144186

145187
await fs.promises.writeFile(
146188
packagePath,
@@ -151,6 +193,12 @@ async function setPackage(
151193
}
152194
}
153195

196+
export async function copyFiles(from: string, to: string) {
197+
if (await fs.pathExists(from)) {
198+
await fs.copy(from, to);
199+
}
200+
}
201+
154202
async function clientGenerator(draftOptions: APILoaderOptions) {
155203
const sourceList = await readDirectoryFiles(
156204
draftOptions.appDir,
@@ -197,19 +245,18 @@ async function clientGenerator(draftOptions: APILoaderOptions) {
197245
const code = await getClitentCode(source.resourcePath, source.source);
198246
if (code?.value) {
199247
await writeTargetFile(source.absTargetDir, code.value);
248+
await copyFiles(
249+
source.relativeTargetDistDir,
250+
source.targetDir.replace(`js`, 'd.ts'),
251+
);
200252
}
201253
}
202254
logger.info(`Client bundle generate succeed`);
203255
} catch (error) {
204256
logger.error(`Client bundle generate failed: ${error}`);
205257
}
206258

207-
setPackage(
208-
sourceList,
209-
draftOptions.appDir,
210-
draftOptions.relativeDistPath,
211-
draftOptions.relativeApiPath,
212-
);
259+
setPackage(sourceList, draftOptions.appDir, draftOptions.relativeDistPath);
213260
}
214261

215262
export default clientGenerator;

packages/cli/plugin-bff/src/utils/runtimeGenerator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ async function runtimeGenerator({
2929
request?: F;
3030
interceptor?: (request: F) => F;
3131
allowedHeaders?: string[];
32+
setDomain?: (ops?: {
33+
target: 'node' | 'browser';
34+
requestId: string;
35+
}) => string;
3236
requestId?: string;
3337
};
3438
export declare const configure: (options: IOptions) => void;`;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
1. run `@modern-js/create` command:
2+
3+
```bash
4+
npx @modern-js/create@latest myapi
5+
```
6+
7+
2. interactive Q & A interface to initialize the project based on the results, with initialization performed according to the default settings:
8+
9+
```bash
10+
? Please select the programming language: TS
11+
? Please select the package manager: pnpm
12+
```
13+
14+
3. Execute the `new` command,enable BFF:
15+
16+
```bash
17+
? Please select the operation you want to perform Enable optional features
18+
? Please select the feature to enable Enable "BFF"
19+
? Please select BFF type Framework mode
20+
```
21+
22+
23+
4. Execute【[Existing BFF-enabled Projects](/en/guides/advanced-features/bff/cross-project.html#existing-bff-enabled-projects)】to turn on the cross-project call switch.
24+
25+
**Note:** When a project serves solely as a BFF producer, its runtime does not depend on the `/src` source directory. Removing the `/src` directory can help optimize the project's build efficiency.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
["function", "frameworks", "extend-server", "sdk", "upload"]
1+
["function", "frameworks", "extend-server", "sdk", "upload", "cross-project"]
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { PackageManagerTabs } from '@theme';
2+
3+
# Cross-Project Invocation
4+
5+
Based on the BFF architecture, Modern.js provides cross-project invocation capabilities, allowing BFF functions created in one project to be invoked by other projects through integrated calls, enabling function sharing and feature reuse across projects.
6+
Cross-project invocation consists of **producer** and **consumer** sides. The producer is responsible for creating and providing BFF services while generating integrated invocation SDK, and the consumer initiates requests through these SDK.
7+
8+
## BFF Producer
9+
10+
Upgrade Modern.js dependencies to version x.64.4 or higher, then enable cross-project invocation via configuration. Projects with BFF capabilities enabled can act as BFF producers, or you can create standalone BFF applications.
11+
When executing `dev` or `build`, the following artifacts for consumers will be automatically generated:
12+
- API functions under the `dist/client` directory
13+
- Runtime configuration functions under the `dist/runtime` directory
14+
- Interface exports defined in `exports` field of `package.json`
15+
- File list for npm publication specified in `files` field of `package.json`
16+
17+
### Existing BFF-enabled Projects
18+
19+
1. Enable Cross-Project Invocation
20+
21+
Ensure the current project has BFF enabled with API files defined under `api/lambda`. Add the following configuration:
22+
23+
```ts title="modern.config.ts"
24+
export default defineConfig({
25+
bff: {
26+
crossProject: true,
27+
}
28+
});
29+
```
30+
31+
2. Generate SDK Type Files
32+
33+
To provide type hints for the integrated invocation SDK, enable the `declaration` option in TypeScript configuration:
34+
35+
```ts title="tsconfig.json"
36+
"compilerOptions": {
37+
"declaration": true,
38+
}
39+
```
40+
41+
### Create BFF Application
42+
43+
import CreateApi from "@site-docs-en/components/create-bff-api-app"
44+
45+
<CreateApi/>
46+
47+
## BFF Consumer
48+
49+
:::info
50+
You can initiate requests to BFF producers from projects using any framework via the SDK.
51+
:::
52+
53+
### Intra-Monorepo Invocation
54+
55+
When producer and consumer are in the same Monorepo, directly import the SDK. API functions reside under `${package_name}/api`:
56+
57+
```ts title="src/routes/page.tsx"
58+
import { useState, useEffect } from 'react';
59+
import { get as hello } from '${package_name}/api/hello';
60+
61+
export default () => {
62+
const [text, setText] = useState('');
63+
64+
useEffect(() => {
65+
hello().then(setText);
66+
}, []);
67+
return <div>{text}</div>;
68+
};
69+
```
70+
71+
### Cross-Project Invocation
72+
73+
When producer and consumer are in separate repositories, publish the BFF producer as an npm package. The invocation method remains the same as intra-Monorepo.
74+
75+
### Domain Configuration and Extensions
76+
77+
For real-world scenarios requiring custom BFF service domains, use the configuration function:
78+
79+
```ts title="src/routes/page.tsx"
80+
import { configure } from '${package_name}/runtime';
81+
82+
configure({
83+
setDomain() {
84+
return 'https://your-bff-api.com';
85+
},
86+
});
87+
```
88+
89+
The `configure` function from `${package_name}/runtime` supports domain configuration via `setDomain`, interceptors, and custom SDK. When extending both **current project** and **cross-project** SDK on the same page:
90+
91+
```ts title="src/routes/page.tsx"
92+
import { configure } from '${package_name}/runtime';
93+
import { configure as innerConfigure } from '@modern-js/runtime/bff';
94+
import axios from 'axios';
95+
96+
configure({
97+
setDomain() {
98+
return 'https://your-bff-api.com';
99+
},
100+
});
101+
102+
innerConfigure({
103+
async request(...config: Parameters<typeof fetch>) {
104+
const [url, params] = config;
105+
const res = await axios({
106+
url: url as string,
107+
method: params?.method as Method,
108+
data: params?.body,
109+
headers: {
110+
'x-header': 'innerConfigure',
111+
},
112+
});
113+
return res.data;
114+
},
115+
});
116+
```

0 commit comments

Comments
 (0)