@mica-js/sdk 0.0.0-stage → 0.2.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -2
  3. package/dist/build/cli.d.ts +22 -0
  4. package/dist/build/cli.js +211 -0
  5. package/dist/build/config.d.ts +16 -0
  6. package/dist/build/config.js +92 -0
  7. package/dist/build/content.d.ts +14 -0
  8. package/dist/build/content.js +159 -0
  9. package/dist/build/prepare.d.ts +51 -0
  10. package/dist/build/prepare.js +306 -0
  11. package/dist/build/project.d.ts +4 -0
  12. package/dist/build/project.js +30 -0
  13. package/dist/build/vite.d.ts +7 -0
  14. package/dist/build/vite.js +60 -0
  15. package/dist/ddl.d.ts +2 -0
  16. package/dist/ddl.js +1 -0
  17. package/dist/device.d.ts +1 -0
  18. package/dist/device.js +1 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/metadata/index.d.ts +5 -0
  22. package/dist/metadata/index.js +2 -0
  23. package/dist/metadata/schema.d.ts +10 -0
  24. package/dist/metadata/schema.js +392 -0
  25. package/dist/metadata/types.d.ts +192 -0
  26. package/dist/metadata/types.js +7 -0
  27. package/dist/metadata/validate.d.ts +25 -0
  28. package/dist/metadata/validate.js +283 -0
  29. package/dist/model.d.ts +1 -0
  30. package/dist/model.js +1 -0
  31. package/dist/widget.d.ts +1 -0
  32. package/dist/widget.js +1 -0
  33. package/package.json +74 -3
  34. package/templates/basic/mica.config.ts +3 -0
  35. package/templates/basic/package.json +14 -0
  36. package/templates/basic/src/devices/resistor/device.json +30 -0
  37. package/templates/basic/src/devices/resistor/index.ts +8 -0
  38. package/templates/basic/src/devices/resistor/model.ddl +6 -0
  39. package/templates/basic/src/widgets/lamp/index.ts +22 -0
  40. package/templates/basic/src/widgets/lamp/widget.json +9 -0
  41. package/templates/basic/tsconfig.json +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DigitalEcho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,312 @@
