lakutata 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +511 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +63 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +508 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/WebRTC.md +1364 -0
  50. package/doc/en/Workspaces.md +189 -0
  51. package/doc/en/examples/BackgroundJobs.md +677 -0
  52. package/doc/en/examples/CommandLineTool.md +664 -0
  53. package/doc/en/examples/DesktopApp.md +809 -0
  54. package/doc/en/examples/DeviceService.md +607 -0
  55. package/doc/en/examples/MCPServer.md +653 -0
  56. package/doc/en/examples/Meeting.md +573 -0
  57. package/doc/en/examples/README.md +17 -0
  58. package/doc/en/examples/RestAPI.md +490 -0
  59. package/doc/en/examples/ServiceCalls.md +829 -0
  60. package/doc/en/examples/WebApp.md +568 -0
  61. package/doc/zh/Application.md +75 -0
  62. package/doc/zh/CLI.md +250 -0
  63. package/doc/zh/Cache.md +328 -0
  64. package/doc/zh/Command.md +78 -0
  65. package/doc/zh/ContainerdIntegration.md +9 -9
  66. package/doc/zh/Controllers.md +228 -0
  67. package/doc/zh/DBus.md +257 -0
  68. package/doc/zh/DTOValidation.md +49 -19
  69. package/doc/zh/Database.md +175 -0
  70. package/doc/zh/DatabaseDrivers.md +261 -0
  71. package/doc/zh/Decorator.md +25 -18
  72. package/doc/zh/DockerIntegration.md +6 -6
  73. package/doc/zh/Entities.md +224 -0
  74. package/doc/zh/Entrypoint.md +193 -0
  75. package/doc/zh/Exceptions.md +158 -0
  76. package/doc/zh/GRPC.md +226 -0
  77. package/doc/zh/GettingStarted.md +148 -0
  78. package/doc/zh/HTTP.md +446 -0
  79. package/doc/zh/HelperFunctions.md +2 -2
  80. package/doc/zh/I18n.md +26 -26
  81. package/doc/zh/Injection.md +129 -0
  82. package/doc/zh/Installers.md +232 -0
  83. package/doc/zh/Logger.md +188 -0
  84. package/doc/zh/MCP.md +265 -0
  85. package/doc/zh/Middlewares.md +198 -0
  86. package/doc/zh/Migrations.md +178 -0
  87. package/doc/zh/MongoEmbedded.md +118 -0
  88. package/doc/zh/Monitor.md +145 -0
  89. package/doc/zh/Mutex.md +10 -10
  90. package/doc/zh/NATS.md +207 -0
  91. package/doc/zh/NativeAddons.md +180 -0
  92. package/doc/zh/Objects.md +119 -0
  93. package/doc/zh/Overview.md +47 -71
  94. package/doc/zh/Packaging.md +154 -0
  95. package/doc/zh/ProjectStructure.md +102 -0
  96. package/doc/zh/Queue.md +214 -0
  97. package/doc/zh/Schedules.md +193 -0
  98. package/doc/zh/Security.md +6 -6
  99. package/doc/zh/ServiceCalls.md +462 -0
  100. package/doc/zh/Time.md +2 -2
  101. package/doc/zh/UpgradingFrom1.md +347 -0
  102. package/doc/zh/UpgradingFrom2.md +230 -0
  103. package/doc/zh/WebRTC.md +1049 -0
  104. package/doc/zh/Workspaces.md +150 -0
  105. package/doc/zh/examples/BackgroundJobs.md +618 -0
  106. package/doc/zh/examples/CommandLineTool.md +618 -0
  107. package/doc/zh/examples/DesktopApp.md +748 -0
  108. package/doc/zh/examples/DeviceService.md +546 -0
  109. package/doc/zh/examples/MCPServer.md +604 -0
  110. package/doc/zh/examples/Meeting.md +524 -0
  111. package/doc/zh/examples/README.md +15 -0
  112. package/doc/zh/examples/RestAPI.md +458 -0
  113. package/doc/zh/examples/ServiceCalls.md +765 -0
  114. package/doc/zh/examples/WebApp.md +520 -0
  115. package/package.json +46 -17
  116. package/doc/en/ActionPattern.md +0 -408
  117. package/doc/en/Core.md +0 -312
  118. package/doc/en/ORMIntegration.md +0 -341
  119. package/doc/zh/ActionPattern.md +0 -334
  120. package/doc/zh/Core.md +0 -256
  121. package/doc/zh/ORMIntegration.md +0 -319
@@ -0,0 +1,180 @@
1
+ # Lakutata 原生插件
2
+
3
+ 原生插件是用 C++ 或 Rust 编写、由 Node.js 以 `.node` 文件加载的库:用于驱动和设备、编解码、系统 API,或者必须跑得快的代码。`lakutata add --native` 在工作区中创建一个原生插件包,可以直接构建和测试;`lakutata native` 给它链接库;`lakutata package` 把它和应用一起发布。这些插件使用 Node-API,所以同一份构建能在所有受支持的 Node.js 版本和 Electron 中运行。
4
+
5
+ ## 1. 创建原生插件
6
+
7
+ 在工作区中(见 [Workspaces](Workspaces.md)):
8
+
9
+ ```bash
10
+ # 三选一:
11
+ lakutata add @acme/capture --native --to app # C++,由 node-gyp 构建
12
+ lakutata add @acme/capture --native cmake --to app # C++,由 cmake-js 构建
13
+ lakutata add @acme/capture --native rust --to app # Rust,由 napi-rs(cargo)构建
14
+ # 然后:
15
+ npm install # 链接这个包并构建插件
16
+ npm run rebuild -w @acme/capture
17
+ npm test -w @acme/capture
18
+ ```
19
+
20
+ | 构建系统 | 它的文件 | 构建需要 |
21
+ |---|---|---|
22
+ | `gyp`(默认) | `binding.gyp`、`cpp/binding.cc` | Python 和 C++ 工具链:macOS 上是 Xcode 命令行工具,Linux 上是 build-essential,Windows 上是 Visual Studio Build Tools |
23
+ | `cmake` | `CMakeLists.txt`、`cpp/binding.cc` | CMake 和 C++ 工具链 |
24
+ | `rust` | `Cargo.toml`、`build.rs`、`rust/lib.rs` | Rust(rustup) |
25
+
26
+ 每个原生插件包还有:加载插件并给它提供类型化 API 的 `src/index.ts`,一个测试(`src/tests/index.spec.ts`),把 `src` 编译到 `dist` 的 `tsconfig.json`,以及一个 `README.md`。C++ 插件:
27
+
28
+ ```cpp
29
+ #include <napi.h>
30
+
31
+ static Napi::Value Greet(const Napi::CallbackInfo &info) {
32
+ Napi::Env env = info.Env();
33
+ if (info.Length() < 1 || !info[0].IsString()) {
34
+ throw Napi::TypeError::New(env, "The name must be a string");
35
+ }
36
+ return Napi::String::New(env, "Hello, " + info[0].As<Napi::String>().Utf8Value() + " (native)");
37
+ }
38
+
39
+ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
40
+ exports.Set("greet", Napi::Function::New(env, Greet));
41
+ return exports;
42
+ }
43
+
44
+ NODE_API_MODULE(capture, Init)
45
+ ```
46
+
47
+ Rust 插件:
48
+
49
+ ```rust
50
+ use napi_derive::napi;
51
+
52
+ /// Greet someone: greet(name)
53
+ #[napi]
54
+ pub fn greet(name: String) -> String {
55
+ format!("Hello, {} (native)", name)
56
+ }
57
+ ```
58
+
59
+ 还有 `src/index.ts`,三种构建系统都一样:
60
+
61
+ ```typescript
62
+ import {createRequire} from 'node:module'
63
+ import path from 'node:path'
64
+ import {fileURLToPath} from 'node:url'
65
+
66
+ type Binding = {
67
+ greet(name: string): string
68
+ }
69
+
70
+ /** The addon built into build/Release, loaded from the directory of the package */
71
+ const binding: Binding = createRequire(import.meta.url)('node-gyp-build')(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'))
72
+
73
+ export function greet(name: string): string {
74
+ return binding.greet(name)
75
+ }
76
+ ```
77
+
78
+ | 脚本 | 作用 |
79
+ |---|---|
80
+ | `install` | 安装这个包时(`npm install`)构建插件 |
81
+ | `build:native` | 把插件构建到 `build/Release/<name>.node` |
82
+ | `build` | 把 TypeScript 编译到 `dist` |
83
+ | `rebuild` | 先 `clean`,再 `build:native` 和 `build` |
84
+ | `test` | 运行测试(`node --test`) |
85
+ | `clean`、`clean:native` | 删除 `dist`,或删除插件的构建结果(`build`,Rust 还有 `target`) |
86
+
87
+ - **错误**:node-addon-api 的 C++ 异常会以同类错误抛到 JavaScript 中(`Napi::TypeError` 变成 `TypeError`)。在 Rust 中,类型不对的参数由 napi-rs 拒绝,抛出一个 `Error`,它的 `code` 说明原因(`StringExpected`)。
88
+ - **macOS**:C++ 插件在一个文件中同时为 Intel 和 Apple 芯片的 Mac 构建;Rust 插件只为本机的架构构建(用 `rustup target add` 添加其他架构)。
89
+ - **Node-API**:插件针对 Node-API 第 8 版构建,所有受支持的 Node.js 和 Electron 版本都提供这一版,所以不需要为它们分别重新构建。
90
+ - 和 `lakutata add` 创建的所有包一样,这个包是 `"private": true`:原生插件通常随应用一起发布,而不单独发布。
91
+
92
+ ## 2. 链接库
93
+
94
+ 在插件的目录中运行 `lakutata native <link|include|define> <value>`,会修改它的构建文件(`binding.gyp`、`CMakeLists.txt`,Rust 则是 `.cargo/config.toml`)。文件就地修改,保留其中的注释和格式,插件的源码不受影响。同样的修改再执行一次不会改变任何内容。
95
+
96
+ ```bash
97
+ lakutata native link z # 系统的库(-lz)
98
+ lakutata native link ws2_32 --platform win # 系统的库,只用于 Windows
99
+ lakutata native link zlib --pkg-config # 由 pkg-config 查找的库(不用于 Windows)
100
+ lakutata native link CoreFoundation --framework # macOS 的框架
101
+ lakutata native link deps/lib/libfoo.a # 包中的静态库,链接进插件
102
+ lakutata native link deps/lib/libbar.dylib # 包中的动态库,随插件一起发布
103
+ lakutata native include deps/include # 头文件目录(C++)
104
+ lakutata native define TRACE_LEVEL=3 # 预处理器的宏定义(C++)
105
+ lakutata native link libc --crate # 一个 crate(Rust:cargo add)
106
+ ```
107
+
108
+ ```
109
+ ✔ Linked deps/lib/libbar.dylib (gyp): binding.gyp
110
+ ```
111
+
112
+ - **文件还是系统的库**:含有 `/`,或者以 `.a`、`.lib`、`.so`、`.dylib`、`.dll` 结尾的值,是包中的文件;其他值是系统中某个库的名称。这些文件必须在包内,因为它们要随包一起发布;头文件目录要么在包内,要么是系统中的绝对路径。
113
+ - **修改适用的平台**:库文件决定它自己的平台(`.dylib` 是 macOS,`.so` 是 Linux,`.dll` 和 `.lib` 是 Windows,`.a` 是除 Windows 以外的所有平台),`--framework` 表示 macOS,`--pkg-config` 表示除 Windows 以外的所有平台,`--platform mac|linux|win` 指定一个平台。修改针对的不是本机的平台时,命令会提示(`! The change is for win: it is built and tried there`)。
114
+ - **动态库**在构建插件时被复制到插件旁边,运行时插件通过它的 rpath(macOS 上是 `@loader_path`,Linux 上是 `$ORIGIN`)在那里找到它们。在 macOS 上,库的安装名称(install name)必须是 `@rpath/libbar.dylib`(`install_name_tool -id @rpath/libbar.dylib deps/lib/libbar.dylib`),不是时命令会提醒你。在 Windows 上,DLL 通过它的导入库链接,导入库必须在它旁边(`deps/lib/bar.dll` 和 `deps/lib/bar.lib`)。
115
+ - **Rust 插件**通过 `build.rs` 链接原生库,`build.rs` 从 `.cargo/config.toml` 读取这些库(`lakutata native link` 写在那里)。`include`、`define` 和 `--pkg-config` 会被拒绝,因为 Rust 用 crate 来做这些事(`--crate` 用 `cargo add` 添加一个 crate,它会在 crate 的仓库中查找)。
116
+ - **多个目标**:构建文件声明了多个目标时,用 `--target <name>` 选择要修改的那一个。
117
+
118
+ 各构建系统中每种修改写入的内容:
119
+
120
+ | | gyp | CMake | Rust |
121
+ |---|---|---|---|
122
+ | 文件 | `binding.gyp`,或它所包含的、声明该目标的 `.gypi` | `CMakeLists.txt` | `.cargo/config.toml`(`[env]`),由 `build.rs` 读取 |
123
+ | 库 | `libraries` | `target_link_libraries` | `rustc-link-lib` |
124
+ | pkg-config | `<!@(pkg-config ...)` | `pkg_check_modules(... IMPORTED_TARGET ...)` | `-sys` crate |
125
+ | 框架 | `xcode_settings.OTHER_LDFLAGS` | `if(APPLE)` 中的 `-framework` | `rustc-link-lib=framework=` |
126
+ | 动态库 | `copies`、rpath | `INSTALL_RPATH`(`BUILD_WITH_INSTALL_RPATH`)、一个 `POST_BUILD` 复制命令 | `rustc-cdylib-link-arg`、`build:native` 中的复制命令 |
127
+ | 头文件、宏定义 | `include_dirs`、`defines` | `target_include_directories`、`target_compile_definitions` | 无(用 crate) |
128
+ | 平台 | `conditions`(`OS=="mac"`、`OS!="win"`) | `if(APPLE)`、`if(WIN32)`、`if(NOT WIN32)`、`if(CMAKE_SYSTEM_NAME STREQUAL "Linux")` | `LAKUTATA_NATIVE_MACOS`、`_LINUX`、`_WINDOWS`、`_UNIX` |
129
+
130
+ 例如,`link deps/lib/libbar.dylib` 把 rpath、库和复制它的命令写入 `binding.gyp` 的 macOS 条件中:
131
+
132
+ ```
133
+ ['OS=="mac"', {
134
+ 'xcode_settings': {
135
+ 'MACOSX_DEPLOYMENT_TARGET': '11.0',
136
+ 'OTHER_CFLAGS': ['-arch x86_64', '-arch arm64'],
137
+ 'OTHER_LDFLAGS': [
138
+ '-arch x86_64', '-arch arm64',
139
+ '-Wl,-rpath,@loader_path',
140
+ ],
141
+ },
142
+ 'libraries': ['<(module_root_dir)/deps/lib/libbar.dylib'],
143
+ 'copies': [
144
+ {
145
+ 'destination': '<(PRODUCT_DIR)',
146
+ 'files': ['<(module_root_dir)/deps/lib/libbar.dylib']
147
+ }
148
+ ]
149
+ }]
150
+ ```
151
+
152
+ (这里省略了生成文件中的注释。)
153
+
154
+ ## 3. 其他构建选项
155
+
156
+ 生成的 `binding.gyp` 和 `CMakeLists.txt` 中的注释给出了 `lakutata native` 不会写入的那些写法:
157
+
158
+ - C++ 标准(`binding.gyp`:macOS 上是 `CLANG_CXX_LANGUAGE_STANDARD`,Linux 上是 `cflags_cc`,Windows 上是 `/std:`;`CMakeLists.txt` 设置为 C++20,这是 Node.js 的头文件所要求的);
159
+ - 某个平台专用的选项,写在 `binding.gyp` 的 `conditions` 中;
160
+ - 在 `CMakeLists.txt` 中,CMake 的包(`find_package(ZLIB REQUIRED)`),或者从自身源码构建的库(`add_subdirectory(deps/foo)`)。
161
+
162
+ 由自己的 Makefile 构建的库,用包中的一个脚本在插件之前构建:
163
+
164
+ ```json
165
+ {
166
+ "scripts": {
167
+ "build:deps": "make -C deps/foo",
168
+ "build:native": "npm run build:deps && node-gyp rebuild"
169
+ }
170
+ }
171
+ ```
172
+
173
+ ## 4. 发布原生插件
174
+
175
+ 应用依赖这个插件包,并像导入其他包一样导入它(`import {greet} from '@acme/capture'`)。
176
+
177
+ - **单文件包和可执行文件**:`lakutata package bundle` 把插件包排除在单文件包之外,连同它们的文件(JavaScript、插件本身、它的动态库,但不包括源码和构建时产生的文件)写入 `dist/<name>.native/`,这个目录随单文件包一起发布;可执行文件则把它们嵌入其中,或者在目录布局中放在自己旁边(见 [Packaging](Packaging.md))。插件是为打包所在机器的平台构建的,所以每个平台的应用要在那个平台上打包(用 CI 的矩阵,Linux 也可以用对应架构的容器)。
178
+ - **Electron**:插件按安装时的样子复制。`lakutata add` 创建的插件不需要重新构建就能在 Electron 中运行,所有基于 Node-API 的插件都是如此;基于 Node.js V8 API 的插件,打包前要安装它为 Electron 构建的版本(`electron-rebuild`)。
179
+ - **Bun**:Bun 能加载 Node-API 的插件;Bun 可执行文件会对基于 Node.js V8 API 的插件发出警告。
180
+ - **Alpine(musl)**:Node.js 的可执行文件在那里不能加载原生插件,请用 Bun 制作。
@@ -0,0 +1,119 @@
1
+ # Lakutata 对象与生命周期
2
+
3
+ 应用中的每个对象(组件、提供者、控制器)都由 IoC 容器创建,从不使用 `new`。本页介绍这些对象的类、生命周期类型与作用域、对象如何创建和销毁,以及如何不建应用直接使用它们。它们来自 `@lakutata/core` 包,由 `lakutata`、`lakutata/decorator/di`、`lakutata/decorator/dto` 和 `lakutata/decorator/asst` 重新导出;更多细节见 `@lakutata/core` 的 README 和各导出的类型声明。
4
+
5
+ ## 1. 类
6
+
7
+ | 类 | 作用 | 默认生命周期 |
8
+ |---|---|---|
9
+ | `BaseObject` | 根类:异步构造、`init()`/`destroy()`、`getObject()` | `@Transient` |
10
+ | `Provider` | 存在于某个作用域(如一次请求)中的功能单元(仓储、哈希器等) | `@Scoped` |
11
+ | `Component` | 整个应用共享的长期服务(日志、数据库、入口等),同时是事件发射器 | `@Singleton`(锁定) |
12
+ | `Application` | 应用本身:声明组件、提供者和对象,由 `Application.run()` 创建 | |
13
+ | `Controller`(`lakutata/com/entrypoint`) | 处理入口请求的提供者 | `@Scoped`(锁定) |
14
+
15
+ ## 2. 生命周期类型
16
+
17
+ | 装饰器(`lakutata/decorator/di`) | 实例数量 |
18
+ |---|---|
19
+ | `@Singleton()` | 每个注册该类的容器一个,由它的使用者和它的作用域共享 |
20
+ | `@Scoped()` | 每个作用域一个(`container.createScope()`;入口的每次请求是一个作用域,根容器本身也是一个作用域) |
21
+ | `@Transient()` | 每次解析都新建,从不缓存 |
22
+
23
+ 子类继承父类的生命周期类型。`Component` 锁定了自己的类型:子类声明其他类型会抛出 `LifetimeLockedException`。`Provider` 默认是 `@Scoped`,也可以声明为 `@Transient` 或 `@Singleton`(与 2.x 相同:一个实例由各作用域共享,例如 `@lakutata/nats` 的 `ServiceProxy`);长期持有资源(连接、订阅、服务器)的对象更适合用 `Component`。单例注入作用域对象时,拿到的是创建它的那个作用域的实例。`@Lifetime('SINGLETON' | 'SCOPED' | 'TRANSIENT', lock)` 是这三个装饰器的完整写法。
24
+
25
+ ## 3. 作用域
26
+
27
+ 作用域是一个子容器,持有自己的作用域对象实例,父容器的单例和注册项则是共享的。入口为每次请求创建一个作用域:该请求的控制器以及它注入的提供者都在其中创建,请求结束时一并销毁。组件可以通过受保护的 `createScope()` 创建自己的作用域。
28
+
29
+ ## 4. 创建与销毁
30
+
31
+ 容器以异步方式创建对象,`getObject()`(或 `@Inject`)返回的 Promise 在对象就绪后才完成。创建依次经过以下步骤:
32
+
33
+ 1. 解析它的 `@Inject` 属性;
34
+ 2. 为它的 `@Configurable` 属性赋予配置值,并由各自的校验规则校验(及转换);未配置的属性保留其初始值,没有初始值时取校验规则的默认值;
35
+ 3. 执行它的 `init()`(在此抛出的错误会导致创建失败);
36
+ 4. 执行它的 `startup()`。此时其他对象已经可以获取它,但要等 `startup()` 完成才拿到(失败时得到同样的错误);例外是在 `startup()` 内部直接或经由它创建的对象去获取的一方,会立即拿到它,所以在那里获取的对象可以反过来注入它(入口就在 `startup()` 里启动各个适配器,适配器的钩子会获取一个注入了入口的 Authenticator)。方法叫 `startup` 而不是 `start`,是为了避免框架去启动那些自带 `start()` 方法的类(比如 `@lakutata/docker` 的容器);
37
+ 5. 应用启动完成(且对象已启动)后,执行它的 `launched()`(不会被等待,其错误会被记录到日志)。
38
+
39
+ 销毁时(调用 `dispose()`,或创建它的对象、作用域、应用被销毁时),先等待 `startup()` 结束,再执行它的 `beforeDestroy()`,再销毁它创建的对象,最后执行它的 `destroy()`。每个对象都拥有自己的容器:通过 `this.getObject()` 创建的对象会随它一起销毁。
40
+
41
+ ```typescript
42
+ import {Application, Component, type ILogger} from 'lakutata'
43
+ import {Inject} from 'lakutata/decorator/di'
44
+
45
+ class Poller extends Component {
46
+ @Inject('log')
47
+ protected readonly log: ILogger
48
+
49
+ protected timer: NodeJS.Timeout
50
+
51
+ //依赖已注入,选项已设置
52
+ protected async init(): Promise<void> {
53
+ this.log.info('init')
54
+ }
55
+
56
+ //其他对象此时已经可以获取它,但要等这里结束
57
+ protected async startup(): Promise<void> {
58
+ this.timer = setInterval((): void => this.log.info('polling'), 10000)
59
+ this.log.info('startup')
60
+ }
61
+
62
+ //应用已启动(不会被等待)
63
+ protected async launched(): Promise<void> {
64
+ this.log.info('launched')
65
+ }
66
+
67
+ //开始销毁:它创建的对象还在
68
+ protected async beforeDestroy(): Promise<void> {
69
+ this.log.info('beforeDestroy')
70
+ }
71
+
72
+ //销毁的最后一步:它创建的对象已被销毁
73
+ protected async destroy(): Promise<void> {
74
+ clearInterval(this.timer)
75
+ this.log.info('destroy')
76
+ }
77
+ }
78
+
79
+ Application
80
+ .run(() => ({
81
+ id: 'poller.app',
82
+ name: 'Poller',
83
+ components: {
84
+ poller: {class: Poller}
85
+ },
86
+ bootstrap: ['poller']
87
+ }))
88
+ .onLaunched((app: Application, log: ILogger): void => log.info('onLaunched'))
89
+ .onDone((app: Application, log: ILogger): void => log.info('onDone'))
90
+ ```
91
+
92
+ 它依次记录 `init`、`startup`、`launched` 和 `onLaunched`。按下 Ctrl+C 后,依次记录 `onDone`、`beforeDestroy` 和 `destroy`,然后进程退出。
93
+
94
+ ## 5. 不使用应用的容器
95
+
96
+ 测试、脚本和库可以不建应用,直接在一个容器里使用这些对象:
97
+
98
+ ```typescript
99
+ import {Component, Container, DTO} from 'lakutata'
100
+ import {Configurable} from 'lakutata/decorator/di'
101
+
102
+ class Mailer extends Component {
103
+ @Configurable(DTO.String().required())
104
+ protected readonly host: string
105
+
106
+ public async send(to: string, text: string): Promise<string> {
107
+ return `${this.host} -> ${to}: ${text}`
108
+ }
109
+ }
110
+
111
+ const container: Container = new Container()
112
+ //按选项创建,并等待它的 init() 完成
113
+ const mailer: Mailer = await container.set(Mailer, {host: 'localhost'})
114
+ await mailer.send('someone@example.com', 'Hello')
115
+ //销毁它创建的所有对象
116
+ await container.destroy()
117
+ ```
118
+
119
+ `container.register()`/`registerNamed()` 只注册类而不创建对象,`get()` 解析对象,`build()` 无论生命周期类型都新建一个对象,`createScope()` 创建作用域。
@@ -1,74 +1,18 @@
1
1
  # Lakutata 概述
