diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ed1672 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..90b9258 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "ignorePatterns": ["tests/samples/*.js","samples/*.generated.ts"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "quotes": "off", + "no-tabs": "off", + "indent": ["error","tab"], + "padded-blocks": "off", + "space-before-function-paren": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": ["error","nofunc"], + "no-debugger": "warn", + "comma-dangle": "off" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 659b03e..639a2fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [14.x, 16.x, 17.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/CHANGELOG b/CHANGELOG index fec80c5..16bd288 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ -1.x (20??/??/??) -- Move reload logic to own method +1.1.1 (2022/02/09) +- Support for Browser-Sync based communication (since pcf-start 1.11.4) +- Major refactoring, move injected code to external library that gets imported into the class instead of embedding all the code. 1.0.1 (2021/11/05) - Fix: Skip files that have the PcfParams type declared to avoid double-generating code. (Issue #1) @@ -7,4 +8,4 @@ - Cleanup: Use access helper method everywhere 1.0.0 (2021/11/03) -- Initial version \ No newline at end of file +- Initial version diff --git a/README.md b/README.md index 75fd5db..4ec327c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Typescript transformation that enabled automatic reloading of PCF Components whe - [Integrating](#integrating) - [Fiddler](#fiddler) - [Settings](#settings) - - [Example](#example) + ## Motivation The PowerApps Component Framework includes a test-harness which automatically reloads the component whenever it changes on disk. @@ -58,19 +58,20 @@ To run the transformer, add it to _plugins_ in your _tsconfig.json_ ### Integrating The transformation injects code in any class that implements `ComponentFramework.StandardControl`. -The code will set up a listener to a websocket, and when receiving a message on the socket, will unload the PCF, reload the `bundle.js`, and re-initialize the PCF. +The code will listen for messages passed to the [PCF Test Harness](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/debugging-custom-controls#debugging-using-the-browser-test-harness), unload the PCF, reload the `bundle.js`, and re-initialize the PCF with the current context. -The code expects the PCF test harness to be running, as such, to use the transformation, after compiling, start the PCF test harness in watch-mode. +The code expects the [PCF test harness](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/debugging-custom-controls#debugging-using-the-browser-test-harness) to be running in watch-mode. #### Fiddler The easiest way to get the updated bundle on the form is to inject it using Fiddler. -After publishing the component for the first time, add two AutoResponder rules on the following format: +After publishing the component for the first time, an AutoResponder rule on the following format: | If request matches... | then respond with... |---------------------- |--------------------- | REGEX:(?insx).+\/WebResources\/cc_(?'namespace'[^.]+)\.([^/]+)\/bundle.js | __sourcedir__\${namespace}\out\controls\${namespace}\bundle.js -| REGEX:(?insx).+\/WebResources\/cc_(?'namespace'[^.]+)\.([^/]+)\/bundle.js | *header:Cache-Control: must-revalidate + +For details on setting up Fiddler for PCF debugging, see [Microsoft Docs](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/debugging-custom-controls#using-fiddler). ### Settings The transformer supports the following configuration settings. @@ -81,4 +82,5 @@ The settings are specified as part of the plugin specification in _tsconfig.json |-------- |------ |------------- |--------- | printGenerated | boolean | If `true`, the generated typescript code will be output to a file alongside the detected file. If the file is named `index.ts`, the generated file will be `index.generated.ts` | `false` | verbose | boolean | If `true`, status messages will be printed during the transformation | `false` -| wsAddress | string | The address to use when listening for update messages. | `ws://127.0.0.1:8181/ws` +| wsAddress | string | The address to use when listening for update messages. | For websocket: `ws://127.0.0.1:8181/ws`
For BrowserSync: `http://localhost:8181` +| useBrowserSync | boolean | If `true` use the BrowserSync.io / Socket.io based integration, otherwise use a raw websocket
Use BrowserSync when running against PCF Start > 1.11 | `true` diff --git a/jest.config.js b/jest.config.js index 7e9194c..b5d961e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,9 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +// eslint-disable-next-line no-undef module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverage: true, - collectCoverageFrom: ["src/**/*.ts"], - coverageDirectory: 'coverage' + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverage: true, + collectCoverageFrom: ["src/**/*.ts"], + coverageDirectory: 'coverage' }; diff --git a/package-lock.json b/package-lock.json index ee17f68..7f96ea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "pcf-reloader", - "version": "1.0.0", + "name": "pcf-reloader-transformer", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -457,6 +457,78 @@ "@cspotcode/source-map-consumer": "0.8.0" } }, + "@eslint/eslintrc": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz", + "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.2.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz", + "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -667,6 +739,32 @@ "chalk": "^4.0.0" } }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -799,6 +897,18 @@ "pretty-format": "^27.0.0" } }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/node": { "version": "16.11.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.5.tgz", @@ -838,6 +948,141 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.2.tgz", + "integrity": "sha512-4W/9lLuE+v27O/oe7hXJKjNtBLnZE8tQAFpapdxwSVHqtmIoPB1gph3+ahNwVuNL37BX7YQHyGF9Xv6XCnIX2Q==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.10.2", + "@typescript-eslint/type-utils": "5.10.2", + "@typescript-eslint/utils": "5.10.2", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.10.2.tgz", + "integrity": "sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.10.2", + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/typescript-estree": "5.10.2", + "debug": "^4.3.2" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", + "integrity": "sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/visitor-keys": "5.10.2" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.10.2.tgz", + "integrity": "sha512-uRKSvw/Ccs5FYEoXW04Z5VfzF2iiZcx8Fu7DGIB7RHozuP0VbKNzP1KfZkHBTM75pCpsWxIthEH1B33dmGBKHw==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.10.2", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", + "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz", + "integrity": "sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/visitor-keys": "5.10.2", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.2.tgz", + "integrity": "sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.10.2", + "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/typescript-estree": "5.10.2", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", + "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.10.2", + "eslint-visitor-keys": "^3.0.0" + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -874,12 +1119,23 @@ } } }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, "acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -889,6 +1145,18 @@ "debug": "4" } }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -938,6 +1206,41 @@ "sprintf-js": "~1.0.2" } }, + "array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + } + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1030,12 +1333,27 @@ "babel-preset-current-node-syntax": "^1.0.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1098,6 +1416,16 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1191,6 +1519,21 @@ "delayed-stream": "~1.0.0" } }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1301,6 +1644,15 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1325,6 +1677,24 @@ "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -1360,6 +1730,95 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "engine.io-client": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + } + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1385,12 +1844,410 @@ "source-map": "~0.6.1" } }, + "eslint": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.8.0.tgz", + "integrity": "sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.0.5", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.2.0", + "espree": "^9.3.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-standard": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", + "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.25.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", + "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.2", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-promise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", + "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==", + "dev": true + }, + "eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", + "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "dev": true + }, + "espree": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz", + "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==", + "dev": true, + "requires": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.1.0" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -1448,6 +2305,36 @@ } } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1460,6 +2347,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -1469,6 +2365,15 @@ "bser": "2.1.1" } }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1484,10 +2389,26 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" } }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, "form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -1518,6 +2439,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1530,6 +2457,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -1542,6 +2480,16 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -1556,6 +2504,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, "global-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", @@ -1573,6 +2530,20 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", @@ -1588,12 +2559,46 @@ "function-bind": "^1.1.1" } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -1645,6 +2650,30 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, "import-local": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", @@ -1661,6 +2690,11 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1683,12 +2717,48 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, "interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, "is-core-module": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", @@ -1698,6 +2768,21 @@ "has": "^1.0.3" } }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1710,30 +2795,102 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2055,6 +3212,15 @@ "@types/node": "*" } }, + "jest-mock-extended": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-2.0.4.tgz", + "integrity": "sha512-MgL3B3GjURQFjjPGqbCANydA5BFNPygv0mYp4Tjfxohh9MWwxxX8Eq2p6ncCt/Vt+RAnaLlDaI7gwrDRD7Pt9A==", + "dev": true, + "requires": { + "ts-essentials": "^7.0.3" + } + }, "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", @@ -2343,6 +3509,18 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -2401,6 +3579,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2440,6 +3624,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -2486,6 +3676,12 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "mock-socket": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.1.2.tgz", + "integrity": "sha512-XKZkCnQ9ISOlTnaPg4LYYSMj7+6i78HyadYzLA5JM4465ibLdjappZD9Csnqc3Tfzep/eEK/LCJ29BTaLHoB1A==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2537,6 +3733,41 @@ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, + "object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2593,12 +3824,31 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2623,6 +3873,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2701,6 +3957,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -2716,6 +3978,12 @@ "resolve": "^1.1.6" } }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2753,6 +4021,12 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2762,6 +4036,15 @@ "glob": "^7.1.3" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2815,6 +4098,17 @@ "rechoir": "^0.6.2" } }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", @@ -2833,6 +4127,73 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "socket.io-client": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", + "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "engine.io-client": "~3.5.0", + "has-binary2": "~1.0.2", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "socket.io-mock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/socket.io-mock/-/socket.io-mock-1.3.2.tgz", + "integrity": "sha512-p4MQBue3NAR8bXIHynRJxK/C+J3I3NpnnpgjptgLFSWv4u9Bdkubf2t0GCmyLmUTi03up0Cx/hQwzQfOpD187g==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0" + } + }, + "socket.io-parser": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", + "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2893,6 +4254,26 @@ "strip-ansi": "^6.0.1" } }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2914,6 +4295,12 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2960,6 +4347,12 @@ "minimatch": "^3.0.4" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "throat": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", @@ -2972,6 +4365,11 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -3007,6 +4405,12 @@ "punycode": "^2.1.1" } }, + "ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true + }, "ts-jest": { "version": "27.0.7", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.7.tgz", @@ -3069,6 +4473,50 @@ "strip-ansi": "^6.0.0" } }, + "tsconfig-paths": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -3100,17 +4548,44 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", + "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, "v8-to-istanbul": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz", @@ -3148,6 +4623,12 @@ "xml-name-validator": "^3.0.0" } }, + "wait-for-expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz", + "integrity": "sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==", + "dev": true + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -3198,6 +4679,19 @@ "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -3251,6 +4745,11 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -3284,6 +4783,11 @@ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 989c682..491354e 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "pcf-reloader-transformer", - "version": "1.0.1", - "description": "Automatic refreshing of PCF components when embedded on a model-driven APP in Dynamics365", + "version": "1.1.0", + "description": "Automatic refreshing of PCF components when embedded on a model-driven App in Dynamics365", "main": "dist/index.js", "scripts": { "presample": "npm run build", "sample": "tsc -p samples/tsconfig.json", "build": "tsc -p tsconfig.json", - "prepare": "ts-patch i ; npm run build", - "test": "jest --verbose" + "prepare": "ts-patch i && npm run build", + "test": "jest", + "lint": "eslint src/ tests/" }, "files": [ "dist" @@ -36,10 +37,24 @@ "@types/jest": "^27.0.2", "@types/node": "^16.11.5", "@types/powerapps-component-framework": "^1.3.3", + "@typescript-eslint/eslint-plugin": "^5.10.2", + "@typescript-eslint/parser": "^5.10.2", + "eslint": "^8.8.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.0.0", "jest": "^27.3.1", + "jest-mock-extended": "^2.0.4", + "mock-socket": "^9.1.2", + "socket.io-mock": "^1.3.2", "ts-jest": "^27.0.7", "ts-node": "^10.4.0", "ts-patch": "^1.4.5", - "typescript": "^4.4.4" + "typescript": "^4.5.5", + "wait-for-expect": "^3.0.2" + }, + "dependencies": { + "socket.io-client": "^2.4.0" } } diff --git a/samples/patched.ts b/samples/patched.ts index 2664f30..4434fd7 100644 --- a/samples/patched.ts +++ b/samples/patched.ts @@ -1,96 +1,58 @@ -import { IInputs, IOutputs } from "./generated/ManifestTypes"; -type PcfReloadParams = { - context: ComponentFramework.Context; - notifyOutputChanged: () => void; - state: ComponentFramework.Dictionary; - container: HTMLDivElement; -}; -interface PcfWindow extends Window { - pcfReloadParams: PcfReloadParams; -} -declare let window: PcfWindow; -const currentScript = document.currentScript; +import * as _pcfReloadLib from "pcf-reloader-transformer/dist/injected"; + +import { + IInputs, + IOutputs, +} from "./generated/ManifestTypes"; + +const _pcfReloadCurrentScript = document.currentScript; export class SampleComponent implements ComponentFramework.StandardControl { - private _container: HTMLDivElement; - /** - * Empty constructor. - */ - constructor() { - if (window.pcfReloadParams) { - const params = window.pcfReloadParams; - this.init(params.context, params.notifyOutputChanged, params.state, params.container); - this.updateView(params.context); - } - } - /** - * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. - * Data-set values are not initialized here, use updateView. - * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions. - * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. - * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface. - * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. - */ - public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement): void { - this.listenToWSUpdates({ - context: context, - notifyOutputChanged: notifyOutputChanged, - state: state, - container: container - }); - this._container = container; - } - /** - * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc. - * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions - */ - public updateView(context: ComponentFramework.Context): void { - if (window.pcfReloadParams) - window.pcfReloadParams.context = context; - this._container.innerHTML = "
Hello, world!
"; - } - /** - * It is called by the framework prior to a control receiving new data. - * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output” - */ - public getOutputs(): IOutputs { - return {}; - } - /** - * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. - * i.e. cancelling any pending remote calls, removing listeners, etc. - */ - public destroy(): void { - } - private _reloadSocket: WebSocket | undefined; - private listenToWSUpdates(params: PcfReloadParams) { - window.pcfReloadParams = params; - const address = "ws://127.0.0.1:8181/ws"; - this._reloadSocket = new WebSocket(address); - this._reloadSocket.onmessage = msg => { - if (msg.data != "reload" && msg.data != "refreshcss") - return; - this.reloadComponent(); - }; - console.log("Live reload enabled on " + address); - } - private reloadComponent() { - console.log("Reload triggered"); - this.destroy(); - if (this._reloadSocket) { - this._reloadSocket.onmessage = null; - this._reloadSocket.close(); - } - const isScript = (s: HTMLOrSVGScriptElement): s is HTMLScriptElement => !!(s as HTMLScriptElement).src; - if (!currentScript || !isScript(currentScript)) - return; - const script = document.createElement("script"); - script.src = currentScript.src; - const parent = currentScript.parentNode; - if (!parent) - return; - currentScript.remove(); - parent.appendChild(script); - } + private _container: HTMLDivElement; + /** + * Empty constructor. + */ + constructor() { + _pcfReloadLib.onConstruct(this, _pcfReloadCurrentScript); + } + /** + * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. + * Data-set values are not initialized here, use updateView. + * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions. + * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. + * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface. + * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. + */ + public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement): void { + const _pcfReloaderParams = { + context: context, + notifyOutputChanged: notifyOutputChanged, + state: state, + container: container + }; + _pcfReloadLib.doConnect("http://localhost:8181", _pcfReloaderParams); + this._container = container; + } + /** + * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc. + * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions + */ + public updateView(context: ComponentFramework.Context): void { + _pcfReloadLib.onUpdateContext(context); + this._container.innerHTML = "
Hello, world!
"; + } + /** + * It is called by the framework prior to a control receiving new data. + * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output” + */ + public getOutputs(): IOutputs { + return {}; + } + /** + * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. + * i.e. cancelling any pending remote calls, removing listeners, etc. + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + public destroy(): void { } } -if (window.pcfReloadParams) - new SampleComponent(); +if (_pcfReloadLib.hasParams()) + new SampleComponent(); diff --git a/src/builders/constructor.ts b/src/builders/constructor.ts new file mode 100644 index 0000000..23f1a09 --- /dev/null +++ b/src/builders/constructor.ts @@ -0,0 +1,78 @@ +import { + ConstructorDeclaration, + factory, + Identifier, +} from "typescript"; + +import { stmt } from "../lib"; +import { + callLib, + currentScriptName, +} from "./"; + +/** + * Create a call to instantiate the PCF class + * + * ``` + * if (_pcfReloadLib.hasParams()) + * new SampleComponent(); + * ``` + * + * @param className The name of the PCF class + * @returns The call to the new PCF class + */ +export function createConstructorCall(className: Identifier) { + + const constructorStatement = factory.createIfStatement( + callLib("hasParams"), + stmt(factory.createNewExpression( + className, + undefined, + [] + )), + undefined + ) + + return constructorStatement +} + +/** + * Generate the body block (not the method) of the constructor + * + * ``` + * _pcfReloader.constructor(this, _pcfReloadCurrentScript) + * ...existing code... + * ``` + * + * @param ctor The constructor declaration, if found + * @returns The body of the updated class constructor + */ +export function createConstructorBody(ctor?: ConstructorDeclaration) { + const callConstruct = callLib("onConstruct", factory.createThis(), currentScriptName) + const existingBody = ctor?.body?.statements ?? factory.createNodeArray([]) + + const block = factory.createBlock([ + stmt(callConstruct), + ...existingBody + ], true) + + return block +} + +/** + * Declare the constructor method, using the body from `createConstructorBody` + * + * ``` + * constructor() { + * ...constructor body... + * } + * ``` + * + * @see createConstructorBody + * @returns The constructor declaration + */ +export const createConstructorDeclaration = () => + factory.createConstructorDeclaration( + undefined, undefined, [], + createConstructorBody() + ) \ No newline at end of file diff --git a/src/lib/currentScript.ts b/src/builders/currentScript.ts similarity index 58% rename from src/lib/currentScript.ts rename to src/builders/currentScript.ts index 7a4d9a1..4462037 100644 --- a/src/lib/currentScript.ts +++ b/src/builders/currentScript.ts @@ -1,7 +1,10 @@ -import { factory } from "typescript" -import { access, declareConst } from "./helpers" +import { + access, + declareConst, +} from "../lib"; +import { id } from "../lib/helpers"; -export const currentScript = factory.createIdentifier("currentScript") +export const currentScriptName = id("_pcfReloadCurrentScript") /** * Create the constant for getting the reference to the current script tag @@ -14,10 +17,10 @@ export const currentScript = factory.createIdentifier("currentScript") */ export function createCurrentScriptAssignment() { const currentScriptAssignment = declareConst( - currentScript, + currentScriptName, access( - factory.createIdentifier("document"), - factory.createIdentifier("currentScript") + id("document"), + id("currentScript") ) ) diff --git a/src/builders/imports.ts b/src/builders/imports.ts new file mode 100644 index 0000000..c324551 --- /dev/null +++ b/src/builders/imports.ts @@ -0,0 +1,51 @@ +import { + Expression, + factory, + forEachChild, + isImportDeclaration, + SourceFile, +} from "typescript"; + +import * as inject from "../injected"; +import { + access, + call, + id, + printNode, +} from "../lib"; + +export type ParamName = keyof inject.ReloadParams +export type MethodName = keyof typeof inject + +const injectLibName = id("_pcfReloadLib") + +export const createLibraryImport = () => + factory.createImportDeclaration( + undefined, + undefined, + factory.createImportClause( + false, + undefined, + factory.createNamespaceImport(injectLibName) + ), + factory.createStringLiteral("pcf-reloader-transformer/dist/injected"), + undefined + ) + +let _importSource: string|undefined +const libraryImportSource = (sourceFile: SourceFile) => _importSource ?? (_importSource = printNode(createLibraryImport(), sourceFile)) + +export const hasLibraryImport = (sourceFile: SourceFile) => { + const existingImportDecl = forEachChild(sourceFile, (n) => { + if (!isImportDeclaration(n)) return undefined + const importText = printNode(n, sourceFile) + return importText === libraryImportSource(sourceFile) + ? n + : undefined + }) + + return !!existingImportDecl +} + +export const callLib = (method: MethodName, ...args: Expression[]) => + call(access(injectLibName, id(method)), ...args) diff --git a/src/builders/index.ts b/src/builders/index.ts new file mode 100644 index 0000000..77ad1e8 --- /dev/null +++ b/src/builders/index.ts @@ -0,0 +1,4 @@ +export * from './constructor'; +export * from './currentScript'; +export * from './imports'; +export * from './methodUpdates'; diff --git a/src/builders/methodUpdates.ts b/src/builders/methodUpdates.ts new file mode 100644 index 0000000..45022c0 --- /dev/null +++ b/src/builders/methodUpdates.ts @@ -0,0 +1,130 @@ +import { + factory, + Identifier, + isIdentifier, + MethodDeclaration, + NodeArray, + ParameterDeclaration, + Statement, +} from "typescript"; + +import { + declareConst, + id, + stmt, +} from "../lib"; +import { IPluginConfig } from "../pluginConfig"; +import { + callLib, + ParamName, +} from "./"; + +/** + * Handles one of `init` and `updateView` methods, generating the following: + * + * Init: + * ``` + * const _pcfReloaderParams = { + * context: context, + * notifyOutputChanged: notifyOutputChanged, + * state: state, + * container: container + * }; + * _pcfReloadLib.connect("http://localhost:8181", _pcfReloaderParams); + * ...existing code... + * ``` + * + * UpdateView: +* ``` + * _pcfReloadLib.updateContext(context) + * ...existing code... + * ``` + * + * @param node The method declaration to update + * @returns The updated method body + */ +export function handleMethod(node: MethodDeclaration, opts: IPluginConfig) { + const existingBody = node.body?.statements ?? factory.createNodeArray() + + switch (node.name.getText()) { + case 'init': + return createInitBody(existingBody, node.parameters, opts) + case 'updateView': + return createUpdateViewBody(existingBody, node.parameters) + } + + return undefined +} + +const getNamesFromParameters = (params: NodeArray) => + params.filter((p): p is ParameterDeclaration & { name: Identifier} => isIdentifier(p.name)).map(p => p.name) + +/** + * Builds the init method body + * ``` + * const _pcfReloaderParams = { + * context: context, + * notifyOutputChanged: notifyOutputChanged, + * state: state, + * container: container + * }; + * _pcfReloadLib.connect("http://localhost:8181", _pcfReloaderParams); + * ...existing code... + * ``` + * + * @param existingBody The existing body block + * @param params The parameters passed to the init method + * @returns The new body block + */ +function createInitBody(existingBody: NodeArray, params: NodeArray, opts: IPluginConfig) { + const names = getNamesFromParameters(params) + if (names.length < 4) return + + const assign = (argName: ParamName, ident: Identifier) => + factory.createPropertyAssignment(argName, ident) + + const paramObj = factory.createObjectLiteralExpression([ + assign("context", names[0]), + assign("notifyOutputChanged", names[1]), + assign("state", names[2]), + assign("container", names[3]) + ], true) + + const paramsName = id("_pcfReloaderParams") + + const defaultAddress = opts.useBrowserSync ?? true + ? "http://localhost:8181" + : "ws://127.0.0.1:8181/ws" + + const declareObj = declareConst(paramsName, paramObj) + const callInit = callLib("doConnect", + factory.createStringLiteral(opts.wsAddress ?? defaultAddress), + paramsName) + + const block = factory.createBlock([ + declareObj, + stmt(callInit), + ...existingBody + ], true) + + return block +} + +/** + * Build the updateView method body + * ``` + * _pcfReloadLib.updateContext(context) + * ...existing code... + * ``` + */ +function createUpdateViewBody(existingBody: NodeArray, params: NodeArray) { + const names = getNamesFromParameters(params) + if (names.length < 1) return + + const block = factory.createBlock([ + stmt(callLib("onUpdateContext", names[0])), + ...existingBody + ], true) + + return block +} diff --git a/src/index.ts b/src/index.ts index 76f0c58..9e44270 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,29 @@ -import * as ts from 'typescript'; -//import { FilePrinter } from './lib/filePrinter'; -import { visitor } from './visitors/sourceFileVisitor'; -import { PluginConfig } from "ts-patch" -import FilePrinter from './lib/filePrinter'; -import { paramsTypeNameString } from './lib/paramsType'; +import ts from "typescript"; -export type IPluginConfig = PluginConfig & { - printGenerated?: boolean, - verbose?: boolean, - wsAddress?: string, -} +import { hasLibraryImport } from "./builders"; +import FilePrinter from "./lib/filePrinter"; +import { IPluginConfig } from "./pluginConfig"; +import { visitor } from "./visitors"; export default (opts: IPluginConfig) => (ctx: ts.TransformationContext) => (sourceFile: ts.SourceFile) => { - // Check: Source already has the type declared, if it does we've probably already handled this - const existingTypeDef = ts.forEachChild(sourceFile, (n) => - ts.isTypeAliasDeclaration(n) && n.name.getText(sourceFile) === paramsTypeNameString - ? n - : undefined - ) - if (existingTypeDef) { - if (opts.verbose) console.log("Params type already declared, skipping " + sourceFile.fileName) + // Check: Source has import declaration, if yes, we back off + if (hasLibraryImport(sourceFile)) { + if (opts.verbose) console.log("PCF Reloader already injected, skipping " + sourceFile.fileName) return sourceFile; } + // Analyze the source file and update if relevant const updatedSource = ts.visitEachChild(sourceFile, visitor(sourceFile, opts, ctx), ctx) + + // If the source was unchanged, abort if (updatedSource == sourceFile) return sourceFile + // The source was changed, check if we want to print it for debugging if (opts.printGenerated) FilePrinter(sourceFile, updatedSource, opts) + // Then return updated source return updatedSource - }; \ No newline at end of file + } diff --git a/src/injected/callouts.ts b/src/injected/callouts.ts new file mode 100644 index 0000000..9fd737c --- /dev/null +++ b/src/injected/callouts.ts @@ -0,0 +1,58 @@ +import { + doDisconnect, + PcfReloaderWindow, +} from "./sync"; + +declare const window: PcfReloaderWindow + +const isScript = (s: HTMLOrSVGScriptElement): s is HTMLScriptElement => !!(s as HTMLScriptElement).src; +export const hasParams = () => !!window.pcfReloadParams + +export type ComponentType = ComponentFramework.StandardControl + +let _currentScript: HTMLOrSVGScriptElement|null +let _instance: ComponentType|undefined +export const onConstruct = (instance: T, currentScript: HTMLOrSVGScriptElement|null) => { + _currentScript = currentScript + _instance = instance + + if (!hasParams()) return + + const params = window.pcfReloadParams; + instance.init(params.context, params.notifyOutputChanged, params.state, params.container); + instance.updateView(params.context); +} + +export const onUpdateContext = (context: ComponentFramework.Context) => { + window.pcfReloadParams = { + ...window.pcfReloadParams, + context: context + } +} + +export const reloadComponent = () => { + // A reload always results in a disconnect, + // if we are not in a valid state, there is no way to recover + doDisconnect() + + // If we don't have an instance, abort + if (!_instance) return + + // If we don't have a currentScript, or it's not a script tag, abort + if (!_currentScript || !isScript(_currentScript)) + return + + // If the script doesn't have a parent, abort + const parent = _currentScript.parentNode + if (!parent) return + + // We are in a valid state, destroy and rebuild + _instance.destroy() + + const script = document.createElement("script") + script.src = _currentScript.src + + _currentScript.remove() + + parent.appendChild(script) +} diff --git a/src/injected/index.ts b/src/injected/index.ts new file mode 100644 index 0000000..7bef137 --- /dev/null +++ b/src/injected/index.ts @@ -0,0 +1,3 @@ +export * from './callouts'; +export * from './logger'; +export * from './sync'; diff --git a/src/injected/logger.ts b/src/injected/logger.ts new file mode 100644 index 0000000..6cef790 --- /dev/null +++ b/src/injected/logger.ts @@ -0,0 +1,5 @@ +const timestamp = () => new Date().toLocaleTimeString('en-US', { + hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 +} as Intl.DateTimeFormatOptions) + +export const log = (...message: string[]) => console.log("[pcf-reloader]", "[" + timestamp() + "]", ...message) diff --git a/src/injected/sync.ts b/src/injected/sync.ts new file mode 100644 index 0000000..c691b1f --- /dev/null +++ b/src/injected/sync.ts @@ -0,0 +1,95 @@ +import socket from "socket.io-client"; + +import { reloadComponent } from "./callouts"; +import { log } from "./logger"; + +type SocketIo = { + on: (eventName: string, callback: (...args: unknown[]) => void) => SocketIo + onevent: (packet: unknown) => void + close: () => SocketIo +} + +export type ReloadParams = { + context: ComponentFramework.Context + notifyOutputChanged: () => void + state: ComponentFramework.Dictionary + container: HTMLDivElement +} + +export interface PcfReloaderWindow extends Window { + pcfReloadParams: ReloadParams +} + +declare const window: PcfReloaderWindow + +let _socket: SocketIo|null +let _websocket: WebSocket|null + +const hasData = (o: unknown): o is { data: unknown } => !!(o as { data: unknown })?.data +const bsConnect = (baseUrl: string, debug?: boolean) => { + const socketConfig = { + "reconnectionAttempts": 50, + "path": "/browser-sync/socket.io", + } + const socketUrl = baseUrl + '/browser-sync' + + _socket = socket(socketUrl, socketConfig) as SocketIo + _socket.on('disconnect', () => log("BrowserSync disconnected")) + _socket.on('connect', () => log("BrowserSync connected")) + _socket.on('browser:reload', () => { + log("Reload triggered") + reloadComponent() + }) + + if (debug) { + const onEvent = _socket.onevent + _socket.onevent = function(packet: unknown) { + onEvent.call(this, packet); + + if (hasData(packet)) + log("> " + JSON.stringify(packet.data)) + }; + } + + return socketUrl +} + +const wsConnect = (address: string, debug?: boolean) => { + _websocket = new WebSocket(address) + _websocket.onmessage = (msg: MessageEvent) => { + if (msg.data !== "reload" && msg.data !== "refreshcss") { + if (debug) { + log("> " + msg.data) + } + return + } + + log("Reload triggered") + reloadComponent() + } + + return address +} + +export const doConnect = (baseUrl: string, params: ReloadParams, debug?: boolean) => { + window.pcfReloadParams = params; + + const address = (baseUrl.indexOf("http") > -1) + ? bsConnect(baseUrl, debug) + : wsConnect(baseUrl, debug) + + log("Live reload enabled on " + address) +} + +export const doDisconnect = () => { + if (_socket) { + _socket.close() + } + + if (_websocket) { + _websocket.onmessage = null; + _websocket.close(); + } + + log("Live reload disabled") +} diff --git a/src/lib/constructor.ts b/src/lib/constructor.ts deleted file mode 100644 index d8e7c11..0000000 --- a/src/lib/constructor.ts +++ /dev/null @@ -1,108 +0,0 @@ -import ts, { Identifier } from "typescript"; -import { access, declareConst, id } from "./helpers"; -import { paramNames } from "./paramsType"; -import { windowVariableName } from "./windowExtensions"; - -export const paramsVariableName = ts.factory.createIdentifier("pcfReloadParams") -export const paramsReference = access(windowVariableName, paramsVariableName) -export const accessParam = (prop: Identifier) => access(windowVariableName, paramsVariableName, prop) - -/** - * Create a call to instantiate the PCF class - * - * ``` - * if (window.pcfReloadParams) - * new SampleComponent(); - * ``` - * - * @param className The name of the PCF class - * @returns The call to the new PCF class - */ -export function createConstructorCall(className: ts.Identifier) { - const constructorStatement = ts.factory.createIfStatement( - paramsReference, - ts.factory.createExpressionStatement(ts.factory.createNewExpression( - className, - undefined, - [] - )), - undefined - ) - - return constructorStatement -} - -/** - * Declare the constructor method, using the body from `createConstructorBody` - * - * ``` - * constructor() { - * ...constructor body... - * } - * ``` - * - * @see createConstructorBody - * @returns The constructor declaration - */ -export const createConstructorDeclaration = () => - ts.factory.createConstructorDeclaration( - undefined, undefined, [], - createConstructorBody() - ) - -/** - * Generate the body block (not the method) of the constructor - * - * ``` - * if (window.pcfReloadParams) { - * const params = window.pcfReloadParams; - * this.init(params.context, params.notifyOutputChanged, params.state, params.container); - * this.updateView(params.context); - * } - * ...existing code... - * ``` - * - * @param ctor The constructor declaration, if found - * @returns The body of the updated class constructor - */ -export function createConstructorBody(ctor?: ts.ConstructorDeclaration) { - const paramsName = id("params") - const paramsConst = declareConst(paramsName, paramsReference) - - const initCall = ts.factory.createExpressionStatement(ts.factory.createCallExpression( - access(ts.factory.createThis(), id("init")), - undefined, - [ - access(paramsName, id(paramNames.context)), - access(paramsName, id(paramNames.noc)), - access(paramsName, id(paramNames.state)), - access(paramsName, id(paramNames.container)) - ] - )) - - const updateViewCall = ts.factory.createExpressionStatement(ts.factory.createCallExpression( - access(ts.factory.createThis(), id("updateView")), - undefined, - [access(paramsName, id(paramNames.context))] - )) - - const thenBlock = ts.factory.createBlock([ - paramsConst, - initCall, - updateViewCall - ], true) - - const ifStatement = ts.factory.createIfStatement( - paramsReference, - thenBlock - ) - - const existingBody = ctor?.body?.statements ?? ts.factory.createNodeArray([]) - - const block = ts.factory.createBlock([ - ifStatement, - ...existingBody - ], true) - - return block -} \ No newline at end of file diff --git a/src/lib/filePrinter.ts b/src/lib/filePrinter.ts index 9229872..1c787d6 100644 --- a/src/lib/filePrinter.ts +++ b/src/lib/filePrinter.ts @@ -1,19 +1,14 @@ -import { writeFileSync } from "fs" -import path = require("path") -import { createPrinter, EmitHint, SourceFile } from "typescript" -import { IPluginConfig } from ".." +import { writeFileSync } from "fs"; +import path = require("path"); +import { + createPrinter, + EmitHint, + Node, + Printer, + SourceFile, +} from "typescript"; -export default (sourceFile: SourceFile, updatedSource: SourceFile, opt: IPluginConfig) => { - const printer = createPrinter() - const generated = printer.printNode(EmitHint.Unspecified, updatedSource, sourceFile) - - const generatedPath = newPath(sourceFile.fileName) - writeFileSync(generatedPath, generated) - - if (opt.verbose) { - console.log("Generated file written to: " + generatedPath) - } -} +import { IPluginConfig } from "../pluginConfig"; export const newPath = (fileName: string) => { const dirname = path.dirname(fileName) @@ -22,3 +17,20 @@ export const newPath = (fileName: string) => { const generatedPath = path.resolve(dirname, newName) return generatedPath } + +let _printer: Printer|undefined +const printer = () => _printer ?? (_printer = createPrinter()) + +export const printNode = (node: Node, sourceFile: SourceFile) => + printer().printNode(EmitHint.Unspecified, node, sourceFile) + +export default (sourceFile: SourceFile, updatedSource: SourceFile, opt: IPluginConfig) => { + const generated = printNode(updatedSource, sourceFile) + + const generatedPath = newPath(sourceFile.fileName) + writeFileSync(generatedPath, generated) + + if (opt.verbose) { + console.log("Generated file written to: " + generatedPath) + } +} \ No newline at end of file diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 4b4d900..c88064b 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,26 +1,23 @@ -import { BindingName, Expression, factory, Identifier, NodeFlags, ParenthesizedExpression, PropertyAccessExpression, PropertyName, SyntaxKind, ThisExpression, TypeNode } from "typescript"; +import { + BindingName, + Expression, + factory, + Identifier, + NodeFlags, + ParenthesizedExpression, + PropertyAccessExpression, + SyntaxKind, + ThisExpression, +} from "typescript"; export const id = factory.createIdentifier export const declareConst = (name: BindingName, initializer: Expression) => - declareVar(name, true, undefined, initializer) - -export const declareVar = (name: BindingName, isConst: boolean, type?: TypeNode, initializer?: Expression, isDeclare = false) => factory.createVariableStatement( - isDeclare ? [ factory.createModifier(SyntaxKind.DeclareKeyword) ] : undefined, - factory.createVariableDeclarationList([ - factory.createVariableDeclaration(name, undefined, type, initializer) - ], isConst ? NodeFlags.Const : NodeFlags.Let | NodeFlags.ContextFlags) - ) - -export const declareProperty = (name: PropertyName, type: TypeNode) => - factory.createPropertyDeclaration( - undefined, - [factory.createModifier(SyntaxKind.PrivateKeyword)], - name, undefined, - type, - undefined + factory.createVariableDeclarationList([ + factory.createVariableDeclaration(name, undefined, undefined, initializer) + ], NodeFlags.Const) ) export const eqGreaterThan = factory.createToken(SyntaxKind.EqualsGreaterThanToken); @@ -35,7 +32,9 @@ export function setVariable(leftHandSide: Expression, rightHandSide: Expression) ); } -export function access(...parts: (Identifier | ThisExpression | ParenthesizedExpression)[]): PropertyAccessExpression | Identifier | ThisExpression | ParenthesizedExpression { +export type AccessPart = Identifier | ThisExpression | ParenthesizedExpression +export type AccessExpression = PropertyAccessExpression | AccessPart +export const access = (...parts: AccessPart[]): AccessExpression => { if (parts.length < 2) return parts[0] const val = parts.pop() as Identifier @@ -43,4 +42,10 @@ export function access(...parts: (Identifier | ThisExpression | ParenthesizedExp access(...parts), val ) -} \ No newline at end of file +} + +export const call = (callee: Expression, ...args: Expression[]) => + factory.createCallExpression(callee, undefined, args) + +export const stmt = (expr: Expression) => factory.createExpressionStatement(expr) + diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..feb47e2 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,2 @@ +export * from './filePrinter'; +export * from './helpers'; diff --git a/src/lib/listener.ts b/src/lib/listener.ts deleted file mode 100644 index 2c54719..0000000 --- a/src/lib/listener.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { factory, SyntaxKind } from "typescript"; -import { paramsReference } from "./constructor"; -import { access, declareConst, declareProperty, eqGreaterThan, setVariable } from "./helpers"; -import { paramsTypeName } from "./paramsType"; -import { reloadComponent } from "./refresher"; - -export const listenCallName = factory.createIdentifier("listenToWSUpdates") -const webSocketTypeName = factory.createIdentifier("WebSocket") -const webSocketFieldName = factory.createIdentifier("_reloadSocket") -export const webSocket = access(factory.createThis(), webSocketFieldName) -export const webSocketOnMessage = access(factory.createThis(), webSocketFieldName, factory.createIdentifier("onmessage")) -export const webSocketClose = access(factory.createThis(), webSocketFieldName, factory.createIdentifier("close")) - -/** - * Create the listenToWSUpdates. - * - * ``` - * private listenToWSUpdates(params: PcfReloadParams) { - * window.pcfReloadParams = params; - * const address = "ws://127.0.0.1:8181/ws"; - * const socket = new WebSocket(address); - * socket.onmessage = msg => { - * if (msg.data != "reload" && msg.data != "refreshcss") - * return; - * this.reloadComponent(); - * }; - * console.log("Live reload enabled on " + address); - * } - * ``` - * - * @param wsListeningAddress The address to listen to websocket messages on (defaults to ws://127.0.01:8181/ws) - * @returns An object containing the socket property declaration and the wsListener method - */ -export function createListenerMethod(wsListeningAddress?: string) { - const listeningAddress = factory.createStringLiteral(wsListeningAddress ?? "ws://127.0.0.1:8181/ws") - - const address = factory.createIdentifier("address") - - const params = factory.createIdentifier("params") - - const onMessageFunc = createOnMessageFunction() - const body = factory.createBlock([ - setVariable(paramsReference, params), - declareConst(address, listeningAddress), - setVariable(webSocket, factory.createNewExpression(webSocketTypeName, undefined, [ address ])), - setVariable(webSocketOnMessage, onMessageFunc), - factory.createExpressionStatement( - factory.createCallExpression( - access(factory.createIdentifier("console"), factory.createIdentifier("log")), undefined, [ - factory.createBinaryExpression( - factory.createStringLiteral("Live reload enabled on "), - SyntaxKind.PlusToken, - address - ) - ] - ) - ) - ], true) - - const method = factory.createMethodDeclaration(undefined, [ - factory.createModifier(SyntaxKind.PrivateKeyword) - ], undefined, listenCallName, undefined, undefined, [ - factory.createParameterDeclaration(undefined, undefined, undefined, params, undefined, - factory.createTypeReferenceNode(paramsTypeName)) - ], undefined, body) - - const propertyType = factory.createUnionTypeNode([ - factory.createTypeReferenceNode(webSocketTypeName), - factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword) - ]) - - const socketVarDecl = declareProperty(webSocketFieldName, propertyType) - - return { socketVarDecl, listener: method } -} - -function createOnMessageFunction() { - const msg = factory.createIdentifier("msg"); - - const msgData = access(msg, factory.createIdentifier("data")) - - const body = factory.createBlock([ - factory.createIfStatement(factory.createBinaryExpression( - factory.createBinaryExpression( - msgData, - SyntaxKind.ExclamationEqualsToken, - factory.createStringLiteral("reload") - ), - SyntaxKind.AmpersandAmpersandToken, - factory.createBinaryExpression( - msgData, - SyntaxKind.ExclamationEqualsToken, - factory.createStringLiteral("refreshcss") - ) - ), factory.createReturnStatement(undefined)), - factory.createExpressionStatement( - factory.createCallExpression(access(factory.createThis(), reloadComponent), undefined, undefined)) - ], true) - - const functionDecl = factory.createArrowFunction(undefined, - undefined, [ - factory.createParameterDeclaration(undefined, undefined, undefined, msg) - ], undefined, eqGreaterThan, body) - - return functionDecl -} diff --git a/src/lib/methodUpdates.ts b/src/lib/methodUpdates.ts deleted file mode 100644 index 67b30b2..0000000 --- a/src/lib/methodUpdates.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { factory, Identifier, isIdentifier, MethodDeclaration, NodeArray, ParameterDeclaration, Statement } from "typescript" -import { accessParam, paramsReference } from "./constructor" -import { access, id, setVariable } from "./helpers" -import { listenCallName } from "./listener" -import { paramNames } from "./paramsType" - -/** - * Handles one of `init` and `updateView` methods, generating the following: - * - * Init: - * ``` - * this.listenToWSUpdates({ - * context: context, - * notifyOutputChanged: notifyOutputChanged, - * state: state, - * container: container - * }); - * ...existing code... - * ``` - * - * UpdateView: - * ``` - * if (window.pcfReloadParams) - * window.pcfReloadParams.context = context; - * ...existing code... - * ``` - * - * @param node The method declaration to update - * @returns The updated method body - */ -export function handleMethod(node: MethodDeclaration) { - const existingBody = node.body?.statements ?? factory.createNodeArray() - - switch (node.name.getText()) { - case 'init': - return createInitBody(existingBody, node.parameters) - case 'updateView': - return createUpdateViewBody(existingBody, node.parameters) - } - - return undefined -} - -function createInitBody(existingBody: NodeArray, params: NodeArray) { - const names = getNamesFromParameters(params) - if (names.length != 4) return - - const listenCall = factory.createExpressionStatement( - factory.createCallExpression( - access(factory.createThis(), listenCallName), - undefined, - [ - factory.createObjectLiteralExpression([ - factory.createPropertyAssignment(paramNames.context, names[0]), - factory.createPropertyAssignment(paramNames.noc, names[1]), - factory.createPropertyAssignment(paramNames.state, names[2]), - factory.createPropertyAssignment(paramNames.container, names[3]) - ], true) - ] - ) - ) - - const block = factory.createBlock([ - listenCall, - ...existingBody - ], true) - - return block -} - -const getNamesFromParameters = (params: NodeArray) => - params.filter((p): p is ParameterDeclaration & { name: Identifier} => isIdentifier(p.name)).map(p => p.name) - -function createUpdateViewBody(existingBody: NodeArray, params: NodeArray) { - const names = getNamesFromParameters(params) - if (names.length != 1) return - - const context = id(paramNames.context) - const ifStatement = factory.createIfStatement( - paramsReference, - setVariable(accessParam(context), names[0]) - ) - - const block = factory.createBlock([ - ifStatement, - ...existingBody - ], true) - - return block -} \ No newline at end of file diff --git a/src/lib/paramsType.ts b/src/lib/paramsType.ts deleted file mode 100644 index f2b7e9b..0000000 --- a/src/lib/paramsType.ts +++ /dev/null @@ -1,36 +0,0 @@ -import ts from "typescript"; - -export const paramsTypeNameString = "PcfReloadParams" -export const paramsTypeName = ts.factory.createIdentifier(paramsTypeNameString) - -export const paramNames = { - context: "context", - noc: "notifyOutputChanged", - state: "state", - container: "container" -} - -/** - * Create the type declaration for extending the window - * - * ``` - * type PcfReloadParams = { - * context: ComponentFramework.Context; - * notifyOutputChanged: () => void; - * state: ComponentFramework.Dictionary; - * container: HTMLDivElement; - * }; - * ``` - * - * @returns The params type - */ -export function createParamsType() { - const paramsTypeLiteral = ts.factory.createTypeLiteralNode([ - ts.factory.createPropertySignature(undefined, paramNames.context, undefined, ts.factory.createTypeReferenceNode("ComponentFramework.Context", undefined)), - ts.factory.createPropertySignature(undefined, paramNames.noc, undefined, ts.factory.createTypeReferenceNode("() => void", undefined)), - ts.factory.createPropertySignature(undefined, paramNames.state, undefined, ts.factory.createTypeReferenceNode("ComponentFramework.Dictionary", undefined)), - ts.factory.createPropertySignature(undefined, paramNames.container, undefined, ts.factory.createTypeReferenceNode("HTMLDivElement", undefined)) - ]); - const paramsTypeNode = ts.factory.createTypeAliasDeclaration(undefined, undefined, paramsTypeName, undefined, paramsTypeLiteral); - return paramsTypeNode; -} \ No newline at end of file diff --git a/src/lib/refresher.ts b/src/lib/refresher.ts deleted file mode 100644 index b800a5c..0000000 --- a/src/lib/refresher.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { factory, SyntaxKind } from "typescript"; -import { currentScript } from "./currentScript"; -import { access, declareConst, eqGreaterThan, setVariable } from "./helpers"; -import { webSocket, webSocketClose, webSocketOnMessage } from "./listener"; - -export const reloadComponent = factory.createIdentifier("reloadComponent") - -const htmlScriptElement = factory.createIdentifier("HTMLScriptElement"); - -/** - * Create the declaration of the reloadComponent method - * - * ``` - * private reloadComponent() { - * console.log("Reload triggered"); - * this.destroy(); - * if (this._reloadSocket) { - * this._reloadSocket.onmessage = null; - * this._reloadSocket.close(); - * } - * const isScript = (s: HTMLOrSVGScriptElement): s is HTMLScriptElement => !!(s as HTMLScriptElement).src; - * if (!currentScript || !isScript(currentScript)) - * return; - * const script = document.createElement("script"); - * script.src = currentScript.src; - * const parent = currentScript.parentNode; - * if (!parent) - * return; - * currentScript.remove(); - * parent.appendChild(script); - * } - * ``` - * - * @returns The reloadComponent method - */ -export function createRefreshMethod() { - - return factory.createMethodDeclaration(undefined, [ - factory.createModifier(SyntaxKind.PrivateKeyword) - ], undefined, reloadComponent, undefined, undefined, [], undefined, createBody()) -} - -function createBody() { - const script = factory.createIdentifier("script"); - const parent = factory.createIdentifier("parent"); - - const stopListening = factory.createIfStatement(webSocket, - factory.createBlock([ - setVariable(webSocketOnMessage, factory.createNull()), - factory.createExpressionStatement(factory.createCallExpression(webSocketClose, undefined, [])), - ], true)) - - return factory.createBlock([ - factory.createExpressionStatement(factory.createCallExpression( - access(factory.createIdentifier("console"), factory.createIdentifier("log")), - undefined, [factory.createStringLiteral("Reload triggered")] - )), - factory.createExpressionStatement(factory.createCallExpression( - access(factory.createThis(), factory.createIdentifier("destroy")), - undefined, - undefined - )), - stopListening, - declareConst(isScript, isScriptFunc()), - factory.createIfStatement(factory.createBinaryExpression( - factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - currentScript - ), - SyntaxKind.BarBarToken, - callIsScript() - ), - factory.createReturnStatement() - ), - declareConst(script, createScriptElement()), - setVariable( - access(script, factory.createIdentifier("src")), - access(currentScript, factory.createIdentifier("src")) - ), - declareConst(parent, access(currentScript, factory.createIdentifier("parentNode")) - ), - factory.createIfStatement(factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - parent - ), factory.createReturnStatement()), - factory.createExpressionStatement(factory.createCallExpression(access(currentScript, factory.createIdentifier("remove")), undefined, undefined)), - factory.createExpressionStatement(factory.createCallExpression(access(parent, factory.createIdentifier("appendChild")), undefined, [script])) - ], true) -} - -const isScriptFunc = () => - factory.createArrowFunction(undefined, undefined, [ - factory.createParameterDeclaration(undefined, undefined, undefined, "s", undefined, - factory.createTypeReferenceNode("HTMLOrSVGScriptElement"), - undefined - ) - ], - factory.createTypePredicateNode(undefined, - factory.createIdentifier("s"), - factory.createTypeReferenceNode(htmlScriptElement)), - eqGreaterThan, - factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - access( - factory.createParenthesizedExpression(factory.createAsExpression( - factory.createIdentifier("s"), - factory.createTypeReferenceNode( - htmlScriptElement, - undefined - ) - )), - factory.createIdentifier("src") - ) - ) - )); - -const isScript = factory.createIdentifier("isScript"); -const callIsScript = () => factory.createPrefixUnaryExpression( - SyntaxKind.ExclamationToken, - factory.createCallExpression(isScript, undefined, [currentScript]) -); - -const createScriptElement = () => - factory.createCallExpression( - access( - factory.createIdentifier("document"), - factory.createIdentifier("createElement") - ), - undefined, - [ - factory.createStringLiteral("script") - ] - ); \ No newline at end of file diff --git a/src/lib/windowExtensions.ts b/src/lib/windowExtensions.ts deleted file mode 100644 index bfcfb4c..0000000 --- a/src/lib/windowExtensions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import ts from "typescript" -import { paramsVariableName } from "./constructor" -import { declareVar } from "./helpers" -import { paramsTypeName } from "./paramsType" - -export const windowInterfaceName = ts.factory.createIdentifier("PcfWindow") -export const windowVariableName = ts.factory.createIdentifier("window") - -/** - * Create the declaration for extending window with the params type - * - * ``` - * interface PcfWindow extends Window { - * pcfReloadParams: PcfReloadParams; - * } - * declare let window: PcfWindow; - * ``` - * - * @returns The updated window interface - */ -export function createAndDeclareWindowInterface() { - const windowInterface = ts.factory.createInterfaceDeclaration(undefined, undefined, windowInterfaceName, undefined, [ - ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ - ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier("Window"), undefined) - ]) - ], [ - ts.factory.createPropertySignature(undefined, paramsVariableName, undefined, ts.factory.createTypeReferenceNode(paramsTypeName)) - ]); - - const decl = declareVar(windowVariableName, false, ts.factory.createTypeReferenceNode(windowInterfaceName), undefined, true) - - return [windowInterface, decl] -} \ No newline at end of file diff --git a/src/pluginConfig.ts b/src/pluginConfig.ts new file mode 100644 index 0000000..8635e7b --- /dev/null +++ b/src/pluginConfig.ts @@ -0,0 +1,8 @@ +import { PluginConfig } from "ts-patch"; + +export type IPluginConfig = PluginConfig & { + printGenerated?: boolean + verbose?: boolean + wsAddress?: string + useBrowserSync?: boolean +} diff --git a/src/visitors/classVisitor.ts b/src/visitors/classVisitor.ts index f8e18b6..e961a70 100644 --- a/src/visitors/classVisitor.ts +++ b/src/visitors/classVisitor.ts @@ -1,30 +1,40 @@ -import { isConstructorDeclaration, isMethodDeclaration, Node, factory } from "typescript" -import { createConstructorBody } from "../lib/constructor" -import { handleMethod } from "../lib/methodUpdates" +import { + factory, + isConstructorDeclaration, + isMethodDeclaration, + Node, +} from "typescript"; -export const classVisitor = (node: Node): Node | Node[] => { - if (isConstructorDeclaration(node)) { - const block = createConstructorBody(node) - return factory.updateConstructorDeclaration(node, node.decorators, node.modifiers, node.parameters, block) - } +import { + createConstructorBody, + handleMethod, +} from "../builders"; +import { IPluginConfig } from "../pluginConfig"; - if (isMethodDeclaration(node)) { - const functionBody = handleMethod(node) - if (functionBody) { - return factory.updateMethodDeclaration( - node, - node.decorators, - node.modifiers, - node.asteriskToken, - node.name, - node.questionToken, - node.typeParameters, - node.parameters, - node.type, - functionBody - ) +export const classVisitor = (opts: IPluginConfig) => + (node: Node): Node | Node[] => { + if (isConstructorDeclaration(node)) { + const block = createConstructorBody(node) + return factory.updateConstructorDeclaration(node, node.decorators, node.modifiers, node.parameters, block) } - } - return node -} \ No newline at end of file + if (isMethodDeclaration(node)) { + const functionBody = handleMethod(node, opts) + if (functionBody) { + return factory.updateMethodDeclaration( + node, + node.decorators, + node.modifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + functionBody + ) + } + } + + return node + } diff --git a/src/visitors/index.ts b/src/visitors/index.ts new file mode 100644 index 0000000..8ea47c1 --- /dev/null +++ b/src/visitors/index.ts @@ -0,0 +1,3 @@ +export * from './classVisitor'; +export * from './constructorVisitor'; +export * from './sourceFileVisitor'; diff --git a/src/visitors/sourceFileVisitor.ts b/src/visitors/sourceFileVisitor.ts index 3cc0f4a..15e0d5a 100644 --- a/src/visitors/sourceFileVisitor.ts +++ b/src/visitors/sourceFileVisitor.ts @@ -1,28 +1,53 @@ -import { factory, forEachChild, isClassDeclaration, Node, SourceFile, SyntaxKind, TransformationContext, visitEachChild } from "typescript" -import { IPluginConfig } from ".." -import { createConstructorCall, createConstructorDeclaration } from "../lib/constructor" -import { createCurrentScriptAssignment } from "../lib/currentScript" -import { createListenerMethod } from "../lib/listener" -import { createParamsType } from "../lib/paramsType" -import { createRefreshMethod } from "../lib/refresher" -import { createAndDeclareWindowInterface } from "../lib/windowExtensions" -import { classVisitor } from "./classVisitor" -import { constructorVisitor } from "./constructorVisitor" +import ts, { + factory, + forEachChild, + isClassDeclaration, + Node, + SourceFile, + SyntaxKind, + TransformationContext, + visitEachChild, +} from "typescript"; + +import { + createConstructorCall, + createConstructorDeclaration, + createCurrentScriptAssignment, + createLibraryImport, +} from "../builders"; +import { IPluginConfig } from "../pluginConfig"; +import { + classVisitor, + constructorVisitor, +} from "./"; + +type hasName = { + name: Exclude +} +const isValidNode = (node: Node): node is ts.ClassDeclaration & hasName => { + // Check: Not a class, skip it + if (!isClassDeclaration(node)) return false + + // Check: Implements ComponentFramework.StandardControl + const implement = node.heritageClauses?.filter(h => h.token == SyntaxKind.ImplementsKeyword + && h.types.find(t => t.getText() === "ComponentFramework.StandardControl")) + + if (!implement?.length) return false + + // Check: Has class name so we can construct meaninfully + const className = node.name + if (!className) return false + + return true +} export const visitor = (sourceFile: SourceFile, opts: IPluginConfig, ctx: TransformationContext) => (node: Node): Node[] | Node => { - // Check: Not a class, skip it - if (!isClassDeclaration(node)) return node - - // Check: Implements ComponentFramework.StandardControl - const implement = node.heritageClauses?.filter(h => h.token == SyntaxKind.ImplementsKeyword - && h.types.find(t => t.getText() === "ComponentFramework.StandardControl")) - - if (!implement?.length) return node + if (!isValidNode(node)) { + return node + } - // Check: Has class name so we can construct meaninfully const className = node.name - if (!className) return node if (opts.verbose) { const fileName = sourceFile.fileName @@ -32,19 +57,13 @@ export const visitor = (sourceFile: SourceFile, opts: IPluginConfig, ctx: Transf // We are in the main class, implementing ComponentFramework.StandardControl - // Generate constructor for after class declaration - const constructorDeclaration = createConstructorCall(className) + // Build the library import + //const useBrowserSync = opts.useBrowserSync ?? true + const importDecl = createLibraryImport() + // Assign currentScript variable const scriptAssignment = createCurrentScriptAssignment() - const typeDef = createParamsType() - - const windowDeclaration = createAndDeclareWindowInterface() - - const { listener: listenMethod, socketVarDecl } = createListenerMethod(opts.wsAddress) - - const refreshMethod = createRefreshMethod() - // Check if the class has a constructor to hook into const foundConstructor = forEachChild(node, constructorVisitor) @@ -54,8 +73,16 @@ export const visitor = (sourceFile: SourceFile, opts: IPluginConfig, ctx: Transf : undefined // We want the class declaration as well, with modified methods - const classDeclaration = visitEachChild(node, classVisitor, ctx) + const classDeclaration = visitEachChild(node, classVisitor(opts), ctx) + + // Update the detected class with the new statements + const classMembers = [ + ...classDeclaration.members, + ...(constructor ? [constructor] : []), + //refreshMethod + ] + // Build the class from the updated members const newClass = factory.updateClassDeclaration( classDeclaration, classDeclaration.decorators, @@ -63,18 +90,15 @@ export const visitor = (sourceFile: SourceFile, opts: IPluginConfig, ctx: Transf classDeclaration.name, classDeclaration.typeParameters, classDeclaration.heritageClauses, - [ - ...classDeclaration.members, - ...(constructor ? [constructor] : []), - socketVarDecl, - listenMethod, - refreshMethod - ] + classMembers ) + // Generate constructor for after class declaration + const constructorDeclaration = createConstructorCall(className) + + // Return the updated source return [ - typeDef, - ...windowDeclaration, + importDecl, scriptAssignment, newClass, constructorDeclaration diff --git a/tests/constructor.test.ts b/tests/constructor.test.ts index fa400b8..15c3bf5 100644 --- a/tests/constructor.test.ts +++ b/tests/constructor.test.ts @@ -1,41 +1,51 @@ -import * as c from "../src/lib/constructor" -import { print } from "./utils/common" -import ts from "typescript" -import { buildClass, extractMethod } from "./utils/codeGeneration" +import ts from "typescript"; -test('constructor call is correct', () => { - const ctor = c.createConstructorCall(ts.factory.createIdentifier("test")) +import * as c from "../src/builders/constructor"; +import { + buildClass, + extractMethod, +} from "./utils/codeGeneration"; +import { + isDefined, + print, +} from "./utils/common"; - const text = print(ctor, true) - expect(text).toBe("if (window.pcfReloadParams) new test();") -}) +const constructorBody = (oldBody: string) => `constructor() { _pcfReloadLib.onConstruct(this, _pcfReloadCurrentScript);${oldBody} }` -test('constructor is created', () => { - const ctor = c.createConstructorDeclaration() +describe('constructor builder', () => { + test('constructor call is correct', () => { + const ctor = c.createConstructorCall(ts.factory.createIdentifier("test")) - const text = print(ctor, true) - expect(text).toBe("constructor() { if (window.pcfReloadParams) { const params = window.pcfReloadParams; this.init(params.context, params.notifyOutputChanged, params.state, params.container); this.updateView(params.context); } }") -}) + const text = print(ctor, true) + expect(text).toBe("if (_pcfReloadLib.hasParams()) new test();") + }) -it.each` + test('constructor is created', () => { + const ctor = c.createConstructorDeclaration() + + const text = print(ctor, true) + expect(text).toBe(constructorBody("")) + }) + + it.each` body | description ${"{}"} | ${"empty"} ${"{ console.log(); }"} | ${"existing"} ${""} | ${"undefined"} -`("constructor, body is $description", ({ body, description: _description }: { body: string, description: string }) => { - const { constructor } = extractMethod(buildClass(`constructor() ${body}`)) - if (!constructor) expect(constructor).toBeDefined() - - const ctor = constructor! - const newBody = c.createConstructorBody(ctor) - const ctorDecl = ts.factory.updateConstructorDeclaration(ctor, - ctor.decorators, - ctor.modifiers, - ctor.parameters, - newBody) - - const normalized = body.replace("{", "").replace("}", "").trim() - const oldBody = normalized.length ? " " + normalized : "" - const source = print(ctorDecl, true) - expect(source).toBe(`constructor() { if (window.pcfReloadParams) { const params = window.pcfReloadParams; this.init(params.context, params.notifyOutputChanged, params.state, params.container); this.updateView(params.context); }${oldBody} }`) +`("constructor, body is $description", ({ body }: { body: string }) => { + const { constructor } = extractMethod(buildClass(`constructor() ${body}`)) + isDefined(constructor) + + const newBody = c.createConstructorBody(constructor) + const ctorDecl = ts.factory.updateConstructorDeclaration(constructor, + constructor.decorators, + constructor.modifiers, + constructor.parameters, + newBody) + + const normalized = body.replace("{", "").replace("}", "").trim() + const oldBody = normalized.length ? " " + normalized : "" + const source = print(ctorDecl, true) + expect(source).toBe(constructorBody(oldBody)) + }) }) \ No newline at end of file diff --git a/tests/injected/callout.test.ts b/tests/injected/callout.test.ts new file mode 100644 index 0000000..f7d3ae6 --- /dev/null +++ b/tests/injected/callout.test.ts @@ -0,0 +1,274 @@ +/** + * @jest-environment jsdom + */ + +import { + mock, + mockClear, + mockDeep, + mockReset, +} from "jest-mock-extended"; + +import * as injected from "../../src/injected"; +import { ComponentType } from "../../src/injected"; + +const init = () => { + const getParams = jest.fn() + const setParams = jest.fn() + + const component = mock({ + init: jest.fn(), + updateView: jest.fn() + }) + + beforeAll(() => { + Object.defineProperty(global.window, "pcfReloadParams", { + configurable: true, + get: getParams, + set: setParams + }) + }) + + beforeEach(() => { + jest.resetAllMocks() + mockReset(component) + }) + + return { getParams, setParams, component } +} + +describe('onConstruct', () => { + const { getParams, component } = init() + + it('calls init and updateView if params are set', () => { + getParams.mockReturnValue(mock()) + injected.onConstruct(component, document.createElement("script")) + + expect(component.init).toHaveBeenCalled() + expect(component.updateView).toHaveBeenCalled() + }) + + it('does not call init and updateView if params are not set', () => { + injected.onConstruct(component, document.createElement("script")) + + expect(component.init).not.toHaveBeenCalled() + expect(component.updateView).not.toHaveBeenCalled() + }) +}) + +describe('hasParams', () => { + const { getParams } = init() + + it('returns hasParams if params are set', () => { + getParams.mockReturnValue(mock()) + expect(injected.hasParams()).toBeTruthy() + + getParams.mockReturnValue(undefined) + expect(injected.hasParams()).toBeFalsy() + }) +}) + +describe('onUpdateContext', () => { + const { getParams, setParams } = init() + it('updates the context in the params', () => { + const oldParams = { + container: document.createElement("div"), + context: mock>(), + notifyOutputChanged: jest.fn(), + state: mock() + } + getParams.mockReturnValue(oldParams) + + const newContext = mock>() + + injected.onUpdateContext(newContext) + expect(getParams).toHaveBeenCalled() + expect(setParams).toHaveBeenCalledWith(expect.objectContaining>({ + context: newContext, + container: oldParams.container, + notifyOutputChanged: oldParams.notifyOutputChanged, + state: oldParams.state + })) + }) +}) + +describe('reloadComponent', () => { + const getComponent = () => mock({ + init: jest.fn(), + updateView: jest.fn(), + destroy: jest.fn() + }) + + const doDisconnect = jest.fn() + beforeAll(() => { + jest.mock('../../src/injected/sync', () => ({ + doDisconnect: doDisconnect + })) + }) + + beforeEach(() => { + jest.resetAllMocks() + }) + + it('reloads the component on reload', () => { + jest.isolateModules(() => { + const component = getComponent() + + const scriptWrapper = document.createElement("div") + const scriptTag = document.createElement("script") + scriptTag.src = "http://example.com" + scriptTag.setAttribute("data-original", "yes") + const currentScript = scriptWrapper.appendChild(scriptTag) + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const callouts = require('../../src/injected/callouts') + callouts.onConstruct(component, currentScript) + callouts.reloadComponent() + + // Disconnect websocket + expect(doDisconnect).toHaveBeenCalled() + + // Destroy the component + expect(component.destroy).toHaveBeenCalled() + + // Script element is replaced + expect(scriptWrapper.innerHTML).toEqual('') + }) + }) + + it('aborts without instance', () => { + jest.isolateModules(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const callouts = require('../../src/injected/callouts') + callouts.reloadComponent() + + expect(doDisconnect).toHaveBeenCalled() + }) + }) + + it('aborts without current script', () => { + jest.isolateModules(() => { + const component = getComponent() + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const callouts = require('../../src/injected/callouts') + callouts.onConstruct(component, null) + callouts.reloadComponent() + + expect(doDisconnect).toHaveBeenCalled() + expect(component.destroy).not.toHaveBeenCalled() + }) + }) + + it('aborts when currentscript is not script tag', () => { + jest.isolateModules(() => { + const component = getComponent() + + const scriptWrapper = document.createElement("div") + const scriptTag = document.createElementNS("http://www.w3.org/2000/svg", "script") + const currentScript = scriptWrapper.appendChild(scriptTag) + + const scriptWrapperHTML = scriptWrapper.innerHTML + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const callouts = require('../../src/injected/callouts') + callouts.onConstruct(component, currentScript) + callouts.reloadComponent() + + // Disconnect websocket + expect(doDisconnect).toHaveBeenCalled() + + // Do not abort, since we cannot rebuild + expect(component.destroy).not.toHaveBeenCalled() + + // Script element is NOT replaced + expect(scriptWrapper.innerHTML).toEqual(scriptWrapperHTML) + }) + }) + + it('aborts when script has no parent', () => { + jest.isolateModules(() => { + const component = getComponent() + + const currentScript = document.createElement("script") + currentScript.src = "http://example.tld" + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const callouts = require('../../src/injected/callouts') + callouts.onConstruct(component, currentScript) + callouts.reloadComponent() + + // Disconnect websocket + expect(doDisconnect).toHaveBeenCalled() + + // Destroy the component + expect(component.destroy).not.toHaveBeenCalled() + }) + }) +}) + +describe('updated code calls callout functions', () => { + let injectMock: typeof injected + const currentScript = document.createElement("script") + + const init = () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const sample = require("../samples/noConstructor") + const sampleClass: ComponentFramework.StandardControl = new sample.SampleComponent() + const ctx = mock>() + const container = document.createElement("div") + + return { sampleClass, ctx, container } + } + + beforeAll(() => { + injectMock = mockDeep({ + hasParams: jest.fn().mockReturnValue(false) + }) + jest.mock('pcf-reloader-transformer/dist/injected', () => injectMock) + + Object.defineProperty(global.document, "currentScript", { value: currentScript }) + }) + + beforeEach(() => { + mockClear(injectMock) + jest.clearAllMocks() + }) + + it.each([true,false])("calls constructor when hasParams [%p]", (hasParams: boolean) => { + jest.isolateModules(() => { + (injectMock.hasParams as jest.Mock).mockReturnValueOnce(hasParams) + + const { sampleClass } = init() + expect(injectMock.hasParams).toHaveBeenCalledTimes(1) + expect(injectMock.onConstruct).toHaveBeenCalledTimes(hasParams ? 2 : 1) + expect(injectMock.onConstruct).toHaveBeenCalledWith(sampleClass, currentScript) + }) + }) + + it('calls updateContext', () => { + const { sampleClass, ctx, container } = init() + + sampleClass.init(ctx, jest.fn(), {}, container) + sampleClass.updateView(ctx) + + expect(injectMock.onUpdateContext).toHaveBeenCalledWith(ctx) + }) + + it('calls connect on init', () => { + const { sampleClass, ctx, container } = init() + + const noc = jest.fn() + const state = mock() + sampleClass.init(ctx, noc, state, container) + + const params = expect.objectContaining({ + context: ctx, + container: container, + notifyOutputChanged: noc, + state: state + }) + + expect(injectMock.doConnect).toHaveBeenCalledWith(expect.any(String), params) + }) +}) diff --git a/tests/injected/logger.test.ts b/tests/injected/logger.test.ts new file mode 100644 index 0000000..11bbb43 --- /dev/null +++ b/tests/injected/logger.test.ts @@ -0,0 +1,34 @@ +import { log } from "../../src/injected"; + +describe('Run-time console logger', () => { + let messages: string[] = [] + const consoleSpy = jest.spyOn(console, 'log').mockImplementation((...m) => messages.push(m.join(' '))).mockName("console.log") + + beforeAll(() => { + jest.useFakeTimers() + .setSystemTime(new Date('2022-01-01 01:02:03.456')) + }) + + afterAll(() => { + jest.useRealTimers() + }) + + beforeEach(() => { + jest.clearAllMocks() + messages = [] + }) + + it("writes a test message", () => { + log("Test") + + expect(consoleSpy).toHaveBeenCalledWith("[pcf-reloader]", "[01:02:03.456]", "Test") + expect(messages).toEqual(expect.arrayContaining(["[pcf-reloader] [01:02:03.456] Test"])) + }) + + it("supports multiple messages", () => { + log("Test", "Multiple", "Messages") + + expect(consoleSpy).toHaveBeenCalledWith("[pcf-reloader]", "[01:02:03.456]", "Test", "Multiple", "Messages") + expect(messages).toEqual(expect.arrayContaining(["[pcf-reloader] [01:02:03.456] Test Multiple Messages"])) + }) +}) diff --git a/tests/injected/sync.bs.test.ts b/tests/injected/sync.bs.test.ts new file mode 100644 index 0000000..b4c31ef --- /dev/null +++ b/tests/injected/sync.bs.test.ts @@ -0,0 +1,100 @@ +/** + * @jest-environment jsdom + */ + +import { mock } from "jest-mock-extended"; +import socket, { io } from "socket.io-client"; +import SocketIoMock from "socket.io-mock"; + +import { + doConnect, + ReloadParams, +} from "../../src/injected/sync"; +import { + SocketClient, + SocketMock, +} from "../socket.io-mock"; + +jest.mock('../../src/injected/callouts', () => ({ + reloadComponent: jest.fn().mockName("reloadComponent") +})) + +jest.mock('../../src/injected/logger', () => ({ + log: jest.fn().mockName("log") +})) + +jest.mock("socket.io-client", () => ({ + io: jest.fn(), + socket: jest.fn() +})) +const mockIO = (io as unknown) as jest.MockedFunction<() => SocketClient> +const mockSocket = (socket as unknown) as jest.MockedFunction<() => SocketClient> + +describe.skip("sync integration (bs)", () => { + const wsAddr = "http://localhost" + let socketServer: SocketMock + let socketClient: SocketClient + let port: number + + beforeAll(() => { + socketServer = new SocketIoMock() + socketClient = socketServer.socketClient + + socketServer.on("connect", () => { + socketServer.emit("connect", { status: "success" }) + }) + + mockIO.mockImplementation(() => { + socketClient.emit("connect") + return socketClient + }) + + mockSocket.mockImplementation(() => { + socketClient.emit("connect") + return socketClient + }) + + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("can connect and disconnect", async () => { + let serverReceivedConnect = false + socketServer.on("connect", () => { + serverReceivedConnect = true + }) + + doConnect(wsAddr + ":" + port, mock()) + + await mockIO + expect(mockIO).toHaveBeenCalledTimes(1) + expect(serverReceivedConnect).toBeTruthy() + /*const onConnect = () => { + + expect(log).toBeCalledWith("Live reload enabled on " + wsAddr + ":" + port + "/browser-sync") + doDisconnect() + } + + const onClose = () => { + expect(log).toBeCalledWith("Live reload disabled") + + io.off("connection", onConnect) + io.off("close", onClose) + + done() + } + + io.on("connection", onConnect) + io.on("close", onClose)*/ + }) + + it("calls reload", () => { + // TODO + }) + + it("logs messages", () => { + // TODO + }) +}) diff --git a/tests/injected/sync.ws.test.ts b/tests/injected/sync.ws.test.ts new file mode 100644 index 0000000..6df4e57 --- /dev/null +++ b/tests/injected/sync.ws.test.ts @@ -0,0 +1,88 @@ +/** + * @jest-environment jsdom + */ + +import { mock } from "jest-mock-extended"; +import { Server } from "mock-socket"; +import waitForExpect from "wait-for-expect"; + +import { + log, + reloadComponent, +} from "../../src/injected"; +import { + doConnect, + doDisconnect, + ReloadParams, +} from "../../src/injected/sync"; + +jest.mock('../../src/injected/callouts', () => ({ + reloadComponent: jest.fn().mockName("reloadComponent") +})) + +jest.mock('../../src/injected/logger', () => ({ + log: jest.fn().mockName("log") +})) + +describe("sync integration (ws)", () => { + const wsAddr = "ws://localhost:8080" + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("can connect and disconnect", (done) => { + const mockServer = new Server(wsAddr) + + mockServer.on('connection', () => { + expect(log).toBeCalledWith("Live reload enabled on " + wsAddr) + doDisconnect() + }) + + mockServer.on('close', () => { + expect(log).toBeCalledWith("Live reload disabled") + mockServer.stop(done) + }) + + doConnect(wsAddr, mock()) + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + it.each(["reload","refreshcss"])('calls reload on "%s"', (message: string, done: jest.DoneCallback) => { + const mockServer = new Server(wsAddr) + + mockServer.on('connection', (socket) => { + expect(log).toBeCalledWith("Live reload enabled on " + wsAddr) + + socket.send(message) + + waitForExpect(() => { + expect(reloadComponent).toHaveBeenCalled() + }).then(() => { + expect(log).toHaveBeenCalledWith("Reload triggered") + mockServer.stop(done) + }) + }) + + doConnect(wsAddr, mock()) + }) + + it("does not call on invalid message", (done) => { + const mockServer = new Server(wsAddr) + + mockServer.on('connection', (socket) => { + expect(log).toBeCalledWith("Live reload enabled on " + wsAddr) + + socket.send("unknown") + + waitForExpect(() => { + expect(log).toHaveBeenCalledWith("> unknown") + }).then(() => { + expect(reloadComponent).not.toHaveBeenCalled() + mockServer.stop(done) + }) + }) + + doConnect(wsAddr, mock(), true) + }) +}) diff --git a/tests/listener.test.ts b/tests/listener.test.ts deleted file mode 100644 index 59adb4a..0000000 --- a/tests/listener.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { transpileModule } from "typescript" -import { createListenerMethod } from "../src/lib/listener" -import { createRefreshMethod } from "../src/lib/refresher" -import { print, readFile } from "./utils/common" -import transformer, { IPluginConfig } from "../src/index" - -const expectedListener = "private listenToWSUpdates(params: PcfReloadParams) { window.pcfReloadParams = params; const address = \"ws://127.0.0.1:8181/ws\"; this._reloadSocket = new WebSocket(address); this._reloadSocket.onmessage = msg => { if (msg.data != \"reload\" && msg.data != \"refreshcss\") return; this.reloadComponent(); }; console.log(\"Live reload enabled on \" + address); }" - -test('listener is created', () => { - const { listener } = createListenerMethod() - - const text = print(listener, true) - expect(text).toBe(expectedListener) -}) - -test('socket var is declared', () => { - const { socketVarDecl } = createListenerMethod() - - const text = print(socketVarDecl, true) - expect(text).toBe("private _reloadSocket: WebSocket | undefined;") -}) - -test('Refresher is created', () => { - const refresher = createRefreshMethod() - - const text = print(refresher, true) - expect(text).toBe("private reloadComponent() { console.log(\"Reload triggered\"); this.destroy(); if (this._reloadSocket) { this._reloadSocket.onmessage = null; this._reloadSocket.close(); } const isScript = (s: HTMLOrSVGScriptElement): s is HTMLScriptElement => !!(s as HTMLScriptElement).src; if (!currentScript || !isScript(currentScript)) return; const script = document.createElement(\"script\"); script.src = currentScript.src; const parent = currentScript.parentNode; if (!parent) return; currentScript.remove(); parent.appendChild(script); }") -}) - -test('listener address can be specified', () => { - const { listener } = createListenerMethod("wss://test.tld:8080/ws") - - const text = print(listener, true) - - expect(text).toBe(expectedListener.replace("ws://127.0.0.1:8181/ws", "wss://test.tld:8080/ws")) -}) - -it('can specify listener address from options', () => { - const { data, filePath } = readFile(`index.ts`) - - const address = "wss://some.tld:8283/wss" - const pluginOptions: IPluginConfig = { wsAddress: address } - - const output = transpileModule(data, { - fileName: filePath, - transformers: { - before: [transformer(pluginOptions)] - } - }) - - const { data: result } = readFile(`index.js`, '../samples') - - const replaced = result.replace("ws://127.0.0.1:8181/ws", address) - - expect(output.outputText).toBe(replaced) -}) \ No newline at end of file diff --git a/tests/methodUpdates.test.ts b/tests/methodUpdates.test.ts index 8c4a887..60f0927 100644 --- a/tests/methodUpdates.test.ts +++ b/tests/methodUpdates.test.ts @@ -1,64 +1,89 @@ -import { factory, MethodDeclaration } from "typescript" -import { print } from "./utils/common" -import * as m from "../src/lib/methodUpdates" -import { buildClass, extractMethod } from "./utils/codeGeneration" +import { + factory, + MethodDeclaration, +} from "typescript"; -it.each` +import * as m from "../src/builders/methodUpdates"; +import { + buildClass, + extractMethod, +} from "./utils/codeGeneration"; +import { + isDefined, + print, +} from "./utils/common"; + +const initBodyInner = 'const _pcfReloaderParams = { context: param0, notifyOutputChanged: param1, state: param2, container: param3 }; _pcfReloadLib.doConnect("http://localhost:8181", _pcfReloaderParams);' +const initBody = (oldBody: string) => `init(param0, param1, param2, param3) { ${initBodyInner}${oldBody} }` +const updateBody = (oldBody: string) => `updateView(context: ComponentFramework.Context) { _pcfReloadLib.onUpdateContext(context);${oldBody} }` + +const parms = (count: number) => Array.from(Array(count).keys()).map((_, i) => 'param' + i).join(", ") + +describe('method updates', () => { + it.each` body | description ${"{}"} | ${"empty"} ${"{ console.log(); }"} | ${"existing"} ${""} | ${"undefined"} -`("init method, body is $description", ({body, description: _description}: {body: string, description: string}) => { - const { method } = extractMethod(buildClass(`init(param0, param1, param2, param3) ${body}`)) - if (!method) expect(method).toBeDefined() - const source = handleMethodInternal(method!) - - const normalized = body.replace("{", "").replace("}", "").trim() - const oldBody = normalized.length ? " " + normalized : "" - expect(source).toBe(`init(param0, param1, param2, param3) { this.listenToWSUpdates({ context: param0, notifyOutputChanged: param1, state: param2, container: param3 });${oldBody} }`) -}) +`("init method, body is $description", ({ body }: { body: string }) => { + const { method } = extractMethod(buildClass(`init(param0, param1, param2, param3) ${body}`)) + isDefined(method) + const source = handleMethodInternal(method) -test('updateView body is built', () => { - const { method } = extractMethod(buildClass("public updateView(context: ComponentFramework.Context): void { this._container.innerHTML = \"
Hello, world!
\" }")) - if (!method) expect(method).toBeDefined() - const source = handleMethodInternal(method!) + const normalized = body.replace("{", "").replace("}", "").trim() + const oldBody = normalized.length ? " " + normalized : "" - // For some reason the printer doesn't print strings - expect(source).toBe("public updateView(context: ComponentFramework.Context): void { if (window.pcfReloadParams) window.pcfReloadParams.context = context; this._container.innerHTML = ; }") -}) + expect(source).toBe(initBody(oldBody)) + }) -test.each` - func | count - ${"init"} | ${4} - ${"updateView"} | ${1} -`('$func not touched with wrong parameter count (!= $count)', ({func, count}) => { - const parms = Array.from(Array(count - 1).keys()).map((_, i) => 'param' + i).join(", ") - const { method } = extractMethod(buildClass(`${func}(${parms}) { }`)) - if (!method) expect(method).toBeDefined() - const node = m.handleMethod(method!) + it.each` + body | description + ${"{}"} | ${"empty"} + ${"{ console.log(); }"} | ${"existing"} + ${""} | ${"undefined"} + `("updateView body is $description", ({ body }: { body: string }) => { + const { method } = extractMethod(buildClass(`updateView(context: ComponentFramework.Context) ${body}`)) + isDefined(method) + const source = handleMethodInternal(method) - expect(node).toBeUndefined() -}) + const normalized = body.replace("{", "").replace("}", "").trim() + const oldBody = normalized.length ? " " + normalized : "" -test.each` + expect(source).toBe(updateBody(oldBody)) + }) + + test.each` func | count ${"init"} | ${4} ${"updateView"} | ${1} -`('$func not touched with wrong parameter name', ({ func, count }) => { - const parms = Array.from(Array(count).keys()).map((_, i) => `{ param${i} }`).join(", ") - console.log(parms) - const { method } = extractMethod(buildClass(`${func}(${parms}) { }`)) + `('$func not touched with too few parameter count (!= $count)', ({ func, count }) => { + const { method } = extractMethod(buildClass(`${func}(${parms(count - 1)}) { }`)) + isDefined(method) + const node = m.handleMethod(method, {}) + + expect(node).toBeUndefined() + }) - if (!method) expect(method).toBeDefined() - const node = m.handleMethod(method!) + test.each` + func | count | inner + ${"init"} | ${4} | ${initBodyInner} + ${"updateView"} | ${1} | ${"_pcfReloadLib.onUpdateContext(param0);"} + `('$func updated with too many parameters (!= $count)', ({ func, count, inner }) => { + const p = parms(count + 1) + const classDef = buildClass(`${func}(${p}) { }`) + const { method } = extractMethod(classDef) + isDefined(method) - expect(node).toBeUndefined() + const source = handleMethodInternal(method) + + expect(source).toBe(`${func}(${p}) { ${inner} }`) + }) }) function handleMethodInternal(method: MethodDeclaration) { - const node = m.handleMethod(method!) - expect(node).toBeDefined() - + const node = m.handleMethod(method, {}) + isDefined(node) + const methodDecl = factory.updateMethodDeclaration(method, method.decorators, method.modifiers, diff --git a/tests/paramsType.test.ts b/tests/paramsType.test.ts deleted file mode 100644 index 25a5674..0000000 --- a/tests/paramsType.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createParamsType } from "../src/lib/paramsType" -import { print } from "./utils/common" - -test('creates static type declaration', () => { - const node = createParamsType() - - const sourceLines = print(node, true) - expect(sourceLines).toStrictEqual("type PcfReloadParams = { context: ComponentFramework.Context; notifyOutputChanged: () => void; state: ComponentFramework.Dictionary; container: HTMLDivElement; };") -}) \ No newline at end of file diff --git a/tests/sample.test.ts b/tests/sample.test.ts index 3ce2d69..d4d92d2 100644 --- a/tests/sample.test.ts +++ b/tests/sample.test.ts @@ -1,92 +1,95 @@ -import path from "path" -import fs from "fs" -import ts from "typescript" -import transformer, { IPluginConfig } from "../src/index" -import { readFile } from "./utils/common" +import fs from "fs"; +import path from "path"; +import ts from "typescript"; -let messages: string[] = [] +import transformer from "../src/index"; +import { IPluginConfig } from "../src/pluginConfig"; +import { readFile } from "./utils/common"; const writeFileSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation().mockName("fs.writeFileSync") +let messages: string[] = [] const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((m) => messages.push(m)).mockName("console.log") -beforeEach(() => { - // Reset the spys - jest.clearAllMocks() - messages = [] -}) - -test.each([ - 'index', - 'initialClass', - 'noConstructor', - 'notMatching', - 'patched' -])('can handle full file (%s)', (f) => { - const { data, filePath } = readFile(`${f}.ts`) - - const pluginOptions = {} - - const output = ts.transpileModule(data, { - fileName: filePath, - transformers: { - before: [transformer(pluginOptions)] - } +describe('full sample compile', () => { + beforeEach(() => { + // Reset the spys + jest.clearAllMocks() + messages = [] }) - const { data: result } = readFile(`${f}.js`, '../samples') - - expect(output.outputText).toBe(result) -}) + test.each([ + 'index', + 'initialClass', + 'noConstructor', + 'notMatching', + 'patched' + ])('can handle full file (%s)', (f) => { + const { data, filePath } = readFile(`${f}.ts`) + const pluginOptions = {} -it('can output to file', () => { - const { data, filePath } = readFile(`index.ts`) + const output = ts.transpileModule(data, { + fileName: filePath, + transformers: { + before: [transformer(pluginOptions)] + } + }) - const pluginOptions: IPluginConfig = { printGenerated: true } + const { data: result } = readFile(`${f}.js`, '../samples') - ts.transpileModule(data, { - fileName: filePath, - transformers: { - before: [transformer(pluginOptions)] - } + expect(output.outputText).toBe(result) }) - const np = path.resolve(path.dirname(filePath), 'index.generated.ts') - expect(writeFileSpy).toHaveBeenCalledWith(np, expect.any(String)) -}) -it('can be verbose', () => { - const { data, filePath } = readFile(`index.ts`) + it('can output to file', () => { + const { data, filePath } = readFile(`index.ts`) + + const pluginOptions: IPluginConfig = { printGenerated: true } - const pluginOptions: IPluginConfig = { verbose: true, printGenerated: true } + ts.transpileModule(data, { + fileName: filePath, + transformers: { + before: [transformer(pluginOptions)] + } + }) - ts.transpileModule(data, { - fileName: filePath, - transformers: { - before: [transformer(pluginOptions)] - } + const np = path.resolve(path.dirname(filePath), 'index.generated.ts') + expect(writeFileSpy).toHaveBeenCalledWith(np, expect.any(String)) }) - const seps = filePath.replace(/\\/g, "\/") - const np = path.resolve(path.dirname(filePath), 'index.generated.ts') - expect(consoleLogSpy).toHaveBeenCalledWith(`Found class: SampleComponent in ${seps}:3`) - expect(consoleLogSpy).toHaveBeenCalledWith(`Generated file written to: ${np}`) -}) + it('can be verbose', () => { + const { data, filePath } = readFile(`index.ts`) -it('prints warning when skipping certain files', () => { - - const { data, filePath } = readFile(`patched.ts`) + const pluginOptions: IPluginConfig = { verbose: true, printGenerated: true } - const pluginOptions: IPluginConfig = { verbose: true, printGenerated: true } + ts.transpileModule(data, { + fileName: filePath, + transformers: { + before: [transformer(pluginOptions)] + } + }) - ts.transpileModule(data, { - fileName: filePath, - transformers: { - before: [transformer(pluginOptions)] - } + const seps = filePath.replace(/\\/g, "/") + const np = path.resolve(path.dirname(filePath), 'index.generated.ts') + expect(consoleLogSpy).toHaveBeenCalledWith(`Found class: SampleComponent in ${seps}:3`) + expect(consoleLogSpy).toHaveBeenCalledWith(`Generated file written to: ${np}`) }) - const replaced = filePath.replace(/\\/g, "\/") - expect(consoleLogSpy).toHaveBeenCalledWith(`Params type already declared, skipping ${replaced}`) - expect(writeFileSpy).toHaveBeenCalledTimes(0) -}) \ No newline at end of file + it('prints warning when skipping certain files', () => { + + const { data, filePath } = readFile(`patched.ts`) + + const pluginOptions: IPluginConfig = { verbose: true, printGenerated: true } + + ts.transpileModule(data, { + fileName: filePath, + transformers: { + before: [transformer(pluginOptions)] + } + }) + + const replaced = filePath.replace(/\\/g, "/") + expect(consoleLogSpy).toHaveBeenCalledWith(`PCF Reloader already injected, skipping ${replaced}`) + expect(writeFileSpy).toHaveBeenCalledTimes(0) + }) +}) diff --git a/tests/samples/index.js b/tests/samples/index.js index bbb58df..549d825 100644 --- a/tests/samples/index.js +++ b/tests/samples/index.js @@ -1,17 +1,14 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SampleComponent = void 0; -var currentScript = document.currentScript; +var _pcfReloadLib = require("pcf-reloader-transformer/dist/injected"); +var _pcfReloadCurrentScript = document.currentScript; var SampleComponent = /** @class */ (function () { /** * Empty constructor. */ function SampleComponent() { - if (window.pcfReloadParams) { - var params = window.pcfReloadParams; - this.init(params.context, params.notifyOutputChanged, params.state, params.container); - this.updateView(params.context); - } + _pcfReloadLib.onConstruct(this, _pcfReloadCurrentScript); } /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. @@ -22,12 +19,13 @@ var SampleComponent = /** @class */ (function () { * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. */ SampleComponent.prototype.init = function (context, notifyOutputChanged, state, container) { - this.listenToWSUpdates({ + var _pcfReloaderParams = { context: context, notifyOutputChanged: notifyOutputChanged, state: state, container: container - }); + }; + _pcfReloadLib.doConnect("http://localhost:8181", _pcfReloaderParams); this._container = container; }; /** @@ -35,8 +33,7 @@ var SampleComponent = /** @class */ (function () { * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions */ SampleComponent.prototype.updateView = function (context) { - if (window.pcfReloadParams) - window.pcfReloadParams.context = context; + _pcfReloadLib.onUpdateContext(context); this._container.innerHTML = "
Hello, world!
"; }; /** @@ -52,38 +49,8 @@ var SampleComponent = /** @class */ (function () { */ SampleComponent.prototype.destroy = function () { }; - SampleComponent.prototype.listenToWSUpdates = function (params) { - var _this = this; - window.pcfReloadParams = params; - var address = "ws://127.0.0.1:8181/ws"; - this._reloadSocket = new WebSocket(address); - this._reloadSocket.onmessage = function (msg) { - if (msg.data != "reload" && msg.data != "refreshcss") - return; - _this.reloadComponent(); - }; - console.log("Live reload enabled on " + address); - }; - SampleComponent.prototype.reloadComponent = function () { - console.log("Reload triggered"); - this.destroy(); - if (this._reloadSocket) { - this._reloadSocket.onmessage = null; - this._reloadSocket.close(); - } - var isScript = function (s) { return !!s.src; }; - if (!currentScript || !isScript(currentScript)) - return; - var script = document.createElement("script"); - script.src = currentScript.src; - var parent = currentScript.parentNode; - if (!parent) - return; - currentScript.remove(); - parent.appendChild(script); - }; return SampleComponent; }()); exports.SampleComponent = SampleComponent; -if (window.pcfReloadParams) +if (_pcfReloadLib.hasParams()) new SampleComponent(); diff --git a/tests/samples/noConstructor.js b/tests/samples/noConstructor.js index 5bf0ca8..e9e4b20 100644 --- a/tests/samples/noConstructor.js +++ b/tests/samples/noConstructor.js @@ -1,14 +1,11 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SampleComponent = void 0; -var currentScript = document.currentScript; +var _pcfReloadLib = require("pcf-reloader-transformer/dist/injected"); +var _pcfReloadCurrentScript = document.currentScript; var SampleComponent = /** @class */ (function () { function SampleComponent() { - if (window.pcfReloadParams) { - var params = window.pcfReloadParams; - this.init(params.context, params.notifyOutputChanged, params.state, params.container); - this.updateView(params.context); - } + _pcfReloadLib.onConstruct(this, _pcfReloadCurrentScript); } /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. @@ -19,12 +16,13 @@ var SampleComponent = /** @class */ (function () { * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. */ SampleComponent.prototype.init = function (context, notifyOutputChanged, state, container) { - this.listenToWSUpdates({ + var _pcfReloaderParams = { context: context, notifyOutputChanged: notifyOutputChanged, state: state, container: container - }); + }; + _pcfReloadLib.doConnect("http://localhost:8181", _pcfReloaderParams); this._container = container; }; /** @@ -32,8 +30,7 @@ var SampleComponent = /** @class */ (function () { * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions */ SampleComponent.prototype.updateView = function (context) { - if (window.pcfReloadParams) - window.pcfReloadParams.context = context; + _pcfReloadLib.onUpdateContext(context); this._container.innerHTML = "
Hello, world!
"; }; /** @@ -49,38 +46,8 @@ var SampleComponent = /** @class */ (function () { */ SampleComponent.prototype.destroy = function () { }; - SampleComponent.prototype.listenToWSUpdates = function (params) { - var _this = this; - window.pcfReloadParams = params; - var address = "ws://127.0.0.1:8181/ws"; - this._reloadSocket = new WebSocket(address); - this._reloadSocket.onmessage = function (msg) { - if (msg.data != "reload" && msg.data != "refreshcss") - return; - _this.reloadComponent(); - }; - console.log("Live reload enabled on " + address); - }; - SampleComponent.prototype.reloadComponent = function () { - console.log("Reload triggered"); - this.destroy(); - if (this._reloadSocket) { - this._reloadSocket.onmessage = null; - this._reloadSocket.close(); - } - var isScript = function (s) { return !!s.src; }; - if (!currentScript || !isScript(currentScript)) - return; - var script = document.createElement("script"); - script.src = currentScript.src; - var parent = currentScript.parentNode; - if (!parent) - return; - currentScript.remove(); - parent.appendChild(script); - }; return SampleComponent; }()); exports.SampleComponent = SampleComponent; -if (window.pcfReloadParams) +if (_pcfReloadLib.hasParams()) new SampleComponent(); diff --git a/tests/samples/patched.js b/tests/samples/patched.js index bbb58df..65afb2e 100644 --- a/tests/samples/patched.js +++ b/tests/samples/patched.js @@ -1,17 +1,14 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SampleComponent = void 0; -var currentScript = document.currentScript; +var _pcfReloadLib = require("pcf-reloader-transformer/dist/injected"); +var _pcfReloadCurrentScript = document.currentScript; var SampleComponent = /** @class */ (function () { /** * Empty constructor. */ function SampleComponent() { - if (window.pcfReloadParams) { - var params = window.pcfReloadParams; - this.init(params.context, params.notifyOutputChanged, params.state, params.container); - this.updateView(params.context); - } + _pcfReloadLib.onConstruct(this, _pcfReloadCurrentScript); } /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. @@ -22,12 +19,13 @@ var SampleComponent = /** @class */ (function () { * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. */ SampleComponent.prototype.init = function (context, notifyOutputChanged, state, container) { - this.listenToWSUpdates({ + var _pcfReloaderParams = { context: context, notifyOutputChanged: notifyOutputChanged, state: state, container: container - }); + }; + _pcfReloadLib.doConnect("http://localhost:8181", _pcfReloaderParams); this._container = container; }; /** @@ -35,8 +33,7 @@ var SampleComponent = /** @class */ (function () { * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions */ SampleComponent.prototype.updateView = function (context) { - if (window.pcfReloadParams) - window.pcfReloadParams.context = context; + _pcfReloadLib.onUpdateContext(context); this._container.innerHTML = "
Hello, world!
"; }; /** @@ -50,40 +47,10 @@ var SampleComponent = /** @class */ (function () { * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. * i.e. cancelling any pending remote calls, removing listeners, etc. */ - SampleComponent.prototype.destroy = function () { - }; - SampleComponent.prototype.listenToWSUpdates = function (params) { - var _this = this; - window.pcfReloadParams = params; - var address = "ws://127.0.0.1:8181/ws"; - this._reloadSocket = new WebSocket(address); - this._reloadSocket.onmessage = function (msg) { - if (msg.data != "reload" && msg.data != "refreshcss") - return; - _this.reloadComponent(); - }; - console.log("Live reload enabled on " + address); - }; - SampleComponent.prototype.reloadComponent = function () { - console.log("Reload triggered"); - this.destroy(); - if (this._reloadSocket) { - this._reloadSocket.onmessage = null; - this._reloadSocket.close(); - } - var isScript = function (s) { return !!s.src; }; - if (!currentScript || !isScript(currentScript)) - return; - var script = document.createElement("script"); - script.src = currentScript.src; - var parent = currentScript.parentNode; - if (!parent) - return; - currentScript.remove(); - parent.appendChild(script); - }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + SampleComponent.prototype.destroy = function () { }; return SampleComponent; }()); exports.SampleComponent = SampleComponent; -if (window.pcfReloadParams) +if (_pcfReloadLib.hasParams()) new SampleComponent(); diff --git a/tests/socket.io-mock.d.ts b/tests/socket.io-mock.d.ts new file mode 100644 index 0000000..281962c --- /dev/null +++ b/tests/socket.io-mock.d.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/ban-types */ +type Emitter = { + new(obj?: Partial): Emitter + _callbacks: { [event: string]: Function[] } + emit: (event: string, ...args: []) => Emitter + listeners: (event: string) => Function[] + hasListeners: (event: string) => boolean + on: (event: string, callback: (payload?: unknown) => void) => Emitter + addEventListener: (event: string, callback: (payload?: unknown) => void) => Emitter + off: (event?: string, fn?: Function) => Emitter + removeListener: (event: string, fn: Function) => Emitter + removeAllListeners: () => Emitter + removeEventListeners: (event: string) => Emitter +} +export type SocketClient = Emitter & { + new(socketMock: SocketMock): SocketClient + connected: boolean + disconnected: boolean + _socketMock: SocketMock + _emitFn: (event: string, ...args: []) => Emitter + emit: (eventKey: string, payload?: unknown, ack?: () => void) => void + fireEvent: (eventKey: string, payload: unknown, callback?: () => void) => void + close: () => SocketClient + disconnect: () => SocketClient +} +export type SocketMock = Emitter & { + new(): SocketMock + generalCallbacks: {} + joinedRooms: string[] + rooms: string[] + socketClient: SocketClient + broadcast: { + to: ( + roomKey: string + ) => { + emit: (eventKey: string, payload?: unknown) => void + } + } + _emitFn: () => void + emitEvent: (eventKey: string, payload?: unknown, ack?: () => void) => void + onEmit: (eventKey: string, callback: (payload?: string, roomKey?: string) => void) => void + emit: (eventKey: string, payload?: unknown) => void + join: (roomKey: string) => void + leave: (roomKey: string) => void + monitor: (value: string) => string + disconnect: (callback?: () => void) => SocketMock +} diff --git a/tests/utils/codeGeneration.ts b/tests/utils/codeGeneration.ts index f76a1d0..df07c20 100644 --- a/tests/utils/codeGeneration.ts +++ b/tests/utils/codeGeneration.ts @@ -1,4 +1,18 @@ -import { Node, ConstructorDeclaration, createSourceFile, forEachChild, isClassDeclaration, isConstructorDeclaration, isMethodDeclaration, MethodDeclaration, ScriptKind, ScriptTarget, SourceFile } from "typescript" +import { + ConstructorDeclaration, + createSourceFile, + forEachChild, + isClassDeclaration, + isConstructorDeclaration, + isMethodDeclaration, + MethodDeclaration, + Node, + ScriptKind, + ScriptTarget, + SourceFile, +} from "typescript"; + +import { isDefined } from "./common"; export function buildClass(inner: string) { return `class test { ${inner} }` @@ -13,10 +27,10 @@ type SourceProps = { export function extractMethod(source: string): SourceProps { const sourceFile = createSourceFile("test.ts", source, ScriptTarget.Latest, true, ScriptKind.TS) const classDef = forEachChild(sourceFile, (n: Node) => isClassDeclaration(n) ? n : undefined) - expect(classDef).toBeDefined() + isDefined(classDef) - const constructor = forEachChild(classDef!, (n: Node) => isConstructorDeclaration(n) ? n : undefined) - const method = forEachChild(classDef!, (n: Node) => isMethodDeclaration(n) ? n : undefined) + const constructor = forEachChild(classDef, (n: Node) => isConstructorDeclaration(n) ? n : undefined) + const method = forEachChild(classDef, (n: Node) => isMethodDeclaration(n) ? n : undefined) return { constructor, method, sourceFile } } diff --git a/tests/utils/common.ts b/tests/utils/common.ts index d455337..94a14d3 100644 --- a/tests/utils/common.ts +++ b/tests/utils/common.ts @@ -1,10 +1,22 @@ -import { readFileSync } from "fs" -import path from "path" -import { createPrinter, EmitHint, factory, NodeFlags, SyntaxKind, Node, SourceFile } from "typescript" +import { readFileSync } from "fs"; +import path from "path"; +import { + createPrinter, + EmitHint, + factory, + Node, + NodeFlags, + SourceFile, + SyntaxKind, +} from "typescript"; const printer = createPrinter() const sourceFile = factory.createSourceFile([], factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.ContainsThis) +export function isDefined(val: T): asserts val is NonNullable { + expect(val).toBeDefined() +} + export function print(node: Node, collapse?: boolean) { const raw = printer.printNode(EmitHint.Unspecified, node, sourceFile) @@ -21,7 +33,7 @@ export function collapseString(s: string) { return s.replace(/\n/g, ' ').replace(/\s\s+/g, ' ') } -export function readFile(filename: string, subpath: string = "../../samples") { +export function readFile(filename: string, subpath = "../../samples") { const filePath = path.resolve(__dirname, subpath, filename) const data = readFileSync(filePath).toString() diff --git a/tsconfig.json b/tsconfig.json index 989b38e..4a67a69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,12 @@ "noFallthroughCasesInSwitch": true, "outDir": "./dist", "rootDir": "./src", - "esModuleInterop": true + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types", + "./types" + ] }, - "include": [ "src" ], - "exclude": [ "samples" ], + "include": [ "src", "types" ], + "exclude": [ "samples" ], } \ No newline at end of file diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..03faafc --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,2 @@ +declare module 'socket.io-client' +declare module "socket.io-mock"