lakutata 3.0.0 → 3.1.0

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 (117) 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 +507 -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 +62 -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 +504 -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/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
@@ -0,0 +1,129 @@
1
+ # Lakutata 依赖注入
2
+
3
+ 对象不自己创建它所依赖的对象,而是声明这些依赖,由容器在创建它时注入。本页介绍 `@Inject` 和 `@Autoload`、对象的选项(`@Configurable`),以及如何把功能做成包、在多个应用之间共享。这些装饰器的全部规则见 [Decorator](Decorator.md) 第 1 节。
4
+
5
+ ## 1. 注入对象
6
+
7
+ `@Inject`(来自 `lakutata/decorator/di`)在对象创建时、`init()` 之前为属性赋值:
8
+
9
+ - `@Inject()`:注入以属性名注册的对象;
10
+ - `@Inject('name')` 或 `@Inject(symbol)`:注入以该名称注册的对象;
11
+ - `@Inject(SomeClass)`:注入为该类注册的对象(`@Inject(Application)` 注入应用本身);
12
+ - `@Inject('name', transform)`:注入 `transform(object)` 的返回值(可以是异步函数)。
13
+
14
+ 标记了 `@Autoload()` 的类在第一次被请求时自动注册,而不是报错:单例注册到根容器,作用域类注册到最近的作用域,瞬态类注册到发起请求的容器。依赖无法解析时,创建失败,抛出 `DependencyInjectionException`。
15
+
16
+ ```typescript
17
+ import {Application, Component, Provider} from 'lakutata'
18
+ import {Autoload, Inject, Scoped} from 'lakutata/decorator/di'
19
+
20
+ //未在应用中声明:第一次被注入时自动注册
21
+ @Autoload()
22
+ @Scoped()
23
+ class RequestClock extends Provider {
24
+ public readonly startedAt: number = Date.now()
25
+ }
26
+
27
+ class Reports extends Component {
28
+ @Inject(RequestClock)
29
+ protected readonly clock: RequestClock
30
+
31
+ @Inject(Application)
32
+ protected readonly app: Application
33
+
34
+ public elapsed(): number {
35
+ return Date.now() - this.clock.startedAt
36
+ }
37
+ }
38
+ ```
39
+
40
+ 本身不是容器中对象的代码(比如 `onLaunched` 处理函数)用 `app.getObject(name)` 获取对象;`app.getObject(SomeClass)` 只适用于单独注册的类(比如自动注册的类),不适用于按名称声明的组件。
41
+
42
+ ## 2. 选项
43
+
44
+ `@Configurable(schema, fn?)` 把属性声明为对象的选项:它的值来自对象的声明(`{class: Mailer, host: 'smtp.example.com'}`)或 `getObject()` 的第二个参数,经校验规则校验后,如果提供了 `fn` 再交给 `fn` 处理。应用在启动时会检查它声明的对象的选项:不合法的值会抛出 `InvalidObjectOptionsException` 并终止启动,不是可配置属性的选项会给出警告。
45
+
46
+ ```typescript
47
+ import {Application, Component, DTO, type ILogger} from 'lakutata'
48
+ import {Configurable, Inject} from 'lakutata/decorator/di'
49
+
50
+ class Mailer extends Component {
51
+ //它的选项,由应用选项中的声明给出
52
+ @Configurable(DTO.String().required())
53
+ protected readonly host: string
54
+
55
+ //未配置时为 25:初始值就是默认值,和配置值一样会被校验
56
+ @Configurable(DTO.Number().integer().min(1).max(65535))
57
+ protected readonly port: number = 25
58
+
59
+ //lakutata 包预置的日志组件 "log"
60
+ @Inject('log')
61
+ protected readonly log: ILogger
62
+
63
+ protected async init(): Promise<void> {
64
+ this.log.info('Mailer connected to %s:%d', this.host, this.port)
65
+ }
66
+
67
+ public async send(to: string, text: string): Promise<void> {
68
+ this.log.info('Mail to %s: %s', to, text)
69
+ }
70
+ }
71
+
72
+ Application
73
+ .run(() => ({
74
+ id: 'com.example.app',
75
+ name: 'Example',
76
+ components: {
77
+ mailer: {class: Mailer, host: 'smtp.example.com'}
78
+ },
79
+ bootstrap: ['mailer']
80
+ }))
81
+ .onLaunched(async (app: Application): Promise<void> => {
82
+ const mailer: Mailer = await app.getObject<Mailer>('mailer')
83
+ await mailer.send('someone@example.com', 'Hello')
84
+ })
85
+ ```
86
+
87
+ 取值的先后顺序(配置的值、初始值、校验规则的默认值)以及启动时的检查,见 [Decorator](Decorator.md) 第 1.2 节。
88
+
89
+ ## 3. 在多个应用之间共享功能
90
+
91
+ 多个应用都要用到的功能,做成一个包来共享。包导出一个 `Component`(或 `Provider`),再导出一个生成其声明的函数,框架自带的包都是这样做的(`lakutata/com/database` 的 `buildDatabaseOptions`、`lakutata/com/queue` 的 `buildQueueOptions`)。应用自己给组件起名,把选项传给这个函数;应用启动时,组件的 `@Configurable` 校验规则会检查这些选项,其他对象按这个名字注入该组件。
92
+
93
+ ```typescript
94
+ import {Application, Component, type ComponentOptions, DTO} from 'lakutata'
95
+ import {Configurable, Inject} from 'lakutata/decorator/di'
96
+
97
+ //包里:组件,以及生成其声明的函数
98
+ export interface BillingOptions {
99
+ currency: string
100
+ }
101
+
102
+ export class Billing extends Component {
103
+ @Configurable(DTO.String().length(3).required())
104
+ public readonly currency: string
105
+
106
+ public charge(amount: number): string {
107
+ return `Charged ${amount} ${this.currency}`
108
+ }
109
+ }
110
+
111
+ export function buildBillingOptions(options: BillingOptions): ComponentOptions<BillingOptions> {
112
+ return {class: Billing, ...options}
113
+ }
114
+
115
+ //应用里
116
+ class Checkout extends Component {
117
+ @Inject('billing')
118
+ protected readonly billing: Billing
119
+ }
120
+
121
+ Application.run(() => ({
122
+ id: 'shop.app',
123
+ name: 'Shop',
124
+ components: {
125
+ billing: buildBillingOptions({currency: 'EUR'}),
126
+ checkout: {class: Checkout}
127
+ }
128
+ }))
129
+ ```
@@ -0,0 +1,232 @@
1
+ # Lakutata 安装包
2
+
3
+ `lakutata package installer` 制作应用的安装包:系统的软件包(deb、rpm、apk、pkg、msi 等),它们安装应用的可执行文件,把程序加入 PATH,还可以把应用注册为随系统启动的服务。安装包在应用的 `package.json` 中配置,和其他打包命令一样,项目需要安装 `@lakutata/builder`(见 [Packaging](Packaging.md))。
4
+
5
+ ## 1. 制作安装包
6
+
7
+ ```bash
8
+ npm run build
9
+ npx lakutata package installer # 配置中的安装包,输出到 dist/installer/
10
+ npx lakutata package installer --target pkg # 只制作其中几种
11
+ ```
12
+
13
+ ```
14
+ › Node.js v24.11.1 embedded
15
+ › Resources: texts
16
+ ✔ dist/installer/greet-1.0.0-arm64.pkg (pkg, 36.0 MB)
17
+ ```
18
+
19
+ 安装包里是目录布局的可执行文件(`lakutata package exe --layout directory`):程序本身、它的原生插件(`native/`)和资源(`resources/`)。程序用 Node.js 制作;`lakutata.installer.runtime` 为 `"bun"` 时用 Bun 制作。
20
+
21
+ | 平台 | 安装包(默认) | 文件名 | 制作工具 |
22
+ |---|---|---|---|
23
+ | Linux(glibc) | `deb`、`rpm`、`tar.gz`(还支持 `archlinux`) | `<name>_<version>_amd64.deb`、`<name>-<version>-1.x86_64.rpm`、`<name>-<version>-linux-amd64.tar.gz` | nFPM |
24
+ | Linux(musl、Alpine) | `apk`、`tar.gz` | `<name>-<version>-r0.x86_64.apk` | nFPM |
25
+ | macOS | `pkg`、`tar.gz` | `<name>-<version>-arm64.pkg`、`<name>-<version>-darwin-arm64.tar.gz` | `pkgbuild`、`productbuild` |
26
+ | Windows | `nsis`、`msi`、`zip` | `<name>-<version>-x64-setup.exe`、`<name>-<version>-x64.msi`、`<name>-<version>-win-x64.zip` | NSIS、WiX、WinSW |
27
+
28
+ - **在各自的平台上制作**:某个平台的安装包要在那个平台上制作,针对的是本机的架构(其他平台和架构见第 7 节),`pkg` 软件包也只能安装在这种架构的 Mac 上。可执行文件使用制作它的系统的 C 库,所以面向 glibc 系统的 Linux 软件包要在 glibc 系统上制作,`apk` 软件包要在 Alpine 上制作。
29
+ - **制作工具** nFPM、NSIS、WiX 和 WinSW 只下载一次,保存在工具的缓存中(`LAKUTATA_CACHE_DIR`)。`LAKUTATA_NFPM`、`LAKUTATA_NSIS`、`LAKUTATA_WIX` 和 `LAKUTATA_WINSW` 可以指定其他副本,用于没有网络的机器,或者 x64 和 arm64 以外的 Linux 架构(nFPM 只发布了这两种)。macOS 不需要下载任何工具。
30
+ - **安装包的版本**就是应用 `package.json` 中的 `version`。
31
+
32
+ ## 2. 配置
33
+
34
+ 安装包由应用 `package.json` 中的 `lakutata.installer` 配置。下面是带 `installer` 可选功能(`lakutata create --with installer`)的 `service` 模板中的配置:
35
+
36
+ ```json
37
+ {
38
+ "bin": {"shop": "build/App.js"},
39
+ "scripts": {"package": "lakutata package installer"},
40
+ "lakutata": {
41
+ "icon": "../resources/logo.png",
42
+ "installer": {
43
+ "name": "shop",
44
+ "service": {"env": {"PORT": "8080"}},
45
+ "linux": {"targets": ["deb", "rpm", "tar.gz"]}
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ | 字段 | 默认值 | 含义 |
52
+ |---|---|---|
53
+ | `name` | 程序名(第一个 `bin`,没有时为去掉作用域的包名)的小写形式 | 软件包、安装目录和文件的名称:小写字母、数字、`+`、`-` 和 `.` |
54
+ | `appId` | `name` | 应用的标识(`com.acme.shop`):在 macOS 上是软件包的标识和服务的标签;在 Windows 上是卸载程序和升级所用的键 |
55
+ | `productName` | `package.json` 的 `productName`,没有时为 `name` | 显示给用户的名称(在安装程序中,以及 Windows 的程序列表中) |
56
+ | `description`、`homepage`、`license` | 取自 `package.json` | 软件包的信息 |
57
+ | `maintainer` | `package.json` 的 `author` | 维护者(`Name <email>`);没有时工具会发出警告 |
58
+ | `vendor` | 无 | 供应商;在 Windows 上,发布者是供应商,没有时是维护者 |
59
+ | `runtime` | `node` | 可执行文件用 `node` 或 `bun`;Electron 应用用 `electron`(第 8 节) |
60
+ | `addToPath` | `true` | 把程序加入 PATH:Linux 上是 `/usr/bin/<program>`,macOS 上是 `/usr/local/bin/<program>`,Windows 上是机器的 PATH |
61
+ | `service` | 无 | `true`,或服务的选项(第 4 节) |
62
+ | `linux.targets` | `deb`、`rpm`、`tar.gz`;Alpine 上是 `apk`、`tar.gz` | Linux 的安装包(`deb`、`rpm`、`archlinux`、`tar.gz`;Alpine 上是 `apk`、`tar.gz`) |
63
+ | `linux.depends`、`linux.recommends` | 无 | 依赖和推荐的系统软件包(总会加上 C++ 运行库) |
64
+ | `linux.section`、`linux.priority` | 无 | 分类和优先级(deb) |
65
+ | `mac.targets` | `pkg`、`tar.gz` | macOS 的安装包 |
66
+ | `win.targets` | `nsis`、`msi`、`zip` | Windows 的安装包 |
67
+ | `linux.scripts`、`mac.scripts`、`win.scripts` 等 | 无 | 安装包执行的脚本(第 5 节) |
68
+ | `electron` | 无 | electron-builder 的配置(第 8 节) |
69
+
70
+ `lakutata.icon`(与 `lakutata.installer` 并列)是一张 256 像素或更大的正方形 PNG 图片:它是 Windows 可执行文件和安装程序的图标,也是 Electron 应用的图标。
71
+
72
+ 配置在构建任何东西之前就会检查,不制作的那些安装包的选项也会检查,出错时会指出是哪个字段:
73
+
74
+ ```
75
+ error: The name of the package "Bad Name" must be lower case letters, digits, "+", "-" and "." (lakutata.installer.name)
76
+ error: The installer msi is not one of macOS (pkg, tar.gz)
77
+ error: The script lakutata.installer.mac.scripts.postinstall (scripts/missing.sh) does not exist
78
+ ```
79
+
80
+ ## 3. 安装位置
81
+
82
+ | | Linux | macOS | Windows |
83
+ |---|---|---|---|
84
+ | 应用 | `/opt/<name>` | `/usr/local/lib/<name>` | `C:\Program Files\<name>` |
85
+ | PATH 中的程序 | `/usr/bin/<program>` | `/usr/local/bin/<program>` | 机器的 PATH |
86
+ | 服务 | `/usr/lib/systemd/system/<service>.service`;`/etc/init.d/<service>`(OpenRC,Alpine) | `/Library/LaunchDaemons/<appId>.plist` | 一个 Windows 服务:程序旁边的 `<name>-service.exe`(WinSW) |
87
+ | 服务的环境变量 | `/etc/<name>/<name>.env` | `/usr/local/etc/<name>/<name>.env` | `C:\ProgramData\<name>\<name>.env` |
88
+ | 工作目录(数据) | `/var/lib/<name>` | `/usr/local/var/lib/<name>` | `C:\ProgramData\<name>` |
89
+ | 日志 | systemd 的日志(`journalctl -u <service>`);OpenRC 下是 `/var/log/<name>.log` | `/usr/local/var/log/<name>.log` | `C:\ProgramData\<name>\logs` |
90
+ | 卸载 | 用包管理器 | `sudo /usr/local/lib/<name>/uninstall` | 和其他 Windows 程序一样 |
91
+
92
+ - **macOS 不会卸载软件包**:随应用安装的 `uninstall` 脚本负责删除应用、它的服务以及 PATH 中的链接。
93
+ - **卸载时保留**数据、环境变量文件和服务的用户,以便重新安装时继续使用它们。
94
+ - 压缩包(`tar.gz`、`zip`)里只有应用本身:不注册服务,也不执行脚本。
95
+
96
+ ## 4. 服务
97
+
98
+ 设置了 `service`(`true`,或服务的选项)时,应用会随系统启动:由 systemd、Alpine 上的 OpenRC、macOS 上的 launchd 运行,或者作为 Windows 服务(WinSW)运行。安装时启用并启动服务,升级时重启它,卸载前先停止并禁用它。
99
+
100
+ ```json
101
+ {
102
+ "lakutata": {
103
+ "installer": {
104
+ "service": {
105
+ "args": ["serve"],
106
+ "env": {"PORT": "8080", "LOG_LEVEL": "info"},
107
+ "restart": "on-failure"
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ | 选项 | 默认值 | 含义 |
115
+ |---|---|---|
116
+ | `name` | `name` | 服务的名称(systemd、OpenRC、Windows);在 macOS 上,服务以 `appId` 命名 |
117
+ | `description` | 软件包的描述 | |
118
+ | `args` | 无 | 程序的参数 |
119
+ | `env` | `MODE=production` | 服务的环境变量,加在 `MODE=production` 之上 |
120
+ | `user` | 安装时创建的系统用户:Linux 上是 `<name>`,macOS 上是 `_<name>`(隐藏);Windows 上是虚拟账户 `NT SERVICE\<service>` | 运行服务的用户;也可以是 `root`(Linux、macOS),或 Windows 上的 `LocalService`、`NetworkService`、`LocalSystem` |
121
+ | `restart` | `on-failure` | 失败时重启(`on-failure`)、每次结束都重启(`always`),或者从不重启(`no`);在 Windows 上和 OpenRC 下,`on-failure` 和 `always` 的效果相同 |
122
+ | `autoStart` | `true` | 安装时启用并启动服务;为 `false` 时由管理员启用并启动它(`sudo systemctl enable --now <service>`;在 macOS 上先 `sudo launchctl enable system/<appId>`,再 `sudo launchctl bootstrap system /Library/LaunchDaemons/<appId>.plist`) |
123
+
124
+ - **环境变量文件**保存 `env`。管理员可以修改它,升级和卸载都会保留这些修改。服务启动时读取它,所以修改后请重启服务(`sudo systemctl restart <service>`、`sudo rc-service <service> restart`、`sudo launchctl kickstart -k system/<appId>`、`Restart-Service <service>`)。
125
+ - **工作目录**属于服务的用户:应用的数据写在这里,用相对路径或 `process.cwd()` 即可。
126
+ - **停止**服务时应用会优雅退出:它收到 `SIGTERM`,在 Windows 上收到 Ctrl+C,Node.js 把它当作 `SIGINT`。systemd、launchd 和 WinSW 给它 30 秒时间退出。
127
+
128
+ ## 5. 安装脚本
129
+
130
+ 安装包可以执行应用的脚本(项目中的文件),用来创建数据库、迁移数据或者清理。脚本可以针对一个平台(该平台的所有安装包都执行),针对某一种安装包(替换平台在同一时机的脚本),作为 Linux 软件包的原生脚本,或者是一个 NSIS 文件:
131
+
132
+ ```json
133
+ {
134
+ "lakutata": {
135
+ "installer": {
136
+ "linux": {
137
+ "scripts": {"postinstall": "build/linux/postinstall.sh", "preremove": "build/linux/preremove.sh"},
138
+ "rpm": {"scripts": {"postinstall": "build/linux/rpm-postinstall.sh"}},
139
+ "deb": {"nativeScripts": {"postinstall": "build/linux/postinst.sh"}}
140
+ },
141
+ "mac": {"scripts": {"postinstall": "build/mac/postinstall.sh"}},
142
+ "win": {"scripts": {"postinstall": "build/win/postinstall.ps1"}, "nsis": {"include": "build/win/installer.nsh"}}
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ | 脚本 | 执行时机 |
149
+ |---|---|
150
+ | `preinstall` | 安装应用的文件之前 |
151
+ | `postinstall` | 文件安装之后、服务启动之前 |
152
+ | `preremove` | 卸载之前,服务已经停止 |
153
+ | `postremove` | 文件删除之后 |
154
+
155
+ ```sh
156
+ #!/bin/sh
157
+ # build/linux/postinstall.sh: $1 is install or upgrade
158
+ if [ "$1" = install ]; then
159
+ echo "Installed in $LAKUTATA_INSTALL_DIR"
160
+ fi
161
+ ```
162
+
163
+ - **参数**(`$1`,PowerShell 中是 `$args[0]`)说明安装包在做什么:`preinstall` 和 `postinstall` 收到 `install` 或 `upgrade`,`preremove` 和 `postremove` 收到 `remove`(升级不执行卸载脚本)。各个包管理器(dpkg、rpm、apk、pacman)表示升级的方式各不相同,安装包会把它们转换成这些参数。
164
+ - **`LAKUTATA_INSTALL_DIR`** 是应用所在的目录。
165
+ - **以 root 身份执行**;在 Windows 上,NSIS 以管理员身份执行,Windows Installer 以系统账户执行。脚本失败时,安装(或卸载)也会失败。
166
+ - **Linux 和 macOS**:脚本用第一行指定的解释器执行(`#!/bin/bash`),默认是 `/bin/sh`。`linux.<package>.nativeScripts` 作为软件包自身的脚本执行,接收包管理器的参数(apk 和 archlinux 还有 `preupgrade`、`postupgrade`)。在 macOS 上,卸载脚本由 `uninstall` 执行。
167
+ - **Windows**:脚本是 PowerShell 文件(`.ps1`),由 Windows PowerShell 执行。Windows Installer 通过命令行传入每个脚本,所以每个脚本限制在大约 23 KB 以内。`win.nsis.include` 是一个 NSIS 文件(`.nsh`),可以定义这些宏:`customHeader`、`customInit`、`customInstall`(在安装的最后)、`customUnInit` 和 `customUnInstall`(在卸载的开始)。
168
+ - **压缩包**不执行任何脚本:某个脚本在要制作的某种安装包中不会执行时,工具会发出警告(`! Some scripts of the application are not run by their installers: tar.gz (postinstall)`)。
169
+
170
+ ## 6. 签名
171
+
172
+ - **macOS**:`pkg` 安装包没有签名,它安装的程序也只有临时签名(ad hoc)。在组织以外的机器上分发所需要的 Developer ID 签名和公证,目前还不会制作。
173
+ - **Windows**:NSIS 和 MSI 安装包没有签名,分发前请用 signtool 签名。用 Node.js 制作的程序保留着 `node.exe` 的签名,而这个签名已经不再匹配,工具会对此发出警告。
174
+ - **Linux**:软件包没有签名。
175
+
176
+ ## 7. 其他平台
177
+
178
+ 其他平台的安装包要在那些平台上制作:用 CI 为每个平台准备一台机器,Linux 也可以用对应架构的容器。带 `installer` 可选功能的模板提供了这两个脚本:
179
+
180
+ ```json
181
+ {
182
+ "scripts": {
183
+ "package:host": "npm run build && npm run package -w app -- --outdir ../../dist/installer",
184
+ "package:linux": "docker buildx build --file Dockerfile.installer --output type=local,dest=dist/installer ."
185
+ }
186
+ }
187
+ ```
188
+
189
+ `npm run package:linux` 在容器中制作 Linux 软件包(在 macOS 和 Windows 上也可以),并把它们复制到 `dist/installer`;`docker buildx build --platform linux/arm64 ...` 制作其他架构的软件包。它用的 `Dockerfile.installer`:
190
+
191
+ ```dockerfile
192
+ FROM node:22-bookworm AS build
193
+ WORKDIR /project
194
+ COPY . .
195
+ RUN (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build && npm run package -w app -- --outdir /installer
196
+
197
+ FROM scratch
198
+ COPY --from=build /installer /
199
+ ```
200
+
201
+ `apk` 软件包用同样的方法在 `node:22-alpine` 上制作,应用有原生插件时用 Bun 制作(Node.js 的可执行文件在 musl 上不能加载原生插件)。其余的安装包由 CI 的矩阵制作,例如用 GitHub Actions:
202
+
203
+ ```yaml
204
+ jobs:
205
+ installers:
206
+ strategy:
207
+ matrix:
208
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
209
+ runs-on: ${{ matrix.os }}
210
+ steps:
211
+ - uses: actions/checkout@v4
212
+ - uses: actions/setup-node@v4
213
+ with:
214
+ node-version: 22
215
+ - run: npm ci
216
+ - run: npm run package:host
217
+ - uses: actions/upload-artifact@v4
218
+ with:
219
+ name: installers-${{ runner.os }}-${{ runner.arch }}
220
+ path: dist/installer/
221
+ ```
222
+
223
+ ## 8. Electron 应用
224
+
225
+ 设置 `"runtime": "electron"` 后,安装包由 electron-builder 制作,项目需要安装它和 `electron`。主进程会被打包(不含 Electron 本身),它的原生插件和资源(`lakutata.resources`:渲染进程和预加载脚本,通过 `resourcesPath()` 读取)放进 Electron 的资源目录。`electron-react` 和 `electron-vue` 模板就是这样配置的。
226
+
227
+ - **安装包**是 electron-builder 的目标:默认在 Linux 上是 `deb`,macOS 上是 `dmg`,Windows 上是 `nsis`(`linux.targets`、`mac.targets`、`win.targets`)。文件名取去掉作用域的包名。
228
+ - **`electron`** 会合并到 electron-builder 的配置中:图标、文件关联、签名、更新等。`name`、`appId`、`productName`、描述、维护者、主页和许可证也会传给它。
229
+ - **Electron 应用不是服务**:它在用户的会话中运行,设置 `service` 会被拒绝。
230
+ - **脚本**在 `deb` 和 `rpm` 软件包(全部四种)、`pkg`(`preinstall`、`postinstall`)和 `nsis` 安装程序(`preinstall`、`postinstall`、`preremove`)中执行;其他目标都不执行。
231
+ - **原生插件**按安装时的样子复制:electron-builder 不会重新构建它们。基于 Node-API 的插件可以直接运行;其他插件请在打包前安装它们为 Electron 构建的版本(`electron-rebuild`)(见 [NativeAddons](NativeAddons.md) 第 4 节)。
232
+ - 在 Linux arm64 上,deb 和 rpm 软件包需要系统中的 fpm(`gem install fpm`、`USE_SYSTEM_FPM=true`),Electron 模板的 `Dockerfile.installer` 就是这样安装它的。
@@ -0,0 +1,188 @@
1
+ # Lakutata 日志
2
+
3
+ 每个 `lakutata` 应用都有一个日志记录器:由这个包预置的 `log` 组件(`lakutata/com/logger` 中的 `Logger`)。它把可读的日志行,或者供日志收集器读取的 pino 格式的 JSON 行,写到标准输出或其他流中。框架本身也通过它写日志。
4
+
5
+ ## 1. `log` 组件
6
+
7
+ ```typescript
8
+ import {Application, Component} from 'lakutata'
9
+ import {Inject} from 'lakutata/decorator/di'
10
+ import {Logger} from 'lakutata/com/logger'
11
+
12
+ class Orders extends Component {
13
+ @Inject('log')
14
+ protected readonly log: Logger
15
+
16
+ public pay(orderId: number, amount: number): void {
17
+ this.log.info({orderId: orderId}, 'order %d paid: %s', orderId, amount.toFixed(2))
18
+ }
19
+ }
20
+
21
+ Application
22
+ .run(() => ({
23
+ id: 'shop.app',
24
+ name: 'Shop',
25
+ components: {
26
+ //预置的日志记录器的选项:不需要 class
27
+ log: {level: 'info', pretty: process.env.NODE_ENV !== 'production'},
28
+ orders: {class: Orders}
29
+ }
30
+ }))
31
+ .onLaunched(async (app: Application): Promise<void> => {
32
+ const orders: Orders = await app.getObject<Orders>('orders')
33
+ orders.pay(42, 9.9)
34
+ })
35
+ ```
36
+
37
+ - 按名字 `log` 注入它。类型可以写成 `Logger`,也可以写成 `lakutata` 中的 `ILogger`,这是应用的所有日志记录器都实现的接口。
38
+ - 应用的处理函数(`onLaunched`、`onDone`、`onUncaughtException`、`onFatalException`)的第二个参数就是它(见 [Application](Application.md) 第 2 节)。
39
+ - 它的选项写在 `components` 的 `log` 中,不需要 `class`(第 4 节)。
40
+
41
+ ## 2. 写一行日志
42
+
43
+ | 方法 | 级别 | 用于 |
44
+ |---|---|---|
45
+ | `fatal()` | 60 | 应用无法继续运行的故障;只有 `Logger` 有这个方法,`ILogger` 没有 |
46
+ | `error()` | 50 | 需要有人处理的故障 |
47
+ | `warn()` | 40 | 应用能够恢复的意外情况 |
48
+ | `info()` | 30 | 应用的正常运行 |
49
+ | `debug()` | 20 | 调试用的细节 |
50
+ | `trace()` | 10 | 比调试更细的跟踪信息 |
51
+
52
+ 每个方法先接受一个可选的对象,它的属性会加到这一行中,然后是消息,最后是消息中占位符的值:
53
+
54
+ ```typescript
55
+ import {Logger} from 'lakutata/com/logger'
56
+
57
+ declare const log: Logger
58
+
59
+ //%s 字符串,%d 和 %f 数字,%i 整数,%o、%O 和 %j JSON,%% 百分号
60
+ log.info('user %s signed in from %s', 'alice', '10.0.0.7')
61
+ //对象的属性加到这一行中
62
+ log.warn({orderId: 42, retries: 3}, 'payment retried %d times', 3)
63
+ //错误:写在 "err" 下;没有给出消息时,它的消息就是这一行的消息
64
+ log.error(new Error('connection refused'), 'payment failed')
65
+ ```
66
+
67
+ - 没有对应占位符的值会被丢弃:`log.info('done', 42)` 只写出 `done`。
68
+ - 错误写在 `err` 下:它的类型;它的消息,后面跟着它的原因的消息;它的调用栈,后面跟着原因的调用栈;以及它自己的属性(比如异常的 `errno` 和 `statusCode`)。
69
+ - HTTP 请求写在 `req` 下(方法、URL、请求头、远端地址),HTTP 响应写在 `res` 下。它们的 `Authorization`、`Cookie`、`Set-Cookie` 和 `Proxy-Authorization` 请求头写成 `[Redacted]`。
70
+ - 除此之外不会隐去任何内容,也没有子日志记录器:只记录需要的字段,不要记录令牌,也不要记录包含令牌的整个对象;上下文(比如请求 id)放在每次调用的对象中。
71
+
72
+ ```text
73
+ [13:44:12.447] ERROR (Shop/24236): payment failed
74
+ err: {
75
+ "type": "PaymentFailedException",
76
+ "message": "Payment of order 7 failed: socket hang up",
77
+ "stack":
78
+ PaymentFailedException: Payment of order 7 failed
79
+ at pay (file:///app/build/Orders.js:48:17)
80
+ caused by: Error: socket hang up
81
+ at charge (file:///app/build/Gateway.js:12:9)
82
+ "appId": "shop.app",
83
+ "appName": "Shop",
84
+ "statusCode": 502,
85
+ "errMsg": "Payment of order 7 failed",
86
+ "err": "PaymentFailedException",
87
+ "errno": "E_PAYMENT_FAILED"
88
+ }
89
+ ```
90
+
91
+ ## 3. 可读的行或 JSON
92
+
93
+ 默认情况下(`pretty: true`),日志行是可读的:本地时区的时间、级别、应用的名字、进程 id、消息,然后每个属性缩进一行:
94
+
95
+ ```text
96
+ [13:45:04.246] INFO (Shop/24297): order 42 paid: 9.90
97
+ orderId: 42
98
+ ```
99
+
100
+ 设置 `pretty: false` 时,每一行是一个 JSON 对象,格式与 pino 相同:`level`、`time`(自纪元起的毫秒数)、`pid`、`hostname`、`name`(应用的名字)、对象的属性,以及 `msg`。日志收集器读取这种格式;生产环境请使用它。
101
+
102
+ ```text
103
+ {"level":30,"time":1792215904479,"pid":24300,"hostname":"api-1","name":"Shop","orderId":42,"msg":"order 42 paid: 9.90"}
104
+ ```
105
+
106
+ ## 4. 选项
107
+
108
+ | 选项 | 默认值 | 含义 |
109
+ |---|---|---|
110
+ | `level` | `'trace'` | 写出的最低级别:`'trace'`、`'debug'`、`'info'`、`'warn'`、`'error'`、`'fatal'`(只写 `fatal()` 的日志)或 `'silent'`(什么都不写) |
111
+ | `pretty` | `true` | 写可读的行;`false` 写 JSON 行 |
112
+ | `colorize` | 在终端中时 | 给写到标准输出的可读行加上颜色 |
113
+ | `destinations` | `[process.stdout]` | 接收日志行的流(第 5 节) |
114
+ | `sync` | `false` | 每一行立即写出,而不是成批写出(第 6 节) |
115
+ | `redactedHeaders` | `['authorization', 'cookie', 'set-cookie', 'proxy-authorization']` | 写成 `[Redacted]` 的请求头;这个列表会替换默认列表,`[]` 表示不隐去任何请求头 |
116
+
117
+ 默认情况下,只有标准输出是终端、并且没有设置环境变量 `NO_COLOR` 时,可读行才带颜色:重定向到文件或者被日志收集器读取的标准输出不会包含颜色控制码。设置 `colorize: true` 时总是加上颜色。
118
+
119
+ ## 5. 输出目标
120
+
121
+ `destinations` 中的每个流都会收到每一行日志:
122
+
123
+ ```typescript
124
+ import {createWriteStream} from 'node:fs'
125
+ import {Application} from 'lakutata'
126
+
127
+ Application.run(() => ({
128
+ id: 'shop.app',
129
+ name: 'Shop',
130
+ components: {
131
+ log: {
132
+ pretty: false,
133
+ //标准输出,以及一个追加写入的文件
134
+ destinations: [process.stdout, createWriteStream('shop.log', {flags: 'a'})]
135
+ }
136
+ }
137
+ }))
138
+ ```
139
+
140
+ - 日志记录器不会关闭这些流,也不会轮转文件:交给系统处理(logrotate、容器的日志收集器)。
141
+ - 标准输出被协议占用的应用(基于 stdio 的 JSON-RPC、基于 stdio 的 MCP)要把日志写到标准错误:`log: {class: Logger, destinations: [process.stderr]}`(见 [ServiceCalls](ServiceCalls.md) 第 4 节)。
142
+
143
+ ## 6. 缓冲
144
+
145
+ 日志行成批写出:在事件循环的下一轮,或者等待写出的内容达到 16 KB 时。进程退出时,以及日志记录器被销毁时,等待中的行也会写出。
146
+
147
+ - `sync: true` 让每一行立即写出。
148
+ - `flush()` 立即写出等待中的行,比如在调用 `process.exit()` 之前。
149
+ - `fatal()` 立即写出它自己的行和等待中的行。
150
+ - 标准输出、标准错误和文件是同步写入的,所以日志行保持原来的顺序。
151
+
152
+ ## 7. 换用其他日志记录器
153
+
154
+ `log` 组件可以换成你自己的类:一个实现了 `ILogger` 的组件,用 `class` 声明。之后框架和应用的处理函数都通过它写日志。
155
+
156
+ ```typescript
157
+ import {Application, Component, type ILogger} from 'lakutata'
158
+
159
+ class ConsoleOnlyLogger extends Component implements ILogger {
160
+ public error(...args: unknown[]): void {
161
+ console.error(...args)
162
+ }
163
+
164
+ public warn(...args: unknown[]): void {
165
+ console.warn(...args)
166
+ }
167
+
168
+ public info(...args: unknown[]): void {
169
+ console.info(...args)
170
+ }
171
+
172
+ public debug(...args: unknown[]): void {
173
+ console.debug(...args)
174
+ }
175
+
176
+ public trace(...args: unknown[]): void {
177
+ console.debug(...args)
178
+ }
179
+ }
180
+
181
+ Application.run(() => ({
182
+ id: 'tool.app',
183
+ name: 'Tool',
184
+ components: {
185
+ log: {class: ConsoleOnlyLogger}
186
+ }
187
+ }))
188
+ ```