2
2
 
3
- **Lakutata** 是一个面向 Node.js 和 TypeScript 的、基于 IoC 的应用框架:Web 服务、命令行工具、桌面(Electron)应用和嵌入式设备使用同一套编程模型。应用是一棵由容器创建的对象树(组件、提供者、模块、控制器),对象的选项和输入由 DTO 校验,控制器由各种传输方式对外提供:HTTP(Express、Fastify)、CLI(Commander)、服务调用(HTTP、Socket.IO、JSON-RPC 2.0、gRPC、NATS、Electron IPC)以及定时任务。同一个控制器方法可以同时通过所有这些方式调用。
3
+ **Lakutata** 是一个基于 IoC 的 Node.js / TypeScript 应用框架,让 Web 服务、命令行工具、桌面(Electron)应用和嵌入式设备共用同一套编程模型。应用是一棵由容器创建的对象树(组件、提供者、控制器):DTO 校验对象的选项和输入,各种传输方式把控制器暴露出去——HTTP(Express、Fastify)、CLI(Commander)、服务调用(HTTP、Socket.IO、JSON-RPC 2.0、gRPC、NATS、Electron IPC)、定时任务和消息队列。同一个控制器方法可以同时通过所有这些方式调用。
4
4
 
5
- `lakutata` 包通过各个子路径重新导出 `@lakutata/*` 系列包(见下表),并为每个应用预置两个组件:`log`(基于 pino 的 `Logger`)和 `entrypoint`(对外提供控制器的 `Entrypoint`)。
5
+ `lakutata` 包通过各个子路径重新导出 `@lakutata/*` 系列包(见下表),并为每个应用预置两个组件:`log`(`Logger`,输出与 pino 兼容的 JSON 日志行)和 `entrypoint`(对外提供控制器的 `Entrypoint`)。
6
6
 
