localmono 0.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.
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ <div align="center">
2
+
3
+ # mono
4
+
5
+ **🚀 Zero-intrusion Monorepo Development Tool**
6
+
7
+ **Use TypeScript source code directly, no build required, no project restructuring needed**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/mono.svg?style=flat-square)](https://www.npmjs.com/package/mono)
10
+ [![license](https://img.shields.io/npm/l/mono.svg?style=flat-square)](./LICENSE)
11
+ [![node version](https://img.shields.io/node/v/mono.svg?style=flat-square)](https://nodejs.org)
12
+
13
+ [English](./README.md) · [简体中文](./README.zh-CN.md)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## � Why mono?
20
+
21
+ ### Zero Intrusion - Keep Your Project Clean
22
+
23
+ **Are you frustrated with:**
24
+ - ❌ Converting your npm project to pnpm workspace
25
+ - ❌ Dealing with `workspace:*` protocol complexities
26
+ - ❌ Creating workspace configuration files
27
+ - ❌ Restructuring your project for monorepo tools
28
+
29
+ **With mono:**
30
+ - ✅ **Zero Configuration** - No new files, works with any npm project
31
+ - ✅ **Non-Invasive** - Your project stays a clean npm project
32
+ - ✅ **Drop-in Replacement** - Use like `node` or `tsx`: `mono xxx.ts`
33
+ - ✅ **Auto Discovery** - Automatically finds local packages
34
+ - ✅ **Simple Config** - Optional `local` field in `package.json`, defaults to `./src/index.ts`
35
+
36
+ ### Traditional vs mono
37
+
38
+ | Approach | Steps Required |
39
+ |----------|---------------|
40
+ | **pnpm workspace** | 1. Convert to pnpm<br/>2. Create `pnpm-workspace.yaml`<br/>3. Use `workspace:*` protocol<br/>4. Restructure project<br/>5. Build packages |
41
+ | **npm link** | 1. Link each package manually<br/>2. Build packages<br/>3. Rebuild on every change |
42
+ | **mono** ✨ | `mono ./src/index.ts` - **that's it!** |
43
+
44
+ ---
45
+
46
+ ## ✨ Key Features
47
+
48
+ - 🎯 **Zero Intrusion** - No project restructuring, no configuration files
49
+ - 🔍 **Auto Discovery** - Recursively scans and finds all local packages
50
+ - 📦 **Package Manager Agnostic** - Works with npm, yarn, pnpm, bun
51
+ - ⚡️ **Instant Reload** - Changes take effect immediately
52
+ - 🛠️ **Zero Config** - Default `./src/index.ts`, optional `local` field for custom paths
53
+ - 🌐 **ESM Only** - Built for modern JavaScript (`type: "module"`)
54
+ - 📝 **Config File** - Auto-generates `.mono/monoConfig.json` for debugging
55
+
56
+ ---
57
+
58
+ ## 📦 Installation
59
+
60
+ ### Global Installation (Recommended)
61
+
62
+ ```bash
63
+ npm install -g mono
64
+ ```
65
+
66
+ ### Requirements
67
+
68
+ - **Node.js** >= 18.19.0
69
+ - **ESM Projects** - Your `package.json` must have `"type": "module"`
70
+
71
+ ---
72
+
73
+ ## 🚀 Quick Start
74
+
75
+ ### Basic Usage - Just Like `node` or `tsx`
76
+
77
+ ```bash
78
+ # Run TypeScript file directly
79
+ mono ./src/index.ts
80
+
81
+ # Run with parameters
82
+ mono ./src/index.ts --port 3000
83
+
84
+ # Enable debug mode
85
+ mono ./src/index.ts debug
86
+ ```
87
+
88
+ ### Use in package.json
89
+
90
+ ```json
91
+ {
92
+ "type": "module",
93
+ "scripts": {
94
+ "dev": "mono ./node_modules/vite/bin/vite.js",
95
+ "start": "mono ./src/index.ts"
96
+ }
97
+ }
98
+ ```
99
+
100
+ **That's it!** No workspace configuration, no project restructuring.
101
+
102
+ ---
103
+
104
+ ## 📚 How It Works
105
+
106
+ ### 1. Auto Package Discovery
107
+
108
+ ```
109
+ Find farthest project root upward (.idea/.vscode/.git/package.json)
110
+ └── Recursive Scan
111
+ └── Find all package.json
112
+ └── Register by "name" field
113
+ ```
114
+
115
+ ### 2. Import Interception
116
+
117
+ ```javascript
118
+ // Your code
119
+ import { utils } from 'my-utils'
120
+
121
+ // mono automatically redirects to source
122
+ // → /path/to/my-utils/src/index.ts
123
+ ```
124
+
125
+ ### 3. Default Convention
126
+
127
+ - **Default Entry**: `./src/index.ts` for all packages
128
+ - **No Config Needed**: Works out of the box
129
+
130
+ ### 4. Optional Custom Entry
131
+
132
+ If you need a different entry point, add `local` field:
133
+
134
+ ```json
135
+ {
136
+ "name": "my-package",
137
+ "local": "./src/main.ts"
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## ⚙️ Configuration
144
+
145
+ ### Zero Config (Recommended)
146
+
147
+ Just use default `./src/index.ts`:
148
+
149
+ ```json
150
+ {
151
+ "name": "my-package",
152
+ "type": "module"
153
+ // No extra config needed!
154
+ }
155
+ ```
156
+
157
+ ### Custom Entry Point (Optional)
158
+
159
+ Add `local` field for custom paths:
160
+
161
+ ```json
162
+ {
163
+ "name": "@my-org/utils",
164
+ "type": "module",
165
+ "local": "./src/custom-entry.ts"
166
+ }
167
+ ```
168
+
169
+ **That's all the configuration you need!** Your project remains a standard npm project.
170
+
171
+ ---
172
+
173
+ ## 🐛 Debugging
174
+
175
+ Five debug formats supported, parameter can be anywhere:
176
+
177
+ ```bash
178
+ # Before file
179
+ mono --debug ./src/index.ts
180
+ mono -debug ./src/index.ts
181
+ mono -d ./src/index.ts
182
+
183
+ # After file (Recommended)
184
+ mono ./src/index.ts debug
185
+ mono ./src/index.ts d
186
+ ```
187
+
188
+ Or use environment variable:
189
+
190
+ ```bash
191
+ # Linux/macOS
192
+ MONO_DEBUG=1 mono ./src/index.ts
193
+
194
+ # Windows PowerShell
195
+ $env:MONO_DEBUG='1'; mono ./src/index.ts
196
+ ```
197
+
198
+ Debug logs output to:
199
+ - Console
200
+ - `mono-debug.log` file
201
+
202
+ ---
203
+
204
+ ## 📋 Common Use Cases
205
+
206
+ ### With Vite
207
+
208
+ ```json
209
+ {
210
+ "type": "module",
211
+ "scripts": {
212
+ "dev": "mono ./node_modules/vite/bin/vite.js"
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### With Webpack
218
+
219
+ ```json
220
+ {
221
+ "type": "module",
222
+ "scripts": {
223
+ "dev": "mono ./node_modules/webpack/bin/webpack.js serve"
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### Custom Build Scripts
229
+
230
+ ```json
231
+ {
232
+ "type": "module",
233
+ "scripts": {
234
+ "build": "mono ./scripts/build.ts",
235
+ "codegen": "mono ./scripts/codegen.ts"
236
+ }
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## ❓ FAQ
243
+
244
+ ### Q: Why not use pnpm workspace?
245
+
246
+ **A:** pnpm workspace requires:
247
+ - Converting to pnpm
248
+ - Creating `pnpm-workspace.yaml`
249
+ - Using `workspace:*` protocol
250
+ - Restructuring project
251
+
252
+ **mono** requires: nothing! Keep your npm project as-is.
253
+
254
+ ### Q: Why not use npm link?
255
+
256
+ **A:**
257
+ | Feature | mono | npm link |
258
+ |---------|------|----------|
259
+ | Setup | ✅ None | ❌ Manual per package |
260
+ | TypeScript | ✅ Direct source | ❌ Requires build |
261
+ | Hot Reload | ✅ Instant | ❌ Rebuild needed |
262
+
263
+ ### Q: Does it modify my package.json?
264
+
265
+ **A:** No! The `local` field is optional. Your package.json stays clean.
266
+
267
+ ### Q: What if I don't add `local` field?
268
+
269
+ **A:** It defaults to `./src/index.ts`. No configuration needed!
270
+
271
+ ### Q: Will it affect production builds?
272
+
273
+ **A:** No. `mono` is for development only. Production uses `node_modules`.
274
+
275
+ ---
276
+
277
+ ## 🔧 Technical Details
278
+
279
+ - **ESM Loader Hooks** - Uses Node.js native module resolution API
280
+ - **TypeScript Compiler** - Powered by `tsx` for latest syntax support
281
+ - **Zero Dependencies** - Only 2 runtime deps: `tsx` and `cross-spawn`
282
+
283
+ ### ESM Only
284
+
285
+ `mono` requires `"type": "module"` in your `package.json`:
286
+
287
+ ```json
288
+ {
289
+ "type": "module"
290
+ }
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 📄 License
296
+
297
+ MIT © [alamhubb](https://github.com/alamhubb)
298
+
299
+ ---
300
+
301
+ ## 🔗 Related
302
+
303
+ - [vite-plugin-mono](../vite-plugin-mono) - Vite plugin for browser-side source development
304
+
305
+ ---
306
+
307
+ <div align="center">
308
+
309
+ Made with ❤️ by [alamhubb](https://github.com/alamhubb)
310
+
311
+ [Report Bug](https://github.com/alamhubb/mono/issues) · [Request Feature](https://github.com/alamhubb/mono/issues)
312
+
313
+ </div>
@@ -0,0 +1,313 @@
1
+ <div align="center">
2
+
3
+ # mono
4
+
5
+ **🚀 零侵入式 Monorepo 开发工具**
6
+
7
+ **直接使用 TypeScript 源码开发,无需构建,无需改造项目**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/mono.svg?style=flat-square)](https://www.npmjs.com/package/mono)
10
+ [![license](https://img.shields.io/npm/l/mono.svg?style=flat-square)](./LICENSE)
11
+ [![node version](https://img.shields.io/node/v/mono.svg?style=flat-square)](https://nodejs.org)
12
+
13
+ [English](./README.md) · [简体中文](./README.zh-CN.md)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## 💡 为什么选择 mono?
20
+
21
+ ### 零侵入 - 保持项目干净
22
+
23
+ **你是否曾为这些烦恼:**
24
+ - ❌ 需要将 npm 项目转换为 pnpm workspace
25
+ - ❌ 处理 `workspace:*` 协议的复杂性
26
+ - ❌ 创建各种 workspace 配置文件
27
+ - ❌ 为了 monorepo 工具而重构项目结构
28
+
29
+ **使用 mono:**
30
+ - ✅ **零配置** - 不创建任何新文件,兼容任何 npm 项目
31
+ - ✅ **无侵入** - 你的项目依然是一个干净的 npm 项目
32
+ - ✅ **即插即用** - 像使用 `node` 或 `tsx` 一样使用:`mono xxx.ts`
33
+ - ✅ **自动发现** - 自动查找本地包
34
+ - ✅ **简单配置** - 可选的 `local` 字段,默认 `./src/index.ts`
35
+
36
+ ### 传统方式 vs mono
37
+
38
+ | 方式 | 需要的步骤 |
39
+ |------|-----------|
40
+ | **pnpm workspace** | 1. 转换为 pnpm<br/>2. 创建 `pnpm-workspace.yaml`<br/>3. 使用 `workspace:*` 协议<br/>4. 重构项目结构<br/>5. 构建包 |
41
+ | **npm link** | 1. 手动链接每个包<br/>2. 构建包<br/>3. 每次修改都要重新构建 |
42
+ | **mono** ✨ | `mono ./src/index.ts` - **就这么简单!** |
43
+
44
+ ---
45
+
46
+ ## ✨ 核心特性
47
+
48
+ - 🎯 **零侵入** - 无需重构项目,无需配置文件
49
+ - 🔍 **自动发现** - 递归扫描并找到所有本地包
50
+ - 📦 **包管理器无关** - 兼容 npm、yarn、pnpm、bun
51
+ - ⚡️ **即时加载** - 修改立即生效
52
+ - 🛠️ **零配置** - 默认 `./src/index.ts`,可选 `local` 字段自定义路径
53
+ - 🌐 **仅支持 ESM** - 为现代 JavaScript 设计(`type: "module"`)
54
+ - 📝 **配置文件** - 自动生成 `.mono/monoConfig.json` 用于调试
55
+
56
+ ---
57
+
58
+ ## 📦 安装
59
+
60
+ ### 全局安装(推荐)
61
+
62
+ ```bash
63
+ npm install -g mono
64
+ ```
65
+
66
+ ### 环境要求
67
+
68
+ - **Node.js** >= 18.19.0
69
+ - **ESM 项目** - 你的 `package.json` 必须包含 `"type": "module"`
70
+
71
+ ---
72
+
73
+ ## 🚀 快速开始
74
+
75
+ ### 基本用法 - 就像 `node` 或 `tsx` 一样
76
+
77
+ ```bash
78
+ # 直接运行 TypeScript 文件
79
+ mono ./src/index.ts
80
+
81
+ # 带参数运行
82
+ mono ./src/index.ts --port 3000
83
+
84
+ # 开启调试模式
85
+ mono ./src/index.ts debug
86
+ ```
87
+
88
+ ### 在 package.json 中使用
89
+
90
+ ```json
91
+ {
92
+ "type": "module",
93
+ "scripts": {
94
+ "dev": "mono ./node_modules/vite/bin/vite.js",
95
+ "start": "mono ./src/index.ts"
96
+ }
97
+ }
98
+ ```
99
+
100
+ **就这样!** 无需 workspace 配置,无需重构项目。
101
+
102
+ ---
103
+
104
+ ## 📚 工作原理
105
+
106
+ ### 1. 自动包发现
107
+
108
+ ```
109
+ 直线向上查找距离最远的项目根目录 (.idea/.vscode/.git/package.json)
110
+ └── 递归扫描
111
+ └── 查找所有 package.json
112
+ └── 根据 "name" 字段注册
113
+ ```
114
+
115
+ ### 2. 导入拦截
116
+
117
+ ```javascript
118
+ // 你的代码
119
+ import { utils } from 'my-utils'
120
+
121
+ // mono 自动重定向到源码
122
+ // → /path/to/my-utils/src/index.ts
123
+ ```
124
+
125
+ ### 3. 默认约定
126
+
127
+ - **默认入口**:所有包默认 `./src/index.ts`
128
+ - **无需配置**:开箱即用
129
+
130
+ ### 4. 可选的自定义入口
131
+
132
+ 如果需要不同的入口点,添加 `local` 字段:
133
+
134
+ ```json
135
+ {
136
+ "name": "my-package",
137
+ "local": "./src/main.ts"
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## ⚙️ 配置
144
+
145
+ ### 零配置(推荐)
146
+
147
+ 直接使用默认的 `./src/index.ts`:
148
+
149
+ ```json
150
+ {
151
+ "name": "my-package",
152
+ "type": "module"
153
+ // 无需额外配置!
154
+ }
155
+ ```
156
+
157
+ ### 自定义入口(可选)
158
+
159
+ 添加 `local` 字段指定自定义路径:
160
+
161
+ ```json
162
+ {
163
+ "name": "@my-org/utils",
164
+ "type": "module",
165
+ "local": "./src/custom-entry.ts"
166
+ }
167
+ ```
168
+
169
+ **这就是你需要的全部配置!** 你的项目依然是标准的 npm 项目。
170
+
171
+ ---
172
+
173
+ ## 🐛 调试
174
+
175
+ 支持 5 种调试格式,参数可以放在任意位置:
176
+
177
+ ```bash
178
+ # 参数在文件前面
179
+ mono --debug ./src/index.ts
180
+ mono -debug ./src/index.ts
181
+ mono -d ./src/index.ts
182
+
183
+ # 参数在文件后面(推荐)
184
+ mono ./src/index.ts debug
185
+ mono ./src/index.ts d
186
+ ```
187
+
188
+ 或使用环境变量:
189
+
190
+ ```bash
191
+ # Linux/macOS
192
+ MONO_DEBUG=1 mono ./src/index.ts
193
+
194
+ # Windows PowerShell
195
+ $env:MONO_DEBUG='1'; mono ./src/index.ts
196
+ ```
197
+
198
+ 调试日志输出到:
199
+ - 控制台
200
+ - `mono-debug.log` 文件
201
+
202
+ ---
203
+
204
+ ## 📋 常见使用场景
205
+
206
+ ### 配合 Vite
207
+
208
+ ```json
209
+ {
210
+ "type": "module",
211
+ "scripts": {
212
+ "dev": "mono ./node_modules/vite/bin/vite.js"
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### 配合 Webpack
218
+
219
+ ```json
220
+ {
221
+ "type": "module",
222
+ "scripts": {
223
+ "dev": "mono ./node_modules/webpack/bin/webpack.js serve"
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### 自定义构建脚本
229
+
230
+ ```json
231
+ {
232
+ "type": "module",
233
+ "scripts": {
234
+ "build": "mono ./scripts/build.ts",
235
+ "codegen": "mono ./scripts/codegen.ts"
236
+ }
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## ❓ 常见问题
243
+
244
+ ### Q: 为什么不用 pnpm workspace?
245
+
246
+ **A:** pnpm workspace 需要:
247
+ - 转换为 pnpm
248
+ - 创建 `pnpm-workspace.yaml`
249
+ - 使用 `workspace:*` 协议
250
+ - 重构项目结构
251
+
252
+ **mono** 需要:什么都不需要!保持你的 npm 项目原样。
253
+
254
+ ### Q: 为什么不用 npm link?
255
+
256
+ **A:**
257
+ | 特性 | mono | npm link |
258
+ |------|------|----------|
259
+ | 设置 | ✅ 无需设置 | ❌ 每个包都要手动链接 |
260
+ | TypeScript | ✅ 直接使用源码 | ❌ 需要构建 |
261
+ | 热更新 | ✅ 即时生效 | ❌ 需要重新构建 |
262
+
263
+ ### Q: 它会修改我的 package.json 吗?
264
+
265
+ **A:** 不会!`local` 字段是可选的。你的 package.json 保持干净。
266
+
267
+ ### Q: 如果我不添加 `local` 字段呢?
268
+
269
+ **A:** 默认使用 `./src/index.ts`。无需任何配置!
270
+
271
+ ### Q: 会影响生产构建吗?
272
+
273
+ **A:** 不会。`mono` 仅用于开发。生产环境使用 `node_modules`。
274
+
275
+ ---
276
+
277
+ ## 🔧 技术细节
278
+
279
+ - **ESM Loader Hooks** - 使用 Node.js 原生模块解析 API
280
+ - **TypeScript 编译器** - 基于 `tsx`,支持最新语法
281
+ - **零依赖** - 仅 2 个运行时依赖:`tsx` 和 `cross-spawn`
282
+
283
+ ### 仅支持 ESM
284
+
285
+ `mono` 要求 `package.json` 中包含 `"type": "module"`:
286
+
287
+ ```json
288
+ {
289
+ "type": "module"
290
+ }
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 📄 License
296
+
297
+ MIT © [alamhubb](https://github.com/alamhubb)
298
+
299
+ ---
300
+
301
+ ## 🔗 相关项目
302
+
303
+ - [vite-plugin-mono](../vite-plugin-mono) - 用于浏览器端源码开发的 Vite 插件
304
+
305
+ ---
306
+
307
+ <div align="center">
308
+
309
+ Made with ❤️ by [alamhubb](https://github.com/alamhubb)
310
+
311
+ [报告 Bug](https://github.com/alamhubb/mono/issues) · [请求功能](https://github.com/alamhubb/mono/issues)
312
+
313
+ </div>
package/bin/mono.mjs ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mono CLI 入口
5
+ *
6
+ * 命令:
7
+ * - mono <file> 运行 TypeScript 文件,注入自定义 loader
8
+ * - mono --debug <file> 开启调试模式运行
9
+ * - mono i 递归安装所有嵌套 monorepo 的依赖
10
+ *
11
+ * loader.mjs 会自动:
12
+ * 1. 注册 tsx ESM loader(TypeScript 支持)
13
+ * 2. 注册 mono resolve hooks(包名拦截)
14
+ */
15
+
16
+ import spawn from 'cross-spawn';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ /**
25
+ * 获取 loader 文件路径(file:// URL 格式)
26
+ */
27
+ function getLoaderUrl() {
28
+ const loaderPath = join(__dirname, '..', 'src', 'loader.mjs');
29
+ return pathToFileURL(loaderPath).href;
30
+ }
31
+
32
+ /**
33
+ * 递归查找所有嵌套的 monorepo 目录
34
+ */
35
+ function findAllMonorepos(startDir) {
36
+ const monorepos = [];
37
+
38
+ function scanDir(dir) {
39
+ const pkgPath = join(dir, 'package.json');
40
+
41
+ if (existsSync(pkgPath)) {
42
+ try {
43
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
44
+
45
+ // 如果有 workspaces 配置,这是一个 monorepo
46
+ if (pkg.workspaces) {
47
+ monorepos.push(dir);
48
+
49
+ // 继续查找嵌套的 workspace
50
+ let patterns = [];
51
+ if (Array.isArray(pkg.workspaces)) {
52
+ patterns = pkg.workspaces;
53
+ } else if (pkg.workspaces?.packages) {
54
+ patterns = pkg.workspaces.packages;
55
+ }
56
+
57
+ for (const pattern of patterns) {
58
+ const dirs = findMatchingDirs(dir, pattern);
59
+ for (const subDir of dirs) {
60
+ scanDir(subDir);
61
+ }
62
+ }
63
+ }
64
+ } catch {
65
+ // 忽略解析错误
66
+ }
67
+ }
68
+ }
69
+
70
+ scanDir(startDir);
71
+ return monorepos;
72
+ }
73
+
74
+ /**
75
+ * 查找匹配 pattern 的目录
76
+ */
77
+ function findMatchingDirs(root, pattern) {
78
+ const dirs = [];
79
+
80
+ if (pattern.endsWith('/*')) {
81
+ const baseDir = join(root, pattern.slice(0, -2));
82
+ if (existsSync(baseDir)) {
83
+ const entries = readdirSync(baseDir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
86
+ dirs.push(join(baseDir, entry.name));
87
+ }
88
+ }
89
+ }
90
+ } else if (pattern.endsWith('/**')) {
91
+ const baseDir = join(root, pattern.slice(0, -3));
92
+ findDirsRecursive(baseDir, dirs);
93
+ } else {
94
+ const dir = join(root, pattern);
95
+ if (existsSync(dir)) {
96
+ dirs.push(dir);
97
+ }
98
+ }
99
+
100
+ return dirs;
101
+ }
102
+
103
+ /**
104
+ * 递归查找目录
105
+ */
106
+ function findDirsRecursive(dir, result) {
107
+ if (!existsSync(dir)) return;
108
+
109
+ const entries = readdirSync(dir, { withFileTypes: true });
110
+ for (const entry of entries) {
111
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
112
+ const subDir = join(dir, entry.name);
113
+ result.push(subDir);
114
+ findDirsRecursive(subDir, result);
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 在指定目录执行 npm install
121
+ */
122
+ async function runNpmInstall(dir) {
123
+ return new Promise((resolve, reject) => {
124
+ console.log(`\n[mono i] Installing in: ${dir}`);
125
+
126
+ const child = spawn('npm', ['install'], {
127
+ cwd: dir,
128
+ stdio: 'inherit'
129
+ });
130
+
131
+ child.on('close', (code) => {
132
+ if (code === 0) {
133
+ resolve();
134
+ } else {
135
+ reject(new Error(`npm install failed with code ${code}`));
136
+ }
137
+ });
138
+
139
+ child.on('error', reject);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * mono i - 递归安装所有嵌套 monorepo 的依赖
145
+ */
146
+ async function installCommand() {
147
+ const cwd = process.cwd();
148
+ console.log('[mono i] 扫描嵌套 monorepo...');
149
+
150
+ const monorepos = findAllMonorepos(cwd);
151
+
152
+ if (monorepos.length === 0) {
153
+ console.log('[mono i] 未找到任何 monorepo');
154
+ return;
155
+ }
156
+
157
+ console.log(`[mono i] 找到 ${monorepos.length} 个 monorepo:`);
158
+ for (const dir of monorepos) {
159
+ console.log(` - ${dir}`);
160
+ }
161
+
162
+ // 从外到内安装(父级先安装)
163
+ for (const dir of monorepos) {
164
+ try {
165
+ await runNpmInstall(dir);
166
+ } catch (err) {
167
+ console.error(`[mono i] 安装失败: ${dir}`, err.message);
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ console.log('\n[mono i] ✅ 所有 monorepo 安装完成!');
173
+ }
174
+
175
+ /**
176
+ * 运行 TypeScript 文件
177
+ */
178
+ function runCommand(args, debug = false) {
179
+ const loaderUrl = getLoaderUrl();
180
+
181
+ const nodeArgs = [
182
+ `--import=${loaderUrl}`,
183
+ ...args
184
+ ];
185
+
186
+ // 如果开启 debug,设置环境变量
187
+ const env = { ...process.env };
188
+ if (debug) {
189
+ env.MONO_DEBUG = '1';
190
+ }
191
+
192
+ const child = spawn('node', nodeArgs, {
193
+ stdio: 'inherit',
194
+ env
195
+ });
196
+
197
+ child.on('close', (code) => {
198
+ process.exit(code ?? 0);
199
+ });
200
+
201
+ child.on('error', (err) => {
202
+ console.error('mono: 无法启动 node 进程', err.message);
203
+ process.exit(1);
204
+ });
205
+ }
206
+
207
+ /**
208
+ * 主函数
209
+ */
210
+ async function main() {
211
+ const args = process.argv.slice(2);
212
+
213
+ // 检查调试参数(支持多种格式,可在任意位置)
214
+ let debug = false;
215
+ const filteredArgs = [];
216
+
217
+ for (const arg of args) {
218
+ if (arg === '--debug' || arg === '-debug' || arg === '-d' || arg === 'debug' || arg === 'd') {
219
+ debug = true;
220
+ } else {
221
+ filteredArgs.push(arg);
222
+ }
223
+ }
224
+
225
+ // 检查是否是 install 命令
226
+ if (filteredArgs[0] === 'i' || filteredArgs[0] === 'install') {
227
+ await installCommand();
228
+ return;
229
+ }
230
+
231
+ // 否则运行 TypeScript 文件
232
+ runCommand(filteredArgs, debug);
233
+ }
234
+
235
+ main();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "localmono",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for monorepo - auto-discover local packages and use source code in Node.js",
5
+ "type": "module",
6
+ "bin": {
7
+ "mono": "./bin/mono.mjs"
8
+ },
9
+ "main": "./src/hooks.mjs",
10
+ "exports": {
11
+ ".": "./src/hooks.mjs",
12
+ "./hooks": "./src/hooks.mjs",
13
+ "./loader": "./src/loader.mjs"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "test": "node test/test.mjs"
21
+ },
22
+ "keywords": [
23
+ "monorepo",
24
+ "cli",
25
+ "node",
26
+ "loader",
27
+ "workspace",
28
+ "esm",
29
+ "typescript"
30
+ ],
31
+ "author": "alamhubb",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18.19.0"
35
+ },
36
+ "dependencies": {
37
+ "cross-spawn": "^7.0.6",
38
+ "tsx": "^4.21.0"
39
+ }
40
+ }
package/src/hooks.mjs ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * ESM Loader Hooks - 被 register() 加载的 hooks 模块
3
+ * 拦截模块解析,对项目中的本地包应用源码入口
4
+ *
5
+ * 新逻辑:
6
+ * - 向上查找最顶层的包含 .idea / .vscode / package.json 的目录作为根目录
7
+ * - 从根目录递归向下查找所有有 package.json 的项目
8
+ * - 根据 package.json 的 name 字段注册
9
+ * - 默认使用 src/index.ts,如果配置了 local 字段则使用配置的
10
+ *
11
+ * 注意:此文件使用纯 JavaScript,以便 Node.js 原生加载
12
+ */
13
+
14
+ import { readFileSync, existsSync, readdirSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
15
+ import { resolve as pathResolve, dirname, join } from 'node:path';
16
+ import { pathToFileURL } from 'node:url';
17
+
18
+ // 根目录标识符列表
19
+ const ROOT_MARKERS = ['.idea', '.vscode', '.git', 'package.json'];
20
+
21
+ // 默认源码入口
22
+ const DEFAULT_ENTRY = './src/index.ts';
23
+
24
+ // 调试日志文件
25
+ const DEBUG_LOG = join(process.cwd(), 'mono-debug.log');
26
+
27
+ // 是否启用调试日志(设置环境变量 MONO_DEBUG=1 开启)
28
+ const DEBUG_ENABLED = process.env.MONO_DEBUG === '1';
29
+
30
+ function debugLog(msg) {
31
+ if (!DEBUG_ENABLED) return;
32
+ // 同时输出到控制台和文件
33
+ console.log(msg);
34
+ try {
35
+ appendFileSync(DEBUG_LOG, `${new Date().toISOString()} ${msg}\n`);
36
+ } catch { }
37
+ }
38
+
39
+ if (DEBUG_ENABLED) {
40
+ debugLog('[mono hooks] hooks.mjs 已加载');
41
+ }
42
+
43
+ // 缓存:workspace 包信息
44
+ let workspacePackages = null;
45
+
46
+ /**
47
+ * 检查目录是否包含根目录标识
48
+ */
49
+ function hasRootMarker(dir) {
50
+ for (const marker of ROOT_MARKERS) {
51
+ if (existsSync(join(dir, marker))) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /**
59
+ * 向上查找最顶层的根目录
60
+ * 找到最顶层的包含 .idea / .vscode / package.json 的目录
61
+ */
62
+ function findProjectRoot(startDir) {
63
+ let currentDir = pathResolve(startDir);
64
+ let topMostRoot = null;
65
+
66
+ while (currentDir !== dirname(currentDir)) {
67
+ if (hasRootMarker(currentDir)) {
68
+ topMostRoot = currentDir;
69
+ }
70
+ currentDir = dirname(currentDir);
71
+ }
72
+
73
+ return topMostRoot;
74
+ }
75
+
76
+ /**
77
+ * 递归向下查找所有有 package.json 的项目
78
+ */
79
+ function findAllPackages(rootDir, packages) {
80
+ // 跳过路径中包含 node_modules 的目录
81
+ if (rootDir.includes('node_modules')) {
82
+ debugLog(`[mono] 跳过 node_modules: ${rootDir}`);
83
+ return;
84
+ }
85
+
86
+ const pkgPath = join(rootDir, 'package.json');
87
+
88
+ if (existsSync(pkgPath)) {
89
+ try {
90
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
91
+
92
+ if (pkg.name) {
93
+ // 确定入口:优先使用 local 配置,否则使用默认值
94
+ const entry = typeof pkg.local === 'string' ? pkg.local : DEFAULT_ENTRY;
95
+
96
+ packages.set(pkg.name, {
97
+ name: pkg.name,
98
+ dir: rootDir,
99
+ localEntry: entry
100
+ });
101
+
102
+ debugLog(`[mono] 发现包: ${pkg.name} -> ${entry}`);
103
+ }
104
+ } catch {
105
+ // 忽略解析错误
106
+ }
107
+ }
108
+
109
+ // 递归查找子目录
110
+ try {
111
+ const entries = readdirSync(rootDir, { withFileTypes: true });
112
+
113
+ for (const entry of entries) {
114
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
115
+ findAllPackages(join(rootDir, entry.name), packages);
116
+ }
117
+ }
118
+ } catch {
119
+ // 忽略读取错误
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 初始化 workspace 信息
125
+ * 从根目录递归向下收集所有包
126
+ */
127
+ function initWorkspace() {
128
+ if (workspacePackages !== null) return;
129
+
130
+ workspacePackages = new Map();
131
+
132
+ // 从当前工作目录开始,向上找最顶层根目录
133
+ const cwd = process.cwd();
134
+ const projectRoot = findProjectRoot(cwd);
135
+
136
+ if (!projectRoot) {
137
+ debugLog('[mono] 未找到项目根目录');
138
+ return;
139
+ }
140
+
141
+ debugLog(`[mono] 项目根目录: ${projectRoot}`);
142
+
143
+ // 从根目录递归向下查找所有包
144
+ findAllPackages(projectRoot, workspacePackages);
145
+
146
+ debugLog(`[mono] 共发现 ${workspacePackages.size} 个包`);
147
+
148
+ // 生成配置文件
149
+ writeMonoConfig(cwd);
150
+ }
151
+
152
+ /**
153
+ * 生成 monoConfig.json 配置文件
154
+ * 记录包名与文件地址的映射关系
155
+ *
156
+ * @param {string} cwd 当前工作目录
157
+ */
158
+ function writeMonoConfig(cwd) {
159
+ const monoDir = join(cwd, '.mono');
160
+ const configPath = join(monoDir, 'monoConfig.json');
161
+
162
+ // 确保 mono 目录存在
163
+ if (!existsSync(monoDir)) {
164
+ try {
165
+ mkdirSync(monoDir, { recursive: true });
166
+ } catch (err) {
167
+ debugLog(`[mono] 创建目录失败: ${err.message}`);
168
+ return;
169
+ }
170
+ }
171
+
172
+ // 构建包映射
173
+ const config = {};
174
+ for (const [name, pkg] of workspacePackages) {
175
+ const entryPath = join(pkg.dir, pkg.localEntry);
176
+ config[name] = entryPath;
177
+ }
178
+
179
+ // 写入配置文件
180
+ try {
181
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
182
+ debugLog(`[mono] 已生成配置文件: ${configPath}`);
183
+ } catch (err) {
184
+ debugLog(`[mono] 写入配置文件失败: ${err.message}`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 判断是否是包的主入口导入(不是子路径导入)
190
+ */
191
+ function isMainEntryImport(specifier) {
192
+ // 跳过相对路径和绝对路径
193
+ if (specifier.startsWith('.') || specifier.startsWith('/')) {
194
+ return false;
195
+ }
196
+
197
+ // 跳过 node: 和其他协议
198
+ if (specifier.includes(':')) {
199
+ return false;
200
+ }
201
+
202
+ // 处理 scoped 包名 @scope/name
203
+ if (specifier.startsWith('@')) {
204
+ const parts = specifier.split('/');
205
+ // @scope/name 正好两部分是主入口
206
+ // @scope/name/sub 三部分及以上是子路径
207
+ return parts.length === 2;
208
+ }
209
+
210
+ // 普通包名 name
211
+ // name 是主入口,name/sub 是子路径
212
+ return !specifier.includes('/');
213
+ }
214
+
215
+ /**
216
+ * ESM Loader: resolve hook
217
+ * 拦截模块解析
218
+ */
219
+ export async function resolve(specifier, context, nextResolve) {
220
+ // 初始化 workspace 信息(只初始化一次)
221
+ initWorkspace();
222
+
223
+ // 只处理主入口导入
224
+ if (!isMainEntryImport(specifier)) {
225
+ return nextResolve(specifier, context);
226
+ }
227
+
228
+ // 检查是否是本地包
229
+ const pkg = workspacePackages?.get(specifier);
230
+
231
+ if (!pkg) {
232
+ // 不是本地包,使用默认解析
233
+ return nextResolve(specifier, context);
234
+ }
235
+
236
+ // 构造新的入口路径
237
+ const newEntry = join(pkg.dir, pkg.localEntry);
238
+ const newUrl = pathToFileURL(newEntry).href;
239
+
240
+ // 日志:显示拦截的包
241
+ debugLog(`[mono] 拦截: ${specifier} -> ${pkg.localEntry}`);
242
+
243
+ return {
244
+ url: newUrl,
245
+ shortCircuit: true
246
+ };
247
+ }
package/src/loader.mjs ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ESM Loader 入口
3
+ * 使用 register() API 注册自定义 hooks
4
+ *
5
+ * 用法: node --import=./src/loader.mjs app.ts
6
+ *
7
+ * 功能:
8
+ * 1. 注册 tsx 的 ESM loader 以支持 TypeScript 编译
9
+ * 2. 注册 mono 的 resolve hooks 以拦截包名解析到源码
10
+ *
11
+ * 注册顺序说明:
12
+ * Node.js loader hooks 使用洋葱模型,后注册的先被调用。
13
+ * 我们希望 mono 先拦截包名,再由 tsx 编译 .ts 文件。
14
+ * 所以先注册 tsx,再注册 mono(后注册先调用)。
15
+ */
16
+
17
+ import { register } from 'node:module';
18
+ import { pathToFileURL } from 'node:url';
19
+ import { dirname, join } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ // 是否启用调试日志(设置环境变量 MONO_DEBUG=1 开启)
26
+ const DEBUG_ENABLED = process.env.MONO_DEBUG === '1';
27
+
28
+ if (DEBUG_ENABLED) console.log('[mono] 加载 loader.mjs');
29
+
30
+ // 1. 先注册 tsx 的 ESM loader(TypeScript 编译)
31
+ if (DEBUG_ENABLED) console.log('[mono] 注册 tsx ESM loader...');
32
+ await import('tsx/esm');
33
+
34
+ // 2. 再注册 mono 的 resolve hooks(包名拦截)
35
+ // 后注册先调用,所以 mono 会先于 tsx 处理 resolve
36
+ const hooksPath = join(__dirname, 'hooks.mjs');
37
+ if (DEBUG_ENABLED) console.log('[mono] 注册 mono hooks:', hooksPath);
38
+ register(pathToFileURL(hooksPath).href, import.meta.url);
39
+