Initial add of Azure DevOps Extension SDK library
This commit is contained in:
Коммит
8ed5a3c07e
|
@ -0,0 +1,3 @@
|
|||
npm-debug.log
|
||||
node_modules/
|
||||
bin/
|
|
@ -0,0 +1 @@
|
|||
npm-debug.log
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
|
@ -0,0 +1,47 @@
|
|||
# Azure DevOps Web Extension SDK
|
||||
|
||||
## Overview
|
||||
|
||||
Client SDK for developing [Azure DevOps extensions](https://docs.microsoft.com/en-us/azure/devops/extend/overview).
|
||||
|
||||
The client SDK enables web extensions to communicate to the host frame. It can be used to:
|
||||
|
||||
- Notify the host that the extension is loaded or has errors
|
||||
- Get basic contextual information about the current page (current user, host and extension information)
|
||||
- Get theme information
|
||||
- Obtain an authorization token to use in REST calls back to Azure DevOps
|
||||
- Get remote services offered by the host frame
|
||||
|
||||
A full API reference of can be found [here](https://docs.microsoft.com/en-us/javascript-typescript/azure-devops-extension-sdk).
|
||||
|
||||
## Get started with a new extension
|
||||
|
||||
See the [Develop a web extension for Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/extend/get-started/node?view=vsts) documentation for instructions on getting started with a new extension. You can also refer to the [azure-devops-extension-sample](https://github.com/Microsoft/azure-devops-extension-sample) repository as a working reference.
|
||||
|
||||
## Import the SDK within your extension project
|
||||
|
||||
1. Add `azure-devops-extension-sdk` to the list of dependencies in your package.json
|
||||
2. Add `import * as SDK from "azure-devops-extension-sdk"` to your TypeScript code
|
||||
|
||||
## Initialize the SDK
|
||||
|
||||
When you have rendered your extension content, call `SDK.init()`. Your extension content will not be displayed until you have notified the host frame that you are ready. There are two options for doing this:
|
||||
|
||||
1. Call `SDK.init()` with no `loaded` option
|
||||
2. Call `SDK.init({ loaded: false })` to start initializing the SDK. Then call `SDK.notifyLoadSucceeded()` once you have finished your initial rendering. This allows you to make other SDK calls while your content is still loading (and hidden behind a spinner).
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
import * as SDK from "azure-devops-extension-sdk";
|
||||
|
||||
SDK.init();
|
||||
```
|
||||
|
||||
## API
|
||||
A full API reference of can be found [here](https://docs.microsoft.com/en-us/javascript-typescript/azure-devops-extension-sdk).
|
||||
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* This is solely a build script, intended to prep the vss-ui npm package for publishing.
|
||||
*/
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const glob = require("glob");
|
||||
const path = require("path");
|
||||
const copy = require("recursive-copy");
|
||||
const shell = require("shelljs");
|
||||
const UglifyES = require("uglify-es");
|
||||
|
||||
(async function() {
|
||||
// Clean bin directory
|
||||
console.log("# Cleaning bin. Running shelljs rm -rf ./bin");
|
||||
shell.rm("-rf", "./bin");
|
||||
|
||||
// Compile typescript
|
||||
console.log("# Compiling TypeScript. Executing `node_modules\\.bin\\tsc -p ./tsconfig.json`.");
|
||||
|
||||
try {
|
||||
execSync("node_modules\\.bin\\tsc -p ./tsconfig.json", {
|
||||
stdio: [0, 1, 2],
|
||||
shell: true,
|
||||
cwd: __dirname,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("ERROR: Failed to build TypeScript.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Copy ts files to bin
|
||||
console.log("# Copy declare files to bin.");
|
||||
try {
|
||||
await copy(path.join(__dirname, "src"), path.join(__dirname, "bin"), {
|
||||
filter: f => {
|
||||
return f.endsWith(".d.ts");
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Copy failed. " + error);
|
||||
}
|
||||
|
||||
// Uglify JavaScript
|
||||
console.log("# Minifying JS using the UglifyES API, replacing un-minified files.");
|
||||
let count = 0;
|
||||
|
||||
const files = await new Promise((resolve, reject) => {
|
||||
glob("./bin/**/*.js", (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(files);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
if (file.includes("node_modules/")) {
|
||||
continue;
|
||||
}
|
||||
fs.writeFileSync(
|
||||
file.substr(0, file.length - 2) + "min.js",
|
||||
UglifyES.minify(fs.readFileSync(file, "utf-8"), { compress: true, mangle: true }).code,
|
||||
"utf-8",
|
||||
);
|
||||
count++;
|
||||
}
|
||||
console.log(`-- Minified ${count} files.`);
|
||||
|
||||
// Copy package.json, LICENSE, README.md to bin
|
||||
console.log("# Copying package.json, LICENSE, and README.md to bin.");
|
||||
try {
|
||||
await copy(path.join(__dirname, "package.json"), path.join(__dirname, "bin", "package.json"));
|
||||
await copy(path.join(__dirname, "LICENSE"), path.join(__dirname, "bin", "LICENSE"));
|
||||
await copy(path.join(__dirname, "README.md"), path.join(__dirname, "bin", "README.md"));
|
||||
} catch (error) {
|
||||
console.log("ERROR: Failed to copy package.json, LICENSE, or README.md - " + error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,396 @@
|
|||
{
|
||||
"name": "azure-devops-extension-sdk",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"array-differ": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
|
||||
"integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
|
||||
"dev": true
|
||||
},
|
||||
"array-union": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||
"integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-uniq": "1.0.3"
|
||||
}
|
||||
},
|
||||
"array-uniq": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
|
||||
"integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
|
||||
"dev": true
|
||||
},
|
||||
"arrify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
|
||||
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
|
||||
"dev": true
|
||||
},
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
|
||||
"dev": true
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"del": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
|
||||
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"globby": "5.0.0",
|
||||
"is-path-cwd": "1.0.0",
|
||||
"is-path-in-cwd": "1.0.1",
|
||||
"object-assign": "4.1.1",
|
||||
"pify": "2.3.0",
|
||||
"pinkie-promise": "2.0.1",
|
||||
"rimraf": "2.6.2"
|
||||
}
|
||||
},
|
||||
"emitter-mixin": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/emitter-mixin/-/emitter-mixin-0.0.3.tgz",
|
||||
"integrity": "sha1-WUjLKG8uSO3DslGnz8H3iDOW1lw=",
|
||||
"dev": true
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prr": "1.0.1"
|
||||
}
|
||||
},
|
||||
"es6-object-assign": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
||||
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz",
|
||||
"integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "1.0.0",
|
||||
"inflight": "1.0.6",
|
||||
"inherits": "2.0.3",
|
||||
"minimatch": "3.0.4",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "1.0.1"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
|
||||
"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-union": "1.0.2",
|
||||
"arrify": "1.0.1",
|
||||
"glob": "7.1.3",
|
||||
"object-assign": "4.1.1",
|
||||
"pify": "2.3.0",
|
||||
"pinkie-promise": "2.0.1"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
||||
"dev": true
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "1.4.0",
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true
|
||||
},
|
||||
"interpret": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
|
||||
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
|
||||
"dev": true
|
||||
},
|
||||
"is-path-cwd": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
|
||||
"integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
|
||||
"dev": true
|
||||
},
|
||||
"is-path-in-cwd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
|
||||
"integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-path-inside": "1.0.1"
|
||||
}
|
||||
},
|
||||
"is-path-inside": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
|
||||
"integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-is-inside": "1.0.2"
|
||||
}
|
||||
},
|
||||
"junk": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz",
|
||||
"integrity": "sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI=",
|
||||
"dev": true
|
||||
},
|
||||
"maximatch": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz",
|
||||
"integrity": "sha1-hs2NawTJ8wfAWmuUGZBtA2D7E6I=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-differ": "1.0.0",
|
||||
"array-union": "1.0.2",
|
||||
"arrify": "1.0.1",
|
||||
"minimatch": "3.0.4"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"dev": true
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
},
|
||||
"path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||
"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
|
||||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"dev": true
|
||||
},
|
||||
"pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
|
||||
"dev": true
|
||||
},
|
||||
"pinkie": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||
"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
|
||||
"dev": true
|
||||
},
|
||||
"pinkie-promise": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
|
||||
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pinkie": "2.0.4"
|
||||
}
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asap": "2.0.6"
|
||||
}
|
||||
},
|
||||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
||||
"dev": true
|
||||
},
|
||||
"rechoir": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"resolve": "1.8.1"
|
||||
}
|
||||
},
|
||||
"recursive-copy": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/recursive-copy/-/recursive-copy-2.0.9.tgz",
|
||||
"integrity": "sha512-0AkHV+QtfS/1jW01z3m2t/TRTW56Fpc+xYbsoa/bqn8BCYPwmsaNjlYmUU/dyGg9w8MmGoUWihU5W+s+qjxvBQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"del": "2.2.2",
|
||||
"emitter-mixin": "0.0.3",
|
||||
"errno": "0.1.7",
|
||||
"graceful-fs": "4.1.11",
|
||||
"junk": "1.0.3",
|
||||
"maximatch": "0.1.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"pify": "2.3.0",
|
||||
"promise": "7.3.1",
|
||||
"slash": "1.0.0"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
|
||||
"integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-parse": "1.0.6"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "7.1.3"
|
||||
}
|
||||
},
|
||||
"shelljs": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz",
|
||||
"integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "7.1.3",
|
||||
"interpret": "1.1.0",
|
||||
"rechoir": "0.6.2"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
|
||||
"integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-es": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.1.10.tgz",
|
||||
"integrity": "sha512-RwBX0aOeHvO8MKKUeLCArQGb9OZ6xA+EqfVxsE9wqK0saFYFVLIFvHeeCOg61C6NO6KCuSiG9OjNjCA+OB4nzg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "2.11.0",
|
||||
"source-map": "0.6.1"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "azure-devops-extension-sdk",
|
||||
"version": "2.0.2",
|
||||
"description": "Azure DevOps web extension JavaScript library.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azure-devops-extension-sdk.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node buildpackage.js"
|
||||
},
|
||||
"keywords": [
|
||||
"extensions",
|
||||
"Azure DevOps",
|
||||
"Visual Studio Team Services"
|
||||
],
|
||||
"author": "Microsoft",
|
||||
"license": "MIT",
|
||||
"homepage": "https://docs.microsoft.com/en-us/azure/devops/integrate",
|
||||
"main": "SDK.js",
|
||||
"types": "SDK.d.ts",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.2.5",
|
||||
"es6-object-assign": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"glob": "~7.1.2",
|
||||
"recursive-copy": "~2.0.7",
|
||||
"shelljs": "~0.7.8",
|
||||
"typescript": "^2.9.2",
|
||||
"uglify-es": "~3.1.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
import { channelManager } from "./XDM";
|
||||
|
||||
/**
|
||||
* Web SDK version number. Can be specified in an extension's set of demands like: vss-sdk-version/3.0
|
||||
*/
|
||||
export const sdkVersion = 3.0;
|
||||
|
||||
/**
|
||||
* Options for extension initialization -- passed to DevOps.init()
|
||||
*/
|
||||
export interface IExtensionInitOptions {
|
||||
|
||||
/**
|
||||
* True (the default) indicates that the content of this extension is ready to be shown/used as soon as the
|
||||
* init handshake has completed. Otherwise (loaded: false), the extension must call DevOps.notifyLoadSucceeded()
|
||||
* once it has finished loading.
|
||||
*/
|
||||
loaded?: boolean;
|
||||
|
||||
/**
|
||||
* Extensions that show UI should specify this to true in order for the current user's theme
|
||||
* to be applied to this extension content. Defaults to true.
|
||||
*/
|
||||
applyTheme?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the current user
|
||||
*/
|
||||
export interface IUserContext {
|
||||
|
||||
/**
|
||||
* Unique id for the user
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Name of the user (email/login)
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The user's display name (First name / Last name)
|
||||
*/
|
||||
displayName: string;
|
||||
|
||||
/**
|
||||
* Url to the user's profile image
|
||||
*/
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DevOps host level
|
||||
*/
|
||||
export const enum HostType {
|
||||
|
||||
/**
|
||||
* The Deployment host
|
||||
*/
|
||||
Deployment = 1,
|
||||
|
||||
/**
|
||||
* The Enterprise host
|
||||
*/
|
||||
Enterprise = 2,
|
||||
|
||||
/**
|
||||
* The organization host
|
||||
*/
|
||||
Organization = 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the current DevOps host (organization)
|
||||
*/
|
||||
export interface IHostContext {
|
||||
|
||||
/**
|
||||
* Unique GUID for this host
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Name of the host (i.e. Organization name)
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* DevOps host level
|
||||
*/
|
||||
type: HostType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier for the current extension
|
||||
*/
|
||||
export interface IExtensionContext {
|
||||
|
||||
/**
|
||||
* Full id of the extension <publisher>.<extension>
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Id of the publisher
|
||||
*/
|
||||
publisherId: string;
|
||||
|
||||
/**
|
||||
* Id of the extension (without the publisher prefix)
|
||||
*/
|
||||
extensionId: string;
|
||||
}
|
||||
|
||||
interface IExtensionHandshakeOptions extends IExtensionInitOptions {
|
||||
|
||||
/**
|
||||
* Version number of this SDK
|
||||
*/
|
||||
sdkVersion: number;
|
||||
}
|
||||
|
||||
interface IExtensionHandshakeResult {
|
||||
contributionId: string;
|
||||
context: {
|
||||
extension: IExtensionContext,
|
||||
user: IUserContext,
|
||||
host: IHostContext
|
||||
},
|
||||
initialConfig?: { [key: string]: any };
|
||||
themeData?: { [ key: string]: string };
|
||||
}
|
||||
|
||||
const hostControlId = "DevOps.HostControl";
|
||||
const serviceManagerId = "DevOps.ServiceManager";
|
||||
const parentChannel = channelManager.addChannel(window.parent);
|
||||
|
||||
let extensionContext: IExtensionContext | undefined;
|
||||
let initialConfiguration: { [key: string]: any } | undefined;
|
||||
let initialContributionId: string | undefined;
|
||||
let userContext: IUserContext | undefined;
|
||||
let hostContext: IHostContext | undefined;
|
||||
let themeElement: HTMLStyleElement;
|
||||
|
||||
let resolveReady: () => void;
|
||||
const readyPromise = new Promise<void>((resolve) => {
|
||||
resolveReady = resolve;
|
||||
});
|
||||
|
||||
/**
|
||||
* Register a method so that the host frame can invoke events
|
||||
*/
|
||||
function dispatchEvent(eventName: string, params: any) {
|
||||
const global = window as any;
|
||||
|
||||
let evt;
|
||||
if (typeof global.CustomEvent === "function") {
|
||||
evt = new global.CustomEvent(eventName, params);
|
||||
}
|
||||
else {
|
||||
params = params || { bubbles: false, cancelable: false };
|
||||
evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail);
|
||||
}
|
||||
|
||||
window.dispatchEvent(evt);
|
||||
}
|
||||
parentChannel.getObjectRegistry().register("DevOps.SdkClient", {
|
||||
dispatchEvent: dispatchEvent
|
||||
});
|
||||
|
||||
/**
|
||||
* Initiates the handshake with the host window.
|
||||
*
|
||||
* @param options - Initialization options for the extension.
|
||||
*/
|
||||
export function init(options?: IExtensionInitOptions): Promise<void> {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
const initOptions = { ...options, sdkVersion };
|
||||
|
||||
parentChannel.invokeRemoteMethod<IExtensionHandshakeResult>("initialHandshake", hostControlId, [initOptions]).then((handshakeData) => {
|
||||
|
||||
initialConfiguration = handshakeData.initialConfig || {};
|
||||
initialContributionId = handshakeData.contributionId;
|
||||
|
||||
const context = handshakeData.context;
|
||||
extensionContext = context.extension;
|
||||
userContext = context.user;
|
||||
hostContext = context.host;
|
||||
|
||||
if (handshakeData.themeData) {
|
||||
applyTheme(handshakeData.themeData);
|
||||
}
|
||||
|
||||
resolveReady();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback that gets called once the initial setup/handshake has completed.
|
||||
* If the initial setup is already completed, the callback is invoked at the end of the current call stack.
|
||||
*/
|
||||
export async function ready(): Promise<void> {
|
||||
return readyPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the host that the extension successfully loaded (stop showing the loading indicator)
|
||||
*/
|
||||
export function notifyLoadSucceeded(): Promise<void> {
|
||||
return parentChannel.invokeRemoteMethod("notifyLoadSucceeded", hostControlId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the host that the extension failed to load
|
||||
*/
|
||||
export function notifyLoadFailed(e: Error | string): Promise<void> {
|
||||
return parentChannel.invokeRemoteMethod("notifyLoadFailed", hostControlId, [e]);
|
||||
}
|
||||
|
||||
function getWaitForReadyError(method: string): string {
|
||||
return `Attempted to call ${method}() before init() was complete. Wait for init to complete or place within a ready() callback.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration data passed in the initial handshake from the parent frame
|
||||
*/
|
||||
export function getConfiguration(): { [key: string]: any } {
|
||||
if (!initialConfiguration) {
|
||||
throw new Error(getWaitForReadyError("getConfiguration"));
|
||||
}
|
||||
return initialConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the information about the contribution that first caused this extension to load.
|
||||
*/
|
||||
export function getContributionId(): string {
|
||||
if (!initialContributionId) {
|
||||
throw new Error(getWaitForReadyError("getContributionId"));
|
||||
}
|
||||
return initialContributionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the current user
|
||||
*/
|
||||
export function getUser(): IUserContext {
|
||||
if (!userContext) {
|
||||
throw new Error(getWaitForReadyError("getUser"));
|
||||
}
|
||||
return userContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the host (i.e. an Azure DevOps organization) that the page is targeting
|
||||
*/
|
||||
export function getHost(): IHostContext {
|
||||
if (!hostContext) {
|
||||
throw new Error(getWaitForReadyError("getHost"));
|
||||
}
|
||||
return hostContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context about the extension that owns the content that is being hosted
|
||||
*/
|
||||
export function getExtensionContext(): IExtensionContext {
|
||||
if (!extensionContext) {
|
||||
throw new Error(getWaitForReadyError("getExtensionContext"));
|
||||
}
|
||||
return extensionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contribution with the given contribution id. The returned contribution has a method to get a registered object within that contribution.
|
||||
*
|
||||
* @param contributionId - Id of the contribution to get
|
||||
*/
|
||||
export async function getService<T>(contributionId: string): Promise<T> {
|
||||
return ready().then(() => {
|
||||
return parentChannel.invokeRemoteMethod<T>("getService", serviceManagerId, [contributionId]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an object (instance or factory method) that this extension exposes to the host frame.
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param instance - Either: (1) an object instance, or (2) a function that takes optional context data and returns an object instance.
|
||||
*/
|
||||
export function register<T = any>(instanceId: string, instance: T): void {
|
||||
parentChannel.getObjectRegistry().register(instanceId, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an object that this extension exposed to the host frame.
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
*/
|
||||
export function unregister(instanceId: string): void {
|
||||
parentChannel.getObjectRegistry().unregister(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an access token which will allow calls to be made to other DevOps services
|
||||
*/
|
||||
export async function getAccessToken(): Promise<string> {
|
||||
return parentChannel.invokeRemoteMethod<{ token: string }>("getAccessToken", hostControlId).then((tokenObj) => { return tokenObj.token; });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an token which can be used to identify the current user
|
||||
*/
|
||||
export async function getAppToken(): Promise<string> {
|
||||
return parentChannel.invokeRemoteMethod<{ token: string }>("getAppToken", hostControlId).then((tokenObj) => { return tokenObj.token; });
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the parent window to resize the container for this extension based on the current extension size.
|
||||
*
|
||||
* @param width - Optional width, defaults to scrollWidth
|
||||
* @param height - Optional height, defaults to scrollHeight
|
||||
*/
|
||||
export function resize(width?: number, height?: number): void {
|
||||
const body = document.body;
|
||||
if (body) {
|
||||
const newWidth = typeof width === "number" ? width : (body ? body.scrollWidth : undefined);
|
||||
const newHeight = typeof height === "number" ? height : (body ? body.scrollHeight : undefined);
|
||||
|
||||
parentChannel.invokeRemoteMethod("resize", hostControlId, [newWidth, newHeight]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies theme variables to the current document
|
||||
*/
|
||||
export function applyTheme(themeData: { [varName: string]: string }): void {
|
||||
|
||||
if (!themeElement) {
|
||||
themeElement = document.createElement("style");
|
||||
themeElement.type = "text/css";
|
||||
document.head!.appendChild(themeElement);
|
||||
}
|
||||
|
||||
const cssVariables = [];
|
||||
if (themeData) {
|
||||
for (const varName in themeData) {
|
||||
cssVariables.push("--" + varName + ": " + themeData[varName]);
|
||||
}
|
||||
}
|
||||
|
||||
themeElement.innerText = ":root { " + cssVariables.join("; ") + " } body { color: var(--text-primary-color) }";
|
||||
|
||||
dispatchEvent("themeApplied", { detail: themeData });
|
||||
}
|
|
@ -0,0 +1,666 @@
|
|||
import "es6-promise/auto";
|
||||
import "es6-object-assign/auto";
|
||||
|
||||
/**
|
||||
* Interface for a single XDM channel
|
||||
*/
|
||||
export interface IXDMChannel {
|
||||
|
||||
/**
|
||||
* Invoke a method via RPC. Lookup the registered object on the remote end of the channel and invoke the specified method.
|
||||
*
|
||||
* @param method - Name of the method to invoke
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param params - Arguments to the method to invoke
|
||||
* @param instanceContextData - Optional context data to pass to a registered object's factory method
|
||||
*/
|
||||
invokeRemoteMethod<T>(methodName: string, instanceId: string, params?: any[], instanceContextData?: Object): Promise<T>;
|
||||
|
||||
/**
|
||||
* Get a proxied object that represents the object registered with the given instance id on the remote side of this channel.
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param contextData - Optional context data to pass to a registered object's factory method
|
||||
*/
|
||||
getRemoteObjectProxy<T>(instanceId: string, contextData?: Object): Promise<T>;
|
||||
|
||||
/**
|
||||
* Get the object registry to handle messages from this specific channel.
|
||||
* Upon receiving a message, this channel registry will be used first, then
|
||||
* the global registry will be used if no handler is found here.
|
||||
*/
|
||||
getObjectRegistry(): IXDMObjectRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of XDM channels kept per target frame/window
|
||||
*/
|
||||
export interface IXDMChannelManager {
|
||||
|
||||
/**
|
||||
* Add an XDM channel for the given target window/iframe
|
||||
*
|
||||
* @param window - Target iframe window to communicate with
|
||||
* @param targetOrigin - Url of the target iframe (if known)
|
||||
*/
|
||||
addChannel(window: Window, targetOrigin?: string): IXDMChannel;
|
||||
|
||||
/**
|
||||
* Removes an XDM channel, allowing it to be disposed
|
||||
*
|
||||
* @param channel - The channel to remove from the channel manager
|
||||
*/
|
||||
removeChannel(channel: IXDMChannel): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of XDM objects that can be invoked by an XDM channel
|
||||
*/
|
||||
export interface IXDMObjectRegistry {
|
||||
|
||||
/**
|
||||
* Register an object (instance or factory method) exposed by this frame to callers in a remote frame
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param instance - Either: (1) an object instance, or (2) a function that takes optional context data and returns an object instance.
|
||||
*/
|
||||
register(instanceId: string, instance: Object | { (contextData?: any): Object; }): void;
|
||||
|
||||
/**
|
||||
* Unregister an object (instance or factory method) that was previously registered by this frame
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
*/
|
||||
unregister(instanceId: string): void;
|
||||
|
||||
/**
|
||||
* Get an instance of an object registered with the given id
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param contextData - Optional context data to pass to the contructor of an object factory method
|
||||
*/
|
||||
getInstance<T>(instanceId: string, contextData?: Object): T | undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Settings related to the serialization of data across iframe boundaries.
|
||||
*/
|
||||
export interface ISerializationSettings {
|
||||
|
||||
/**
|
||||
* By default, properties that begin with an underscore are not serialized across
|
||||
* the iframe boundary. Set this option to true to serialize such properties.
|
||||
*/
|
||||
includeUnderscoreProperties: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a remote procedure call (rpc) between frames.
|
||||
*/
|
||||
export interface IJsonRpcMessage {
|
||||
id: number;
|
||||
instanceId?: string;
|
||||
instanceContext?: Object;
|
||||
methodName?: string;
|
||||
params?: any[]; // if method is present then params should be present
|
||||
result?: any; // method, result, and error are mutucally exclusive. method is set for requrests, result and error are for responses
|
||||
error?: any;
|
||||
handshakeToken?: string;
|
||||
serializationSettings?: ISerializationSettings;
|
||||
}
|
||||
|
||||
const smallestRandom = parseInt("10000000000", 36);
|
||||
const maxSafeInteger: number = (<any>Number).MAX_SAFE_INTEGER || 9007199254740991;
|
||||
|
||||
/**
|
||||
* Create a new random 22-character fingerprint.
|
||||
* @return string fingerprint
|
||||
*/
|
||||
function newFingerprint() {
|
||||
// smallestRandom ensures we will get a 11-character result from the base-36 conversion.
|
||||
return Math.floor((Math.random() * (maxSafeInteger - smallestRandom)) + smallestRandom).toString(36) +
|
||||
Math.floor((Math.random() * (maxSafeInteger - smallestRandom)) + smallestRandom).toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog of objects exposed for XDM
|
||||
*/
|
||||
export class XDMObjectRegistry implements IXDMObjectRegistry {
|
||||
|
||||
private objects: any = {};
|
||||
|
||||
/**
|
||||
* Register an object (instance or factory method) exposed by this frame to callers in a remote frame
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param instance - Either: (1) an object instance, or (2) a function that takes optional context data and returns an object instance.
|
||||
*/
|
||||
public register(instanceId: string, instance: Object | { (contextData?: any): Object; }) {
|
||||
this.objects[instanceId] = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an object (instance or factory method) that was previously registered by this frame
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
*/
|
||||
public unregister(instanceId: string) {
|
||||
delete this.objects[instanceId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of an object registered with the given id
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param contextData - Optional context data to pass to a registered object's factory method
|
||||
*/
|
||||
public getInstance<T>(instanceId: string, contextData?: Object): T | undefined {
|
||||
var instance = this.objects[instanceId];
|
||||
if (!instance) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof instance === "function") {
|
||||
return instance(contextData);
|
||||
}
|
||||
else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_XDM_DEPTH = 100;
|
||||
let nextChannelId = 1;
|
||||
|
||||
/**
|
||||
* Represents a channel of communication between frames\document
|
||||
* Stays "alive" across multiple funtion\method calls
|
||||
*/
|
||||
export class XDMChannel implements IXDMChannel {
|
||||
|
||||
private promises: { [id: number]: { resolve: Function, reject: Function } } = {};
|
||||
private postToWindow: Window;
|
||||
private targetOrigin: string | undefined;
|
||||
private handshakeToken: string | undefined;
|
||||
private registry: XDMObjectRegistry;
|
||||
private channelId: number;
|
||||
|
||||
private nextMessageId: number = 1;
|
||||
private nextProxyId: number = 1;
|
||||
private proxyFunctions: { [name: string]: Function } = {};
|
||||
|
||||
constructor(postToWindow: Window, targetOrigin?: string) {
|
||||
|
||||
this.postToWindow = postToWindow;
|
||||
this.targetOrigin = targetOrigin;
|
||||
this.registry = new XDMObjectRegistry();
|
||||
this.channelId = nextChannelId++;
|
||||
|
||||
if (!this.targetOrigin) {
|
||||
this.handshakeToken = newFingerprint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object registry to handle messages from this specific channel.
|
||||
* Upon receiving a message, this channel registry will be used first, then
|
||||
* the global registry will be used if no handler is found here.
|
||||
*/
|
||||
public getObjectRegistry(): IXDMObjectRegistry {
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a method via RPC. Lookup the registered object on the remote end of the channel and invoke the specified method.
|
||||
*
|
||||
* @param method - Name of the method to invoke
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param params - Arguments to the method to invoke
|
||||
* @param instanceContextData - Optional context data to pass to a registered object's factory method
|
||||
* @param serializationSettings - Optional serialization settings
|
||||
*/
|
||||
public async invokeRemoteMethod<T>(methodName: string, instanceId: string, params?: any[], instanceContextData?: Object, serializationSettings?: ISerializationSettings): Promise<T> {
|
||||
|
||||
const message: IJsonRpcMessage = {
|
||||
id: this.nextMessageId++,
|
||||
methodName: methodName,
|
||||
instanceId: instanceId,
|
||||
instanceContext: instanceContextData,
|
||||
params: <any[]>this._customSerializeObject(params, serializationSettings),
|
||||
serializationSettings: serializationSettings
|
||||
};
|
||||
|
||||
if (!this.targetOrigin) {
|
||||
message.handshakeToken = this.handshakeToken;
|
||||
}
|
||||
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
this.promises[message.id] = { resolve, reject };
|
||||
});
|
||||
|
||||
this._sendRpcMessage(message);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a proxied object that represents the object registered with the given instance id on the remote side of this channel.
|
||||
*
|
||||
* @param instanceId - unique id of the registered object
|
||||
* @param contextData - Optional context data to pass to a registered object's factory method
|
||||
*/
|
||||
public getRemoteObjectProxy<T>(instanceId: string, contextData?: Object): Promise<T> {
|
||||
return this.invokeRemoteMethod("", instanceId, undefined, contextData);
|
||||
}
|
||||
|
||||
private invokeMethod(registeredInstance: any, rpcMessage: IJsonRpcMessage) {
|
||||
|
||||
if (!rpcMessage.methodName) {
|
||||
// Null/empty method name indicates to return the registered object itself.
|
||||
this._success(rpcMessage, registeredInstance, rpcMessage.handshakeToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var method: Function = registeredInstance[rpcMessage.methodName];
|
||||
if (typeof method !== "function") {
|
||||
this.error(rpcMessage, new Error("RPC method not found: " + rpcMessage.methodName));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call specified method. Add nested success and error call backs with closure
|
||||
// so we can post back a response as a result or error as appropriate
|
||||
var methodArgs = [];
|
||||
if (rpcMessage.params) {
|
||||
methodArgs = <any[]>this._customDeserializeObject(rpcMessage.params, {});
|
||||
}
|
||||
|
||||
var result = method.apply(registeredInstance, methodArgs);
|
||||
if (result && result.then && typeof result.then === "function") {
|
||||
result.then((asyncResult: any) => {
|
||||
this._success(rpcMessage, asyncResult, rpcMessage.handshakeToken);
|
||||
}, (e: any) => {
|
||||
this.error(rpcMessage, e);
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._success(rpcMessage, result, rpcMessage.handshakeToken);
|
||||
}
|
||||
}
|
||||
catch (exception) {
|
||||
// send back as error if an exception is thrown
|
||||
this.error(rpcMessage, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private getRegisteredObject(instanceId: string, instanceContext?: Object): Object | undefined {
|
||||
|
||||
if (instanceId === "__proxyFunctions") {
|
||||
// Special case for proxied functions of remote instances
|
||||
return this.proxyFunctions;
|
||||
}
|
||||
|
||||
// Look in the channel registry first
|
||||
var registeredObject = this.registry.getInstance(instanceId, instanceContext);
|
||||
if (!registeredObject) {
|
||||
// Look in the global registry as a fallback
|
||||
registeredObject = globalObjectRegistry.getInstance(instanceId, instanceContext);
|
||||
}
|
||||
|
||||
return registeredObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a received message on this channel. Dispatch to the appropriate object found via object registry
|
||||
*
|
||||
* @param rpcMessage - Message data
|
||||
* @return True if the message was handled by this channel. Otherwise false.
|
||||
*/
|
||||
public onMessage(rpcMessage: IJsonRpcMessage): boolean {
|
||||
|
||||
if (rpcMessage.instanceId) {
|
||||
// Find the object that handles this requestNeed to find implementation
|
||||
|
||||
// Look in the channel registry first
|
||||
const registeredObject: any = this.getRegisteredObject(rpcMessage.instanceId, rpcMessage.instanceContext);
|
||||
if (!registeredObject) {
|
||||
// If not found return false to indicate that the message was not handled
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof registeredObject["then"] === "function") {
|
||||
(<Promise<any>>registeredObject).then((resolvedInstance) => {
|
||||
this.invokeMethod(resolvedInstance, rpcMessage);
|
||||
}, (e) => {
|
||||
this.error(rpcMessage, e);
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.invokeMethod(registeredObject, rpcMessage);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const promise = this.promises[rpcMessage.id];
|
||||
if (!promise) {
|
||||
// Message not handled by this channel.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rpcMessage.error) {
|
||||
promise.reject(this._customDeserializeObject([rpcMessage.error], {})[0]);
|
||||
}
|
||||
else {
|
||||
promise.resolve(this._customDeserializeObject([rpcMessage.result], {})[0]);
|
||||
}
|
||||
|
||||
delete this.promises[rpcMessage.id];
|
||||
}
|
||||
|
||||
// Message handled by this channel
|
||||
return true;
|
||||
}
|
||||
|
||||
public owns(source: Window, origin: string, rpcMessage: IJsonRpcMessage): boolean {
|
||||
/// Determines whether the current message belongs to this channel or not
|
||||
if (this.postToWindow === source) {
|
||||
// For messages coming from sandboxed iframes the origin will be set to the string "null". This is
|
||||
// how onprem works. If it is not a sandboxed iFrame we will get the origin as expected.
|
||||
if (this.targetOrigin) {
|
||||
if (origin) {
|
||||
return origin.toLowerCase() === "null" || this.targetOrigin.toLowerCase().indexOf(origin.toLowerCase()) === 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (rpcMessage.handshakeToken && rpcMessage.handshakeToken === this.handshakeToken) {
|
||||
this.targetOrigin = origin;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public error(messageObj: IJsonRpcMessage, errorObj: Error) {
|
||||
this._sendRpcMessage({
|
||||
id: messageObj.id,
|
||||
error: this._customSerializeObject([errorObj], messageObj.serializationSettings)[0],
|
||||
handshakeToken: messageObj.handshakeToken
|
||||
});
|
||||
}
|
||||
|
||||
private _success(messageObj: IJsonRpcMessage, result: any, handshakeToken?: string) {
|
||||
this._sendRpcMessage({
|
||||
id: messageObj.id,
|
||||
result: this._customSerializeObject([result], messageObj.serializationSettings)[0],
|
||||
handshakeToken
|
||||
});
|
||||
}
|
||||
|
||||
private _sendRpcMessage(message: IJsonRpcMessage) {
|
||||
this.postToWindow.postMessage(JSON.stringify(message), "*");
|
||||
}
|
||||
|
||||
private _customSerializeObject(obj: Object | undefined, settings: ISerializationSettings | undefined, prevParentObjects?: { originalObjects: any[]; newObjects: any[]; }, nextCircularRefId: number = 1, depth: number = 1): any | undefined {
|
||||
|
||||
if (!obj || depth > MAX_XDM_DEPTH) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (obj instanceof Node || obj instanceof Window || obj instanceof Event) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var returnValue: any;
|
||||
|
||||
let parentObjects: { originalObjects: any[]; newObjects: any[]; };
|
||||
if (!prevParentObjects) {
|
||||
parentObjects = {
|
||||
newObjects: [],
|
||||
originalObjects: []
|
||||
};
|
||||
}
|
||||
else {
|
||||
parentObjects = prevParentObjects;
|
||||
}
|
||||
|
||||
parentObjects.originalObjects.push(obj);
|
||||
|
||||
var serializeMember = (parentObject: any, newObject: any, key: any) => {
|
||||
var item;
|
||||
|
||||
try {
|
||||
item = parentObject[key];
|
||||
}
|
||||
catch (ex) {
|
||||
// Cannot access this property. Skip its serialization.
|
||||
}
|
||||
|
||||
var itemType = typeof item;
|
||||
if (itemType === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for a circular reference by looking at parent objects
|
||||
var parentItemIndex = -1;
|
||||
if (itemType === "object") {
|
||||
parentItemIndex = parentObjects.originalObjects.indexOf(item);
|
||||
}
|
||||
if (parentItemIndex >= 0) {
|
||||
// Circular reference found. Add reference to parent
|
||||
var parentItem = parentObjects.newObjects[parentItemIndex];
|
||||
if (!parentItem.__circularReferenceId) {
|
||||
parentItem.__circularReferenceId = nextCircularRefId++;
|
||||
}
|
||||
newObject[key] = {
|
||||
__circularReference: parentItem.__circularReferenceId
|
||||
};
|
||||
}
|
||||
else {
|
||||
if (itemType === "function") {
|
||||
var proxyFunctionId = this.nextProxyId++;
|
||||
newObject[key] = {
|
||||
__proxyFunctionId: this._registerProxyFunction(item, obj),
|
||||
_channelId: this.channelId
|
||||
};
|
||||
}
|
||||
else if (itemType === "object") {
|
||||
if (item && item instanceof Date) {
|
||||
newObject[key] = {
|
||||
__proxyDate: item.getTime()
|
||||
};
|
||||
}
|
||||
else {
|
||||
newObject[key] = this._customSerializeObject(item, settings, parentObjects, nextCircularRefId, depth + 1);
|
||||
}
|
||||
}
|
||||
else if (key !== "__proxyFunctionId") {
|
||||
// Just add non object/function properties as-is. Don't include "__proxyFunctionId" to protect
|
||||
// our proxy methods from being invoked from other messages.
|
||||
newObject[key] = item;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (obj instanceof Array) {
|
||||
|
||||
returnValue = [];
|
||||
parentObjects.newObjects.push(returnValue);
|
||||
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
serializeMember(obj, returnValue, i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
returnValue = {};
|
||||
parentObjects.newObjects.push(returnValue);
|
||||
|
||||
var keys: any = {};
|
||||
|
||||
try {
|
||||
// We want to get both enumerable and non-enumerable properties
|
||||
// including inherited enumerable properties. for..in grabs
|
||||
// enumerable properties (including inherited properties) and
|
||||
// getOwnPropertyNames includes non-enumerable properties.
|
||||
// Merge these results together.
|
||||
for (var key in obj) {
|
||||
keys[key] = true;
|
||||
}
|
||||
var ownProperties = Object.getOwnPropertyNames(obj);
|
||||
for (var i = 0, l = ownProperties.length; i < l; i++) {
|
||||
keys[ownProperties[i]] = true;
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
// We may not be able to access the iterator of this object. Skip its serialization.
|
||||
}
|
||||
|
||||
for (var key in keys) {
|
||||
// Don't serialize properties that start with an underscore.
|
||||
if ((key && key[0] !== "_") || (settings && settings.includeUnderscoreProperties)) {
|
||||
serializeMember(obj, returnValue, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parentObjects.originalObjects.pop();
|
||||
parentObjects.newObjects.pop();
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private _registerProxyFunction(func: Function, context: any): number {
|
||||
var proxyFunctionId = this.nextProxyId++;
|
||||
this.proxyFunctions["proxy" + proxyFunctionId] = function () {
|
||||
return func.apply(context, Array.prototype.slice.call(arguments, 0));
|
||||
};
|
||||
return proxyFunctionId;
|
||||
}
|
||||
|
||||
private _customDeserializeObject(obj: Object, circularRefs: { [key: number]: Object }): any {
|
||||
var that = this;
|
||||
|
||||
if (!obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var deserializeMember = (parentObject: any, key: any) => {
|
||||
var item = parentObject[key];
|
||||
var itemType = typeof item;
|
||||
|
||||
if (key === "__circularReferenceId" && itemType === 'number') {
|
||||
circularRefs[item] = parentObject;
|
||||
delete parentObject[key];
|
||||
}
|
||||
else if (itemType === "object" && item) {
|
||||
|
||||
if (item.__proxyFunctionId) {
|
||||
parentObject[key] = function () {
|
||||
return that.invokeRemoteMethod("proxy" + item.__proxyFunctionId, "__proxyFunctions", Array.prototype.slice.call(arguments, 0), {}, { includeUnderscoreProperties: true });
|
||||
}
|
||||
}
|
||||
else if (item.__proxyDate) {
|
||||
parentObject[key] = new Date(item.__proxyDate);
|
||||
}
|
||||
else if (item.__circularReference) {
|
||||
parentObject[key] = circularRefs[item.__circularReference];
|
||||
}
|
||||
else {
|
||||
this._customDeserializeObject(item, circularRefs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (obj instanceof Array) {
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
deserializeMember(obj, i);
|
||||
}
|
||||
}
|
||||
else if (typeof obj === "object") {
|
||||
for (var key in obj) {
|
||||
deserializeMember(obj, key);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of XDM channels kept per target frame/window
|
||||
*/
|
||||
class XDMChannelManager implements IXDMChannelManager {
|
||||
|
||||
private _channels: XDMChannel[] = [];
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("message", this._handleMessageReceived);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an XDM channel for the given target window/iframe
|
||||
*
|
||||
* @param window - Target iframe window to communicate with
|
||||
* @param targetOrigin - Url of the target iframe (if known)
|
||||
*/
|
||||
public addChannel(window: Window, targetOrigin?: string): IXDMChannel {
|
||||
const channel = new XDMChannel(window, targetOrigin);
|
||||
this._channels.push(channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
public removeChannel(channel: IXDMChannel) {
|
||||
this._channels = this._channels.filter(c => c !== channel);
|
||||
}
|
||||
|
||||
private _handleMessageReceived = (event: any) => {
|
||||
// get channel and dispatch to it
|
||||
let rpcMessage: IJsonRpcMessage | undefined;
|
||||
|
||||
if (typeof event.data === "string") {
|
||||
try {
|
||||
rpcMessage = JSON.parse(event.data);
|
||||
}
|
||||
catch (error) {
|
||||
// The message is not a valid JSON string. Not one of our events.
|
||||
}
|
||||
}
|
||||
|
||||
if (rpcMessage) {
|
||||
let handled = false;
|
||||
let channelOwner: XDMChannel | undefined;
|
||||
|
||||
for (const channel of this._channels) {
|
||||
if (channel.owns(event.source, event.origin, rpcMessage)) {
|
||||
// keep a reference to the channel owner found.
|
||||
channelOwner = channel;
|
||||
handled = channel.onMessage(rpcMessage) || handled;
|
||||
}
|
||||
}
|
||||
|
||||
if (channelOwner && !handled) {
|
||||
if (window.console) {
|
||||
console.error(`No handler found on any channel for message: ${JSON.stringify(rpcMessage)}`);
|
||||
}
|
||||
|
||||
// for instance based proxies, send an error on the channel owning the message to resolve any control creation promises
|
||||
// on the host frame.
|
||||
if (rpcMessage.instanceId) {
|
||||
channelOwner.error(rpcMessage, new Error(`The registered object ${rpcMessage.instanceId} could not be found.`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry of global XDM handlers
|
||||
*/
|
||||
export const globalObjectRegistry: IXDMObjectRegistry = new XDMObjectRegistry();
|
||||
|
||||
/**
|
||||
* Manages XDM channels per target window/frame
|
||||
*/
|
||||
export const channelManager: IXDMChannelManager = new XDMChannelManager();
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"declaration": true,
|
||||
"rootDir": "src/",
|
||||
"outDir": "bin/",
|
||||
"experimentalDecorators": true,
|
||||
"module": "amd",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strict": true,
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6",
|
||||
"dom",
|
||||
"es2015.promise"
|
||||
]
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче