lack 1.3.17 → 1.4.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.
@@ -0,0 +1,55 @@
1
+ # lack
2
+ [![NPM version](https://img.shields.io/npm/v/lack.svg?style=flat-square)](https://npmjs.org/package/lack)
3
+ [![node version](https://img.shields.io/badge/node.js-%3E=_8-green.svg?style=flat-square)](http://nodejs.org/download/)
4
+ [![npm download](https://img.shields.io/npm/dm/lack.svg?style=flat-square)](https://npmjs.org/package/lack)
5
+ [![NPM count](https://img.shields.io/npm/dt/lack.svg?style=flat-square)](https://www.npmjs.com/package/lack)
6
+ [![License](https://img.shields.io/npm/l/lack.svg?style=flat-square)](https://www.npmjs.com/package/lack)
7
+
8
+ [中文](./README.md) · English
9
+
10
+ A scaffolding tool for rapid [Whistle](https://github.com/avwo/whistle) plugin development.
11
+
12
+ ### Installation
13
+ ```sh
14
+ npm i -g lack
15
+ ```
16
+
17
+ ### Usage
18
+ 1. Create plugin directory
19
+ ```sh
20
+ mkdir whistle.your-plugin-name
21
+ cd whistle.your-plugin-name
22
+ ```
23
+ > Note: Plugin name must follow `whistle.xxx` or `@scope/whistle.xxx` format where `xxx` can only contain `a-z`, `0-9`, `-` and `_` (underscore not recommended)
24
+
25
+ 2. Initialize project
26
+ - Interactive mode: `lack init`
27
+ > Prompts to select required plugin hooks (multiple selection supported)
28
+ - Quick command (`lack init hook1,hook2...`): [Plugin Development](https://wproxy.org/docs/extensions/dev.html)
29
+
30
+ 3. Install dependencies
31
+ ```sh
32
+ npm i
33
+ ```
34
+
35
+ 4. [Optional] Code style setup
36
+ ```sh
37
+ npx install-peerdeps --dev eslint-config-airbnb
38
+ ```
39
+ > Configuration reference: https://www.npmjs.com/package/eslint-config-airbnb
40
+
41
+ 5. Development mode
42
+ ```sh
43
+ lack watch
44
+ ```
45
+ Features:
46
+ - Auto-reloads plugin into running Whistle instance
47
+ - Automatically reloads on code changes
48
+ - Displays plugin `console.xxx` output in terminal
49
+
50
+ 6. View help
51
+ ```sh
52
+ lack --help
53
+ ```
54
+
55
+ Complete development guide: [Plugin Development](https://wproxy.org/docs/extensions/dev.html)
package/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
  [![NPM count](https://img.shields.io/npm/dt/lack.svg?style=flat-square)](https://www.npmjs.com/package/lack)
6
6
  [![License](https://img.shields.io/npm/l/lack.svg?style=flat-square)](https://www.npmjs.com/package/lack)
7
7
 
8
- 生成 [whistle](https://github.com/avwo/whistle) 插件的脚手架。
8
+ 中文 · [English](./README-en_US.md)
9
+
10
+ 用于快速生成 [Whistle](https://github.com/avwo/whistle) 插件的脚手架工具。
9
11
 
10
12
  ### 安装
11
13
  ``` sh
@@ -13,25 +15,36 @@ npm i -g lack
13
15
  ```
14
16
 
15
17
  ### 使用
16
- 严格按以下步骤操作:
17
- 1. 新建插件目录 `whistle.xxx`(如果已存在忽略此步骤)
18
- > xxx 表示只包含 `a-z\d_-` 的任意字符串,具体参见帮助文档:[插件开发](https://wproxy.org/whistle/plugins.html)
19
- 2. 进入插件目录,执行 `lack init` 后根据需要选择插件的钩子
20
- > 有关插件钩子的功能参见帮助文档:[插件开发](https://wproxy.org/whistle/plugins.html)
21
- 3. 选择好插件所需钩子并确定后,如果需要修改或新增钩子,可以删除已存在的钩子,并执行上面步骤2
22
- 4. 【可选】配置eslint规则,参考:[eslint-config-imweb](https://github.com/imweb/eslint-config-imweb)
23
- 5. 安装依赖 `npm i`
24
- 6. 执行 `npm link` 将插件link到全局,这样可以在 whistle 界面的 Plugins 列表看到此插件
25
- 7. 开启 whistle 调试模式
18
+ 1. 创建插件目录
19
+ ``` sh
20
+ mkdir whistle.your-plugin-name
21
+ cd whistle.your-plugin-name
22
+ ```
23
+ > 注意:插件名必须符合 `whistle.xxx` 或 `@scope/whistle.xxx` 格式,其中 `xxx` 只能包含 `a-z`、`0-9`、`-` 和 `_`(下划线不推荐使用)
24
+ 2. 初始化项目
25
+ - 手动选择:`lack init`
26
+ > 该命令会交互式询问你需要哪些插件钩子(支持多选),按需选择即可
27
+ - 快捷命令(`lack init hook1,hook2...`):[插件开发](https://wproxy.org/docs/extensions/dev.html)
28
+ 3. 安装依赖
29
+ ``` sh
30
+ npm i
31
+ ```
32
+ 4. 【可选】代码规范配置
26
33
  ``` sh
27
- w2 stop
28
- w2 run
34
+ npx install-peerdeps --dev eslint-config-airbnb
29
35
  ```
30
- > 这样可以在控制台里面看到插件 `console.log` 输出的内容
31
- 8. 开启监听插件变更自动重启:
32
- ```sh
36
+ > 详细配置参考:https://www.npmjs.com/package/eslint-config-airbnb
37
+ 5. 开发模式
38
+ ``` sh
33
39
  lack watch
34
40
  ```
35
- 9. 更多帮助执行 `lack --help`
41
+ 该命令的功能:
42
+ - 自动重新加载插件到运行的 Whistle 实例
43
+ - 插件代码变更时会自动重新加载
44
+ - 可以在命令行查看插件 `console.xxx` 输出的日志
45
+ 6. 查看帮助
46
+ ``` sh
47
+ lack --help
48
+ ```
36
49
 
37
- 更多信息参考插件示例:[https://github.com/whistle-plugins/examples](https://github.com/whistle-plugins/examples)
50
+ 完整插件开发流程参考文档:[插件开发](https://wproxy.org/docs/extensions/dev.html)
@@ -1,5 +1,38 @@
1
-
2
1
  module.exports = async (req, options) => {
2
+ /**
3
+ const { fullUrl } = req;
4
+ // Returns 403 Forbidden status code if URL contains '/test/forbidden'
5
+ if (fullUrl.includes('/test/forbidden')) {
6
+ return false;
7
+ }
8
+ // Returns 403 status code with custom HTML message if URL contains '/test/message/forbidden'
9
+ if (fullUrl.includes('/test/message/forbidden')) {
10
+ req.setHtml('<strong>Access Denied</strong>');
11
+ return false;
12
+ }
13
+
14
+ // Requires username/password authentication if URL contains '/test/login'
15
+ if (fullUrl.includes('/test/login')) {
16
+ const auth = req.headers.authorization || req.headers['proxy-authorization'];
17
+ if (auth) {
18
+ // TODO: Validate username and password - return true if valid, false otherwise
19
+ return true;
20
+ }
21
+ req.setLogin(true); // Triggers basic auth prompt
22
+ return false;
23
+ }
24
+
25
+ // Returns 302 redirect if URL contains '/test/redirect'
26
+ if (fullUrl.includes('/test/redirect')) {
27
+ req.setRedirect('https://www.example.com/test');
28
+ return false;
29
+ }
30
+
31
+ // All other requests are allowed by default
32
+ // Custom headers can be added using `req.setHeader`
33
+ // Only headers prefixed with 'x-whistle-' are supported
34
+ // Example: req.setHeader('x-whistle-xxx', 'value');
3
35
  req.setHeader('x-whistle-custom-header', 'lack');
4
- return true; // false 直接返回 403
36
+ **/
37
+ return true;
5
38
  };
@@ -1,6 +1,13 @@
1
1
 
2
2
  module.exports = (server, options) => {
3
3
  server.on('request', (req, res) => {
4
- res.end();
4
+ // rules & values
5
+ // res.end(JSON.stringify({
6
+ // rules: '',
7
+ // values: {},
8
+ // }));
9
+
10
+ // rules
11
+ res.end('');
5
12
  });
6
13
  };
@@ -1,6 +1,18 @@
1
1
 
2
- module.exports = (server, options) => {
3
- server.on('request', (req, res) => {
4
- // do something
2
+ export default (server, options) => {
3
+ server.on('request', (req) => {
4
+ const { originalReq, originalRes } = req;
5
+ console.log('Value:', originalReq.ruleValue);
6
+ console.log('URL:', originalReq.fullUrl);
7
+ console.log('Method:', originalReq.method);
8
+ console.log('Server IP', originalRes.serverIp);
9
+ console.log('Status Code:', originalRes.statusCode);
10
+ console.log('Response Headers:', originalReq.headers);
11
+ // get session data
12
+ req.getSession((reqSession) => {
13
+ if (reqSession) {
14
+ console.log('Response Body:', reqSession.res.body);
15
+ }
16
+ });
5
17
  });
6
18
  };
@@ -1,6 +1,13 @@
1
1
 
2
2
  module.exports = (server, options) => {
3
3
  server.on('request', (req, res) => {
4
- res.end();
4
+ // rules & values
5
+ // res.end(JSON.stringify({
6
+ // rules: '',
7
+ // values: {},
8
+ // }));
9
+
10
+ // rules
11
+ res.end('');
5
12
  });
6
13
  };
@@ -1,4 +1,18 @@
1
-
1
+ // sniCallback plugin handler - Dynamically controls TLS tunneling behavior based on request URL
2
2
  module.exports = async (req, options) => {
3
- // return { key, cert }; // 可以返回 false、证书 { key, cert }、及其它
3
+ /**
4
+ const { fullUrl, originalReq } = req;
5
+ // Preserve TLS encryption for specific domains (skip MITM decryption)
6
+ if (fullUrl === 'https://tunnel.example.com' || originalReq.sniValue === 'tunnel') {
7
+ return false;
8
+ }
9
+
10
+ // Use custom certificate for targeted decryption
11
+ // Supports .crt, .pem, .cer certificate formats
12
+ if (fullUrl === 'https://custom.example.com') {
13
+ return { key, cert };
14
+ }
15
+ **/
16
+ // Default behavior: Decrypt TLS using Whistle's built-in certificate
17
+ return true;
4
18
  };
@@ -1,6 +1,15 @@
1
-
2
- module.exports = (server, options) => {
3
- server.on('request', (req, res) => {
4
- // do something
1
+ export default (server, options) => {
2
+ server.on('request', (req) => {
3
+ const { originalReq } = req;
4
+ console.log('Value:', originalReq.ruleValue);
5
+ console.log('URL:', originalReq.fullUrl);
6
+ console.log('Method:', originalReq.method);
7
+ console.log('Request Headers:', originalReq.headers);
8
+ // get request session
9
+ req.getReqSession((reqSession) => {
10
+ if (reqSession) {
11
+ console.log('Request Body:', reqSession.req.body);
12
+ }
13
+ });
5
14
  });
6
15
  };
@@ -3,13 +3,14 @@ const bodyParser = require('koa-bodyparser');
3
3
  const onerror = require('koa-onerror');
4
4
  const serve = require('koa-static');
5
5
  const path = require('path');
6
- const router = require('koa-router')();
6
+ const Router = require('@koa/router');
7
7
  const setupRouter = require('./router');
8
8
 
9
9
  const MAX_AGE = 1000 * 60 * 5;
10
10
 
11
11
  module.exports = (server, options) => {
12
12
  const app = new Koa();
13
+ const router = new Router();
13
14
  app.proxy = true;
14
15
  app.silent = true;
15
16
  onerror(app);
@@ -1,4 +1,4 @@
1
- // For help see https://github.com/ZijianHe/koa-router#api-reference
1
+ // For help, see https://github.com/koajs/router
2
2
  module.exports = (router) => {
3
3
  // router.get('/cgi-bin/init', (ctx) => {
4
4
  // ctx.body = 'Hello whistle.';
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "whistle.lack",
3
3
  "version": "1.0.0",
4
- "description": "插件功能简介",
4
+ "description": "",
5
+ "whistleConfig": {},
5
6
  "dependencies": {
7
+ "@koa/router": "^13.1.1",
6
8
  "koa": "^2.15.0",
7
9
  "koa-bodyparser": "^4.4.1",
8
10
  "koa-onerror": "^4.2.0",
9
- "koa-router": "^12.0.1",
10
11
  "koa-static": "^5.0.0"
11
12
  }
12
13
  }
@@ -1,24 +1,26 @@
1
1
  {
2
2
  "name": "whistle.lack",
3
3
  "version": "1.0.0",
4
- "description": "插件功能简介",
4
+ "description": "",
5
5
  "scripts": {
6
6
  "dev": "tsc -w"
7
7
  },
8
+ "whistleConfig": {},
8
9
  "dependencies": {
9
10
  "koa": "^2.15.0",
10
11
  "koa-bodyparser": "^4.4.1",
11
12
  "koa-onerror": "^4.2.0",
12
- "koa-router": "^12.0.1",
13
+ "@koa/router": "^13.1.1",
13
14
  "koa-static": "^5.0.0"
14
15
  },
15
16
  "devDependencies": {
17
+ "@types/node": "^24.0.14",
16
18
  "typescript": "^4.6.2"
17
19
  },
18
20
  "tsTypes": {
19
21
  "@types/koa": "^2.13.4",
22
+ "@types/koa__router": "^12.0.4",
20
23
  "@types/koa-bodyparser": "^4.3.5",
21
- "@types/koa-router": "^7.4.4",
22
24
  "@types/koa-static": "^4.0.2"
23
25
  }
24
26
  }
@@ -1,5 +1,39 @@
1
1
 
2
2
  export default async (req: Whistle.PluginAuthRequest, options: Whistle.PluginOptions) => {
3
+ /**
4
+ const { fullUrl } = req;
5
+ // Returns 403 Forbidden status code if URL contains '/test/forbidden'
6
+ if (fullUrl.includes('/test/forbidden')) {
7
+ return false;
8
+ }
9
+ // Returns 403 status code with custom HTML message if URL contains '/test/message/forbidden'
10
+ if (fullUrl.includes('/test/message/forbidden')) {
11
+ req.setHtml('<strong>Access Denied</strong>');
12
+ return false;
13
+ }
14
+
15
+ // Requires username/password authentication if URL contains '/test/login'
16
+ if (fullUrl.includes('/test/login')) {
17
+ const auth = req.headers.authorization || req.headers['proxy-authorization'];
18
+ if (auth) {
19
+ // TODO: Validate username and password - return true if valid, false otherwise
20
+ return true;
21
+ }
22
+ req.setLogin(true); // Triggers basic auth prompt
23
+ return false;
24
+ }
25
+
26
+ // Returns 302 redirect if URL contains '/test/redirect'
27
+ if (fullUrl.includes('/test/redirect')) {
28
+ req.setRedirect('https://www.example.com/test');
29
+ return false;
30
+ }
31
+
32
+ // All other requests are allowed by default
33
+ // Custom headers can be added using `req.setHeader`
34
+ // Only headers prefixed with 'x-whistle-' are supported
35
+ // Example: req.setHeader('x-whistle-xxx', 'value');
3
36
  req.setHeader('x-whistle-custom-header', 'lack');
4
- return true; // false 直接返回 403
37
+ **/
38
+ return true;
5
39
  };
@@ -1,6 +1,13 @@
1
1
 
2
2
  export default (server: Whistle.PluginServer, options: Whistle.PluginOptions) => {
3
3
  server.on('request', (req: Whistle.PluginRequest, res: Whistle.PluginResponse) => {
4
- res.end();
4
+ // rules & values
5
+ // res.end(JSON.stringify({
6
+ // rules: '',
7
+ // values: {},
8
+ // }));
9
+
10
+ // rules
11
+ res.end('');
5
12
  });
6
13
  };
@@ -1,6 +1,18 @@
1
1
 
2
2
  export default (server: Whistle.PluginServer, options: Whistle.PluginOptions) => {
3
- server.on('request', (req: Whistle.PluginRequest, res: Whistle.PluginResponse) => {
4
- // do something
3
+ server.on('request', (req: Whistle.PluginRequest) => {
4
+ const { originalReq, originalRes } = req;
5
+ console.log('Value:', originalReq.ruleValue);
6
+ console.log('URL:', originalReq.fullUrl);
7
+ console.log('Method:', originalReq.method);
8
+ console.log('Server IP', originalRes.serverIp);
9
+ console.log('Status Code:', originalRes.statusCode);
10
+ console.log('Response Headers:', originalReq.headers);
11
+ // get session data
12
+ req.getSession((reqSession) => {
13
+ if (reqSession) {
14
+ console.log('Response Body:', reqSession.res.body);
15
+ }
16
+ });
5
17
  });
6
18
  };
@@ -1,6 +1,13 @@
1
1
 
2
2
  export default (server: Whistle.PluginServer, options: Whistle.PluginOptions) => {
3
3
  server.on('request', (req: Whistle.PluginRequest, res: Whistle.PluginResponse) => {
4
- res.end();
4
+ // rules & values
5
+ // res.end(JSON.stringify({
6
+ // rules: '',
7
+ // values: {},
8
+ // }));
9
+
10
+ // rules
11
+ res.end('');
5
12
  });
6
13
  };
@@ -1,4 +1,18 @@
1
-
1
+ // sniCallback plugin handler - Dynamically controls TLS tunneling behavior based on request URL
2
2
  export default async (req: Whistle.PluginSNIRequest, options: Whistle.PluginOptions) => {
3
- // return { key, cert }; // 可以返回 false、证书 { key, cert }、及其它
3
+ /**
4
+ const { fullUrl } = req;
5
+ // Preserve TLS encryption for specific domains (skip MITM decryption)
6
+ if (fullUrl === 'https://tunnel.example.com') {
7
+ return false;
8
+ }
9
+
10
+ // Use custom certificate for targeted decryption
11
+ // Supports .crt, .pem, .cer certificate formats
12
+ if (fullUrl === 'https://custom.example.com') {
13
+ return { key, cert };
14
+ }
15
+ **/
16
+ // Default behavior: Decrypt TLS using Whistle's built-in certificate
17
+ return true;
4
18
  };
@@ -1,6 +1,15 @@
1
-
2
1
  export default (server: Whistle.PluginServer, options: Whistle.PluginOptions) => {
3
- server.on('request', (req: Whistle.PluginRequest, res: Whistle.PluginResponse) => {
4
- // do something
2
+ server.on('request', (req: Whistle.PluginRequest) => {
3
+ const { originalReq } = req;
4
+ console.log('Value:', originalReq.ruleValue);
5
+ console.log('URL:', originalReq.fullUrl);
6
+ console.log('Method:', originalReq.method);
7
+ console.log('Request Headers:', originalReq.headers);
8
+ // get request session
9
+ req.getReqSession((reqSession) => {
10
+ if (reqSession) {
11
+ console.log('Request Body:', reqSession.req.body);
12
+ }
13
+ });
5
14
  });
6
15
  };
@@ -200,6 +200,7 @@ declare namespace Whistle {
200
200
  sharedStorage: SharedStorage;
201
201
  baseUrl: string;
202
202
  LRU: LRUCache;
203
+ zipBody(body: any, stream: WhistleBase.Request | WhistleBase.Response, cb: (result: Buffer | '') => void): void;
203
204
  getValue(key: string, cb: (value: string) => void): void;
204
205
  getCert(domain: string, cb: (cert: any) => void): void;
205
206
  getRootCA(cb: (cert: any) => void): void;
@@ -243,13 +244,13 @@ declare namespace Whistle {
243
244
  type PassThrough = (uri?: PassThroughReq, trailers?: PassThroughRes) => void;
244
245
 
245
246
  interface WriteHead {
246
- (code: string | number, msg?: string, headers?: any): void;
247
- (code: string | number, headers?: any): void;
247
+ (code?: string | number, msg?: string, headers?: any): void;
248
+ (code?: string | number, headers?: any): void;
248
249
  }
249
250
 
250
251
  interface RequestFn {
251
- (uri: any, cb?: (res: any) => void, opts?: any): any;
252
- (uri: any, opts?: any, cb?: (res: any) => void): any;
252
+ (uri?: any, cb?: (res: any) => void, opts?: any): any;
253
+ (uri?: any, opts?: any, cb?: (res: any) => void): any;
253
254
  }
254
255
 
255
256
  class PluginRequest extends WhistleBase.Request {
@@ -309,6 +310,8 @@ declare namespace Whistle {
309
310
  isRexExp?: boolean;
310
311
  pattern?: string;
311
312
  customParser?: boolean | '';
313
+ serverIp: string;
314
+ statusCode: string;
312
315
  };
313
316
  originalRes: {
314
317
  serverIp: string;
@@ -338,6 +341,7 @@ declare namespace Whistle {
338
341
  setReqRules: SetRules;
339
342
  setResRules: SetRules;
340
343
  disableTrailers?: boolean;
344
+ writeHead: WriteHead;
341
345
  }
342
346
  class PluginUIRequest extends WhistleBase.Request {
343
347
  clientIp: string;
@@ -3,7 +3,7 @@ import bodyParser from 'koa-bodyparser';
3
3
  import onerror from 'koa-onerror';
4
4
  import serve from 'koa-static';
5
5
  import path from 'path';
6
- import Router from 'koa-router';
6
+ import Router from '@koa/router';
7
7
  import setupRouter from './router';
8
8
 
9
9
  const MAX_AGE = 1000 * 60 * 5;
@@ -1,6 +1,6 @@
1
- import Router from 'koa-router';
1
+ import Router from '@koa/router';
2
2
 
3
- // For help see https://github.com/ZijianHe/koa-router#api-reference
3
+ // For help, see https://github.com/koajs/router
4
4
  export default (router: Router) => {
5
5
  // router.get('/cgi-bin/init', (ctx) => {
6
6
  // ctx.body = 'Hello whistle.';
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "rootDir": "./src",
3
4
  "outDir": "./dist/",
4
5
  "noImplicitAny": true,
5
6
  "removeComments": true,
package/bin/index.js CHANGED
@@ -8,11 +8,11 @@ let bingo;
8
8
 
9
9
  program
10
10
  .version(conf.version)
11
- .command('init')
11
+ .command('init [hooks]')
12
12
  .description('create whistle plugin project.')
13
- .action(() => {
13
+ .action((hooks) => {
14
14
  bingo = true;
15
- init();
15
+ init(hooks);
16
16
  });
17
17
 
18
18
  program
package/bin/init.js CHANGED
@@ -5,9 +5,12 @@ const path = require('path');
5
5
  /* eslint-disable no-sync */
6
6
  let type = 'ts';
7
7
  let srcDir = 'src';
8
+ let curHooks = {};
9
+ let curType;
10
+ let hasHooks;
8
11
  const ROOT = path.join(__dirname, '../');
9
12
  const NAME_RE = /^(@[a-z\d_-]+\/)?(whistle\.)?([a-z\d_-]+)$/;
10
- const NAME_TIPS = 'The plugin name can only contain [a~z0~9_-].';
13
+ const NAME_TIPS = 'Plugin name must match either \'whistle.[a-z0-9_-]+\' or \'@scope/whistle.[a-z0-9_-]+\'';
11
14
  const TS_PKG = fse.readJSONSync(path.join(ROOT, 'assets/ts/package.json'));
12
15
  const JS_PKG = fse.readJSONSync(path.join(ROOT, 'assets/js/package.json'));
13
16
  const TEMPLATES = [
@@ -43,13 +46,13 @@ const getPackage = () => {
43
46
  try {
44
47
  pkg = fse.readJSONSync('package.json');
45
48
  } catch (e) {}
46
- return Object.assign({}, pkg);
49
+ return { ...pkg };
47
50
  };
48
51
 
49
52
  const copySync = (src, dest) => {
50
- dest = dest || src;
51
- if (!fs.existsSync(dest)) {
52
- fse.copySync(path.join(ROOT, src), dest);
53
+ const d = dest || src;
54
+ if (!fs.existsSync(d)) {
55
+ fse.copySync(path.join(ROOT, src), d);
53
56
  }
54
57
  };
55
58
 
@@ -69,7 +72,7 @@ const readIndexFile = () => {
69
72
  };
70
73
 
71
74
  const selectTemplate = async () => {
72
- const { template } = await inquirer.prompt([
75
+ const template = curType ? TEMPLATES[curType === 'ts' ? 0 : 1] : (await inquirer.prompt([
73
76
  {
74
77
  type: 'list',
75
78
  name: 'template',
@@ -77,7 +80,7 @@ const selectTemplate = async () => {
77
80
  message: 'Select template:',
78
81
  choices: TEMPLATES,
79
82
  },
80
- ]);
83
+ ])).template;
81
84
  if (template === 'TypeScript') {
82
85
  return template;
83
86
  }
@@ -87,105 +90,132 @@ const selectTemplate = async () => {
87
90
  };
88
91
 
89
92
  const selectAuth = async () => {
90
- const { auth } = await inquirer.prompt([
91
- {
92
- type: 'confirm',
93
- name: 'auth',
94
- default: false,
95
- message: 'Do you need auth function?',
96
- },
97
- ]);
93
+ let { auth } = curHooks;
94
+ if (!hasHooks) {
95
+ auth = (await inquirer.prompt([
96
+ {
97
+ type: 'confirm',
98
+ name: 'auth',
99
+ default: false,
100
+ message: 'Do you need auth function?',
101
+ },
102
+ ])).auth;
103
+ }
98
104
  return auth && `${srcDir}/auth.${type}`;
99
105
  };
100
106
 
101
107
  const selectSni = async () => {
102
- const { sni } = await inquirer.prompt([
103
- {
104
- type: 'confirm',
105
- name: 'sni',
106
- default: false,
107
- message: 'Do you need sniCallback function?',
108
- },
109
- ]);
108
+ let { sni } = curHooks;
109
+ if (!hasHooks) {
110
+ sni = (await inquirer.prompt([
111
+ {
112
+ type: 'confirm',
113
+ name: 'sni',
114
+ default: false,
115
+ message: 'Do you need sniCallback function?',
116
+ },
117
+ ])).sni;
118
+ }
110
119
  return sni && `${srcDir}/sniCallback.${type}`;
111
120
  };
112
121
 
113
122
  const selectUIServer = async () => {
114
- const { uiServer } = await inquirer.prompt([
115
- {
116
- type: 'confirm',
117
- name: 'uiServer',
118
- default: false,
119
- message: 'Do you need uiServer?',
120
- },
121
- ]);
123
+ let { uiServer } = curHooks;
124
+ if (!hasHooks) {
125
+ uiServer = (await inquirer.prompt([
126
+ {
127
+ type: 'confirm',
128
+ name: 'uiServer',
129
+ default: false,
130
+ message: 'Do you need uiServer?',
131
+ },
132
+ ])).uiServer;
133
+ }
122
134
  return uiServer && `${srcDir}/uiServer`;
123
135
  };
124
136
 
125
137
  const setHookFile = (hooks) => {
126
138
  const servers = {};
127
- hooks.forEach((hook) => {
128
- servers[hook] = `${srcDir}/${hook}.${type}`;
129
- });
139
+ if (hooks) {
140
+ hooks.forEach((hook) => {
141
+ servers[hook] = `${srcDir}/${hook}.${type}`;
142
+ });
143
+ }
130
144
  return servers;
131
145
  };
132
146
 
133
147
  const selectRulesServers = async () => {
134
- const { rulesServers } = await inquirer.prompt([
135
- {
136
- type: 'checkbox',
137
- name: 'rulesServers',
138
- message: 'Select rules servers:',
139
- choices: RULES_SERVERS,
140
- },
141
- ]);
148
+ let { rulesServers } = curHooks;
149
+ if (!hasHooks) {
150
+ rulesServers = (await inquirer.prompt([
151
+ {
152
+ type: 'checkbox',
153
+ name: 'rulesServers',
154
+ message: 'Select rules servers:',
155
+ choices: RULES_SERVERS,
156
+ },
157
+ ])).rulesServers;
158
+ }
142
159
  return setHookFile(rulesServers);
143
160
  };
144
161
 
145
162
  const selectStatsServers = async () => {
146
- const { statsServers } = await inquirer.prompt([
147
- {
148
- type: 'checkbox',
149
- name: 'statsServers',
150
- message: 'Select stats servers:',
151
- choices: STATS_SERVERS,
152
- },
153
- ]);
163
+ let { statsServers } = curHooks;
164
+ if (!hasHooks) {
165
+ statsServers = (await inquirer.prompt([
166
+ {
167
+ type: 'checkbox',
168
+ name: 'statsServers',
169
+ message: 'Select stats servers:',
170
+ choices: STATS_SERVERS,
171
+ },
172
+ ])).statsServers;
173
+ }
154
174
  return setHookFile(statsServers);
155
175
  };
156
176
 
157
177
  const selectPipeServers = async () => {
158
- const { pipeServers } = await inquirer.prompt([
159
- {
160
- type: 'checkbox',
161
- name: 'pipeServers',
162
- message: 'Select pipe servers:',
163
- choices: PIPE_SERVERS,
164
- },
165
- ]);
178
+ let { pipeServers } = curHooks;
179
+ if (!hasHooks) {
180
+ pipeServers = (await inquirer.prompt([
181
+ {
182
+ type: 'checkbox',
183
+ name: 'pipeServers',
184
+ message: 'Select pipe servers:',
185
+ choices: PIPE_SERVERS,
186
+ },
187
+ ])).pipeServers;
188
+ }
166
189
  const hooks = [];
167
- pipeServers.join('+').split('+').forEach((hook) => {
168
- hook = hook.trim();
169
- if (hook) {
170
- hooks.push(hook);
171
- }
172
- });
190
+ if (pipeServers) {
191
+ pipeServers.join('+').split('+').forEach((hook) => {
192
+ const h = hook.trim();
193
+ if (h) {
194
+ hooks.push(h);
195
+ }
196
+ });
197
+ }
173
198
  return setHookFile(hooks);
174
199
  };
175
200
 
176
201
  const selectRulesFiles = async () => {
202
+ let { rulesFiles } = curHooks;
203
+ if (!hasHooks) {
204
+ rulesFiles = (await inquirer.prompt([
205
+ {
206
+ type: 'checkbox',
207
+ name: 'rulesFiles',
208
+ message: 'Select rules files:',
209
+ choices: RULES_FILES,
210
+ },
211
+ ])).rulesFiles;
212
+ }
177
213
  const result = {};
178
- const { rulesFiles } = await inquirer.prompt([
179
- {
180
- type: 'checkbox',
181
- name: 'rulesFiles',
182
- message: 'Select rules files:',
183
- choices: RULES_FILES,
184
- },
185
- ]);
186
- rulesFiles.forEach((hook) => {
187
- result[hook] = hook;
188
- });
214
+ if (rulesFiles) {
215
+ rulesFiles.forEach((hook) => {
216
+ result[hook] = hook;
217
+ });
218
+ }
189
219
  return result;
190
220
  };
191
221
 
@@ -198,59 +228,201 @@ const addMsg = (obj, msg, tips) => {
198
228
  }
199
229
  };
200
230
 
201
- const setPackage = (pkg, hasUIServer) => {
231
+ const setPackage = (pkg, hasUIServer, hasJs) => {
202
232
  const newPkg = type === 'js' ? JS_PKG : TS_PKG;
203
- const keys = ['scripts', 'devDependencies'];
233
+ const keys = hasJs ? ['scripts', 'devDependencies'] : [];
204
234
  if (hasUIServer) {
205
235
  keys.push('dependencies', 'tsTypes');
206
236
  }
207
237
  keys.forEach((key) => {
208
238
  const newValue = newPkg[key];
209
- if (key === 'tsTypes') {
210
- key = 'devDependencies';
211
- }
212
- const value = pkg[key];
239
+ const k = key === 'tsTypes' ? 'devDependencies' : key;
240
+ const value = pkg[k];
213
241
  if (!newValue) {
214
242
  return;
215
243
  }
216
244
  if (!value) {
217
- pkg[key] = newValue;
245
+ pkg[k] = newValue;
218
246
  return;
219
247
  }
220
248
  Object.keys(newValue).forEach((name) => {
221
- if (!value[name]) {
222
- value[name] = newValue[name];
223
- }
249
+ value[name] = value[name] || newValue[name];
224
250
  });
225
251
  });
226
252
  };
227
253
 
228
- module.exports = async () => {
254
+ const trim = (str) => (typeof str === 'string' ? str.trim() : str);
255
+
256
+ const getHooks = (hook) => {
257
+ const result = {};
258
+ const hooks = typeof hook === 'string' ? hook.trim().toLowerCase() : null;
259
+ if (!hooks) {
260
+ return result;
261
+ }
262
+ const addItem = (prop, name) => {
263
+ const list = result[prop] || [];
264
+ result[prop] = list;
265
+ if (list.indexOf(name) === -1) {
266
+ list.push(name);
267
+ }
268
+ };
269
+ hooks.split(/[^._a-z]/i).forEach((name) => {
270
+ const h = name.trim();
271
+ if (h === 'empty' || h === 'blank' || h === 'none') {
272
+ result.empty = true;
273
+ return;
274
+ }
275
+ if (h === 'ts' || h === 'typescript') {
276
+ curType = 'ts';
277
+ return;
278
+ }
279
+ if (h === 'js' || h === 'javascript') {
280
+ curType = 'js';
281
+ return;
282
+ }
283
+ if (h === 'rules' || h === 'rules.txt') {
284
+ addItem('rulesFiles', 'rules.txt');
285
+ return;
286
+ }
287
+ if (h === '_rules' || h === '_rules.txt' || h === 'reqrules' || h === 'reqrules.txt') {
288
+ addItem('rulesFiles', '_rules.txt');
289
+ return;
290
+ }
291
+ if (h === 'resrules' || h === 'resrules.txt') {
292
+ addItem('rulesFiles', 'resRules.txt');
293
+ return;
294
+ }
295
+
296
+ if (h === 'uiserver') {
297
+ result.uiServer = true;
298
+ return;
299
+ }
300
+
301
+ if (h === 'auth' || h === 'verify') {
302
+ result.auth = true;
303
+ return;
304
+ }
305
+
306
+ if (h === 'snicallback' || h === 'sni') {
307
+ result.sni = true;
308
+ return;
309
+ }
310
+
311
+ if (h === 'server') {
312
+ addItem('pipeServers', 'server');
313
+ return;
314
+ }
315
+
316
+ if (h === 'rulesserver') {
317
+ addItem('rulesServers', 'rulesServer');
318
+ return;
319
+ }
320
+
321
+ if (h === 'tunnelrulesserver') {
322
+ addItem('rulesServers', 'tunnelRulesServer');
323
+ return;
324
+ }
325
+
326
+ if (h === 'resrulesserver') {
327
+ addItem('rulesServers', 'resRulesServer');
328
+ return;
329
+ }
330
+ if (h === 'statsserver') {
331
+ addItem('statsServers', 'statsServer');
332
+ return;
333
+ }
334
+ if (h === 'resstatsserver') {
335
+ addItem('statsServers', 'resStatsServer');
336
+ return;
337
+ }
338
+
339
+ const isPipe = h === 'pipe';
340
+ const pipeTunnel = h === 'pipetunnel' || h === 'tunnelpipe';
341
+ const pipeHtttp = h === 'pipehttp' || h === 'httppipe';
342
+ const pipeWs = h === 'pipews' || h === 'wspipe';
343
+
344
+ if (isPipe || pipeTunnel || h === 'pipetunnelreq' || h === 'tunnelreqpipe' || h === 'tunnelreqread' || h === 'tunnelreqwrite') {
345
+ addItem('pipeServers', 'tunnelReqRead');
346
+ addItem('pipeServers', 'tunnelReqWrite');
347
+ }
348
+ if (isPipe || pipeTunnel || h === 'pipetunnelres' || h === 'tunnelrespipe' || h === 'tunnelresread' || h === 'tunnelreswrite') {
349
+ addItem('pipeServers', 'tunnelResRead');
350
+ addItem('pipeServers', 'tunnelResWrite');
351
+ }
352
+
353
+ if (isPipe || pipeHtttp || h === 'pipereq' || h === 'reqpipe' || h === 'reqread' || h === 'reqwrite') {
354
+ addItem('pipeServers', 'reqRead');
355
+ addItem('pipeServers', 'reqWrite');
356
+ }
357
+ if (isPipe || pipeHtttp || h === 'piperes' || h === 'respipe' || h === 'resread' || h === 'reswrite') {
358
+ addItem('pipeServers', 'resRead');
359
+ addItem('pipeServers', 'resWrite');
360
+ }
361
+
362
+ if (isPipe || pipeWs || h === 'pipewsreq' || h === 'wsreqpipe' || h === 'wsreqread' || h === 'wsreqwrite') {
363
+ addItem('pipeServers', 'wsReqRead');
364
+ addItem('pipeServers', 'wsReqWrite');
365
+ }
366
+ if (isPipe || pipeWs || h === 'pipewsres' || h === 'wsrespipe' || h === 'wsresread' || h === 'wsreswrite') {
367
+ addItem('pipeServers', 'wsResRead');
368
+ addItem('pipeServers', 'wsResWrite');
369
+ }
370
+ });
371
+ return result;
372
+ };
373
+
374
+ module.exports = async (hooks) => {
375
+ const isBlank = hooks === 'blank' || hooks === 'empty';
376
+ curHooks = isBlank ? {} : getHooks(hooks);
377
+ hasHooks = isBlank || Object.keys(curHooks).length > 0;
229
378
  const pkg = getPackage();
230
379
  let defaultName;
380
+ if (hasHooks && !curType) {
381
+ curType = fs.existsSync('lib') ? 'js' : 'ts';
382
+ }
231
383
  if (/^@[a-z\d_-]+\/whistle\.[a-z\d_-]+$/.test(pkg.name)) {
232
384
  defaultName = pkg.name;
233
385
  } else if (NAME_RE.test(path.basename(process.cwd()))) {
234
386
  defaultName = `${RegExp.$1 || ''}${RegExp.$2 || 'whistle.'}${RegExp.$3}`;
235
387
  }
236
- console.log('\n\nFor help see https://github.com/avwo/lack\n\n'); // eslint-disable-line
237
- const { name } = await inquirer.prompt([
388
+ console.log('\nFor help see https://github.com/avwo/lack\n'); // eslint-disable-line
389
+ const name = hasHooks ? defaultName : (await inquirer.prompt([
238
390
  {
239
391
  type: 'input',
240
392
  name: 'name',
241
393
  message: 'Plugin Name:',
242
394
  default: defaultName,
243
- validate: (input) => {
244
- return NAME_RE.test(input) || NAME_TIPS;
245
- },
395
+ validate: (input) => NAME_RE.test(input) || NAME_TIPS,
246
396
  },
247
- ]);
397
+ ])).name;
248
398
  if (!NAME_RE.test(name)) {
249
399
  throw new Error(NAME_TIPS);
250
400
  }
251
401
  pkg.name = `${RegExp.$1 || ''}${RegExp.$2 || 'whistle.'}${RegExp.$3}`;
252
- pkg.version = pkg.version || '1.0.0';
253
- pkg.description = pkg.description || '';
402
+ const defaultVersion = pkg.version || '1.0.0';
403
+ let version;
404
+ let description;
405
+ if (!hasHooks) {
406
+ version = (await inquirer.prompt([
407
+ {
408
+ type: 'input',
409
+ name: 'version',
410
+ message: 'Version:',
411
+ default: defaultVersion,
412
+ },
413
+ ])).version;
414
+ description = (await inquirer.prompt([
415
+ {
416
+ type: 'input',
417
+ name: 'description',
418
+ message: 'Description:',
419
+ default: pkg.description || null,
420
+ },
421
+ ])).description;
422
+ }
423
+ pkg.version = trim(version) || defaultVersion;
424
+ pkg.description = trim(description) || pkg.description || '';
425
+ pkg.whistleConfig = pkg.whistleConfig || {};
254
426
 
255
427
  const template = await selectTemplate();
256
428
  const uiServer = await selectUIServer();
@@ -260,7 +432,8 @@ module.exports = async () => {
260
432
  const statsServers = await selectStatsServers();
261
433
  const pipeServers = await selectPipeServers();
262
434
  const rulesFiles = await selectRulesFiles();
263
- const msg = [`\n\n\nPlugin Name: ${pkg.name}`, `\nTemplate: ${template}`];
435
+ const msg = [`Plugin Name: ${pkg.name}`, `\nVersion: ${pkg.version}`,
436
+ `\nDescription: ${pkg.description}`, `\nTemplate: ${template}`];
264
437
  if (authFn) {
265
438
  msg.push('\nAuth function: Yes');
266
439
  }
@@ -274,19 +447,28 @@ module.exports = async () => {
274
447
  addMsg(statsServers, msg, 'Stats Servers:');
275
448
  addMsg(pipeServers, msg, 'Pipe Servers:');
276
449
  addMsg(rulesFiles, msg, 'Rules Files:');
277
- if (msg.length < 3) {
450
+ const len = msg.length;
451
+ if (len < 4) {
278
452
  return;
279
453
  }
280
- msg.push('\nIs this ok?');
281
- const { ok } = await inquirer.prompt([
282
- {
283
- type: 'confirm',
284
- name: 'ok',
285
- message: msg.join('\n'),
286
- },
287
- ]);
454
+ if (len === 4) {
455
+ msg.pop();
456
+ }
457
+ let ok = hasHooks;
288
458
  if (!ok) {
289
- return;
459
+ msg.push('\nIs this ok?');
460
+ ok = (await inquirer.prompt([
461
+ {
462
+ type: 'confirm',
463
+ name: 'ok',
464
+ message: msg.join('\n'),
465
+ },
466
+ ])).ok;
467
+ if (!ok) {
468
+ return;
469
+ }
470
+ } else {
471
+ console.log(msg.join('\n')); // eslint-disable-line
290
472
  }
291
473
 
292
474
  initReadme(pkg);
@@ -333,17 +515,18 @@ module.exports = async () => {
333
515
  if (hasChanged) {
334
516
  fs.writeFileSync('index.js', indexFile);
335
517
  }
336
- setPackage(pkg, uiServer);
518
+ setPackage(pkg, uiServer, hasChanged);
337
519
  fs.writeFileSync('package.json', JSON.stringify(pkg, null, ' '));
338
520
  let showInstall = uiServer;
339
- if (!isJs) {
521
+ if (!isJs && (hasChanged || !fs.existsSync('assets/ts/src/types'))) {
340
522
  showInstall = true;
341
523
  copySync('assets/ts/src/types/base.d.ts', 'src/types/base.d.ts');
342
524
  copySync('assets/ts/src/types/global.d.ts', 'src/types/global.d.ts');
343
525
  copySync('assets/ts/tsconfig.json', 'tsconfig.json');
344
526
  }
345
527
  if (showInstall) {
346
- console.log(`\nRun \`npm i\` to install dependencies`); // eslint-disable-line
528
+ console.log(`\nRun \`npm i\` to install dependencies\n`); // eslint-disable-line
529
+ } else {
530
+ console.log(); // eslint-disable-line
347
531
  }
348
- console.log(`${showInstall ? '\n' : '\n\n'}For help see https://github.com/avwo/lack\n\n`); // eslint-disable-line
349
532
  };
package/bin/watch.js CHANGED
@@ -8,6 +8,7 @@ const HOME_DIR_RE = /^[~~]\//;
8
8
  const PLUGIN_NAME_RE = /^(?:@[\w-]+\/)?(whistle\.[a-z\d_-]+)$/;
9
9
  const REL_RE = /^\.\.[\\/]+/;
10
10
  const SLASH_RE = /[\\/]/;
11
+ const BOUNDARY = '\n************************************************\n';
11
12
 
12
13
  const getWhistlePath = () => {
13
14
  const dir = process.env.WHISTLE_PATH;
@@ -17,6 +18,10 @@ const getWhistlePath = () => {
17
18
  return path.join(os.homedir(), '.WhistleAppData');
18
19
  };
19
20
 
21
+ const showLog = (msg) => {
22
+ console.log(msg); // eslint-disable-line
23
+ };
24
+
20
25
  const DEV_PLUGINS = path.join(getWhistlePath(), 'dev_plugins');
21
26
  const ROOT = process.cwd();
22
27
  const pkgFile = path.join(ROOT, 'package.json');
@@ -80,13 +85,13 @@ module.exports = (dirs) => {
80
85
  fs.unlinkSync(logFile); // eslint-disable-line
81
86
  } catch (e) {}
82
87
  fse.ensureDirSync(DEV_PLUGINS);
83
- const watchList = ['index.js', 'rules.txt', '_rules.txt', 'reqRules.txt', 'resRules.txt',
84
- '_values.txt', 'lib', 'dist', 'public', 'initial.js', 'initialize.js'];
88
+ const watchList = ['index.js', 'rules.txt', '_rules(reqRules).txt', '_values.txt',
89
+ 'resRules.txt', 'lib', 'dist', 'public', 'initial(initialize).js'];
85
90
  if (dirs && typeof dirs === 'string') {
86
91
  dirs.split(',').forEach((dir) => {
87
- dir = dir.trim();
88
- if (dir && watchList.indexOf(dir) === -1) {
89
- watchList.push(dir);
92
+ const d = dir.trim();
93
+ if (d && watchList.indexOf(d) === -1) {
94
+ watchList.push(d);
90
95
  }
91
96
  });
92
97
  }
@@ -104,15 +109,17 @@ module.exports = (dirs) => {
104
109
  return true;
105
110
  }
106
111
  }
112
+ return false;
107
113
  };
108
- console.log(`Watching the following files/folders changes:\n${tips}`); // eslint-disable-line
109
- console.log('\n*********************************************\n'); // eslint-disable-line
114
+ showLog(BOUNDARY);
115
+ showLog(`Watching the following files/folders changes:\n${tips}`);
116
+ showLog(BOUNDARY);
110
117
  const ignoredRe = /(^|[/\\])(\..|node_modules([/\\]|$))/;
111
118
  chokidar.watch(watchList, {
112
119
  ignored: ignoredRe,
113
120
  }).on('raw', (_, filename, details) => {
114
- if (filename.includes('package.json') || filename.includes('.console.log') ||
115
- ignoredRe.test(filename)) {
121
+ if (filename.includes('package.json') || filename.includes('.console.log')
122
+ || ignoredRe.test(filename)) {
116
123
  return;
117
124
  }
118
125
  const watchedPath = (details && details.watchedPath) || filename;
@@ -122,8 +129,7 @@ module.exports = (dirs) => {
122
129
  }
123
130
  clearTimeout(timer);
124
131
  timer = setTimeout(() => {
125
- console.log(''); // eslint-disable-line
126
- console.log(`${hasSlash ? watchedPath : filename} is changed.`); // eslint-disable-line
132
+ showLog(`\n${hasSlash ? watchedPath : filename} is changed.`);
127
133
  touch();
128
134
  }, 1000);
129
135
  }).on('error', () => {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lack",
3
- "version": "1.3.17",
3
+ "version": "1.4.0",
4
4
  "description": "whistle extension tools",
5
5
  "author": "avenwu <avwu@qq.com>",
6
6
  "license": "MIT",
@@ -35,9 +35,8 @@
35
35
  "lru-cache": "^6.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "*",
39
- "babel-eslint": "^10.0.1",
40
- "eslint": "^5.16.0",
41
- "eslint-config-imweb": "^0.2.17"
38
+ "eslint": "^8.57.1",
39
+ "eslint-config-airbnb-base": "^15.0.0",
40
+ "eslint-plugin-import": "^2.32.0"
42
41
  }
43
42
  }