| /* eslint-disable unused-imports/no-unused-vars */ | ||
| const net = require('node:net') | ||
| const process = require('node:process') | ||
| // noinspection JSUnusedLocalSymbols | ||
| const http = require('node:http') | ||
| /** | ||
| * @type {http.Server[]} | ||
| */ | ||
| const services = [] | ||
| function hookNetServer() { | ||
| const oldCreateServer = net.createServer | ||
| net.createServer = (...args) => { | ||
| const server = oldCreateServer.call(this, ...args) | ||
| services.push(server) | ||
| return server | ||
| } | ||
| } | ||
| function hookHttpServer() { | ||
| const http = require('node:http') | ||
| const oldCreateServer = http.createServer | ||
| http.createServer = (...args) => { | ||
| const service = oldCreateServer.call(this, ...args) | ||
| services.push(service) | ||
| return service | ||
| } | ||
| } | ||
| async function closeServices() { | ||
| let count = 0 | ||
| process.on('uncaughtException', (error) => { | ||
| console.error('Uncaught Exception:', error) | ||
| }) | ||
| for (const index in services) { | ||
| const service = services[index] | ||
| await new Promise((resolve, reject) => { | ||
| try { | ||
| // noinspection JSUnresolvedReference | ||
| service.closeAllConnections() | ||
| // noinspection JSUnresolvedReference | ||
| service.closeIdleConnections() | ||
| // noinspection JSUnresolvedReference | ||
| service.close((err) => { | ||
| if (err) { | ||
| reject(err) | ||
| return | ||
| } | ||
| count++ | ||
| resolve() | ||
| }) | ||
| } | ||
| catch (e) { | ||
| console.error(e) | ||
| } | ||
| resolve() | ||
| }).catch((e) => { | ||
| console.error(e) | ||
| }) | ||
| } | ||
| services.splice(0, services.length) | ||
| return count | ||
| } | ||
| function hookServices() { | ||
| hookNetServer() | ||
| hookHttpServer() | ||
| } | ||
| async function unblocks() { | ||
| return await closeServices() | ||
| } | ||
| module.exports.hookServices = hookServices | ||
| module.exports.unblocks = unblocks |
| const http = require('node:http') | ||
| module.exports = http |
| const net = require('node:net') | ||
| module.exports = net |
| const url = require('node:url') | ||
| function getHookNetUrl() { | ||
| return url.pathToFileURL(require.resolve('./hook-net.cjs')).toString() | ||
| } | ||
| function getHookHttpUrl() { | ||
| return url.pathToFileURL(require.resolve('./hook-http.cjs')).toString() | ||
| } | ||
| module.exports.getHookNetUrl = getHookNetUrl | ||
| module.exports.getHookHttpUrl = getHookHttpUrl |
| const url = require('node:url') | ||
| const path = require('node:path') | ||
| const loaders = [ | ||
| ['--require', './hmr-proxy.cjs'], | ||
| ['--import', './loaders/loader-block.mjs'], | ||
| ] | ||
| function getLoaderUrl(includeType, pathname) { | ||
| if (includeType === '--require') | ||
| return path.join(__dirname, pathname) | ||
| else | ||
| return url.pathToFileURL(require.resolve(pathname)).toString() | ||
| } | ||
| function getLoaderArgs() { | ||
| const args = [] | ||
| for (const loader of loaders) { | ||
| const [includeType, pathname] = loader | ||
| args.push(includeType, getLoaderUrl(includeType, pathname)) | ||
| } | ||
| return args | ||
| } | ||
| function getSpawnArgs(oldArgs) { | ||
| const newArgs = [...oldArgs] | ||
| newArgs.splice(0, 0, ...getLoaderArgs()) | ||
| return newArgs | ||
| } | ||
| class Debounce { | ||
| constructor(func, delay) { | ||
| this.func = func | ||
| this.delay = delay | ||
| this.timeout = null | ||
| } | ||
| // noinspection JSUnusedGlobalSymbols | ||
| call(...args) { | ||
| if (this.timeout) | ||
| clearTimeout(this.timeout) | ||
| this.timeout = setTimeout(() => { | ||
| this.func(...args) | ||
| }, this.delay) | ||
| } | ||
| } | ||
| module.exports.Debounce = Debounce | ||
| module.exports.getSpawnArgs = getSpawnArgs | ||
| module.exports.getLoaderArgs = getLoaderArgs |
+10
-8
| #!/usr/bin/env node | ||
| const childProcess = require('child_process'); | ||
| const oldSpawn = childProcess.spawn; | ||
| const childProcess = require('node:child_process') | ||
| const oldSpawn = childProcess.spawn | ||
| // noinspection JSValidateTypes | ||
@@ -9,11 +10,12 @@ childProcess.spawn = (command, args, options) => { | ||
| */ | ||
| const newArgs = require('./util.js').getSpawnArgs(args); | ||
| const result = oldSpawn.call(this, command, newArgs, options); | ||
| const newArgs = require('./util.cjs').getSpawnArgs(args) | ||
| const result = oldSpawn.call(this, command, newArgs, options) | ||
| /** | ||
| * After launching `spawn` in the TSX, there is no need to hook again. To avoid any adverse effects, revert the `spawn`. | ||
| */ | ||
| childProcess.spawn = oldSpawn; | ||
| return result; | ||
| }; | ||
| childProcess.spawn = oldSpawn | ||
| return result | ||
| } | ||
| import('tsx/cli'); | ||
| // noinspection NpmUsedModulesInstalled | ||
| import('tsx/cli') |
+55
-52
@@ -0,77 +1,82 @@ | ||
| const process = require('node:process') | ||
| /** | ||
| * @type {string} | ||
| */ | ||
| const pathname = process.argv.slice(1)[0]; | ||
| const pathname = process.argv.slice(1)[0] | ||
| /** | ||
| * @type {{[filename: string]: string[]}} | ||
| */ | ||
| const relationModuleFilenames = {}; | ||
| const relationModuleFilenames = {} | ||
| /** | ||
| * @type {string[]} | ||
| */ | ||
| const aliveFilenames = [pathname]; | ||
| const aliveFilenames = [pathname] | ||
| const Module = require('node:module'); | ||
| const Module = require('node:module') | ||
| // noinspection JSUnresolvedReference | ||
| const oldLoad = Module._load; | ||
| const oldLoad = Module._load | ||
| Module._load = function (request, parent, isMain) { | ||
| const loaded = oldLoad.call(this, request, parent, isMain); | ||
| const loaded = oldLoad.call(this, request, parent, isMain) | ||
| if (parent) { | ||
| const { filename, children } = parent; | ||
| if (!aliveFilenames.includes(filename)) { | ||
| aliveFilenames.push(filename); | ||
| } | ||
| const { filename, children } = parent | ||
| if (!aliveFilenames.includes(filename)) | ||
| aliveFilenames.push(filename) | ||
| /** | ||
| * @type {string[]} | ||
| */ | ||
| const newFilenames = children.map(child => { | ||
| if (!aliveFilenames.includes(child.filename)) { | ||
| aliveFilenames.push(child.filename); | ||
| } | ||
| return child.filename; | ||
| }); | ||
| const newFilenames = children.map((child) => { | ||
| if (!aliveFilenames.includes(child.filename)) | ||
| aliveFilenames.push(child.filename) | ||
| return child.filename | ||
| }) | ||
| if (!relationModuleFilenames[filename]) { | ||
| relationModuleFilenames[filename] = newFilenames; | ||
| } else { | ||
| relationModuleFilenames[filename] = newFilenames | ||
| } | ||
| else { | ||
| /** | ||
| * @type {string[]} | ||
| */ | ||
| const oldFilenames = relationModuleFilenames[filename]; | ||
| relationModuleFilenames[filename] = [...new Set([...newFilenames, ...oldFilenames])]; | ||
| const oldFilenames = relationModuleFilenames[filename] | ||
| relationModuleFilenames[filename] = [...new Set([...newFilenames, ...oldFilenames])] | ||
| } | ||
| } | ||
| return loaded; | ||
| }; | ||
| return loaded | ||
| } | ||
| const cleanModuleCache = filename => { | ||
| function cleanModuleCache(filename) { | ||
| // noinspection JSUnresolvedReference | ||
| delete Module._cache[filename]; | ||
| delete relationModuleFilenames[filename]; | ||
| const aliveIndex = aliveFilenames.indexOf(filename); | ||
| if (aliveIndex !== -1) { | ||
| aliveFilenames.splice(aliveIndex, 1); | ||
| } | ||
| delete Module._cache[filename] | ||
| delete relationModuleFilenames[filename] | ||
| const aliveIndex = aliveFilenames.indexOf(filename) | ||
| if (aliveIndex !== -1) | ||
| aliveFilenames.splice(aliveIndex, 1) | ||
| for (const parentFilename in relationModuleFilenames) { | ||
| const developFilenames = relationModuleFilenames[parentFilename]; | ||
| if (developFilenames.includes(filename)) { | ||
| cleanModuleCache(parentFilename); | ||
| } | ||
| const developFilenames = relationModuleFilenames[parentFilename] | ||
| if (developFilenames.includes(filename)) | ||
| cleanModuleCache(parentFilename) | ||
| } | ||
| }; | ||
| } | ||
| const { Debounce } = require('./util.js'); | ||
| const { hookServices, unblocks } = require('./hmr-hook.js'); | ||
| hookServices(); | ||
| const debouncedHmrFunc = new Debounce(freshFilename => { | ||
| const chokidar = require('chokidar') | ||
| const { Debounce } = require('./util.cjs') | ||
| const { hookServices, unblocks } = require('./hmr-hook.cjs') | ||
| hookServices() | ||
| const debouncedHmrFunc = new Debounce((freshFilename) => { | ||
| unblocks().then(() => { | ||
| cleanModuleCache(freshFilename); | ||
| cleanModuleCache(freshFilename) | ||
| try { | ||
| aliveFilenames.push(pathname); | ||
| require(pathname); | ||
| } catch (e) { | ||
| console.error(e); | ||
| aliveFilenames.push(pathname) | ||
| require(pathname) | ||
| } | ||
| }); | ||
| }, 200); | ||
| catch (e) { | ||
| console.error(e) | ||
| } | ||
| }) | ||
| }, 200) | ||
@@ -81,8 +86,6 @@ /** | ||
| */ | ||
| const chokidar = require('chokidar'); | ||
| const chokidarWatcher = chokidar.watch(process.cwd(), { ignored: /node_modules/ }); | ||
| chokidarWatcher.on('change', freshFilename => { | ||
| if (aliveFilenames.includes(freshFilename)) { | ||
| debouncedHmrFunc.call(freshFilename); | ||
| } | ||
| }); | ||
| const chokidarWatcher = chokidar.watch(process.cwd(), { ignored: /node_modules/ }) | ||
| chokidarWatcher.on('change', (freshFilename) => { | ||
| if (aliveFilenames.includes(freshFilename)) | ||
| debouncedHmrFunc.call(freshFilename) | ||
| }) |
@@ -1,2 +0,2 @@ | ||
| import { getHookHttpUrl, getHookNetUrl } from './util.js'; | ||
| import { getHookHttpUrl, getHookNetUrl } from './util.cjs' | ||
@@ -9,13 +9,12 @@ // noinspection JSUnusedGlobalSymbols | ||
| */ | ||
| let newSpecifier = specifier; | ||
| if (newSpecifier.startsWith('node:')) { | ||
| newSpecifier = newSpecifier.slice('node:'.length); | ||
| } | ||
| if (newSpecifier === 'net') { | ||
| specifier = getHookNetUrl(); | ||
| } else if (newSpecifier === 'http') { | ||
| specifier = getHookHttpUrl(); | ||
| } | ||
| let newSpecifier = specifier | ||
| if (newSpecifier.startsWith('node:')) | ||
| newSpecifier = newSpecifier.slice('node:'.length) | ||
| if (newSpecifier === 'net') | ||
| specifier = getHookNetUrl() | ||
| else if (newSpecifier === 'http') | ||
| specifier = getHookHttpUrl() | ||
| } | ||
| return defaultResolve(specifier, context, defaultResolve); | ||
| return defaultResolve(specifier, context, defaultResolve) | ||
| } |
+10
-14
| # tsxx <a href="https://npm.im/tsxx"><img src="https://badgen.net/npm/v/tsxx"></a> <a href="https://npm.im/tsxx"><img src="https://badgen.net/npm/dm/tsxx"></a> <a href="https://packagephobia.now.sh/result?p=tsxx"><img src="https://packagephobia.now.sh/badge?p=tsxx"></a> | ||
| install: `npm i -g tsxx` or `npm i tsxx` | ||
| real node hmr | ||
| usage: `tsxx ./some-script.ts` | ||
| [read tsx README.md](https://github.com/esbuild-kit/tsx/tree/develop) | ||
| --- | ||
| <div style="color: #e82274; font-size: 1.2rem;"> | ||
| 中文解释: | ||
| </div> | ||
| global install: | ||
| ``` | ||
| npm i -g tsxx | ||
| ``` | ||
| tsxx 是基于 tsx 开发的 nodejs runtime hmr。使用方法与 tsx 完全一致。使用时相对于 tsx 默认开启 hmr 功能。帮助你大幅度增速程序重启。 | ||
| <div style="color: #e82274; font-size: 1.2rem"> | ||
| en description: | ||
| </div> | ||
| tsxx is a Node.js runtime HMR (Hot Module Replacement) tool developed based on TSX. Its usage is completely identical to TSX. When using it, HMR functionality is enabled by default compared to TSX, helping you significantly speed up program restarts. | ||
| [read tsx README](https://github.com/esbuild-kit/tsx/tree/develop) | ||
| now, you can use `tsxx` command in your terminal. | ||
| ``` | ||
| tsxx ./src/index.ts | ||
| ``` |
+6
-6
@@ -1,4 +0,4 @@ | ||
| The MIT License (MIT) | ||
| MIT License | ||
| Copyright (c) 2016 Christian Speckner <cnspeckn@googlemail.com> | ||
| Copyright (c) 2022 Anthony Fu <https://github.com/antfu> | ||
@@ -12,4 +12,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| The above copyright notice and this permission notice shall be included in | ||
| all copies or substantial portions of the Software. | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
@@ -21,3 +21,3 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
+44
-18
| { | ||
| "name": "tsxx", | ||
| "version": "1.0.18", | ||
| "description": "tsx + hmr for node js", | ||
| "type": "module", | ||
| "version": "1.1.0", | ||
| "description": "true node hmr", | ||
| "author": "Anthony Fu <anthonyfu117@hotmail.com>", | ||
| "license": "MIT", | ||
| "funding": "https://github.com/sponsors/sia-fl", | ||
| "homepage": "https://github.com/sia-fl/tsxx#readme", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/sia-fl/tsxx" | ||
| "url": "git+https://github.com/sia-fl/tsxx.git" | ||
| }, | ||
| "scripts": { | ||
| "build": "node build.mjs", | ||
| "lint": "prettier --write .", | ||
| "test": "vitest" | ||
| "bugs": "https://github.com/sia-fl/tsxx/issues", | ||
| "keywords": [], | ||
| "sideEffects": false, | ||
| "bin": { | ||
| "tsxx": "./dist/cli.cjs" | ||
| }, | ||
@@ -17,16 +23,36 @@ "files": [ | ||
| ], | ||
| "bin": { | ||
| "tsxx": "dist/cli.cjs" | ||
| }, | ||
| "keywords": ["hmr", "node", "tsx", "esbuild", "cjs", "esm"], | ||
| "author": "杨伟杰 <sia-fl@outlook.com> (https://github.com/sia-fl)", | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "chokidar": "^3.5.3", | ||
| "tsx": "^3.12.8" | ||
| "@types/kill-port": "^2.0.3", | ||
| "@types/signale": "^1.4.7", | ||
| "chokidar": "^3.6.0", | ||
| "kill-port": "^2.0.1", | ||
| "signale": "^1.4.0", | ||
| "tsx": "^4.7.3" | ||
| }, | ||
| "devDependencies": { | ||
| "prettier": "^3.0.3", | ||
| "vitest": "^0.34.5" | ||
| "@antfu/eslint-config": "^2.14.0", | ||
| "@antfu/ni": "^0.21.12", | ||
| "@antfu/utils": "^0.7.7", | ||
| "@types/node": "^20.12.7", | ||
| "eslint": "^9.0.0", | ||
| "lint-staged": "^15.2.2", | ||
| "pnpm": "^9.0.2", | ||
| "rimraf": "^5.0.5", | ||
| "simple-git-hooks": "^2.11.1", | ||
| "typescript": "^5.4.5", | ||
| "vite": "^5.2.9", | ||
| "vitest": "^1.5.0" | ||
| }, | ||
| "simple-git-hooks": { | ||
| "pre-commit": "pnpm lint-staged" | ||
| }, | ||
| "lint-staged": { | ||
| "*": "eslint --fix" | ||
| }, | ||
| "scripts": { | ||
| "build": "rimraf dist && node build.mjs", | ||
| "lint": "eslint .", | ||
| "test": "vitest", | ||
| "typecheck": "tsc --noEmit" | ||
| } | ||
| } | ||
| } |
+1
-22
| # tsxx <a href="https://npm.im/tsxx"><img src="https://badgen.net/npm/v/tsxx"></a> <a href="https://npm.im/tsxx"><img src="https://badgen.net/npm/dm/tsxx"></a> <a href="https://packagephobia.now.sh/result?p=tsxx"><img src="https://packagephobia.now.sh/badge?p=tsxx"></a> | ||
| tsxx is a Node.js runtime HMR (Hot Module Replacement) tool developed based on tsx. Its usage is completely identical to tsx. When using it, HMR functionality is enabled by default compared to tsx, helping you significantly speed up program restarts. | ||
| real node hmr | ||
@@ -9,7 +9,2 @@ [read tsx README.md](https://github.com/esbuild-kit/tsx/tree/develop) | ||
| install to npm devDependencies : | ||
| ``` | ||
| npm i -D tsxx | ||
| ``` | ||
| global install: | ||
@@ -24,17 +19,1 @@ ``` | ||
| ``` | ||
| usage: | ||
| ```json | ||
| { | ||
| "scripts": { | ||
| "dev": "tsxx ./src/index.tsx" | ||
| } | ||
| } | ||
| ``` | ||
| # warning ⚠️⚠️⚠️⚠️ | ||
| not set `"type": "module"` in `package.json`, if you use `.js`, change file extension to `.mjs` or `.cjs` | ||
| . | ||
| determining the file type helps tsx better address import issues. |
| const net = require('net'); | ||
| const http = require('http'); | ||
| /** | ||
| * @type {http.Server[]} | ||
| */ | ||
| const services = []; | ||
| const hookNetServer = () => { | ||
| const oldCreateServer = net.createServer; | ||
| net.createServer = (...args) => { | ||
| const server = oldCreateServer.call(this, ...args); | ||
| services.push(server); | ||
| return server; | ||
| }; | ||
| }; | ||
| const hookHttpServer = () => { | ||
| const http = require('http'); | ||
| const oldCreateServer = http.createServer; | ||
| http.createServer = (...args) => { | ||
| const server = oldCreateServer.call(this, ...args); | ||
| services.push(server); | ||
| return server; | ||
| }; | ||
| }; | ||
| const closeServices = async () => { | ||
| let count = 0; | ||
| process.on('uncaughtException', error => { | ||
| console.error('Uncaught Exception:', error); | ||
| }); | ||
| for (let index in services) { | ||
| const service = services[index]; | ||
| await new Promise((resolve, reject) => { | ||
| try { | ||
| // noinspection JSUnresolvedReference | ||
| service.closeAllConnections(); | ||
| // noinspection JSUnresolvedReference | ||
| service.closeIdleConnections(); | ||
| // noinspection JSUnresolvedReference | ||
| service.close(err => { | ||
| if (err) { | ||
| reject(err); | ||
| return; | ||
| } | ||
| count++; | ||
| resolve(); | ||
| }); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } | ||
| resolve(); | ||
| }).catch(e => { | ||
| console.error(e); | ||
| }); | ||
| } | ||
| services.splice(0, services.length); | ||
| return count; | ||
| }; | ||
| const hookServices = () => { | ||
| hookNetServer(); | ||
| hookHttpServer(); | ||
| }; | ||
| const unblocks = async () => { | ||
| return await closeServices(); | ||
| }; | ||
| module.exports.hookServices = hookServices; | ||
| module.exports.unblocks = unblocks; |
| const http = require('http'); | ||
| module.exports = http; |
| const net = require('net'); | ||
| module.exports = net; |
| const url = require('url'); | ||
| const getHookNetUrl = () => { | ||
| return url.pathToFileURL(require.resolve('./hook-net.js')).toString(); | ||
| }; | ||
| const getHookHttpUrl = () => { | ||
| return url.pathToFileURL(require.resolve('./hook-http.js')).toString(); | ||
| }; | ||
| module.exports.getHookNetUrl = getHookNetUrl; | ||
| module.exports.getHookHttpUrl = getHookHttpUrl; |
-51
| const url = require('url'); | ||
| const path = require('path'); | ||
| const loaders = [ | ||
| ['--require', './hmr-proxy.cjs'], | ||
| ['--loader', './loaders/loader-block.mjs'] | ||
| ]; | ||
| const getLoaderUrl = (includeType, pathname) => { | ||
| if (includeType === '--require') { | ||
| return path.join(__dirname, pathname); | ||
| } else { | ||
| return url.pathToFileURL(require.resolve(pathname)).toString(); | ||
| } | ||
| }; | ||
| const getLoaderArgs = () => { | ||
| const args = []; | ||
| for (const loader of loaders) { | ||
| const [includeType, pathname] = loader; | ||
| args.push(includeType, getLoaderUrl(includeType, pathname)); | ||
| } | ||
| return args; | ||
| }; | ||
| const getSpawnArgs = oldArgs => { | ||
| const newArgs = [...oldArgs]; | ||
| newArgs.splice(0, 0, ...getLoaderArgs()); | ||
| return newArgs; | ||
| }; | ||
| class Debounce { | ||
| constructor(func, delay) { | ||
| this.func = func; | ||
| this.delay = delay; | ||
| this.timeout = null; | ||
| } | ||
| call(...args) { | ||
| if (this.timeout) { | ||
| clearTimeout(this.timeout); | ||
| } | ||
| this.timeout = setTimeout(() => { | ||
| this.func(...args); | ||
| }, this.delay); | ||
| } | ||
| } | ||
| module.exports.Debounce = Debounce; | ||
| module.exports.getSpawnArgs = getSpawnArgs; | ||
| module.exports.getLoaderArgs = getLoaderArgs; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No website
QualityPackage does not have a website.
10724
0.01%0
-100%Yes
NaN6
200%12
500%18
-53.85%7
16.67%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated