├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── package.json ├── src ├── ansi.ts ├── asyncQueue.ts ├── base64.ts ├── commandLine.ts ├── crypto.ts ├── css.ts ├── deferred.ts ├── eventEmitter.ts ├── fileSystem.ts ├── fileSystemSync.ts ├── fileSystemWatcher.ts ├── html.ts ├── htmlEntities.json ├── httpServer.ts ├── index.ts ├── js.ts ├── json.ts ├── jsx.ts ├── lineColumn.ts ├── logger.ts ├── matcher.ts ├── memoryFileSystem.ts ├── misc.ts ├── net.ts ├── path.ts ├── process.ts ├── request.ts ├── require.ts ├── resolver.ts ├── sourceMap.ts ├── textDocument.ts ├── textWriter.ts ├── tpl.ts ├── url.ts ├── vm.ts ├── webSocket.ts ├── workerPool.ts └── zipFile.ts ├── test ├── ansi.test.ts ├── asyncQueue.test.ts ├── base64.test.ts ├── commandLine.test.ts ├── crypto.test.ts ├── css.test.ts ├── deferred.test.ts ├── eventEmitter.test.ts ├── fileSystem.test.ts ├── fileSystemSync.test.ts ├── fileSystemWatcher.test.ts ├── helpers │ ├── consoleHelper.ts │ └── fsHelper.ts ├── html.test.ts ├── httpServer.test.ts ├── js.test.ts ├── json.test.ts ├── jsx.test.ts ├── lineColumn.test.ts ├── logger.test.ts ├── matcher.test.ts ├── memoryFileSystem.test.ts ├── misc.test.ts ├── path.test.ts ├── process.test.ts ├── request.test.ts ├── require.test.ts ├── resolver.test.ts ├── sourceMap.test.ts ├── textDocument.test.ts ├── textWriter.test.ts ├── tpl.test.ts ├── url.test.ts ├── vm.test.ts ├── webSocket.test.ts ├── workerPool.test.ts └── zipFile.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | trim_trailing_whitespace = true 7 | 8 | [*.{md,txt}] 9 | trim_trailing_whitespace = false 10 | 11 | [{*.yml,*.yaml,package.json}] 12 | indent_style = space 13 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 前端项目 2 | 3 | ## 私有文件 4 | __* 5 | 6 | ## 生成文件 7 | dist/ 8 | package-lock.json 9 | pnpm-lock.yaml 10 | yarn.lock 11 | 12 | ## Node.js 13 | node_modules/ 14 | jspm_packages/ 15 | bower_components/ 16 | .nyc_output/ 17 | coverage/ 18 | .eslintcache 19 | 20 | # 编辑器 21 | 22 | ## Sublime 23 | *.sublime-workspace 24 | 25 | ## Webstorm 26 | .idea/ 27 | *.iws 28 | 29 | ## Visual Studio 30 | .vs/ 31 | **/[Bb]in/[Dd]ebug/ 32 | **/[Bb]in/[Rr]elease/ 33 | [Oo]bj/ 34 | *.sln.* 35 | *.vshost.* 36 | *.suo 37 | *.user 38 | _ReSharper*/ 39 | *.ReSharper 40 | .ntvs_analysis.dat 41 | **/[Bb]in/Microsoft.NodejsTools.WebRole.dll 42 | 43 | # 操作系统 44 | 45 | ## 临时文件 46 | *.tmp 47 | *.log 48 | *~ 49 | ._* 50 | 51 | ## Windows 52 | $RECYCLE.BIN/ 53 | Desktop.ini 54 | ehthumbs.db 55 | Thumbs.db 56 | 57 | ## OSX 58 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | - 10.12 6 | script: 7 | - npm run coverage 8 | after_script: 9 | - npm run coveralls -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug Tests(Current)", 8 | "runtimeExecutable": "mocha", 9 | "runtimeArgs": [ 10 | "-r", 11 | "ts-node/register/transpile-only", 12 | "--ui", 13 | "exports", 14 | "--colors", 15 | "--no-timeout", 16 | "${relativeFile}" 17 | ], 18 | "internalConsoleOptions": "openOnSessionStart", 19 | "skipFiles": [ 20 | "/**", 21 | "node_modules/mocha/**", 22 | "node_modules/ts-node/**", 23 | "node_modules/v8-compile-cache/**" 24 | ] 25 | }, 26 | { 27 | "type": "node", 28 | "request": "launch", 29 | "name": "Debug Tests", 30 | "runtimeExecutable": "mocha", 31 | "runtimeArgs": [ 32 | "-r", 33 | "ts-node/register/transpile-only", 34 | "--ui", 35 | "exports", 36 | "--colors", 37 | "--no-timeout", 38 | "${workspaceFolder}/**/*.test.ts" 39 | ], 40 | "internalConsoleOptions": "openOnSessionStart", 41 | "skipFiles": [ 42 | "/**", 43 | "node_modules/mocha/**", 44 | "node_modules/ts-node/**", 45 | "node_modules/v8-compile-cache/**" 46 | ] 47 | }, 48 | { 49 | "type": "node", 50 | "request": "attach", 51 | "name": "Attach", 52 | "skipFiles": [ 53 | "/**", 54 | "node_modules/mocha/**", 55 | "node_modules/ts-node/**", 56 | "node_modules/v8-compile-cache/**" 57 | ] 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "[yaml]": { 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2 6 | } 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "group": { 6 | "kind": "build", 7 | "isDefault": true 8 | }, 9 | "label": "Build", 10 | "type": "shell", 11 | "command": "npm run build --silent", 12 | "problemMatcher": [], 13 | "presentation": { 14 | "showReuseMessage": false, 15 | "clear": true 16 | } 17 | }, 18 | { 19 | "group": "build", 20 | "label": "Watch", 21 | "type": "shell", 22 | "command": "npm run watch --silent", 23 | "problemMatcher": [], 24 | "presentation": { 25 | "showReuseMessage": false, 26 | "clear": true 27 | } 28 | }, 29 | { 30 | "group": "test", 31 | "label": "Test", 32 | "type": "shell", 33 | "command": "npm run test --silent", 34 | "problemMatcher": [], 35 | "presentation": { 36 | "showReuseMessage": false, 37 | "clear": true 38 | } 39 | }, 40 | { 41 | "group": "test", 42 | "label": "Coverage", 43 | "type": "shell", 44 | "command": "npm run coverage --silent", 45 | "problemMatcher": [], 46 | "presentation": { 47 | "showReuseMessage": false, 48 | "clear": true 49 | } 50 | }, 51 | { 52 | "group": { 53 | "kind": "test", 54 | "isDefault": true 55 | }, 56 | "type": "shell", 57 | "label": "Coverage(Current)", 58 | "command": "npm run coverage \"${relativeFile}\" --silent", 59 | "problemMatcher": [], 60 | "presentation": { 61 | "showReuseMessage": false, 62 | "clear": true 63 | } 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Teal License 2 | 3 | Copyright (c) 2010-2020, The Teal Team 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of the Teal nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | * Software development libraries or toolkits derived from this software 18 | cannot be used, accessed, installed or obtained by persons from 19 | outside your organization or company. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY 22 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 25 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutils", 3 | "private": true, 4 | "version": "2.4.0", 5 | "repository": "https://github.com/Teal/TUtils", 6 | "description": "200+ well-tested helper functions & classes for Node.js, including globbing, file watcher, zip, socket and so on | 200+ 个 NodeJS 工具库,顶 140+ 个 NPM 依赖,比如通配符、文件监听、网络库、Zip 压缩等", 7 | "license": "SEE LICENSE IN LICENSE", 8 | "author": "xuld ", 9 | "engines": { 10 | "node": ">=10.12" 11 | }, 12 | "devDependencies": { 13 | "@types/mocha": "^9.1.1", 14 | "@types/node": "^18.7.15", 15 | "coveralls": "^3.1.1", 16 | "mocha": "^10.0.0", 17 | "nyc": "^15.1.0", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^4.8.2" 20 | }, 21 | "scripts": { 22 | "build": "tsc -p tsconfig.json --declaration && node -e \"var pkg = require('./package.json'); delete pkg.private; delete pkg.devDependencies; delete pkg.scripts; fs.writeFileSync('dist/package.json', JSON.stringify(pkg, undefined, 2)); fs.copyFileSync('README.md', 'dist/README.md'); fs.copyFileSync('LICENSE', 'dist/LICENSE');\"", 23 | "watch": "tsc -p tsconfig.json --declaration --watch", 24 | "test": "node -e \"if (process.argv[7]) { process.argv[6] = process.argv[7].replace(/^src([\\\\/].*)\\.ts/, 'test$' + '1.test.ts'); process.argv.splice(7, 1) } require('mocha/bin/mocha')\" mocha -r ts-node/register/transpile-only --ui exports '**/*.test.ts'", 25 | "coverage": "node -e \"process.argv[8] = require.resolve('mocha/bin/mocha'); if (process.argv[14]) { process.argv[13] = process.argv[14].replace(/^src([\\\\/].*)\\.ts/, 'test$' + '1.test.ts'); process.argv.splice(14, 1) } require('nyc/bin/nyc')\" nyc --reporter=text-summary --reporter=html --report-dir=coverage --temp-dir=coverage/.nyc_output --extension=.ts --include=src/** mocha -r ts-node/register/transpile-only --ui exports '**/*.test.ts'", 26 | "coveralls": "nyc report --reporter=text-lcov --report-dir=coverage --temp-dir=coverage/.nyc_output | coveralls" 27 | } 28 | } -------------------------------------------------------------------------------- /src/asyncQueue.ts: -------------------------------------------------------------------------------- 1 | /** 表示一个异步队列,用于串行执行多个异步任务 */ 2 | export class AsyncQueue implements PromiseLike { 3 | 4 | /** 正在执行的第一个异步任务,如果没有任务正在执行则为 `undefined` */ 5 | private _firstTask?: { 6 | readonly callback: () => any 7 | readonly resolve: (value: any) => void 8 | readonly reject: (reason: any) => void 9 | next?: AsyncQueue["_firstTask"] 10 | } 11 | 12 | /** 正在执行的最后一个异步任务,如果没有任务正在执行则为 `undefined` */ 13 | private _lastTask?: AsyncQueue["_firstTask"] 14 | 15 | /** 判断是否有异步任务正在执行 */ 16 | get isEmpty() { return !this._lastTask } 17 | 18 | /** 19 | * 串行执行一个同步或异步函数 20 | * @param callback 待执行的函数 21 | * @returns 返回表示当前函数已执行完成的确认对象 22 | */ 23 | then(callback: (value?: any) => T | Promise) { 24 | return new Promise((resolve, reject) => { 25 | const nextTask = { callback, resolve, reject } 26 | if (this._lastTask) { 27 | this._lastTask = this._lastTask.next = nextTask 28 | } else { 29 | this._firstTask = this._lastTask = nextTask 30 | this._next() 31 | } 32 | }) 33 | } 34 | 35 | /** 执行下一个任务 */ 36 | private async _next() { 37 | const currentTask = this._firstTask! 38 | try { 39 | currentTask.resolve(await currentTask.callback()) 40 | } catch (e) { 41 | currentTask.reject(e) 42 | } finally { 43 | const nextTask = this._firstTask = currentTask.next 44 | if (nextTask) { 45 | this._next() 46 | } else { 47 | this._lastTask = undefined 48 | } 49 | } 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/base64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用 Base64 编码指定的数据 3 | * @param data 要编码的字符串或二进制数据 4 | */ 5 | export function encodeBase64(data: string | Buffer) { 6 | return (Buffer.isBuffer(data) ? data : Buffer.from(data)).toString("base64") 7 | } 8 | 9 | /** 10 | * 解码指定的 Base64 字符串,如果解码失败则返回空字符串 11 | * @param value 要解码的 Base64 字符串 12 | */ 13 | export function decodeBase64(value: string) { 14 | return Buffer.from(value, "base64").toString() 15 | } 16 | 17 | /** 18 | * 编码指定数据的统一资源标识符(URI) 19 | * @param mimeType 数据的 MIME 类型 20 | * @param data 要编码的字符串或二进制数据 21 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 22 | */ 23 | export function encodeDataURI(mimeType: string, data: string | Buffer) { 24 | if (typeof data === "string") { 25 | return `data:${mimeType},${encodeURIComponent(data)}` 26 | } 27 | return `data:${mimeType};base64,${encodeBase64(data)}` 28 | } 29 | 30 | /** 31 | * 解码指定的统一资源标识符(URI),如果解码失败则返回 `null` 32 | * @param value 要解码的统一资源标识符(URI) 33 | */ 34 | export function decodeDataURI(value: string) { 35 | const match = /^data:([^,]*),/.exec(value) 36 | if (!match) { 37 | return null 38 | } 39 | let mimeType = match[1] 40 | let data: string | Buffer = value.substring(match[0].length) 41 | if (mimeType.endsWith(";base64")) { 42 | mimeType = mimeType.slice(0, -7 /*";base64".length*/) 43 | data = Buffer.from(data, "base64") 44 | } else { 45 | try { 46 | data = decodeURIComponent(data) 47 | } catch { } 48 | } 49 | return { mimeType, data } 50 | } -------------------------------------------------------------------------------- /src/commandLine.ts: -------------------------------------------------------------------------------- 1 | import { wrapString, getStringWidth } from "./ansi" 2 | import { offExit, onExit } from "./process" 3 | 4 | /** 显示命令行的光标 */ 5 | export function showCursor() { 6 | const stdout = process.stdout 7 | if (stdout.isTTY) { 8 | offExit(showCursor) 9 | stdout.write("\x1b[?25h") 10 | } 11 | } 12 | 13 | /** 隐藏命令行的光标 */ 14 | export function hideCursor() { 15 | const stdout = process.stdout 16 | if (stdout.isTTY) { 17 | onExit(showCursor) 18 | stdout.write("\x1b[?25l") 19 | } 20 | } 21 | 22 | /** 清空命令行(含缓冲区)*/ 23 | export function clear() { 24 | const stdout = process.stdout 25 | if (stdout.isTTY) { 26 | stdout.write(process.platform === "win32" ? "\x1b[2J\x1b[0f\x1bc" : "\x1b[2J\x1b[3J\x1b[H") 27 | } 28 | } 29 | 30 | /** 31 | * 解析命令行参数 32 | * @param commandLineOptions 所有内置的命令行选项 33 | * @param onError 解析出错后的回调函数 34 | * @param argv 要解析的命令行参数列表 35 | * @param startIndex 开始解析的索引 36 | * @returns 返回一个对象,对象的键是参数名或索引,对象的值是对应的参数值(如果没有参数值则为 `true`) 37 | * @example 38 | * ``` 39 | * // 假设启动程序的命令行是:`node a --x b --y c` 40 | * parseCommandLineArguments({ 41 | * "--x": { 42 | * argument: "argument" 43 | * } 44 | * }) 45 | * // {"0": "a", "1": "c", "x": "b", "y": true} 46 | * ``` 47 | */ 48 | export function parseCommandLineArguments(commandLineOptions?: { [option: string]: CommandLineOption }, onError?: (message: string) => void, argv = process.argv, startIndex = 2) { 49 | const result: { [option: string]: string | string[] | true | typeof result } = { __proto__: null! } 50 | let index = 0 51 | for (; startIndex < argv.length; startIndex++) { 52 | let argument = argv[startIndex] 53 | if (argument.charCodeAt(0) === 45 /*-*/) { 54 | // -- 后的参数直接解析成键值对 55 | if (argument === "--") { 56 | result["--"] = parseCommandLineArguments(undefined, onError, argv, startIndex + 1) 57 | break 58 | } 59 | let value: string | undefined 60 | // 将 --x=a 转为 --x a 61 | const equalIndex = argument.search(/[=:]/) 62 | if (equalIndex >= 0) { 63 | value = argument.substring(equalIndex + 1) 64 | argument = argument.substring(0, equalIndex) 65 | } 66 | // 查找关联的选项配置 67 | let key = argument 68 | let commandLineOption: CommandLineOption | undefined 69 | if (commandLineOptions) { 70 | commandLineOption = commandLineOptions[argument] 71 | if (!commandLineOption) { 72 | for (const currentKey in commandLineOptions) { 73 | const current = commandLineOptions[currentKey] 74 | if (current.alias) { 75 | if (Array.isArray(current.alias)) { 76 | if (current.alias.indexOf(argument) >= 0) { 77 | key = currentKey 78 | commandLineOption = current 79 | break 80 | } 81 | } else if (current.alias === argument) { 82 | key = currentKey 83 | commandLineOption = current 84 | break 85 | } 86 | } 87 | } 88 | } 89 | } 90 | // 读取选项值 91 | const oldValue = result[key] 92 | if (commandLineOption) { 93 | if (commandLineOption.argument) { 94 | if (value === undefined) { 95 | if (startIndex + 1 < argv.length && argv[startIndex + 1].charCodeAt(0) !== 45 /*-*/) { 96 | value = argv[++startIndex] 97 | } else if (commandLineOption.default !== undefined) { 98 | value = commandLineOption.default 99 | } else { 100 | onError?.(`Option '${argument}' requires an argument`) 101 | continue 102 | } 103 | } 104 | if (commandLineOption.multiple) { 105 | if (oldValue) { 106 | (oldValue as string[]).push(value!) 107 | } else { 108 | result[key] = [value!] 109 | } 110 | } else { 111 | if (oldValue !== undefined) { 112 | onError?.(`Duplicate option '${argument}'`) 113 | } 114 | result[key] = value! 115 | } 116 | } else if (oldValue && !commandLineOption.multiple) { 117 | onError?.(`Duplicate option '${argument}'`) 118 | } else { 119 | if (value !== undefined) { 120 | onError?.(`Option '${argument}' has no argument, got '${value}'`) 121 | } 122 | result[key] = true 123 | } 124 | } else { 125 | if (value === undefined && startIndex + 1 < argv.length && argv[startIndex + 1].charCodeAt(0) !== 45 /*-*/) { 126 | value = argv[++startIndex] 127 | } 128 | if (value !== undefined) { 129 | if (Array.isArray(oldValue)) { 130 | oldValue.push(value) 131 | } else if (typeof oldValue === "string") { 132 | result[key] = [oldValue, value] 133 | } else { 134 | result[key] = value 135 | } 136 | } else if (oldValue === undefined) { 137 | result[key] = true 138 | } 139 | } 140 | } else { 141 | result[index++] = argument 142 | } 143 | } 144 | return result 145 | } 146 | 147 | /** 148 | * 格式化所有选项 149 | * @param commandLineOptions 所有内置的命令行选项 150 | * @param maxWidth 允许布局的最大宽度(一般地,西文字母宽度为 1,中文文字宽度为 2) 151 | */ 152 | export function formatCommandLineOptions(commandLineOptions: { [option: string]: CommandLineOption }, maxWidth = process.stdout.columns || Infinity) { 153 | // 计算所有的标题 154 | const keys = new Map() 155 | let width = 0 156 | for (const key in commandLineOptions) { 157 | const commandOption = commandLineOptions[key] 158 | if (!commandOption.description) { 159 | continue 160 | } 161 | let title = key 162 | if (commandOption.alias) { 163 | title = `${Array.isArray(commandOption.alias) ? commandOption.alias.join(", ") : commandOption.alias}, ${title}` 164 | } 165 | if (commandOption.argument) { 166 | title += commandOption.default === undefined ? ` <${commandOption.argument}>` : ` [${commandOption.argument}]` 167 | } 168 | width = Math.max(width, getStringWidth(title)) 169 | keys.set(title, commandOption) 170 | } 171 | // 加上左右各两个空格 172 | width += 4 173 | // 生成最终结果 174 | let result = "" 175 | for (const [title, commandOption] of keys.entries()) { 176 | if (result) { 177 | result += "\n" 178 | } 179 | if (commandOption.group) { 180 | result += `\n${commandOption.group}:\n` 181 | } 182 | result += ` ${title.padEnd(width - 2)}${wrapString(commandOption.description! + (commandOption.default ? ` [default: ${commandOption.default}]` : ""), 2, maxWidth - width).join(`\n${" ".repeat(width)}`)}` 183 | } 184 | return result 185 | } 186 | 187 | /** 表示一个命令行选项 */ 188 | export interface CommandLineOption { 189 | /** 当前选项所属的分组,主要用于格式化时显示 */ 190 | group?: string 191 | /** 当前选项的别名 */ 192 | alias?: string | string[] 193 | /** 当前选项的描述,主要用于格式化时显示 */ 194 | description?: string 195 | /** 当前选项的参数名,如果未设置说明没有参数 */ 196 | argument?: string 197 | /** 当前选项的默认值,如果未设置则表示当前选项是必填的 */ 198 | default?: any 199 | /** 是否允许重复使用当前选项 */ 200 | multiple?: boolean 201 | } 202 | 203 | /** 204 | * 读取命令行的输入 205 | * @param message 提示的信息 206 | * @example await commandLine.input("请输入名字:") 207 | */ 208 | export async function input(message = "") { 209 | return new Promise(resolve => { 210 | const result = (require("readline") as typeof import("readline")).createInterface({ 211 | input: process.stdin, 212 | output: process.stdout 213 | }) 214 | result.question(message, answer => { 215 | result.close() 216 | resolve(answer) 217 | }) 218 | }) 219 | } 220 | 221 | /** 222 | * 让用户选择一项 223 | * @param choices 要展示的选择项 224 | * @param message 提示的信息 225 | * @param defaultValue 默认值 226 | * @example await commandLine.select(["打开", "关闭"], "请选择一个:") 227 | */ 228 | export async function select(choices: string[], message = "", defaultValue?: string) { 229 | message = `\n${choices.map((choice, index) => `[${index + 1}] ${choice}`).join("\n")} \n\n${message || ""} ` 230 | while (true) { 231 | const line = await input(message) 232 | if (line) { 233 | const index = +line - 1 234 | if (index >= 0 && index < choices.length) { 235 | return choices[index] 236 | } 237 | return line 238 | } 239 | if (defaultValue !== undefined) { 240 | return defaultValue 241 | } 242 | } 243 | } -------------------------------------------------------------------------------- /src/crypto.ts: -------------------------------------------------------------------------------- 1 | import { createHash, BinaryToTextEncoding } from "crypto" 2 | 3 | /** 4 | * 计算指定数据的 MD5 值 5 | * @param data 要计算的字符串或二进制数据 6 | * @param encoding 要使用的编码,默认为 32 位小写十六进制字符串 7 | */ 8 | export function md5(data: string | Buffer, encoding: BinaryToTextEncoding = "hex") { 9 | const hash = createHash("md5") 10 | hash.update(data) 11 | return hash.digest(encoding) 12 | } 13 | 14 | /** 15 | * 计算指定数据的 SHA-1 值 16 | * @param data 要计算的字符串或二进制数据 17 | * @param encoding 要使用的编码,默认为 40 位小写十六进制字符串 18 | */ 19 | export function sha1(data: string | Buffer, encoding: BinaryToTextEncoding = "hex") { 20 | const hash = createHash("sha1") 21 | hash.update(data) 22 | return hash.digest(encoding) 23 | } -------------------------------------------------------------------------------- /src/css.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 编码 CSS 中的特殊字符 3 | * @param value 要编码的字符串 4 | */ 5 | export function encodeCSS(value: string) { 6 | if (value === "-") { 7 | return "\\-" 8 | } 9 | let result = "" 10 | for (let i = 0; i < value.length; i++) { 11 | const char = value.charCodeAt(i) 12 | // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point 13 | if (char <= 0x001F || char === 0x007F || (i <= 1 && char >= 0x0030 /*0*/ && char <= 0x0039 /*9*/ && (i === 0 || value.charCodeAt(0) === 0x002D /*-*/))) { 14 | result += `\\${char.toString(16)} ` 15 | continue 16 | } 17 | if (char >= 0x0080 || char == 0x002D /*-*/ || char == 0x005F /*_*/ || char >= 0x0030 /*0*/ && char <= 0x0039 /*9*/ || char >= 0x0041 /*A*/ && char <= 0x005A /*Z*/ || char >= 0x0061 /*a*/ && char <= 0x007A /*z*/) { 18 | result += value.charAt(i) 19 | continue 20 | } 21 | // https://drafts.csswg.org/cssom/#escape-a-character 22 | result += `\\${value.charAt(i)}` 23 | } 24 | return result 25 | } 26 | 27 | /** 28 | * 解码 CSS 转义字符 29 | * @param value 要解码的字符串 30 | * @see http://dev.w3.org/csswg/css-syntax/#ident-token-diagram 31 | */ 32 | export function decodeCSS(value: string) { 33 | return value.replace(/\\(?:([\da-fA-F]{1,6})\s?|.)/sg, (source, unicode: string) => { 34 | if (unicode) { 35 | return String.fromCodePoint(parseInt(unicode, 16) || 0xFFFD) 36 | } 37 | return source.charAt(1) 38 | }) 39 | } 40 | 41 | /** 42 | * 编码 CSS 字符串并添加引号 43 | * @param value 要编码的字符串 44 | * @param quote 要添加的引号,默认根据字符串自动推导 45 | */ 46 | export function quoteCSSString(value: string, quote = /[)'"]/.test(value) ? '"' : "") { 47 | let result = quote 48 | for (let i = 0; i < value.length; i++) { 49 | const char = value.charCodeAt(i) 50 | if (char <= 0x001F || char === 0x007F) { 51 | result += `\\${char.toString(16)} ` 52 | continue 53 | } 54 | switch (char) { 55 | case 41 /*)*/: 56 | case 34 /*"*/: 57 | case 39 /*'*/: 58 | result += quote && char !== quote.charCodeAt(0) ? value.charAt(i) : `\\${value.charAt(i)}` 59 | continue 60 | case 92 /*\*/: 61 | result += `\\${value.charAt(i)}` 62 | continue 63 | } 64 | result += value.charAt(i) 65 | } 66 | result += quote 67 | return result 68 | } 69 | 70 | /** 71 | * 删除 CSS 字符串的引号并解码 72 | * @param value 要解码的字符串 73 | */ 74 | export function unquoteCSSString(value: string) { 75 | return decodeCSS(value.replace(/^(['"])(.*)\1$/s, "$2")) 76 | } -------------------------------------------------------------------------------- /src/deferred.ts: -------------------------------------------------------------------------------- 1 | /** 表示一个延时等待对象,用于同时等待多个异步任务 */ 2 | export class Deferred implements PromiseLike { 3 | 4 | /** 第一个待执行的任务 */ 5 | private _firstTask?: { 6 | resolve(data?: any): any 7 | reject?(reason: any): any 8 | next?: Deferred["_firstTask"] 9 | } 10 | 11 | /** 最后一个待执行的任务 */ 12 | private _lastTask?: Deferred["_firstTask"] 13 | 14 | /** 是否已触发错误 */ 15 | private _rejected?: boolean 16 | 17 | /** 关联的错误对象 */ 18 | private _error?: any 19 | 20 | /** 获取正在执行的异步任务数 */ 21 | rejectCount = 0 22 | 23 | /** 记录即将执行一个异步任务 */ 24 | reject() { 25 | this.rejectCount++ 26 | } 27 | 28 | /** 记录一个异步任务已完成 */ 29 | resolve() { 30 | if (--this.rejectCount === 0) { 31 | process.nextTick(() => { 32 | if (this._rejected) { 33 | while (this._firstTask) { 34 | const task = this._firstTask 35 | this._firstTask = this._firstTask.next 36 | if (task.reject) { 37 | try { 38 | task.reject(this._error) 39 | } catch { } 40 | } 41 | } 42 | } else { 43 | while (this.rejectCount === 0 && this._firstTask) { 44 | let task = this._firstTask 45 | this._firstTask = this._firstTask.next 46 | try { 47 | task.resolve() 48 | } catch (e) { 49 | this._rejected = true 50 | this._error = e 51 | this._lastTask = this._firstTask = undefined 52 | do { 53 | if (task.reject) { 54 | try { 55 | task.reject(e) 56 | } catch { } 57 | } 58 | } while (task = task.next!) 59 | return 60 | } 61 | } 62 | } 63 | }) 64 | } 65 | } 66 | 67 | /** 68 | * 添加所有异步任务执行完成后的回调函数 69 | * @param resolve 要执行的回调函数 70 | * @param reject 执行出现错误的回调函数 71 | */ 72 | then(resolve: (_?: any) => any, reject?: (reason: any) => any) { 73 | if (this._firstTask) { 74 | this._lastTask = this._lastTask!.next = { 75 | resolve, 76 | reject 77 | } 78 | } else { 79 | this._lastTask = this._firstTask = { 80 | resolve, 81 | reject 82 | } 83 | } 84 | if (this.rejectCount === 0) { 85 | this.reject() 86 | this.resolve() 87 | } 88 | return this 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/eventEmitter.ts: -------------------------------------------------------------------------------- 1 | /** 表示一个事件触发器,支持异步事件 */ 2 | export class EventEmitter { 3 | 4 | /** 所有已添加的事件处理函数 */ 5 | private _events?: Map 6 | 7 | /** 8 | * 添加一个事件处理函数 9 | * @param eventName 要添加的事件名 10 | * @param eventHandler 要添加的事件处理函数 11 | * @example 12 | * const events = new EventEmitter() 13 | * events.on("error", data => console.log(data)) // 绑定 error 事件 14 | * events.emit("error", "hello") // 触发 error 事件,输出 hello 15 | */ 16 | on(eventName: string, eventHandler: Function) { 17 | const events = this._events || (this._events = new Map()) 18 | const eventHandlers = events.get(eventName) 19 | if (eventHandlers === undefined) { 20 | events.set(eventName, eventHandler) 21 | } else if (typeof eventHandlers === "function") { 22 | events.set(eventName, [eventHandlers, eventHandler]) 23 | } else { 24 | eventHandlers.push(eventHandler) 25 | } 26 | return this 27 | } 28 | 29 | /** 30 | * 添加一个只执行一次的事件处理函数 31 | * @param eventName 要添加的事件名 32 | * @param eventHandler 要添加的事件处理函数 33 | * @example 34 | * const events = new EventEmitter() 35 | * events.once("error", data => console.log(data)) // 绑定 error 事件 36 | * events.emit("error", "hello") // 触发 error 事件,输出 hello 37 | * events.emit("error", "hello") // 不触发事件 38 | */ 39 | once(eventName: string, eventHandler: Function) { 40 | function wrapper(this: EventEmitter, ...args: any[]) { 41 | this.off(eventName, wrapper) 42 | return eventHandler.apply(this, args) 43 | } 44 | return this.on(eventName, wrapper) 45 | } 46 | 47 | /** 48 | * 删除一个或多个事件处理函数 49 | * @param eventName 要删除的事件名,如果不传递此参数,则删除所有事件处理函数 50 | * @param eventHandler 要删除的事件处理函数,如果不传递此参数,则删除指定事件的所有处理函数,如果同一个处理函数被添加多次,则只删除第一个 51 | * @example 52 | * const events = new EventEmitter() 53 | * events.on("error", console.log) // 绑定 error 事件 54 | * events.off("error", console.log) // 解绑 error 事件 55 | * events.emit("error", "hello") // 触发 error 事件,不输出内容 56 | */ 57 | off(eventName?: string, eventHandler?: Function) { 58 | const events = this._events 59 | if (events) { 60 | if (eventName === undefined) { 61 | delete this._events 62 | } else if (eventHandler === undefined) { 63 | events.delete(eventName) 64 | } else { 65 | const eventHandlers = events.get(eventName) 66 | if (eventHandlers !== undefined) { 67 | if (typeof eventHandlers === "function") { 68 | if (eventHandlers === eventHandler) { 69 | events.delete(eventName) 70 | } 71 | } else { 72 | const index = eventHandlers.indexOf(eventHandler) 73 | if (index >= 0) { 74 | eventHandlers.splice(index, 1) 75 | if (eventHandlers.length === 1) { 76 | events.set(eventName, eventHandlers[0]) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | return this 84 | } 85 | 86 | /** 87 | * 触发一个事件,执行已添加的所有事件处理函数 88 | * @param eventName 要触发的事件名 89 | * @param eventArgs 传递给事件处理函数的所有参数 90 | * @returns 如果任一个事件处理函数返回 `false` 则返回 `false`,否则返回 `true` 91 | * @example 92 | * const events = new EventEmitter() 93 | * events.on("error", console.log) // 绑定 error 事件 94 | * events.emit("error", "hello") // 触发 error 事件,输出 hello 95 | */ 96 | async emit(eventName: string, ...eventArgs: any[]) { 97 | const events = this._events 98 | if (events) { 99 | const eventHandlers = events.get(eventName) 100 | if (eventHandlers !== undefined) { 101 | if (typeof eventHandlers === "function") { 102 | return await eventHandlers.apply(this, eventArgs) !== false 103 | } 104 | // 避免在执行事件期间解绑事件,影响后续事件处理函数执行,所以需要复制一份列表 105 | for (const eventHandler of eventHandlers.slice(0)) { 106 | if (await eventHandler.apply(this, eventArgs) === false) { 107 | return false 108 | } 109 | } 110 | } 111 | } 112 | return true 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /src/html.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 编码 HTML 中的特殊字符 3 | * @param value 要编码的字符串 4 | * @desc > [i] 此函数仅编码 `<`、`>` 和 `&` 5 | */ 6 | export function encodeHTML(value: string) { 7 | return value.replace(/[<>&]/g, toHTMLEntity) 8 | } 9 | 10 | /** 11 | * 获取指定字符对应的 HTML 转义字符 12 | * @param char 要处理的字符 13 | */ 14 | function toHTMLEntity(char: string) { 15 | switch (char.charCodeAt(0)) { 16 | case 60 /*<*/: return "<" 17 | case 62 /*>*/: return ">" 18 | case 38 /*&*/: return "&" 19 | case 34 /*"*/: return """ 20 | case 39 /*'*/: return "'" 21 | default: return `&#x${char.codePointAt(0)!.toString(16)};` 22 | } 23 | } 24 | 25 | /** 26 | * 解码 HTML 转义字符 27 | * @param value 要解码的字符串 28 | */ 29 | export function decodeHTML(value: string) { 30 | // 在 HTML 4.1 规范中,实体符号的 `;` 是可选的 31 | // 但在 HTML5 规范中,实体符号区分大小写,且 `;` 是必须的 32 | // 为降低复杂度、提升性能,不考虑兼容历史代码 33 | return value.replace(/&(#(\d+)|#x([\da-f]+)|\w+);/ig, (source, entity?: string, unicode?: string, hex?: string) => { 34 | if (unicode) { 35 | return String.fromCodePoint(parseInt(unicode)) 36 | } 37 | if (hex) { 38 | return String.fromCodePoint(parseInt(hex, 16)) 39 | } 40 | // https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references 41 | switch (entity) { 42 | case "amp": return "&" 43 | case "lt": return "<" 44 | case "gt": return ">" 45 | case "quot": return "\"" 46 | case "apos": return "'" 47 | case "nbsp": return "\u00A0" 48 | default: return (require("./htmlEntities.json") as typeof import("./htmlEntities.json"))[entity!] || source 49 | } 50 | }) 51 | } 52 | 53 | /** 54 | * 编码 HTML 属性值并添加引号 55 | * @param value 要编码的字符串 56 | * @param quote 要添加的引号,默认根据字符串自动推导 57 | */ 58 | export function quoteHTMLAttribute(value: string, quote = value.includes('"') && !value.includes("'") ? "'" : '"') { 59 | return `${quote}${value.replace(quote.charCodeAt(0) === 34 /*"*/ ? /["&]/g : quote.charCodeAt(0) === 39 /*'*/ ? /['&]/g : /["'&<>=\s]/g, toHTMLEntity)}${quote}` 60 | } 61 | 62 | /** 63 | * 删除 HTML 属性值的引号并解码 64 | * @param value 要解码的字符串 65 | */ 66 | export function unquoteHTMLAttribute(value: string) { 67 | return decodeHTML(value.replace(/^(['"])(.*)\1$/s, "$2")) 68 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ansi" 2 | export * from "./asyncQueue" 3 | export * from "./base64" 4 | export * from "./commandLine" 5 | export * from "./crypto" 6 | export * from "./css" 7 | export * from "./deferred" 8 | export * from "./eventEmitter" 9 | export * from "./fileSystem" 10 | export * from "./fileSystemSync" 11 | export * from "./fileSystemWatcher" 12 | export * from "./html" 13 | export * from "./httpServer" 14 | export * from "./js" 15 | export * from "./json" 16 | export * from "./jsx" 17 | export * from "./lineColumn" 18 | export * from "./logger" 19 | export * from "./matcher" 20 | export * from "./memoryFileSystem" 21 | export * from "./misc" 22 | export * from "./net" 23 | export * from "./path" 24 | export * from "./process" 25 | export * from "./request" 26 | export * from "./require" 27 | export * from "./resolver" 28 | export * from "./sourceMap" 29 | export * from "./textDocument" 30 | export * from "./textWriter" 31 | export * from "./tpl" 32 | export * from "./url" 33 | export * from "./vm" 34 | export * from "./webSocket" 35 | export * from "./workerPool" 36 | export * from "./zipFile" -------------------------------------------------------------------------------- /src/js.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 编码 JavaScript 中的特殊字符 3 | * @param value 要编码的字符串 4 | */ 5 | export function encodeJS(value: string) { 6 | return quoteJSString(value, "") 7 | } 8 | 9 | /** 10 | * 解码 JavaScript 转义字符 11 | * @param value 要解码的字符串 12 | */ 13 | export function decodeJS(value: string) { 14 | return value.replace(/\\(?:x([\da-fA-F]{2})|u([\da-fA-F]{4})|u\{([\da-fA-F]+)\}|.)/sg, (source, hex?: string, unicode?: string, unicodeCodePoint?: string) => { 15 | if (source.length > 2) { 16 | return String.fromCodePoint(parseInt(hex || unicode || unicodeCodePoint!, 16)) 17 | } 18 | switch (source.charCodeAt(1)) { 19 | case 34 /*"*/: 20 | return '\"' 21 | case 39 /*'*/: 22 | return "'" 23 | case 92 /*\*/: 24 | return "\\" 25 | case 10 /*\n*/: 26 | case 13 /*\r*/: 27 | return "" 28 | case 110 /*n*/: 29 | return "\n" 30 | case 114 /*r*/: 31 | return "\r" 32 | case 118 /*v*/: 33 | return "\v" 34 | case 116 /*t*/: 35 | return "\t" 36 | case 98 /*b*/: 37 | return "\b" 38 | case 102 /*f*/: 39 | return "\f" 40 | case 48 /*0*/: 41 | return "\0" 42 | default: 43 | return source.charAt(1) 44 | } 45 | }) 46 | } 47 | 48 | /** 49 | * 编码 JavaScript 字符串并添加引号 50 | * @param value 要编码的字符串 51 | * @param quote 要添加的引号,默认根据字符串自动推导 52 | */ 53 | export function quoteJSString(value: string, quote = value.includes('"') && !value.includes("'") ? "'" : '"') { 54 | return `${quote}${value.replace(/["'`\\\n\r\t\0\v\b\f\u2028\u2029]/g, char => { 55 | switch (char.charCodeAt(0)) { 56 | case 34 /*"*/: 57 | case 39 /*'*/: 58 | case 96 /*`*/: 59 | return quote && char.charCodeAt(0) !== quote.charCodeAt(0) ? char : `\\${char}` 60 | case 10 /*\n*/: 61 | return "\\n" 62 | case 13 /*\r*/: 63 | return "\\r" 64 | case 9/*\t*/: 65 | return "\\t" 66 | case 0/*\0*/: 67 | return "\\0" 68 | case 11 /*\v*/: 69 | return "\\v" 70 | case 8 /*\b*/: 71 | return "\\b" 72 | case 12 /*\f*/: 73 | return "\\f" 74 | case 0x2028: 75 | return "\\u2028" 76 | case 0x2029: 77 | return "\\u2029" 78 | default: 79 | return `\\${char}` 80 | } 81 | })}${quote}` 82 | } 83 | 84 | /** 85 | * 删除 JavaScript 字符串的引号并解码 86 | * @param value 要解码的字符串 87 | */ 88 | export function unquoteJSString(value: string) { 89 | return decodeJS(value.replace(/^(['"`])(.*)\1$/s, "$2")) 90 | } -------------------------------------------------------------------------------- /src/json.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 解析 JSON 数据,支持非常规的 JSON 格式及注释 3 | * @param value 要解析的字符串 4 | */ 5 | export function parseJSON(value: string) { 6 | try { 7 | return new Function(`return ${value}`)() as T 8 | } catch { 9 | return undefined 10 | } 11 | } 12 | 13 | /** 14 | * 格式化 JSON 数据为字符串 15 | * @param value 要格式化的对象 16 | */ 17 | export function formatJSON(value: any) { 18 | return JSON.stringify(value, undefined, 2) 19 | } 20 | 21 | /** 22 | * 删除 JSON 字符串中的注释和末尾多余的逗号 23 | * @param value 要处理的字符串 24 | * @param aligned 如果为 `true` 则将注释替换为等长空格而非删除 25 | */ 26 | export function normalizeJSON(value: string, aligned = true) { 27 | return value.replace(/"(?:[^\\"\n\r]|\\.)*"|\/\/[^\n\r]*|\/\*.*?(?:\*\/|$)|#[^\n\r]*|,(?=\s*[\]\}])/sg, source => { 28 | if (source.charCodeAt(0) === 34 /* " */) { 29 | return source 30 | } 31 | return aligned ? source.replace(/[^\n\r]/g, " ") : "" 32 | }) 33 | } 34 | 35 | /** 36 | * 读取一个 JSON 数据 37 | * @param value JSON 数据 38 | * @param key 键值,多级字段用 `.` 分割,如果键本身包含 `.`,需要写成 `..` 39 | */ 40 | export function readJSONByPath(value: any, key: string) { 41 | const info = lookupKey(value, key) 42 | if (info) { 43 | return info.json[info.key] 44 | } 45 | } 46 | 47 | /** 48 | * 写入一个 JSON 数据 49 | * @param value JSON 数据 50 | * @param key 键值,多级字段用 `.` 分割,如果键本身包含 `.`,需要写成 `..` 51 | * @param data 要写入的数据 52 | */ 53 | export function writeJSONByPath(value: any, key: string, data: any) { 54 | const info = lookupKey(value, key, true)! 55 | info.json[info.key] = data 56 | } 57 | 58 | /** 59 | * 移动一个 JSON 数据 60 | * @param value JSON 数据 61 | * @param keys 移动的键值,多级字段用 `.` 分割,如果键本身包含 `.`,需要写成 `..` 62 | * @param before 插入的位置 63 | */ 64 | export function moveJSONByPath(value: any, keys: string[], before: string | null) { 65 | const beforeInfo = before != null ? lookupKey(value, before, true)! : { json: value, key: before } 66 | const current = {} 67 | for (const key of keys) { 68 | const info = lookupKey(value, key, false) 69 | if (info && info.key in info.json) { 70 | current[info.key] = info.json[info.key] 71 | delete info.json[info.key] 72 | } 73 | } 74 | let foundBefore = false 75 | const next = {} 76 | for (const key in beforeInfo.json) { 77 | if (key === beforeInfo.key) { 78 | foundBefore = true 79 | } 80 | if (foundBefore) { 81 | next[key] = beforeInfo.json[key] 82 | delete beforeInfo.json[key] 83 | } 84 | } 85 | Object.assign(beforeInfo.json, current, next) 86 | } 87 | 88 | /** 89 | * 删除一个 JSON 数据 90 | * @param value JSON 数据 91 | * @param key 键值,多级字段用 `.` 分割,如果键本身包含 `.`,需要写成 `..` 92 | * @returns 返回是否删除成功 93 | */ 94 | export function deleteJSONByPath(value: any, key: string) { 95 | const info = lookupKey(value, key) 96 | if (info) { 97 | return delete info.json[info.key] 98 | 99 | } 100 | return false 101 | } 102 | 103 | function lookupKey(json: any, key: string, create?: boolean) { 104 | const keys = key.split(".") 105 | for (let i = 0; i < keys.length - 1; i++) { 106 | let key = keys[i] 107 | while (i + 2 < keys.length && !keys[i + 1]) { 108 | key += "." + keys[i += 2] 109 | if (i >= keys.length - 1) { 110 | return { 111 | json, 112 | key 113 | } 114 | } 115 | } 116 | if (json[key] == undefined || typeof json[key] !== "object") { 117 | if (!create) { 118 | return null 119 | } 120 | json = json[key] = {} 121 | } else { 122 | json = json[key] 123 | } 124 | } 125 | return { 126 | json, 127 | key: keys[keys.length - 1] 128 | } 129 | } -------------------------------------------------------------------------------- /src/jsx.ts: -------------------------------------------------------------------------------- 1 | import { encodeHTML, quoteHTMLAttribute } from "./html" 2 | 3 | /** 4 | * 创建一段 HTML 5 | * @param type 标签名 6 | * @param props 所有属性列表 7 | * @param children 子节点 8 | */ 9 | export function jsx(type: string, props: { [key: string]: any } | null, ...children: any[]) { 10 | let html = "" 11 | if (type) { 12 | html += `<${type}` 13 | for (const key in props!) { 14 | const prop = props[key] 15 | if (prop != undefined) { 16 | if (typeof prop === "boolean") { 17 | if (prop) { 18 | html += ` ${key}` 19 | } 20 | } else { 21 | html += ` ${key}=${quoteHTMLAttribute(String(prop))}` 22 | } 23 | } 24 | } 25 | html += `>` 26 | } 27 | for (const child of children) { 28 | if (child != undefined) { 29 | if (typeof child === "string") { 30 | html += encodeHTML(child) 31 | } else if (Array.isArray(child)) { 32 | for (const child2 of child) { 33 | if (child2 != undefined) { 34 | html += typeof child2 === "string" ? encodeHTML(child2) : child2 35 | } 36 | } 37 | } else { 38 | html += child 39 | } 40 | } 41 | } 42 | if (type && !(type in noCloseTags)) { 43 | html += `` 44 | } 45 | return new HTML(html) 46 | } 47 | 48 | /** 表示一段 HTML */ 49 | export class HTML extends String { } 50 | 51 | /** 表示节点片段 */ 52 | export const Fragment = "" 53 | 54 | /** 无需关闭的标签列表 */ 55 | export const noCloseTags = { 56 | meta: true, 57 | link: true, 58 | img: true, 59 | input: true, 60 | br: true, 61 | hr: true, 62 | area: true, 63 | col: true, 64 | command: true, 65 | embed: true, 66 | param: true, 67 | source: true, 68 | track: true, 69 | wbr: true, 70 | base: true, 71 | keygen: true, 72 | } -------------------------------------------------------------------------------- /src/lineColumn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 计算指定索引对应的行列号 3 | * @param content 要计算的内容 4 | * @param index 要计算的索引(从 0 开始) 5 | */ 6 | export function indexToLineColumn(content: string, index: number) { 7 | if (index <= 0) { 8 | return { line: 0, column: index } 9 | } 10 | let line = 0 11 | let column = 0 12 | for (let i = 0; i < index; i++) { 13 | switch (content.charCodeAt(i)) { 14 | case 13 /*\r*/: 15 | if (content.charCodeAt(i + 1) === 10 /*\n*/) { 16 | i++ 17 | if (index === i) { 18 | column++ 19 | break 20 | } 21 | } 22 | // fall through 23 | case 10 /*\n*/: 24 | line++ 25 | column = 0 26 | break 27 | default: 28 | column++ 29 | break 30 | } 31 | } 32 | return { line, column } as LineColumn 33 | } 34 | 35 | /** 36 | * 计算指定行列号对应的索引(从 0 开始) 37 | * @param content 要计算的内容 38 | * @param location 要计算的行列号 39 | */ 40 | export function lineColumnToIndex(content: string, location: LineColumn) { 41 | if (location.line > 0) { 42 | let index = 0 43 | let line = 0 44 | outer: while (index < content.length) { 45 | switch (content.charCodeAt(index++)) { 46 | case 13 /*\r*/: 47 | if (content.charCodeAt(index) === 10 /*\n*/) { 48 | index++ 49 | } 50 | // fall through 51 | case 10 /*\n*/: 52 | if (++line === location.line) { 53 | break outer 54 | } 55 | break 56 | } 57 | } 58 | return index + location.column 59 | } 60 | return location.column 61 | } 62 | 63 | /** 表示一个行列号 */ 64 | export interface LineColumn { 65 | /** 行号(从 0 开始)*/ 66 | line: number 67 | /** 列号(从 0 开始)*/ 68 | column: number 69 | } 70 | 71 | /** 表示每行第一个字符的映射表 */ 72 | export class LineMap extends Array { 73 | 74 | /** 获取最后一个字符的索引 */ 75 | readonly endIndex: number 76 | 77 | /** 获取或设置最后一次查询的索引 */ 78 | lastIndex = 0 79 | 80 | /** 81 | * 初始化新的映射表 82 | * @param content 要计算的内容 83 | */ 84 | constructor(content: string) { 85 | super(1) 86 | this[0] = 0 87 | for (let i = 0; i < content.length; i++) { 88 | switch (content.charCodeAt(i)) { 89 | case 13 /*\r*/: 90 | if (content.charCodeAt(i + 1) === 10 /*\n*/) { 91 | i++ 92 | } 93 | // fall through 94 | case 10 /*\n*/: 95 | this.push(i + 1) 96 | break 97 | } 98 | } 99 | this.endIndex = content.length 100 | } 101 | 102 | /** 103 | * 计算指定索引对应的行列号 104 | * @param index 要计算的索引(从 0 开始) 105 | */ 106 | indexToLineColumn(index: number) { 107 | if (index <= 0) { 108 | // 如果存在缓存,则重置缓存索引 109 | this.lastIndex = 0 110 | return { line: 0, column: index } 111 | } 112 | // 实际项目中,每次计算的位置都会比较靠近,所以每次都要记住本次搜索的位置,加速下次搜索 113 | let cacheIndex = this.lastIndex || 0 114 | while (cacheIndex < this.length - 1 && this[cacheIndex] <= index) { 115 | cacheIndex++ 116 | } 117 | while (cacheIndex > 0 && this[cacheIndex] > index) { 118 | cacheIndex-- 119 | } 120 | this.lastIndex = cacheIndex 121 | return { line: cacheIndex, column: index - this[cacheIndex] } as LineColumn 122 | } 123 | 124 | /** 125 | * 计算指定行列号对应的索引(从 0 开始) 126 | * @param location 要计算的行列号 127 | */ 128 | lineColumnToIndex(location: LineColumn) { 129 | if (location.line > 0) { 130 | if (location.line < this.length) { 131 | return this[location.line] + location.column 132 | } 133 | return this.endIndex + location.column 134 | } 135 | return location.column 136 | } 137 | 138 | } 139 | 140 | /** 141 | * 计算指定的行列号添加偏移后的行列号 142 | * @param location 要计算的行列号 143 | * @param line 要偏移的行数 144 | * @param column 要偏移的列数 145 | */ 146 | export function addLineColumn(location: LineColumn, line: number, column: number) { 147 | if (line) { 148 | return { line: location.line + line, column: column } 149 | } 150 | return { line: location.line, column: location.column + column } 151 | } 152 | 153 | /** 154 | * 比较确定两个行列号的顺序 155 | * @param x 要比较的第一个行列号 156 | * @param y 要比较的第二个行列号 157 | * @returns 如果两个行列号相同则返回 0,如果前者靠前,则返回负数,如果后者靠前,则返回正数 158 | */ 159 | export function compareLineColumn(x: LineColumn, y: LineColumn) { 160 | return (x.line - y.line) || (x.column - y.column) 161 | } -------------------------------------------------------------------------------- /src/misc.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 合并所有对象,如果两个对象包含同名的数组,则将这些数组合并为一个 4 | * @param target 要合并的目标对象 5 | * @param sources 要合并的源对象 6 | * @example merge({x: [0], y: 0}, {x: [1], y: 2}) // {x: [0, 1], y: 2} 7 | */ 8 | export function merge(target: T, ...sources: S[]) { 9 | const cloned = new Map() 10 | for (const source of sources) { 11 | target = merge(target, source) 12 | } 13 | return target as T & S 14 | 15 | function merge(target: any, source: any) { 16 | if (source == undefined) { 17 | return target 18 | } 19 | if (typeof target === "object" && typeof source === "object") { 20 | if (Array.isArray(target) && Array.isArray(source)) { 21 | return [...target, ...source] 22 | } 23 | const exists = cloned.get(source) 24 | if (exists !== undefined) { 25 | return exists 26 | } 27 | const result: { [key: string]: any } = { ...target } 28 | cloned.set(source, result) 29 | for (const key in source) { 30 | result[key] = merge(result[key], source[key]) 31 | } 32 | return result 33 | } 34 | return source 35 | } 36 | } 37 | 38 | /** 39 | * 删除字符串开头的 UTF-8 BOM 字符 40 | * @param content 要处理的字符串 41 | */ 42 | export function stripBOM(content: string) { 43 | if (content.charCodeAt(0) === 0xfeff) { 44 | content = content.substring(1) 45 | } 46 | return content 47 | } 48 | 49 | /** 50 | * 返回首字母大写的字符串 51 | * @param value 要处理的字符串 52 | */ 53 | export function capitalize(value: string) { 54 | return value.charAt(0).toUpperCase() + value.slice(1) 55 | } 56 | 57 | /** 58 | * 替换字符串,如果字符串未替换则返回 `null` 59 | * @param search 替换的源 60 | * @param replacer 替换的目标 61 | */ 62 | export function replaceString(value: string, search: RegExp, replacer: string | ((source: string, ...args: any[]) => string)) { 63 | let changed = false 64 | const repalced = value.replace(search, (source, ...match: any[]) => { 65 | changed = true 66 | if (typeof replacer === "function") { 67 | return replacer(source, ...match) 68 | } 69 | return replacer.replace(/\$&|\$(\d+)/g, (all, index) => { 70 | if (!index) { 71 | return source 72 | } 73 | index-- 74 | if (index < match.length - 2) { 75 | return match[index] 76 | } 77 | return all 78 | }) 79 | }) 80 | return changed ? repalced : null 81 | } 82 | 83 | /** 84 | * 生成指定长度的随机字符串 85 | * @param length 要生成的字符串长度 86 | */ 87 | export function randomString(length: number) { 88 | let result = "" 89 | while (result.length < length) { 90 | result += Math.random().toString(36).substring(2) 91 | } 92 | return result.substring(0, length) 93 | } 94 | 95 | /** 96 | * 拼接两个数组 97 | * @param x 要串联的数组 98 | * @param y 要串联的数组 99 | */ 100 | export function concat(x: T[] | null | undefined, y: T[] | null | undefined) { 101 | if (!x) { 102 | return y 103 | } 104 | if (!y) { 105 | return x 106 | } 107 | return [...x, ...y] 108 | } 109 | 110 | /** 111 | * 如果数组中不存在指定的项则添加到数组末尾 112 | * @param arr 要处理的数组 113 | * @param item 要添加的项 114 | * @return 如果已添加到数组则返回 `true`,否则说明该项已存在,返回 `false` 115 | * @example pushIfNotExists(1, 9, 0], 1) // 数组变成 [1, 9, 0] 116 | * @example pushIfNotExists([1, 9, 0], 2) // 数组变成 [1, 9, 0, 2] 117 | */ 118 | export function pushIfNotExists(arr: T[], item: T) { 119 | return arr.indexOf(item) < 0 && arr.push(item) > 0 120 | } 121 | 122 | /** 123 | * 在已排序的数组中二分查找指定的项,如果找到则返回该值的位置,否则返回离该值最近的位置的位反值(总是小于 0) 124 | * @param arr 要遍历的数组 125 | * @param item 要查找的项 126 | * @param keySelector 用于获取每项元素待比较的键的回调函数 127 | * @param keyComparer 用于确定两个键排序顺序的回调函数 128 | * @param keyComparer.left 要比较的左值 129 | * @param keyComparer.right 要比较的右值 130 | * @param keyComparer.return 如果左值应排在右值前面,应返回负数,如果右值应排在右值后面,应返回正数,如果两者相同则返回零 131 | * @param start 开始查找的索引(从 0 开始) 132 | * @param end 结束查找的索引(从 0 开始,不含) 133 | */ 134 | export function binarySearch(arr: readonly T[], item: V, keySelector = (item: T | V) => item as unknown as K, keyComparer = (left: K, right: K) => left < right ? -1 : left > right ? 1 : 0 as number, start = 0, end = arr.length) { 135 | end-- 136 | const key = keySelector(item) 137 | while (start <= end) { 138 | const middle = start + ((end - start) >> 1) 139 | const midKey = keySelector(arr[middle]) 140 | const result = keyComparer(midKey, key) 141 | if (result < 0) { 142 | start = middle + 1 143 | } else if (result > 0) { 144 | end = middle - 1 145 | } else { 146 | return middle 147 | } 148 | } 149 | return ~start 150 | } 151 | 152 | /** 153 | * 按顺序插入元素到已排序的数组中,如果值已存在则插入到存在的值之后 154 | * @param sortedArray 已排序的数组 155 | * @param item 要插入的值 156 | * @param comparer 确定元素顺序的回调函数,如果函数返回 `true`,则将 `x` 排在 `y` 的前面,否则相反 157 | * @param comparer.x 要比较的第一个元素 158 | * @param comparer.y 要比较的第二个元素 159 | */ 160 | export function insertSorted(sortedArray: T[], item: T, comparer: (x: T, y: T) => boolean) { 161 | let start = 0 162 | let end = sortedArray.length - 1 163 | while (start <= end) { 164 | const middle = start + ((end - start) >> 1) 165 | if (comparer(sortedArray[middle], item)) { 166 | start = middle + 1 167 | } else { 168 | end = middle - 1 169 | } 170 | } 171 | sortedArray.splice(start, 0, item) 172 | } 173 | 174 | /** 175 | * 删除数组中指定的项,如果有多个匹配项则只删除第一项 176 | * @param arr 要处理的数组 177 | * @param item 要删除的项 178 | * @param startIndex 开始搜索的索引(从 0 开始) 179 | * @return 返回被删除的项在原数组中的索引,如果数组中找不到指定的项则返回 `-1` 180 | * @example remove([1, 9, 9, 0], 9) // 1, 数组变成 [1, 9, 0] 181 | * @example while(remove(arr, "wow") >= 0) {} // 删除所有 "wow" 182 | */ 183 | export function remove(arr: T[], item: T, startIndex?: number) { 184 | startIndex = arr.indexOf(item, startIndex) 185 | startIndex >= 0 && arr.splice(startIndex, 1) 186 | return startIndex 187 | } 188 | 189 | /** 190 | * 编码正则表达式中的特殊字符 191 | * @param pattern 要编码的正则表达式模式 192 | */ 193 | export function escapeRegExp(pattern: string) { 194 | return pattern.replace(/[.\\(){}[\]\-+*?^$|]/g, "\\$&") 195 | } 196 | 197 | /** 所有日期格式化器 */ 198 | const dateFormatters = { 199 | y: (date: Date, format: string) => { 200 | const year = date.getFullYear() 201 | return format.length < 3 ? year % 100 : year 202 | }, 203 | M: (date: Date) => date.getMonth() + 1, 204 | d: (date: Date) => date.getDate(), 205 | H: (date: Date) => date.getHours(), 206 | m: (date: Date) => date.getMinutes(), 207 | s: (date: Date) => date.getSeconds() 208 | } 209 | 210 | /** 211 | * 格式化指定的日期对象 212 | * @param date 要处理的日期对象 213 | * @param format 格式字符串,其中以下字符(区分大小写)会被替换: 214 | * 215 | * 字符| 意义 | 示例 216 | * ----|---------------|-------------------- 217 | * y | 年 | yyyy: 1999, yy: 99 218 | * M | 月(从 1 开始)| MM: 09, M: 9 219 | * d | 日(从 1 开始)| dd: 09, d: 9 220 | * H | 时(24 小时制)| HH: 13, H: 13 221 | * m | 分 | mm: 06, m: 6 222 | * s | 秒 | ss: 06, s: 6 223 | * 224 | * @example formatDate(new Date("2016/01/01 00:00:00"), "yyyyMMdd") // "20160101" 225 | * @see https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html 226 | */ 227 | export function formatDate(date: Date, format: string) { 228 | return format.replace(/([yMdHms])\1*/g, (all, key: keyof typeof dateFormatters) => dateFormatters[key](date, all).toString().padStart(all.length, "0")) 229 | } 230 | 231 | /** 232 | * 格式化时间为类似“几分钟前”的格式 233 | * @param date 要格式化的时间 234 | * @param now 当前时间 235 | */ 236 | export function formatRelativeDate(date: Date, now = new Date()) { 237 | const offset = now.getTime() - date.getTime() 238 | if (offset < 0) { 239 | return date.toLocaleString() 240 | } 241 | if (offset < 1000) { 242 | return "just now" 243 | } 244 | if (offset < 60000) { 245 | return `${Math.floor(offset / 1000)} seconds ago` 246 | } 247 | if (offset < 60000 * 60) { 248 | return `${Math.floor(offset / 60000)} minutes ago` 249 | } 250 | if (now.getFullYear() !== date.getFullYear()) { 251 | return date.toLocaleDateString() 252 | } 253 | if (date.getMonth() !== now.getMonth()) { 254 | return date.toLocaleDateString() 255 | } 256 | switch (now.getDate() - date.getDate()) { 257 | case 0: 258 | return `${Math.floor(offset / (60000 * 60))} hours ago` 259 | case 1: 260 | return `yesterday` 261 | case 2: 262 | return `2 days ago` 263 | default: 264 | return date.toLocaleDateString() 265 | } 266 | } 267 | 268 | /** 269 | * 格式化指定的高精度时间段 270 | * @param hrTime 由秒和纳秒部分组成的数组 271 | * @example formatHRTime([1, 20000000]) // "1.02s" 272 | */ 273 | export function formatHRTime(hrTime: readonly [number, number]) { 274 | const second = hrTime[0] 275 | if (second < 1) { 276 | const ms = hrTime[1] 277 | if (!ms) { 278 | return "0ms" 279 | } 280 | if (ms < 1e4) { 281 | return "<0.01ms" 282 | } 283 | if (ms < 1e6) { 284 | return `${(ms / 1e6).toFixed(2).replace(/\.00$|0$/, "")}ms` 285 | } 286 | return `${(ms / 1e6).toFixed(0)}ms` 287 | } 288 | if (second < 60) { 289 | return `${(second + hrTime[1] / 1e9).toFixed(2).replace(/\.00$|0$/, "")}s` 290 | } 291 | const s = second % 60 292 | return `${Math.floor(second / 60)}min${s ? s + "s" : ""}` 293 | } 294 | 295 | /** 296 | * 格式化指定的字节大小 297 | * @param byteSize 要格式化的字节大小 298 | * @example formatSize(1024) // "1KB" 299 | */ 300 | export function formatSize(byteSize: number) { 301 | let unit: string 302 | if (byteSize < 1000) { 303 | unit = "B" 304 | } else if (byteSize < 1024 * 1000) { 305 | byteSize /= 1024 306 | unit = "KB" 307 | } else if (byteSize < 1024 * 1024 * 1000) { 308 | byteSize /= 1024 * 1024 309 | unit = "MB" 310 | } else if (byteSize < 1024 * 1024 * 1024 * 1000) { 311 | byteSize /= 1024 * 1024 * 1024 312 | unit = "GB" 313 | } else { 314 | byteSize /= 1024 * 1024 * 1024 * 1024 315 | unit = "TB" 316 | } 317 | return byteSize.toFixed(2).replace(/\.00$|0$/, "") + unit 318 | } 319 | 320 | /** 321 | * 同时遍历数组每一项并执行异步回调函数 322 | * @param array 要遍历的数组 323 | * @param callback 要执行的回调函数 324 | */ 325 | export function parallelForEach(array: T[], callback: (item: T) => R) { 326 | const promises: R[] = [] 327 | for (const item of array) { 328 | promises.push(callback(item)) 329 | } 330 | return Promise.all(promises) 331 | } -------------------------------------------------------------------------------- /src/net.ts: -------------------------------------------------------------------------------- 1 | import { networkInterfaces } from "os" 2 | 3 | /** 4 | * 获取本机的远程 IP 5 | * @param ipV6 如果为 `true` 则返回 IPv6 地址,如果为 `false` 则返回 IPv6 地址,默认优先返回 IPv4 地址 6 | */ 7 | export function remoteIP(ipV6?: boolean) { 8 | const ifaces = networkInterfaces() 9 | let ipV6Address: string | undefined 10 | for (const key in ifaces) { 11 | for (const iface of ifaces[key]) { 12 | if (iface.internal) { 13 | continue 14 | } 15 | if (iface.family !== "IPv4") { 16 | if (ipV6 === true) { 17 | return iface.address 18 | } 19 | if (ipV6 !== false) { 20 | ipV6Address ??= iface.address 21 | } 22 | continue 23 | } 24 | return iface.address 25 | } 26 | } 27 | return ipV6Address 28 | } -------------------------------------------------------------------------------- /src/path.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path" 2 | import { escapeRegExp } from "./misc" 3 | 4 | /** 5 | * 获取指定路径对应的绝对路径 6 | * @param paths 要处理的路径 7 | * @returns 返回以 `/`(非 Windows) 或 `\`(Windows) 为分隔符的绝对路径,路径末尾多余的分隔符会被删除 8 | * @example resolvePath("foo/goo/hoo", "../relative") 9 | */ 10 | export function resolvePath(...paths: readonly string[]) { 11 | return resolve(...paths) 12 | } 13 | 14 | /** 15 | * 获取指定路径对应的相对路径 16 | * @param base 要使用的基路径 17 | * @param path 要处理的路径 18 | * @returns 返回以 `/` 为分隔符的相对路径,路径末尾多余的分隔符会被删除 19 | * @example relativePath("foo/goo/hoo", "foo/goo/relative") // "../relative" 20 | */ 21 | export function relativePath(base: string, path: string) { 22 | path = relative(base, path) 23 | return sep === "/" ? path : path.replace(/\\/g, "/") 24 | } 25 | 26 | /** 27 | * 合并多个路径 28 | * @param paths 要处理的路径 29 | * @returns 返回以 `/`(非 Windows) 或 `\`(Windows) 为分隔符的绝对路径或以 `/` 为分隔符的相对路径,路径末尾多余的分隔符会被保留 30 | * @example joinPath("a", "b") // "a/b" 31 | */ 32 | export function joinPath(...paths: readonly string[]) { 33 | const path = join(...paths) 34 | return path === "." ? "" : sep === "/" || isAbsolute(path) ? path : path.replace(/\\/g, "/") 35 | } 36 | 37 | /** 38 | * 规范化指定的路径 39 | * @param path 要处理的路径 40 | * @returns 返回以 `/`(非 Windows) 或 `\`(Windows) 为分隔符的绝对路径或以 `/` 为分隔符的相对路径,路径末尾多余的分隔符会被保留 41 | * @example normalizePath("./foo/") // "foo/" 42 | */ 43 | export function normalizePath(path: string) { 44 | path = normalize(path) 45 | return path === "." ? "" : sep === "/" || isAbsolute(path) ? path : path.replace(/\\/g, "/") 46 | } 47 | 48 | /** 49 | * 判断指定的路径是否是绝对路径 50 | * @param path 要判断的路径 51 | * @example isAbsolutePath("foo") // false 52 | */ 53 | export function isAbsolutePath(path: string) { 54 | return isAbsolute(path) 55 | } 56 | 57 | /** 58 | * 获取指定路径的文件夹部分 59 | * @param path 要处理的路径 60 | * @example getDir("/root/foo.txt") // "/root" 61 | */ 62 | export function getDir(path: string) { 63 | path = dirname(path) 64 | return path === "." ? "" : path 65 | } 66 | 67 | /** 68 | * 设置指定路径的文件夹部分 69 | * @param path 要处理的路径 70 | * @param value 要设置的新文件夹路径 71 | * @param base 如果提供了原文件夹路径,则保留文件在原文件夹内的路径 72 | * @returns 如果新文件夹路径是绝对路径,返回以 `/`(非 Windows) 或 `\`(Windows) 为分隔符的绝对路径,否则返回以 `/` 为分隔符的相对路径 73 | * @example setDir("/root/foo.txt", "goo") // "goo/foo.txt" 74 | * @example setDir("/root/goo/foo.txt", "/user", "/root") // "/user/goo/foo.txt" 75 | */ 76 | export function setDir(path: string, value: string, base?: string) { 77 | if (base) { 78 | base = relative(base, path) 79 | if (isAbsolute(base) || base.startsWith(`..${sep}`)) { 80 | return path 81 | } 82 | } else { 83 | base = basename(path) 84 | } 85 | path = join(value, base) 86 | return sep === "/" || isAbsolute(path) ? path : path.replace(/\\/g, "/") 87 | } 88 | 89 | /** 90 | * 获取指定路径的根部分 91 | * @param path 要处理的路径 92 | * @example getRoot("/root/goo/foo.txt") // "/root" 93 | */ 94 | export function getRoot(path: string) { 95 | let index = 0 96 | while (++index < path.length) { 97 | const char = path.charCodeAt(index) 98 | if (char === 47 /*/*/ || char === 92 /*\*/) { 99 | return path.substring(0, index) 100 | } 101 | } 102 | return path 103 | } 104 | 105 | /** 106 | * 设置指定路径的根部分 107 | * @param path 要处理的路径 108 | * @example setRoot("/root/goo/foo.txt", "/user") // "/user/goo/foo.txt" 109 | */ 110 | export function setRoot(path: string, value: string) { 111 | return setDir(path, value, getRoot(path)) 112 | } 113 | 114 | /** 115 | * 获取指定路径的文件名部分 116 | * @param path 要处理的路径 117 | * @param includeExt 如果为 `true`(默认)则包含扩展名(含点),否则不包含扩展名 118 | * @example getName("/root/foo.txt") // "foo.txt" 119 | * @example getName("/root/foo.txt", false) // "foo" 120 | */ 121 | export function getName(path: string, includeExt = true) { 122 | return basename(path, includeExt ? undefined : extname(path)) 123 | } 124 | 125 | /** 126 | * 设置指定路径的文件名部分 127 | * @param path 要处理的路径 128 | * @param value 要更改的新文件名 129 | * @param includeExt 如果为 `true`(默认)则同时更改扩展名(含点),否则保留原扩展名 130 | * @example setName("/root/foo.txt", "goo.jpg") // "/root/goo.jpg" 131 | * @example setName("/root/foo.txt", "goo", false) // "/root/goo.jpg" 132 | */ 133 | export function setName(path: string, value: string, includeExt = true): string { 134 | if (/[/\\]$/.test(path)) { 135 | return setName(path.slice(0, -1), value, includeExt) + path.charAt(path.length - 1) 136 | } 137 | const base = basename(path) 138 | return path.slice(0, -base.length) + value + (includeExt ? "" : extname(base)) 139 | } 140 | 141 | /** 142 | * 在指定路径的文件名前追加内容 143 | * @param path 要处理的路径 144 | * @param value 要追加的内容 145 | * @example prependName("foo/goo.txt", "fix_") // "foo/fix_goo.txt" 146 | */ 147 | export function prependName(path: string, value: string) { 148 | return setName(path, value + getName(path)) 149 | } 150 | 151 | /** 152 | * 在指定路径的文件名(不含扩展名部分)后追加内容 153 | * @param path 要处理的路径 154 | * @param value 要追加的内容 155 | * @example appendName("foo/goo.src.txt", "_fix") // "foo/goo_fix.src.txt" 156 | */ 157 | export function appendName(path: string, value: string): string { 158 | if (/[/\\]$/.test(path)) { 159 | return appendName(path.slice(0, -1), value) + path.charAt(path.length - 1) 160 | } 161 | const base = basename(path) 162 | const dot = base.indexOf(".") 163 | return path.slice(0, -base.length) + (dot < 0 ? base : base.substring(0, dot)) + value + (dot < 0 ? "" : base.substring(dot)) 164 | } 165 | 166 | /** 167 | * 在指定路径的文件名(不含扩展名部分)后追加索引 168 | * @param path 要处理的路径 169 | * @param index 要追加的索引,其中的数字会递增 170 | * @example appendIndex("foo/goo.src.txt") // "foo/goo_2.src.txt" 171 | * @example appendIndex("foo/goo_2.src.txt") // "foo/goo_3.src.txt" 172 | */ 173 | export function appendIndex(path: string, index = "_2") { 174 | let append = true 175 | path = path.replace(new RegExp(`^((?:[^\\\\/]*[\\/\\\\])*[^\\.]*${escapeRegExp(index).replace(/\d+/, ")(\\d+)(")})`), (_, prefix, index, postfix) => { 176 | append = false 177 | return prefix + (+index + 1) + postfix 178 | }) 179 | if (append) { 180 | path = appendName(path, index) 181 | } 182 | return path 183 | } 184 | 185 | /** 186 | * 计算重命名后的路径 187 | * @params path 目标字段 188 | * @params append 转换附加字符串 189 | * @params ext 排除的扩展名 190 | * @example getNewPath("测试 - 副本", " - 副本") // "测试 - 副本2" 191 | * @example getNewPath("test", "-2") // "test-2" 192 | * @example getNewPath("test-2", "-2") // "test-3" 193 | */ 194 | export function getNewPath(path: string, append = "-2", ext = getExt(path)) { 195 | if (ext) path = path.substring(0, path.length - ext.length) 196 | const digits = /\d+$/.exec(path) 197 | if (digits) { 198 | return path.substring(0, digits.index) + (parseInt(digits[0]) + 1) + ext 199 | } 200 | if (path.endsWith(append)) { 201 | return path + "2" + ext 202 | } 203 | return path + append + ext 204 | } 205 | 206 | /** 207 | * 获取指定路径的扩展名(含点)部分 208 | * @param path 要处理的地址 209 | * @example getExt("/root/foo.txt") // ".txt" 210 | * @example getExt(".gitignore") // "" 211 | */ 212 | export function getExt(path: string) { 213 | if (path.endsWith("/") || path.endsWith(sep)) { 214 | return "" 215 | } 216 | return extname(path) 217 | } 218 | 219 | /** 220 | * 设置指定路径的扩展名(含点)部分,如果源路径不含扩展名则追加到末尾 221 | * @param path 要处理的路径 222 | * @param value 要更改的新扩展名(含点) 223 | * @example setExt("/root/foo.txt", ".jpg") // "/root/foo.jpg" 224 | * @example setExt("/root/foo.txt", "") // "/root/foo" 225 | * @example setExt("/root/foo", ".jpg") // "/root/foo.jpg" 226 | */ 227 | export function setExt(path: string, value: string) { 228 | return path.substring(0, path.length - getExt(path).length) + value 229 | } 230 | 231 | /** 判断当前系统是否忽略路径的大小写 */ 232 | export const isCaseInsensitive = process.platform === "win32" || process.platform === "darwin" || process.platform === "freebsd" || process.platform === "openbsd" 233 | 234 | /** 235 | * 判断两个路径是否相同 236 | * @param path1 要判断的第一个路径,路径必须已规范化 237 | * @param path2 要判断的第二个路径,路径必须已规范化 238 | * @param ignoreCase 是否忽略路径的大小写 239 | * @example pathEquals("/root", "/root") // true 240 | */ 241 | export function pathEquals(path1: string, path2: string, ignoreCase = isCaseInsensitive) { 242 | if (path1.length !== path2.length) { 243 | return false 244 | } 245 | if (ignoreCase) { 246 | path1 = path1.toLowerCase() 247 | path2 = path2.toLowerCase() 248 | } else if (sep === "\\") { 249 | path1 = path1.replace(/^[a-z]:/, char => char.toUpperCase()) 250 | path2 = path2.replace(/^[a-z]:/, char => char.toUpperCase()) 251 | } 252 | return path1 === path2 253 | } 254 | 255 | /** 256 | * 判断指定的文件夹是否包含另一个文件或文件夹 257 | * @param parent 要判断的父文件夹路径,路径必须已规范化 258 | * @param child 要判断的子文件或文件夹路径,路径必须已规范化 259 | * @param ignoreCase 是否忽略路径的大小写 260 | * @example containsPath("/root", "/root/foo") // true 261 | * @example containsPath("/root/foo", "/root/goo") // false 262 | */ 263 | export function containsPath(parent: string, child: string, ignoreCase = isCaseInsensitive) { 264 | if (child.length < parent.length) { 265 | return false 266 | } 267 | if (ignoreCase) { 268 | parent = parent.toLowerCase() 269 | child = child.toLowerCase() 270 | } 271 | if (!child.startsWith(parent)) { 272 | return false 273 | } 274 | const endChar = parent.charCodeAt(parent.length - 1) 275 | if (endChar === 47 /*/*/ || endChar === 92 /*\*/ || endChar !== endChar /*NaN*/) { 276 | return true 277 | } 278 | const char = child.charCodeAt(parent.length) 279 | return char === 47 /*/*/ || char === 92 /*\*/ || char !== char /*NaN*/ 280 | } 281 | 282 | /** 283 | * 获取两个路径中最深的路径,如果没有公共部分则返回 `null` 284 | * @param path1 要处理的第一个路径,路径必须已规范化 285 | * @param path2 要处理的第二个路径,路径必须已规范化 286 | * @param ignoreCase 是否忽略路径的大小写 287 | * @example deepestPath("/root", "/root/foo") // "/root/foo" 288 | */ 289 | export function deepestPath(path1: string | null | undefined, path2: string | null | undefined, ignoreCase = isCaseInsensitive) { 290 | if (path1 == null || path2 == null) { 291 | return null 292 | } 293 | if (containsPath(path1, path2, ignoreCase)) { 294 | return path2 295 | } 296 | if (containsPath(path2, path1, ignoreCase)) { 297 | return path1 298 | } 299 | return null 300 | } 301 | 302 | /** 303 | * 获取两个路径的公共文件夹,如果没有公共部分则返回 `null` 304 | * @param path1 要处理的第一个路径,路径必须已规范化 305 | * @param path2 要处理的第二个路径,路径必须已规范化 306 | * @param ignoreCase 是否忽略路径的大小写 307 | * @example commonDir("/root/foo", "/root/foo/goo") // "/root/foo" 308 | */ 309 | export function commonDir(path1: string | null | undefined, path2: string | null | undefined, ignoreCase = isCaseInsensitive) { 310 | if (path1 == null || path2 == null) { 311 | return null 312 | } 313 | // 确保 path1.length <= path2.length 314 | if (path1.length > path2.length) { 315 | [path1, path2] = [path2, path1] 316 | } 317 | // 计算相同的开头部分,以分隔符为界 318 | let index = -1 319 | let i = 0 320 | for (; i < path1.length; i++) { 321 | let ch1 = path1.charCodeAt(i) 322 | let ch2 = path2.charCodeAt(i) 323 | // 如果不区分大小写则将 ch1 和 ch2 全部转小写 324 | if (ignoreCase) { 325 | if (ch1 >= 65 /*A*/ && ch1 <= 90 /*Z*/) { 326 | ch1 |= 0x20 327 | } 328 | if (ch2 >= 65 /*A*/ && ch2 <= 90 /*Z*/) { 329 | ch2 |= 0x20 330 | } 331 | } 332 | // 发现不同字符后终止 333 | if (ch1 !== ch2) { 334 | break 335 | } 336 | // 如果发现一个分隔符,则标记之前的内容是公共部分 337 | if (ch1 === 47 /*/*/ || ch1 === 92 /*\*/) { 338 | index = i 339 | } 340 | } 341 | // 特殊处理:path1 = "foo", path2 = "foo" 或 "foo/goo" 342 | if (i === path1.length && (i === path2.length || path2.charCodeAt(i) === 47 /*/*/ || path2.charCodeAt(i) === 92 /*\*/ || path1.length === 0)) { 343 | return path1 344 | } 345 | return index < 0 ? null : path1.substring(0, index) 346 | } -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess as NativeChildProcess, spawn, SpawnOptions } from "child_process" 2 | 3 | /** 4 | * 执行一个命令 5 | * @param command 要执行的命令 6 | * @param options 附加选项 7 | * @param options.args 启动命令时的附加参数 8 | * @param options.timeout 执行超时毫秒数,设置 0 表示不超时 9 | * @returns 返回子进程 10 | */ 11 | export function exec(command: string, options: SpawnOptions & { args?: readonly string[], timeout?: number } = {}) { 12 | if (options.shell === undefined) { 13 | options.shell = true 14 | } 15 | const cp = spawn(command, options.args || [], options) as ChildProcess 16 | const result = cp.result = {} as ExecResult 17 | if (cp.stdout) { 18 | result.stdout = "" 19 | cp.stdout.setEncoding("utf8").on("data", d => { 20 | result.stdout! += d 21 | }) 22 | } 23 | if (cp.stderr) { 24 | result.stderr = "" 25 | cp.stderr.setEncoding("utf8").on("data", d => { 26 | result.stderr! += d 27 | }) 28 | } 29 | let timer: ReturnType | undefined 30 | if (options.timeout !== 0) { 31 | timer = setTimeout(() => { 32 | timer = undefined 33 | cp.kill() 34 | cp.emit("error", new Error("Exec Timeout")) 35 | }, options.timeout || 300000) 36 | } 37 | cp.on("error", error => { 38 | if (timer) clearTimeout(timer) 39 | cp.result.error = error 40 | cp.emit("reject", error) 41 | }) 42 | cp.on("close", code => { 43 | if (timer) clearTimeout(timer) 44 | cp.result.exitCode = code 45 | cp.emit("resolve", result) 46 | }) 47 | cp.then = (onfulfilled?: ((value: ExecResult) => void) | null, onrejected?: ((reason: Error) => void) | null): any => { 48 | if (cp.result.exitCode !== undefined) { 49 | if (cp.result.error) { 50 | onrejected?.(cp.result.error) 51 | } else { 52 | onfulfilled?.(cp.result) 53 | } 54 | } else { 55 | if (onfulfilled) cp.once("resolve", onfulfilled) 56 | if (onrejected) cp.once("reject", onrejected) 57 | } 58 | return cp 59 | } 60 | return cp 61 | } 62 | 63 | /** 表示一个子进程 */ 64 | export interface ChildProcess extends NativeChildProcess, PromiseLike { 65 | /** 获取进程的执行结果 */ 66 | result: ExecResult 67 | } 68 | 69 | /** 表示执行命令的结果 */ 70 | export interface ExecResult { 71 | /** 获取执行的错误 */ 72 | error?: Error 73 | /** 获取命令的退出码 */ 74 | exitCode?: number 75 | /** 获取命令的标准流输出 */ 76 | stdout?: string 77 | /** 获取命令的错误流输出 */ 78 | stderr?: string 79 | } 80 | 81 | /** 82 | * 在浏览器打开指定的地址 83 | * @param url 要打开的地址 84 | * @param wait 是否等待浏览器启动后再返回 85 | * @param app 使用的浏览器程序,默认由操作系统决定 86 | * @param appArgs 浏览器程序的附加启动参数 87 | */ 88 | export function open(url: string, wait = false, app?: string, appArgs?: readonly string[]) { 89 | let cmd: string 90 | const args: string[] = [] 91 | let options: SpawnOptions | undefined 92 | if (process.platform === "win32") { 93 | cmd = "cmd" 94 | args.push("/c", "start", '""', "/b") 95 | url = url.replace(/&/g, "^&") 96 | if (wait) args.push("/wait") 97 | if (app) args.push(app) 98 | if (appArgs) args.push(...appArgs) 99 | args.push(url) 100 | } else if (process.platform === "darwin") { 101 | cmd = "open" 102 | if (wait) args.push("-W") 103 | if (app) args.push("-a", app) 104 | args.push(url) 105 | if (appArgs) args.push("--args", ...appArgs) 106 | } else { 107 | cmd = app || "xdg-open" 108 | if (appArgs) args.push(...appArgs) 109 | if (!wait) { 110 | options = { 111 | stdio: "ignore", 112 | detached: true 113 | } 114 | } 115 | args.push(url) 116 | } 117 | 118 | const cp = spawn(cmd, args, options!) 119 | if (wait) { 120 | return new Promise((resolve, reject) => { 121 | cp.on("error", reject) 122 | cp.on("close", code => { 123 | if (code > 0) { 124 | reject(new Error(`The 'open' command exited with code '${code}'`)) 125 | } else { 126 | resolve(cp) 127 | } 128 | }) 129 | }) 130 | } 131 | cp.unref() 132 | return Promise.resolve(cp) 133 | } 134 | 135 | /** 所有退出函数 */ 136 | const handlers: Function[] = [] 137 | 138 | /** 139 | * 添加当前程序即将退出的回调函数 140 | * @param callback 要执行的回调函数,函数可以返回 `Promise` 表示正在执行异步任务,但在通过主动调用 `process.exit()` 退出进程时,`Promise` 会被忽略 141 | */ 142 | export function onExit(callback: (reason: "exit" | (ReturnType extends IterableIterator ? T : never), code: number) => void) { 143 | if (handlers.push(callback) > 1) { 144 | return 145 | } 146 | process.once("beforeExit", emitExit) 147 | for (const signal of signals()) { 148 | try { 149 | process.once(signal, signalHandler as any) 150 | } catch { } 151 | } 152 | } 153 | 154 | /** 155 | * 删除当前程序即将退出的回调函数 156 | * @param callback 要执行的回调函数 157 | */ 158 | export function offExit(callback: Parameters[0]) { 159 | const index = handlers.indexOf(callback) 160 | if (index < 0) { 161 | return 162 | } 163 | handlers.splice(index, 1) 164 | if (handlers.length) { 165 | return 166 | } 167 | process.off("beforeExit", emitExit) 168 | for (const signal of signals()) { 169 | try { 170 | process.off(signal, signalHandler as any) 171 | } catch { } 172 | } 173 | } 174 | 175 | /** 176 | * 触发退出事件 177 | * @param code 退出的状态码 178 | * @param signal 退出的信号名 179 | */ 180 | async function emitExit(code?: number, signal?: NodeJS.Signals) { 181 | if (handlers.length === 1) { 182 | const handler = handlers[0] 183 | handlers.length = 0 184 | await handler(signal || "exit", code) 185 | } else { 186 | const cloned = handlers.slice(0) 187 | handlers.length = 0 188 | for (const handler of cloned) { 189 | await handler(signal || "exit", code) 190 | } 191 | } 192 | } 193 | 194 | /** 当前进程被终止的回调 */ 195 | async function signalHandler(signal: NodeJS.Signals, code: number) { 196 | await emitExit(code, signal) 197 | if (process.listenerCount(signal) === 0) { 198 | process.kill(process.pid, signal) 199 | } 200 | } 201 | 202 | /** 203 | * 获取所有退出信号名 204 | * @see https://github.com/tapjs/signal-exit/blob/master/signals.js 205 | */ 206 | function* signals() { 207 | yield "SIGABRT" 208 | yield "SIGALRM" 209 | yield "SIGHUP" 210 | yield "SIGINT" 211 | yield "SIGTERM" 212 | if (process.platform !== "win32") { 213 | yield "SIGVTALRM" 214 | yield "SIGXCPU" 215 | yield "SIGXFSZ" 216 | yield "SIGUSR2" 217 | yield "SIGTRAP" 218 | yield "SIGSYS" 219 | yield "SIGQUIT" 220 | yield "SIGIOT" 221 | } 222 | if (process.platform === "linux") { 223 | yield "SIGIO" 224 | yield "SIGPOLL" 225 | yield "SIGPWR" 226 | yield "SIGSTKFLT" 227 | yield "SIGUNUSED" 228 | } 229 | } -------------------------------------------------------------------------------- /src/require.ts: -------------------------------------------------------------------------------- 1 | import { delimiter } from "path" 2 | 3 | /** 4 | * 注册 ES6 模块加载器(使 .js 文件支持使用 `import`/`export` 语法) 5 | * @param extension 支持的模块扩展名 6 | * @returns 返回原加载器 7 | */ 8 | export function registerESMLoader(extension = ".js") { 9 | const originalLoader = require.extensions[extension] 10 | const loader = require.extensions[extension] = function (module: any, filename: string) { 11 | const compile = module._compile 12 | module._compile = function (code: string, fileName: string) { 13 | return compile.call(this, transformESModuleToCommonJS(code), fileName) 14 | } 15 | return originalLoader.call(this, module, filename) 16 | } 17 | // @ts-ignore 18 | loader._originalLoader = originalLoader 19 | } 20 | 21 | /** 22 | * 取消注册 ES6 模块加载器 23 | * @param extension 支持的模块扩展名 24 | */ 25 | export function unregisterESMLoader(extension = ".js") { 26 | const loader = require.extensions[extension] as any 27 | if (loader._originalLoader) { 28 | require.extensions[extension] = loader._originalLoader 29 | } 30 | } 31 | 32 | /** 33 | * 快速转换 ES6 模块代码到 CommonJS 模块 34 | * @param code 要转换的 ES6 模块代码 35 | * @description 出于性能考虑,本函数有以下功能限制: 36 | * - 不支持同时导出多个变量(`export let a, b`/`export let [a, b]`),需逐个导出 37 | * - 模板字符串或正则表达式内出现 `import/export` 语句可能会出错,可写成如 `i\mport` 38 | * - 导出赋值操作会在最后执行,如果有循环依赖可能无法获取导出项 39 | */ 40 | export function transformESModuleToCommonJS(code: string) { 41 | let exportCode = "" 42 | code = code.replace(/'(?:[^\\'\n\r\u2028\u2029]|\\.)*'|"(?:[^\\"\n\r\u2028\u2029]|\\.)*"|\/\/[^\n\r\u2028\u2029]*|\/\*.*?(?:\*\/|$)|\/(?:[^\\/\n\r\u2028\u2029]|\\.)+\/|`(?:[^\\`$]|\\.|\$\{(?:[^{]|\{[^}]*\})*?\}|\$(?!\{))*`|\b(export\s+(?:(default\s+)?((?:const\s|let\s|var\s|(?:async\s+)?function\b(?:\s*\*)?|class\s)\s*)([a-zA-Z0-9_$\xAA-\uDDEF]+)|default\b)|(?:import\s*(?:\*\s*as\s*([a-zA-Z0-9_$\xAA-\uDDEF]+)|(\{.*?\})|\s([a-zA-Z0-9_$\xAA-\uDDEF]+)\s*(?:,\s*(\{.*?\}))?)\s*from|import\s*|export\s*(\*)\s*from|export\s*(\{.*?\})\s*from)\s*('(?:[^\\'\n\r\u2028\u2029]|\\.)*'|"(?:[^\\"\n\r\u2028\u2029]|\\.)*"))/sg, (source, importExport?: string, exportDefault?: string, exportPrefix?: string, exportName?: string, importAll?: string, importNames?: string, importDefault?: string, importNames2?: string, exportAll?: string, exportNames?: string, fromModule?: string) => { 43 | if (importExport) { 44 | if (fromModule) { 45 | if (importAll) { 46 | return `const ${importAll} = require(${fromModule});` 47 | } 48 | if (importNames) { 49 | return `const ${importNames.replace(/([a-zA-Z0-9_$\xAA-\uDDEF]\s*)\bas\b/g, "$1:")} = require(${fromModule});` 50 | } 51 | if (importDefault) { 52 | return `const __${importDefault} = require(${fromModule}), ${importDefault} = __${importDefault}.__esModule ? __${importDefault}.default : __${importDefault}${importNames2 ? `, ${importNames2.replace(/([a-zA-Z0-9_$\xAA-\uDDEF]\s*)\bas\b/g, "$1:")} = __${importDefault}` : ""};` 53 | } 54 | if (exportAll) { 55 | return `Object.assign(module.exports, require(${fromModule}));` 56 | } 57 | if (exportNames) { 58 | exportNames = exportNames.replace(/([a-zA-Z0-9_$\xAA-\uDDEF]\s*)\bas\b/g, "$1:") 59 | return `const ${exportNames} = require(${fromModule}); Object.assign(module.exports, ${exportNames});` 60 | } 61 | return `require(${fromModule});` 62 | } 63 | if (exportDefault || !exportName) { 64 | exportCode += `\nObject.defineProperty(module.exports, "__esModule", { value: true });` 65 | } 66 | if (exportName) { 67 | exportCode += `\nmodule.exports.${exportDefault ? "default" : exportName} = ${exportName};` 68 | return `${exportPrefix}${exportName}` 69 | } 70 | return `module.exports.default =` 71 | } 72 | return source 73 | }) 74 | return code + exportCode 75 | } 76 | 77 | /** 78 | * 将指定的目录添加到全局请求路径 79 | * @param dir 要添加的绝对路径 80 | */ 81 | export function addGlobalPath(dir: string) { 82 | // HACK 使用内部 API 添加 83 | const Module = require("module") 84 | if (!Module.globalPaths.includes(dir)) { 85 | process.env.NODE_PATH = process.env.NODE_PATH ? `${process.env.NODE_PATH}${delimiter}${dir}` : dir 86 | Module._initPaths() 87 | } 88 | } -------------------------------------------------------------------------------- /src/textDocument.ts: -------------------------------------------------------------------------------- 1 | import { indexToLineColumn, LineMap } from "./lineColumn" 2 | import { SourceMapBuilder, SourceMapData, toSourceMapBuilder } from "./sourceMap" 3 | import { SourceMapTextWriter, TextWriter } from "./textWriter" 4 | 5 | /** 表示一个文本文档,用于多次修改并生成新文本内容和源映射(Source Map)*/ 6 | export class TextDocument { 7 | 8 | /** 获取文档的原始内容 */ 9 | readonly content: string 10 | 11 | /** 获取文档的原始路径 */ 12 | readonly path?: string 13 | 14 | /** 获取文档的原始源映射 */ 15 | readonly sourceMap?: SourceMapBuilder 16 | 17 | /** 18 | * 初始化新的文档 19 | * @param content 文档的原始内容 20 | * @param path 文档的原始路径 21 | * @param sourceMap 文档的原始源映射 22 | */ 23 | constructor(content: string, path?: string, sourceMap?: SourceMapData | null) { 24 | this.content = content 25 | this.path = path 26 | if (sourceMap) { 27 | this.sourceMap = toSourceMapBuilder(sourceMap) 28 | } 29 | } 30 | 31 | /** 获取所有替换记录 */ 32 | readonly replacements: { 33 | /** 要替换的开始索引 */ 34 | startIndex: number 35 | /** 要替换的结束索引(不含)*/ 36 | endIndex: number 37 | /** 38 | * 计算要替换的新内容,如果是函数,则替换的内容将在最后生成时计算 39 | * @param args 生成时的附加参数 40 | * @returns 返回替换后的内容 41 | */ 42 | content: string | { write: TextDocument["write"] } | ((...args: readonly any[]) => string | { write: TextDocument["write"] }) 43 | }[] = [] 44 | 45 | /** 46 | * 替换文档中指定区间的内容 47 | * @param startIndex 要替换的开始索引 48 | * @param endIndex 要替换的结束索引(不含) 49 | * @param content 要替换的新内容,如果是函数则为最后根据生成目标自动计算的内容 50 | * @returns 返回替换记录 51 | */ 52 | replace(startIndex: number, endIndex: number, content: TextDocument["replacements"][0]["content"]) { 53 | const replacement = { startIndex, endIndex, content } 54 | let index = this.replacements.length 55 | for (; index > 0; index--) { 56 | if (startIndex >= this.replacements[index - 1].startIndex) { 57 | break 58 | } 59 | } 60 | if (index >= this.replacements.length) { 61 | this.replacements.push(replacement) 62 | } else { 63 | this.replacements.splice(index, 0, replacement) 64 | } 65 | return replacement 66 | } 67 | 68 | /** 69 | * 在文档中指定位置插入内容 70 | * @param index 要插入的索引 71 | * @param content 要插入的内容,如果是函数则为最后根据生成目标自动计算的内容 72 | * @returns 返回替换记录 73 | */ 74 | insert(index: number, content: TextDocument["replacements"][0]["content"]) { 75 | return this.replace(index, index, content) 76 | } 77 | 78 | /** 79 | * 在文档末尾插入内容 80 | * @param content 要插入的内容,如果是函数则为最后根据生成目标自动计算的内容 81 | * @returns 返回替换记录 82 | */ 83 | append(content: TextDocument["replacements"][0]["content"]) { 84 | const replacement = { startIndex: this.content.length, endIndex: this.content.length, content } 85 | this.replacements.push(replacement) 86 | return replacement 87 | } 88 | 89 | /** 90 | * 删除文档中指定区间的内容 91 | * @param startIndex 要删除的开始索引 92 | * @param endIndex 要删除的结束索引(不含) 93 | * @returns 返回替换记录 94 | */ 95 | remove(startIndex: number, endIndex: number) { 96 | return this.replace(startIndex, endIndex, "") 97 | } 98 | 99 | /** 行号索引 */ 100 | private _lineMap?: LineMap 101 | 102 | /** 103 | * 将当前文档的内容写入到目标写入器 104 | * @param writer 目标写入器 105 | * @param args 传递给计算内容的函数参数 106 | */ 107 | write(writer: TextWriter, ...args: any[]) { 108 | const sourceMap = writer instanceof SourceMapTextWriter 109 | let lastIndex = 0 110 | for (const replacement of this.replacements) { 111 | // 写入上一次替换记录到这次更新记录中间的文本 112 | if (lastIndex < replacement.startIndex) { 113 | if (sourceMap) { 114 | const loc = (this._lineMap || (this._lineMap = new LineMap(this.content))).indexToLineColumn(lastIndex) 115 | writer.write(this.content, lastIndex, replacement.startIndex, this.path, loc.line, loc.column, undefined, this.sourceMap) 116 | } else { 117 | writer.write(this.content, lastIndex, replacement.startIndex) 118 | } 119 | } 120 | // 写入替换的文本 121 | const content = typeof replacement.content === "function" ? replacement.content(...args) : replacement.content 122 | if (typeof content === "string") { 123 | writer.write(content) 124 | } else { 125 | content.write(writer, ...args) 126 | } 127 | // 更新最后一次替换位置 128 | lastIndex = replacement.endIndex 129 | } 130 | // 写入最后一个替换记录之后的文本 131 | if (lastIndex < this.content.length) { 132 | if (sourceMap) { 133 | const loc = this._lineMap ? this._lineMap.indexToLineColumn(lastIndex) : indexToLineColumn(this.content, lastIndex) 134 | writer.write(this.content, lastIndex, this.content.length, this.path, loc.line, loc.column, undefined, this.sourceMap) 135 | } else { 136 | writer.write(this.content, lastIndex, this.content.length) 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * 生成最终文本内容和源映射 143 | * @param args 传递给计算内容的函数参数 144 | */ 145 | generate(...args: any[]) { 146 | const writer = new SourceMapTextWriter() 147 | this.write(writer, ...args) 148 | return writer 149 | } 150 | 151 | /** 152 | * 生成最终文本内容 153 | * @param args 传递给计算内容的函数参数 154 | */ 155 | toString(...args: any[]) { 156 | const writer = new TextWriter() 157 | this.write(writer, ...args) 158 | return writer.toString() 159 | } 160 | 161 | } 162 | 163 | /** 164 | * 增删指定的内容并更新源映射(Source Map) 165 | * @param data 要更新的数据 166 | * @param data.content 要更新的内容 167 | * @param data.path 内容的源路径,用于生成新的源映射 168 | * @param data.sourceMapData 内容的源映射 169 | * @param index 增删的索引(从 0 开始) 170 | * @param deleteCount 要删除的数目 171 | * @param insert 要插入的字符串内容 172 | * @returns 返回替换后的数据 173 | */ 174 | export function splice(data: { content: string, path?: string, sourceMap?: SourceMapData | null }, index: number, deleteCount: number, insert: any) { 175 | insert = String(insert) 176 | if (deleteCount === 0 && insert.length === 0) { 177 | return data 178 | } 179 | const document = new TextDocument(data.content, data.path, data.sourceMap) 180 | document.replace(index, index + deleteCount, insert) 181 | return document.generate() 182 | } 183 | 184 | /** 185 | * 替换指定的内容并更新源映射(Source Map) 186 | * @param data 要更新的数据 187 | * @param data.content 要更新的内容 188 | * @param data.path 内容的源路径,用于生成新的源映射 189 | * @param data.sourceMapData 内容的源映射 190 | * @param search 要搜索的内容 191 | * @param replacement 要替换的内容 192 | * @returns 返回替换后的数据 193 | */ 194 | export function replace(data: { content: string, path?: string, sourceMap?: SourceMapData | null }, search: string | RegExp, replacement: any | ((source: string, ...args: any[]) => string)) { 195 | if (search instanceof RegExp) { 196 | const document = new TextDocument(data.content, data.path, data.sourceMap) 197 | data.content.replace(search, (...args: any[]) => { 198 | const source = args[0] as string 199 | const index = args[args.length - 2] as number 200 | document.replace(index, index + source.length, typeof replacement === "string" ? replacement.replace(/\$([&1-9])/g, (source2, groupIndex: string) => { 201 | const groupIndexNumber = +groupIndex || 0 202 | return groupIndexNumber < args.length - 2 ? args[groupIndexNumber] : source2 203 | }) : typeof replacement === "function" ? (replacement as Function)(...args) : String(replacement)) 204 | return "" 205 | }) 206 | return document.generate() 207 | } 208 | const index = data.content.indexOf(search) 209 | if (index < 0) { 210 | return data 211 | } 212 | return splice(data, index, search.length, replacement) 213 | } -------------------------------------------------------------------------------- /src/textWriter.ts: -------------------------------------------------------------------------------- 1 | import { SourceMapBuilder, SourceMapData, toSourceMapBuilder } from "./sourceMap" 2 | 3 | /** 表示一个文本写入器 */ 4 | export class TextWriter { 5 | 6 | /** 获取或设置已写入的文本内容 */ 7 | content = "" 8 | 9 | /** 获取已写入的文本内容 */ 10 | toString() { return this.content } 11 | 12 | /** 获取或设置缩进字符 */ 13 | indentChar = "\t" 14 | 15 | /** 获取或设置当前使用的缩进字符串 */ 16 | indentString = "" 17 | 18 | /** 增加一个缩进 */ 19 | indent() { this.indentString += this.indentChar } 20 | 21 | /** 减少一个缩进 */ 22 | unindent() { this.indentString = this.indentString.substring(this.indentChar.length) } 23 | 24 | /** 25 | * 在末尾写入一段文本 26 | * @param content 要写入的内容 27 | * @param startIndex 要写入的内容的开始索引(从 0 开始) 28 | * @param endIndex 要写入的内容的结束索引(从 0 开始)(不含) 29 | * @param sourcePath 内容的源文件路径或索引 30 | * @param sourceLine 内容在源文件中的行号(从 0 开始) 31 | * @param sourceColumn 内容在源文件中的列号(从 0 开始) 32 | * @param name 内容对应的符号名称或索引 33 | * @param sourceMap 如果指定了源文件的源映射,则复制所有映射点 34 | */ 35 | write(content: string, startIndex = 0, endIndex = content.length, sourcePath?: string | number, sourceLine?: number, sourceColumn?: number, name?: string | number, sourceMap?: SourceMapData) { 36 | let lastIndex = startIndex 37 | if (this.indentString) { 38 | const prevChar = this.content.charCodeAt(this.content.length - 1) 39 | let isLineStart = prevChar === 10 /*\n*/ || prevChar === 13 /*\r*/ || prevChar !== prevChar /*NaN*/ 40 | for (; startIndex < endIndex; startIndex++) { 41 | const char = content.charCodeAt(startIndex) 42 | if (char === 10 /*\n*/) { 43 | isLineStart = true 44 | } else if (char === 13 /*\r*/) { 45 | if (content.charCodeAt(startIndex + 1) === 10 /*\n*/) { 46 | startIndex++ 47 | } 48 | isLineStart = true 49 | } else if (isLineStart) { 50 | if (startIndex > lastIndex) { 51 | this.content += content.substring(lastIndex, startIndex) 52 | lastIndex = startIndex 53 | } 54 | this.content += this.indentString 55 | isLineStart = false 56 | } 57 | } 58 | } 59 | this.content += content.substring(lastIndex, endIndex) 60 | } 61 | 62 | } 63 | 64 | /** 表示一个支持源映射(Source Map)的文本写入器 */ 65 | export class SourceMapTextWriter extends TextWriter { 66 | 67 | /** 当前使用的源映射生成器 */ 68 | readonly sourceMapBuilder = new SourceMapBuilder() 69 | 70 | /** 获取当前生成的源映射 */ 71 | get sourceMap() { return this.sourceMapBuilder.toJSON() } 72 | 73 | /** 判断或设置是否只生成行映射信息 */ 74 | noColumnMappings = false 75 | 76 | /** 获取当前写入的行号 */ 77 | line = 0 78 | 79 | /** 获取当前写入的列号 */ 80 | column = 0 81 | 82 | /** 83 | * 在末尾写入一段文本 84 | * @param content 要写入的内容 85 | * @param startIndex 要写入的内容的开始索引(从 0 开始) 86 | * @param endIndex 要写入的内容的结束索引(从 0 开始)(不含) 87 | * @param sourcePath 内容的源文件路径或索引 88 | * @param sourceLine 内容在源文件中的行号(从 0 开始) 89 | * @param sourceColumn 内容在源文件中的列号(从 0 开始) 90 | * @param name 内容对应的符号名称或索引 91 | * @param sourceMap 如果指定了源文件的源映射,则复制所有映射点 92 | */ 93 | write(content: string, startIndex = 0, endIndex = content.length, sourcePath?: string | number, sourceLine?: number, sourceColumn?: number, name?: string | number, sourceMap?: SourceMapData) { 94 | let line = this.line 95 | let column = this.column 96 | let lastIndex = startIndex 97 | const sourceMapBuilder = this.sourceMapBuilder 98 | 99 | const prevChar = this.content.charCodeAt(this.content.length - 1) 100 | let isLineStart = prevChar === 10 /*\n*/ || prevChar === 13 /*\r*/ || prevChar !== prevChar /*NaN*/ 101 | 102 | if (sourceColumn === undefined) { 103 | // 如果没有提供源位置,无法生成源映射,直接写入文本 104 | for (; startIndex < endIndex; startIndex++) { 105 | switch (content.charCodeAt(startIndex)) { 106 | case 13 /*\r*/: 107 | if (content.charCodeAt(startIndex + 1) === 10 /*\n*/) { 108 | startIndex++ 109 | } 110 | // fall through 111 | case 10 /*\n*/: 112 | isLineStart = true 113 | line++ 114 | break 115 | default: 116 | if (isLineStart) { 117 | if (startIndex > lastIndex) { 118 | this.content += content.substring(lastIndex, startIndex) 119 | lastIndex = startIndex 120 | } 121 | this.content += this.indentString 122 | isLineStart = false 123 | } 124 | column++ 125 | break 126 | } 127 | } 128 | } else if (sourceMap === undefined) { 129 | // 如果没有提供源映射,生成新源映射 130 | if (typeof sourcePath === "string") { 131 | sourcePath = sourceMapBuilder.addSource(sourcePath) 132 | } 133 | for (let prevCharType: number | undefined; startIndex < endIndex; startIndex++) { 134 | const char = content.charCodeAt(startIndex) 135 | switch (char) { 136 | case 13 /*\r*/: 137 | if (content.charCodeAt(startIndex + 1) === 10 /*\n*/) { 138 | startIndex++ 139 | } 140 | // fall through 141 | case 10 /*\n*/: 142 | isLineStart = true 143 | line++ 144 | sourceLine!++ 145 | sourceColumn = column = 0 146 | prevCharType = undefined 147 | break 148 | default: 149 | if (isLineStart) { 150 | if (startIndex > lastIndex) { 151 | this.content += content.substring(lastIndex, startIndex) 152 | lastIndex = startIndex 153 | } 154 | this.content += this.indentString 155 | isLineStart = false 156 | } 157 | const charType = this.noColumnMappings ? 0 : 158 | char === 32 /* */ || char === 9 /*\t*/ ? 32 : 159 | char >= 97 /*a*/ && char <= 122 /*z*/ || char >= 65 /*A*/ && char <= 90 /*Z*/ || char >= 48 /*0*/ && char <= 57 /*9*/ || char >= 0xAA && char <= 0xDDEF || char === 95 /*_*/ || char === 36 /*$*/ ? 65 : 160 | char === 44 /*,*/ || char === 59 /*;*/ || char === 40 /*(*/ || char === 41 /*)*/ || char === 123 /*{*/ || char === 125 /*}*/ || char === 91 /*[*/ || char === 93 /*]*/ ? char : 1 161 | if (charType !== prevCharType) { 162 | this.sourceMapBuilder.addMapping(line, column, sourcePath, sourceLine, sourceColumn, name) 163 | prevCharType = charType 164 | } 165 | column++ 166 | sourceColumn!++ 167 | break 168 | } 169 | } 170 | } else { 171 | const originalSourceMap = toSourceMapBuilder(sourceMap) 172 | // 如果提供了源映射,直接拷贝所有映射点 173 | const sourceMappings: number[] = [] 174 | let mappings = originalSourceMap.mappings[sourceLine!] || [] 175 | let mappingIndex = 0 176 | let foundStartMapping = false 177 | for (; mappingIndex < mappings.length; mappingIndex++) { 178 | const lastMapping = mappings[mappingIndex] 179 | if (lastMapping.generatedColumn >= sourceColumn!) { 180 | foundStartMapping = lastMapping.generatedColumn === sourceColumn 181 | break 182 | } 183 | } 184 | // 如果插入的开始位置没有匹配的映射点,则补插一个 185 | if (!foundStartMapping && startIndex < endIndex) { 186 | if (mappingIndex > 0) { 187 | const prevMapping = mappings[mappingIndex - 1] 188 | if (prevMapping.sourceIndex === undefined) { 189 | sourceMapBuilder.addMapping(line, column) 190 | } else { 191 | sourceMapBuilder.addMapping(line, column, sourceMappings[prevMapping.sourceIndex] = sourceMapBuilder.addSource(originalSourceMap.sources[prevMapping.sourceIndex]), prevMapping.sourceLine!, prevMapping.sourceColumn! + (sourceColumn! - prevMapping.generatedColumn), prevMapping.nameIndex === undefined ? undefined : originalSourceMap.names[prevMapping.nameIndex]) 192 | } 193 | } else { 194 | sourceMapBuilder.addMapping(line, column) 195 | } 196 | } 197 | // 复制映射点 198 | for (; startIndex < endIndex; startIndex++) { 199 | switch (content.charCodeAt(startIndex)) { 200 | case 13 /*\r*/: 201 | if (content.charCodeAt(startIndex + 1) === 10 /*\n*/) { 202 | startIndex++ 203 | } 204 | // fall through 205 | case 10 /*\n*/: 206 | isLineStart = true 207 | line++ 208 | mappingIndex = sourceColumn = column = 0 209 | mappings = originalSourceMap.mappings[++sourceLine!] || [] 210 | break 211 | default: 212 | if (isLineStart) { 213 | if (startIndex > lastIndex) { 214 | this.content += content.substring(lastIndex, startIndex) 215 | lastIndex = startIndex 216 | } 217 | this.content += this.indentString 218 | isLineStart = false 219 | } 220 | if (mappingIndex < mappings.length) { 221 | const mapping = mappings[mappingIndex] 222 | if (mapping.generatedColumn === sourceColumn) { 223 | mappingIndex++ 224 | if (mapping.sourceIndex === undefined) { 225 | sourceMapBuilder.addMapping(line, column) 226 | } else { 227 | let newSourceIndex = sourceMappings[mapping.sourceIndex] 228 | if (newSourceIndex === undefined) { 229 | sourceMappings[mapping.sourceIndex] = newSourceIndex = sourceMapBuilder.addSource(originalSourceMap.sources[mapping.sourceIndex]) 230 | } 231 | sourceMapBuilder.addMapping(line, column, newSourceIndex, mapping.sourceLine!, mapping.sourceColumn!, mapping.nameIndex === undefined ? undefined : originalSourceMap.names[mapping.nameIndex]) 232 | } 233 | } 234 | } 235 | column++ 236 | sourceColumn!++ 237 | break 238 | } 239 | } 240 | } 241 | this.content += content.substring(lastIndex, endIndex) 242 | this.line = line 243 | this.column = column 244 | } 245 | 246 | } -------------------------------------------------------------------------------- /src/tpl.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from "path" 2 | import { encodeHTML } from "./html" 3 | import { quoteJSString } from "./js" 4 | import { transformESModuleToCommonJS } from "./require" 5 | 6 | /** 7 | * 编译指定的模板为函数 8 | * @param content 要编译的模板内容 9 | * @param async 是否返回异步函数 10 | * @param path 模块的路径,用于在模块内部导入其它模块 11 | * @param paramName 函数接收的参数名,在模板中可以直接使用此参数值 12 | * @param escape 编码模板中表达式计算结果的回调函数 13 | */ 14 | export function compileTPL(content: string, async?: boolean, path = "", paramName = "$", escape = (s: any) => encodeHTML(String(s))): (data?: any) => string | Promise { 15 | const code = compileTPLWorker(content) 16 | const dir = dirname(path) 17 | const Module = module.constructor as any as typeof import("module") 18 | const require = Module.createRequire ? Module.createRequire(resolve(path)) : Module["createRequireFromPath"](path) 19 | if (async) { 20 | const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor 21 | return new AsyncFunction("require", "__dirname", "__filename", "__escape__", paramName, code).bind(null, require, dir, path, escape) 22 | } 23 | return new Function("require", "__dirname", "__filename", "__escape__", paramName, code).bind(null, require, dir, path, escape) 24 | } 25 | 26 | /** 27 | * 解析模板并返回生成的 JavaScript 代码 28 | * @param content 要解析的模板内容 29 | */ 30 | function compileTPLWorker(content: string) { 31 | let result = `var __result__="",__t__;` 32 | let index = 0 33 | for (; ; index++) { 34 | const braceStart = content.indexOf("{", index) 35 | if (braceStart < 0) { 36 | break 37 | } 38 | const hasAt = content.charCodeAt(braceStart - 1) === 0x40 /*@*/ 39 | const prefixStart = index 40 | let prefixEnd = hasAt ? braceStart - 1 : braceStart 41 | let code: string | undefined 42 | const keywordStart = braceStart + 1 43 | if (content.charCodeAt(keywordStart) === 0x2F /*/*/) { 44 | index = findBrace(content, keywordStart + 1) 45 | switch (content.substring(keywordStart + 1, index).trim()) { 46 | case "if": 47 | case "switch": 48 | case "for": 49 | case "while": 50 | case "try": 51 | code = "}" 52 | break 53 | case "function": 54 | code = "return __result__}" 55 | break 56 | default: 57 | index = findBrace(content, keywordStart) 58 | break 59 | } 60 | } else { 61 | const keywordEnd = readKeyword(content, keywordStart) 62 | index = findBrace(content, keywordEnd) 63 | switch (content.substring(keywordStart, keywordEnd)) { 64 | case "void": 65 | code = `${content.substring(keywordEnd, index)};` 66 | break 67 | case "if": 68 | case "switch": 69 | case "for": 70 | case "while": 71 | case "try": 72 | code = `${content.substring(keywordStart, index)}{` 73 | break 74 | case "else": 75 | case "catch": 76 | case "finally": 77 | code = `}${content.substring(keywordStart, index)}{` 78 | break 79 | case "let": 80 | case "const": 81 | case "var": 82 | case "break": 83 | case "continue": 84 | case "return": 85 | code = `${content.substring(keywordStart, index)};` 86 | break 87 | case "case": 88 | case "default": 89 | code = `${content.substring(keywordStart, index)}:` 90 | break 91 | case "function": 92 | code = `${content.substring(keywordStart, index)}{var __result__="",__t__;` 93 | break 94 | case "import": 95 | code = `${transformESModuleToCommonJS(content.substring(keywordStart, index))};` 96 | break 97 | } 98 | } 99 | if (code) { 100 | let end = prefixEnd - 1 101 | while (isSpace(content.charCodeAt(end))) { 102 | end-- 103 | } 104 | if (isLineBreak(content.charCodeAt(end))) { 105 | if (content.charCodeAt(end) === 10 /*\n*/ && content.charCodeAt(end - 1) === 13 /*\r*/) { 106 | end-- 107 | } 108 | prefixEnd = end 109 | } 110 | let start = index + 1 111 | while (isSpace(content.charCodeAt(start))) { 112 | start++ 113 | } 114 | if (isLineBreak(content.charCodeAt(start))) { 115 | index = start - 1 116 | } 117 | } else if (keywordStart < index) { 118 | code = `if((__t__=${content.substring(keywordStart, index)})!=null)__result__+=${hasAt ? "__t__" : "__escape__(__t__)"};` 119 | } else { 120 | code = "" 121 | } 122 | if (prefixStart < prefixEnd) { 123 | result += `__result__+=${quoteJSString(content.substring(prefixStart, prefixEnd))};` 124 | } 125 | result += code 126 | } 127 | if (index < content.length) { 128 | result += `__result__+=${quoteJSString(content.substring(index))};` 129 | } 130 | return result + `return __result__;` 131 | } 132 | 133 | /** 判断指定的字符是否是空格 */ 134 | function isSpace(char: number) { 135 | return char === 0x20 || char === 9 /*\t*/ || char === 0x00A0 136 | } 137 | 138 | /** 判断指定的字符是否是空格 */ 139 | function isLineBreak(char: number) { 140 | return char === 10 /*\n*/ || char === 13 /*\r*/ || char === 0x2028 || char === 0x2029 || isNaN(char) 141 | } 142 | 143 | /** 获取开头的关键字 */ 144 | function readKeyword(content: string, index: number) { 145 | for (; index < content.length; index++) { 146 | const char = content.charCodeAt(index) 147 | if (char <= 0x2F /*/*/ || char >= 0x3A /*:*/ && char <= 0x40 /*@*/ || char >= 0x5B /*[*/ && char <= 0x60 /*`*/ || char >= 0x7B /*{*/ && char <= 0x7F /*DEL*/) { 148 | break 149 | } 150 | } 151 | return index 152 | } 153 | 154 | /** 查找匹配的右大括号 */ 155 | function findBrace(content: string, index: number) { 156 | let braceCount = 1 157 | outer: for (; index < content.length; index++) { 158 | const char = content.charCodeAt(index) 159 | switch (char) { 160 | case 0x7D /*}*/: 161 | if (--braceCount < 1) { 162 | break outer 163 | } 164 | break 165 | case 0x7B /*{*/: 166 | braceCount++ 167 | break 168 | case 0x2F /*/*/: 169 | switch (content.charCodeAt(index + 1)) { 170 | case 0x2A/***/: 171 | index = content.indexOf("*/", index + 2) 172 | if (index < 0) { 173 | return content.length 174 | } 175 | index++ 176 | continue 177 | case 0x2F/*/*/: 178 | index++ 179 | while (++index < content.length) { 180 | switch (content.charCodeAt(index)) { 181 | case 10/*\n*/: 182 | case 13/*\r*/: 183 | case 0x2028: 184 | case 0x2029: 185 | continue outer 186 | } 187 | } 188 | break outer 189 | default: 190 | const startIndex = index 191 | while (++index < content.length) { 192 | switch (content.charCodeAt(index)) { 193 | case char: 194 | continue outer 195 | case 10/*\n*/: 196 | case 13/*\r*/: 197 | case 0x2028: 198 | case 0x2029: 199 | index = startIndex + 1 200 | continue outer 201 | case 0x5C /*\*/: 202 | index++ 203 | continue 204 | } 205 | } 206 | index = startIndex + 1 207 | continue 208 | } 209 | case 0x27 /*'*/: 210 | case 0x22 /*"*/: 211 | while (++index < content.length) { 212 | switch (content.charCodeAt(index)) { 213 | case char: 214 | case 10/*\n*/: 215 | case 13/*\r*/: 216 | case 0x2028: 217 | case 0x2029: 218 | continue outer 219 | case 0x5C /*\*/: 220 | index++ 221 | continue 222 | } 223 | } 224 | break outer 225 | case 0x60 /*`*/: 226 | while (++index < content.length) { 227 | switch (content.charCodeAt(index)) { 228 | case char: 229 | continue outer 230 | case 0x5C /*\*/: 231 | index++ 232 | continue 233 | case 0x24 /*$*/: 234 | if (content.charCodeAt(index + 1) === 0x7B /*{*/) { 235 | index = findBrace(content, index + 2) 236 | } 237 | continue 238 | } 239 | } 240 | break outer 241 | } 242 | } 243 | return index 244 | } -------------------------------------------------------------------------------- /src/url.ts: -------------------------------------------------------------------------------- 1 | import { posix } from "path" 2 | import { format, parse, resolve } from "url" 3 | 4 | /** 5 | * 获取指定地址对应的绝对地址 6 | * @param base 要使用的基地址 7 | * @param url 要处理的地址 8 | * @example resolveURL("http://example.com", "foo") // "http://example.com/foo" 9 | */ 10 | export function resolveURL(base: string, url: string) { 11 | return resolve(base, url) 12 | } 13 | 14 | /** 15 | * 获取指定地址对应的相对地址 16 | * @param base 要使用的基地址 17 | * @param url 要处理的地址 18 | * @example relativeURL("http://example.com", "http://example.com/foo") // "foo" 19 | */ 20 | export function relativeURL(base: string, url: string) { 21 | // 忽略 data:... 等 URI 22 | if (/^[\w+\-\.\+]+:(?!\/)/.test(url)) { 23 | return url 24 | } 25 | const baseObject = parse(base, false, true) 26 | const urlObject = parse(url, false, true) 27 | // 协议不同,只能使用绝对路径 28 | if (baseObject.protocol !== urlObject.protocol) { 29 | return format(urlObject) 30 | } 31 | // 协议相同但主机(含端口)或用户名(含密码)不同,使用省略协议的绝对路径 32 | if (baseObject.host !== urlObject.host || baseObject.auth !== urlObject.auth) { 33 | if (urlObject.slashes) { 34 | delete urlObject.protocol 35 | } 36 | return format(urlObject) 37 | } 38 | // 两个地址必须都是相对路径或都是绝对路径,否则只能使用绝对路径 39 | if (baseObject.pathname && urlObject.pathname && (baseObject.pathname.charCodeAt(0) === 47 /*/*/) !== (urlObject.pathname.charCodeAt(0) === 47 /*/*/)) { 40 | return format(urlObject) 41 | } 42 | // 计算地址开头的相同部分,以 `/` 为界 43 | base = baseObject.pathname ? posix.normalize(baseObject.pathname) : "" 44 | url = urlObject.pathname ? posix.normalize(urlObject.pathname) : "" 45 | let index = -1 46 | let i = 0 47 | for (; i < base.length && i < url.length; i++) { 48 | const ch1 = base.charCodeAt(i) 49 | const ch2 = url.charCodeAt(i) 50 | if (ch1 !== ch2) { 51 | break 52 | } 53 | if (ch1 === 47 /*/*/) { 54 | index = i 55 | } 56 | } 57 | // 重新追加不同的路径部分 58 | let pathname = url.substring(index + 1) || (i === base.length ? "" : ".") 59 | for (let i = index + 1; i < base.length; i++) { 60 | if (base.charCodeAt(i) === 47 /*/*/) { 61 | pathname = pathname === "." ? "../" : `../${pathname}` 62 | } 63 | } 64 | return `${pathname}${urlObject.search || ""}${urlObject.hash || ""}` 65 | } 66 | 67 | /** 68 | * 规范化指定的地址 69 | * @param url 要处理的地址 70 | * @example normalizeURL("http://example.com/foo/../relative") // "http://example.com/relative" 71 | */ 72 | export function normalizeURL(url: string) { 73 | if (!url || /^[\w+\-\.\+]+:(?!\/)/.test(url)) { 74 | return url 75 | } 76 | const urlObject = parse(url, false, true) 77 | if (urlObject.pathname) { 78 | urlObject.pathname = posix.normalize(urlObject.pathname) 79 | } 80 | return format(urlObject) 81 | } 82 | 83 | /** 84 | * 判断指定的地址是否是绝对地址 85 | * @param url 要判断的地址 86 | * @example isAbsoluteURL("http://example.com/foo") // true 87 | */ 88 | export function isAbsoluteURL(url: string) { 89 | return /^(?:\/|[\w+\-\.\+]+:)/.test(url) 90 | } 91 | 92 | /** 93 | * 判断指定的地址是否是外部地址 94 | * @param url 要判断的地址 95 | * @example isExternalURL("http://example.com/foo") // true 96 | */ 97 | export function isExternalURL(url: string) { 98 | return /^([\w\-]*:)?\/\//.test(url) 99 | } 100 | 101 | /** 102 | * 如果地址是相对地址则更新基地址,否则返回原地址 103 | * @param base 要使用的基地址 104 | * @param url 要处理的地址 105 | * @example setBaseURL("foo", "base") // "base/foo" 106 | */ 107 | export function setBaseURL(url: string, base: string) { 108 | if (isAbsoluteURL(url)) { 109 | return url 110 | } 111 | return resolveURL(base + "/", url) 112 | } 113 | 114 | /** 115 | * 替换字符串中的地址 116 | * @param content 要处理的内容 117 | * @param replacement 要替换的内容,如果是字符串,则其中的 `$&` 代表匹配的地址 118 | * @param replacement.url 匹配的地址 119 | * @param replacement.index 本次匹配的地址在原内容的索引 120 | * @param replacement.return 返回替换的内容 121 | * @example replaceURL("请点击 http://example.com 继续", url => `${url}`) // "请点击 http://example.com 继续" 122 | */ 123 | export function replaceURL(content: string, replacement: string | ((url: string, index: number) => string)) { 124 | return content.replace(/\b((?:[a-z][\w\-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig, replacement as any) 125 | } -------------------------------------------------------------------------------- /src/vm.ts: -------------------------------------------------------------------------------- 1 | import Module = require("module") 2 | import { dirname, resolve } from "path" 3 | import { Context, runInNewContext, RunningScriptOptions } from "vm" 4 | 5 | /** 6 | * 在沙盒中执行指定的 JavaScript 代码 7 | * @param code 要执行的代码 8 | * @param context 传入的全局变量,键为变量名,值为变量值 9 | * @param options 附加选项 10 | * @returns 返回代码的执行结果 11 | * @description 注意本函数不提供安全隔离,不能用于执行不信任的代码 12 | */ 13 | export function runInVM(code: string, context?: Context, options?: RunningScriptOptions | string) { 14 | const path = typeof options === "string" ? options : options?.filename ?? "" 15 | let require: typeof module.require | undefined 16 | context = { 17 | __proto__: global, 18 | get global() { return context }, 19 | // @ts-ignore 20 | get require() { return require || (require = Module.createRequire ? Module.createRequire(resolve(path)) : Module.createRequireFromPath(path)) }, 21 | get __filename() { return path }, 22 | get __dirname() { return dirname(path) }, 23 | ...context 24 | } 25 | return runInNewContext(code, context, options) 26 | } -------------------------------------------------------------------------------- /src/workerPool.ts: -------------------------------------------------------------------------------- 1 | import { MessagePort, Worker as NativeWorker, WorkerOptions } from "worker_threads" 2 | 3 | /** 表示一个线程池 */ 4 | export class WorkerPool { 5 | 6 | /** 获取在子线程执行的代码 */ 7 | readonly workerCode: string 8 | 9 | /** 获取创建子线程的附加选项 */ 10 | readonly workerOptions: WorkerOptions 11 | 12 | /** 获取允许创建的最大子线程数, 如果为 0 表示不启用线程 */ 13 | readonly size: number 14 | 15 | /** 16 | * 初始化新的线程池 17 | * @param worker 要在子线程执行的函数,函数不可使用闭包 18 | * @param worker.data 由主线程发送的附加数据 19 | * @param worker.context 子线程的上下文对象 20 | * @param worker.return 返回回传给主线程的对象 21 | * @param options 附加选项 22 | */ 23 | constructor(worker: (data: any, context: WorkerContext) => any, options?: WorkerPoolOptions) { 24 | // 只有 Node v11.5+ 默认支持线程,对于不支持的版本,强制设置 size 为 0,关闭多线程 25 | let size = options?.size 26 | if (supportWorker()) { 27 | if (size === undefined) size = Math.ceil(require("os").cpus().length / 2) 28 | } else { 29 | size = 0 30 | } 31 | this.size = size 32 | this.functions = options?.functions 33 | this.workerOptions = { ...options, eval: true } 34 | // 内部父子线程通信协议: 35 | // 主线程发送: 36 | // 1) [0, 参数]: 调用子线程主函数 37 | // 2) [1, 调用ID, 函数名, 参数]: 调用子线程其它函数 38 | // 3) [2, 调用ID, 数据]: 通知子线程调用成功 39 | // 4) [-3, 调用ID, 错误信息]: 通知子线程调用出错 40 | // 子线程发送: 41 | // 1) [0, 数据]: 通知主线程调用成功 42 | // 2) [-1, 错误信息]: 通知主线程调用出错 43 | // 3) [1, 调用ID, 函数名,参数]: 调用主线程其它函数 44 | // 4) [2, 调用ID, 数据]: 通知主线程调用成功 45 | // 5) [-3, 调用ID, 错误信息]: 通知主线程调用出错 46 | this.workerCode = `(worker => { 47 | const {isMainThread, parentPort, workerData} = require("worker_threads") 48 | process.on("uncaughtException", error => { 49 | console.error(error) 50 | process.exit(-1) 51 | }) 52 | process.on("unhandledRejection", error => { 53 | console.error(error) 54 | process.exit(-1) 55 | }) 56 | let remoteCallId = 0 57 | const remoteCalls = new Map() 58 | const context = { 59 | isMainThread, 60 | workerData, 61 | call(name, data, transferList) { 62 | return new Promise((resolve, reject) => { 63 | const callId = remoteCallId++ 64 | remoteCalls.set(callId, [resolve, reject]) 65 | try { 66 | parentPort.postMessage([1, callId, name, data], transferList) 67 | } catch (e) { 68 | reject({name: e.name, message: "Cannot send data to main thread: " + e.message, stack: e.stack }) 69 | } 70 | }) 71 | }, 72 | transferList: undefined 73 | } 74 | parentPort.on("message", async data => { 75 | const code = data[0] 76 | if (code) { 77 | const callId = data[1] 78 | const remoteCall = remoteCalls.get(callId) 79 | if (remoteCall) { 80 | remoteCalls.delete(callId) 81 | remoteCall[code > 0 ? 0 : 1](data[2]) 82 | } 83 | return 84 | } 85 | try { 86 | data[1] = await worker(data[1], context) 87 | } catch (e) { 88 | data[0] = -1 89 | data[1] = e instanceof Error ? {name: e.name, message: e.message, stack: e.stack, code: e.code } : String(e) 90 | } 91 | try { 92 | const transferList = context.transferList 93 | if (transferList) { 94 | context.transferList = undefined 95 | parentPort.postMessage(data, transferList) 96 | } else { 97 | parentPort.postMessage(data) 98 | } 99 | } catch (e) { 100 | data[0] = -1 101 | data[1] = {name: e.name, message: "Cannot send data to main thread: " + e.message, stack: e.stack } 102 | parentPort.postMessage(data) 103 | } 104 | }) 105 | })(${worker})` 106 | if (size <= 0) { 107 | const context: WorkerContext = { 108 | isMainThread: true, 109 | workerData: this.workerOptions.workerData, 110 | call: async (name, data) => await this.onCall(name, data, []), 111 | set transferList(value: (ArrayBuffer | MessagePort)[]) { } 112 | } 113 | this.exec = async data => await worker(data, context) 114 | } 115 | } 116 | 117 | /** 获取所有子线程 */ 118 | readonly workers: Worker[] = [] 119 | 120 | /** 正在排队的第一个任务,如果没有任务正在执行则为 `undefined` */ 121 | private _firstTask?: { 122 | readonly data?: any 123 | readonly transferList?: (ArrayBuffer | MessagePort)[] 124 | readonly resolve: (value: any) => void 125 | readonly reject: (reason: any) => void 126 | next?: WorkerPool["_firstTask"] 127 | } 128 | 129 | /** 正在排队的最后一个异步任务,如果没有任务正在排队则为 `undefined` */ 130 | private _lastTask?: WorkerPool["_firstTask"] 131 | 132 | /** 133 | * 在子线程中执行任务 134 | * @param data 传递给子线程的数据,数据的类型必须可复制 135 | * @param transferList 要移动的内存对象,移动后当前线程将无法使用该对象,如果未设置,则对象将被复制到其它线程 136 | */ 137 | exec(data?: any, transferList?: (ArrayBuffer | MessagePort)[]) { 138 | // 先使用空闲的线程 139 | const idleWorker = this.workers.find(worker => !worker.running) || (this.workers.length < this.size ? this.createNativeWorker() as Worker : null) 140 | if (idleWorker) { 141 | return this._execWorker(idleWorker, data, transferList) 142 | } 143 | // 排队等待 144 | return new Promise((resolve, reject) => { 145 | const nextTask = { data, transferList, resolve, reject } 146 | if (this._lastTask) { 147 | this._lastTask = this._lastTask.next = nextTask 148 | } else { 149 | this._firstTask = this._lastTask = nextTask 150 | } 151 | }) 152 | } 153 | 154 | /** 155 | * 创建原生子线程 156 | */ 157 | protected createNativeWorker() { 158 | const worker = new ((require("worker_threads") as typeof import("worker_threads")).Worker)(this.workerCode, this.workerOptions) 159 | worker.unref() 160 | this.workers.push(worker) 161 | const removeSelf = () => { 162 | const index = this.workers.indexOf(worker) 163 | if (index >= 0) { 164 | this.workers.splice(index, 1) 165 | } 166 | } 167 | worker.on("exit", removeSelf) 168 | worker.on("error", removeSelf) 169 | return worker 170 | } 171 | 172 | /** 173 | * 在指定的子线程执行任务 174 | * @param worker 要使用的子线程 175 | * @param data 传递给子线程的数据,数据的类型必须可复制 176 | * @param transferList 要移动的内存对象,移动后当前线程将无法使用该对象,如果未设置,则对象将被复制到其它线程 177 | */ 178 | private _execWorker(worker: Worker, data?: any, transferList?: (ArrayBuffer | MessagePort)[]) { 179 | worker.running = true 180 | return new Promise((resolve, reject) => { 181 | const handleMessage = async (data: any[]) => { 182 | const code = data[0] 183 | if (code === 1) { 184 | const transferList: (ArrayBuffer | MessagePort)[] = [] 185 | try { 186 | data[2] = await this.onCall(data[2], data[3], transferList, worker) 187 | data[0] = 2 188 | } catch (e) { 189 | data[2] = e instanceof Error ? { name: e.name, message: e.message, stack: e.stack, code: (e as any).code, filename: (e as any).filename } : String(e) 190 | data[0] = -3 191 | } 192 | data.length = 3 193 | try { 194 | worker.postMessage(data, transferList) 195 | } catch (e) { 196 | data[2] = { name: e.name, message: `Cannot send data to child thread: ${e.message}`, stack: e.stack } 197 | data[0] = -3 198 | worker.postMessage(data) 199 | } 200 | return 201 | } 202 | worker.off("message", handleMessage) 203 | worker.off("error", handleError) 204 | worker.off("exit", handleExit) 205 | worker.running = false 206 | if (code) { 207 | reject(data[1]) 208 | } else { 209 | resolve(data[1]) 210 | } 211 | // 当前线程已空闲,尝试执行下一个任务 212 | const currentTask = this._firstTask 213 | if (currentTask) { 214 | const nextTask = this._firstTask = currentTask.next 215 | if (!nextTask) { 216 | this._lastTask = this._firstTask = undefined 217 | } 218 | this._execWorker(code === 0 ? worker : this.createNativeWorker(), currentTask.data, currentTask.transferList).then(currentTask.resolve, currentTask.reject) 219 | } 220 | } 221 | const handleError = (error: Error) => { 222 | handleMessage([-11, error]) 223 | } 224 | const handleExit = (code: number) => { 225 | handleMessage([-10, new Error(`The worker exited with code '${code}'`)]) 226 | } 227 | worker.on("message", handleMessage) 228 | worker.on("error", handleError) 229 | worker.on("exit", handleExit) 230 | try { 231 | worker.postMessage([0, data], transferList) 232 | } catch (e) { 233 | reject({ 234 | name: e.name, 235 | message: `Cannot send data to child thread: ${e.message}`, 236 | stack: e.stack 237 | }) 238 | } 239 | }) 240 | } 241 | 242 | /** 获取供子线程调用的全局函数 */ 243 | readonly functions?: { [name: string]: (data: any, transferList: (ArrayBuffer | MessagePort)[], worker?: Worker) => any } 244 | 245 | /** 246 | * 当收到子线程的远程调用后执行 247 | * @param name 要调用的函数名 248 | * @param data 要调用的函数参数 249 | * @param transferList 设置要移动的内存对象,移动后当前线程将无法使用该对象,如果未设置,则对象将被复制到其它线程 250 | * @param worker 来源的子线程,如果是主线程则为 `undefined` 251 | */ 252 | protected onCall(name: string, data: any, transferList: (ArrayBuffer | MessagePort)[], worker?: Worker) { 253 | return this.functions![name](data, transferList, worker) 254 | } 255 | 256 | /** 关闭所有线程 */ 257 | async close() { 258 | const promises: Promise[] = [] 259 | for (const worker of this.workers) { 260 | worker.removeAllListeners() 261 | promises.push(worker.terminate()) 262 | } 263 | this.workers.length = 0 264 | this._lastTask = this._firstTask = undefined 265 | return await Promise.all(promises) 266 | } 267 | 268 | } 269 | 270 | /** 表示一个线程池的选项 */ 271 | export interface WorkerPoolOptions extends Omit { 272 | /** 273 | * 最大允许同时执行的线程数, 如果为 0 表示不启用多线程 274 | * @default Math.ceil(require("os").cpus().length / 2) 275 | */ 276 | size?: number 277 | /** 供子线程调用的全局函数,在子线程中可以使用 `await this.call("name", ...)` 调用 */ 278 | functions?: { [name: string]: (data: any, transferList: (ArrayBuffer | MessagePort)[], worker?: Worker) => any } 279 | } 280 | 281 | /** 表示一个子线程 */ 282 | export interface Worker extends NativeWorker { 283 | /** 判断当前线程是否正在运行 */ 284 | running?: boolean 285 | } 286 | 287 | /** 表示执行线程的上下文 */ 288 | export interface WorkerContext { 289 | [key: string]: any 290 | /** 判断当前线程是否是主线程 */ 291 | isMainThread: boolean 292 | /** 由主线程发送的全局数据 */ 293 | workerData: any 294 | /** 295 | * 调用主线程定义的函数 296 | * @param name 要调用的函数名 297 | * @param data 同时发送的数据,数据的类型必须可复制 298 | * @param transferList 要移动的内存对象,移动后当前线程将无法使用该对象,如果未设置,则对象将被复制到其它线程 299 | */ 300 | call(name: string, data?: any, transferList?: (ArrayBuffer | MessagePort)[]): Promise 301 | /** 获取或设置本次要移动的内存对象,移动后当前线程将无法使用该对象,如果未设置,则对象将被复制到其它线程 */ 302 | transferList?: (ArrayBuffer | MessagePort)[] 303 | } 304 | 305 | /** 306 | * 判断指定的对象是否可在线程之间传递 307 | * @param obj 要判断的对象 308 | * @see https://nodejs.org/api/worker_threads.html 309 | */ 310 | export function isStructuredCloneable(obj: any, _processed = new Set()) { 311 | switch (typeof obj) { 312 | case "object": 313 | if (obj === null || _processed.has(obj)) { 314 | return true 315 | } 316 | const prototype = Object.getPrototypeOf(obj) 317 | switch (prototype) { 318 | case Object.prototype: 319 | case Array.prototype: 320 | _processed.add(obj) 321 | for (const key in obj) { 322 | if (!isStructuredCloneable(obj[key], _processed)) { 323 | return false 324 | } 325 | } 326 | return true 327 | case Set.prototype: 328 | _processed.add(obj) 329 | for (const item of (obj as Set).values()) { 330 | if (!isStructuredCloneable(item, _processed)) { 331 | return false 332 | } 333 | } 334 | return true 335 | case Map.prototype: 336 | _processed.add(obj) 337 | for (const [key, value] of (obj as Map).entries()) { 338 | if (!isStructuredCloneable(value, _processed) || !isStructuredCloneable(key, _processed)) { 339 | return false 340 | } 341 | } 342 | return true 343 | case Date.prototype: 344 | case RegExp.prototype: 345 | case Boolean.prototype: 346 | case Number.prototype: 347 | case ArrayBuffer.prototype: 348 | case SharedArrayBuffer.prototype: 349 | case Buffer.prototype: 350 | case Int8Array.prototype: 351 | case Uint8Array.prototype: 352 | case Uint8ClampedArray.prototype: 353 | case Int16Array.prototype: 354 | case Uint16Array.prototype: 355 | case Int32Array.prototype: 356 | case Uint32Array.prototype: 357 | case Float32Array.prototype: 358 | case Float64Array.prototype: 359 | case BigInt64Array.prototype: 360 | case BigUint64Array.prototype: 361 | case String.prototype: 362 | return true 363 | default: 364 | if (prototype.constructor === "MessagePort" && supportWorker()) { 365 | const { MessagePort } = require("worker_threads") as typeof import("worker_threads") 366 | if (prototype === MessagePort.prototype) { 367 | return true 368 | } 369 | } 370 | return false 371 | } 372 | case "function": 373 | case "symbol": 374 | return false 375 | default: 376 | return true 377 | } 378 | } 379 | 380 | /** 判断是否原生支持 Worker */ 381 | function supportWorker() { 382 | return require("module").builtinModules.includes("worker_threads") 383 | } -------------------------------------------------------------------------------- /test/asyncQueue.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as asyncQueue from "../src/asyncQueue" 3 | 4 | export namespace asyncQueueTest { 5 | 6 | export async function asyncQueueTest() { 7 | const q = new asyncQueue.AsyncQueue() 8 | let value = 1 9 | assert.strictEqual(await q.then(async () => { 10 | await sleep(2) 11 | return ++value 12 | }), 2) 13 | 14 | assert.deepStrictEqual(await Promise.all([q.then(async () => { 15 | assert.strictEqual(q.isEmpty, false) 16 | await sleep(2) 17 | return ++value 18 | }), q.then(async () => { 19 | await sleep(1) 20 | return ++value 21 | })]), [3, 4]) 22 | assert.strictEqual(q.isEmpty, true) 23 | 24 | try { 25 | await q.then(async () => { 26 | throw "error" 27 | }) 28 | } catch (e) { 29 | assert.strictEqual(e.toString(), "error") 30 | } 31 | 32 | function sleep(ms: number) { 33 | return new Promise(r => setTimeout(r, ms)) 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /test/base64.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as base64 from "../src/base64" 3 | 4 | export namespace base64Test { 5 | 6 | export function encodeBase64Test() { 7 | assert.strictEqual(base64.encodeBase64("foo"), "Zm9v") 8 | assert.strictEqual(base64.encodeBase64(Buffer.from("foo")), "Zm9v") 9 | 10 | assert.strictEqual(base64.encodeBase64("A"), "QQ==") 11 | assert.strictEqual(base64.encodeBase64(""), "") 12 | assert.strictEqual(base64.encodeBase64(Buffer.from("")), "") 13 | } 14 | 15 | export function decodeBase64Test() { 16 | assert.strictEqual(base64.decodeBase64("Zm9v"), "foo") 17 | 18 | assert.strictEqual(base64.decodeBase64("QQ=="), "A") 19 | assert.strictEqual(base64.decodeBase64(""), "") 20 | assert.strictEqual(base64.decodeBase64("A"), "", "Should ignore error") 21 | } 22 | 23 | export function encodeDataURITest() { 24 | assert.strictEqual(base64.encodeDataURI("text/plain", "foo"), "data:text/plain,foo") 25 | assert.strictEqual(base64.encodeDataURI("text/plain", Buffer.from("foo")), "data:text/plain;base64,Zm9v") 26 | 27 | assert.strictEqual(base64.encodeDataURI("text/plain", "

