From 6ea2970535da5b60f01e82377bfa5641a00d1559 Mon Sep 17 00:00:00 2001 From: zhangxiang Date: Thu, 28 Nov 2024 22:03:20 +0800 Subject: [PATCH] feat: bff support independent project --- .changeset/mean-bobcats-relax.md | 13 ++ packages/cli/core/src/types/context.ts | 2 + packages/cli/plugin-bff/package.json | 5 + packages/cli/plugin-bff/src/cli.ts | 185 ++++++++++++++---- packages/cli/plugin-bff/src/create-request.ts | 1 + packages/cli/plugin-bff/src/server.ts | 19 +- .../plugin-bff/src/utils/client-generator.ts | 185 ++++++++++++++++++ .../plugin-bff/src/utils/plugin-generator.ts | 32 +++ .../plugin-bff/src/utils/runtime-generator.ts | 27 +++ .../cli/plugin-bff/src/utils/server-plugin.ts | 38 ++++ .../tests/__snapshots__/cli.test.ts.snap | 41 ++++ .../tests/__snapshots__/loader.test.ts.snap | 2 +- packages/cli/plugin-bff/tests/cli.test.ts | 13 ++ .../bff-core/src/client/generateClient.ts | 48 +++-- packages/server/bff-core/src/types.ts | 3 +- .../__snapshots__/generateClient.test.ts.snap | 19 +- .../tests/client/generateClient.test.ts | 23 +++ packages/server/core/src/serverBase.ts | 4 +- packages/server/core/src/types/config/bff.ts | 1 + packages/server/create-request/package.json | 10 + packages/server/create-request/src/browser.ts | 45 +++-- packages/server/create-request/src/node.ts | 42 ++-- packages/server/create-request/src/types.ts | 25 ++- .../create-request/tests/browser.test.ts | 37 +++- .../server/create-request/tests/node.test.ts | 22 ++- .../server/plugin-express/src/cli/index.ts | 7 +- packages/server/plugin-koa/src/cli/index.ts | 5 +- .../solutions/app-tools/src/commands/dev.ts | 4 +- .../solutions/app-tools/src/commands/serve.ts | 26 ++- packages/solutions/app-tools/src/index.ts | 14 +- .../src/plugins/deploy/platforms/node.ts | 1 + packages/solutions/app-tools/src/types/new.ts | 1 + packages/toolkit/utils/src/cli/require.ts | 1 + tests/integration/bff-api-app/api/_app.ts | 19 ++ .../bff-api-app/api/context/index.ts | 10 + tests/integration/bff-api-app/api/index.ts | 55 ++++++ tests/integration/bff-api-app/api/upload.ts | 21 ++ .../integration/bff-api-app/api/user/[id].ts | 6 + .../integration/bff-api-app/api/user/index.ts | 28 +++ .../bff-api-app/modern-app-env.d.ts | 4 + .../integration/bff-api-app/modern.config.ts | 11 ++ tests/integration/bff-api-app/package.json | 63 ++++++ .../bff-api-app/tests/index.test.ts | 82 ++++++++ .../bff-api-app/tests/tsconfig.json | 12 ++ tests/integration/bff-api-app/tsconfig.json | 21 ++ tests/integration/bff-client-app/api/_app.ts | 12 ++ .../bff-client-app/api/context/index.ts | 10 + tests/integration/bff-client-app/api/index.ts | 51 +++++ .../integration/bff-client-app/api/upload.ts | 19 ++ .../bff-client-app/modern.config.ts | 14 ++ tests/integration/bff-client-app/package.json | 37 ++++ .../bff-client-app/src/base/App.tsx | 51 +++++ .../bff-client-app/src/custom-sdk/App.tsx | 25 +++ .../bff-client-app/src/modern-app-env.d.ts | 4 + .../bff-client-app/src/ssr/App.tsx | 13 ++ .../bff-client-app/src/upload/App.tsx | 82 ++++++++ .../bff-client-app/tests/index.test.ts | 173 ++++++++++++++++ .../bff-client-app/tests/tsconfig.json | 12 ++ .../integration/bff-client-app/tsconfig.json | 17 ++ tests/integration/bff-express/api/upload.ts | 2 +- .../integration/bff-express/src/base/App.tsx | 2 +- .../bff-indep-client-app/api/_app.ts | 12 ++ .../bff-indep-client-app/api/context/index.ts | 10 + .../bff-indep-client-app/api/index.ts | 51 +++++ .../bff-indep-client-app/api/upload.ts | 19 ++ .../bff-indep-client-app/modern.config.ts | 13 ++ .../bff-indep-client-app/package.json | 38 ++++ .../bff-indep-client-app/src/base/App.tsx | 73 +++++++ .../src/custom-sdk/App.tsx | 25 +++ .../src/modern-app-env.d.ts | 4 + .../bff-indep-client-app/src/ssr/App.tsx | 34 ++++ .../bff-indep-client-app/src/upload/App.tsx | 93 +++++++++ .../bff-indep-client-app/tests/index.test.ts | 145 ++++++++++++++ .../bff-indep-client-app/tests/tsconfig.json | 12 ++ .../bff-indep-client-app/tsconfig.json | 17 ++ 75 files changed, 2169 insertions(+), 134 deletions(-) create mode 100644 .changeset/mean-bobcats-relax.md create mode 100644 packages/cli/plugin-bff/src/create-request.ts create mode 100644 packages/cli/plugin-bff/src/utils/client-generator.ts create mode 100644 packages/cli/plugin-bff/src/utils/plugin-generator.ts create mode 100644 packages/cli/plugin-bff/src/utils/runtime-generator.ts create mode 100644 packages/cli/plugin-bff/src/utils/server-plugin.ts create mode 100644 tests/integration/bff-api-app/api/_app.ts create mode 100644 tests/integration/bff-api-app/api/context/index.ts create mode 100644 tests/integration/bff-api-app/api/index.ts create mode 100644 tests/integration/bff-api-app/api/upload.ts create mode 100644 tests/integration/bff-api-app/api/user/[id].ts create mode 100644 tests/integration/bff-api-app/api/user/index.ts create mode 100644 tests/integration/bff-api-app/modern-app-env.d.ts create mode 100644 tests/integration/bff-api-app/modern.config.ts create mode 100644 tests/integration/bff-api-app/package.json create mode 100644 tests/integration/bff-api-app/tests/index.test.ts create mode 100644 tests/integration/bff-api-app/tests/tsconfig.json create mode 100644 tests/integration/bff-api-app/tsconfig.json create mode 100644 tests/integration/bff-client-app/api/_app.ts create mode 100644 tests/integration/bff-client-app/api/context/index.ts create mode 100644 tests/integration/bff-client-app/api/index.ts create mode 100644 tests/integration/bff-client-app/api/upload.ts create mode 100644 tests/integration/bff-client-app/modern.config.ts create mode 100644 tests/integration/bff-client-app/package.json create mode 100644 tests/integration/bff-client-app/src/base/App.tsx create mode 100644 tests/integration/bff-client-app/src/custom-sdk/App.tsx create mode 100644 tests/integration/bff-client-app/src/modern-app-env.d.ts create mode 100644 tests/integration/bff-client-app/src/ssr/App.tsx create mode 100644 tests/integration/bff-client-app/src/upload/App.tsx create mode 100644 tests/integration/bff-client-app/tests/index.test.ts create mode 100644 tests/integration/bff-client-app/tests/tsconfig.json create mode 100644 tests/integration/bff-client-app/tsconfig.json create mode 100644 tests/integration/bff-indep-client-app/api/_app.ts create mode 100644 tests/integration/bff-indep-client-app/api/context/index.ts create mode 100644 tests/integration/bff-indep-client-app/api/index.ts create mode 100644 tests/integration/bff-indep-client-app/api/upload.ts create mode 100644 tests/integration/bff-indep-client-app/modern.config.ts create mode 100644 tests/integration/bff-indep-client-app/package.json create mode 100644 tests/integration/bff-indep-client-app/src/base/App.tsx create mode 100644 tests/integration/bff-indep-client-app/src/custom-sdk/App.tsx create mode 100644 tests/integration/bff-indep-client-app/src/modern-app-env.d.ts create mode 100644 tests/integration/bff-indep-client-app/src/ssr/App.tsx create mode 100644 tests/integration/bff-indep-client-app/src/upload/App.tsx create mode 100644 tests/integration/bff-indep-client-app/tests/index.test.ts create mode 100644 tests/integration/bff-indep-client-app/tests/tsconfig.json create mode 100644 tests/integration/bff-indep-client-app/tsconfig.json diff --git a/.changeset/mean-bobcats-relax.md b/.changeset/mean-bobcats-relax.md new file mode 100644 index 00000000000..a89f97af4a9 --- /dev/null +++ b/.changeset/mean-bobcats-relax.md @@ -0,0 +1,13 @@ +--- +'@modern-js/create-request': patch +'@modern-js/app-tools': patch +'@modern-js/bff-runtime': patch +'@modern-js/bff-core': patch +'@modern-js/plugin-bff': patch +'@modern-js/server': patch +'@modern-js/core': patch +--- + +feat: bff support independent project + +feat: bff 支持跨项目调用 diff --git a/packages/cli/core/src/types/context.ts b/packages/cli/core/src/types/context.ts index 224cd37807a..92f4112bd80 100644 --- a/packages/cli/core/src/types/context.ts +++ b/packages/cli/core/src/types/context.ts @@ -57,6 +57,8 @@ export interface IAppContext { serverRoutes: ServerRoute[]; /** Whether to use api only mode */ apiOnly: boolean; + /** prefix for independent bff projects */ + indepBffPrefix?: string; /** The Builder instance */ builder?: UniBuilderInstance | UniBuilderWebpackInstance; /** Tools type of the current project */ diff --git a/packages/cli/plugin-bff/package.json b/packages/cli/plugin-bff/package.json index 69e61a305f5..8ffbce98743 100644 --- a/packages/cli/plugin-bff/package.json +++ b/packages/cli/plugin-bff/package.json @@ -41,6 +41,11 @@ "types": "./dist/types/loader.d.ts", "jsnext:source": "./src/loader.ts", "default": "./dist/cjs/loader.js" + }, + "./create-request": { + "types": "./dist/types/create-request.d.ts", + "jsnext:source": "./src/create-request.ts", + "default": "./dist/cjs/create-request.js" } }, "typesVersions": { diff --git a/packages/cli/plugin-bff/src/cli.ts b/packages/cli/plugin-bff/src/cli.ts index 45aae77531a..a3adf16cab9 100644 --- a/packages/cli/plugin-bff/src/cli.ts +++ b/packages/cli/plugin-bff/src/cli.ts @@ -4,6 +4,9 @@ import { ApiRouter } from '@modern-js/bff-core'; import { compile } from '@modern-js/server-utils'; import type { ServerRoute } from '@modern-js/types'; import { fs, API_DIR, SHARED_DIR, normalizeOutputPath } from '@modern-js/utils'; +import clientGenerator from './utils/client-generator'; +import pluginGenerator from './utils/plugin-generator'; +import runtimeGenerator from './utils/runtime-generator'; const DEFAULT_API_PREFIX = '/api'; const TS_CONFIG_FILENAME = 'tsconfig.json'; @@ -11,16 +14,116 @@ const TS_CONFIG_FILENAME = 'tsconfig.json'; export const bffPlugin = (): CliPlugin => ({ name: '@modern-js/plugin-bff', setup: api => { + const compileApi = async () => { + const { + appDirectory, + distDirectory, + apiDirectory, + sharedDirectory, + moduleType, + } = api.useAppContext(); + const modernConfig = api.useResolvedConfigContext(); + + const distDir = path.resolve(distDirectory); + const apiDir = apiDirectory || path.resolve(appDirectory, API_DIR); + const sharedDir = + sharedDirectory || path.resolve(appDirectory, SHARED_DIR); + const tsconfigPath = path.resolve(appDirectory, TS_CONFIG_FILENAME); + + const sourceDirs = []; + if (await fs.pathExists(apiDir)) { + sourceDirs.push(apiDir); + } + + if (await fs.pathExists(sharedDir)) { + sourceDirs.push(sharedDir); + } + + const { server } = modernConfig; + const { alias } = modernConfig.source; + const { babel } = modernConfig.tools; + + if (sourceDirs.length > 0) { + await compile( + appDirectory, + { + server, + alias, + babelConfig: babel, + }, + { + sourceDirs, + distDir, + tsconfigPath, + moduleType, + }, + ); + } + }; + + const generator = async () => { + const { appDirectory, apiDirectory, lambdaDirectory, port } = + api.useAppContext(); + + const modernConfig = api.useResolvedConfigContext(); + const { bff } = modernConfig || {}; + const prefix = bff?.prefix || DEFAULT_API_PREFIX; + const httpMethodDecider = bff?.httpMethodDecider; + + const apiRouter = new ApiRouter({ + apiDir: apiDirectory, + appDir: appDirectory, + lambdaDir: lambdaDirectory, + prefix, + httpMethodDecider, + isBuild: true, + }); + + const lambdaDir = apiRouter.getLambdaDir(); + const existLambda = apiRouter.isExistLambda(); + + const options = { + prefix, + appDir: appDirectory, + apiDir: apiDirectory, + lambdaDir, + existLambda, + port, + requestCreator: (bff as any)?.requestCreator, + httpMethodDecider, + }; + + const runtime = + (bff as any)?.runtime || '@modern-js/plugin-bff/create-request'; + await clientGenerator(options); + await pluginGenerator(prefix); + await runtimeGenerator(runtime); + }; + + const handleCrossProjectInvocation = async () => { + const { bff } = api.useResolvedConfigContext(); + if (bff?.enableCrossProjectInvocation) { + await compileApi(); + await generator(); + } + }; + return { config() { return { tools: { bundlerChain: (chain, { CHAIN_ID, isServer }) => { - const { port, appDirectory, apiDirectory, lambdaDirectory } = - api.useAppContext(); + const { + port, + appDirectory, + apiDirectory, + lambdaDirectory, + indepBffPrefix, + } = api.useAppContext(); const modernConfig = api.useResolvedConfigContext(); const { bff } = modernConfig || {}; - const prefix = bff?.prefix || DEFAULT_API_PREFIX; + const prefix = + indepBffPrefix || bff?.prefix || DEFAULT_API_PREFIX; const httpMethodDecider = bff?.httpMethodDecider; const apiRouter = new ApiRouter({ @@ -60,6 +163,7 @@ export const bffPlugin = (): CliPlugin => ({ }); chain.resolve.alias.set('@api', apiDirectory); + chain.resolve.alias.set( '@modern-js/runtime/bff', isServer @@ -76,8 +180,10 @@ export const bffPlugin = (): CliPlugin => ({ modifyServerRoutes({ routes }) { const modernConfig = api.useResolvedConfigContext(); + const { indepBffPrefix } = api.useAppContext(); + const { bff } = modernConfig || {}; - const prefix = bff?.prefix || '/api'; + const prefix = indepBffPrefix || bff?.prefix || '/api'; const prefixList: string[] = []; @@ -117,51 +223,46 @@ export const bffPlugin = (): CliPlugin => ({ return { plugins }; }, + async beforeDev() { + await handleCrossProjectInvocation(); + }, async afterBuild() { - const { - appDirectory, - distDirectory, - apiDirectory, - sharedDirectory, - moduleType, - } = api.useAppContext(); - const modernConfig = api.useResolvedConfigContext(); - - const distDir = path.resolve(distDirectory); - const apiDir = apiDirectory || path.resolve(appDirectory, API_DIR); - const sharedDir = - sharedDirectory || path.resolve(appDirectory, SHARED_DIR); - const tsconfigPath = path.resolve(appDirectory, TS_CONFIG_FILENAME); + const { bff } = api.useResolvedConfigContext(); + await compileApi(); - const sourceDirs = []; - if (await fs.pathExists(apiDir)) { - sourceDirs.push(apiDir); + if (bff?.enableCrossProjectInvocation) { + await generator(); } + }, + async watchFiles() { + const appContext = api.useAppContext(); + const config = api.useResolvedConfigContext(); + const { generateWatchFiles } = require('@modern-js/app-tools'); + const files = await generateWatchFiles( + appContext, + config.source.configDir, + ); - if (await fs.pathExists(sharedDir)) { - sourceDirs.push(sharedDir); + if (config?.bff?.enableCrossProjectInvocation) { + files.push(appContext.apiDirectory); } - const { server } = modernConfig; - const { alias } = modernConfig.source; - const { babel } = modernConfig.tools; - - if (sourceDirs.length > 0) { - await compile( - appDirectory, - { - server, - alias, - babelConfig: babel, - }, - { - sourceDirs, - distDir, - tsconfigPath, - moduleType, - }, - ); + return files; + }, + + async fileChange(e: { + filename: string; + eventType: string; + isPrivate: boolean; + }) { + const { filename, eventType, isPrivate } = e; + if ( + !isPrivate && + (eventType === 'change' || eventType === 'unlink') && + filename.startsWith('api/') + ) { + await handleCrossProjectInvocation(); } }, }; diff --git a/packages/cli/plugin-bff/src/create-request.ts b/packages/cli/plugin-bff/src/create-request.ts new file mode 100644 index 00000000000..cad65b6574e --- /dev/null +++ b/packages/cli/plugin-bff/src/create-request.ts @@ -0,0 +1 @@ +export * from '@modern-js/create-request'; diff --git a/packages/cli/plugin-bff/src/server.ts b/packages/cli/plugin-bff/src/server.ts index 0defe0d371a..185028796b0 100644 --- a/packages/cli/plugin-bff/src/server.ts +++ b/packages/cli/plugin-bff/src/server.ts @@ -35,9 +35,20 @@ export default (): ServerPlugin => ({ return { async prepare() { const appContext = api.useAppContext(); - const { appDirectory, distDirectory, render } = appContext; + const { + appDirectory, + distDirectory, + render, + indepBffPrefix, + apiDirectory, + } = appContext; const root = isProd() ? distDirectory : appDirectory; - const apiPath = path.resolve(root || process.cwd(), API_DIR); + + let apiPath = path.resolve(root || process.cwd(), API_DIR); + + if (indepBffPrefix && apiDirectory) { + apiPath = apiDirectory; + } apiAppPath = path.resolve(apiPath, API_APP_NAME); const apiMod = await requireExistModule(apiAppPath); @@ -53,7 +64,8 @@ export default (): ServerPlugin => ({ /** bind api server */ const config = api.useConfigContext(); - const prefix = config?.bff?.prefix || '/api'; + const prefix = + (indepBffPrefix as string) || config?.bff?.prefix || '/api'; const enableHandleWeb = config?.bff?.enableHandleWeb; const httpMethodDecider = config?.bff?.httpMethodDecider; @@ -131,6 +143,7 @@ export default (): ServerPlugin => ({ const apiDir = path.resolve(pwd, API_DIR); const appContext = api.useAppContext(); const { apiDirectory, lambdaDirectory } = appContext; + apiRouter = new ApiRouter({ appDir: pwd, apiDir: (apiDirectory as string) || apiDir, diff --git a/packages/cli/plugin-bff/src/utils/client-generator.ts b/packages/cli/plugin-bff/src/utils/client-generator.ts new file mode 100644 index 00000000000..ce18c3a3f68 --- /dev/null +++ b/packages/cli/plugin-bff/src/utils/client-generator.ts @@ -0,0 +1,185 @@ +import path from 'path'; +import { type GenClientOptions, generateClient } from '@modern-js/bff-core'; +import type { HttpMethodDecider } from '@modern-js/types'; +import { fs, logger } from '@modern-js/utils'; + +export type APILoaderOptions = { + prefix: string; + appDir: string; + apiDir: string; + lambdaDir: string; + existLambda: boolean; + port?: number; + requestCreator?: string; + httpMethodDecider?: HttpMethodDecider; +}; + +export async function readDirectoryFiles( + appDirectory: string, + directory: string, +) { + const filesList: { + resourcePath: string; + source: string; + targetDir: string; + name: string; + absTargetDir: string; + relativeTargetDistDir: string; + exportKey: string; + }[] = []; + + async function readFiles(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.name === '_app.ts') { + continue; + } + const resourcePath = path.join(currentPath, entry.name); + if (entry.isDirectory()) { + await readFiles(resourcePath); + } else { + const source = await fs.readFile(resourcePath, 'utf8'); + const targetDir = path.join( + './dist/client', + path.relative(directory, currentPath), + entry.name.replace('.ts', '.js'), + ); + const name = path.basename(entry.name, '.ts'); + const absTargetDir = path.resolve(targetDir); + + const relativePathFromAppDirectory = path.relative( + appDirectory, + currentPath, + ); + const typesFilePath = path.join( + './dist', + relativePathFromAppDirectory, + `${name}.d.ts`, + ); + const relativeTargetDistDir = `./${typesFilePath}`; + + const relativePath = path.relative(directory, resourcePath); + const parsedPath = path.parse(relativePath); + const exportKey = path.join(parsedPath.dir, parsedPath.name); + + filesList.push({ + resourcePath, + source, + targetDir, + name, + absTargetDir, + relativeTargetDistDir, + exportKey, + }); + } + } + } + + await readFiles(directory); + return filesList; +} + +async function writeTargetFile(absTargetDir: string, content: string) { + await fs.mkdir(path.dirname(absTargetDir), { recursive: true }); + await fs.writeFile(absTargetDir, content); +} + +async function setPackage( + files: { + exportKey: string; + targetDir: string; + relativeTargetDistDir: string; + }[], +) { + const packagePath = path.join(process.cwd(), 'package.json'); + try { + const packageData = await fs.readFile(packagePath, 'utf8'); + const packageJson = JSON.parse(packageData); + + if (!packageJson.exports) { + packageJson.exports = {}; + } + + files.forEach(file => { + const exportKey = `./${file.exportKey}`; + const jsFilePath = `./${file.targetDir}`; + + packageJson.exports[exportKey] = { + import: jsFilePath, + types: file.relativeTargetDistDir, + }; + }); + + packageJson.exports['./server-plugin'] = `./dist/server-plugin/index.js`; + + packageJson.exports['./runtime'] = { + import: './dist/runtime/index.js', + types: './dist/runtime/index.d.ts', + }; + + await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2)); + logger.info(`Update package.json succeed`); + } catch (error) { + logger.error(`Update package.json error: ${error}`); + } +} + +async function clientGenerator(draftOptions: APILoaderOptions) { + const sourceList = await readDirectoryFiles( + draftOptions.appDir, + draftOptions.lambdaDir, + ); + + const getClitentCode = async (resourcePath: string, source: string) => { + const warning = `The file ${resourcePath} is not allowd to be imported in src directory, only API definition files are allowed.`; + + if (!draftOptions.existLambda) { + logger.warn(warning); + return; + } + + const options: GenClientOptions = { + prefix: (Array.isArray(draftOptions.prefix) + ? draftOptions.prefix[0] + : draftOptions.prefix) as string, + appDir: draftOptions.appDir, + apiDir: draftOptions.apiDir, + lambdaDir: draftOptions.lambdaDir, + port: Number(draftOptions.port), + source, + resourcePath, + target: 'bundle', + httpMethodDecider: draftOptions.httpMethodDecider, + requestCreator: draftOptions.requestCreator, + }; + + const { lambdaDir } = draftOptions as any; + if (!resourcePath.startsWith(lambdaDir)) { + logger.warn(warning); + return; + } + + options.requireResolve = require.resolve; + + const result = await generateClient(options); + + return result; + }; + + try { + for (const source of sourceList) { + const code = await getClitentCode(source.resourcePath, source.source); + if (code?.value) { + await writeTargetFile(source.absTargetDir, code.value); + } + } + logger.info(`Generate client bundle succeed`); + } catch (error) { + logger.error(`Generate Client bundle error: ${error}`); + } + + setPackage(sourceList); +} + +export default clientGenerator; diff --git a/packages/cli/plugin-bff/src/utils/plugin-generator.ts b/packages/cli/plugin-bff/src/utils/plugin-generator.ts new file mode 100644 index 00000000000..9b4bc638667 --- /dev/null +++ b/packages/cli/plugin-bff/src/utils/plugin-generator.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { fs, logger } from '@modern-js/utils'; +import { API_APP_PACKAGE_NAME, API_APP_PREFIX } from './server-plugin'; + +async function pluginGenerator(prefix: string) { + const packagePath = path.join(process.cwd(), 'package.json'); + const packageData = await fs.readFile(packagePath, 'utf8'); + const packageJson = JSON.parse(packageData); + + const cwd = process.cwd(); + const pluginPath = path.resolve(cwd, './dist', 'server-plugin'); + if (!fs.existsSync(pluginPath)) { + fs.mkdirSync(pluginPath); + } + + let source = await fs.readFile( + path.resolve(__dirname, 'server-plugin.js'), + 'utf8', + ); + + source = source.replace( + new RegExp(API_APP_PACKAGE_NAME, 'g'), + packageJson.name, + ); + source = source.replace(new RegExp(API_APP_PREFIX, 'g'), prefix); + + fs.writeFileSync(path.join(pluginPath, 'index.js'), source); + + logger.info(`Generate server-plugin succeed`); +} + +export default pluginGenerator; diff --git a/packages/cli/plugin-bff/src/utils/runtime-generator.ts b/packages/cli/plugin-bff/src/utils/runtime-generator.ts new file mode 100644 index 00000000000..ec57e730e5d --- /dev/null +++ b/packages/cli/plugin-bff/src/utils/runtime-generator.ts @@ -0,0 +1,27 @@ +import path from 'path'; +import { fs } from '@modern-js/utils'; + +async function runtimeGenerator(runtime: string) { + const cwd = process.cwd(); + const pluginPath = path.resolve(cwd, './dist', 'runtime'); + if (!fs.existsSync(pluginPath)) { + fs.mkdirSync(pluginPath); + } + const source = `import { configure as _configure } from '${runtime}' + const configure = (options) => { + return _configure({ + ...options, + requestId: '${process.env.npm_package_name}', + }); + } + export { configure } + `; + + const tsSource = `export * from '${runtime}'`; + + fs.writeFileSync(path.join(pluginPath, 'index.js'), source); + + fs.writeFileSync(path.join(pluginPath, 'index.d.ts'), tsSource); +} + +export default runtimeGenerator; diff --git a/packages/cli/plugin-bff/src/utils/server-plugin.ts b/packages/cli/plugin-bff/src/utils/server-plugin.ts new file mode 100644 index 00000000000..e02623d3ea6 --- /dev/null +++ b/packages/cli/plugin-bff/src/utils/server-plugin.ts @@ -0,0 +1,38 @@ +import path from 'path'; +import type { ServerPlugin } from '@modern-js/server-core'; + +export const API_APP_PACKAGE_NAME = '{packageName}'; +export const API_APP_PREFIX = '{prefix}'; + +const API_DIR = 'api'; +const DIST_DIR = 'dist'; +const LAMBDA_DIR = 'lambda'; +const NODE_MODULES = 'node_modules'; + +export const serverPlugin = (): ServerPlugin => ({ + name: '@modern-js/plugin-independent-bff', + setup: api => { + return { + async prepare() { + const cwd = process.cwd(); + + const sdkPath = path.join(cwd, NODE_MODULES, API_APP_PACKAGE_NAME); + + const sdkDistPath = path.join(sdkPath, DIST_DIR); + const apiDirectory = path.join(sdkDistPath, API_DIR); + const lambdaDirectory = path.resolve(sdkDistPath, API_DIR, LAMBDA_DIR); + + const appContext = api.useAppContext(); + + api.setAppContext({ + ...appContext, + apiDirectory, + lambdaDirectory, + indepBffPrefix: API_APP_PREFIX, + }); + }, + }; + }, +}); + +export default serverPlugin; diff --git a/packages/cli/plugin-bff/tests/__snapshots__/cli.test.ts.snap b/packages/cli/plugin-bff/tests/__snapshots__/cli.test.ts.snap index 2ba98ece0ea..3a92d899524 100644 --- a/packages/cli/plugin-bff/tests/__snapshots__/cli.test.ts.snap +++ b/packages/cli/plugin-bff/tests/__snapshots__/cli.test.ts.snap @@ -16,6 +16,47 @@ exports[`bff cli plugin config 1`] = ` ] `; +exports[`bff cli plugin generator client 1`] = ` +[ + { + "absTargetDir": "/tests/dist/client/hello.js", + "exportKey": "hello", + "name": "hello", + "relativeTargetDistDir": "dist/api/hello.d.ts", + "resourcePath": "/fixtures/function/api/hello.ts", + "source": "export const get = ({ query }: { query: Record }) => ({ + query, +}); +", + "targetDir": "dist/client/hello.js", + }, + { + "absTargetDir": "/tests/dist/client/upload.js", + "exportKey": "upload", + "name": "upload", + "relativeTargetDistDir": "dist/api/upload.d.ts", + "resourcePath": "/fixtures/function/api/upload.ts", + "source": "export const get = ({ query }: { query: Record }) => ({ + query, +}); +", + "targetDir": "dist/client/upload.js", + }, + { + "absTargetDir": "/tests/dist/client/user/[id].js", + "exportKey": "user/[id]", + "name": "[id]", + "relativeTargetDistDir": "dist/api/user/[id].d.ts", + "resourcePath": "/fixtures/function/api/user/[id].ts", + "source": "export const get = ({ query }: { query: Record }) => ({ + query, +}); +", + "targetDir": "dist/client/user/[id].js", + }, +] +`; + exports[`bff cli plugin routes 1`] = ` { "routes": [ diff --git a/packages/cli/plugin-bff/tests/__snapshots__/loader.test.ts.snap b/packages/cli/plugin-bff/tests/__snapshots__/loader.test.ts.snap index 356be68a191..640f081007e 100644 --- a/packages/cli/plugin-bff/tests/__snapshots__/loader.test.ts.snap +++ b/packages/cli/plugin-bff/tests/__snapshots__/loader.test.ts.snap @@ -3,6 +3,6 @@ exports[`bff loader should works well 1`] = ` "import { createRequest } from '/packages/cli/plugin-bff/tests/fixtures/requestCreator/client'; -export var get = createRequest('/api/hello', 'GET', 80, 'functionName' ); +export var get = createRequest({"path":"/api/hello","method":"GET","port":80,"httpMethodDecider":"functionName"}); " `; diff --git a/packages/cli/plugin-bff/tests/cli.test.ts b/packages/cli/plugin-bff/tests/cli.test.ts index 3d2713414af..f4f1f952d70 100644 --- a/packages/cli/plugin-bff/tests/cli.test.ts +++ b/packages/cli/plugin-bff/tests/cli.test.ts @@ -10,6 +10,7 @@ import { } from '@modern-js/core'; import Chain from '@modern-js/utils/webpack-chain'; import plugin from '../src/cli'; +import { readDirectoryFiles } from '../src/utils/client-generator'; import './helper'; const CHAIN_ID = { @@ -86,4 +87,16 @@ describe('bff cli plugin', () => { }, }); }); + it('generator client', async () => { + console.log( + 'path.resol', + path.resolve(__dirname, './fixtures/function/api'), + ); + const result = await readDirectoryFiles( + path.resolve(__dirname, './fixtures/function'), + path.resolve(__dirname, './fixtures/function/api'), + ); + + expect(result).toMatchSnapshot(); + }); }); diff --git a/packages/server/bff-core/src/client/generateClient.ts b/packages/server/bff-core/src/client/generateClient.ts index e442deb3e50..b8ad4824ecb 100644 --- a/packages/server/bff-core/src/client/generateClient.ts +++ b/packages/server/bff-core/src/client/generateClient.ts @@ -18,10 +18,14 @@ export type GenClientOptions = { target?: string; requireResolve?: typeof require.resolve; httpMethodDecider?: HttpMethodDecider; + domain?: string; }; export const DEFAULT_CLIENT_REQUEST_CREATOR = '@modern-js/create-request'; +export const INNER_CLIENT_REQUEST_CREATOR = + '@modern-js/plugin-bff/create-request'; + export const generateClient = async ({ appDir, resourcePath, @@ -34,8 +38,11 @@ export const generateClient = async ({ fetcher, requireResolve = require.resolve, httpMethodDecider, + domain, }: GenClientOptions): Promise => { - if (!requestCreator) { + if (target === 'bundle') { + requestCreator = requestCreator || INNER_CLIENT_REQUEST_CREATOR; + } else if (!requestCreator) { requestCreator = requireResolve( `${DEFAULT_CLIENT_REQUEST_CREATOR}${target ? `/${target}` : ''}`, ).replace(/\\/g, '/'); @@ -58,7 +65,6 @@ export const generateClient = async ({ prefix, httpMethodDecider, }); - const handlerInfos = await apiRouter.getSingleModuleHandlers(resourcePath); if (!handlerInfos) { return Err(`generate client error: Cannot require module ${resourcePath}`); @@ -74,27 +80,35 @@ export const generateClient = async ({ const upperHttpMethod = httpMethod.toUpperCase(); const routeName = routePath; - if (action) { - handlersCode += `export ${exportStatement} createUploader('${routeName}');`; - } else if (target === 'server') { - handlersCode += `export ${exportStatement} createRequest('${routeName}', '${upperHttpMethod}', process.env.PORT || ${String( - port, - )}, '${httpMethodDecider ? httpMethodDecider : 'functionName'}' ${ - fetcher ? `, fetch` : '' - }); - `; + + const requestId = + target === 'bundle' ? process.env.npm_package_name : undefined; + + if (action === 'upload') { + const requestOptions = { + path: routeName, + domain, + requestId, + }; + handlersCode += `export ${exportStatement} createUploader(${JSON.stringify(requestOptions)});`; } else { - handlersCode += `export ${exportStatement} createRequest('${routeName}', '${upperHttpMethod}', ${String( - port, - )}, '${httpMethodDecider ? httpMethodDecider : 'functionName'}' ${ - fetcher ? `, fetch` : '' - }); + const requestOptions = { + path: routeName, + method: upperHttpMethod, + port: process.env.PORT || port, + httpMethodDecider: httpMethodDecider || 'functionName', + domain, + ...(fetcher ? { fetch: 'fetch' } : {}), + requestId, + }; + + handlersCode += `export ${exportStatement} createRequest(${JSON.stringify(requestOptions)}); `; } } const importCode = `import { createRequest${ - handlerInfos.find(i => i.action) ? ', createUploader' : '' + handlerInfos.find(i => i.action === 'upload') ? ', createUploader' : '' } } from '${requestCreator}'; ${fetcher ? `import { fetch } from '${fetcher}';\n` : ''}`; diff --git a/packages/server/bff-core/src/types.ts b/packages/server/bff-core/src/types.ts index bbdfe347f96..6599dc4b991 100644 --- a/packages/server/bff-core/src/types.ts +++ b/packages/server/bff-core/src/types.ts @@ -1,4 +1,5 @@ -import type { Merge } from 'type-fest'; +// import type { Merge } from 'type-fest'; +type Merge = T & Omit; export enum OperatorType { Trigger = 0, diff --git a/packages/server/bff-core/tests/client/__snapshots__/generateClient.test.ts.snap b/packages/server/bff-core/tests/client/__snapshots__/generateClient.test.ts.snap index ab7fab25150..c261974d39b 100644 --- a/packages/server/bff-core/tests/client/__snapshots__/generateClient.test.ts.snap +++ b/packages/server/bff-core/tests/client/__snapshots__/generateClient.test.ts.snap @@ -1,18 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`client generateClient should support cross croject invocation 1`] = ` +"import { createRequest } from '@modern-js/plugin-bff/create-request'; + +export default createRequest({"path":"/normal/origin","method":"GET","port":3000,"httpMethodDecider":"functionName","requestId":"tests"}); + export var DELETE = createRequest({"path":"/normal/origin","method":"DELETE","port":3000,"httpMethodDecider":"functionName","requestId":"tests"}); + export var putRepo = createRequest({"path":"/put-repo","method":"PUT","port":3000,"httpMethodDecider":"functionName","requestId":"tests"}); + " +`; + exports[`client generateClient should support operator 1`] = ` "import { createRequest } from '@modern-js/create-request'; -export default createRequest('/normal/origin', 'GET', 3000, 'functionName' ); - export var DELETE = createRequest('/normal/origin', 'DELETE', 3000, 'functionName' ); - export var putRepo = createRequest('/put-repo', 'PUT', 3000, 'functionName' ); +export default createRequest({"path":"/normal/origin","method":"GET","port":3000,"httpMethodDecider":"functionName"}); + export var DELETE = createRequest({"path":"/normal/origin","method":"DELETE","port":3000,"httpMethodDecider":"functionName"}); + export var putRepo = createRequest({"path":"/put-repo","method":"PUT","port":3000,"httpMethodDecider":"functionName"}); " `; exports[`client generateClient should works correctly 1`] = ` "import { createRequest } from '@modern-js/create-request'; -export var get = createRequest('/api/:id/origin/foo', 'GET', 3000, 'functionName' ); - export var post = createRequest('/api/:id/origin/foo', 'POST', 3000, 'functionName' ); +export var get = createRequest({"path":"/api/:id/origin/foo","method":"GET","port":3000,"httpMethodDecider":"functionName"}); + export var post = createRequest({"path":"/api/:id/origin/foo","method":"POST","port":3000,"httpMethodDecider":"functionName"}); " `; diff --git a/packages/server/bff-core/tests/client/generateClient.test.ts b/packages/server/bff-core/tests/client/generateClient.test.ts index 18f7c340ee8..ef877548fad 100644 --- a/packages/server/bff-core/tests/client/generateClient.test.ts +++ b/packages/server/bff-core/tests/client/generateClient.test.ts @@ -59,4 +59,27 @@ describe('client', () => { expect(result.isOk).toBeTruthy(); expect(result.value).toMatchSnapshot(); }); + + test('generateClient should support cross croject invocation', async () => { + const prefix = '/'; + const port = 3000; + const resourcePath = path.resolve( + __dirname, + '../fixtures/function/normal/origin/index.ts', + ); + const source = await fs.readFile(resourcePath, 'utf-8'); + + const result = await generateClient({ + prefix, + port, + resourcePath, + source, + apiDir: PWD, + lambdaDir: path.join(PWD, './lambda'), + requireResolve: ((input: any) => input) as any, + target: 'bundle', + }); + expect(result.isOk).toBeTruthy(); + expect(result.value).toMatchSnapshot(); + }); }); diff --git a/packages/server/core/src/serverBase.ts b/packages/server/core/src/serverBase.ts index b8edc003191..d392801b77b 100644 --- a/packages/server/core/src/serverBase.ts +++ b/packages/server/core/src/serverBase.ts @@ -31,6 +31,7 @@ export type ServerBaseOptions = { sharedDirectory?: string; apiDirectory?: string; lambdaDirectory?: string; + indepBffPrefix?: string; }; runMode?: 'apiOnly' | 'ssrOnly' | 'webOnly'; }; @@ -96,11 +97,12 @@ export class ServerBase { internalDirectory: context?.internalDirectory || '', lambdaDirectory: context?.lambdaDirectory, sharedDirectory: context?.sharedDirectory || '', + indepBffPrefix: context?.indepBffPrefix || '', distDirectory: pwd, plugins: [], metaName: metaName || 'modern-js', serverBase: this, - }; + } as any; return createContext(appContext); } diff --git a/packages/server/core/src/types/config/bff.ts b/packages/server/core/src/types/config/bff.ts index b5ecb8ab098..0f51031f960 100644 --- a/packages/server/core/src/types/config/bff.ts +++ b/packages/server/core/src/types/config/bff.ts @@ -6,6 +6,7 @@ export interface BffUserConfig { proxy?: Record; httpMethodDecider?: HttpMethodDecider; enableHandleWeb?: boolean; + enableCrossProjectInvocation?: boolean; } export type BffNormalizedConfig = BffUserConfig; diff --git a/packages/server/create-request/package.json b/packages/server/create-request/package.json index fd685078a8b..03c8d4d594d 100644 --- a/packages/server/create-request/package.json +++ b/packages/server/create-request/package.json @@ -44,6 +44,16 @@ "types": "./dist/types/node.d.ts", "jsnext:source": "./src/node.ts", "default": "./dist/cjs/node.js" + }, + "./browser": { + "types": "./dist/types/browser.d.ts", + "jsnext:source": "./src/browser.ts", + "default": "./dist/esm/browser.js" + }, + "./default": { + "types": "./dist/types/node.d.ts", + "jsnext:source": "./src/node.ts", + "default": "./dist/cjs/node.js" } }, "typesVersions": { diff --git a/packages/server/create-request/src/browser.ts b/packages/server/create-request/src/browser.ts index 23532d7c24e..ff0c0c39496 100644 --- a/packages/server/create-request/src/browser.ts +++ b/packages/server/create-request/src/browser.ts @@ -5,12 +5,13 @@ import type { BFFRequestPayload, IOptions, RequestCreator, - RequestUploader, Sender, + UploadCreator, } from './types'; import { getUploadPayload } from './utiles'; -let realRequest: typeof fetch; +const realRequest: Map = new Map(); + let realAllowedHeaders: string[]; const originFetch = (...params: Parameters) => { @@ -19,34 +20,42 @@ const originFetch = (...params: Parameters) => { if (init?.method?.toLowerCase() === 'get') { init.body = undefined; } - return fetch(url, init).then(handleRes); }; export const configure = (options: IOptions) => { - const { request, interceptor, allowedHeaders } = options; - realRequest = request || originFetch; + const { + request, + interceptor, + allowedHeaders, + requestId = 'default', + } = options; + let configuredRequest = request || originFetch; if (interceptor && !request) { - realRequest = interceptor(fetch); + configuredRequest = interceptor(fetch); } if (Array.isArray(allowedHeaders)) { realAllowedHeaders = allowedHeaders; } + realRequest.set(requestId, configuredRequest); }; -export const createRequest: RequestCreator = ( +export const createRequest: RequestCreator = ({ path, method, port, httpMethodDecider = 'functionName', // 后续可能要修改,暂时先保留 fetch = originFetch, -) => { + domain, + requestId = 'default', +}) => { const getFinalPath = compile(path, { encode: encodeURIComponent }); const keys: Key[] = []; pathToRegexp(path, keys); const sender: Sender = async (...args) => { - const fetcher = realRequest || originFetch; + const fetcher = realRequest.get(requestId) || originFetch; + let body; let finalURL: string; let headers: Record; @@ -119,6 +128,9 @@ export const createRequest: RequestCreator = ( headers.accept = `application/json,*/*;q=0.8`; + if (domain) { + finalURL = `${domain}${finalURL}`; + } return fetcher(finalURL, { method, body, @@ -129,11 +141,20 @@ export const createRequest: RequestCreator = ( return sender; }; -export const createUploader: RequestUploader = (path: string) => { +export const createUploader: UploadCreator = ({ + path, + domain, + requestId = 'default', +}) => { const sender: Sender = (...args) => { - const fetcher = realRequest || originFetch; + const fetcher = realRequest.get(requestId) || originFetch; + const { body, headers } = getUploadPayload(args); - return fetcher(path, { method: 'POST', body, headers }); + return fetcher(domain ? `${domain}${path}` : path, { + method: 'POST', + body, + headers, + }); }; return sender; diff --git a/packages/server/create-request/src/node.ts b/packages/server/create-request/src/node.ts index f1b5e62ac01..86c5aef4a67 100644 --- a/packages/server/create-request/src/node.ts +++ b/packages/server/create-request/src/node.ts @@ -7,14 +7,15 @@ import type { BFFRequestPayload, IOptions, RequestCreator, - RequestUploader, Sender, + UploadCreator, } from './types'; import { getUploadPayload } from './utiles'; type Fetch = typeof nodeFetch; -let realRequest: Fetch; +const realRequest: Map = new Map(); + let realAllowedHeaders: string[] = []; const originFetch = (...params: Parameters) => { const [, init] = params; @@ -27,24 +28,30 @@ const originFetch = (...params: Parameters) => { }; export const configure = (options: IOptions) => { - const { request, interceptor, allowedHeaders } = options; - realRequest = (request as Fetch) || originFetch; + const { + request, + interceptor, + allowedHeaders, + requestId = 'default', + } = options; + let configuredRequest = (request as Fetch) || originFetch; if (interceptor && !request) { - realRequest = interceptor(nodeFetch); + configuredRequest = interceptor(nodeFetch); } if (Array.isArray(allowedHeaders)) { realAllowedHeaders = allowedHeaders; } + realRequest.set(requestId, configuredRequest); }; -export const createRequest: RequestCreator = ( - path: string, - method: string, - port: number, - httpMethodDecider = 'functionName', - // 后续可能要修改,暂时先保留 - fetch = nodeFetch, -) => { +export const createRequest: RequestCreator = ({ + path, + method, + port, + httpMethodDecider = 'functionName', // 后续可能要修改,暂时先保留 + fetch = originFetch, + requestId = 'default', +}) => { const getFinalPath = compile(path, { encode: encodeURIComponent }); const keys: Key[] = []; pathToRegexp(path, keys); @@ -116,7 +123,7 @@ export const createRequest: RequestCreator = ( url = `http://127.0.0.1:${port}${finalPath}`; } - const fetcher = realRequest || originFetch; + const fetcher = realRequest.get(requestId) || originFetch; if (method.toLowerCase() === 'get') { body = undefined; @@ -130,9 +137,12 @@ export const createRequest: RequestCreator = ( return sender; }; -export const createUploader: RequestUploader = (path: string) => { +export const createUploader: UploadCreator = ({ + path, + requestId = 'default', +}) => { const sender: Sender = (...args) => { - const fetcher = realRequest || originFetch; + const fetcher = realRequest.get(requestId) || originFetch; const { body, headers } = getUploadPayload(args); return fetcher(path, { method: 'POST', body, headers }); }; diff --git a/packages/server/create-request/src/types.ts b/packages/server/create-request/src/types.ts index bbbb619ca9a..b27563b6822 100644 --- a/packages/server/create-request/src/types.ts +++ b/packages/server/create-request/src/types.ts @@ -16,18 +16,31 @@ export type Sender = ((...args: any[]) => Promise) & { fetch?: F; }; +export type RequestOptions = { + path: string; + method: string; + port: number; + httpMethodDecider?: HttpMethodDecider; + domain?: string; + fetch?: F; + requestId?: string; +}; + export type RequestCreator = ( - path: string, - method: string, - port: number, - httpMethodDecider: HttpMethodDecider, - fetch?: F, + options: RequestOptions, ) => Sender; -export type RequestUploader = (path: string) => Sender; +export type UploadOptions = { + path: string; + domain?: string; + requestId?: string; +}; + +export type UploadCreator = (options: UploadOptions) => Sender; export type IOptions = { request?: F; interceptor?: (request: F) => F; allowedHeaders?: string[]; + requestId?: string; }; diff --git a/packages/server/create-request/tests/browser.test.ts b/packages/server/create-request/tests/browser.test.ts index 9e1a3cc67be..3f3030e9448 100644 --- a/packages/server/create-request/tests/browser.test.ts +++ b/packages/server/create-request/tests/browser.test.ts @@ -34,7 +34,11 @@ describe('configure', () => { }); configure({ request: customRequest }); - const request = createRequest(path, method, 8080, undefined); + const request = createRequest({ + path, + method, + port: 8080, + }); const res = await request(); const data = await res.json(); @@ -57,7 +61,11 @@ describe('configure', () => { }); configure({ request: customRequest }); - const request = createRequest(path, method, 8080, undefined); + const request = createRequest({ + path, + method, + port: 8080, + }); const res = await request({ query: { users: ['foo', 'bar'], @@ -78,7 +86,11 @@ describe('configure', () => { }); configure({ interceptor }); - const request = createRequest(path, method, 8080, undefined); + const request = createRequest({ + path, + method, + port: 8080, + }); const res = await request(); const data = await res.json(); @@ -100,7 +112,11 @@ describe('configure', () => { }); configure({ request: customRequest, interceptor }); - const request = createRequest(path, method, 8080, undefined); + const request = createRequest({ + path, + method, + port: 8080, + }); const res = await request(); const data = await res.json(); @@ -119,8 +135,11 @@ describe('configure', () => { }); configure({ interceptor }); - - const request = createRequest(`${path}/:id`, method, 8080, undefined); + const request = createRequest({ + path: `${path}/:id`, + method, + port: 8080, + }); const res = await request('modernjs'); const data = await res.json(); expect(res instanceof Response).toBe(true); @@ -137,7 +156,11 @@ describe('configure', () => { configure({ interceptor }); - const request = createRequest(`${path}/:id`, method, 8080, undefined); + const request = createRequest({ + path: `${path}/:id`, + method, + port: 8080, + }); const res = await request({ params: { id: 'modernjs', diff --git a/packages/server/create-request/tests/node.test.ts b/packages/server/create-request/tests/node.test.ts index 3c2c05ba1c4..769ad33aaf9 100644 --- a/packages/server/create-request/tests/node.test.ts +++ b/packages/server/create-request/tests/node.test.ts @@ -41,7 +41,7 @@ describe('configure', () => { const customRequest = jest.fn((requestPath: any) => fetch(requestPath)); configure({ request: customRequest as unknown as typeof fetch }); - const request = createRequest(path, method, port); + const request = createRequest({ path, method, port }); const res = await request(); const data = await res.json(); @@ -72,7 +72,7 @@ describe('configure', () => { const customRequest = jest.fn((requestPath: any) => fetch(requestPath)); configure({ request: customRequest as unknown as typeof fetch }); - const request = createRequest(path, method, port); + const request = createRequest({ path, method, port }); const res = await request({ query: { users: ['foo', 'bar'], @@ -96,7 +96,7 @@ describe('configure', () => { ); configure({ interceptor: interceptor as any }); - const request = createRequest(path, method, 8080); + const request = createRequest({ path, method, port: 8080 }); const res = await request(); const data = await res.json(); @@ -120,7 +120,7 @@ describe('configure', () => { request: customRequest as unknown as typeof fetch, interceptor: interceptor as any, }); - const request = createRequest(path, method, 8080); + const request = createRequest({ path, method, port: 8080 }); const res = await request(); const data = await res.json(); @@ -149,7 +149,7 @@ describe('configure', () => { .reply(200, response); configure({ allowedHeaders: ['authorization'] }); - const request = createRequest(path, method, 8080); + const request = createRequest({ path, method, port: 8080 }); const data = await request(); expect(data).toStrictEqual(response); @@ -168,7 +168,11 @@ describe('configure', () => { configure({ interceptor: interceptor as any }); - const request = createRequest(`${path}/:id`, method, 8080, undefined); + const request = createRequest({ + path: `${path}/:id`, + method, + port: 8080, + }); const res = await request('modernjs'); const data = await res.json(); expect(res instanceof Response).toBe(true); @@ -187,7 +191,11 @@ describe('configure', () => { configure({ interceptor: interceptor as any }); - const request = createRequest(`${path}/:id`, method, 8080, undefined); + const request = createRequest({ + path: `${path}/:id`, + method, + port: 8080, + }); const res = await request({ params: { id: 'modernjs', diff --git a/packages/server/plugin-express/src/cli/index.ts b/packages/server/plugin-express/src/cli/index.ts index 7d57228c0af..9d255270df9 100644 --- a/packages/server/plugin-express/src/cli/index.ts +++ b/packages/server/plugin-express/src/cli/index.ts @@ -16,10 +16,15 @@ export const expressPlugin = (): CliPlugin => ({ appContext.internalDirectory, 'server', ); + + const useConfig = api.useConfigContext(); + const runtimePath = - process.env.NODE_ENV === 'development' + process.env.NODE_ENV === 'development' && + !useConfig?.bff?.enableCrossProjectInvocation ? require.resolve('@modern-js/plugin-express/runtime') : '@modern-js/plugin-express/runtime'; + return { source: { alias: { diff --git a/packages/server/plugin-koa/src/cli/index.ts b/packages/server/plugin-koa/src/cli/index.ts index 88cad50107f..1528bc6d657 100644 --- a/packages/server/plugin-koa/src/cli/index.ts +++ b/packages/server/plugin-koa/src/cli/index.ts @@ -17,9 +17,12 @@ export const koaPlugin = (): CliPlugin => ({ 'server', ); + const modernConfig = api.useResolvedConfigContext(); + const runtimePath = '@modern-js/plugin-koa/runtime'; const alias = - process.env.NODE_ENV === 'production' + process.env.NODE_ENV === 'production' || + !!modernConfig?.bff?.enableCrossProjectInvocation ? runtimePath : require.resolve(runtimePath); diff --git a/packages/solutions/app-tools/src/commands/dev.ts b/packages/solutions/app-tools/src/commands/dev.ts index 439f842655a..83d29243384 100644 --- a/packages/solutions/app-tools/src/commands/dev.ts +++ b/packages/solutions/app-tools/src/commands/dev.ts @@ -102,6 +102,7 @@ export const dev = async ( internalDirectory: appContext.internalDirectory, apiDirectory: appContext.apiDirectory, lambdaDirectory: appContext.lambdaDirectory, + indepBffPrefix: appContext.indepBffPrefix, sharedDirectory: appContext.sharedDirectory, }, serverConfigPath, @@ -111,7 +112,7 @@ export const dev = async ( serverConfigFile, plugins: pluginInstances, ...devServerOptions, - }; + } as any; const host = normalizedConfig.dev?.host || DEFAULT_DEV_HOST; @@ -137,6 +138,7 @@ export const dev = async ( ); }, ); + setServer(server); } else { const { server, afterListen } = await createDevServer( { diff --git a/packages/solutions/app-tools/src/commands/serve.ts b/packages/solutions/app-tools/src/commands/serve.ts index 11431f278cc..a1d5fc4efdb 100644 --- a/packages/solutions/app-tools/src/commands/serve.ts +++ b/packages/solutions/app-tools/src/commands/serve.ts @@ -25,6 +25,7 @@ export const start = async (api: CLIPluginAPI>) => { metaName, serverRoutes, serverConfigFile, + indepBffPrefix, } = appContext; logger.info(`Starting production server...`); @@ -72,16 +73,21 @@ export const start = async (api: CLIPluginAPI>) => { appContext.appDirectory, appContext.distDirectory, ), - apiDirectory: getTargetDir( - appContext.apiDirectory, - appContext.appDirectory, - appContext.distDirectory, - ), - lambdaDirectory: getTargetDir( - appContext.lambdaDirectory, - appContext.appDirectory, - appContext.distDirectory, - ), + apiDirectory: indepBffPrefix + ? appContext.apiDirectory + : getTargetDir( + appContext.apiDirectory, + appContext.appDirectory, + appContext.distDirectory, + ), + lambdaDirectory: indepBffPrefix + ? appContext.lambdaDirectory + : getTargetDir( + appContext.lambdaDirectory, + appContext.appDirectory, + appContext.distDirectory, + ), + indepBffPrefix, }, runMode, }); diff --git a/packages/solutions/app-tools/src/index.ts b/packages/solutions/app-tools/src/index.ts index 5be38e12bd0..d2c4bd58ffd 100644 --- a/packages/solutions/app-tools/src/index.ts +++ b/packages/solutions/app-tools/src/index.ts @@ -2,6 +2,7 @@ import { getLocaleLanguage } from '@modern-js/plugin-i18n/language-detector'; import { createAsyncHook, createCollectAsyncHook } from '@modern-js/plugin-v2'; import { castArray } from '@modern-js/uni-builder'; import { + fs, cleanRequireCache, deprecatedCommands, emptyDir, @@ -178,7 +179,17 @@ export const appTools = ( api.onFileChanged(async e => { const { filename, eventType, isPrivate } = e; - if (!isPrivate && (eventType === 'change' || eventType === 'unlink')) { + const config = api.getNormalizedConfig(); + + const isApiProject = + !!config?.bff?.enableCrossProjectInvocation && + filename.startsWith('api/'); + + if ( + !isPrivate && + (eventType === 'change' || eventType === 'unlink') && + !isApiProject + ) { const { closeServer } = await import('./utils/createServer.js'); await closeServer(); await restart(api.getHooks(), filename); @@ -197,6 +208,7 @@ export type { RuntimeUserConfig } from './types/config'; export { dev } from './commands/dev'; export type { DevOptions } from './utils/types'; +export { generateWatchFiles } from './utils/generateWatchFiles'; export * from './types'; diff --git a/packages/solutions/app-tools/src/plugins/deploy/platforms/node.ts b/packages/solutions/app-tools/src/plugins/deploy/platforms/node.ts index e96f528977f..cd155ec49cc 100644 --- a/packages/solutions/app-tools/src/plugins/deploy/platforms/node.ts +++ b/packages/solutions/app-tools/src/plugins/deploy/platforms/node.ts @@ -49,6 +49,7 @@ export const createNodePreset: CreatePreset = (appContext, config) => { }; const pluginImportCode = genPluginImportsCode(plugins || []); + const dynamicProdOptions = { config: serverConfig, serverConfigFile: DEFAULT_SERVER_CONFIG, diff --git a/packages/solutions/app-tools/src/types/new.ts b/packages/solutions/app-tools/src/types/new.ts index 27f7ddea21e..b801684e19e 100644 --- a/packages/solutions/app-tools/src/types/new.ts +++ b/packages/solutions/app-tools/src/types/new.ts @@ -153,6 +153,7 @@ export type AppToolsExtendContext = { internalSrcAlias: string; apiDirectory: string; lambdaDirectory: string; + indepBffPrefix: string; serverConfigFile: string; serverPlugins: ServerPlugin[]; moduleType: 'module' | 'commonjs'; diff --git a/packages/toolkit/utils/src/cli/require.ts b/packages/toolkit/utils/src/cli/require.ts index 23e6ccdc874..3729cd4a456 100644 --- a/packages/toolkit/utils/src/cli/require.ts +++ b/packages/toolkit/utils/src/cli/require.ts @@ -23,6 +23,7 @@ export async function compatibleRequire( const modulePath = isAbsolute(path) ? pathToFileURL(path).href : path; if (process.env.NODE_ENV === 'development') { const timestamp = Date.now(); + requiredModule = await import(`${modulePath}?t=${timestamp}`); } else { requiredModule = await import(modulePath); diff --git a/tests/integration/bff-api-app/api/_app.ts b/tests/integration/bff-api-app/api/_app.ts new file mode 100644 index 00000000000..68545206139 --- /dev/null +++ b/tests/integration/bff-api-app/api/_app.ts @@ -0,0 +1,19 @@ +import { hook } from '@modern-js/runtime/server'; +import type { NextFunction, Request, Response } from 'express'; + +export default hook(({ addMiddleware }) => { + addMiddleware(async (req: Request, res: Response, next: NextFunction) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, x-header, Authorization, X-Requested-With, x-env', + ); + + if (req.path === '/api-app/foo') { + res.end('foo'); + } else { + res.setHeader('x-env', 'test'); + next(); + } + }); +}); diff --git a/tests/integration/bff-api-app/api/context/index.ts b/tests/integration/bff-api-app/api/context/index.ts new file mode 100644 index 00000000000..dda927523ce --- /dev/null +++ b/tests/integration/bff-api-app/api/context/index.ts @@ -0,0 +1,10 @@ +import { useContext } from '@modern-js/runtime/server'; + +export default async () => { + const ctx = useContext(); + const { res } = ctx; + res.append('x-id', '1'); + return { + message: 'Hello Modern.js', + }; +}; diff --git a/tests/integration/bff-api-app/api/index.ts b/tests/integration/bff-api-app/api/index.ts new file mode 100644 index 00000000000..72a96b77f4f --- /dev/null +++ b/tests/integration/bff-api-app/api/index.ts @@ -0,0 +1,55 @@ +import { + Api, + Data, + Headers, + Params, + Post, + Query, + useContext, +} from '@modern-js/runtime/server'; +import { z } from 'zod'; + +export default async () => { + return { + message: 'Hello get bff-api-app', + }; +}; + +export const post = async () => { + return { + message: 'Hello post bff-api-app', + }; +}; + +const QuerySchema = z.object({ + user: z.string().email(), +}); + +const DataSchema = z.object({ + info: z.array(z.record(z.string(), z.union([z.string(), z.number()]))), +}); + +const ParamsSchema = z.object({ + id: z.string(), +}); + +const HeadersSchema = z.object({ + 'x-header': z.string(), +}); + +export const postHello = Api( + Post('/hello/:id'), + Params(ParamsSchema), + Query(QuerySchema), + Data(DataSchema), + Headers(HeadersSchema), + async ({ query, data, params, headers }) => { + const ctx = useContext(); + return { + params, + query, + data, + headers, + }; + }, +); diff --git a/tests/integration/bff-api-app/api/upload.ts b/tests/integration/bff-api-app/api/upload.ts new file mode 100644 index 00000000000..fbe415c648b --- /dev/null +++ b/tests/integration/bff-api-app/api/upload.ts @@ -0,0 +1,21 @@ +import { Api, Upload } from '@modern-js/runtime/server'; +import { z } from 'zod'; + +const FileSchema = z.object({ + images: z.record(z.string(), z.any()), + params: z.string(), +}); + +export const upload = Api( + Upload('/upload', FileSchema), + async ({ formData }) => { + // do somethings + return { + data: { + code: 0, + file_name: formData.images.name as string, + params: JSON.parse(formData.params), + }, + }; + }, +); diff --git a/tests/integration/bff-api-app/api/user/[id].ts b/tests/integration/bff-api-app/api/user/[id].ts new file mode 100644 index 00000000000..63d80a5f4d0 --- /dev/null +++ b/tests/integration/bff-api-app/api/user/[id].ts @@ -0,0 +1,6 @@ +export default async (id: string) => { + return { + id, + message: `bff-api-app/user/[id]`, + }; +}; diff --git a/tests/integration/bff-api-app/api/user/index.ts b/tests/integration/bff-api-app/api/user/index.ts new file mode 100644 index 00000000000..c59fc4e0dba --- /dev/null +++ b/tests/integration/bff-api-app/api/user/index.ts @@ -0,0 +1,28 @@ +import { Api, Post, Query } from '@modern-js/runtime/server'; +import { z } from 'zod'; + +const UserSchema = z.object({ + phone: z.string(), +}); + +export const email = Api(Post('/email'), Query(UserSchema), async () => { + return { + data: { + code: 0, + }, + }; +}); + +export default async () => { + console.log('User get bff-api-app'); + return { + message: 'User get bff-api-app', + }; +}; + +export const post = async () => { + console.log('User post bff-api-app'); + return { + message: 'User post bff-api-app12', + }; +}; diff --git a/tests/integration/bff-api-app/modern-app-env.d.ts b/tests/integration/bff-api-app/modern-app-env.d.ts new file mode 100644 index 00000000000..eaf4337e191 --- /dev/null +++ b/tests/integration/bff-api-app/modern-app-env.d.ts @@ -0,0 +1,4 @@ +/// +/// +/// +/// diff --git a/tests/integration/bff-api-app/modern.config.ts b/tests/integration/bff-api-app/modern.config.ts new file mode 100644 index 00000000000..6748c9036b0 --- /dev/null +++ b/tests/integration/bff-api-app/modern.config.ts @@ -0,0 +1,11 @@ +import { bffPlugin } from '@modern-js/plugin-bff'; +import { expressPlugin } from '@modern-js/plugin-express'; +import { applyBaseConfig } from '../../utils/applyBaseConfig'; + +export default applyBaseConfig({ + bff: { + prefix: '/api-app', + enableCrossProjectInvocation: true, + }, + plugins: [expressPlugin(), bffPlugin()], +}); diff --git a/tests/integration/bff-api-app/package.json b/tests/integration/bff-api-app/package.json new file mode 100644 index 00000000000..74fad8091fa --- /dev/null +++ b/tests/integration/bff-api-app/package.json @@ -0,0 +1,63 @@ +{ + "name": "bff-api-app", + "version": "0.0.2", + "scripts": { + "dev": "modern dev", + "dev:bff": "modern dev --api-only", + "build": "modern build", + "serve": "modern serve", + "deploy": "modern deploy", + "start:bff": "modern serve --api-only", + "new": "modern new" + }, + "engines": { + "node": ">=14.17.6" + }, + "dependencies": { + "@modern-js/plugin-express": "workspace:*", + "@modern-js/plugin-bff": "workspace:*", + "@modern-js/runtime": "workspace:*", + "express": "^4.17.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "ts-node": "^10.9.1", + "tsconfig-paths": "3.14.1", + "zod": "^3.22.3" + }, + "devDependencies": { + "@modern-js/app-tools": "workspace:*", + "@types/express": "^4.17.13", + "@types/jest": "^29", + "@types/node": "^14", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "typescript": "^5" + }, + "exports": { + "./context/index": { + "import": "./dist/client/context/index.js", + "types": "./dist/api/context/index.d.ts" + }, + "./index": { + "import": "./dist/client/index.js", + "types": "./dist/api/index.d.ts" + }, + "./upload": { + "import": "./dist/client/upload.js", + "types": "./dist/api/upload.d.ts" + }, + "./user/[id]": { + "import": "./dist/client/user/[id].js", + "types": "./dist/api/user/[id].d.ts" + }, + "./user/index": { + "import": "./dist/client/user/index.js", + "types": "./dist/api/user/index.d.ts" + }, + "./server-plugin": "./dist/server-plugin/index.js", + "./runtime": { + "import": "./dist/runtime/index.js", + "types": "./dist/runtime/index.d.ts" + } + } +} \ No newline at end of file diff --git a/tests/integration/bff-api-app/tests/index.test.ts b/tests/integration/bff-api-app/tests/index.test.ts new file mode 100644 index 00000000000..907c14a7286 --- /dev/null +++ b/tests/integration/bff-api-app/tests/index.test.ts @@ -0,0 +1,82 @@ +import dns from 'node:dns'; +import path from 'path'; +import { + getPort, + killApp, + launchApp, + modernBuild, + modernServe, +} from '../../../utils/modernTestUtils'; +import 'isomorphic-fetch'; + +dns.setDefaultResultOrder('ipv4first'); + +const appDir = path.resolve(__dirname, '../'); + +const testApiWorked = async ({ + host, + port, + prefix, +}: { + host: string; + port: number; + prefix: string; +}) => { + const expectedText = 'Hello get bff-api-app'; + const res = await fetch(`${host}:${port}${prefix}`); + expect(res.status).toBe(200); + const text = await res.text(); + expect(text).toBe(JSON.stringify({ message: expectedText })); +}; + +describe('bff api-app in dev', () => { + let port = 8080; + const host = `http://localhost`; + const prefix = '/api-app'; + let app: any; + + beforeAll(async () => { + jest.setTimeout(1000 * 60 * 2); + port = await getPort(); + app = await launchApp(appDir, port, {}); + }); + + test('api-app should works', async () => { + await testApiWorked({ + host, + port, + prefix, + }); + }); + + afterAll(async () => { + await killApp(app); + }); +}); + +describe('bff api-app in prod', () => { + let port = 8080; + const host = `http://localhost`; + const prefix = '/api-app'; + let app: any; + + beforeAll(async () => { + port = await getPort(); + + await modernBuild(appDir, [], {}); + + app = await modernServe(appDir, port, {}); + }); + + test('api-app should works', async () => { + await testApiWorked({ + host, + port, + prefix, + }); + }); + + afterAll(async () => { + await killApp(app); + }); +}); diff --git a/tests/integration/bff-api-app/tests/tsconfig.json b/tests/integration/bff-api-app/tests/tsconfig.json new file mode 100644 index 00000000000..10f49432232 --- /dev/null +++ b/tests/integration/bff-api-app/tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": true, + "jsx": "preserve", + "baseUrl": "./", + "emitDeclarationOnly": true, + "isolatedModules": true, + "paths": {}, + "types": ["node", "jest"] + } +} diff --git a/tests/integration/bff-api-app/tsconfig.json b/tests/integration/bff-api-app/tsconfig.json new file mode 100644 index 00000000000..24d19c3192d --- /dev/null +++ b/tests/integration/bff-api-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": true, + "jsx": "preserve", + "baseUrl": "./", + "paths": { + "@shared/*": ["./shared/*"], + "@api/*": ["./api/*"] + }, + "types": ["jest"] + }, + "include": [ + "shared", + "config", + "api", + "modern.config.ts", + "modern-app-env.d.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/tests/integration/bff-client-app/api/_app.ts b/tests/integration/bff-client-app/api/_app.ts new file mode 100644 index 00000000000..53396293b9f --- /dev/null +++ b/tests/integration/bff-client-app/api/_app.ts @@ -0,0 +1,12 @@ +import { hook } from '@modern-js/runtime/server'; +import type { NextFunction, Request, Response } from 'express'; + +export default hook(({ addMiddleware }) => { + addMiddleware(async (req: Request, res: Response, next: NextFunction) => { + if (req.path === '/api-app/foo') { + res.end('foo'); + } else { + next(); + } + }); +}); diff --git a/tests/integration/bff-client-app/api/context/index.ts b/tests/integration/bff-client-app/api/context/index.ts new file mode 100644 index 00000000000..dda927523ce --- /dev/null +++ b/tests/integration/bff-client-app/api/context/index.ts @@ -0,0 +1,10 @@ +import { useContext } from '@modern-js/runtime/server'; + +export default async () => { + const ctx = useContext(); + const { res } = ctx; + res.append('x-id', '1'); + return { + message: 'Hello Modern.js', + }; +}; diff --git a/tests/integration/bff-client-app/api/index.ts b/tests/integration/bff-client-app/api/index.ts new file mode 100644 index 00000000000..1fb15fa0baa --- /dev/null +++ b/tests/integration/bff-client-app/api/index.ts @@ -0,0 +1,51 @@ +import { + Api, + Data, + Headers, + Params, + Post, + Query, + useContext, +} from '@modern-js/runtime/server'; +import { z } from 'zod'; + +export default async () => ({ + message: 'Hello Modern.js get client', +}); + +export const post = async () => ({ + message: 'Hello Modern.js post client', +}); + +const QuerySchema = z.object({ + user: z.string().email(), +}); + +const DataSchema = z.object({ + message: z.string(), +}); + +const ParamsSchema = z.object({ + id: z.string(), +}); + +const HeadersSchema = z.object({ + 'x-header': z.string(), +}); + +export const postHello = Api( + Post('/hello/:id'), + Params(ParamsSchema), + Query(QuerySchema), + Data(DataSchema), + Headers(HeadersSchema), + async ({ query, data, params, headers }) => { + const ctx = useContext(); + return { + params, + query, + data, + headers, + }; + }, +); diff --git a/tests/integration/bff-client-app/api/upload.ts b/tests/integration/bff-client-app/api/upload.ts new file mode 100644 index 00000000000..4e052da86ec --- /dev/null +++ b/tests/integration/bff-client-app/api/upload.ts @@ -0,0 +1,19 @@ +import { Api, Upload } from '@modern-js/runtime/server'; +import { z } from 'zod'; + +const FileSchema = z.object({ + images: z.record(z.string(), z.any()), +}); + +export const upload = Api( + Upload('/upload', FileSchema), + async ({ formData }) => { + // do somethings + return { + data: { + code: 10, + file_name: formData.images.name, + }, + }; + }, +); diff --git a/tests/integration/bff-client-app/modern.config.ts b/tests/integration/bff-client-app/modern.config.ts new file mode 100644 index 00000000000..99e845179e1 --- /dev/null +++ b/tests/integration/bff-client-app/modern.config.ts @@ -0,0 +1,14 @@ +import { bffPlugin } from '@modern-js/plugin-bff'; +import { expressPlugin } from '@modern-js/plugin-express'; +import { serverPlugin } from 'bff-api-app/server-plugin'; +import { applyBaseConfig } from '../../utils/applyBaseConfig'; + +export default applyBaseConfig({ + bff: { + prefix: '/web-app', + }, + server: { + ssr: true, + }, + plugins: [bffPlugin(), expressPlugin(), serverPlugin()], +}); diff --git a/tests/integration/bff-client-app/package.json b/tests/integration/bff-client-app/package.json new file mode 100644 index 00000000000..35ed60032b4 --- /dev/null +++ b/tests/integration/bff-client-app/package.json @@ -0,0 +1,37 @@ +{ + "private": true, + "name": "bff-client-app", + "version": "2.9.0", + "scripts": { + "dev": "modern dev", + "dev:bff": "modern dev --api-only", + "build": "modern build", + "serve": "modern serve", + "start:bff": "modern serve --api-only", + "deploy": "modern deploy", + "new": "modern new" + }, + "engines": { + "node": ">=14.17.6" + }, + "dependencies": { + "@modern-js/plugin-express": "workspace:*", + "@modern-js/runtime": "workspace:*", + "express": "^4.17.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "ts-node": "^10.9.1", + "tsconfig-paths": "3.14.1", + "bff-api-app": "workspace:*", + "zod": "^3.22.3" + }, + "devDependencies": { + "@modern-js/app-tools": "workspace:*", + "@types/express": "^4.17.13", + "@types/jest": "^29", + "@types/node": "^14", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "typescript": "^5" + } +} diff --git a/tests/integration/bff-client-app/src/base/App.tsx b/tests/integration/bff-client-app/src/base/App.tsx new file mode 100644 index 00000000000..c6f13366351 --- /dev/null +++ b/tests/integration/bff-client-app/src/base/App.tsx @@ -0,0 +1,51 @@ +import context from 'bff-api-app/context/index'; +import hello, { postHello } from 'bff-api-app/index'; +import { configure } from 'bff-api-app/runtime'; +import getUser from 'bff-api-app/user/[id]'; +import { useEffect, useState } from 'react'; + +configure({ + interceptor(request) { + return async (url, params) => { + const res = await request(url, params); + return res.json(); + }; + }, +}); + +const App = () => { + const [message, setMessage] = useState('bff-client'); + useEffect(() => { + const fetchData = async () => { + const res = await hello(); + // 加一个延时,帮助集测取第一次的 message 值 + await new Promise(resolve => setTimeout(resolve, 50)); + setMessage(res.message); + }; + + fetchData(); + context(); + getUser('1234'); + postHello({ + params: { + id: '123', + }, + query: { + user: 'test@mail.com', + }, + data: { + info: [ + { + key: '123', + }, + ], + }, + headers: { + 'x-header': '3333', + }, + }); + }, []); + return
{message}
; +}; + +export default App; diff --git a/tests/integration/bff-client-app/src/custom-sdk/App.tsx b/tests/integration/bff-client-app/src/custom-sdk/App.tsx new file mode 100644 index 00000000000..f0f369202cd --- /dev/null +++ b/tests/integration/bff-client-app/src/custom-sdk/App.tsx @@ -0,0 +1,25 @@ +import { useLoader } from '@modern-js/runtime'; +import hello from 'bff-api-app/index'; +import { configure } from 'bff-api-app/runtime'; + +configure({ + interceptor(request) { + return async (url, params) => { + const res = await request(url, params); + const data = await res.json(); + data.message = 'Hello Custom SDK'; + return data; + }; + }, +}); + +const App = () => { + const { data } = useLoader(async () => { + const res = await hello(); + return res; + }); + const { message = 'bff-express' } = data || {}; + return
{message}
; +}; + +export default App; diff --git a/tests/integration/bff-client-app/src/modern-app-env.d.ts b/tests/integration/bff-client-app/src/modern-app-env.d.ts new file mode 100644 index 00000000000..eaf4337e191 --- /dev/null +++ b/tests/integration/bff-client-app/src/modern-app-env.d.ts @@ -0,0 +1,4 @@ +/// +/// +/// +/// diff --git a/tests/integration/bff-client-app/src/ssr/App.tsx b/tests/integration/bff-client-app/src/ssr/App.tsx new file mode 100644 index 00000000000..b52805d204b --- /dev/null +++ b/tests/integration/bff-client-app/src/ssr/App.tsx @@ -0,0 +1,13 @@ +import { useLoader } from '@modern-js/runtime'; +import hello from 'bff-api-app/index'; + +const App = () => { + const { data } = useLoader(async () => { + const res = await hello(); + return res; + }); + const { message = 'bff-express' } = data || {}; + return
{message}
; +}; + +export default App; diff --git a/tests/integration/bff-client-app/src/upload/App.tsx b/tests/integration/bff-client-app/src/upload/App.tsx new file mode 100644 index 00000000000..1a3b3dae0c6 --- /dev/null +++ b/tests/integration/bff-client-app/src/upload/App.tsx @@ -0,0 +1,82 @@ +import { upload } from 'bff-api-app/upload'; +import React, { useEffect } from 'react'; + +const getMockImage = () => { + const imageData = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; + const blob = new Blob( + [Uint8Array.from(atob(imageData.split(',')[1]), c => c.charCodeAt(0))], + { type: 'image/png' }, + ); + + return new File([blob], 'mock_image.png', { type: 'image/png' }); +}; + +const Index = (): JSX.Element => { + const [file, setFile] = React.useState(); + const [fileName, setFileName] = React.useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(); + if (file) { + for (let i = 0; i < file.length; i++) { + formData.append('images', file[i]); + } + await fetch('/bff-api/upload', { + method: 'POST', + body: formData, + }); + } + }; + + const click = async () => { + if (!file) { + return; + } + const res = await upload({ + files: { + images: file, + params: JSON.stringify({ + a: 1, + b: 2, + }), + }, + }); + }; + + useEffect(() => { + upload({ + files: { + images: getMockImage(), + params: JSON.stringify({ + a: 1, + b: 2, + }), + }, + }).then(res => { + setFileName(res.data.file_name); + }); + }, []); + + return ( + <> +

File Upload

+

{fileName}

+
+ + +
+
+ + +
+ + ); +}; + +export default Index; diff --git a/tests/integration/bff-client-app/tests/index.test.ts b/tests/integration/bff-client-app/tests/index.test.ts new file mode 100644 index 00000000000..8233eafa1e8 --- /dev/null +++ b/tests/integration/bff-client-app/tests/index.test.ts @@ -0,0 +1,173 @@ +import dns from 'node:dns'; +import path from 'path'; +import puppeteer, { type Browser, type Page } from 'puppeteer'; +import { + getPort, + killApp, + launchApp, + launchOptions, + modernBuild, + modernServe, +} from '../../../utils/modernTestUtils'; +import 'isomorphic-fetch'; + +dns.setDefaultResultOrder('ipv4first'); + +const appDir = path.resolve(__dirname, '../'); +const apiAppDir = path.resolve(__dirname, '../../bff-api-app'); + +describe('bff client-app in dev', () => { + const expectedText = 'Hello get bff-api-app'; + let port = 8080; + let apiPort = 8081; + const SSR_PAGE = 'ssr'; + const BASE_PAGE = 'base'; + const CUSTOM_PAGE = 'custom-sdk'; + const UPLOAD_PAGE = 'upload'; + const host = `http://localhost`; + const prefix = '/api-app'; + let app: any; + let apiApp: any; + let page: Page; + let browser: Browser; + + beforeAll(async () => { + jest.setTimeout(1000 * 60 * 2); + apiPort = await getPort(); + apiApp = await launchApp(apiAppDir, apiPort, {}); + + jest.setTimeout(1000 * 60 * 2); + port = await getPort(); + app = await launchApp(appDir, port, {}); + browser = await puppeteer.launch(launchOptions as any); + page = await browser.newPage(); + }); + + test('basic usage', async () => { + await page.goto(`${host}:${port}/${BASE_PAGE}`, { + timeout: 50000, + }); + await new Promise(resolve => setTimeout(resolve, 3000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe(expectedText); + }); + + test('basic usage with ssr', async () => { + await page.goto(`${host}:${port}/${SSR_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 3000)); + const text1 = await page.$eval('.hello', el => el?.textContent); + expect(text1).toBe(expectedText); + }); + + test('support useContext', async () => { + const res = await fetch(`${host}:${port}${prefix}/context`); + const info = await res.json(); + expect(res.headers.get('x-id')).toBe('1'); + expect(info.message).toBe('Hello Modern.js'); + }); + + test('support _app.ts', async () => { + const res = await fetch(`${host}:${port}${prefix}/foo`); + const text = await res.text(); + expect(text).toBe('foo'); + }); + + test('support custom sdk', async () => { + await page.goto(`${host}:${port}/${CUSTOM_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe('Hello Custom SDK'); + }); + + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + + afterAll(async () => { + await killApp(app); + await killApp(apiApp); + await page.close(); + await browser.close(); + }); +}); + +describe('bff client-app in prod', () => { + const expectedText = 'Hello get bff-api-app'; + let port = 8080; + let apiPort = 8081; + const SSR_PAGE = 'ssr'; + const BASE_PAGE = 'base'; + const CUSTOM_PAGE = 'custom-sdk'; + const UPLOAD_PAGE = 'upload'; + const host = `http://localhost`; + const prefix = '/api-app'; + let app: any; + let apiApp: any; + let page: Page; + let browser: Browser; + + beforeAll(async () => { + apiPort = await getPort(); + await modernBuild(apiAppDir, [], {}); + apiApp = await modernServe(apiAppDir, apiPort, {}); + + port = await getPort(); + await modernBuild(appDir, [], {}); + app = await modernServe(appDir, port, {}); + + browser = await puppeteer.launch(launchOptions as any); + page = await browser.newPage(); + }); + + test('basic usage', async () => { + await page.goto(`${host}:${port}/${BASE_PAGE}`, { + timeout: 50000, + }); + await new Promise(resolve => setTimeout(resolve, 3000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe(expectedText); + }); + + test('basic usage with ssr', async () => { + await page.goto(`${host}:${port}/${SSR_PAGE}`); + const text1 = await page.$eval('.hello', el => el?.textContent); + expect(text1).toBe(expectedText); + }); + + test('support useContext', async () => { + const res = await fetch(`${host}:${port}${prefix}/context`); + const info = await res.json(); + expect(res.headers.get('x-id')).toBe('1'); + expect(info.message).toBe('Hello Modern.js'); + }); + + test('support _app.ts', async () => { + const res = await fetch(`${host}:${port}${prefix}/foo`); + const text = await res.text(); + expect(text).toBe('foo'); + }); + + test('support custom sdk', async () => { + await page.goto(`${host}:${port}/${CUSTOM_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe('Hello Custom SDK'); + }); + + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + + afterAll(async () => { + await killApp(app); + await killApp(apiApp); + await page.close(); + await browser.close(); + }); +}); diff --git a/tests/integration/bff-client-app/tests/tsconfig.json b/tests/integration/bff-client-app/tests/tsconfig.json new file mode 100644 index 00000000000..10f49432232 --- /dev/null +++ b/tests/integration/bff-client-app/tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": true, + "jsx": "preserve", + "baseUrl": "./", + "emitDeclarationOnly": true, + "isolatedModules": true, + "paths": {}, + "types": ["node", "jest"] + } +} diff --git a/tests/integration/bff-client-app/tsconfig.json b/tests/integration/bff-client-app/tsconfig.json new file mode 100644 index 00000000000..1141e5c1bb0 --- /dev/null +++ b/tests/integration/bff-client-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "jsx": "preserve", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"], + "@shared/*": ["./shared/*"], + "@api/*": ["./api/*"] + }, + "types": ["jest"] + }, + "include": ["src", "shared", "config", "api", "modern.config.ts"] +} diff --git a/tests/integration/bff-express/api/upload.ts b/tests/integration/bff-express/api/upload.ts index 73fa2ecf5eb..4e052da86ec 100644 --- a/tests/integration/bff-express/api/upload.ts +++ b/tests/integration/bff-express/api/upload.ts @@ -11,7 +11,7 @@ export const upload = Api( // do somethings return { data: { - code: 0, + code: 10, file_name: formData.images.name, }, }; diff --git a/tests/integration/bff-express/src/base/App.tsx b/tests/integration/bff-express/src/base/App.tsx index 4defb7bb842..5a4c8b8353d 100644 --- a/tests/integration/bff-express/src/base/App.tsx +++ b/tests/integration/bff-express/src/base/App.tsx @@ -36,7 +36,7 @@ const App = () => { 'x-header': '3333', }, }); - }); + }, []); return
{message}
; }; diff --git a/tests/integration/bff-indep-client-app/api/_app.ts b/tests/integration/bff-indep-client-app/api/_app.ts new file mode 100644 index 00000000000..53396293b9f --- /dev/null +++ b/tests/integration/bff-indep-client-app/api/_app.ts @@ -0,0 +1,12 @@ +import { hook } from '@modern-js/runtime/server'; +import type { NextFunction, Request, Response } from 'express'; + +export default hook(({ addMiddleware }) => { + addMiddleware(async (req: Request, res: Response, next: NextFunction) => { + if (req.path === '/api-app/foo') { + res.end('foo'); + } else { + next(); + } + }); +}); diff --git a/tests/integration/bff-indep-client-app/api/context/index.ts b/tests/integration/bff-indep-client-app/api/context/index.ts new file mode 100644 index 00000000000..dda927523ce --- /dev/null +++ b/tests/integration/bff-indep-client-app/api/context/index.ts @@ -0,0 +1,10 @@ +import { useContext } from '@modern-js/runtime/server'; + +export default async () => { + const ctx = useContext(); + const { res } = ctx; + res.append('x-id', '1'); + return { + message: 'Hello Modern.js', + }; +}; diff --git a/tests/integration/bff-indep-client-app/api/index.ts b/tests/integration/bff-indep-client-app/api/index.ts new file mode 100644 index 00000000000..1fb15fa0baa --- /dev/null +++ b/tests/integration/bff-indep-client-app/api/index.ts @@ -0,0 +1,51 @@ +import { + Api, + Data, + Headers, + Params, + Post, + Query, + useContext, +} from '@modern-js/runtime/server'; +import { z } from 'zod'; + +export default async () => ({ + message: 'Hello Modern.js get client', +}); + +export const post = async () => ({ + message: 'Hello Modern.js post client', +}); + +const QuerySchema = z.object({ + user: z.string().email(), +}); + +const DataSchema = z.object({ + message: z.string(), +}); + +const ParamsSchema = z.object({ + id: z.string(), +}); + +const HeadersSchema = z.object({ + 'x-header': z.string(), +}); + +export const postHello = Api( + Post('/hello/:id'), + Params(ParamsSchema), + Query(QuerySchema), + Data(DataSchema), + Headers(HeadersSchema), + async ({ query, data, params, headers }) => { + const ctx = useContext(); + return { + params, + query, + data, + headers, + }; + }, +); diff --git a/tests/integration/bff-indep-client-app/api/upload.ts b/tests/integration/bff-indep-client-app/api/upload.ts new file mode 100644 index 00000000000..4e052da86ec --- /dev/null +++ b/tests/integration/bff-indep-client-app/api/upload.ts @@ -0,0 +1,19 @@ +import { Api, Upload } from '@modern-js/runtime/server'; +import { z } from 'zod'; + +const FileSchema = z.object({ + images: z.record(z.string(), z.any()), +}); + +export const upload = Api( + Upload('/upload', FileSchema), + async ({ formData }) => { + // do somethings + return { + data: { + code: 10, + file_name: formData.images.name, + }, + }; + }, +); diff --git a/tests/integration/bff-indep-client-app/modern.config.ts b/tests/integration/bff-indep-client-app/modern.config.ts new file mode 100644 index 00000000000..b0d47c613c5 --- /dev/null +++ b/tests/integration/bff-indep-client-app/modern.config.ts @@ -0,0 +1,13 @@ +import { bffPlugin } from '@modern-js/plugin-bff'; +import { expressPlugin } from '@modern-js/plugin-express'; +import { applyBaseConfig } from '../../utils/applyBaseConfig'; + +export default applyBaseConfig({ + bff: { + prefix: '/indep-web-app', + }, + server: { + ssr: true, + }, + plugins: [bffPlugin(), expressPlugin()], +}); diff --git a/tests/integration/bff-indep-client-app/package.json b/tests/integration/bff-indep-client-app/package.json new file mode 100644 index 00000000000..9bd7bade578 --- /dev/null +++ b/tests/integration/bff-indep-client-app/package.json @@ -0,0 +1,38 @@ +{ + "private": true, + "name": "bff-indep-client-app", + "version": "2.9.0", + "scripts": { + "dev": "modern dev", + "dev:bff": "modern dev --api-only", + "build": "modern build", + "serve": "modern serve", + "start:bff": "modern serve --api-only", + "deploy": "modern deploy", + "new": "modern new" + }, + "engines": { + "node": ">=14.17.6" + }, + "dependencies": { + "@modern-js/plugin-express": "workspace:*", + "@modern-js/runtime": "workspace:*", + "express": "^4.17.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "ts-node": "^10.9.1", + "tsconfig-paths": "3.14.1", + "bff-api-app": "workspace:*", + "axios": "^1.7.4", + "zod": "^3.22.3" + }, + "devDependencies": { + "@modern-js/app-tools": "workspace:*", + "@types/express": "^4.17.13", + "@types/jest": "^29", + "@types/node": "^14", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "typescript": "^5" + } +} diff --git a/tests/integration/bff-indep-client-app/src/base/App.tsx b/tests/integration/bff-indep-client-app/src/base/App.tsx new file mode 100644 index 00000000000..681ea6ea14c --- /dev/null +++ b/tests/integration/bff-indep-client-app/src/base/App.tsx @@ -0,0 +1,73 @@ +import innerHello from '@api/index'; +import context from 'bff-api-app/context/index'; +import hello, { postHello, post } from 'bff-api-app/index'; + +import { configure as innerConfigure } from '@modern-js/runtime/bff'; +import type { AxiosRequestHeaders as Headers, Method } from 'axios'; +import axios from 'axios'; +import { configure } from 'bff-api-app/runtime'; +import getUser from 'bff-api-app/user/[id]'; +import { useEffect, useState } from 'react'; + +configure({ + interceptor(request) { + return async (url, params) => { + const path = `http://localhost:8080${url}`; + const res = await request(path, params); + return res.json(); + }; + }, +}); + +innerConfigure({ + async request(...config: Parameters) { + const [url, params] = config; + params?.headers && ((params.headers as any)['x-header'] = 'innerConfigure'); + const res = await axios({ + url: url as string, // 这里因为 fetch 和 axios 类型有些不兼容,需要使用 as + method: params?.method as Method, + data: params?.body, + headers: params?.headers as Headers, + }); + return res.data; + }, +}); + +const App = () => { + const [message, setMessage] = useState('bff-client'); + useEffect(() => { + const fetchData = async () => { + const res = await hello(); + // 加一个延时,帮助集测取第一次的 message 值 + await new Promise(resolve => setTimeout(resolve, 50)); + setMessage(res.message); + }; + + fetchData(); + context(); + getUser('1234'); + postHello({ + params: { + id: '123', + }, + query: { + user: 'test@mail.com', + }, + data: { + info: [ + { + key: '123', + }, + ], + }, + headers: { + 'x-header': '3333', + }, + }); + post(); + innerHello(); + }, []); + return
{message}
; +}; + +export default App; diff --git a/tests/integration/bff-indep-client-app/src/custom-sdk/App.tsx b/tests/integration/bff-indep-client-app/src/custom-sdk/App.tsx new file mode 100644 index 00000000000..f0f369202cd --- /dev/null +++ b/tests/integration/bff-indep-client-app/src/custom-sdk/App.tsx @@ -0,0 +1,25 @@ +import { useLoader } from '@modern-js/runtime'; +import hello from 'bff-api-app/index'; +import { configure } from 'bff-api-app/runtime'; + +configure({ + interceptor(request) { + return async (url, params) => { + const res = await request(url, params); + const data = await res.json(); + data.message = 'Hello Custom SDK'; + return data; + }; + }, +}); + +const App = () => { + const { data } = useLoader(async () => { + const res = await hello(); + return res; + }); + const { message = 'bff-express' } = data || {}; + return
{message}
; +}; + +export default App; diff --git a/tests/integration/bff-indep-client-app/src/modern-app-env.d.ts b/tests/integration/bff-indep-client-app/src/modern-app-env.d.ts new file mode 100644 index 00000000000..eaf4337e191 --- /dev/null +++ b/tests/integration/bff-indep-client-app/src/modern-app-env.d.ts @@ -0,0 +1,4 @@ +/// +/// +/// +/// diff --git a/tests/integration/bff-indep-client-app/src/ssr/App.tsx b/tests/integration/bff-indep-client-app/src/ssr/App.tsx new file mode 100644 index 00000000000..3fc341caee2 --- /dev/null +++ b/tests/integration/bff-indep-client-app/src/ssr/App.tsx @@ -0,0 +1,34 @@ +import { useLoader } from '@modern-js/runtime'; +import hello from 'bff-api-app/index'; +import { configure } from 'bff-api-app/runtime'; +import user from 'bff-api-app/user/index'; +import { useEffect } from 'react'; + +configure({ + interceptor(request) { + return async (url, params) => { + let path = url; + if (!url.toString().includes('http')) { + path = `http://127.0.0.1:8080${url}`; + } + const res = await request(path, params); + return res.json(); + }; + }, +}); + +const App = () => { + const { data } = useLoader(async () => { + const res = await hello(); + return res; + }); + + useEffect(() => { + user(); + }, []); + + const { message = 'bff-express' } = data || {}; + return
{message}
; +}; + +export default App; diff --git a/tests/integration/bff-indep-client-app/src/upload/App.tsx b/tests/integration/bff-indep-client-app/src/upload/App.tsx new file mode 100644 index 00000000000..af89b7a434c --- /dev/null +++ b/tests/integration/bff-indep-client-app/src/upload/App.tsx @@ -0,0 +1,93 @@ +import { configure } from 'bff-api-app/runtime'; +import { upload } from 'bff-api-app/upload'; +import React, { useEffect } from 'react'; + +configure({ + interceptor(request) { + return async (url, params) => { + const path = `http://localhost:8080${url}`; + const res = await request(path, params); + return res.json(); + }; + }, +}); + +const getMockImage = () => { + const imageData = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; + const blob = new Blob( + [Uint8Array.from(atob(imageData.split(',')[1]), c => c.charCodeAt(0))], + { type: 'image/png' }, + ); + + return new File([blob], 'mock_image.png', { type: 'image/png' }); +}; + +const Index = (): JSX.Element => { + const [file, setFile] = React.useState(); + const [fileName, setFileName] = React.useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setFile(e.target.files); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(); + if (file) { + for (let i = 0; i < file.length; i++) { + formData.append('images', file[i]); + } + await fetch('/bff-api/upload', { + method: 'POST', + body: formData, + }); + } + }; + + const click = async () => { + if (!file) { + return; + } + const res = await upload({ + files: { + images: file, + params: JSON.stringify({ + a: 1, + b: 2, + }), + }, + }); + }; + + useEffect(() => { + upload({ + files: { + images: getMockImage(), + params: JSON.stringify({ + a: 1, + b: 2, + }), + }, + }).then(res => { + setFileName(res.data.file_name); + }); + }, []); + + return ( + <> +

File Upload

+

{fileName}

+
+ + +
+
+ + +
+ + ); +}; + +export default Index; diff --git a/tests/integration/bff-indep-client-app/tests/index.test.ts b/tests/integration/bff-indep-client-app/tests/index.test.ts new file mode 100644 index 00000000000..e71274475ce --- /dev/null +++ b/tests/integration/bff-indep-client-app/tests/index.test.ts @@ -0,0 +1,145 @@ +import dns from 'node:dns'; +import path from 'path'; +import puppeteer, { type Browser, type Page } from 'puppeteer'; +import { + getPort, + killApp, + launchApp, + launchOptions, + modernBuild, + modernServe, +} from '../../../utils/modernTestUtils'; +import 'isomorphic-fetch'; + +dns.setDefaultResultOrder('ipv4first'); + +const appDir = path.resolve(__dirname, '../'); +const apiAppDir = path.resolve(__dirname, '../../bff-api-app'); + +describe('bff client-app in dev', () => { + const expectedText = 'Hello get bff-api-app'; + const apiPort = 8080; + let port = 8081; + const SSR_PAGE = 'ssr'; + const BASE_PAGE = 'base'; + const CUSTOM_PAGE = 'custom-sdk'; + const UPLOAD_PAGE = 'upload'; + const host = `http://localhost`; + const prefix = '/api-app'; + let app: any; + let apiApp: any; + let page: Page; + let browser: Browser; + + beforeAll(async () => { + jest.setTimeout(1000 * 60 * 2); + apiApp = await launchApp(apiAppDir, apiPort, {}); + + jest.setTimeout(1000 * 60 * 2); + port = await getPort(); + app = await launchApp(appDir, port, {}); + browser = await puppeteer.launch(launchOptions as any); + page = await browser.newPage(); + }); + + test('basic usage', async () => { + await page.goto(`${host}:${port}/${BASE_PAGE}`, { + timeout: 50000, + }); + await new Promise(resolve => setTimeout(resolve, 2000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe(expectedText); + }); + + test('basic usage with ssr', async () => { + await page.goto(`${host}:${port}/${SSR_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + const text1 = await page.$eval('.hello', el => el?.textContent); + expect(text1).toBe(expectedText); + }); + + test('support custom sdk', async () => { + await page.goto(`${host}:${port}/${CUSTOM_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe('Hello Custom SDK'); + }); + + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + + afterAll(async () => { + await killApp(app); + await killApp(apiApp); + await page.close(); + await browser.close(); + }); +}); + +describe('bff client-app in prod', () => { + const expectedText = 'Hello get bff-api-app'; + const apiPort = 8080; + let port = 8081; + const SSR_PAGE = 'ssr'; + const BASE_PAGE = 'base'; + const CUSTOM_PAGE = 'custom-sdk'; + const UPLOAD_PAGE = 'upload'; + const host = `http://localhost`; + const prefix = '/api-app'; + let app: any; + let apiApp: any; + let page: Page; + let browser: Browser; + + beforeAll(async () => { + await modernBuild(apiAppDir, [], {}); + apiApp = await modernServe(apiAppDir, apiPort, {}); + + port = await getPort(); + await modernBuild(appDir, [], {}); + app = await modernServe(appDir, port, {}); + + browser = await puppeteer.launch(launchOptions as any); + page = await browser.newPage(); + }); + + test('basic usage', async () => { + await page.goto(`${host}:${port}/${BASE_PAGE}`, { + timeout: 50000, + }); + await new Promise(resolve => setTimeout(resolve, 2000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe(expectedText); + }); + + test('basic usage with ssr', async () => { + await page.goto(`${host}:${port}/${SSR_PAGE}`); + const text1 = await page.$eval('.hello', el => el?.textContent); + expect(text1).toBe(expectedText); + }); + + test('support custom sdk', async () => { + await page.goto(`${host}:${port}/${CUSTOM_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.hello', el => el?.textContent); + expect(text).toBe('Hello Custom SDK'); + }); + + test('support uoload', async () => { + await page.goto(`${host}:${port}/${UPLOAD_PAGE}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + const text = await page.$eval('.mock_file', el => el?.textContent); + expect(text).toBe('mock_image.png'); + }); + + afterAll(async () => { + await killApp(app); + await killApp(apiApp); + await page.close(); + await browser.close(); + }); +}); diff --git a/tests/integration/bff-indep-client-app/tests/tsconfig.json b/tests/integration/bff-indep-client-app/tests/tsconfig.json new file mode 100644 index 00000000000..10f49432232 --- /dev/null +++ b/tests/integration/bff-indep-client-app/tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": true, + "jsx": "preserve", + "baseUrl": "./", + "emitDeclarationOnly": true, + "isolatedModules": true, + "paths": {}, + "types": ["node", "jest"] + } +} diff --git a/tests/integration/bff-indep-client-app/tsconfig.json b/tests/integration/bff-indep-client-app/tsconfig.json new file mode 100644 index 00000000000..1141e5c1bb0 --- /dev/null +++ b/tests/integration/bff-indep-client-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@modern-js/tsconfig/base", + "compilerOptions": { + "declaration": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "jsx": "preserve", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"], + "@shared/*": ["./shared/*"], + "@api/*": ["./api/*"] + }, + "types": ["jest"] + }, + "include": ["src", "shared", "config", "api", "modern.config.ts"] +}