Initial add of Azure DevOps Extension SDK library

This commit is contained in:
Nick Kirchem 2018-11-01 15:04:45 -04:00
Коммит 8ed5a3c07e
10 изменённых файлов: 1629 добавлений и 0 удалений

3
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
npm-debug.log
node_modules/
bin/

1
.npmignore Normal file
Просмотреть файл

@ -0,0 +1 @@
npm-debug.log

21
LICENSE Normal file
Просмотреть файл

@ -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

47
README.md Normal file
Просмотреть файл

@ -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.

81
buildpackage.js Normal file
Просмотреть файл

@ -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);
}
})();

396
package-lock.json сгенерированный Normal file
Просмотреть файл

@ -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
}
}
}

33
package.json Normal file
Просмотреть файл

@ -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"
}
}

361
src/SDK.ts Normal file
Просмотреть файл

@ -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 });
}

666
src/XDM.ts Normal file
Просмотреть файл

@ -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();

20
tsconfig.json Normal file
Просмотреть файл

@ -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"
]
}
}