Hello, World!

"), "data:text/plain,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E") 28 | } 29 | 30 | export function decodeDataURITest() { 31 | assert.deepStrictEqual(base64.decodeDataURI("data:text/plain,foo"), { mimeType: "text/plain", data: "foo" }) 32 | assert.deepStrictEqual(base64.decodeDataURI("data:text/plain;base64,Zm9v"), { mimeType: "text/plain", data: Buffer.from("foo") }) 33 | 34 | assert.deepStrictEqual(base64.decodeDataURI("data:text/plain;base64,QQ=="), { mimeType: "text/plain", data: Buffer.from("A") }) 35 | assert.deepStrictEqual(base64.decodeDataURI("data:text/plain;base64,"), { mimeType: "text/plain", data: Buffer.from("") }) 36 | assert.deepStrictEqual(base64.decodeDataURI("data:text/plain,"), { mimeType: "text/plain", data: "" }) 37 | 38 | assert.strictEqual(base64.decodeDataURI("data:text/javascript;base64"), null) 39 | assert.strictEqual(base64.decodeDataURI("data:text/javascript"), null) 40 | assert.strictEqual(base64.decodeDataURI("data:"), null) 41 | assert.strictEqual(base64.decodeDataURI(""), null) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /test/commandLine.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as commandLine from "../src/commandLine" 3 | 4 | export namespace commandLineTest { 5 | 6 | export function showCursorTest() { 7 | commandLine.showCursor() 8 | commandLine.hideCursor() 9 | commandLine.showCursor() 10 | } 11 | 12 | export function parseCommandLineArgumentsTest() { 13 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({}, undefined, ["--x", "foo", "--y"], 0), { __proto__: null, "--x": "foo", "--y": true }) 14 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({}, undefined, ["foo"], 0), { __proto__: null, "0": "foo" }) 15 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--full": { alias: "-f" } }, undefined, ["-f"], 0), { __proto__: null, "--full": true }) 16 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--full": { alias: "-f" } }, undefined, ["-f2"], 0), { __proto__: null, "-f2": true }) 17 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--full": { alias: ["-f"] } }, undefined, ["-f"], 0), { __proto__: null, "--full": true }) 18 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "-x": {}, "--full": { alias: ["-f", "-g"] } }, undefined, ["-f"], 0), { __proto__: null, "--full": true }) 19 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "-x": {}, "--full": { alias: ["-f", "-g"] } }, undefined, ["-g"], 0), { __proto__: null, "--full": true }) 20 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({}, undefined, ["--", "-x"], 0), { __proto__: null, "--": { __proto__: null, "-x": true } }) 21 | 22 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({}, undefined, ["--x=foo"], 0), { __proto__: null, "--x": "foo" }) 23 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({}, undefined, ["--x:foo"], 0), { __proto__: null, "--x": "foo" }) 24 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({}, undefined, ["--x", "f", "--x=g", "--x:h"], 0), { __proto__: null, "--x": ["f", "g", "h"] }) 25 | 26 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo", default: "foo" } }, undefined, ["--x"], 0), { __proto__: null, "--x": "foo" }) 27 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo" } }, undefined, ["--x"], 0), { __proto__: null }) 28 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo", multiple: true } }, undefined, ["--x", "x"], 0), { __proto__: null, "--x": ["x"] }) 29 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo", multiple: true, default: "default" } }, undefined, ["--x", "--x", "x"], 0), { __proto__: null, "--x": ["default", "x"] }) 30 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo", multiple: true, default: "default" } }, undefined, ["--x"], 0), { __proto__: null, "--x": ["default"] }) 31 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo", default: "default" } }, () => { }, ["--x", "--x", "x"], 0), { __proto__: null, "--x": "x" }) 32 | 33 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo", default: "default" } }, () => { }, ["--x", "--x", "x"], 0), { __proto__: null, "--x": "x" }) 34 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": {} }, () => { }, ["--x", "--x", "x"], 0), { __proto__: null, "0": "x", "--x": true }) 35 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": {} }, () => { }, ["--x:x"], 0), { __proto__: null, "--x": true }) 36 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo" } }, () => { }, ["--x:y"], 0), { __proto__: null, "--x": "y" }) 37 | assert.deepStrictEqual(commandLine.parseCommandLineArguments({ "--x": { argument: "foo" } }, () => { }, ["--x"], 0), { __proto__: null }) 38 | } 39 | 40 | export function formatCommandLineOptionsTest() { 41 | assert.strictEqual(commandLine.formatCommandLineOptions({ 42 | "--help": { 43 | description: "help" 44 | } 45 | }, Infinity), ` --help help`) 46 | assert.strictEqual(commandLine.formatCommandLineOptions({ 47 | "--help": { 48 | description: "help", 49 | alias: "-h" 50 | } 51 | }, Infinity), ` -h, --help help`) 52 | assert.strictEqual(commandLine.formatCommandLineOptions({ 53 | "--help": { 54 | description: "help", 55 | alias: ["-h", "-?"] 56 | } 57 | }, Infinity), ` -h, -?, --help help`) 58 | assert.strictEqual(commandLine.formatCommandLineOptions({ 59 | "--help": { 60 | description: "help", 61 | argument: "help" 62 | } 63 | }, Infinity), ` --help help`) 64 | 65 | assert.strictEqual(commandLine.formatCommandLineOptions({ 66 | "--help": { 67 | group: "HELP", 68 | description: "help", 69 | argument: "help" 70 | }, 71 | "--help2": { 72 | group: "HELP2", 73 | description: "help", 74 | argument: "help", 75 | default: "" 76 | }, 77 | "--help3": { 78 | }, 79 | "--help4": { 80 | description: "help" 81 | } 82 | }, Infinity), [ 83 | ``, 84 | `HELP:`, 85 | ` --help help`, 86 | ``, 87 | `HELP2:`, 88 | ` --help2 [help] help`, 89 | ` --help4 help` 90 | ].join("\n")) 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /test/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as crypto from "../src/crypto" 3 | 4 | export namespace cryptoTest { 5 | 6 | export function md5Test() { 7 | assert.strictEqual(crypto.md5("foo"), "acbd18db4cc2f85cedef654fccc4a4d8") 8 | 9 | assert.strictEqual(crypto.md5("A"), "7fc56270e7a70fa81a5935b72eacbe29") 10 | assert.strictEqual(crypto.md5(""), "d41d8cd98f00b204e9800998ecf8427e") 11 | } 12 | 13 | export function sha1Test() { 14 | assert.strictEqual(crypto.sha1("foo"), "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33") 15 | 16 | assert.strictEqual(crypto.sha1("A"), "6dcd4ce23d88e2ee9568ba546c007c63d9131c1b") 17 | assert.strictEqual(crypto.sha1(""), "da39a3ee5e6b4b0d3255bfef95601890afd80709") 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /test/css.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as css from "../src/css" 3 | 4 | export namespace cssTest { 5 | 6 | export function encodeCSSTest() { 7 | assert.strictEqual(css.encodeCSS("a.b"), "a\\.b") 8 | 9 | // https://github.com/mathiasbynens/CSS.escape/blob/master/tests/tests.js 10 | assert.strictEqual(css.encodeCSS("abc"), "abc") 11 | 12 | assert.strictEqual(css.encodeCSS("\uFFFD"), "\uFFFD") 13 | assert.strictEqual(css.encodeCSS("a\uFFFD"), "a\uFFFD") 14 | assert.strictEqual(css.encodeCSS("\uFFFDb"), "\uFFFDb") 15 | assert.strictEqual(css.encodeCSS("a\uFFFDb"), "a\uFFFDb") 16 | 17 | assert.strictEqual(css.encodeCSS("\x01\x02\x1E\x1F"), "\\1 \\2 \\1e \\1f ") 18 | 19 | assert.strictEqual(css.encodeCSS("0a"), "\\30 a") 20 | assert.strictEqual(css.encodeCSS("1a"), "\\31 a") 21 | assert.strictEqual(css.encodeCSS("2a"), "\\32 a") 22 | assert.strictEqual(css.encodeCSS("3a"), "\\33 a") 23 | assert.strictEqual(css.encodeCSS("4a"), "\\34 a") 24 | assert.strictEqual(css.encodeCSS("5a"), "\\35 a") 25 | assert.strictEqual(css.encodeCSS("6a"), "\\36 a") 26 | assert.strictEqual(css.encodeCSS("7a"), "\\37 a") 27 | assert.strictEqual(css.encodeCSS("8a"), "\\38 a") 28 | assert.strictEqual(css.encodeCSS("9a"), "\\39 a") 29 | 30 | assert.strictEqual(css.encodeCSS("a0b"), "a0b") 31 | assert.strictEqual(css.encodeCSS("a1b"), "a1b") 32 | assert.strictEqual(css.encodeCSS("a2b"), "a2b") 33 | assert.strictEqual(css.encodeCSS("a3b"), "a3b") 34 | assert.strictEqual(css.encodeCSS("a4b"), "a4b") 35 | assert.strictEqual(css.encodeCSS("a5b"), "a5b") 36 | assert.strictEqual(css.encodeCSS("a6b"), "a6b") 37 | assert.strictEqual(css.encodeCSS("a7b"), "a7b") 38 | assert.strictEqual(css.encodeCSS("a8b"), "a8b") 39 | assert.strictEqual(css.encodeCSS("a9b"), "a9b") 40 | 41 | assert.strictEqual(css.encodeCSS("-0a"), "-\\30 a") 42 | assert.strictEqual(css.encodeCSS("-1a"), "-\\31 a") 43 | assert.strictEqual(css.encodeCSS("-2a"), "-\\32 a") 44 | assert.strictEqual(css.encodeCSS("-3a"), "-\\33 a") 45 | assert.strictEqual(css.encodeCSS("-4a"), "-\\34 a") 46 | assert.strictEqual(css.encodeCSS("-5a"), "-\\35 a") 47 | assert.strictEqual(css.encodeCSS("-6a"), "-\\36 a") 48 | assert.strictEqual(css.encodeCSS("-7a"), "-\\37 a") 49 | assert.strictEqual(css.encodeCSS("-8a"), "-\\38 a") 50 | assert.strictEqual(css.encodeCSS("-9a"), "-\\39 a") 51 | 52 | assert.strictEqual(css.encodeCSS("-"), "\\-") 53 | assert.strictEqual(css.encodeCSS("-a"), "-a") 54 | assert.strictEqual(css.encodeCSS("--"), "--") 55 | assert.strictEqual(css.encodeCSS("--a"), "--a") 56 | 57 | assert.strictEqual(css.encodeCSS("\x80\x2D\x5F\xA9"), "\x80\x2D\x5F\xA9") 58 | assert.strictEqual(css.encodeCSS("\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F"), "\\7f \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F") 59 | assert.strictEqual(css.encodeCSS("\xA0\xA1\xA2"), "\xA0\xA1\xA2") 60 | assert.strictEqual(css.encodeCSS("a0123456789b"), "a0123456789b") 61 | assert.strictEqual(css.encodeCSS("abcdefghijklmnopqrstuvwxyz"), "abcdefghijklmnopqrstuvwxyz") 62 | assert.strictEqual(css.encodeCSS("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") 63 | 64 | assert.strictEqual(css.encodeCSS("\x20\x21\x78\x79"), "\\ \\!xy") 65 | 66 | // astral symbol (U+1D306 TETRAGRAM FOR CENTRE) 67 | assert.strictEqual(css.encodeCSS("\uD834\uDF06"), "\uD834\uDF06") 68 | // lone surrogates 69 | assert.strictEqual(css.encodeCSS("\uDF06"), "\uDF06") 70 | assert.strictEqual(css.encodeCSS("\uD834"), "\uD834") 71 | } 72 | 73 | export function decodeCSSTest() { 74 | assert.strictEqual(css.decodeCSS("a\\.b"), "a.b") 75 | 76 | assert.strictEqual(css.decodeCSS("\\0"), "\uFFFD") 77 | assert.strictEqual(css.decodeCSS("\\00"), "\uFFFD") 78 | assert.strictEqual(css.decodeCSS("\\0 "), "\uFFFD") 79 | assert.strictEqual(css.decodeCSS("\\00 "), "\uFFFD") 80 | 81 | assert.strictEqual(css.decodeCSS("\uFFFD"), "\uFFFD") 82 | assert.strictEqual(css.decodeCSS("a\uFFFD"), "a\uFFFD") 83 | assert.strictEqual(css.decodeCSS("\uFFFDb"), "\uFFFDb") 84 | assert.strictEqual(css.decodeCSS("a\uFFFDb"), "a\uFFFDb") 85 | 86 | assert.strictEqual(css.decodeCSS("\\1 \\2 \\1e \\1f "), "\x01\x02\x1E\x1F") 87 | 88 | assert.strictEqual(css.decodeCSS("\\30 a"), "0a") 89 | assert.strictEqual(css.decodeCSS("\\31 a"), "1a") 90 | assert.strictEqual(css.decodeCSS("\\32 a"), "2a") 91 | assert.strictEqual(css.decodeCSS("\\33 a"), "3a") 92 | assert.strictEqual(css.decodeCSS("\\34 a"), "4a") 93 | assert.strictEqual(css.decodeCSS("\\35 a"), "5a") 94 | assert.strictEqual(css.decodeCSS("\\36 a"), "6a") 95 | assert.strictEqual(css.decodeCSS("\\37 a"), "7a") 96 | assert.strictEqual(css.decodeCSS("\\38 a"), "8a") 97 | assert.strictEqual(css.decodeCSS("\\39 a"), "9a") 98 | 99 | assert.strictEqual(css.decodeCSS("a0b"), "a0b") 100 | assert.strictEqual(css.decodeCSS("a1b"), "a1b") 101 | assert.strictEqual(css.decodeCSS("a2b"), "a2b") 102 | assert.strictEqual(css.decodeCSS("a3b"), "a3b") 103 | assert.strictEqual(css.decodeCSS("a4b"), "a4b") 104 | assert.strictEqual(css.decodeCSS("a5b"), "a5b") 105 | assert.strictEqual(css.decodeCSS("a6b"), "a6b") 106 | assert.strictEqual(css.decodeCSS("a7b"), "a7b") 107 | assert.strictEqual(css.decodeCSS("a8b"), "a8b") 108 | assert.strictEqual(css.decodeCSS("a9b"), "a9b") 109 | 110 | assert.strictEqual(css.decodeCSS("-\\30 a"), "-0a") 111 | assert.strictEqual(css.decodeCSS("-\\31 a"), "-1a") 112 | assert.strictEqual(css.decodeCSS("-\\32 a"), "-2a") 113 | assert.strictEqual(css.decodeCSS("-\\33 a"), "-3a") 114 | assert.strictEqual(css.decodeCSS("-\\34 a"), "-4a") 115 | assert.strictEqual(css.decodeCSS("-\\35 a"), "-5a") 116 | assert.strictEqual(css.decodeCSS("-\\36 a"), "-6a") 117 | assert.strictEqual(css.decodeCSS("-\\37 a"), "-7a") 118 | assert.strictEqual(css.decodeCSS("-\\38 a"), "-8a") 119 | assert.strictEqual(css.decodeCSS("-\\39 a"), "-9a") 120 | 121 | assert.strictEqual(css.decodeCSS("\\-"), "-") 122 | assert.strictEqual(css.decodeCSS("-a"), "-a") 123 | assert.strictEqual(css.decodeCSS("--"), "--") 124 | assert.strictEqual(css.decodeCSS("--a"), "--a") 125 | 126 | assert.strictEqual(css.decodeCSS("\x80\x2D\x5F\xA9"), "\x80\x2D\x5F\xA9") 127 | assert.strictEqual(css.decodeCSS("\\7f \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F"), "\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F") 128 | assert.strictEqual(css.decodeCSS("\xA0\xA1\xA2"), "\xA0\xA1\xA2") 129 | assert.strictEqual(css.decodeCSS("a0123456789b"), "a0123456789b") 130 | assert.strictEqual(css.decodeCSS("abcdefghijklmnopqrstuvwxyz"), "abcdefghijklmnopqrstuvwxyz") 131 | assert.strictEqual(css.decodeCSS("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), "ABCDEFGHIJKLMNOPQRSTUVWXYZ") 132 | 133 | assert.strictEqual(css.decodeCSS("\\ \\!xy"), "\x20\x21\x78\x79") 134 | 135 | // astral symbol (U+1D306 TETRAGRAM FOR CENTRE) 136 | assert.strictEqual(css.decodeCSS("\uD834\uDF06"), "\uD834\uDF06") 137 | // lone surrogates 138 | assert.strictEqual(css.decodeCSS("\uDF06"), "\uDF06") 139 | assert.strictEqual(css.decodeCSS("\uD834"), "\uD834") 140 | } 141 | 142 | export function quoteCSSStringTest() { 143 | assert.strictEqual(css.quoteCSSString("abc"), `abc`) 144 | assert.strictEqual(css.quoteCSSString("abc(0)"), `"abc(0)"`) 145 | assert.strictEqual(css.quoteCSSString("abc", '"'), `"abc"`) 146 | 147 | assert.strictEqual(css.quoteCSSString("abc'"), `"abc'"`) 148 | assert.strictEqual(css.quoteCSSString("abc'", "'"), `'abc\\''`) 149 | assert.strictEqual(css.quoteCSSString("abc\""), `"abc\\""`) 150 | assert.strictEqual(css.quoteCSSString("abc", '"'), `"abc"`) 151 | assert.strictEqual(css.quoteCSSString("abc", "'"), `'abc'`) 152 | 153 | assert.strictEqual(css.quoteCSSString("\uFFFD"), "\uFFFD") 154 | assert.strictEqual(css.quoteCSSString("a\uFFFD"), "a\uFFFD") 155 | assert.strictEqual(css.quoteCSSString("\uFFFDb"), "\uFFFDb") 156 | assert.strictEqual(css.quoteCSSString("a\uFFFDb"), "a\uFFFDb") 157 | 158 | assert.strictEqual(css.quoteCSSString("\x01\x02\x1E\x1F"), "\\1 \\2 \\1e \\1f ") 159 | assert.strictEqual(css.quoteCSSString("\\"), "\\\\") 160 | } 161 | 162 | export function unquoteCSSStringTest() { 163 | assert.strictEqual(css.unquoteCSSString(`abc`), "abc") 164 | assert.strictEqual(css.unquoteCSSString(`"abc"`), "abc") 165 | assert.strictEqual(css.unquoteCSSString(`'abc'`), "abc") 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /test/deferred.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as deferred from "../src/deferred" 3 | 4 | export namespace deferredTest { 5 | 6 | export async function deferredTest() { 7 | const q = new deferred.Deferred() 8 | await q 9 | 10 | let value = 1 11 | q.reject() 12 | q.reject() 13 | q.resolve() 14 | setTimeout(() => { 15 | q.resolve() 16 | assert.strictEqual(++value, 3) 17 | }, 1) 18 | assert.strictEqual(++value, 2) 19 | 20 | await q 21 | assert.strictEqual(++value, 4) 22 | } 23 | 24 | export async function errorTest() { 25 | const q = new deferred.Deferred() 26 | let value = 1 27 | q.reject() 28 | q.then(() => { 29 | throw "error" 30 | }) 31 | q.then(() => { 32 | value = 3 33 | }) 34 | setTimeout(() => { 35 | value = 2 36 | q.resolve() 37 | }, 10) 38 | await assert.rejects(async () => { 39 | await q 40 | value = 4 41 | }) 42 | q.reject() 43 | q.resolve() 44 | assert.strictEqual(value, 2) 45 | } 46 | 47 | export async function errorTest2() { 48 | const q = new deferred.Deferred() 49 | let value = 1 50 | q.reject() 51 | q.then(() => { 52 | throw "error" 53 | }) 54 | await assert.rejects(async () => { 55 | value = 2 56 | q.resolve() 57 | await q 58 | value = 4 59 | }) 60 | q.then(() => { 61 | value = 3 62 | }) 63 | q.reject() 64 | q.resolve() 65 | assert.strictEqual(value, 2) 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /test/eventEmitter.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as eventEmitter from "../src/eventEmitter" 3 | 4 | export namespace eventEmitterTest { 5 | 6 | export async function onTest() { 7 | const events = new eventEmitter.EventEmitter() 8 | const func = (arg1: any, arg2: any) => { 9 | assert.strictEqual(arg1, "arg1") 10 | assert.strictEqual(arg2, "arg2") 11 | } 12 | events.on("foo", func) 13 | await events.emit("foo", "arg1", "arg2") 14 | events.off("foo", func) 15 | await events.emit("foo", "arg1-error", "arg2-error") 16 | 17 | events.on("foo", func) 18 | events.on("foo", func) 19 | events.on("foo", func) 20 | await events.emit("foo", "arg1", "arg2") 21 | events.off("foo", func) 22 | await events.emit("foo", "arg1", "arg2") 23 | events.off("foo", func) 24 | events.off("foo", func) 25 | await events.emit("foo", "arg1-error", "arg2-error") 26 | 27 | events.on("foo", () => false) 28 | events.on("foo", () => { assert.ok(false, "Returning false will prevent rest event listeners") }) 29 | events.off("foo") 30 | await events.emit("foo") 31 | 32 | events.on("foo", () => false) 33 | events.on("foo", () => { assert.ok(false, "Returning false will prevent rest event listeners") }) 34 | await events.emit("foo") 35 | events.off() 36 | await events.emit("foo") 37 | 38 | events.once("once", (argument: any) => { 39 | assert.strictEqual(argument, "1") 40 | }) 41 | await events.emit("once", "1") 42 | await events.emit("once", "2") 43 | } 44 | 45 | export async function offTest() { 46 | const events = new eventEmitter.EventEmitter() 47 | const func = (arg1: any, arg2: any) => { 48 | assert.strictEqual(arg1, "arg1") 49 | assert.strictEqual(arg2, "arg2") 50 | } 51 | 52 | events.off() 53 | events.off("foo") 54 | events.off("foo", func) 55 | 56 | events.on("foo", func) 57 | events.off("goo", () => { }) 58 | events.off("goo") 59 | events.off("foo") 60 | 61 | events.on("foo", func) 62 | events.off() 63 | 64 | events.on("foo", func) 65 | events.off("foo", () => { }) 66 | 67 | events.on("foo", func) 68 | events.off("foo", () => { }) 69 | events.off("foo") 70 | 71 | let value = 0 72 | events.once("goo", (arg: any) => assert.strictEqual(++value, 1)) 73 | await events.emit("goo") 74 | await events.emit("goo") 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /test/fileSystemWatcher.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as fs from "fs" 3 | import { resolve as resolvePath } from "path" 4 | import * as fileSystemWatcher from "../src/fileSystemWatcher" 5 | import { init, rootDir, uninit } from "./helpers/fsHelper" 6 | 7 | export namespace fileSystemWatcherTest { 8 | 9 | export async function beforeEach() { 10 | await init({}) 11 | } 12 | 13 | export async function afterEach() { 14 | await uninit() 15 | } 16 | 17 | export async function watchDirAndCreateFile() { 18 | await new Promise(resolve => { 19 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 20 | watcher.on("create", path => { 21 | assert.strictEqual(path, resolvePath("foo/watchDirAndCreateFile.txt")) 22 | assert.strictEqual(fs.readFileSync("foo/watchDirAndCreateFile.txt", "utf-8"), "A") 23 | watcher.close(resolve) 24 | }) 25 | watcher.on("change", path => { assert.fail(path) }) 26 | watcher.on("delete", path => { assert.fail(path) }) 27 | watcher.add(rootDir, () => { 28 | fs.mkdirSync("foo/") 29 | fs.writeFileSync("foo/watchDirAndCreateFile.txt", "A") 30 | }) 31 | }) 32 | } 33 | 34 | export async function watchDirAndChangeFile() { 35 | await new Promise(resolve => { 36 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 37 | watcher.on("create", path => { 38 | assert.strictEqual(path, resolvePath("foo/watchDirAndChangeFile.txt")) 39 | assert.strictEqual(fs.readFileSync("foo/watchDirAndChangeFile.txt", "utf-8"), "A") 40 | fs.writeFileSync("foo/watchDirAndChangeFile.txt", "B") 41 | }) 42 | watcher.on("change", path => { 43 | assert.strictEqual(path, resolvePath("foo/watchDirAndChangeFile.txt")) 44 | assert.strictEqual(fs.readFileSync("foo/watchDirAndChangeFile.txt", "utf-8"), "B") 45 | watcher.close(resolve) 46 | }) 47 | watcher.on("delete", path => { assert.fail(path) }) 48 | watcher.add(rootDir, () => { 49 | fs.mkdirSync("foo/") 50 | fs.writeFileSync("foo/watchDirAndChangeFile.txt", "A") 51 | }) 52 | }) 53 | } 54 | 55 | export async function watchDirAndDeleteFile() { 56 | fs.mkdirSync("foo") 57 | fs.writeFileSync("foo/watchDirAndDeleteFile.txt", "A") 58 | await new Promise(resolve => { 59 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 60 | watcher.on("create", path => { assert.fail(path) }) 61 | watcher.on("change", path => { assert.fail(path) }) 62 | watcher.on("delete", path => { 63 | assert.strictEqual(path, resolvePath("foo/watchDirAndDeleteFile.txt")) 64 | assert.strictEqual(fs.existsSync("foo/watchDirAndDeleteFile.txt"), false) 65 | watcher.close(resolve) 66 | }) 67 | watcher.add(rootDir, () => { 68 | fs.unlinkSync("foo/watchDirAndDeleteFile.txt") 69 | }) 70 | }) 71 | } 72 | 73 | export async function watchDirAndDeleteFileAndCreateDir() { 74 | fs.mkdirSync("foo") 75 | fs.writeFileSync("foo/watchDirAndDeleteFileAndCreateDir.txt", "A") 76 | await new Promise(resolve => { 77 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 78 | let fileDeleted = false 79 | watcher.on("create", path => { assert.fail(path) }) 80 | watcher.on("change", path => { assert.fail(path) }) 81 | watcher.on("delete", path => { 82 | fileDeleted = true 83 | assert.strictEqual(path, resolvePath("foo/watchDirAndDeleteFileAndCreateDir.txt")) 84 | }) 85 | watcher.on("createDir", path => { 86 | assert.strictEqual(path, resolvePath("foo/watchDirAndDeleteFileAndCreateDir.txt")) 87 | assert.ok(fileDeleted) 88 | watcher.close(resolve) 89 | }) 90 | watcher.add(rootDir, () => { 91 | fs.unlinkSync("foo/watchDirAndDeleteFileAndCreateDir.txt") 92 | fs.mkdirSync("foo/watchDirAndDeleteFileAndCreateDir.txt") 93 | }) 94 | }) 95 | } 96 | 97 | export async function watchDirAndDeleteFileFast() { 98 | await new Promise(resolve => { 99 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 100 | watcher.on("create", path => { assert.fail(path) }) 101 | watcher.on("change", path => { assert.fail(path) }) 102 | watcher.on("delete", path => { assert.fail(path) }) 103 | watcher.add(rootDir, () => { 104 | fs.mkdirSync("foo/") 105 | fs.writeFileSync("foo/watchDirAndDeleteFileFast.txt", "A") 106 | fs.unlinkSync("foo/watchDirAndDeleteFileFast.txt") 107 | watcher.close(resolve) 108 | }) 109 | }) 110 | } 111 | 112 | export async function watchDirAndCreateDir() { 113 | fs.mkdirSync("foo") 114 | await new Promise(resolve => { 115 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 116 | watcher.on("create", path => { assert.fail(path) }) 117 | watcher.on("change", path => { assert.fail(path) }) 118 | watcher.on("delete", path => { assert.fail(path) }) 119 | watcher.on("createDir", path => { 120 | assert.strictEqual(path, resolvePath("foo/watchDirAndCreateDir")) 121 | watcher.close(resolve) 122 | }) 123 | watcher.on("deleteDir", path => { assert.fail(path) }) 124 | watcher.add(rootDir, () => { 125 | fs.mkdirSync("foo/watchDirAndCreateDir") 126 | }) 127 | }) 128 | } 129 | 130 | export async function watchDirAndDeleteDir() { 131 | await new Promise(resolve => { 132 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 133 | watcher.on("create", path => { assert.fail(path) }) 134 | watcher.on("change", path => { assert.fail(path) }) 135 | watcher.on("delete", path => { assert.fail(path) }) 136 | watcher.on("createDir", path => { 137 | assert.strictEqual(path, resolvePath("foo")) 138 | fs.rmdirSync("foo") 139 | }) 140 | watcher.on("deleteDir", path => { 141 | assert.strictEqual(path, resolvePath("foo")) 142 | watcher.close(resolve) 143 | }) 144 | watcher.add(rootDir, () => { 145 | fs.mkdirSync("foo") 146 | }) 147 | }) 148 | } 149 | 150 | export async function watchDirAndDeleteDirFast() { 151 | await new Promise(resolve => { 152 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 153 | watcher.on("createDir", path => { assert.fail(path) }) 154 | watcher.on("deleteDir", path => { assert.fail(path) }) 155 | watcher.add(rootDir, () => { 156 | fs.mkdirSync("foo") 157 | fs.rmdirSync("foo") 158 | watcher.close(resolve) 159 | }) 160 | }) 161 | } 162 | 163 | export async function watchFileAndChangeFile() { 164 | await new Promise(resolve => { 165 | let step = 0 166 | fs.mkdirSync("foo/") 167 | fs.writeFileSync("foo/watchFileAndChangeFile.txt", "O") 168 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10, compareModifyTime: false }) 169 | watcher.on("create", path => { assert.fail(path) }) 170 | watcher.on("change", (path: string) => { 171 | switch (step++) { 172 | case 0: 173 | assert.strictEqual(path, resolvePath("foo/watchFileAndChangeFile.txt")) 174 | assert.strictEqual(fs.readFileSync("foo/watchFileAndChangeFile.txt", "utf-8"), "A") 175 | fs.writeFileSync("foo/watchFileAndChangeFile.txt", "B") 176 | break 177 | case 1: 178 | assert.strictEqual(path, resolvePath("foo/watchFileAndChangeFile.txt")) 179 | assert.strictEqual(fs.readFileSync("foo/watchFileAndChangeFile.txt", "utf-8"), "B") 180 | watcher.close(resolve) 181 | break 182 | } 183 | }) 184 | watcher.on("delete", path => { assert.fail(path) }) 185 | watcher.add(resolvePath("foo/watchFileAndChangeFile.txt"), () => { 186 | fs.writeFileSync("foo/watchFileAndChangeFile.txt", "A") 187 | }) 188 | }) 189 | } 190 | 191 | export async function addTest() { 192 | await new Promise(resolve => { 193 | fs.mkdirSync("foo/") 194 | fs.mkdirSync("foo/sub1") 195 | const watcher = new fileSystemWatcher.FileSystemWatcher() 196 | watcher.add(resolvePath("foo"), error => { 197 | assert.ifError(error) 198 | assert.strictEqual(watcher.isWatching, true) 199 | assert.strictEqual(watcher.isWatchingPath(resolvePath("foo")), true) 200 | assert.strictEqual(watcher.isWatchingPath(resolvePath("foo/sub1")), true) 201 | assert.strictEqual(watcher.isWatchingPath(resolvePath("goo")), false) 202 | watcher.add(resolvePath("foo/sub1"), () => { 203 | watcher.add(rootDir, () => { 204 | watcher.close(() => { 205 | assert.strictEqual(watcher.isWatching, false) 206 | resolve() 207 | }) 208 | }) 209 | }) 210 | }) 211 | }) 212 | } 213 | 214 | export async function removeTest() { 215 | await new Promise(resolve => { 216 | const watcher = new fileSystemWatcher.FileSystemWatcher() 217 | watcher.add(rootDir, () => { 218 | assert.strictEqual(watcher.isWatching, true) 219 | watcher.remove(resolvePath("foo"), () => { 220 | watcher.remove(rootDir, () => { 221 | assert.strictEqual(watcher.isWatching, false) 222 | watcher.remove("404", () => { 223 | assert.strictEqual(watcher.isWatching, false) 224 | watcher.close(resolve) 225 | }) 226 | }) 227 | }) 228 | }) 229 | }) 230 | } 231 | 232 | export async function ignoredTest() { 233 | await new Promise(resolve => { 234 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10, persistent: false }) 235 | watcher.ignored = () => true 236 | watcher.on("create", path => { assert.fail(path) }) 237 | watcher.add(".", () => { 238 | fs.mkdirSync("foo/") 239 | fs.writeFileSync("foo/你好.txt", "A") 240 | watcher.close(resolve) 241 | }) 242 | }) 243 | } 244 | 245 | export async function pauseTest() { 246 | await new Promise(async resolve => { 247 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 248 | let paused = true 249 | watcher.on("create", path => { 250 | assert.strictEqual(path, resolvePath("foo/created.txt")) 251 | assert.strictEqual(paused, false) 252 | watcher.close(resolve) 253 | }) 254 | watcher.on("delete", path => { assert.fail(path) }) 255 | watcher.add(rootDir, () => { 256 | fs.mkdirSync("foo/") 257 | fs.writeFileSync("foo/created.txt", "A") 258 | }) 259 | watcher.pause() 260 | await new Promise(r => setTimeout(r, 60)) 261 | watcher.pause() 262 | paused = false 263 | watcher.resume() 264 | watcher.resume() 265 | watcher.pause() 266 | watcher.resume() 267 | }) 268 | } 269 | 270 | export async function readyTest() { 271 | await new Promise(async resolve => { 272 | const watcher = new fileSystemWatcher.FileSystemWatcher({ delay: 10 }) 273 | const paths: string[] = [] 274 | watcher.on("create", path => { 275 | paths.push(path) 276 | }) 277 | watcher.on("ready", () => { 278 | if (paths.length === 2) { 279 | paths.sort() 280 | assert.strictEqual(paths[0], resolvePath("foo/ready-1.txt")) 281 | assert.strictEqual(paths[1], resolvePath("foo/ready-2.txt")) 282 | watcher.close(resolve) 283 | } 284 | }) 285 | watcher.add(rootDir, () => { 286 | fs.mkdirSync("foo/") 287 | fs.writeFileSync("foo/ready-1.txt", "A") 288 | fs.writeFileSync("foo/ready-2.txt", "A") 289 | }) 290 | }) 291 | } 292 | 293 | export async function watchErrorTest() { 294 | await new Promise(resolve => { 295 | const watcher = new fileSystemWatcher.FileSystemWatcher() 296 | watcher.add("404", (error) => { 297 | assert.ok(error) 298 | assert.strictEqual(watcher.isWatchingPath("404"), false) 299 | watcher.close() 300 | watcher.close(resolve) 301 | }) 302 | }) 303 | } 304 | 305 | export namespace recursiveTest { 306 | if (new fileSystemWatcher.FileSystemWatcher().watchOptions.recursive) { 307 | for (const key in fileSystemWatcherTest) { 308 | if ((key.startsWith("watch") || key.endsWith("Test")) && key !== "recursiveTest" && key !== "pollingTest") { 309 | recursiveTest[key] = async function () { 310 | const FileSystemWatcher = fileSystemWatcher.FileSystemWatcher 311 | // @ts-ignore 312 | fileSystemWatcher.FileSystemWatcher = class extends FileSystemWatcher { 313 | constructor(options: fileSystemWatcher.FileSystemWatcherOptions) { 314 | super(options) 315 | this.watchOptions.recursive = false 316 | } 317 | } 318 | try { 319 | return await fileSystemWatcherTest[key].call(this, arguments) 320 | } finally { 321 | // @ts-ignore 322 | fileSystemWatcher.FileSystemWatcher = FileSystemWatcher 323 | } 324 | } 325 | } 326 | } 327 | } 328 | 329 | } 330 | 331 | export namespace pollingTest { 332 | for (const key in fileSystemWatcherTest) { 333 | if (key === "watchDirAndDeleteFile") { 334 | pollingTest[key] = async function () { 335 | this.slow(600) 336 | const FileSystemWatcher = fileSystemWatcher.FileSystemWatcher 337 | // @ts-ignore 338 | fileSystemWatcher.FileSystemWatcher = class extends FileSystemWatcher { 339 | constructor(options: fileSystemWatcher.FileSystemWatcherOptions) { 340 | super({ ...options, usePolling: true, interval: 100 }) 341 | } 342 | } 343 | try { 344 | return await fileSystemWatcherTest[key].call(this, arguments) 345 | } finally { 346 | // @ts-ignore 347 | fileSystemWatcher.FileSystemWatcher = FileSystemWatcher 348 | } 349 | } 350 | } 351 | } 352 | } 353 | 354 | } -------------------------------------------------------------------------------- /test/helpers/consoleHelper.ts: -------------------------------------------------------------------------------- 1 | import { format } from "util" 2 | 3 | /** 4 | * 执行一个函数并捕获执行期间标准流的所有输出 5 | * @param callback 要执行的函数 6 | * @param callback.stdout 获取已捕获的标准输出流的所有内容 7 | * @param callback.stderr 获取已捕获的标准错误流的所有内容 8 | * @returns 返回函数的返回值 9 | */ 10 | export async function captureStdio(callback: (stdout: (string | Buffer | Uint8Array)[], stderr: (string | Buffer | Uint8Array)[]) => T | Promise) { 11 | const stdouts: (string | Buffer | Uint8Array)[] = [] 12 | const stderrs: (string | Buffer | Uint8Array)[] = [] 13 | // Node.js 并未提供标准流输出事件,只能劫持对应函数 14 | const originalStdoutWrite = process.stdout.write 15 | const originalStdErrorWrite = process.stderr.write 16 | process.stdout.write = (buffer: string | Buffer | Uint8Array, encoding?: string | Function, callback?: Function) => { 17 | stdouts.push(buffer) 18 | if (typeof encoding === "function") { 19 | encoding() 20 | } 21 | if (typeof callback === "function") { 22 | callback() 23 | } 24 | return true 25 | } 26 | process.stderr.write = (buffer: string | Buffer | Uint8Array, encoding?: string | Function, callback?: Function) => { 27 | stderrs.push(buffer) 28 | if (typeof encoding === "function") { 29 | encoding() 30 | } 31 | if (typeof callback === "function") { 32 | callback() 33 | } 34 | return true 35 | } 36 | // console.log 等内部会调用 process.stdout.write,理论上可以被正常捕获 37 | // 但有些场景(比如使用 VSCode 调试代码)也会劫持 console.log 等,使得捕获失败,因此这里需要再次劫持 console 38 | // tslint:disable-next-line: no-console 39 | const originalConsoleLog = console.log 40 | const originalConsoleInfo = console.info 41 | const originalConsoleDebug = console.debug 42 | const originalConsoleWarn = console.warn 43 | const originalConsoleError = console.error 44 | // tslint:disable-next-line: no-console 45 | console.debug = console.info = console.log = (...args: any[]) => { 46 | stdouts.push(format(args)) 47 | } 48 | console.warn = console.error = (...args: any[]) => { 49 | stderrs.push(format(args)) 50 | } 51 | // 无论函数是否出现异常,都要确保捕获被还原 52 | try { 53 | return await callback(stdouts, stderrs) 54 | } finally { 55 | // 还原劫持的函数 56 | process.stdout.write = originalStdoutWrite 57 | process.stderr.write = originalStdErrorWrite 58 | // tslint:disable-next-line: no-console 59 | console.log = originalConsoleLog 60 | console.info = originalConsoleInfo 61 | console.debug = originalConsoleDebug 62 | console.warn = originalConsoleWarn 63 | console.error = originalConsoleError 64 | } 65 | } 66 | 67 | /** 68 | * 模拟非 TTY 的输出流并执行函数 69 | * @param callback 要执行的函数 70 | * @returns 返回函数的返回值 71 | */ 72 | export async function simulateNoneTTYStream(callback: () => any | Promise) { 73 | const originalStdOutIsITY = process.stdout.isTTY 74 | const originalStdOutColumns = process.stdout.columns 75 | const originalStdOutRows = process.stdout.rows 76 | const originalStdErrIsITY = process.stderr.isTTY 77 | const originalStdErrColumns = process.stderr.columns 78 | const originalStdErrRows = process.stderr.rows 79 | process.stderr.isTTY = process.stdout.isTTY = undefined 80 | process.stderr.rows = process.stderr.columns = process.stdout.columns = process.stdout.rows = undefined 81 | try { 82 | return await callback() 83 | } finally { 84 | process.stdout.isTTY = originalStdOutIsITY 85 | process.stdout.columns = originalStdOutColumns 86 | process.stdout.rows = originalStdOutRows 87 | process.stderr.isTTY = originalStdErrIsITY 88 | process.stdout.columns = originalStdErrColumns 89 | process.stdout.rows = originalStdErrRows 90 | } 91 | } -------------------------------------------------------------------------------- /test/helpers/fsHelper.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import { join, resolve } from "path" 3 | import fs = require("fs") 4 | 5 | /** 更改前的工作文件夹路径 */ 6 | var originalWorkingDir: string | undefined 7 | 8 | /** 用于存放测试文件的文件夹路径 */ 9 | export const rootDir = resolve("node_modules/__test__") 10 | 11 | /** 12 | * 初始化用于测试的文件 13 | * @param entries 要创建的文件或文件夹项 14 | */ 15 | export async function init(entries: FileEntries) { 16 | if (originalWorkingDir) await uninit() 17 | await retryIfError(() => { deleteEntry(rootDir) }) 18 | await retryIfError(() => { createEntries(entries, rootDir) }) 19 | await retryIfError(() => { originalWorkingDir = changeWorkingDir(rootDir) }) 20 | } 21 | 22 | /** 23 | * 表示一个文件或文件夹项,键为文件或文件夹名,值可能有三个类型: 24 | * - 字符串:表示文本文件的内容 25 | * - 缓存对象:表示二进制文件数据 26 | * - 文件或文件夹项:表示一个子文件夹 27 | */ 28 | export interface FileEntries { 29 | [name: string]: string | Buffer | FileEntries 30 | } 31 | 32 | /** 33 | * 创建指定的文件或文件夹项 34 | * @param entries 要创建的文件或文件夹项 35 | * @param dir 根文件夹路径 36 | */ 37 | function createEntries(entries: FileEntries, dir: string) { 38 | fs.mkdirSync(dir, { recursive: true }) 39 | for (const key in entries) { 40 | const entry = entries[key] 41 | const child = resolve(dir, key) 42 | if (typeof entry === "string" || Buffer.isBuffer(entry)) { 43 | fs.writeFileSync(child, entry) 44 | } else { 45 | createEntries(entry, child) 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * 删除指定的文件或文件夹 52 | * @param path 要删除的文件或文件夹路径 53 | */ 54 | function deleteEntry(path: string) { 55 | try { 56 | if (fs.lstatSync(path).isDirectory()) { 57 | for (const entry of fs.readdirSync(path)) { 58 | deleteEntry(join(path, entry)) 59 | } 60 | fs.rmdirSync(path) 61 | } else { 62 | fs.unlinkSync(path) 63 | } 64 | } catch (e) { 65 | if (e.code === "ENOENT") { 66 | return 67 | } 68 | throw e 69 | } 70 | } 71 | 72 | /** 73 | * 更改当前的工作文件夹 74 | * @param dir 新文件夹路径,如果文件夹不存在会被自动创建 75 | * @returns 返回更改前的工作文件夹路径 76 | */ 77 | function changeWorkingDir(dir: string) { 78 | const originalWorkingDir = process.cwd() 79 | fs.mkdirSync(dir, { recursive: true }) 80 | process.chdir(dir) 81 | return originalWorkingDir 82 | } 83 | 84 | /** 85 | * 执行指定的函数,如果出错则自动重试 86 | * @param callback 要执行的函数 87 | * @param times 允许自动重试的次数,超过次数限制后将抛出错误 88 | */ 89 | function retryIfError(callback: () => T, times = 3) { 90 | return new Promise((resolve, reject) => { 91 | try { 92 | resolve(callback()) 93 | } catch (e) { 94 | if (times > 0) { 95 | setTimeout(() => { 96 | retryIfError(callback, times - 1).then(resolve, reject) 97 | }, times > 2 ? 50 : 9) 98 | return 99 | } 100 | reject(e) 101 | } 102 | }) 103 | } 104 | 105 | /** 删除用于测试的文件 */ 106 | export async function uninit() { 107 | if (originalWorkingDir) { 108 | await retryIfError(() => { changeWorkingDir(originalWorkingDir) }) 109 | await retryIfError(() => { deleteEntry(rootDir) }) 110 | originalWorkingDir = undefined 111 | } 112 | } 113 | 114 | /** 115 | * 校验指定的文件项 116 | * @param entries 要校验的文件项 117 | * @param dir 根文件夹路径 118 | */ 119 | export function check(entries: FileEntries, dir = rootDir) { 120 | for (const key in entries) { 121 | const entry = entries[key] 122 | const child = join(dir, key) 123 | if (typeof entry === "string") { 124 | assert.strictEqual(fs.readFileSync(child, "utf-8"), entry) 125 | } else if (Buffer.isBuffer(entry)) { 126 | assert.deepStrictEqual(fs.readFileSync(child), entry) 127 | } else { 128 | assert.strictEqual(fs.statSync(child).isDirectory(), true) 129 | check(entry, child) 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * 执行指定的函数,并在执行期间模拟 IO 错误 136 | * @param callback 要执行的函数 137 | * @param errorCodes 模拟的错误代码,每次调用 IO 函数将返回对应的错误代码,调用次数超出数组长度后恢复正常调用 138 | * @param syscalls 要模拟错误的系统 IO 调用 139 | */ 140 | export async function simulateIOError(callback: () => T | Promise, errorCodes = ["UNKNOWN"], syscalls = ["access", "accessSync", "readFile", "readFileSync", "rename", "renameSync", "truncate", "truncateSync", "ftruncate", "ftruncateSync", "rmdir", "rmdirSync", "fdatasync", "fdatasyncSync", "fsync", "fsyncSync", "mkdir", "mkdirSync", "readdir", "readdirSync", "fstat", "lstat", "stat", "fstatSync", "lstatSync", "statSync", "readlink", "readlinkSync", "writeFile", "writeFileSync", "symlink", "symlinkSync", "link", "linkSync", "unlink", "unlinkSync", "fchmod", "fchmodSync", "chmod", "chmodSync", "fchown", "fchownSync", "chown", "chownSync", "utimes", "utimesSync", "futimes", "futimesSync", "realpathSync", "realpath", "mkdtemp", "mkdtempSync"]) { 141 | const originalFS = {} 142 | for (const syscall of syscalls) { 143 | const originalSyscall = originalFS[syscall] = fs[syscall] 144 | let index = 0 145 | fs[syscall] = (...args: any[]) => { 146 | if (index < errorCodes.length) { 147 | const error = new Error(`Simulated IO Error: ${errorCodes[index]}, ${syscall} '${args[0]}'`) as NodeJS.ErrnoException 148 | error.code = errorCodes[index++] 149 | error.syscall = syscall 150 | error.path = args[0] 151 | if (args.length && typeof args[args.length - 1] === "function") { 152 | return args[args.length - 1](error, null) 153 | } else { 154 | throw error 155 | } 156 | } 157 | fs[syscall] = originalSyscall 158 | delete originalFS[syscall] 159 | return originalSyscall(...args) 160 | } 161 | if (syscall === "realpath" || syscall === "realpathSync") { 162 | fs[syscall].native = fs[syscall] 163 | } 164 | } 165 | try { 166 | return await callback() 167 | } finally { 168 | for (const key in originalFS) { 169 | fs[key] = originalFS[key] 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /test/js.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as js from "../src/js" 3 | 4 | export namespace jsTest { 5 | 6 | export function encodeJSStringTest() { 7 | assert.strictEqual(js.encodeJS("abc"), `abc`) 8 | 9 | assert.strictEqual(js.encodeJS("\r\n\u2028\u2029"), `\\r\\n\\u2028\\u2029`) 10 | } 11 | 12 | export function decodeJSStringTest() { 13 | assert.strictEqual(js.decodeJS("abc"), `abc`) 14 | 15 | assert.strictEqual(js.decodeJS("\\r\\n\\u2028\\u2029"), `\r\n\u2028\u2029`) 16 | assert.strictEqual(js.decodeJS("\\\"\\\'\\\\\\v\\\r\\\n\\t\\b\\f\\0\\2"), `\"\'\\\v\t\b\f\u00002`) 17 | assert.strictEqual(js.decodeJS("\\u{1122}"), `\u1122`) 18 | } 19 | 20 | export function quoteJSStringTest() { 21 | assert.strictEqual(js.quoteJSString("abc"), `"abc"`) 22 | 23 | assert.strictEqual(js.quoteJSString("abc\'"), `"abc'"`) 24 | assert.strictEqual(js.quoteJSString("abc\""), `'abc"'`) 25 | assert.strictEqual(js.quoteJSString("abc\"\'"), `"abc\\"'"`) 26 | 27 | assert.strictEqual(js.quoteJSString("abc\'", "'"), `'abc\\''`) 28 | assert.strictEqual(js.quoteJSString("abc\"", "'"), `'abc"'`) 29 | assert.strictEqual(js.quoteJSString("abc\"\'", "'"), `'abc"\\''`) 30 | 31 | assert.strictEqual(js.quoteJSString("abc\'", '"'), `"abc'"`) 32 | assert.strictEqual(js.quoteJSString("abc\"", '"'), `"abc\\""`) 33 | assert.strictEqual(js.quoteJSString("abc\"\'", '"'), `"abc\\"'"`) 34 | 35 | assert.strictEqual(js.quoteJSString("abc\'", "\`"), `\`abc'\``) 36 | assert.strictEqual(js.quoteJSString("abc\"", "\`"), `\`abc"\``) 37 | assert.strictEqual(js.quoteJSString("abc\"\'", "\`"), `\`abc"'\``) 38 | 39 | assert.strictEqual(js.quoteJSString(""), `""`) 40 | assert.strictEqual(js.quoteJSString("你好"), `"你好"`) 41 | assert.strictEqual(js.quoteJSString("\r\n\u2028\u2029"), `"\\r\\n\\u2028\\u2029"`) 42 | assert.strictEqual(js.quoteJSString(`"'\\\v\t\r\n\b\f\u00002`, ""), "\\\"\\\'\\\\\\v\\t\\r\\n\\b\\f\\02") 43 | 44 | assert.strictEqual(js.quoteJSString("abc", "'"), `'abc'`) 45 | assert.strictEqual(js.quoteJSString("abc\'", "'"), `'abc\\\''`) 46 | assert.strictEqual(js.quoteJSString("abc\'", '"'), `"abc'"`) 47 | } 48 | 49 | export function unquoteJSStringTest() { 50 | assert.strictEqual(js.unquoteJSString(`"abc"`), "abc") 51 | 52 | assert.strictEqual(js.unquoteJSString(`""`), "") 53 | assert.strictEqual(js.unquoteJSString(`"你好"`), "你好") 54 | assert.strictEqual(js.unquoteJSString(`"\\r\\n\\u2028\\u2029"`), "\r\n\u2028\u2029") 55 | assert.strictEqual(js.unquoteJSString(`\`\\r\\n\\u2028\\u2029\``), "\r\n\u2028\u2029") 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /test/json.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as json from "../src/json" 3 | 4 | export namespace jsonTest { 5 | 6 | export function parseJSONTest() { 7 | assert.deepStrictEqual(json.parseJSON(`{}`), {}) 8 | assert.strictEqual(json.parseJSON(`3`), 3) 9 | assert.strictEqual(json.parseJSON(``), undefined) 10 | assert.strictEqual(json.parseJSON(`x`), undefined) 11 | } 12 | 13 | export function formatJSONTest() { 14 | assert.deepStrictEqual(json.formatJSON({}), `{}`) 15 | assert.deepStrictEqual(json.formatJSON(3), `3`) 16 | } 17 | 18 | export function normalizeJSONTest() { 19 | // https://github.com/sindresorhus/strip-json-comments/blob/master/test.js 20 | assert.strictEqual(json.normalizeJSON('//comment\n{"a":"b"}'), ' \n{"a":"b"}') 21 | assert.strictEqual(json.normalizeJSON('/*//comment*/{"a":"b"}'), ' {"a":"b"}') 22 | 23 | assert.strictEqual(json.normalizeJSON('{"a":"b"//comment\n}'), '{"a":"b" \n}') 24 | assert.strictEqual(json.normalizeJSON('{"a":"b"/*comment*/}'), '{"a":"b" }') 25 | assert.strictEqual(json.normalizeJSON('{"a"/*\n\n\ncomment\r\n*/:"b"}'), '{"a" \n\n\n \r\n :"b"}') 26 | assert.strictEqual(json.normalizeJSON('/*!\n * comment\n */\n{"a":"b"}'), ' \n \n \n{"a":"b"}') 27 | assert.strictEqual(json.normalizeJSON('{/*comment*/"a":"b"}'), '{ "a":"b"}') 28 | 29 | assert.strictEqual(json.normalizeJSON('//comment\n{"a":"b"}', false), '\n{"a":"b"}') 30 | assert.strictEqual(json.normalizeJSON('/*//comment*/{"a":"b"}', false), '{"a":"b"}') 31 | assert.strictEqual(json.normalizeJSON('{"a":"b"//comment\n}', false), '{"a":"b"\n}') 32 | assert.strictEqual(json.normalizeJSON('{"a":"b"/*comment*/}', false), '{"a":"b"}') 33 | assert.strictEqual(json.normalizeJSON('{"a"/*\n\n\ncomment\r\n*/:"b"}', false), '{"a":"b"}') 34 | assert.strictEqual(json.normalizeJSON('/*!\n * comment\n */\n{"a":"b"}', false), '\n{"a":"b"}') 35 | assert.strictEqual(json.normalizeJSON('{/*comment*/"a":"b"}', false), '{"a":"b"}') 36 | 37 | assert.strictEqual(json.normalizeJSON('{"a":"b//c"}'), '{"a":"b//c"}') 38 | assert.strictEqual(json.normalizeJSON('{"a":"b/*c*/"}'), '{"a":"b/*c*/"}') 39 | assert.strictEqual(json.normalizeJSON('{"/*a":"b"}'), '{"/*a":"b"}') 40 | assert.strictEqual(json.normalizeJSON('{"\\"/*a":"b"}'), '{"\\"/*a":"b"}') 41 | 42 | assert.strictEqual(json.normalizeJSON('{"\\\\":"https://foobar.com"}'), '{"\\\\":"https://foobar.com"}') 43 | assert.strictEqual(json.normalizeJSON('{"foo\\"":"https://foobar.com"}'), '{"foo\\"":"https://foobar.com"}') 44 | assert.strictEqual(json.normalizeJSON('{"a":"b"\n}'), '{"a":"b"\n}') 45 | assert.strictEqual(json.normalizeJSON('{"a":"b"\r\n}'), '{"a":"b"\r\n}') 46 | 47 | assert.strictEqual(json.normalizeJSON('{"a":"b"//c\n}'), '{"a":"b" \n}') 48 | assert.strictEqual(json.normalizeJSON('{"a":"b"//c\r\n}'), '{"a":"b" \r\n}') 49 | 50 | assert.strictEqual(json.normalizeJSON('{"a":"b"/*c*/\n}'), '{"a":"b" \n}') 51 | assert.strictEqual(json.normalizeJSON('{"a":"b"/*c*/\r\n}'), '{"a":"b" \r\n}') 52 | 53 | assert.strictEqual(json.normalizeJSON('{"a":"b",/*c\nc2*/"x":"y"\n}'), '{"a":"b", \n "x":"y"\n}') 54 | assert.strictEqual(json.normalizeJSON('{"a":"b",/*c\r\nc2*/"x":"y"\r\n}'), '{"a":"b", \r\n "x":"y"\r\n}') 55 | 56 | assert.strictEqual(json.normalizeJSON('{\r\n\t"a":"b"\r\n} //EOF'), '{\r\n\t"a":"b"\r\n} ') 57 | assert.strictEqual(json.normalizeJSON('{\r\n\t"a":"b"\r\n} //EOF', false), '{\r\n\t"a":"b"\r\n} ') 58 | 59 | assert.strictEqual(json.normalizeJSON(String.raw`{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}`), String.raw`{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}`) 60 | 61 | assert.strictEqual(json.normalizeJSON('{\r\n\t"a":"b"\r\n,} //EOF', false), '{\r\n\t"a":"b"\r\n} ') 62 | 63 | } 64 | 65 | export function readJSONByPathTest() { 66 | assert.strictEqual(json.readJSONByPath({ a: 1 }, "a"), 1) 67 | assert.strictEqual(json.readJSONByPath({ a: { b: 1 } }, "a.b"), 1) 68 | assert.strictEqual(json.readJSONByPath({ a: [1] }, "a.0"), 1) 69 | assert.strictEqual(json.readJSONByPath({ a: [1] }, "a.length"), 1) 70 | assert.strictEqual(json.readJSONByPath({ a: [1] }, "b"), undefined) 71 | assert.strictEqual(json.readJSONByPath({ a: [1] }, "b.d"), undefined) 72 | assert.strictEqual(json.readJSONByPath({ "a.b": 1 }, "a..b"), 1) 73 | assert.strictEqual(json.readJSONByPath({ "a..b": 1 }, "a....b"), 1) 74 | assert.strictEqual(json.readJSONByPath({ "a.b": { "c.d": 1 } }, "a..b.c..d"), 1) 75 | } 76 | 77 | export function writeJSONByPathTest() { 78 | const obj: any = {} 79 | json.writeJSONByPath(obj, `a`, 1) 80 | assert.strictEqual(obj.a, 1) 81 | json.writeJSONByPath(obj, `b.c`, 1) 82 | assert.deepStrictEqual(obj.b, { c: 1 }) 83 | json.writeJSONByPath(obj, `b.b`, 1) 84 | assert.deepStrictEqual(obj.b, { c: 1, b: 1 }) 85 | json.writeJSONByPath(obj, `a.x`, 0) 86 | assert.deepStrictEqual(obj.a, { x: 0 }) 87 | json.writeJSONByPath(obj, `a..x`, 0) 88 | assert.deepStrictEqual(obj["a.x"], 0) 89 | } 90 | 91 | export function moveJSONByPathTest() { 92 | const obj: any = { 93 | a: 1, 94 | b: 2, 95 | c: { 96 | d: 1 97 | } 98 | } 99 | json.moveJSONByPath(obj, [`b`], 'c.d') 100 | assert.deepStrictEqual(obj, { 101 | a: 1, 102 | c: { 103 | b: 2, 104 | d: 1 105 | } 106 | }) 107 | json.moveJSONByPath(obj, [`a`], 'c.') 108 | assert.deepStrictEqual(obj, { 109 | c: { 110 | b: 2, 111 | d: 1, 112 | a: 1, 113 | } 114 | }) 115 | json.moveJSONByPath(obj, [`c.b`, 'c.a'], null) 116 | assert.deepStrictEqual(obj, { 117 | c: { 118 | d: 1, 119 | }, 120 | b: 2, 121 | a: 1, 122 | }) 123 | json.moveJSONByPath(obj, [`c.b.d`, 'c.a.d'], null) 124 | assert.deepStrictEqual(obj, { 125 | c: { 126 | d: 1, 127 | }, 128 | b: 2, 129 | a: 1, 130 | }) 131 | } 132 | 133 | export function deleteJSONByPathTest() { 134 | const obj: any = { a: 1, b: { x: 2 } } 135 | json.deleteJSONByPath(obj, `a`) 136 | assert.strictEqual(obj.a, undefined) 137 | json.deleteJSONByPath(obj, `b.x`) 138 | assert.deepStrictEqual(obj.b, {}) 139 | json.deleteJSONByPath(obj, `c`) 140 | assert.deepStrictEqual(obj.b, {}) 141 | json.deleteJSONByPath(obj, `c.d`) 142 | assert.deepStrictEqual(obj.b, {}) 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /test/jsx.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as jsx from "../src/jsx" 3 | 4 | export namespace jsxTest { 5 | 6 | export function jsxTest() { 7 | assert.strictEqual(jsx.jsx("button", { 8 | id: "my", 9 | title: undefined, 10 | disabled: true, 11 | readOnly: false 12 | }, "Hello", " World").toString(), ``) 13 | 14 | assert.strictEqual(jsx.jsx("br", null).toString(), `
`) 15 | assert.strictEqual(jsx.jsx("div", null, ["1", jsx.jsx("span", null), "2", null, undefined], null).toString(), `
12
`) 16 | assert.strictEqual(jsx.jsx(jsx.Fragment, null, null, undefined, jsx.jsx("img", null)).toString(), ``) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /test/lineColumn.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as lineColumn from "../src/lineColumn" 3 | 4 | export namespace lineColumnTest { 5 | 6 | export function indexToLineColumnTest() { 7 | assert.deepStrictEqual(lineColumn.indexToLineColumn("012\n456", 4), { line: 1, column: 0 }) 8 | 9 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", -1), { line: 0, column: -1 }) 10 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 0), { line: 0, column: 0 }) 11 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 1), { line: 0, column: 1 }) 12 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 2), { line: 1, column: 0 }) 13 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 3), { line: 1, column: 1 }) 14 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 4), { line: 2, column: 0 }) 15 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 5), { line: 2, column: 1 }) 16 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 6), { line: 2, column: 2 }) 17 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 7), { line: 3, column: 0 }) 18 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 8), { line: 3, column: 1 }) 19 | assert.deepStrictEqual(lineColumn.indexToLineColumn("0\r2\n4\r\n7", 9), { line: 3, column: 2 }) 20 | } 21 | 22 | export function lineColumnToIndexTest() { 23 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("012\n456", { line: 1, column: 0 }), 4) 24 | 25 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 0, column: -1 }), -1) 26 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 0, column: 0 }), 0) 27 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 0, column: 1 }), 1) 28 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 1, column: 0 }), 2) 29 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 1, column: 1 }), 3) 30 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 2, column: 0 }), 4) 31 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 2, column: 1 }), 5) 32 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 2, column: 2 }), 6) 33 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 3, column: 0 }), 7) 34 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 3, column: 1 }), 8) 35 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 3, column: 2 }), 9) 36 | 37 | assert.deepStrictEqual(lineColumn.lineColumnToIndex("0\r2\n4\r\n7", { line: 4, column: 0 }), 8) 38 | } 39 | 40 | export namespace lineMapTest { 41 | 42 | export function indexToLineColumnTest() { 43 | const lineMap = new lineColumn.LineMap("0\r2\n4\r\n7") 44 | assert.deepStrictEqual(lineMap.indexToLineColumn(-1), { line: 0, column: -1 }) 45 | assert.deepStrictEqual(lineMap.indexToLineColumn(0), { line: 0, column: 0 }) 46 | assert.deepStrictEqual(lineMap.indexToLineColumn(1), { line: 0, column: 1 }) 47 | assert.deepStrictEqual(lineMap.indexToLineColumn(2), { line: 1, column: 0 }) 48 | assert.deepStrictEqual(lineMap.indexToLineColumn(3), { line: 1, column: 1 }) 49 | assert.deepStrictEqual(lineMap.indexToLineColumn(4), { line: 2, column: 0 }) 50 | assert.deepStrictEqual(lineMap.indexToLineColumn(5), { line: 2, column: 1 }) 51 | assert.deepStrictEqual(lineMap.indexToLineColumn(6), { line: 2, column: 2 }) 52 | assert.deepStrictEqual(lineMap.indexToLineColumn(7), { line: 3, column: 0 }) 53 | assert.deepStrictEqual(lineMap.indexToLineColumn(8), { line: 3, column: 1 }) 54 | assert.deepStrictEqual(lineMap.indexToLineColumn(9), { line: 3, column: 2 }) 55 | 56 | assert.deepStrictEqual(lineMap.indexToLineColumn(9), { line: 3, column: 2 }) 57 | assert.deepStrictEqual(lineMap.indexToLineColumn(3), { line: 1, column: 1 }) 58 | assert.deepStrictEqual(lineMap.indexToLineColumn(7), { line: 3, column: 0 }) 59 | } 60 | 61 | export function lineColumnToIndexTest() { 62 | const lineMap = new lineColumn.LineMap("0\r2\n4\r\n7") 63 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 0, column: -1 }), -1) 64 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 0, column: 0 }), 0) 65 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 0, column: 1 }), 1) 66 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 1, column: 0 }), 2) 67 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 1, column: 1 }), 3) 68 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 2, column: 0 }), 4) 69 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 2, column: 1 }), 5) 70 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 2, column: 2 }), 6) 71 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 3, column: 0 }), 7) 72 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 3, column: 1 }), 8) 73 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 3, column: 2 }), 9) 74 | 75 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 3, column: 2 }), 9) 76 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 1, column: 1 }), 3) 77 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 3, column: 0 }), 7) 78 | 79 | assert.deepStrictEqual(lineMap.lineColumnToIndex({ line: 4, column: 0 }), 8) 80 | } 81 | 82 | } 83 | 84 | export function addLineColumnTest() { 85 | assert.deepStrictEqual(lineColumn.addLineColumn({ line: 1, column: 2 }, 10, 4), { line: 11, column: 4 }) 86 | 87 | assert.deepStrictEqual(lineColumn.addLineColumn({ line: 1, column: 2 }, 0, 4), { line: 1, column: 6 }) 88 | assert.deepStrictEqual(lineColumn.addLineColumn({ line: 1, column: 2 }, -1, 4), { line: 0, column: 4 }) 89 | } 90 | 91 | export function compareLineColumnTest() { 92 | assert.ok(lineColumn.compareLineColumn({ line: 1, column: 2 }, { line: 11, column: 4 }) < 0) 93 | 94 | assert.ok(lineColumn.compareLineColumn({ line: 11, column: 4 }, { line: 1, column: 2 }) > 0) 95 | assert.ok(lineColumn.compareLineColumn({ line: 11, column: 4 }, { line: 11, column: 2 }) > 0) 96 | assert.ok(lineColumn.compareLineColumn({ line: 11, column: 2 }, { line: 11, column: 4 }) < 0) 97 | assert.ok(lineColumn.compareLineColumn({ line: 1, column: 100 }, { line: 2, column: 0 }) < 0) 98 | assert.ok(lineColumn.compareLineColumn({ line: 1, column: 2 }, { line: 1, column: 2 }) === 0) 99 | } 100 | 101 | 102 | } -------------------------------------------------------------------------------- /test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as logger from "../src/logger" 3 | import { captureStdio } from "./helpers/consoleHelper" 4 | 5 | export namespace loggerTest { 6 | 7 | export async function traceTest() { 8 | await captureStdio((stdout, stderr) => { 9 | const l = new logger.Logger() 10 | l.trace("trace") 11 | assert.strictEqual(stdout.join("\n"), "") 12 | 13 | l.logLevel = logger.LogLevel.trace 14 | l.trace("trace") 15 | assert.strictEqual(stdout.join("\n").includes("trace"), true) 16 | }) 17 | } 18 | 19 | export async function debugTest() { 20 | await captureStdio((stdout, stderr) => { 21 | const l = new logger.Logger() 22 | l.debug("debug") 23 | assert.strictEqual(stdout.join("\n"), "") 24 | 25 | l.logLevel = logger.LogLevel.debug 26 | l.debug("debug") 27 | assert.strictEqual(stdout.join("\n").includes("debug"), true) 28 | }) 29 | } 30 | 31 | export async function logTest() { 32 | await captureStdio((stdout, stderr) => { 33 | const l = new logger.Logger() 34 | l.log("log") 35 | assert.strictEqual(stdout.join("\n").includes("log"), true) 36 | }) 37 | } 38 | 39 | export async function infoTest() { 40 | await captureStdio((stdout, stderr) => { 41 | const l = new logger.Logger() 42 | l.info("info") 43 | assert.strictEqual(stdout.join("\n").includes("info"), true) 44 | }) 45 | } 46 | 47 | export async function successTest() { 48 | await captureStdio((stdout, stderr) => { 49 | const l = new logger.Logger() 50 | l.success("success") 51 | assert.strictEqual(stdout.join("\n").includes("success"), true) 52 | }) 53 | } 54 | 55 | export async function warningTest() { 56 | await captureStdio((stdout, stderr) => { 57 | const l = new logger.Logger() 58 | l.warning("warning") 59 | assert.strictEqual(stderr.join("\n").includes("warning"), true) 60 | }) 61 | } 62 | 63 | export async function errorTest() { 64 | await captureStdio((stdout, stderr) => { 65 | const l = new logger.Logger() 66 | l.error("error") 67 | assert.strictEqual(stderr.join("\n").includes("error"), true) 68 | }) 69 | } 70 | 71 | export async function fatalTest() { 72 | await captureStdio((stdout, stderr) => { 73 | const l = new logger.Logger() 74 | l.fatal("fatal") 75 | assert.strictEqual(stderr.join("\n").includes("fatal"), true) 76 | }) 77 | } 78 | 79 | export async function formatLogTest() { 80 | const l = new logger.Logger({ timestamp: false }) 81 | assert.ok(l.formatLog({ 82 | source: "source", 83 | message: "message", 84 | stack: new Error("error").stack, 85 | showStack: true, 86 | fileName: "fileName", 87 | content: "content", 88 | line: 1, 89 | column: 2, 90 | endLine: 3, 91 | endColumn: 4, 92 | detail: "detail" 93 | }).includes("message")) 94 | 95 | assert.ok(l.formatLog({ 96 | message: "message", 97 | fileName: "fileName", 98 | line: 1, 99 | endLine: 3, 100 | }, logger.LogLevel.success, false).includes("message")) 101 | 102 | assert.strictEqual(l.formatLog({}), "") 103 | assert.strictEqual(l.formatLog(""), "") 104 | } 105 | 106 | export async function taskTest() { 107 | await captureStdio((stdout, stderr) => { 108 | const l = new logger.Logger() 109 | l.logLevel = logger.LogLevel.trace 110 | l.end(l.begin("Current")) 111 | assert.strictEqual(stdout.join("\n").includes("Current"), true) 112 | }) 113 | 114 | await captureStdio((stdout, stderr) => { 115 | const l = new logger.Logger() 116 | l.progressPercent = 10 117 | l.showProgress("Current") 118 | l.hideProgress() 119 | }) 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /test/misc.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as misc from "../src/misc" 3 | 4 | export namespace miscTest { 5 | 6 | export function mergeTest() { 7 | assert.strictEqual(misc.merge(1, 2), 2) 8 | assert.deepStrictEqual(misc.merge(undefined, { a: 1 }, undefined, { b: 2 }, undefined), { a: 1, b: 2 }) 9 | assert.deepStrictEqual(misc.merge({ a: [1] }, { a: [2] }), { a: [1, 2] }) 10 | 11 | const obj = { a: null as any, b: 1 } 12 | obj.a = obj 13 | assert.strictEqual(misc.merge({ a: {} }, obj).b, 1) 14 | } 15 | 16 | export function stripBOMTest() { 17 | assert.deepStrictEqual(misc.stripBOM("\ufeffg"), "g") 18 | 19 | assert.deepStrictEqual(misc.stripBOM(""), "") 20 | assert.deepStrictEqual(misc.stripBOM("\ufeff"), "") 21 | } 22 | 23 | export function capitalizeTest() { 24 | assert.strictEqual(misc.capitalize("qwert"), "Qwert") 25 | } 26 | 27 | export function replaceStringTest() { 28 | assert.strictEqual(misc.replaceString("abc", /a/g, "$&"), "abc") 29 | assert.strictEqual(misc.replaceString("abc", /a/g, "$1"), "$1bc") 30 | assert.strictEqual(misc.replaceString("abc", /(a)/g, "$1"), "abc") 31 | assert.strictEqual(misc.replaceString("abc", /(a)/g, () => "$1"), "$1bc") 32 | } 33 | 34 | export function randomStringTest() { 35 | assert.deepStrictEqual(misc.randomString(8).length, 8) 36 | 37 | assert.deepStrictEqual(misc.randomString(100).length, 100) 38 | assert.deepStrictEqual(misc.randomString(0), "") 39 | } 40 | 41 | export function concatTest() { 42 | assert.deepStrictEqual(misc.concat([1, 2, 3, 4], [5]), [1, 2, 3, 4, 5]) 43 | assert.deepStrictEqual(misc.concat(null, null), null) 44 | assert.deepStrictEqual(misc.concat([1], null), [1]) 45 | assert.deepStrictEqual(misc.concat(null, [1]), [1]) 46 | } 47 | 48 | export function pushIfNotExistsTest() { 49 | const foo = [1, 9, 0] 50 | misc.pushIfNotExists(foo, 1) 51 | assert.deepStrictEqual(foo, [1, 9, 0]) 52 | misc.pushIfNotExists(foo, 2) 53 | assert.deepStrictEqual(foo, [1, 9, 0, 2]) 54 | } 55 | 56 | export function binarySearchTest() { 57 | assert.deepStrictEqual(misc.binarySearch([1, 2, 3, 4, 5], 3), 2) 58 | assert.deepStrictEqual(misc.binarySearch([1, 2, 3, 4, 5], 4), 3) 59 | assert.deepStrictEqual(misc.binarySearch([1, 2, 3, 4, 5], 2), 1) 60 | assert.deepStrictEqual(misc.binarySearch([1, 2, 3, 4, 5], 3.5), ~3) 61 | } 62 | 63 | export function insertSortedTest() { 64 | assert.deepStrictEqual(test([1, 3, 5], 2), [1, 2, 3, 5]) 65 | 66 | assert.deepStrictEqual(test([], 1), [1]) 67 | assert.deepStrictEqual(test([0], 1), [0, 1]) 68 | assert.deepStrictEqual(test([2], 1), [1, 2]) 69 | assert.deepStrictEqual(test([1, 3], 2), [1, 2, 3]) 70 | assert.deepStrictEqual(test([1, 3, 5], 3), [1, 3, 3, 5]) 71 | assert.deepStrictEqual(test([1, 3, 5], 5), [1, 3, 5, 5]) 72 | assert.deepStrictEqual(test([{ value: 1 }, { value: 3 }], { value: 1, foo: 1 }, (x, y) => x.value <= y.value), [{ value: 1 }, { value: 1, foo: 1 }, { value: 3 }]) 73 | 74 | function test(array: any[], value: any, comparer = (x: any, y: any) => x <= y) { 75 | misc.insertSorted(array, value, comparer) 76 | return array 77 | } 78 | } 79 | 80 | export function removeTest() { 81 | const foo = [1, 9, 9, 0] 82 | assert.strictEqual(misc.remove(foo, 9), 1) 83 | assert.deepStrictEqual(foo, [1, 9, 0]) 84 | assert.strictEqual(misc.remove(foo, 9), 1) 85 | assert.deepStrictEqual(foo, [1, 0]) 86 | assert.strictEqual(misc.remove(foo, 9), -1) 87 | assert.deepStrictEqual(foo, [1, 0]) 88 | } 89 | 90 | export function escapeRegExpTest() { 91 | assert.strictEqual(new RegExp(misc.escapeRegExp("\\s")).source, /\\s/.source) 92 | } 93 | 94 | export function formatDateTest() { 95 | assert.strictEqual(misc.formatDate(new Date("2014/01/01 03:05:07"), "yyMdHms"), "1411357") 96 | 97 | assert.strictEqual(misc.formatDate(new Date("2014/01/01 03:05:07"), "yyyy-MM-dd HH:mm:ss"), "2014-01-01 03:05:07") 98 | assert.strictEqual(misc.formatDate(new Date("2014/01/01 03:05:07"), "yyMMddHHmmss"), "140101030507") 99 | assert.strictEqual(misc.formatDate(new Date("2014/01/01 03:05:07"), "你好"), "你好") 100 | assert.strictEqual(misc.formatDate(new Date("2014/01/01 03:05:07"), "abc"), "abc") 101 | } 102 | 103 | export function formatRelativeDateTest() { 104 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:05:07")), "just now") 105 | 106 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:05:00")), new Date("2014/01/01 03:05:07").toLocaleString()) 107 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:05:08")), "1 seconds ago") 108 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:05:57")), "50 seconds ago") 109 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:06:06")), "59 seconds ago") 110 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:06:07")), "1 minutes ago") 111 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 03:06:19")), "1 minutes ago") 112 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/01 04:06:19")), "1 hours ago") 113 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/02 04:06:19")), "yesterday") 114 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/03 04:06:19")), "2 days ago") 115 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/01/04 04:06:19")), new Date("2014/01/01 03:05:07").toLocaleDateString()) 116 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2014/02/03 04:06:19")), new Date("2014/01/01 03:05:07").toLocaleDateString()) 117 | assert.strictEqual(misc.formatRelativeDate(new Date("2014/01/01 03:05:07"), new Date("2015/02/03 04:06:19")), new Date("2014/01/01 03:05:07").toLocaleDateString()) 118 | 119 | assert.strictEqual(typeof misc.formatRelativeDate(new Date("2014/01/01 03:05:07")), "string") 120 | } 121 | 122 | export function formatHRTimeTest() { 123 | assert.strictEqual(misc.formatHRTime([1, 120000000]), "1.12s") 124 | 125 | assert.strictEqual(misc.formatHRTime([0, 0]), "0ms") 126 | assert.strictEqual(misc.formatHRTime([0, 1000]), "<0.01ms") 127 | assert.strictEqual(misc.formatHRTime([0, 9999]), "<0.01ms") 128 | assert.strictEqual(misc.formatHRTime([0, 10000]), "0.01ms") 129 | assert.strictEqual(misc.formatHRTime([0, 20000]), "0.02ms") 130 | assert.strictEqual(misc.formatHRTime([0, 100000]), "0.1ms") 131 | assert.strictEqual(misc.formatHRTime([0, 1000000]), "1ms") 132 | assert.strictEqual(misc.formatHRTime([0, 10000000]), "10ms") 133 | assert.strictEqual(misc.formatHRTime([0, 100000000]), "100ms") 134 | assert.strictEqual(misc.formatHRTime([0, 999999999]), "1000ms") 135 | assert.strictEqual(misc.formatHRTime([1, 0]), "1s") 136 | assert.strictEqual(misc.formatHRTime([1, 100000000]), "1.1s") 137 | assert.strictEqual(misc.formatHRTime([1, 110000000]), "1.11s") 138 | assert.strictEqual(misc.formatHRTime([1, 119000000]), "1.12s") 139 | assert.strictEqual(misc.formatHRTime([1, 306083663]), "1.31s") 140 | assert.strictEqual(misc.formatHRTime([1, 999999999]), "2s") 141 | assert.strictEqual(misc.formatHRTime([10, 0]), "10s") 142 | assert.strictEqual(misc.formatHRTime([60, 100000000]), "1min") 143 | assert.strictEqual(misc.formatHRTime([60, 999999999]), "1min") 144 | assert.strictEqual(misc.formatHRTime([120, 100000000]), "2min") 145 | assert.strictEqual(misc.formatHRTime([150, 100000000]), "2min30s") 146 | assert.strictEqual(misc.formatHRTime([200, 100000000]), "3min20s") 147 | assert.strictEqual(misc.formatHRTime([1500, 100000000]), "25min") 148 | assert.strictEqual(misc.formatHRTime([15000, 100000000]), "250min") 149 | } 150 | 151 | export function formatSizeTest() { 152 | assert.strictEqual(misc.formatSize(1000), "0.98KB") 153 | 154 | assert.strictEqual(misc.formatSize(0), "0B") 155 | assert.strictEqual(misc.formatSize(1), "1B") 156 | assert.strictEqual(misc.formatSize(1024), "1KB") 157 | assert.strictEqual(misc.formatSize(1024 * 1024), "1MB") 158 | assert.strictEqual(misc.formatSize(1024 * 1024 * 1024), "1GB") 159 | assert.strictEqual(misc.formatSize(1024 * 1024 * 1024 * 1024), "1TB") 160 | } 161 | 162 | export async function parallelForEachTest() { 163 | assert.deepStrictEqual(await misc.parallelForEach([1, 2], item => item), [1, 2]) 164 | } 165 | 166 | } -------------------------------------------------------------------------------- /test/process.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as proc from "../src/process" 3 | 4 | export namespace processTest { 5 | 6 | export async function execTest() { 7 | assert.strictEqual((await proc.exec("echo 1")).stdout.trim(), "1") 8 | assert.strictEqual((await proc.exec("exit 1")).exitCode, 1) 9 | 10 | assert.ok((await proc.exec("command-not-exists")).exitCode !== 0) 11 | } 12 | 13 | export async function onExitTest() { 14 | const fn = () => { } 15 | proc.onExit(fn) 16 | proc.offExit(fn) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /test/request.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as request from "../src/request" 3 | 4 | export namespace requestTest { 5 | 6 | export namespace cookieJarTest { 7 | 8 | export function basicTest() { 9 | const cookieJar = new request.CookieJar() 10 | cookieJar.setCookie("/", { name: "n1", value: "v1" }) 11 | assert.strictEqual(cookieJar.getCookie("/", "n1"), "v1") 12 | 13 | cookieJar.setCookie("/", { name: "n1", value: "v1" }) 14 | assert.strictEqual(cookieJar.getCookies("/").length, 1) 15 | assert.strictEqual(cookieJar.getCookies("/")[0].name, "n1") 16 | assert.strictEqual(cookieJar.getCookies("/")[0].value, "v1") 17 | 18 | } 19 | 20 | export function shouldExpires() { 21 | const cookieJar = new request.CookieJar() 22 | const date = new Date() 23 | cookieJar.setCookie("/", { name: "n1", value: "v1", expires: date }, date) 24 | assert.strictEqual(cookieJar.getCookie("/", "n1"), undefined) 25 | 26 | const lastSecond = new Date(date.getTime() - 1000) 27 | cookieJar.setCookie("http://example.com/req", { name: "c2", value: "c2", path: "/path", expires: lastSecond }, date) 28 | cookieJar.setCookie("http://example.com/req", { name: "c3", value: "c3", path: "/path", expires: lastSecond }) 29 | assert.strictEqual(cookieJar.getCookie("http://example.com/path/sub", "c2", date, undefined, true), undefined) 30 | assert.strictEqual(cookieJar.getCookies("http://example.com/path/sub", date, undefined, true).find(cookie => cookie.name === "c2"), undefined) 31 | assert.strictEqual(cookieJar.getCookie("http://example.com/path/sub", "c3", date, undefined, true), undefined) 32 | assert.strictEqual(cookieJar.getCookies("http://example.com/path/sub", date, undefined, true).find(cookie => cookie.name === "c3"), undefined) 33 | 34 | cookieJar.setCookie("http://example.com/req", { name: "c3", value: "c3", path: "/path", expires: lastSecond }, date) 35 | } 36 | 37 | export function shouldFollowAttributes() { 38 | const cookieJar = new request.CookieJar() 39 | 40 | cookieJar.setCookie("https://example.com", { name: "n1", value: "v1", domain: "www.example.com", path: "/path", httpOnly: true, secure: true }) 41 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path", "n1"), "v1") 42 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path/", "n1"), "v1") 43 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path/any", "n1"), "v1") 44 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/", "n1"), undefined) 45 | assert.strictEqual(cookieJar.getCookie("https://www.other.com/path", "n1"), undefined) 46 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path", "n1", undefined, true), undefined) 47 | assert.strictEqual(cookieJar.getCookie("http://www.example.com/path", "n1"), undefined) 48 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path", "n1", undefined, undefined, true), undefined) 49 | assert.strictEqual(cookieJar.getCookie("wss://www.example.com/path", "n1"), "v1") 50 | 51 | assert.strictEqual(cookieJar.getCookies("https://www.example.com/path").length, 1) 52 | assert.strictEqual(cookieJar.getCookies("https://www.example.com/path/").length, 1) 53 | assert.strictEqual(cookieJar.getCookies("https://www.example.com/path/any").length, 1) 54 | assert.strictEqual(cookieJar.getCookies("https://www.example.com/").length, 0) 55 | assert.strictEqual(cookieJar.getCookies("https://www.other.com/path").length, 0) 56 | assert.strictEqual(cookieJar.getCookies("https://www.example.com/path", undefined, true).length, 0) 57 | assert.strictEqual(cookieJar.getCookies("https://www.example.com/path", undefined, undefined, true).length, 0) 58 | } 59 | 60 | export function shouldFollowDomain() { 61 | const cookieJar = new request.CookieJar() 62 | 63 | cookieJar.setCookie("https://example.com", { name: "n1", value: "v1", domain: "www.example.com", path: "/path", httpOnly: true, secure: true }) 64 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path", "n1"), "v1") 65 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path/", "n1"), "v1") 66 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path/any", "n1"), "v1") 67 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/", "n1"), undefined) 68 | assert.strictEqual(cookieJar.getCookie("https://www.other.com/path", "n1"), undefined) 69 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path", "n1", undefined, true), undefined) 70 | assert.strictEqual(cookieJar.getCookie("https://www.example.com/path", "n1", undefined, undefined, true), undefined) 71 | } 72 | 73 | export function shouldFollowPath() { 74 | const cookieJar = new request.CookieJar() 75 | cookieJar.setCookie(new URL("http://example.com/req"), { name: "c2", value: "c2", path: "/path" }) 76 | assert.strictEqual(cookieJar.getCookie(new URL("http://example.com/path/sub"), "c2", undefined, undefined, true), "c2") 77 | assert.strictEqual(cookieJar.getCookie("http://sub.example.com/path/sub", "c2", undefined, undefined, true), undefined) 78 | assert.strictEqual(cookieJar.getCookies(new URL("http://sub.example.com/path/sub"), undefined, undefined, true).length, 0) 79 | } 80 | 81 | export function shouldIgnorePort() { 82 | const cookieJar = new request.CookieJar() 83 | cookieJar.setCookie("http://example.com/req", { name: "n1", value: "v1" }) 84 | assert.strictEqual(cookieJar.getCookie("http://example.com:82", "n1"), "v1") 85 | assert.strictEqual(cookieJar.getCookies("http://example.com:82")[0].value, "v1") 86 | assert.strictEqual(cookieJar.getCookie("http://my.com", "n1"), undefined) 87 | assert.strictEqual(cookieJar.getCookies("http://my.com:82").length, 0) 88 | } 89 | 90 | export function shouldIgnoreInvalidDomain() { 91 | const cookieJar = new request.CookieJar() 92 | cookieJar.setCookie("http://example.com/req", { name: "n1", value: "v1", domain: "my.com" }) 93 | assert.strictEqual(cookieJar.getCookie("http://example.com", "n1"), undefined) 94 | assert.strictEqual(cookieJar.getCookies("http://example.com").find(cookie => cookie.name === "n1"), undefined) 95 | assert.strictEqual(cookieJar.getCookie("http://my.com", "n1"), undefined) 96 | assert.strictEqual(cookieJar.getCookies("http://my.com").find(cookie => cookie.name === "n1"), undefined) 97 | 98 | cookieJar.setCookie("http://example.com", { name: "n2", value: "v2", domain: ".example.com" }) 99 | assert.strictEqual(cookieJar.getCookie("http://EXAMPLE.COM", "n2"), "v2") 100 | assert.strictEqual(cookieJar.getCookie("http://example.com", "n2"), "v2") 101 | assert.strictEqual(cookieJar.getCookie("http://www.example.com", "n2"), "v2") 102 | assert.strictEqual(cookieJar.getCookie("http://sub.2.example.com", "n2"), "v2") 103 | } 104 | 105 | export function shouldSupportIP() { 106 | const cookieJar = new request.CookieJar() 107 | cookieJar.setCookie("http://127.0.0.1/req", { name: "n1", value: "v1", domain: "127.0.0.1" }) 108 | assert.strictEqual(cookieJar.getCookie("http://127.0.0.1", "n1"), "v1") 109 | 110 | cookieJar.setCookie("http://[::FFFF:127.0.0.0]/req", { name: "n2", value: "v2", domain: "::FFFF:127.0.0.0" }) 111 | assert.strictEqual(cookieJar.getCookie("http://[::FFFF:127.0.0.0]", "n2"), "v2") 112 | 113 | cookieJar.setCookie("http://0.0.1/req", { name: "n3", value: "v3" }) 114 | assert.strictEqual(cookieJar.getCookie("http://127.0.0.1", "n3"), undefined) 115 | } 116 | 117 | export function shouldIgnoreInvalidPath() { 118 | const cookieJar = new request.CookieJar() 119 | cookieJar.setCookie("http://example.com/req", { name: "n1", value: "v1", path: "x" }) 120 | assert.strictEqual(cookieJar.getCookie("http://example.com", "n1"), "v1") 121 | 122 | cookieJar.setCookie("http://example.com/req", { name: "n2", value: "v2", path: "/x" }) 123 | assert.strictEqual(cookieJar.getCookie("http://example.com/xp", "n2"), undefined) 124 | assert.strictEqual(cookieJar.getCookie("http://example.com/x/", "n2"), "v2") 125 | assert.strictEqual(cookieJar.getCookie("http://example.com/x", "n2"), "v2") 126 | assert.strictEqual(cookieJar.getCookie("http://example.com/x/y", "n2"), "v2") 127 | } 128 | 129 | export function getCookiesHeaderTest() { 130 | const cookieJar = new request.CookieJar() 131 | cookieJar.setCookie("http://example.com/req", { name: "n1", value: "v1" }) 132 | assert.strictEqual(cookieJar.getCookiesHeader("http://example.com"), "n1=v1") 133 | 134 | cookieJar.setCookie("http://example.com/req", { name: "n2", value: "v2" }) 135 | assert.strictEqual(cookieJar.getCookiesHeader("http://example.com"), "n1=v1; n2=v2") 136 | } 137 | 138 | export function setCookiesFromHeaderTest() { 139 | const cookieJar = new request.CookieJar() 140 | cookieJar.setCookiesFromHeader("http://example.com", "n1=v1") 141 | assert.strictEqual(cookieJar.getCookie("http://example.com", "n1"), "v1") 142 | 143 | cookieJar.setCookiesFromHeader("https://example.com", "n2=v2; Domain=example.com; Path=/; Max-Age=1; Expires=Wed, 30 Aug 2019 00:00:00 GM; HttpOnly; Secure") 144 | assert.strictEqual(cookieJar.getCookie("https://example.com", "n2"), "v2") 145 | 146 | cookieJar.setCookiesFromHeader("https://example.com", "n3=v3; Domain=example.com; Path=/; Expires=Wed, 30 Aug 2019 00:00:00 GM; HttpOnly; Secure") 147 | assert.strictEqual(cookieJar.getCookie("https://example.com", "n3"), "v3") 148 | } 149 | 150 | } 151 | 152 | } -------------------------------------------------------------------------------- /test/require.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as req from "../src/require" 3 | 4 | export namespace requireTest { 5 | 6 | export function transformESModuleToCommonJSTest() { 7 | assert.strictEqual(req.transformESModuleToCommonJS(`export var a = 1`), `var a = 1\nmodule.exports.a = a;`) 8 | 9 | assert.strictEqual(req.transformESModuleToCommonJS(`var a = 1`), `var a = 1`) 10 | assert.strictEqual(req.transformESModuleToCommonJS(`var a = "export var x"`), `var a = "export var x"`) 11 | 12 | assert.strictEqual(req.transformESModuleToCommonJS(`export let a = 1`), `let a = 1\nmodule.exports.a = a;`) 13 | assert.strictEqual(req.transformESModuleToCommonJS(`export const a = 1`), `const a = 1\nmodule.exports.a = a;`) 14 | assert.strictEqual(req.transformESModuleToCommonJS(`export function a() {}`), `function a() {}\nmodule.exports.a = a;`) 15 | assert.strictEqual(req.transformESModuleToCommonJS(`export async function a() {}`), `async function a() {}\nmodule.exports.a = a;`) 16 | assert.strictEqual(req.transformESModuleToCommonJS(`export function * a() {}`), `function * a() {}\nmodule.exports.a = a;`) 17 | assert.strictEqual(req.transformESModuleToCommonJS(`export function * a() {}`), `function * a() {}\nmodule.exports.a = a;`) 18 | assert.strictEqual(req.transformESModuleToCommonJS(`export function *a() {}`), `function *a() {}\nmodule.exports.a = a;`) 19 | assert.strictEqual(req.transformESModuleToCommonJS(`export async function *a() {}`), `async function *a() {}\nmodule.exports.a = a;`) 20 | 21 | assert.strictEqual(req.transformESModuleToCommonJS(`export default 1`), `module.exports.default = 1\nObject.defineProperty(module.exports, "__esModule", { value: true });`) 22 | assert.strictEqual(req.transformESModuleToCommonJS(`export default var a`), `var a\nObject.defineProperty(module.exports, "__esModule", { value: true });\nmodule.exports.default = a;`) 23 | 24 | assert.strictEqual(req.transformESModuleToCommonJS(`export * from "fs"`), `Object.assign(module.exports, require("fs"));`) 25 | assert.strictEqual(req.transformESModuleToCommonJS(`export {x} from "fs"`), `const {x} = require("fs"); Object.assign(module.exports, {x});`) 26 | assert.strictEqual(req.transformESModuleToCommonJS(`export {x as y} from "fs"`), `const {x : y} = require("fs"); Object.assign(module.exports, {x : y});`) 27 | 28 | assert.strictEqual(req.transformESModuleToCommonJS(`import "fs"`), `require("fs");`) 29 | assert.strictEqual(req.transformESModuleToCommonJS(`import * as fs from "fs"`), `const fs = require("fs");`) 30 | assert.strictEqual(req.transformESModuleToCommonJS(`import {readFile} from "fs"`), `const {readFile} = require("fs");`) 31 | assert.strictEqual(req.transformESModuleToCommonJS(`import {readFile as read} from "fs"`), `const {readFile : read} = require("fs");`) 32 | assert.strictEqual(req.transformESModuleToCommonJS(`import {readFile as read, writeFile} from "fs"`), `const {readFile : read, writeFile} = require("fs");`) 33 | 34 | assert.strictEqual(req.transformESModuleToCommonJS(`import fs from "fs"`), `const __fs = require("fs"), fs = __fs.__esModule ? __fs.default : __fs;`) 35 | assert.strictEqual(req.transformESModuleToCommonJS(`import fs, {readFile} from "fs"`), `const __fs = require("fs"), fs = __fs.__esModule ? __fs.default : __fs, {readFile} = __fs;`) 36 | assert.strictEqual(req.transformESModuleToCommonJS(`importfs from "fs"`), `importfs from "fs"`) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /test/textDocument.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as textDocument from "../src/textDocument" 3 | 4 | export namespace textDocumentTest { 5 | 6 | export function appendTest() { 7 | const writer = new textDocument.TextDocument("0123456789") 8 | writer.replace(1, 4, "BCD") 9 | assert.strictEqual(writer.toString(), "0BCD456789") 10 | 11 | writer.insert(5, "X") 12 | assert.strictEqual(writer.toString(), "0BCD4X56789") 13 | 14 | writer.remove(7, 8) 15 | assert.strictEqual(writer.toString(), "0BCD4X5689") 16 | 17 | writer.append("Y") 18 | assert.strictEqual(writer.toString(), "0BCD4X5689Y") 19 | 20 | writer.remove(0, 0) 21 | assert.strictEqual(writer.toString(), "0BCD4X5689Y") 22 | } 23 | 24 | export function appendFunctionTest() { 25 | const writer = new textDocument.TextDocument("0123456789") 26 | writer.replace(1, 4, (...args: any[]) => args.join("")) 27 | assert.strictEqual(writer.toString("B", "C", "D"), "0BCD456789") 28 | assert.strictEqual(writer.toString(), "0456789") 29 | 30 | writer.insert(5, new textDocument.TextDocument("X")) 31 | assert.strictEqual(writer.toString("B", "C", "D"), "0BCD4X56789") 32 | 33 | writer.remove(7, 8) 34 | assert.strictEqual(writer.toString("B", "C", "D"), "0BCD4X5689") 35 | 36 | writer.append((...args: any[]) => new textDocument.TextDocument(args.join(""))) 37 | assert.strictEqual(writer.toString("Y"), "0Y4X5689Y") 38 | } 39 | 40 | export function sourceMapTest() { 41 | const writer = new textDocument.TextDocument("0123456789", "sourcePath") 42 | writer.replace(1, 4, "X") 43 | assert.strictEqual(writer.generate().content, "0X456789") 44 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 0).sourcePath, "sourcePath") 45 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 0).line, 0) 46 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 0).column, 0) 47 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 0).sourcePath, "sourcePath") 48 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 5).sourcePath, "sourcePath") 49 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 5).line, 0) 50 | assert.strictEqual(writer.generate().sourceMapBuilder.getSource(0, 5).column, 4) 51 | assert.strictEqual(writer.generate().sourceMap.sources[0], "sourcePath") 52 | 53 | const writer2 = new textDocument.TextDocument("0123456789", "sourcePath") 54 | assert.strictEqual(writer2.generate().content, "0123456789") 55 | } 56 | 57 | export function spliceTest() { 58 | assert.strictEqual(textDocument.splice({ content: "ABC", path: "source" }, 2, 0, "").content, "ABC") 59 | assert.strictEqual(textDocument.splice({ content: "ABC", path: "source" }, 1, 1, "D").content, "ADC") 60 | 61 | assert.strictEqual(textDocument.splice({ content: "ABC", path: "source", sourceMap: textDocument.splice({ content: "ABC", path: "source" }, 2, 0, "").sourceMap }, 1, 1, "D").content, "ADC") 62 | } 63 | 64 | export function replaceTest() { 65 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, "C", "D").content, "ABCDEFGHABCDEFG".replace("C", "D")) 66 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, /[B]/, "*$&*").content, "ABCDEFGHABCDEFG".replace(/[B]/, "*$&*")) 67 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, /([B])/, "-$1-$2-").content, "ABCDEFGHABCDEFG".replace(/([B])/, "-$1-$2-")) 68 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, /([B])/g, "-$1-$2-").content, "ABCDEFGHABCDEFG".replace(/([B])/g, "-$1-$2-")) 69 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, /([B])/g, (all, source, index) => all + index + source).content, "ABCDEFGHABCDEFG".replace(/([B])/g, (all, source, index) => all + index + source)) 70 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, /([B])/g, null).content, "ABCDEFGHABCDEFG".replace(/([B])/g, null)) 71 | 72 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source", sourceMap: textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, "C", "D").sourceMap }, "C", "D").content, "ABCDEFGHABCDEFG".replace("C", "D")) 73 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source", sourceMap: textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, /([B])/g, null).sourceMap }, /([B])/g, null).content, "ABCDEFGHABCDEFG".replace(/([B])/g, null)) 74 | assert.strictEqual(textDocument.replace({ content: "ABCDEFGHABCDEFG", path: "source" }, "404", null).content, "ABCDEFGHABCDEFG".replace("404", null)) 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /test/tpl.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as tpl from "../src/tpl" 3 | 4 | export namespace tplTest { 5 | 6 | export async function compileTPLTest() { 7 | assert.strictEqual(tpl.compileTPL(`plain`)(), `plain`) 8 | assert.strictEqual(tpl.compileTPL(`
`)(), `
`) 9 | assert.strictEqual(tpl.compileTPL(`
{$}
`)(1), `
1
`) 10 | assert.strictEqual(tpl.compileTPL(`
{$.x}
`)({ x: 1 }), `
1
`) 11 | assert.strictEqual(tpl.compileTPL(`
{$.y}
`)({}), `
`) 12 | assert.strictEqual(tpl.compileTPL(`
{if($)}1{/if}
`)(true), `
1
`) 13 | assert.strictEqual(tpl.compileTPL(`
{if($)}1{/if}
`)(false), `
`) 14 | assert.strictEqual(tpl.compileTPL(`
{if($)}1{else}2{/if}
`)(true), `
1
`) 15 | assert.strictEqual(tpl.compileTPL(`
{if($)}1{else}2{/if}
`)(false), `
2
`) 16 | assert.strictEqual(tpl.compileTPL(`
{for(const p of $)}{p}{/if}
`)([1, 2, 3]), `
123
`) 17 | assert.strictEqual(tpl.compileTPL(`
{var i = 1}{while (i <= $)}{i++}{/while}
`)(3), `
123
`) 18 | assert.strictEqual(tpl.compileTPL(`
{const x = 1}{switch (x)}{case 1}1{break}{/switch}
`)(3), `
1
`) 19 | assert.strictEqual(tpl.compileTPL(`
{let x = 1}{try}1{catch}2{finally}3{/try}
`)(3), `
13
`) 20 | assert.strictEqual(tpl.compileTPL(`{function fn(x)}{x}{/function}{fn(1)}`)(3), `1`) 21 | assert.strictEqual(tpl.compileTPL(`1{}3`)(), `13`) 22 | assert.strictEqual(tpl.compileTPL(`{void 1}`)(), ``) 23 | assert.strictEqual(tpl.compileTPL(`{"<"}`)(), `<`) 24 | assert.strictEqual(tpl.compileTPL(`@{"<"}`)(), `<`) 25 | assert.strictEqual(tpl.compileTPL(`{'}'}`)(), `}`) 26 | assert.strictEqual(tpl.compileTPL(`{/}/}`)(), `/}/`) 27 | assert.strictEqual(tpl.compileTPL(`{\`\${/}/ /*}*/}$\\}\`}`)(), `/}/$}`) 28 | assert.strictEqual(tpl.compileTPL(`{{x: 2}["x"]`)(), `2`) 29 | assert.strictEqual(tpl.compileTPL(`div{1//}\n}2`)(), `div12`) 30 | assert.strictEqual(tpl.compileTPL(`{"\\\"}"}`)(), `\"}`) 31 | assert.strictEqual(tpl.compileTPL(`{1 / 2}`)(), `0.5`) 32 | assert.strictEqual(tpl.compileTPL(`{1 / 2 / 2}`)(), `0.25`) 33 | assert.strictEqual(tpl.compileTPL(`{import * as path from "path"}{path.normalize("")}`)(), `.`) 34 | assert.strictEqual(tpl.compileTPL(`1 {if (1)} 2 {/if} 3`)(), `1 2 3`) 35 | assert.strictEqual(tpl.compileTPL(`1\r\n {if (1)} 2 {/if}\r\n 3`)(), `1 2 \r\n 3`) 36 | assert.strictEqual(tpl.compileTPL(`1\n {if (1)}\n {2} {2}\n {/if}\n 3`)(), `1\n 2 2\n 3`) 37 | assert.strictEqual(tpl.compileTPL(` {switch (1)}\n {case 1} 1 {/if} `)(), ` 1 `) 38 | assert.strictEqual(await tpl.compileTPL(`{await 3}`, true)(), `3`) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /test/vm.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as vm from "../src/vm" 3 | 4 | export namespace vmTest { 5 | 6 | export function runInVMTest() { 7 | assert.strictEqual(vm.runInVM("1"), 1) 8 | assert.strictEqual(vm.runInVM("x", { x: 1 }), 1) 9 | 10 | assert.strictEqual(vm.runInVM("global.x", { x: 1 }), 1) 11 | assert.strictEqual(vm.runInVM("require('util').format('hi')"), "hi") 12 | assert.strictEqual(vm.runInVM("__filename", undefined, { filename: "a/b" }), "a/b") 13 | assert.strictEqual(vm.runInVM("__dirname", undefined, "a/b"), "a") 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /test/workerPool.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import * as workerPool from "../src/workerPool" 3 | 4 | export namespace workerPoolTest { 5 | 6 | var pool: workerPool.WorkerPool 7 | 8 | export async function afterEach() { 9 | if (pool) { 10 | pool.close() 11 | } 12 | } 13 | 14 | export async function execTest() { 15 | pool = new workerPool.WorkerPool(([taskName, x, y]: [string, number, number]) => { 16 | switch (taskName) { 17 | case "t1": 18 | return x + y 19 | case "t2": 20 | return Promise.resolve(x + y) 21 | case "t3": 22 | return new Promise(resolve => setTimeout(() => { resolve(x + y) }, 10)) 23 | case "t4": 24 | throw "ERROR" 25 | } 26 | }, { size: 2 }) 27 | await Promise.all([ 28 | (async () => { assert.strictEqual(await pool.exec(["t1", 1, 2]), 3) })(), 29 | (async () => { assert.strictEqual(await pool.exec(["t2", 1, 2]), 3) })(), 30 | (async () => { assert.strictEqual(await pool.exec(["t3", 1, 2]), 3) })(), 31 | (async () => { await assert.rejects(async () => { await pool.exec(["t4", 1, 2]) }) })(), 32 | ]) 33 | } 34 | 35 | export async function exitTest() { 36 | pool = new workerPool.WorkerPool(() => { process.exit(3) }, { size: 1 }) 37 | if (pool.size <= 0) { 38 | return 39 | } 40 | await Promise.all([ 41 | (async () => { await assert.rejects(async () => { await pool.exec("t2") }) })(), 42 | (async () => { await assert.rejects(async () => { await pool.exec("t2") }) })() 43 | ]) 44 | } 45 | 46 | export async function errorTest() { 47 | pool = new workerPool.WorkerPool(() => { throw "ERROR" }, { size: 1 }) 48 | if (pool.size <= 0) { 49 | return 50 | } 51 | await Promise.all([ 52 | (async () => { await assert.rejects(async () => { await pool.exec("t2") }) })(), 53 | (async () => { await assert.rejects(async () => { await pool.exec("t2") }) })() 54 | ]) 55 | } 56 | 57 | export async function cannotCloneTest() { 58 | pool = new workerPool.WorkerPool(() => { throw new Error("E") }, { size: 1 }) 59 | if (pool.size <= 0) { 60 | return 61 | } 62 | await assert.rejects(async () => { await pool.exec() }) 63 | await assert.rejects(async () => { await pool.exec(new Error("E")) }) 64 | } 65 | 66 | export async function simulateTest() { 67 | new workerPool.WorkerPool(() => { }).close() 68 | 69 | pool = new workerPool.WorkerPool(([taskName, x, y]: [string, number, number]) => { 70 | switch (taskName) { 71 | case "t1": 72 | return x + y 73 | case "t2": 74 | return Promise.resolve(x + y) 75 | case "t3": 76 | return new Promise(resolve => setTimeout(() => { resolve(x + y) }, 10)) 77 | case "t4": 78 | throw "ERROR" 79 | } 80 | }, { size: 0 }) 81 | await Promise.all([ 82 | (async () => { assert.strictEqual(await pool.exec(["t1", 1, 2]), 3) })(), 83 | (async () => { assert.strictEqual(await pool.exec(["t2", 1, 2]), 3) })(), 84 | (async () => { assert.strictEqual(await pool.exec(["t3", 1, 2]), 3) })(), 85 | (async () => { await assert.rejects(async () => { await pool.exec(["t4", 1, 2]) }) })(), 86 | ]) 87 | } 88 | 89 | export async function callTest() { 90 | pool = new workerPool.WorkerPool(async (data: any, context: workerPool.WorkerContext) => { 91 | return await context.call("hello", 3) 92 | }, { size: 2, functions: { hello(x: number) { return x - 2 } } }) 93 | assert.strictEqual(await pool.exec(), 1) 94 | } 95 | 96 | export async function callTest2() { 97 | pool = new workerPool.WorkerPool(async (data: any, context: workerPool.WorkerContext) => { 98 | return await context.call("hello", 3) 99 | }, { size: 2, functions: { hello(x: number) { throw "ERROR" } } }) 100 | await assert.rejects(async () => { await pool.exec() }) 101 | } 102 | 103 | export async function callTest3() { 104 | pool = new workerPool.WorkerPool(async (data: any, context: workerPool.WorkerContext) => { 105 | return await context.call("hello", 3) 106 | }, { size: -1, functions: { hello(x: number) { return x - 2 } } }) 107 | assert.strictEqual(await pool.exec(), 1) 108 | } 109 | 110 | export async function callTest4() { 111 | pool = new workerPool.WorkerPool(async (data: any, context: workerPool.WorkerContext) => { 112 | return await context.call("hello", 3) 113 | }, { size: -1, functions: { hello(x: number) { throw "ERROR" } } }) 114 | await assert.rejects(async () => { await pool.exec() }) 115 | } 116 | 117 | export async function callTest5() { 118 | pool = new workerPool.WorkerPool(async (data: any, context: workerPool.WorkerContext) => { 119 | return await context.call("hello", undefined) 120 | }, { size: 2, functions: { hello() { throw new Error("E") } } }) 121 | await assert.rejects(async () => { await pool.exec(new Error("E")) }) 122 | await assert.rejects(async () => { await pool.exec() }) 123 | } 124 | 125 | export function isStructuredCloneableTest() { 126 | assert.strictEqual(workerPool.isStructuredCloneable({ x: 1, y: [1, 2] }), true) 127 | 128 | assert.strictEqual(workerPool.isStructuredCloneable(true), true) 129 | assert.strictEqual(workerPool.isStructuredCloneable(false), true) 130 | assert.strictEqual(workerPool.isStructuredCloneable(0), true) 131 | assert.strictEqual(workerPool.isStructuredCloneable("string"), true) 132 | assert.strictEqual(workerPool.isStructuredCloneable(null), true) 133 | assert.strictEqual(workerPool.isStructuredCloneable(undefined), true) 134 | assert.strictEqual(workerPool.isStructuredCloneable([1, 2]), true) 135 | assert.strictEqual(workerPool.isStructuredCloneable(() => { }), false) 136 | assert.strictEqual(workerPool.isStructuredCloneable(new Set([1, 2])), true) 137 | assert.strictEqual(workerPool.isStructuredCloneable(new Map([["x", 1], ["y", 2]])), true) 138 | assert.strictEqual(workerPool.isStructuredCloneable({ x: 1, y: 2 }), true) 139 | assert.strictEqual(workerPool.isStructuredCloneable({ foo() { } }), false) 140 | assert.strictEqual(workerPool.isStructuredCloneable(new Set([1, () => { }])), false) 141 | assert.strictEqual(workerPool.isStructuredCloneable(new Map([[() => { }, () => { }]])), false) 142 | assert.strictEqual(workerPool.isStructuredCloneable(new class { }), false) 143 | 144 | assert.strictEqual(workerPool.isStructuredCloneable(new Date()), true) 145 | assert.strictEqual(workerPool.isStructuredCloneable(/re/), true) 146 | assert.strictEqual(workerPool.isStructuredCloneable(Buffer.alloc(0)), true) 147 | assert.strictEqual(workerPool.isStructuredCloneable(new Uint16Array(0)), true) 148 | assert.strictEqual(workerPool.isStructuredCloneable(new Uint8Array(0)), true) 149 | assert.strictEqual(workerPool.isStructuredCloneable(new ArrayBuffer(0)), true) 150 | assert.strictEqual(workerPool.isStructuredCloneable(new SharedArrayBuffer(0)), true) 151 | assert.strictEqual(workerPool.isStructuredCloneable(new String("0")), true) 152 | assert.strictEqual(workerPool.isStructuredCloneable(new Number(false)), true) 153 | assert.strictEqual(workerPool.isStructuredCloneable(new Boolean(false)), true) 154 | assert.strictEqual(workerPool.isStructuredCloneable(Symbol("symbol")), false) 155 | assert.strictEqual(workerPool.isStructuredCloneable(BigInt(0)), true) 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /test/zipFile.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert" 2 | import { existsSync } from "fs" 3 | import * as zipFile from "../src/zipFile" 4 | import { check, init, rootDir, simulateIOError, uninit } from "./helpers/fsHelper" 5 | 6 | export namespace zipFileTest { 7 | 8 | export async function beforeEach() { 9 | await init({ 10 | "dir": { 11 | "sub1": { 12 | "f3.txt": "f3.txt", 13 | "f4.txt": "f4.txt" 14 | }, 15 | "sub2": { 16 | "f5.txt": "f5.txt" 17 | } 18 | }, 19 | "empty-dir": {}, 20 | "f1.txt": "f1.txt", 21 | "f2.txt": "f2.txt" 22 | }) 23 | } 24 | 25 | export async function afterEach() { 26 | await uninit() 27 | } 28 | 29 | export function basicTest() { 30 | zipFile.compressFolder("dir") 31 | assert.ok(existsSync("dir.zip")) 32 | zipFile.extractZip("dir.zip", "dir2") 33 | check({ 34 | "dir2": { 35 | "sub1": { 36 | "f3.txt": "f3.txt", 37 | "f4.txt": "f4.txt" 38 | }, 39 | "sub2": { 40 | "f5.txt": "f5.txt" 41 | } 42 | } 43 | }) 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2018", 5 | "module": "CommonJS", 6 | "newLine": "LF", 7 | "useDefineForClassFields": false, 8 | "experimentalDecorators": true, 9 | "preserveConstEnums": true, 10 | "esModuleInterop": true, 11 | "stripInternal": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "allowUmdGlobalAccess": true 16 | }, 17 | "include": [ 18 | "./src" 19 | ] 20 | } --------------------------------------------------------------------------------