7
- ## 环境要求
7
+ ## 从哪里开始
8
8
 
9
- - Node.js 22 或更高版本。
10
- - ESM(`"type": "module"`);包里同时提供了 CommonJS 构建。
11
- - TypeScript 需开启 `experimentalDecorators` 和 `emitDecoratorMetadata`。框架所需的编译选项以 `lakutata/tsconfig.json` 发布。
12
-
13
- ## 安装
14
-
15
- ```bash
16
- npx @lakutata/cli create --name my-app # 从模板创建新项目(Web、服务、CLI、Electron 等)
17
- npm install lakutata # 或在已有项目中安装框架
18
- ```
19
-
20
- ```json
21
- {
22
- "extends": "lakutata/tsconfig.json",
23
- "compilerOptions": {"rootDir": "src", "outDir": "dist"}
24
- }
25
- ```
26
-
27
- 传输方式和各项集成依赖可选的 peer 依赖,用到时再安装:`express` 或 `fastify`、`commander`、`socket.io`、`ws`、数据库驱动(`pg`、`mysql2`、`sqlite3` 等)、缓存存储(`redis` 等)。
28
-
29
- ## 第一个应用
30
-
31
- ```typescript
32
- import {Application, DTO, type ILogger} from 'lakutata'
33
- import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
34
- import {GET, ServiceAction} from 'lakutata/decorator/ctrl'
35
- import {Expect} from 'lakutata/decorator/dto'
36
- import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
37
-
38
- class GreetDTO extends DTO {
39
- @Expect(DTO.String().optional().default('world'))
40
- public name: string
41
- }
42
-
43
- class GreetController extends Controller {
44
- //GET /greet?name=you,以及服务动作 {cmd: 'greet', name: 'you'}
45
- @GET('/greet', GreetDTO)
46
- @ServiceAction({cmd: 'greet'}, GreetDTO)
47
- public async greet(input: GreetDTO): Promise<{ message: string }> {
48
- return {message: `Hello, ${input.name}!`}
49
- }
50
- }
51
-
52
- Application
53
- .run(() => ({
54
- id: 'com.example.greeter',
55
- name: 'Greeter',
56
- components: {
57
- entrypoint: buildEntrypoints({
58
- controllers: [GreetController],
59
- http: expressHTTPEntrypoint({port: 8080})
60
- })
61
- },
62
- bootstrap: ['entrypoint']
63
- }))
64
- .onLaunched((app: Application, log: ILogger): void => log.info('%s started', app.appName))
65
- ```
9
+ [GettingStarted](GettingStarted.md) 用模板创建第一个项目,并演示第一个应用;[ProjectStructure](ProjectStructure.md) 介绍项目中的文件;下面的各篇指南分别介绍框架的各个部分。[示例](examples/README.md) 从模板出发构建完整的应用:REST API、命令行工具、Web 应用和桌面应用、服务之间的调用、后台任务、MCP 服务器,以及设备上的服务。
66
10
 
67
11
  ## 子路径
68
12
 
69
13
  | 子路径 | 内容 |
70
14
  |---|---|
71
- | `lakutata` | `Application`、`Module`、`Component`、`Provider`、`BaseObject`、`Container`、`DTO`、`Exception`、`Time`、`isPackaged`、`resourcesPath`、校验消息 |
15
+ | `lakutata` | `Application`、`Component`、`Provider`、`BaseObject`、`Container`、`DTO`、`Exception`、`Time`、`isPackaged`、`resourcesPath`、校验消息 |
72
16
  | `lakutata/decorator/di` | `@Inject`、`@Configurable`、`@Autoload`、`@Singleton`、`@Scoped`、`@Transient`、`@Lifetime` |
73
17
  | `lakutata/decorator/dto` | `@Expect`、`@Accept`、`@Return`、`@IndexSignature` |
74
18
  | `lakutata/decorator/asst` | `@Before`、`@After` |
@@ -78,11 +22,12 @@ Application
78
22
  | `lakutata/adapter/express`、`lakutata/adapter/fastify` | HTTP 传输 |
79
23
  | `lakutata/adapter/commander` | CLI 传输 |
80
24
  | `lakutata/adapter/socket.io`、`lakutata/adapter/jsonrpc`、`lakutata/adapter/electron` | 服务传输 |
81
- | `lakutata/com/logger` | `Logger` 组件(pino),预置为 `log` |
25
+ | `lakutata/com/logger` | `Logger` 组件,预置为 `log`(输出与 pino 兼容的 JSON 行) |
82
26
  | `lakutata/orm`、`lakutata/decorator/orm` | TypeORM 及其装饰器 |
83
27
  | `lakutata/com/database`、`lakutata/provider/database` | `Database` 组件和提供者 |
84
28
  | `lakutata/com/cacher` | `Cacher` 组件 |
85
29
  | `lakutata/com/mutex` | `Mutex` 组件、`@Synchronized` |
30
+ | `lakutata/com/queue` | `Queue` 组件、`@QueueAction`、`queueEntrypoint` |
86
31
  | `lakutata/com/i18n` | `I18n` 组件、`Translator`、语言中间件 |
87
32
  | `lakutata/com/docker` | `Docker` 组件 |
88
33
  | `lakutata/com/containerd` | `Containerd` 组件 |
@@ -90,23 +35,54 @@ Application
90
35
  | `lakutata/security`、`lakutata/provider/passwordHash` | 哈希、加解密、签名、`PasswordHash` 提供者 |
91
36
  | `lakutata/utils` | 辅助函数 |
92
37
 
