Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion packages/core/src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,24 @@ export function isClass(fn) {
return false;
}

if (/^class[\s{]/.test(ToString.call(fn))) {
const fnSource = ToString.call(fn);
if (/^class[\s{]/.test(fnSource)) {
return true;
}

// Tools like Bytenode replace function source text, so Function#toString is
// unreliable. ECMAScript class constructors have a non-writable `prototype`.
// Native constructors also satisfy this shape, so skip "[native code]" here.
const descriptor = Object.getOwnPropertyDescriptor(fn, 'prototype');
if (
descriptor &&
descriptor.writable === false &&
!/\[native code\]/.test(fnSource)
) {
return true;
}

return false;
}

export function isAsyncFunction(value) {
Expand Down
36 changes: 36 additions & 0 deletions packages/core/test/decorator/util/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,42 @@ describe('/test/util/index.test.ts', () => {
expect(Types.isString('')).toBeTruthy();
expect(Types.isString(undefined)).toBeFalsy();
expect(Types.isString({})).toBeFalsy();

function plainFn() {}
const asyncFn = async function () {};
expect(Types.isClass(plainFn)).toBeFalsy();
expect(Types.isClass(asyncFn)).toBeFalsy();
expect(Types.isClass(() => {})).toBeFalsy();
expect(Types.isClass(Object)).toBeFalsy();
expect(Types.isClass(Array)).toBeFalsy();
expect(Types.isClass(Function)).toBeFalsy();
});

it('should detect class when toString source is obscured', () => {
class ObscuredCtor {}
const origToString = Function.prototype.toString;
try {
jest.resetModules();
Function.prototype.toString = function () {
if (this === ObscuredCtor) {
return 'function () {}';
}
return origToString.call(this);
};

let isolatedTypes!: typeof Types;
jest.isolateModules(() => {
isolatedTypes = require('../../../src/util/types').Types;
});

expect(isolatedTypes.isClass(ObscuredCtor)).toBeTruthy();
expect(isolatedTypes.isClass(Object)).toBeFalsy();
expect(isolatedTypes.isClass(Array)).toBeFalsy();
expect(isolatedTypes.isClass(Function)).toBeFalsy();
} finally {
Function.prototype.toString = origToString;
jest.resetModules();
}
});

it('should test toAsyncFunction', async () => {
Expand Down
59 changes: 59 additions & 0 deletions site/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,65 @@ $ npm run bundle_start



### 使用 Bytenode 编译为字节码(可选)

在已完成 [单文件构建](#构建)(例如得到 `build/index.js`)之后,可以再用 [Bytenode](https://github.com/bytenode/bytenode) 将 JavaScript 编译为 V8 字节码(`.jsc`),进一步减少明文源码暴露。注意:**字节码与编译时使用的 Node.js 主版本强相关**,部署环境应使用相同(或官方保证兼容)的 Node 版本;跨机器部署前请在同版本 Node 下做冒烟验证。

Midway 在运行时会通过 `isClass` 等逻辑识别 ES `class` 构造函数。Bytenode 会替换 `Function.prototype.toString` 所见的源码,旧实现仅依赖 `toString` 可能把类误判为普通函数,从而导致依赖注入异常。**请使用包含该修复版本的 `@midwayjs/core`**(见源码 [`packages/core/src/util/types.ts`](https://github.com/midwayjs/midway/blob/main/packages/core/src/util/types.ts) 中 `isClass`)。

:::warning

Bytenode 官方说明:任何依赖 `Function.prototype.toString` 的逻辑在字节码下都可能异常(参见 [bytenode#34](https://github.com/bytenode/bytenode/issues/34))。除框架侧识别外,业务代码也应避免依赖函数源码字符串。

:::

#### 前置依赖

```bash
$ npm i bytenode --save-dev
```

在 `package.json` 中可写作:

```json
{
"devDependencies": {
"bytenode": "^1.5.7"
}
}
```

(版本号请安装时以 npm 可查到的最新兼容版本为准。)

#### 编译与启动

在单文件产物生成后,将入口 JS 编译为字节码,例如:

```bash
$ npx bytenode --compile build/index.js
```

默认会在同目录生成 `build/index.jsc`。使用 Bytenode 自带的方式启动(需已安装 `bytenode`,可为项目依赖或全局):

```bash
$ NODE_ENV=production npx bytenode ./build/index.jsc
```

也可将上述步骤写入 `scripts`,例如:

```json
{
"scripts": {
"bundle_jsc": "npm run bundle && npx bytenode --compile build/index.js",
"bundle_jsc_start": "NODE_ENV=production npx bytenode ./build/index.jsc"
}
}
```

[单文件构建部署](#单文件构建部署) 一节的约束仍然适用:例如依赖注入相关代码不要使用默认导出、配置需为对象模式、数据源 `entities` 路径扫描等限制不变。字节码发布后调试难度更高,建议保留未编译的构建流水线用于排障。



## 二进制文件部署

将 Node.js 打包为一个单独的可执行文件,部署时直接拷贝执行即可,这种方式包含了 node 运行时,业务代码,有利于保护知识产权。
Expand Down
59 changes: 59 additions & 0 deletions site/i18n/en/docusaurus-plugin-content-docs/current/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,65 @@ If boot access is fine, then you can distribute your build in the build director



### Optional: compile to bytecode with Bytenode

After a [single-file build](#construct) (for example `build/index.js`), you can use [Bytenode](https://github.com/bytenode/bytenode) to compile JavaScript into V8 bytecode (`.jsc`) and further reduce plaintext source exposure. **Bytecode is tied to the Node.js major version used at compile time**; production should run the same (or officially compatible) Node version, and you should smoke-test on that version before rollout.

Midway identifies ES `class` constructors in places like `isClass`. Bytenode replaces what `Function.prototype.toString` returns, so an implementation that only checks `toString` may treat classes as plain functions and break dependency injection. **Use a `@midwayjs/core` release that includes this fix** (see `isClass` in [`packages/core/src/util/types.ts`](https://github.com/midwayjs/midway/blob/main/packages/core/src/util/types.ts)).

:::warning

Bytenode documents that anything relying on `Function.prototype.toString` may break with bytecode (see [bytenode#34](https://github.com/bytenode/bytenode/issues/34)). Besides framework detection, avoid depending on function source strings in application code.

:::

#### Prerequisites

```bash
$ npm i bytenode --save-dev
```

In `package.json`:

```json
{
"devDependencies": {
"bytenode": "^1.5.7"
}
}
```

(Use a current compatible version from npm when you install.)

#### Compile and run

After the single-file bundle exists, compile the entry file:

```bash
$ npx bytenode --compile build/index.js
```

This typically produces `build/index.jsc` next to the source. Start it with Bytenode (local `node_modules` or global install):

```bash
$ NODE_ENV=production npx bytenode ./build/index.jsc
```

You can script it, for example:

```json
{
"scripts": {
"bundle_jsc": "npm run bundle && npx bytenode --compile build/index.js",
"bundle_jsc_start": "NODE_ENV=production npx bytenode ./build/index.jsc"
}
}
```

All constraints from the [single-file deployment](#single-file-deployment) section still apply: no default exports in DI-related code, config as object mode, datasource `entities` path scanning limitations, etc. Bytecode is harder to debug; keep an uncompiled build path for troubleshooting.
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in-page link target #single-file-deployment doesn’t appear to exist in this document. The relevant section header is ## Single file build deployment, which Docusaurus will slug to #single-file-build-deployment, so this link likely won’t navigate correctly.

Suggested change
All constraints from the [single-file deployment](#single-file-deployment) section still apply: no default exports in DI-related code, config as object mode, datasource `entities` path scanning limitations, etc. Bytecode is harder to debug; keep an uncompiled build path for troubleshooting.
All constraints from the [single-file deployment](#single-file-build-deployment) section still apply: no default exports in DI-related code, config as object mode, datasource `entities` path scanning limitations, etc. Bytecode is harder to debug; keep an uncompiled build path for troubleshooting.

Copilot uses AI. Check for mistakes.



## Binary deployment

Package Node.js into a single executable file, which can be directly copied and executed during deployment. This method includes the node runtime and business code, which is conducive to the protection of intellectual property rights.
Expand Down
Loading