1
- # Temporary Holding Version
1
+ # @mica-js/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 用于开发 Mica 器件(Device)和界面组件(Widget)。SDK 提供类型定义和 `mica` CLI,将 TypeScript、DDL 与 JSON 元数据构建成宿主可加载的组件。
4
+
5
+ - **Device**:使用 DDL 描述引脚和仿真行为,按需编写 TypeScript 适配器。
6
+ - **Widget**:使用 TypeScript 绘制界面,通过端口接收和发送数据。
7
+ - **组件产物**:每个组件一份 JS 和一份 JSON,依赖与资源全部内联。
8
+
9
+ ## 快速开始
10
+
11
+ 建议使用 Node.js 24 和 pnpm。`mica` 命令随 `@mica-js/sdk` 安装,不需要另外安装名为 `mica` 的包,也不需要自行配置 Vite。
12
+
13
+ ### 1. 安装
14
+
15
+ ```shell
16
+ mkdir my-components
17
+ cd my-components
18
+ pnpm init
19
+ pnpm add -D @mica-js/sdk typescript
20
+ ```
21
+
22
+ 在 `package.json` 中添加以下字段,保留安装生成的依赖配置:
23
+
24
+ ```json
25
+ {
26
+ "private": true,
27
+ "type": "module",
28
+ "scripts": {
29
+ "build": "mica build",
30
+ "prepare:types": "mica prepare",
31
+ "typecheck": "mica prepare && tsc --noEmit"
32
+ }
33
+ }
34
+ ```
35
+
36
+ 这里的 `private` 表示组件源码工程不作为 npm 包发布,不影响生成和交付组件产物。
37
+
38
+ ### 2. 创建工程配置
39
+
40
+ 在工程根目录创建 `mica.config.ts`:
41
+
42
+ ```typescript
43
+ import { defineConfig } from '@mica-js/sdk/config';
44
+
45
+ export default defineConfig({});
46
+ ```
47
+
48
+ 该文件是 CLI 识别 Mica 工程的标志。空配置默认读取 `src`,输出到 `dist`。
49
+
50
+ 创建 `tsconfig.json`:
51
+
52
+ ```json
53
+ {
54
+ "compilerOptions": {
55
+ "target": "ES2022",
56
+ "module": "ESNext",
57
+ "moduleResolution": "Bundler",
58
+ "lib": ["ES2023", "DOM"],
59
+ "strict": true,
60
+ "noEmit": true,
61
+ "skipLibCheck": true,
62
+ "allowArbitraryExtensions": true,
63
+ "verbatimModuleSyntax": true
64
+ },
65
+ "include": ["src", "mica.config.ts"]
66
+ }
67
+ ```
68
+
69
+ ### 3. 编写第一个 Widget
70
+
71
+ 创建 `src/widgets/lamp/widget.json`:
72
+
73
+ ```json
74
+ {
75
+ "kind": "widget",
76
+ "id": "lamp",
77
+ "version": "1.0.0",
78
+ "displayName": "Lamp"
79
+ }
80
+ ```
81
+
82
+ 同目录创建 `index.ts`:
83
+
84
+ ```typescript
85
+ import type { WidgetFactory } from '@mica-js/sdk/widget';
86
+
87
+ const create: WidgetFactory = (api) => ({
88
+ render(ctx) {
89
+ ctx.fillStyle = '#22aa55';
90
+ ctx.fillRect(0, 0, api.width, api.height);
91
+ },
92
+ });
93
+
94
+ export default create;
95
+ ```
96
+
97
+ ### 4. 检查并构建
98
+
99
+ ```shell
100
+ pnpm run typecheck
101
+ pnpm run build
102
+ ```
103
+
104
+ 生成:
105
+
106
+ ```text
107
+ dist/
108
+ widgets/lamp/widget.json
109
+ widgets/lamp/widget.js
110
+ ```
111
+
112
+ 将这两个文件交给支持 Mica 组件的宿主加载。CLI 负责构建,预览界面和运行环境由宿主提供;源码变更后重新运行构建。
113
+
114
+ 也可以从随包提供的 [basic 模板](./templates/basic/) 开始,其中包含电阻器件和通过亮度端口更新的灯组件。安装 SDK 后,模板位于 `node_modules/@mica-js/sdk/templates/basic`;复制到自己的工程目录后,执行 `pnpm add -D @mica-js/sdk@latest typescript`,更新模板的 SDK 依赖并安装当前发布版本。
115
+
116
+ ## 使用 mica CLI
117
+
118
+ 在终端通过 `pnpm exec` 调用项目本地安装的 CLI;在 `package.json` 的 scripts 中可直接使用 `mica`。
119
+
120
+ | 命令 | 用途 |
121
+ | -------------------------------- | -------------------------------------------------------- |
122
+ | `pnpm exec mica build` | 生成 DDL 类型、检查元数据并构建所有组件 |
123
+ | `pnpm exec mica prepare` | 只生成 DDL 类型声明,不构建组件 |
124
+ | `pnpm exec mica prepare --watch` | 启动 DDL 类型监听,适合与编辑器或 `tsc --watch` 同时使用 |
125
+ | `pnpm exec mica --help` | 查看命令和参数;仅输入 `mica` 也会显示帮助 |
126
+
127
+ 常用参数:
128
+
129
+ | 参数 | 含义 |
130
+ | -------------------- | ----------------------------------------------- |
131
+ | `--root <dir>` | 从指定目录开始查找工程,默认当前目录 |
132
+ | `--config <file>` | 显式指定 `mica.config.ts`;相对路径基于查找起点 |
133
+ | `--source-dir <dir>` | 覆盖配置中的源码目录 |
134
+ | `--watch` | 监听 DDL 文件,仅用于 `prepare` |
135
+
136
+ ```shell
137
+ pnpm exec mica build --root components
138
+ pnpm exec mica build --config mica.config.ts
139
+ pnpm exec mica prepare --source-dir content
140
+ ```
141
+
142
+ 默认从当前目录向上寻找最近的 `mica.config.ts`,其所在目录作为工程根目录。因此,在工程子目录中也能运行命令。`--root` 是查找起点,不一定是最终工程根目录。显式配置文件也必须命名为 `mica.config.ts`。
143
+
144
+ `prepare --watch` 只更新类型声明,不会持续构建 JS。当前 CLI 不提供开发服务器。
145
+
146
+ ## 工程目录
147
+
148
+ 一个工程可以同时包含多个 Device 和 Widget:
149
+
150
+ ```text
151
+ my-components/
152
+ mica.config.ts
153
+ package.json
154
+ tsconfig.json
155
+ src/
156
+ devices/
157
+ resistor/
158
+ device.json
159
+ model.ddl
160
+ index.ts # 可选的宿主适配器
161
+ widgets/
162
+ lamp/
163
+ widget.json
164
+ index.ts
165
+ shared/ # 组件共享的源码或内联资源
166
+ ```
167
+
168
+ 组件目录名必须与元数据 `id` 相同,只使用字母、数字和中划线。各组件的 `id` 和 `version` 在自己的 JSON 中维护,与工程 `package.json` 的名称和版本分开。
169
+
170
+ `shared` 不是组件入口;组件可以通过普通静态导入复用其中的代码。构建时,共享代码会分别打包进每个使用它的组件,不生成共享 chunk。
171
+
172
+ ## 添加 Device
173
+
174
+ 创建 `src/devices/resistor/device.json`:
175
+
176
+ ```json
177
+ {
178
+ "kind": "device",
179
+ "id": "resistor",
180
+ "version": "1.0.0",
181
+ "displayName": "Resistor",
182
+ "designatorPrefix": "R",
183
+ "properties": {
184
+ "resistance": {
185
+ "type": "number",
186
+ "required": false,
187
+ "default": 1000,
188
+ "unit": "ohm"
189
+ }
190
+ },
191
+ "pins": { "a": {}, "b": {} }
192
+ }
193
+ ```
194
+
195
+ 同目录创建 `model.ddl`:
196
+
197
+ ```text
198
+ device Resistor(resistance: number default 1000) {
199
+ pin a = [resistor(to: b, resistance: resistance)];
200
+ pin b;
201
+ }
202
+ ```
203
+
204
+ 纯 DDL 器件不需要 `index.ts`,SDK 会生成最小同步工厂。需要与宿主交换输入、输出或资源时,再添加适配器:
205
+
206
+ ```typescript
207
+ import type { Factory } from './model.ddl';
208
+
209
+ const create: Factory = () => ({
210
+ sync(batch) {},
211
+ dispose() {},
212
+ });
213
+
214
+ export default create;
215
+ ```
216
+
217
+ 工厂和适配器钩子必须同步返回。使用生成的 `Factory` 类型可以获得与 DDL 对应的上下文、属性、输入和批次输出类型。
218
+
219
+ ### DDL 类型声明
220
+
221
+ 首次编写适配器或修改 DDL 后,运行:
222
+
223
+ ```shell
224
+ pnpm exec mica prepare
225
+ ```
226
+
227
+ 命令会在 `model.ddl` 旁生成 `model.d.ddl.ts`。开发时可单独运行 `pnpm exec mica prepare --watch` 保持声明更新。`mica build` 也会生成声明;单独运行 `tsc` 前则需要先执行 `mica prepare`。
228
+
229
+ 使用 `import type` 引用 DDL 类型,不要把 DDL 文件作为运行时 JS 模块导入。在 `.gitignore` 中忽略构建产物和生成的声明:
230
+
231
+ ```gitignore
232
+ dist/
233
+ node_modules/
234
+ **/*.d.ddl.ts
235
+ ```
236
+
237
+ ## 项目配置
238
+
239
+ ```typescript
240
+ import { defineConfig } from '@mica-js/sdk/config';
241
+
242
+ export default defineConfig({
243
+ sourceDir: 'src',
244
+ outDir: 'dist',
245
+ alias: { '@': './src' },
246
+ });
247
+ ```
248
+
249
+ | 字段 | 默认值 | 用途 |
250
+ | ----------- | ------ | ------------------------------------------ |
251
+ | `sourceDir` | `src` | 包含 `devices` 和 `widgets` 的源码目录 |
252
+ | `outDir` | `dist` | 组件输出目录 |
253
+ | `alias` | 无 | 静态导入的路径别名,例如 `@/shared/colors` |
254
+ | `base` | `/` | 底层构建的基准路径,不配置预览服务 |
255
+
256
+ 源码、输出和别名路径都相对于工程根目录解析。输出目录不能是工程根目录或其上级目录,也不能与源码目录互相包含,以免构建清理覆盖源码。
257
+
258
+ `alias` 用于构建时解析导入。若 TypeScript 源码也使用别名,需要在 `tsconfig.json` 的 `compilerOptions.paths` 中配置对应映射。
259
+
260
+ Vite 已由 SDK 管理,不需要额外安装 Vite 或创建 `vite.config.ts`。配置文件是可执行 TypeScript,只应加载受信任的工程配置。
261
+
262
+ ## 产物与资源
263
+
264
+ 运行 `mica build` 后,每个组件生成一对文件:
265
+
266
+ ```text
267
+ dist/
268
+ devices/resistor/device.json
269
+ devices/resistor/device.js
270
+ widgets/lamp/widget.json
271
+ widgets/lamp/widget.js
272
+ ```
273
+
274
+ - Device JS 具名导出 `model` 和同步 `create` 工厂;Widget JS 默认导出同步工厂。
275
+ - 每份 JS 都是独立的 ESM,依赖在构建时打包,产物不保留外部模块导入、`import()` 或 `import.meta`。
276
+ - 资源必须内联。文本可以使用 `?raw` 导入,标准 `?url` 资源导入在库构建中内联。
277
+ - 构建结果仅包含 JS 和 JSON。会产生独立 CSS 或其他资源文件的入口会构建失败。
278
+
279
+ 组件产物不是 npm 包;将文件交付给宿主的组件库或静态资源服务即可,模块加载和执行隔离由宿主负责。
280
+
281
+ ## 元数据
282
+
283
+ `device.json` 和 `widget.json` 由组件作者维护,用于声明显示名称、属性、端口以及器件符号等信息。SDK 检查元数据结构和目录 ID,但不会从 DDL 自动生成 JSON;器件属性、引脚和端口需要与实际实现保持一致。
284
+
285
+ 器件的 `symbol` 可以定义矢量图形、引脚连接点和位号位置,示例见 [电阻模板](./templates/basic/src/devices/resistor/device.json)。Widget 的端口订阅与重绘示例见 [灯组件模板](./templates/basic/src/widgets/lamp/index.ts)。
286
+
287
+ 需要自行检查元数据时,使用 `@mica-js/sdk/metadata` 的 `validateMetadata`、`deviceMetadataSchema` 或 `widgetMetadataSchema`。字节属性目前不支持在 JSON 中设置 `default`。
288
+
289
+ ## 常见问题
290
+
291
+ **提示找不到 `mica` 命令。** 确认已在当前工程安装 `@mica-js/sdk`,在终端使用 `pnpm exec mica`,或通过 package scripts 调用。
292
+
293
+ **提示找不到 `mica.config.ts`。** 在工程根目录添加配置文件,或用 `--root` / `--config` 指向现有工程。Device 与 Widget 工程都需要该标志文件。
294
+
295
+ **TypeScript 无法导入 `./model.ddl`。** 先运行 `mica prepare`,确认生成了 `model.d.ddl.ts`,并启用 `allowArbitraryExtensions` 与 `moduleResolution: "Bundler"`。
296
+
297
+ **修改源码后产物没有更新。** 重新运行 `mica build`;`prepare --watch` 只处理类型声明。
298
+
299
+ ## API 入口
300
+
301
+ | 入口 | 用途 |
302
+ | ----------------------- | --------------------------------------------------- |
303
+ | `@mica-js/sdk/widget` | `WidgetFactory`、绘制、输入、端口、资源与音频类型 |
304
+ | `@mica-js/sdk/device` | 器件上下文、工厂、批次、通道与 JIT 类型 |
305
+ | `@mica-js/sdk/metadata` | Device / Widget 元数据类型、Schema 与校验 |
306
+ | `@mica-js/sdk/config` | `defineConfig()` 与 `MicaConfig` |
307
+ | `@mica-js/sdk/prepare` | 程序化类型生成和监听 |
308
+ | `@mica-js/sdk/model` | 器件模型 IR、Builder 与校验 |
309
+ | `@mica-js/sdk/ddl` | 程序化 DDL 编译与类型投影 |
310
+ | `@mica-js/sdk` | `createDeviceLibrary`、`createBoard` 与宿主仿真类型 |
311
+
312
+ Widget / Device 作者类型入口不会引入 Vite 或 Node 构建依赖。编写组件通常只需类型导入,实际代码由宿主通过构建产物加载。
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /** Writable half of `process.stdout` / `process.stderr`, so tests can capture CLI output. */
3
+ export interface CliStreams {
4
+ readonly out: {
5
+ write(chunk: string): unknown;
6
+ };
7
+ readonly err: {
8
+ write(chunk: string): unknown;
9
+ };
10
+ }
11
+ export interface CliArguments {
12
+ readonly command: 'prepare' | 'build';
13
+ readonly help: boolean;
14
+ readonly watch: boolean;
15
+ readonly root: string;
16
+ readonly config?: string;
17
+ readonly sourceDir?: string;
18
+ }
19
+ /** Parses SDK commands without reading or executing project configuration. */
20
+ export declare function parseCliArguments(argv: readonly string[]): CliArguments;
21
+ /** Runs one SDK command and returns its exit code. */
22
+ export declare function runCli(argv: readonly string[], streams?: CliStreams): Promise<number>;
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { loadProjectConfig } from './config.js';
4
+ import { relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { prepare, watchDevices } from './prepare.js';
7
+ const USAGE = `Usage: mica <command> [options]
8
+
9
+ Commands:
10
+ build build standalone Device / Widget artifacts
11
+ prepare generate model.d.ddl.ts declarations
12
+
13
+ A mica.config.ts file identifies the project; it is discovered upwards from --root.
14
+
15
+ Options:
16
+ --config <file> explicit mica.config.ts, relative to --root
17
+ --root <dir> discovery directory (default: current directory)
18
+ --source-dir <dir> override the configured source directory
19
+ --watch watch DDL definitions (prepare only)
20
+ -h, --help show this help`;
21
+ /** Parses SDK commands without reading or executing project configuration. */
22
+ export function parseCliArguments(argv) {
23
+ // A bare `mica` only explains the command instead of guessing a package root.
24
+ let help = argv.length === 0;
25
+ let watch = false;
26
+ let root = process.cwd();
27
+ let sourceDir;
28
+ let config;
29
+ let command = 'prepare';
30
+ let index = 0;
31
+ const first = argv[0];
32
+ if (first !== undefined && !first.startsWith('-')) {
33
+ if (first !== 'prepare' && first !== 'build')
34
+ throw new Error(`Unknown command '${first}'.`);
35
+ command = first;
36
+ index = 1;
37
+ }
38
+ for (; index < argv.length; index++) {
39
+ const argument = argv[index];
40
+ const separator = argument.indexOf('=');
41
+ const flag = separator === -1 ? argument : argument.slice(0, separator);
42
+ const inline = separator === -1 ? undefined : argument.slice(separator + 1);
43
+ const flagValue = () => {
44
+ if (inline !== undefined) {
45
+ if (!inline)
46
+ throw new Error(`Missing value for ${flag}.`);
47
+ return inline;
48
+ }
49
+ const next = argv[++index];
50
+ if (next === undefined || next.startsWith('-'))
51
+ throw new Error(`Missing value for ${flag}.`);
52
+ return next;
53
+ };
54
+ const bare = () => {
55
+ if (inline !== undefined)
56
+ throw new Error(`${flag} does not take a value.`);
57
+ };
58
+ switch (flag) {
59
+ case '-h':
60
+ case '--help':
61
+ bare();
62
+ help = true;
63
+ break;
64
+ case '--watch':
65
+ bare();
66
+ watch = true;
67
+ break;
68
+ case '--root':
69
+ root = resolve(flagValue());
70
+ break;
71
+ case '--source-dir':
72
+ sourceDir = flagValue();
73
+ break;
74
+ case '--config':
75
+ config = flagValue();
76
+ break;
77
+ default:
78
+ throw new Error(`Unknown argument '${argument}'.`);
79
+ }
80
+ }
81
+ if (watch && command !== 'prepare')
82
+ throw new Error('--watch is only supported by mica prepare.');
83
+ return {
84
+ command,
85
+ help,
86
+ watch,
87
+ root: resolve(root),
88
+ ...(sourceDir === undefined ? {} : { sourceDir }),
89
+ ...(config === undefined ? {} : { config }),
90
+ };
91
+ }
92
+ /** Runs one SDK command and returns its exit code. */
93
+ export async function runCli(argv, streams = { out: process.stdout, err: process.stderr }) {
94
+ let options;
95
+ try {
96
+ options = parseCliArguments(argv);
97
+ }
98
+ catch (error) {
99
+ streams.err.write(`mica: ${messageOf(error)}\n\n${USAGE}\n`);
100
+ return 1;
101
+ }
102
+ if (options.help) {
103
+ streams.out.write(`${USAGE}\n`);
104
+ return 0;
105
+ }
106
+ try {
107
+ const project = await loadProjectConfig({
108
+ root: options.root,
109
+ ...(options.config === undefined ? {} : { config: options.config }),
110
+ });
111
+ const configured = {
112
+ ...project,
113
+ config: {
114
+ ...project.config,
115
+ ...(options.sourceDir === undefined ? {} : { sourceDir: options.sourceDir }),
116
+ },
117
+ };
118
+ if (options.command === 'build') {
119
+ const { buildProject } = await import('./project.js');
120
+ await buildProject(configured);
121
+ streams.out.write('mica build: complete\n');
122
+ return 0;
123
+ }
124
+ const preparation = { root: project.root, sourceDir: configured.config.sourceDir ?? 'src' };
125
+ return options.watch
126
+ ? await watchLoop(preparation, streams)
127
+ : await prepareOnce(preparation, streams);
128
+ }
129
+ catch (error) {
130
+ streams.err.write(`mica ${options.command}: ${messageOf(error)}\n`);
131
+ return 1;
132
+ }
133
+ }
134
+ async function prepareOnce(options, streams) {
135
+ try {
136
+ report(await prepare({ root: options.root, sourceDir: options.sourceDir }), streams);
137
+ return 0;
138
+ }
139
+ catch (error) {
140
+ streams.err.write(`mica prepare: ${messageOf(error)}\n`);
141
+ return 1;
142
+ }
143
+ }
144
+ function watchLoop(options, streams) {
145
+ return new Promise((settle) => {
146
+ let watcher;
147
+ try {
148
+ watcher = watchDevices({
149
+ root: options.root,
150
+ sourceDir: options.sourceDir,
151
+ onPrepare: (files) => report(files, streams),
152
+ onError: (error) => streams.err.write(`mica prepare: ${messageOf(error)}\n`),
153
+ });
154
+ }
155
+ catch (error) {
156
+ streams.err.write(`mica prepare: ${messageOf(error)}\n`);
157
+ settle(1);
158
+ return;
159
+ }
160
+ let closing = false;
161
+ const stop = () => {
162
+ // A second signal must not wait for a regeneration that may already be stuck.
163
+ if (closing)
164
+ process.exit(0);
165
+ closing = true;
166
+ process.off('SIGINT', stop);
167
+ process.off('SIGTERM', stop);
168
+ void watcher.dispose().then(() => settle(0), (error) => {
169
+ streams.err.write(`mica prepare: ${messageOf(error)}\n`);
170
+ settle(1);
171
+ });
172
+ };
173
+ process.on('SIGINT', stop);
174
+ process.on('SIGTERM', stop);
175
+ void watcher.ready.then(() => {
176
+ if (closing)
177
+ return;
178
+ const sourceRoot = resolve(options.root, options.sourceDir);
179
+ streams.out.write(`mica prepare: watching ${sourceRoot} for changes\n`);
180
+ });
181
+ });
182
+ }
183
+ function report(files, streams) {
184
+ for (const file of files)
185
+ streams.out.write(`${relative(process.cwd(), file)}\n`);
186
+ if (!files.length) {
187
+ streams.out.write('mica prepare: no device definitions found\n');
188
+ return;
189
+ }
190
+ const plural = files.length === 1 ? 'declaration' : 'declarations';
191
+ streams.out.write(`mica prepare: ${files.length} device ${plural} ready\n`);
192
+ }
193
+ function messageOf(error) {
194
+ return error instanceof Error ? error.message : String(error);
195
+ }
196
+ function isMainModule() {
197
+ const entry = process.argv[1];
198
+ if (entry === undefined)
199
+ return false;
200
+ try {
201
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }
207
+ if (isMainModule()) {
208
+ void runCli(process.argv.slice(2)).then((code) => {
209
+ process.exitCode = code;
210
+ });
211
+ }
@@ -0,0 +1,16 @@
1
+ export interface MicaConfig {
2
+ readonly sourceDir?: string;
3
+ readonly outDir?: string;
4
+ readonly base?: string;
5
+ readonly alias?: Readonly<Record<string, string>>;
6
+ }
7
+ export declare function defineConfig(config: MicaConfig): MicaConfig;
8
+ export declare function findProjectConfig(root?: string, config?: string): Promise<string>;
9
+ export declare function loadProjectConfig(options?: {
10
+ root?: string;
11
+ config?: string;
12
+ }): Promise<{
13
+ root: string;
14
+ configFile: string;
15
+ config: MicaConfig;
16
+ }>;
@@ -0,0 +1,92 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
+ import { loadConfigFromFile } from 'vite';
4
+ export function defineConfig(config) {
5
+ return config;
6
+ }
7
+ const CONFIG_FILE = 'mica.config.ts';
8
+ async function isFile(file) {
9
+ try {
10
+ return (await stat(file)).isFile();
11
+ }
12
+ catch (error) {
13
+ const code = error.code;
14
+ if (code === 'ENOENT' || code === 'ENOTDIR')
15
+ return false;
16
+ throw error;
17
+ }
18
+ }
19
+ export async function findProjectConfig(root = process.cwd(), config) {
20
+ const startingRoot = resolve(root);
21
+ if (config !== undefined) {
22
+ const file = resolve(startingRoot, config);
23
+ if (basename(file) !== CONFIG_FILE) {
24
+ throw new Error(`Mica project config must be named ${CONFIG_FILE}: ${file}`);
25
+ }
26
+ if (!(await isFile(file))) {
27
+ throw new Error(`Mica project config is not a file or does not exist: ${file}`);
28
+ }
29
+ return file;
30
+ }
31
+ let directory = startingRoot;
32
+ while (true) {
33
+ const file = join(directory, CONFIG_FILE);
34
+ if (await isFile(file))
35
+ return file;
36
+ const parent = dirname(directory);
37
+ if (parent === directory) {
38
+ throw new Error(`No ${CONFIG_FILE} project marker found from ${startingRoot} upward`);
39
+ }
40
+ directory = parent;
41
+ }
42
+ }
43
+ function invalid(field, expected) {
44
+ throw new Error(`Invalid Mica config ${field}: expected ${expected}`);
45
+ }
46
+ function plainObject(value, field) {
47
+ if (value === null ||
48
+ typeof value !== 'object' ||
49
+ (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
50
+ return invalid(field, 'a plain object');
51
+ }
52
+ return value;
53
+ }
54
+ function knownKeys(value, keys, field) {
55
+ for (const key of Reflect.ownKeys(value)) {
56
+ if (typeof key !== 'string' || !keys.includes(key)) {
57
+ invalid(`${field}.${String(key)}`, 'a supported config key');
58
+ }
59
+ }
60
+ }
61
+ function validateConfig(value) {
62
+ const config = plainObject(value, 'default export');
63
+ knownKeys(config, ['sourceDir', 'outDir', 'base', 'alias'], 'default export');
64
+ for (const field of ['sourceDir', 'outDir', 'base']) {
65
+ const entry = config[field];
66
+ if (entry !== undefined && (typeof entry !== 'string' || entry.trim().length === 0)) {
67
+ invalid(field, 'a nonempty string');
68
+ }
69
+ }
70
+ if (config.alias !== undefined) {
71
+ const aliases = plainObject(config.alias, 'alias');
72
+ for (const key of Reflect.ownKeys(aliases)) {
73
+ if (typeof key !== 'string' || typeof aliases[key] !== 'string') {
74
+ invalid(`alias.${String(key)}`, 'a string');
75
+ }
76
+ }
77
+ }
78
+ return {
79
+ ...config,
80
+ sourceDir: config.sourceDir ?? 'src',
81
+ outDir: config.outDir ?? 'dist',
82
+ base: config.base ?? '/',
83
+ };
84
+ }
85
+ export async function loadProjectConfig(options = {}) {
86
+ const configFile = await findProjectConfig(options.root, options.config);
87
+ const root = dirname(configFile);
88
+ const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' }, configFile, root, 'silent', undefined, 'bundle');
89
+ if (!loaded)
90
+ throw new Error(`Unable to load Mica project config: ${configFile}`);
91
+ return { root, configFile, config: validateConfig(loaded.config) };
92
+ }
@@ -0,0 +1,14 @@
1
+ import { type AliasOptions } from 'vite';
2
+ export interface ContentBuildOptions {
3
+ readonly root: string;
4
+ readonly sourceDir?: string;
5
+ readonly aliases?: AliasOptions;
6
+ readonly minify?: boolean;
7
+ }
8
+ export interface ContentFile {
9
+ readonly fileName: string;
10
+ readonly source: string;
11
+ }
12
+ export declare function modelExpression(value: unknown): string;
13
+ /** Builds each registration separately so no shared chunk or runtime import escapes. */
14
+ export declare function buildContent(options: ContentBuildOptions): Promise<readonly ContentFile[]>;