93
- 不在本包中的:`@lakutata/cli`(`lakutata` 命令:项目、打包、可执行文件、迁移)、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)和 `@lakutata/mcp`(Model Context Protocol)。
38
+ 不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)和 `@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)。
94
39
 
95
40
  ## 指南
96
41
 
97
42
  | 指南 | 主题 |
98
43
  |---|---|
99
- | [Core](Core.md) | 应用、容器、生命周期类型、依赖注入、模块、应用的生命周期 |
44
+ | [GettingStarted](GettingStarted.md) | 环境要求、创建项目、在已有项目中加入 Lakutata、第一个应用 |
45
+ | [ProjectStructure](ProjectStructure.md) | 用模板创建的项目中的文件、脚本和可选功能,以及自己的模板 |
46
+ | [UpgradingFrom2](UpgradingFrom2.md) | 把项目从 Lakutata 2.x(`lakutata` 包的 2.x 版本)升级 |
47
+ | [UpgradingFrom1](UpgradingFrom1.md) | 把项目从 Lakutata 1.x(1.x 版本的 `@lakutata/core` 以及 `@lakutata-module/*`、`@lakutata-component/*` 包)移植过来 |
48
+ | [Application](Application.md) | 应用:选项、链式 API、生命周期、运行模式、打包后的应用 |
49
+ | [Objects](Objects.md) | 组件、提供者及其容器:生命周期类型、作用域、创建与销毁、不使用应用的容器 |
50
+ | [Injection](Injection.md) | 依赖注入、对象的选项、在多个应用之间共享功能 |
100
51
  | [DTOValidation](DTOValidation.md) | DTO、校验规则,以及选项、输入和返回值的校验 |
52
+ | [Exceptions](Exceptions.md) | 声明异常、异常的消息、客户端收到的内容、框架的异常 |
53
+ | [Logger](Logger.md) | `log` 组件:写日志、可读的行或 JSON、输出目标、换用其他日志记录器 |
101
54
  | [Decorator](Decorator.md) | 各子路径的装饰器 |
102
- | [ActionPattern](ActionPattern.md) | 控制器、动作及其传输方式(HTTP、CLI、服务、定时任务) |
103
- | [ORMIntegration](ORMIntegration.md) | `Database` 组件与 TypeORM |
104
- | [I18n](I18n.md) | 多语言消息 |
55
+ | [Time](Time.md) | `Time` 类 |
56
+ | [HelperFunctions](HelperFunctions.md) | `lakutata/utils` 的辅助函数 |
57
+ | [Controllers](Controllers.md) | 控制器与动作:地址、输入与结果、前缀、继承 |
58
+ | [Entrypoint](Entrypoint.md) | 入口组件:选项与适配器、启动与停止、动作列表 |
59
+ | [Middlewares](Middlewares.md) | 动作的中间件与访问控制 |
60
+ | [HTTP](HTTP.md) | HTTP 适配器:路由、输入、响应、错误,以及 Express 和 Fastify 的功能 |
61
+ | [CLI](CLI.md) | 命令行工具:命令、选项、提示输入、输出与退出码 |
62
+ | [ServiceCalls](ServiceCalls.md) | 通过 HTTP、Socket.IO、JSON-RPC 2.0 和 Electron 调用服务动作:模式、响应、客户端及其事件 |
63
+ | [GRPC](GRPC.md) | 通过 gRPC 提供服务动作(`@lakutata/grpc`),以及从其他应用和其他语言调用它们 |
64
+ | [NATS](NATS.md) | NATS 组件、通过 NATS 提供服务动作(`@lakutata/nats`)、服务代理及其事件 |
65
+ | [Schedules](Schedules.md) | 定时任务:间隔、cron 表达式、只在一个实例中执行的任务 |
66
+ | [Queue](Queue.md) | 消息队列:生产者、`@QueueAction` 消费者、重试、死信、结果 |
67
+ | [Database](Database.md) | `Database` 组件与提供者、连接选项、多个数据库 |
68
+ | [DatabaseDrivers](DatabaseDrivers.md) | 逐个介绍数据库驱动:需要安装什么、选项、限制 |
69
+ | [MongoEmbedded](MongoEmbedded.md) | 应用内的 MongoDB,文档保存在文件中 |
70
+ | [Entities](Entities.md) | 实体、查询、事务与错误 |
71
+ | [Migrations](Migrations.md) | 编写、生成和执行迁移 |
72
+ | [Cache](Cache.md) | `Cacher` 组件:`wrap()`、各种存储及其分层、存储保存的值 |
105
73
  | [Mutex](Mutex.md) | 实例与进程之间的锁 |
74
+ | [I18n](I18n.md) | 多语言消息 |
106
75
  | [DockerIntegration](DockerIntegration.md) | `Docker` 组件 |
107
76
  | [ContainerdIntegration](ContainerdIntegration.md) | `Containerd` 组件 |
77
+ | [Monitor](Monitor.md) | CPU、内存、事件循环和 HTTP 请求的监控器 |
108
78
  | [Security](Security.md) | 哈希、加解密、签名、密码 |
109
- | [Time](Time.md) | `Time` 类 |
110
- | [HelperFunctions](HelperFunctions.md) | `lakutata/utils` 的辅助函数 |
111
-
112
- 每个 `@lakutata/*` 包都有自己的 README,各子路径的类型声明也为每个声明附带了说明和示例。
79
+ | [DBus](DBus.md) | D-Bus 服务和客户端(`@lakutata/dbus`):方法、对象、属性、信号 |
80
+ | [MCP](MCP.md) | 模型上下文协议(`@lakutata/mcp`)的工具、资源和提示词,基于 stdio 和 HTTP |
81
+ | [WebRTC](WebRTC.md) | 点对点连接和媒体服务器(`@lakutata/webrtc`):信令、TURN 服务器、浏览器和应用、通过数据通道调用应用的动作、WHIP 和 WHEP |
82
+ | [Command](Command.md) | `lakutata` 命令:创建项目、迁移、翻译、环境变量 |
83
+ | [Workspaces](Workspaces.md) | 由多个包组成的项目:运行各个包的脚本、统一版本号、添加包 |
84
+ | [NativeAddons](NativeAddons.md) | 用 C++ 或 Rust 编写的原生插件:创建、链接库、发布 |
85
+ | [Packaging](Packaging.md) | 单文件包和可执行文件,以及打包后的应用在运行时读取什么 |
86
+ | [Installers](Installers.md) | 各平台的安装包、服务、安装脚本、签名 |
87
+
88
+ 每个 `@lakutata/*` 包都有自己的 README,各子路径的类型声明中,每一项也都附有说明和示例。
@@ -0,0 +1,154 @@
1
+ # Lakutata 打包
2
+
3
+ 编译好的应用可以不带 `node_modules` 发布,有三种形式:
4
+
5
+ - **单文件包**(bundle):一个由 Node.js 运行的 JavaScript 文件;
6
+ - **可执行文件**:一个程序,不需要安装 Node.js 就能运行;
7
+ - **安装包**:系统的软件包(deb、rpm、pkg、msi 等),它安装可执行文件,把程序加入 PATH,还可以把它注册为服务(见 [Installers](Installers.md))。
8
+
9
+ `lakutata package bundle`、`package exe` 和 `package installer` 借助 `@lakutata/builder` 生成它们,项目需要安装它(`npm install --save-dev @lakutata/builder`;需要打包的模板已经带上了)。
10
+
11
+ ## 1. 打包的内容
12
+
13
+ - **编译后的应用**:打包的是 `tsc` 的输出(`tsc` 会生成装饰器的元数据),而不是 TypeScript 源码,所以要先构建。入口依次是 `--entry`、`package.json` 的 `main`、`build/App.js`。
14
+ - **应用的依赖**:由 esbuild 打进一个文件,原生插件和按路径读取的文件除外(第 3 节和第 4 节)。框架的包以及基于框架的包总是打进包里,这样框架、它的类和它的容器都只有一份。
15
+ - **CommonJS**:单文件包是 CommonJS 文件,所以应用不能使用顶层 `await`,请在异步函数里 await。
16
+ - **未安装的可选依赖**(应用没有用到的存储驱动)会被排除:运行时加载它会抛出异常,和不打包时一样。
17
+ - **资源**:应用运行时读取的其他文件(Web 界面构建好的页面、模板、数据)在 `package.json` 中声明(第 6 节)。
18
+
19
+ ## 2. 单文件包
20
+
21
+ ```bash
22
+ npm run build
23
+ npx lakutata package bundle # dist/<name>.cjs
24
+ node dist/greet.cjs
25
+ ```
26
+
27
+ ```
28
+ ✔ dist/greet.cjs (1.1 MB): node dist/greet.cjs
29
+ ✔ dist/greet.native: the native addons (better-sqlite3) and the scripts run on their own (build/child.js), distributed with the bundle
30
+ ✔ dist/greet.resources: the resources (texts), distributed with the bundle
31
+ ```
32
+
33
+ 文件名取 `package.json` 中第一个 `bin` 的名称,没有时取去掉作用域的包名。需要时,单文件包旁边还会生成两个目录,必须和它一起发布:`dist/<name>.native/`(原生插件、单独运行的脚本、按路径读取的文件)和 `dist/<name>.resources/`(资源)。和这两个目录一起移动后,单文件包可以在任何目录中运行。
34
+
35
+ | 选项 | 含义 |
36
+ |---|---|
37
+ | `--entry <file>` | 编译后的入口 |
38
+ | `--outfile <file>` | 单文件包的路径,默认 `dist/<name>.cjs` |
39
+ | `--no-minify` | 不压缩(无论是否压缩,类名都会保留) |
40
+ | `--external <modules...>` | 不打进包里的模块,运行时从单文件包旁边的 `node_modules` 加载 |
41
+
42
+ ## 3. 原生插件
43
+
44
+ 含有 `.node` 文件的包(better-sqlite3,以及工作区中由 [`lakutata add --native`](NativeAddons.md) 创建的插件),以及通过计算出的路径加载它们的包(sharp),不能打进单文件包。它们被排除在外,按 `node_modules` 的结构放在 `dist/<name>.native/` 中,连同它们在运行时加载的内容:
45
+
46
+ - 它们的全部文件,以及插件所链接的共享库所在的包(例如 sharp 的 libvips)的全部文件,但不包括 C 和 C++ 源码和目标文件(better-sqlite3 带有 SQLite 的源码),也不包括构建它们时产生的文件(`build/` 中 node-gyp 和 CMake 的文件、cargo 的 `target` 目录、静态库和导入库);
47
+ - 它们加载的 JavaScript;
48
+ - 应用自己的插件(`createRequire(import.meta.url)('./x.node')`)。
49
+
50
+ 这些包的类型声明、source map、TypeScript 源码和文档会被排除。插件是为本机的平台和架构构建的,需要它构建时所针对的系统库,所以每个平台的应用要在那个平台上打包。
51
+
52
+ ## 4. 按路径读取的文件
53
+
54
+ 单文件包并不在原来各个模块所在的位置:单独启动的脚本(worker、`fork()` 的子进程)或者应用读取的文件(`defaults.json`、一个存放 schema 的目录)在单文件包旁边找不到。工具会在应用的模块和它所用包的模块中,找出由模块位置构造的路径,并把它们改写为指向 `dist/<name>.native/` 中的文件:
55
+
56
+ | 路径 | |
57
+ |---|---|
58
+ | `new URL('./worker.js', import.meta.url)` | 也包括 `pathToFileURL(__filename)` |
59
+ | `path.join(import.meta.dirname, 'child.js')`、`path.resolve(__dirname, '../schema')` | 也包括用 `path.dirname(fileURLToPath(import.meta.url))` 的写法 |
60
+ | `__dirname + '/worker.js'`、`` `${__dirname}/worker.js` `` | |
61
+ | `require.resolve('./worker')` | 扩展名按 Node.js 的方式查找 |
62
+ | `` path.join(__dirname, 'locales', `${lang}.json`) `` | 部分已知:发布所有与这个模式匹配的文件 |
63
+
64
+ | 路径指向 | |
65
+ |---|---|
66
+ | 脚本(`.js`、`.cjs`) | 单独打包,连同它加载的内容 |
67
+ | 其他文件、存放数据的目录 | 复制 |
68
+ | 包含该模块的目录(`path.join(__dirname, '..')`:应用、它所在的包) | 不发布,并发出警告:请把其中的文件声明为资源 |
69
+ | 包含脚本或包的目录(`package.json`、`node_modules`) | 不发布,并发出警告:其中的脚本运行时需要它们的包 |
70
+ | 还不存在的路径(应用运行时创建的目录) | 保持原样 |
71
+
72
+ ```typescript
73
+ import {fork} from 'node:child_process'
74
+ import {readFileSync} from 'node:fs'
75
+ import path from 'node:path'
76
+
77
+ //两者都会发布:脚本单独打包,文件被复制
78
+ fork(path.join(import.meta.dirname, 'child.js'), ['one', 'two'])
79
+ const defaults: Record<string, unknown> = JSON.parse(readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'))
80
+ ```
81
+
82
+ 对于应用模块中无法跟踪的写法,工具会给出警告并指出文件和行号:路径只在运行时才知道的 worker(`new Worker(file)`),或者相对于当前目录的 worker(`new Worker('./worker.js')`),以及应用同时又导入了的脚本(它会被打包两次,模块有两份副本)。请从模块所在的目录构造这些路径,或者把这些文件作为资源发布。
83
+
84
+ ## 5. 可执行文件
85
+
86
+ ```bash
87
+ npx lakutata package exe # dist/<name>(Windows 上是 dist/<name>.exe),使用 Node.js
88
+ npx lakutata package exe --runtime bun # 使用 Bun
89
+ ```
90
+
91
+ ```
92
+ › Node.js v24.11.1 embedded
93
+ › Resources embedded, extracted at the first run: texts
94
+ › Scripts run on their own embedded, extracted at the first run: build/child.js
95
+ ✔ dist/greet (darwin-arm64, 112.9 MB)
96
+ ```
97
+
98
+ 可执行文件只能为生成它的平台制作。其他平台的可执行文件要在那些平台上制作,因为它们的原生插件要在那里构建:可以用 CI 的矩阵(Linux、macOS、Windows 的机器),Linux 也可以用对应架构的容器(`docker run --platform linux/arm64`);`cli` 模板的 README 给出了这两种做法。
99
+
100
+ | 选项 | 含义 |
101
+ |---|---|
102
+ | `--runtime node`(默认) | 把单文件包嵌入运行工具的那个 Node.js 的副本中(Node.js 的单可执行文件应用):在 macOS arm64 上约 110 MB |
103
+ | `--runtime bun` | 由 Bun 编译单文件包(优先用项目中的 `bun` 包,否则用系统的 `bun`):在 macOS arm64 上约 70 MB,启动更快;运行时也就是 Bun |
104
+ | `--layout file`(默认) | 单个文件:原生插件、按路径读取的文件和资源都嵌入其中,启动时解压 |
105
+ | `--layout directory` | `dist/<name>/` 中包含可执行文件、`native/` 和 `resources/`,直接从那里加载,不解压(安装包用的就是这种布局) |
106
+ | `--outdir <directory>` | 可执行文件的输出目录,默认 `dist` |
107
+ | `--entry`、`--no-minify`、`--external` | 与单文件包相同 |
108
+
109
+ - **解压**:单文件布局的可执行文件在第一次运行时,把嵌入的内容解压到缓存目录中一个以内容命名的子目录,所以每个版本只解压一次:macOS 上是 `~/Library/Caches/<name>`,Windows 上是 `%LOCALAPPDATA%\<name>\Cache`,Linux 上是 `$XDG_CACHE_HOME/<name>` 或 `~/.cache/<name>`,也可以用 `LAKUTATA_NATIVE_DIR` 指定目录。缓存目录不可写时,改用临时目录中的 `<name>`(指定了 `LAKUTATA_NATIVE_DIR` 时不会这样)。
110
+ - **原生插件**:Bun 能加载基于 Node-API 的插件,但不一定能加载基于 Node.js V8 API 的插件:Bun 可执行文件会对这类插件发出警告,并在加载这样的包时失败(例如 better-sqlite3)。在 Alpine(musl)上,Node.js 的可执行文件不能加载原生插件,请用 Bun 制作。
111
+ - **外部模块**:`--external` 指定的模块从可执行文件旁边的 `node_modules` 加载。
112
+ - **子进程**:可执行文件被启动来运行它的某个脚本时(`fork(path.join(import.meta.dirname, 'child.js'))`),会运行这个脚本并传入它的参数,两种布局都是如此。用 Node.js 制作时,`require.main` 就是这个脚本,和 Node.js 一样;用 Bun 制作时则不是。
113
+ - **不读取 `.env`**:可执行文件不会加载所在目录的 `.env`,请使用 `LAKUTATA_ENV_FILE`(第 6 节)。
114
+ - **图标与签名**:`package.json` 中的 `lakutata.icon`(256 像素或更大的正方形 PNG)会成为 Windows 可执行文件的图标。macOS 的可执行文件使用临时签名(ad hoc)。用 Node.js 制作的 Windows 可执行文件保留着 `node.exe` 的签名,而这个签名已经不再匹配:发布前请用 signtool 重新签名。
115
+
116
+ ## 6. 运行时
117
+
118
+ 资源在 `lakutata.resources` 中声明为 `{from, to}` 对象的列表:`from` 是项目中的文件或目录(可以在包以外,例如 `../ui/dist`),`to` 是它在资源中的路径。
119
+
120
+ ```json
121
+ {
122
+ "lakutata": {
123
+ "resources": [{"from": "../ui/dist", "to": "ui"}]
124
+ }
125
+ }
126
+ ```
127
+
128
+ `isPackaged()` 和 `resourcesPath()`(来自 `lakutata`)告诉应用它是怎样运行的:
129
+
130
+ | 运行方式 | `isPackaged()` | `resourcesPath()` |
131
+ |---|---|---|
132
+ | 未打包(`node build/App.js`、`electron .`) | `false` | `undefined` |
133
+ | 单文件包 | `true` | 单文件包旁边的 `dist/<name>.resources` |
134
+ | 可执行文件,单文件布局 | `true` | 资源解压到的目录 |
135
+ | 可执行文件,目录布局,或已安装的应用 | `true` | 可执行文件旁边的 `resources` |
136
+ | 打包后的 Electron 应用 | `true` | Electron 资源目录中的 `main.resources` |
137
+
138
+ 即使应用没有声明资源,`resourcesPath()` 也会返回路径,所以这个目录可能不存在。应用自己决定从哪里读取文件:
139
+
140
+ ```typescript
141
+ import {createRequire} from 'node:module'
142
+ import path from 'node:path'
143
+ import {resourcesPath} from 'lakutata'
144
+
145
+ const resources: string | undefined = resourcesPath()
146
+ //工作区中 "ui" 包构建好的页面:打包后在资源中,否则在它的 dist 目录中
147
+ const ui: string = resources ? path.join(resources, 'ui') : path.join(path.dirname(createRequire(import.meta.url).resolve('ui/package.json')), 'dist')
148
+ ```
149
+
150
+ 所有打包形式都会在应用启动前加载 `LAKUTATA_ENV_FILE` 指定的环境文件:每行是 `NAME=value` 或 `NAME="value"`,`#` 开头的是注释。文件中的值会替换已经设置的同名环境变量。
151
+
152
+ ```bash
153
+ LAKUTATA_ENV_FILE=/etc/greet.env ./greet
154
+ ```