@linyjs/cli 0.0.4 → 0.0.5

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.
@@ -1,462 +0,0 @@
1
- # 零配置插件开发指南
2
-
3
- ## 🎯 核心理念
4
-
5
- **你只需要:**
6
- 1. 编写 TypeScript/TSX 代码
7
- 2. 运行 `npx @linyjs/cli pack`
8
- 3. 得到优化后的 `.mpk` 文件
9
-
10
- **不需要:**
11
- - ❌ 配置 webpack
12
- - ❌ 配置 rollup
13
- - ❌ 配置 babel
14
- - ❌ 配置 esbuild
15
- - ❌ 处理 bundle 优化
16
- - ❌ 管理外部依赖
17
-
18
- CLI 会自动处理所有构建细节!
19
-
20
- ---
21
-
22
- ## 🚀 5分钟快速开始
23
-
24
- ### 步骤 1: 创建项目结构
25
-
26
- ```bash
27
- mkdir my-plugin && cd my-plugin
28
- npm init -y
29
- ```
30
-
31
- ### 步骤 2: 安装依赖
32
-
33
- ```bash
34
- npm install @linyjs/server-module-interface @linyjs/client-module-interface react
35
- npm install -D typescript @types/react
36
- ```
37
-
38
- ### 步骤 3: 创建 tsconfig.json
39
-
40
- ```json
41
- {
42
- "compilerOptions": {
43
- "target": "ES2020",
44
- "module": "ESNext",
45
- "lib": ["ES2020", "DOM"],
46
- "jsx": "react-jsx",
47
- "strict": true,
48
- "esModuleInterop": true,
49
- "skipLibCheck": true,
50
- "moduleResolution": "node"
51
- },
52
- "include": ["src/**/*"]
53
- }
54
- ```
55
-
56
- **注意:** 不需要配置 `outDir`,CLI 会处理输出!
57
-
58
- ### 步骤 4: 编写代码
59
-
60
- **src/server/index.ts**
61
- ```typescript
62
- import type { IModule } from '@linyjs/server-module-interface';
63
-
64
- class HelloController {
65
- getRoutes() {
66
- return [{
67
- method: 'GET' as const,
68
- path: '/api/hello',
69
- middlewares: [],
70
- responseType: 'json' as const,
71
- handler: async () => {
72
- return { message: 'Hello from plugin!' };
73
- }
74
- }];
75
- }
76
- }
77
-
78
- export const helloServerModule: IModule = {
79
- name: 'helloServer',
80
- serviceDefs: [{
81
- serviceTag: 'helloController',
82
- serviceImpl: HelloController,
83
- meta: { usageType: 'controller' }
84
- }]
85
- };
86
- ```
87
-
88
- **src/client/index.tsx**
89
- ```typescript
90
- import type { IModule } from '@linyjs/client-module-interface';
91
- import React from 'react';
92
-
93
- function HelloPage() {
94
- return <div>Hello from plugin!</div>;
95
- }
96
-
97
- export const helloClientModule: IModule = {
98
- name: 'helloClient',
99
- serviceDefs: [{
100
- serviceTag: 'helloRoute',
101
- serviceImpl: class {
102
- basePath = '/hello';
103
- getRoutes() {
104
- return [{
105
- path: '/',
106
- component: HelloPage,
107
- key: 'hello-page',
108
- meta: { title: 'Hello' }
109
- }];
110
- }
111
- },
112
- meta: { usageType: 'route' }
113
- }]
114
- };
115
- ```
116
-
117
- ### 步骤 5: 一键打包
118
-
119
- ```bash
120
- npx @linyjs/cli pack
121
- ```
122
-
123
- **就这么简单!** 🎉
124
-
125
- CLI 会自动:
126
- 1. ✅ 检测入口文件(`src/server/index.ts` 和 `src/client/index.tsx`)
127
- 2. ✅ 使用 esbuild 进行优化构建
128
- 3. ✅ Bundle 所有依赖
129
- 4. ✅ Minify 代码(减小体积)
130
- 5. ✅ Tree shaking(移除未使用代码)
131
- 6. ✅ 处理外部依赖(react、express 等)
132
- 7. ✅ 生成 manifest.json
133
- 8. ✅ 验证插件结构
134
- 9. ✅ 打包为 `.mpk` 文件
135
-
136
- ### 步骤 6: 查看结果
137
-
138
- ```
139
- ✓ Plugin ready!
140
- File: /path/to/my-plugin-1.0.0.mpk
141
- Size: 45.23 KB
142
- Version: 1.0.0
143
- ```
144
-
145
- ---
146
-
147
- ## 📂 标准项目结构
148
-
149
- CLI 期望以下目录结构(约定优于配置):
150
-
151
- ```
152
- my-plugin/
153
- ├── package.json
154
- ├── tsconfig.json # 只需要基本的 TS 配置
155
- ├── src/
156
- │ ├── server/
157
- │ │ └── index.ts # 服务端入口(必需)
158
- │ ├── client/
159
- │ │ └── index.tsx # 客户端入口(可选)
160
- │ └── shared/
161
- │ └── types.ts # 共享类型(可选)
162
- └── README.md
163
- ```
164
-
165
- **入口文件命名约定:**
166
-
167
- | 类型 | 路径 | 说明 |
168
- |------|------|------|
169
- | 服务端 | `src/server/index.ts` | 主入口文件 |
170
- | 客户端 | `src/client/index.tsx` | 主入口文件 |
171
-
172
- 如果找不到这些文件,CLI 会尝试其他常见模式:
173
- - `src/server.ts`
174
- - `src/client.tsx`
175
- - `server/index.ts`
176
- - `client/index.tsx`
177
-
178
- ---
179
-
180
- ## ⚙️ 默认行为详解
181
-
182
- ### 1. 自动使用 esbuild
183
-
184
- 无论你项目中是否有 webpack、rollup 或 tsc 配置,CLI **始终使用 esbuild** 进行构建。
185
-
186
- **为什么?**
187
- - ⚡ **速度** - 比 webpack 快 10-100 倍
188
- - 📦 **体积小** - 更好的 tree shaking
189
- - 🔧 **零配置** - 无需复杂的配置文件
190
- - 🎯 **优化** - 内置 minify、bundle、code splitting
191
-
192
- ### 2. 智能外部依赖处理
193
-
194
- CLI 自动将以下依赖标记为 external(不打包):
195
-
196
- ```typescript
197
- const defaultExternal = [
198
- 'react', // 由 SSR 宿主提供
199
- 'react-dom', // 由 SSR 宿主提供
200
- 'express', // 由 Node.js 环境提供
201
- '@linyjs/*', // Ling 框架核心
202
- '@ling/*' // 兼容旧版本
203
- ];
204
- ```
205
-
206
- **好处:**
207
- - 📉 **减小体积** - 避免重复打包共享库
208
- - ⚡ **加载更快** - 利用浏览器/Node.js 缓存
209
- - 🔄 **版本一致** - 确保使用宿主环境的版本
210
-
211
- ### 3. 自动优化
212
-
213
- 默认启用的优化:
214
-
215
- ```bash
216
- # Server Bundle
217
- --bundle # 打包所有依赖
218
- --minify # 代码压缩
219
- --sourcemap # 生成 sourcemap
220
- --platform=node # Node.js 优化
221
- --target=node18 # 目标平台
222
-
223
- # Client Bundle
224
- --bundle # 打包所有依赖
225
- --minify # 代码压缩
226
- --sourcemap # 生成 sourcemap
227
- --platform=browser # 浏览器优化
228
- --target=es2020 # 目标平台
229
- ```
230
-
231
- ### 4. 输出结构
232
-
233
- CLI 生成的标准输出:
234
-
235
- ```
236
- dist/
237
- ├── server/
238
- │ └── index.js # 服务端 bundle
239
- ├── client/
240
- │ └── index.js # 客户端 bundle
241
- └── manifest.json # 插件清单
242
- ```
243
-
244
- ---
245
-
246
- ## 🎨 自定义(可选)
247
-
248
- 虽然零配置是默认行为,但你仍然可以自定义:
249
-
250
- ### 方式 1: CLI 选项
251
-
252
- ```bash
253
- # 跳过构建(使用已有 dist/)
254
- npx @linyjs/cli pack --no-build
255
-
256
- # 详细模式(查看构建详情)
257
- npx @linyjs/cli pack --verbose
258
-
259
- # 指定输出目录
260
- npx @linyjs/cli pack -o ./releases
261
- ```
262
-
263
- ### 方式 2: .lingrc.json(高级)
264
-
265
- ```json
266
- {
267
- "build": {
268
- "external": [
269
- "react",
270
- "lodash", // 添加额外的 external 依赖
271
- "moment"
272
- ],
273
- "exclude": [
274
- "*.test.ts", // 排除测试文件
275
- "**/__tests__/**"
276
- ]
277
- }
278
- }
279
- ```
280
-
281
- ### 方式 3: package.json 元数据
282
-
283
- ```json
284
- {
285
- "name": "@my-org/my-plugin",
286
- "version": "1.0.0",
287
- "description": "My awesome plugin",
288
- "author": "Your Name",
289
- "ling": {
290
- "permissions": [
291
- "article:read",
292
- "article:write"
293
- ]
294
- }
295
- }
296
- ```
297
-
298
- ---
299
-
300
- ## 💡 最佳实践
301
-
302
- ### 1. 保持简单
303
-
304
- ```typescript
305
- // ✅ 推荐:简单的入口文件
306
- export const myServerModule: IModule = {
307
- name: 'myServer',
308
- serviceDefs: [...]
309
- };
310
-
311
- // ❌ 避免:复杂的构建逻辑
312
- // 不需要 webpack.config.js
313
- // 不需要 rollup.config.js
314
- // 不需要 babel.config.js
315
- ```
316
-
317
- ### 2. 使用 TypeScript
318
-
319
- ```json
320
- // tsconfig.json - 保持简洁
321
- {
322
- "compilerOptions": {
323
- "strict": true,
324
- "esModuleInterop": true
325
- }
326
- }
327
- ```
328
-
329
- ### 3. 合理的依赖管理
330
-
331
- ```json
332
- {
333
- "dependencies": {
334
- "@linyjs/server-module-interface": "^0.0.1",
335
- "@linyjs/client-module-interface": "^0.0.1",
336
- "react": "^19.0.0"
337
- // ❌ 不要添加 express、webpack 等构建工具
338
- }
339
- }
340
- ```
341
-
342
- ### 4. 清晰的目录结构
343
-
344
- ```
345
- src/
346
- ├── server/ # 服务端代码
347
- │ ├── index.ts # 入口
348
- │ ├── controllers/ # 控制器
349
- │ └── services/ # 服务
350
- ├── client/ # 客户端代码
351
- │ ├── index.tsx # 入口
352
- │ ├── components/ # 组件
353
- │ └── pages/ # 页面
354
- └── shared/ # 共享代码
355
- └── types.ts # 类型定义
356
- ```
357
-
358
- ---
359
-
360
- ## 🐛 常见问题
361
-
362
- ### Q1: 我需要配置 webpack 吗?
363
-
364
- **不需要!** CLI 内置了 esbuild,自动处理所有构建细节。
365
-
366
- ```bash
367
- # 只需运行
368
- npx @linyjs/cli pack
369
- ```
370
-
371
- ### Q2: 如何添加第三方库?
372
-
373
- 直接安装即可,CLI 会自动处理:
374
-
375
- ```bash
376
- npm install lodash
377
- ```
378
-
379
- 在代码中使用:
380
-
381
- ```typescript
382
- import _ from 'lodash';
383
-
384
- // CLI 会自动判断是否打包或 external
385
- ```
386
-
387
- 如果需要 external(减小体积),在 `.lingrc.json` 中配置:
388
-
389
- ```json
390
- {
391
- "build": {
392
- "external": ["lodash"]
393
- }
394
- }
395
- ```
396
-
397
- ### Q3: 如何调试构建问题?
398
-
399
- 使用 `--verbose` 模式:
400
-
401
- ```bash
402
- npx @linyjs/cli pack --verbose
403
- ```
404
-
405
- 会显示:
406
- - 检测到的构建系统
407
- - 使用的构建命令
408
- - 每个 bundle 的大小
409
- - 详细的错误信息
410
-
411
- ### Q4: 支持 JSX/TSX 吗?
412
-
413
- **完全支持!** CLI 自动处理 JSX 转换。
414
-
415
- ```tsx
416
- // src/client/index.tsx
417
- function MyComponent() {
418
- return <div>Hello</div>; // ✅ 正常工作
419
- }
420
- ```
421
-
422
- ### Q5: 如何处理环境变量?
423
-
424
- 在代码中直接使用:
425
-
426
- ```typescript
427
- const apiKey = process.env.MY_API_KEY;
428
- ```
429
-
430
- CLI 不会处理环境变量,它们会在运行时从宿主环境读取。
431
-
432
- ---
433
-
434
- ## 📊 性能对比
435
-
436
- | 构建方式 | 配置复杂度 | 构建速度 | 输出体积 | 推荐度 |
437
- |---------|-----------|---------|---------|--------|
438
- | **CLI (esbuild)** | ⭐ 零配置 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅✅✅ |
439
- | Webpack | ⭐⭐⭐⭐ 复杂 | ⭐⭐ | ⭐⭐⭐ | ❌ |
440
- | Rollup | ⭐⭐⭐ 中等 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
441
- | tsc only | ⭐⭐ 简单 | ⭐⭐⭐⭐ | ⭐⭐ | ❌ |
442
-
443
- ---
444
-
445
- ## 🎓 总结
446
-
447
- **零配置插件开发的三个原则:**
448
-
449
- 1. **约定优于配置** - 遵循标准目录结构
450
- 2. **智能默认值** - CLI 自动选择最优策略
451
- 3. **渐进式增强** - 需要时可以自定义
452
-
453
- **你只需要:**
454
- ```bash
455
- # 1. 写代码
456
- # 2. 打包
457
- npx @linyjs/cli pack
458
-
459
- # 完成!✨
460
- ```
461
-
462
- 享受简单、高效的插件开发体验吧!🚀
@@ -1,251 +0,0 @@
1
- /**
2
- * Dev command - Quick plugin development workflow
3
- *
4
- * Packs the plugin, copies to extensions folder, and registers in packages.json
5
- */
6
-
7
- import fs from 'fs';
8
- import path from 'path';
9
- import chalk from 'chalk';
10
- import ora from 'ora';
11
- import { packPlugin } from './pack.js';
12
-
13
- interface DevOptions {
14
- target?: string;
15
- verbose: boolean;
16
- }
17
-
18
- /**
19
- * Load .env file and return environment variables
20
- */
21
- function loadEnvConfig(projectRoot: string): Record<string, string> {
22
- const envPath = path.join(projectRoot, '.env');
23
-
24
- if (!fs.existsSync(envPath)) {
25
- // Try parent directories (monorepo structure)
26
- const parentEnvPath = path.join(projectRoot, '..', '.env');
27
- if (fs.existsSync(parentEnvPath)) {
28
- return parseEnvFile(parentEnvPath);
29
- }
30
- return {};
31
- }
32
-
33
- return parseEnvFile(envPath);
34
- }
35
-
36
- /**
37
- * Parse .env file content
38
- */
39
- function parseEnvFile(envPath: string): Record<string, string> {
40
- const content = fs.readFileSync(envPath, 'utf-8');
41
- const config: Record<string, string> = {};
42
-
43
- content.split('\n').forEach(line => {
44
- line = line.trim();
45
- if (line && !line.startsWith('#')) {
46
- const [key, ...valueParts] = line.split('=');
47
- if (key && valueParts.length > 0) {
48
- config[key.trim()] = valueParts.join('=').trim();
49
- }
50
- }
51
- });
52
-
53
- return config;
54
- }
55
-
56
- /**
57
- * Execute dev workflow: pack -> copy -> register
58
- */
59
- export async function devPlugin(options: DevOptions) {
60
- const projectRoot = process.cwd();
61
-
62
- console.log(chalk.blue('🚀 Ling Plugin Dev Mode\n'));
63
-
64
- // Load environment configuration
65
- const envConfig = loadEnvConfig(projectRoot);
66
-
67
- // Step 1: Pack the plugin
68
- const spinner1 = ora('Packing plugin...').start();
69
-
70
- try {
71
- // Create temporary output directory
72
- const tempOutputDir = path.join(projectRoot, '.ling-tmp');
73
-
74
- await packPlugin({
75
- output: tempOutputDir,
76
- build: true,
77
- verbose: options.verbose
78
- });
79
-
80
- // Find the generated .mpk file
81
- const mpkFiles = fs.readdirSync(tempOutputDir).filter(f => f.endsWith('.mpk'));
82
-
83
- if (mpkFiles.length === 0) {
84
- spinner1.fail('No .mpk file generated');
85
- process.exit(1);
86
- }
87
-
88
- const mpkFile = mpkFiles[0];
89
- const mpkPath = path.join(tempOutputDir, mpkFile);
90
-
91
- spinner1.succeed(`Plugin packed: ${mpkFile}`);
92
-
93
- // Step 2: Determine target extensions directory
94
- const targetDir = options.target || findExtensionsDir(projectRoot, envConfig);
95
-
96
- if (!targetDir) {
97
- spinner1.fail('Cannot find extensions directory');
98
- console.error(chalk.red('\nPlease specify target directory:'));
99
- console.error(chalk.gray('\nOption 1: Use --target flag'));
100
- console.error(chalk.gray(' npx @linyjs/cli dev --target /path/to/extensions'));
101
- console.error(chalk.gray('\nOption 2: Add to .env file'));
102
- console.error(chalk.gray(' EXTENSIONS_DIR=/path/to/ling/packages/ssr/extensions'));
103
- process.exit(1);
104
- }
105
-
106
- // Step 3: Extract and copy to extensions
107
- const spinner2 = ora('Installing to extensions...').start();
108
-
109
- const pluginName = mpkFile.replace('.mpk', '').replace(/-\d+\.\d+\.\d+$/, '');
110
- const installPath = path.join(targetDir, pluginName);
111
-
112
- // Remove old installation if exists
113
- if (fs.existsSync(installPath)) {
114
- fs.rmSync(installPath, { recursive: true, force: true });
115
- }
116
-
117
- // Create installation directory
118
- fs.mkdirSync(installPath, { recursive: true });
119
-
120
- // Extract .mpk file (it's a ZIP)
121
- await extractMpk(mpkPath, installPath);
122
-
123
- spinner2.succeed(`Installed to: ${installPath}`);
124
-
125
- // Step 4: Register in packages.json
126
- const spinner3 = ora('Registering plugin...').start();
127
-
128
- const packagesJsonPath = path.join(targetDir, 'packages.json');
129
-
130
- let packages: any[] = [];
131
-
132
- if (fs.existsSync(packagesJsonPath)) {
133
- const content = fs.readFileSync(packagesJsonPath, 'utf-8');
134
- packages = JSON.parse(content);
135
- }
136
-
137
- // Read manifest from installed plugin
138
- const manifestPath = path.join(installPath, 'manifest.json');
139
- let manifest: any = {};
140
-
141
- if (fs.existsSync(manifestPath)) {
142
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
143
- }
144
-
145
- // Check if already registered
146
- const existingIndex = packages.findIndex(p => p.name === pluginName);
147
-
148
- const packageInfo = {
149
- name: pluginName,
150
- version: manifest.version || '1.0.0',
151
- installPath: `extensions/${pluginName}`,
152
- status: 'enabled',
153
- installTime: Date.now(),
154
- manifest: {
155
- name: manifest.name || pluginName,
156
- version: manifest.version || '1.0.0',
157
- description: manifest.description || '',
158
- author: manifest.author || '',
159
- serverEntry: manifest.serverEntry || 'server/index.js',
160
- clientEntry: manifest.clientEntry || 'client/index.js'
161
- }
162
- };
163
-
164
- if (existingIndex >= 0) {
165
- packages[existingIndex] = packageInfo;
166
- spinner3.succeed(`Plugin updated in packages.json`);
167
- } else {
168
- packages.push(packageInfo);
169
- spinner3.succeed(`Plugin registered in packages.json`);
170
- }
171
-
172
- // Write packages.json
173
- fs.writeFileSync(packagesJsonPath, JSON.stringify(packages, null, 2) + '\n', 'utf-8');
174
-
175
- // Cleanup temporary files
176
- if (fs.existsSync(tempOutputDir)) {
177
- fs.rmSync(tempOutputDir, { recursive: true, force: true });
178
- }
179
-
180
- // Show success message
181
- console.log('\n' + chalk.green('✓ Development setup complete!\n'));
182
- console.log(chalk.cyan('Plugin installed:'));
183
- console.log(` Name: ${chalk.bold(pluginName)}`);
184
- console.log(` Path: ${chalk.gray(installPath)}`);
185
- console.log(` Status: ${chalk.green('enabled')}\n`);
186
-
187
- console.log(chalk.cyan('Next steps:'));
188
- console.log(` 1. Restart SSR server:`);
189
- console.log(` ${chalk.gray('cd packages/ssr && npx tsx src/demo/dev-server-with-pkg.tsx')}`);
190
- console.log(` 2. Or use hot reload (if supported)`);
191
- console.log(` 3. Test your plugin in the browser\n`);
192
-
193
- } catch (error) {
194
- spinner1.fail('Dev workflow failed');
195
- console.error(chalk.red('\nError:'), error instanceof Error ? error.message : error);
196
- process.exit(1);
197
- }
198
- }
199
-
200
- /**
201
- * Find extensions directory with priority:
202
- * 1. --target command line option
203
- * 2. EXTENSIONS_DIR from .env file
204
- * 3. Auto-detect common paths
205
- */
206
- function findExtensionsDir(projectRoot: string, envConfig: Record<string, string>): string | null {
207
- // Priority 1: Environment variable from .env
208
- const envExtensionsDir = envConfig.EXTENSIONS_DIR || envConfig.EXTENSIONS_PATH;
209
- if (envExtensionsDir) {
210
- // Resolve relative path from project root
211
- const resolvedPath = path.isAbsolute(envExtensionsDir)
212
- ? envExtensionsDir
213
- : path.join(projectRoot, envExtensionsDir);
214
-
215
- if (fs.existsSync(resolvedPath)) {
216
- return resolvedPath;
217
- }
218
- }
219
-
220
- // Priority 2: Auto-detect common paths (monorepo structure)
221
- const candidates = [
222
- path.join(projectRoot, '..', 'ssr', 'extensions'),
223
- path.join(projectRoot, '..', '..', 'packages', 'ssr', 'extensions'),
224
- path.join(projectRoot, 'packages', 'ssr', 'extensions'),
225
- ];
226
-
227
- for (const candidate of candidates) {
228
- if (fs.existsSync(candidate)) {
229
- return candidate;
230
- }
231
- }
232
-
233
- return null;
234
- }
235
-
236
- /**
237
- * Extract .mpk file (ZIP format) to target directory
238
- */
239
- async function extractMpk(mpkPath: string, targetDir: string): Promise<void> {
240
- // Use unzipper or similar library
241
- // For now, use system unzip command
242
- const { execSync } = await import('child_process');
243
-
244
- try {
245
- execSync(`unzip -o "${mpkPath}" -d "${targetDir}"`, {
246
- stdio: 'pipe'
247
- });
248
- } catch (error) {
249
- throw new Error('Failed to extract .mpk file. Make sure unzip is installed.');
250
- }
251
- }