This commit is contained in:
lovebird 2021-07-18 18:11:37 +02:00
commit 16ba9b5400
196 changed files with 87255 additions and 0 deletions

11
.gh-sync.json Normal file
View File

@ -0,0 +1,11 @@
{
"debug": false,
"matching": [
"*.json",
"*.md",
"*.yaml",
"*.csv",
"*.xls",
"*.html"
]
}

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# Logs
logs
*.log
npm-debug.log*
yarn.lock
yarn-error.log
credentials.json
gcreds.json
token.json
package-lock.json
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history

5
.npmignore Normal file
View File

@ -0,0 +1,5 @@
datasets
gcreds.json
xcredentials.json
tests
src

14
config.json Normal file
View File

@ -0,0 +1,14 @@
{
"products_path": "../../products/products",
"fragments_path": "../../products/bazar/fragments",
"vendor_name": "Plastic Hub",
"vendor_website": "www.precious-plastic.org",
"vendor_products_external": "https://github.com/plastic-hub/products/tree/master/products/",
"vendor_instagram": "https://www.instagram.com/plastichubcat",
"vendor_youtube": "https://www.youtube.com/channel/UCuWDxJtV2pf5BefHEy09Cew/featured?view_as=subscriber",
"vendor_github": "https://github.com/plastic-hub",
"vendor_contact_email": "mailto://cgoflyn@gmail.com",
"vendor_whatsapp": "tel://0034666894789",
"vendor_facebook" : "https://precious-plastic.org/library/machines/",
"vendor_blog" : "https://precious-plastic.org/"
}

112
package.json Normal file
View File

@ -0,0 +1,112 @@
{
"name": "@plastichub/cli",
"version": "1.0.10",
"description": "",
"main": "./build/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"help": "node build/main.js --help",
"build": "tsc -p .",
"dev": "tsc -w -p ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/plastic-hub/lang.git"
},
"author": "",
"bin": {
"ph-cli": "build/main.js"
},
"license": "ISC",
"bugs": {
"url": "https://github.com/plastic-hub/lang/issues"
},
"homepage": "https://github.com/plastic-hub/lang#readme",
"dependencies": {
"@plastichub/fs": "^0.13.21",
"@plastichub/osr-sync": "0.0.22",
"@types/bluebird": "^3.5.29",
"add": "^2.0.6",
"apify": "^0.17.0",
"axios": "^0.21.1",
"bluebird": "^3.7.2",
"chalk": "^2.4.1",
"cheerio": "^1.0.0-rc.3",
"chokidar": "^3.3.1",
"discourser": "^1.0.0",
"download": "^8.0.0",
"duration-timestamp": "^2.3.0",
"errlop": "^2.1.0",
"escape-html": "^1.0.3",
"fast-glob": "^3.1.1",
"filenamify": "^4.1.0",
"googleapis": "^39.2.0",
"inline-css": "^2.6.2",
"isomorphic-unfetch": "^3.0.0",
"js-beautify": "^1.11.0",
"jsome": "^2.5.0",
"json-to-pretty-yaml": "^1.2.2",
"lodash": "^4.17.10",
"markdown-table": "^2.0.0",
"moment": "^2.26.0",
"monocle-ts": "^1.2.0",
"native-promise-pool": "^3.0.0",
"ora": "^2.1.0",
"partial.lenses": "^13.13.2",
"pretty": "^2.0.0",
"puppeteer": "^5.2.1",
"ramda": "^0.25.0",
"readline": "^1.3.0",
"regexp.escape": "^1.1.0",
"sanitize-filename": "^1.6.3",
"showdown": "^1.9.1",
"simple-git": "^2.6.0",
"slash": "^3.0.0",
"slugify": "^1.4.6",
"source-map-support": "^0.5.16",
"ts-node-dev": "^1.0.0-pre.44",
"tslint": "^5.10.0",
"turndown": "^7.0.0",
"typescript": "^3.7.4",
"which": "^2.0.2",
"yargonaut": "^1.1.4",
"yargs": "^15.0.2"
},
"devDependencies": {
"@bevry/links": "^1.1.1",
"@bevry/update-contributors": "^1.0.1",
"@types/node-fetch": "^2.5.7",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"@types/chalk": "^2.2.0",
"@types/cheerio": "^0.22.15",
"@types/chokidar": "^2.1.3",
"@types/download": "^6.2.4",
"@types/inline-css": "0.0.32",
"@types/js-beautify": "^1.8.2",
"@types/lodash": "^4.14.110",
"@types/moment": "^2.13.0",
"@types/node": "^13.1.1",
"@types/ora": "^1.3.4",
"@types/pretty": "^2.0.0",
"@types/puppeteer": "^1.5.0",
"@types/ramda": "^0.25.51",
"@types/showdown": "^1.9.3",
"@types/which": "^1.3.2",
"@types/yargs": "^13.0.4",
"@xblox/core": "^0.0.19",
"assert-helpers": "^6.1.0",
"eslint": "^6.8.0",
"eslint-config-bevry": "^3.3.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"jsdom": "^16.2.2",
"kava": "^4.4.0",
"prettier": "^2.0.5",
"projectz": "^1.19.1",
"surge": "^0.21.3",
"typedoc": "^0.17.6",
"valid-directory": "^1.6.0",
"valid-module": "^1.0.0"
}
}

13
src/_cli.ts Normal file
View File

@ -0,0 +1,13 @@
// tweaks and handlers
export const defaults = () => {
// default command
const DefaultCommand = 'summary';
if (process.argv.length === 2) {
process.argv.push(DefaultCommand);
}
// currently no default handler, display only :
process.on('unhandledRejection', (reason: string) => {
console.error('Unhandled rejection, reason: ', reason);
});
};

9346
src/api.ts Normal file

File diff suppressed because it is too large Load Diff

55
src/argv.ts Normal file
View File

@ -0,0 +1,55 @@
import * as CLI from 'yargs';
import {
warn, error,
default_path,
Options, OutputFormat, OutputTarget, inspect
} from './';
const LIGHT = 'http://google.co.uk';
const HEAVY = 'http://0.0.0.0:5555/app/xcf?debug=true&xblox=debug&xgrid=debug&davinci=debug&userDirectory=/PMaster/x4mm/user;'
// default options for all commands
export const defaultOptions = (yargs: CLI.Argv) => {
return yargs.option('url', {
default: LIGHT,
describe: 'The URL to analyze'
}).option('format', {
default: 'text',
describe: 'Normal human readable text or JSON [text|json]'
}).option('target', {
default: 'console',
describe: 'Output target [console|file]'
}).option('path', {
default: '',
describe: 'The target location on the local filesystem for --target=file'
}).option('debug', {
default: 'false',
describe: 'Enable internal debug message'
})
};
// Sanitizes faulty user argv options for all commands.
export const sanitize = (argv: CLI.Arguments): Options => {
const args = argv as Options;
args.cwd = args.cwd || process.cwd();
if (!args.url) {
// internal user error, should never happen!
error('Invalid url, abort');
return process.exit();
}
// path given but target is not file, correct to file
if (args.path && args.target !== OutputTarget.FILE) {
args.target = OutputTarget.FILE;
}
// target is file but no path given, correct to default file
if (args.target === OutputTarget.FILE && !args.path) {
args.path = default_path(args.cwd, args.url);
}
// format string not valid
if (!(argv.format as string in OutputFormat)) {
warn(`Unknown output format ${argv.format}! Default to ${OutputFormat.text}`);
args.format = OutputFormat.text;
}
return args;
};

2
src/constants.ts Normal file
View File

@ -0,0 +1,2 @@
export const GIT_CHANGELOG_MESSAGE_PREFIX = 'ChangeLog:';
export const GIT_REPO = 'https://github.com/plastic-hub/products'

19
src/format.ts Normal file
View File

@ -0,0 +1,19 @@
import { Options, OutputFormat } from "./types";
import { error } from "./log";
export const render = (result: any, options: Options) => {
switch (options.format) {
case OutputFormat.text: {
//@TODO: human readable format
return JSON.stringify(result, null, 2);
}
case OutputFormat.json: {
return JSON.stringify(result, null, 2);
}
default: {
//private, should never happen since options had to be sanitized
error('format::render Invalid value in options.format');
return '';
}
}
}

18
src/formatter.ts Normal file
View File

@ -0,0 +1,18 @@
export const sizeToString = (bytes: number, si: boolean = true) => {
var units;
var u;
var b = bytes;
var thresh = si ? 1000 : 1024;
if (Math.abs(b) < thresh) {
return b + ' B';
}
units = si
? ['kB', 'MB', 'GB', 'TB']
: ['KiB', 'MiB', 'GiB', 'TiB'];
u = -1;
do {
b /= thresh;
++u;
} while (Math.abs(b) >= thresh && u < units.length - 1);
return b.toFixed(1) + ' ' + units[u];
};

52
src/git_push.sh Normal file
View File

@ -0,0 +1,52 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 swagger-petstore-perl "minor update"
git_user_id=$1
git_repo_id=$2
release_note=$3
if [ "$git_user_id" = "" ]; then
git_user_id=""
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id=""
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note=""
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=`git remote`
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://github.com/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:${GIT_TOKEN}@github.com/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://github.com/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

6
src/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './paths';
export * from './format';
export * from './log';
export * from './types';
export * from './formatter';
export * from './main';

15
src/lib/common/array.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Return last element
* @private
* @param {(Array<any>)} array
* @returns object | undefined
*/
export const lastOf = (array: Array<any>) => array[array.length - 1];
/**
* Return first element
* @private
* @param {(Array<any>)} array
* @returns object | undefined
*/
export const firstOf = (array: Array<any>) => array[0];

55
src/lib/common/enums.ts Normal file
View File

@ -0,0 +1,55 @@
export const EVENTS = {
};
export enum COMMANDS {
RUN_FILE = 'Run_File',
RUN_CLASS = 'Run_Class',
RUN_APP_SERVER_CLASS = 'Run_App_Server_Class',
RUN_APP_SERVER_CLASS_METHOD = 'Run_App_Server_Class_Method',
RUN_APP_SERVER_COMPONENT_METHOD = 'Run_App_Server_Component_Method',
CANCEL_APP_SERVER_COMPONENT_METHOD = 'Cancel_App_Server_Component_Method',
ANSWER_APP_SERVER_COMPONENT_METHOD_INTERRUPT = 'Answer_App_Server_Component_Method_Interrupt'
}
export enum LOGGING_SIGNAL {
};
export enum LOGGING_FLAGS {
/**
* No logging
* @constant
* @type int
*/
NONE = 0x00000000,
/**
* Log in the IDE's global console
* @constant
* @type int
*/
GLOBAL_CONSOLE = 0x00000001,
/**
* Log in the IDE's status bar
* @constant
* @type int
*/
STATUS_BAR = 0x00000002,
/**
* Create notification popup in the IDE
* @constant
* @type int
*/
POPUP = 0x00000004,
/**
* Log to file
* @constant
* @type int
*/
FILE = 0x00000008,
/**
* Log into the IDE's dev tool's console
* @constant
* @type int
*/
DEV_CONSOLE = 0x00000010
};

View File

@ -0,0 +1,12 @@
import { platform, arch } from 'os';
export const os = () => {
if (platform() === 'win32') {
return 'windows';
} else if (platform() === 'darwin') {
return 'osx';
} else if (arch() === 'arm') {
return 'arm';
}
return 'linux';
}

View File

@ -0,0 +1,247 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
const _typeof = {
number: 'number',
string: 'string',
undefined: 'undefined',
object: 'object',
function: 'function'
};
/**
* @returns whether the provided parameter is a JavaScript Array or not.
*/
export function isArray(array: any): array is any[] {
if (Array.isArray) {
return Array.isArray(array);
}
if (array && typeof (array.length) === _typeof.number && array.constructor === Array) {
return true;
}
return false;
}
/**
* @returns whether the provided parameter is a JavaScript String or not.
*/
export function isString(str: any): str is string {
if (typeof (str) === _typeof.string || str instanceof String) {
return true;
}
return false;
}
/**
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a string.
*/
export function isStringArray(value: any): value is string[] {
return isArray(value) && (<any[]>value).every(elem => isString(elem));
}
/**
*
* @returns whether the provided parameter is of type `object` but **not**
* `null`, an `array`, a `regexp`, nor a `date`.
*/
export function isObject(obj: any): boolean {
// The method can't do a type cast since there are type (like strings) which
// are subclasses of any put not positvely matched by the function. Hence type
// narrowing results in wrong results.
return typeof obj === _typeof.object
&& obj !== null
&& !Array.isArray(obj)
&& !(obj instanceof RegExp)
&& !(obj instanceof Date);
}
/**
* In **contrast** to just checking `typeof` this will return `false` for `NaN`.
* @returns whether the provided parameter is a JavaScript Number or not.
*/
export function isNumber(obj: any): obj is number {
if ((typeof (obj) === _typeof.number || obj instanceof Number) && !isNaN(obj)) {
return true;
}
return false;
}
/**
* @returns whether the provided parameter is a JavaScript Boolean or not.
*/
export function isBoolean(obj: any): obj is boolean {
return obj === true || obj === false;
}
/**
* @returns whether the provided parameter is undefined.
*/
export function isUndefined(obj: any): boolean {
return typeof (obj) === _typeof.undefined;
}
/**
* @returns whether the provided parameter is undefined or null.
*/
export function isUndefinedOrNull(obj: any): boolean {
return isUndefined(obj) || obj === null;
}
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* @returns whether the provided parameter is an empty JavaScript Object or not.
*/
export function isEmptyObject(obj: any): obj is any {
if (!isObject(obj)) {
return false;
}
for (let key in obj) {
if (hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
/**
* @returns whether the provided parameter is a JavaScript Function or not.
*/
export function isFunction(obj: any): obj is Function {
return typeof obj === _typeof.function;
}
/**
* @returns whether the provided parameters is are JavaScript Function or not.
*/
export function areFunctions(...objects: any[]): boolean {
return objects && objects.length > 0 && objects.every(isFunction);
}
export type TypeConstraint = string | Function;
export function validateConstraints(args: any[], constraints: TypeConstraint[]): void {
const len = Math.min(args.length, constraints.length);
for (let i = 0; i < len; i++) {
validateConstraint(args[i], constraints[i]);
}
}
export function validateConstraint(arg: any, constraint: TypeConstraint): void {
if (isString(constraint)) {
if (typeof arg !== constraint) {
throw new Error(`argument does not match constraint: typeof ${constraint}`);
}
} else if (isFunction(constraint)) {
if (arg instanceof constraint) {
return;
}
if (arg && arg.constructor === constraint) {
return;
}
if (constraint.length === 1 && constraint.call(undefined, arg) === true) {
return;
}
throw new Error(`argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true`);
}
}
/**
* Creates a new object of the provided class and will call the constructor with
* any additional argument supplied.
*/
export function create(ctor: Function, ...args: any[]): any {
let obj = Object.create(ctor.prototype);
ctor.apply(obj, args);
return obj;
}
export interface IFunction0<T> {
(): T;
}
export interface IFunction1<A1, T> {
(a1: A1): T;
}
export interface IFunction2<A1, A2, T> {
(a1: A1, a2: A2): T;
}
export interface IFunction3<A1, A2, A3, T> {
(a1: A1, a2: A2, a3: A3): T;
}
export interface IFunction4<A1, A2, A3, A4, T> {
(a1: A1, a2: A2, a3: A3, a4: A4): T;
}
export interface IFunction5<A1, A2, A3, A4, A5, T> {
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5): T;
}
export interface IFunction6<A1, A2, A3, A4, A5, A6, T> {
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6): T;
}
export interface IFunction7<A1, A2, A3, A4, A5, A6, A7, T> {
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7): T;
}
export interface IFunction8<A1, A2, A3, A4, A5, A6, A7, A8, T> {
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8): T;
}
export interface IAction0 extends IFunction0<void> { }
export interface IAction1<A1> extends IFunction1<A1, void> { }
export interface IAction2<A1, A2> extends IFunction2<A1, A2, void> { }
export interface IAction3<A1, A2, A3> extends IFunction3<A1, A2, A3, void> { }
export interface IAction4<A1, A2, A3, A4> extends IFunction4<A1, A2, A3, A4, void> { }
export interface IAction5<A1, A2, A3, A4, A5> extends IFunction5<A1, A2, A3, A4, A5, void> { }
export interface IAction6<A1, A2, A3, A4, A5, A6> extends IFunction6<A1, A2, A3, A4, A5, A6, void> { }
export interface IAction7<A1, A2, A3, A4, A5, A6, A7> extends IFunction7<A1, A2, A3, A4, A5, A6, A7, void> { }
export interface IAction8<A1, A2, A3, A4, A5, A6, A7, A8> extends IFunction8<A1, A2, A3, A4, A5, A6, A7, A8, void> { }
export type NumberCallback = (index: number) => void;
export function count(to: number, callback: NumberCallback): void;
export function count(from: number, to: number, callback: NumberCallback): void;
export function count(fromOrTo: number, toOrCallback?: NumberCallback | number, callback?: NumberCallback): any {
var from: number, to: number;
if (isNumber(toOrCallback)) {
from = fromOrTo;
to = <number>toOrCallback;
} else {
from = 0;
to = fromOrTo;
callback = <NumberCallback>toOrCallback;
}
var op = from <= to ? (i: number) => i + 1 : (i: number) => i - 1;
var cmp = from <= to ? (a: number, b: number) => a < b : (a: number, b: number) => a > b;
for (var i = from; cmp(i, to); i = op(i)) {
callback(i);
}
}
export function countToArray(to: number): number[];
export function countToArray(from: number, to: number): number[];
export function countToArray(fromOrTo: number, to?: number): number[] {
var result: number[] = [];
var fn = (i: number) => result.push(i);
if (isUndefined(to)) {
count(fromOrTo, fn);
} else {
count(fromOrTo, to, fn);
}
return result;
}

487
src/lib/common/strings.ts Normal file
View File

@ -0,0 +1,487 @@
import { isArray, isObject, } from './primitives';
import { Hash } from './types';
import { isString } from 'lodash';
const escapeRegExpPattern = /[[\]{}()|\/\\^$.*+?]/g;
const escapeXmlPattern = /[&<]/g;
const escapeXmlForPattern = /[&<>'"]/g;
const escapeXmlMap: Hash<string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;'
};
export const DefaultDelimiter = {
begin: '<%',
end: '%>'
};
export const hasFlag = (field, enumValue) => {
//noinspection JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage
// tslint:disable-next-line:no-bitwise
return ((1 << enumValue) & field) ? true : false;
};
export const hasFlagHex = (field, enumValue) => {
//noinspection JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage
// tslint:disable-next-line:no-bitwise
return enumValue & field ? true : false;
};
export const disableFlag = (enumValue, field) => {
enumValue &= ~(1 << field);
return enumValue;
};
/**
* The minimum location of high surrogates
*/
export const HIGH_SURROGATE_MIN = 0xD800;
/**
* The maximum location of high surrogates
*/
export const HIGH_SURROGATE_MAX = 0xDBFF;
/**
* The minimum location of low surrogates
*/
export const LOW_SURROGATE_MIN = 0xDC00;
/**
* The maximum location of low surrogates
*/
export const LOW_SURROGATE_MAX = 0xDFFF;
const BASE64_KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
export const capitalize = (word) => {
return word.substring(0, 1).toUpperCase() + word.substring(1);
};
export const getJson = (inData, validOnly, ommit) => {
try {
return isString(inData) ? JSON.parse(inData) : validOnly === true ? null : inData;
} catch (e) {
ommit !== false && console.error('error parsing json data ' + inData + ' error = ' + e);
}
return null;
};
/**
* Escapes a string so that it can safely be passed to the RegExp constructor.
* @param text The string to be escaped
* @return The escaped string
*/
export function escapeRegExpEx(text: string): string {
return !text ? text : text.replace(escapeRegExpPattern, '\\$&');
}
/**
* Sanitizes a string to protect against tag injection.
* @param xml The string to be escaped
* @param forAttribute Whether to also escape ', ", and > in addition to < and &
* @return The escaped string
*/
export function escapeXml(xml: string, forAttribute: boolean = true): string {
if (!xml) {
return xml;
}
const pattern = forAttribute ? escapeXmlForPattern : escapeXmlPattern;
return xml.replace(pattern, function (character: string): string {
return escapeXmlMap[character];
});
}
export function createUUID(): string {
const S4 = function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
}
export function escapeRegExp(str: string): string {
const special = ['[', ']', '(', ')', '{', '}', '*', '+', '.', '|', '||'];
for (let n = 0; n < special.length; n++) {
str = str.replace(special[n], '\\' + special[n]);
}
return str;
};
export function findOcurrences(expression: string, delimiters: IDelimiter): Array<string> {
// tslint:disable-next-line:no-object-literal-type-assertion
const d = {
begin: escapeRegExp(delimiters.begin),
end: escapeRegExp(delimiters.end)
} as IDelimiter;
return expression.match(new RegExp(d.begin + '([^' + d.end + ']*)' + d.end, 'g'));
};
export function multipleReplace(str: string, hash: any): string {
// to array
const a = [];
// tslint:disable-next-line:forin
for (let key in hash) {
a[a.length] = key;
}
return str.replace(new RegExp(a.join('\\b|\\b'), 'g'), function (m) {
return hash[m] || hash['\\' + m];
});
};
export function replaceAll(find: string, replace: string, str: string): string {
return str ? str.split(find).join(replace) : '';
};
export interface IDelimiter {
begin: string;
end: string;
}
export function replace(str: string, needle: any | null, what: string | any, delimiters: IDelimiter): string {
if (!str) {
return '';
}
if (what && isObject(what) || isArray(what)) {
what = what as any;
if (!delimiters) {
// fast case
return multipleReplace(str, what);
}
const occurrence = findOcurrences(str, delimiters);
if (!occurrence) {
return str;
} else {
for (let i = 0, j = occurrence.length; i < j; i++) {
const el = occurrence[i];
// strip off delimiters
let _variableName = replaceAll(delimiters.begin, '', el);
_variableName = replaceAll(delimiters.end, '', _variableName);
str = replaceAll(el, (what[_variableName]), str);
}
}
return str;
}
// fast case
return replaceAll(needle, what as string, str);
};
export const substitute = (template, map ) => {
const transform = (k) => k || '';
return template.replace(/\$\{([^\s\:\}]+)(?:\:([^\s\:\}]+))?\}/g,
(match, key, format) => transform(map[key]).toString());
};
function decodeUtf8EncodedCodePoint(codePoint: number, validationRange: number[] = [0, Infinity], checkSurrogate?: boolean): string {
if (codePoint < validationRange[0] || codePoint > validationRange[1]) {
throw Error('Invalid continuation byte');
}
if (checkSurrogate && codePoint >= HIGH_SURROGATE_MIN && codePoint <= LOW_SURROGATE_MAX) {
throw Error('Surrogate is not a scalar value');
}
let encoded = '';
if (codePoint > 0xFFFF) {
codePoint -= 0x010000;
encoded += String.fromCharCode(codePoint >>> 0x10 & 0x03FF | HIGH_SURROGATE_MIN);
codePoint = LOW_SURROGATE_MIN | codePoint & 0x03FF;
}
encoded += String.fromCharCode(codePoint);
return encoded;
}
function validateUtf8EncodedCodePoint(codePoint: number): void {
if ((codePoint & 0xC0) !== 0x80) {
throw Error('Invalid continuation byte');
}
}
export type ByteBuffer = Uint16Array | Uint8Array | Buffer | number[];
export interface Codec {
encode(data: string): number[];
decode(data: ByteBuffer): string;
}
/**
* Provides facilities for encoding a string into an ASCII-encoded byte buffer and
* decoding an ASCII-encoded byte buffer into a string.
*/
export const ascii: Codec = {
/**
* Encodes a string into an ASCII-encoded byte buffer.
*
* @param data The text string to encode
*/
encode(data: string): number[] {
if (data == null) {
return [];
}
const buffer: number[] = [];
for (let i = 0, length = data.length; i < length; i++) {
buffer[i] = data.charCodeAt(i);
}
return buffer;
},
/**
* Decodes an ASCII-encoded byte buffer into a string.
*
* @param data The byte buffer to decode
*/
decode(data: ByteBuffer): string {
if (data == null) {
return '';
}
let decoded = '';
for (let i = 0, length = data.length; i < length; i++) {
decoded += String.fromCharCode(data[i]);
}
return decoded;
}
};
/**
* Provides facilities for encoding a string into a Base64-encoded byte buffer and
* decoding a Base64-encoded byte buffer into a string.
*/
export const base64: Codec = {
/**
* Encodes a Base64-encoded string into a Base64 byte buffer.
*
* @param data The Base64-encoded string to encode
*/
encode(data: string): number[] {
if (data == null) {
return [];
}
const buffer: number[] = [];
let i = 0;
let length = data.length;
while (data[--length] === '=') { }
while (i < length) {
let encoded = BASE64_KEYSTR.indexOf(data[i++]) << 18;
if (i <= length) {
encoded |= BASE64_KEYSTR.indexOf(data[i++]) << 12;
}
if (i <= length) {
encoded |= BASE64_KEYSTR.indexOf(data[i++]) << 6;
}
if (i <= length) {
encoded |= BASE64_KEYSTR.indexOf(data[i++]);
}
buffer.push((encoded >>> 16) & 0xff);
buffer.push((encoded >>> 8) & 0xff);
buffer.push(encoded & 0xff);
}
while (buffer[buffer.length - 1] === 0) {
buffer.pop();
}
return buffer;
},
/**
* Decodes a Base64-encoded byte buffer into a Base64-encoded string.
*
* @param data The byte buffer to decode
*/
decode(data: ByteBuffer): string {
if (data == null) {
return '';
}
let decoded = '';
let i = 0;
for (let length = data.length - (data.length % 3); i < length;) {
let encoded = data[i++] << 16 | data[i++] << 8 | data[i++];
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3F);
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3F);
decoded += BASE64_KEYSTR.charAt((encoded >>> 6) & 0x3F);
decoded += BASE64_KEYSTR.charAt(encoded & 0x3F);
}
if (data.length % 3 === 1) {
let encoded = data[i++] << 16;
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3f);
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3f);
decoded += '==';
} else if (data.length % 3 === 2) {
let encoded = data[i++] << 16 | data[i++] << 8;
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3f);
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3f);
decoded += BASE64_KEYSTR.charAt((encoded >>> 6) & 0x3f);
decoded += '=';
}
return decoded;
}
};
/**
* Provides facilities for encoding a string into a hex-encoded byte buffer and
* decoding a hex-encoded byte buffer into a string.
*/
export const hex: Codec = {
/**
* Encodes a string into a hex-encoded byte buffer.
*
* @param data The hex-encoded string to encode
*/
encode(data: string): number[] {
if (data == null) {
return [];
}
const buffer: number[] = [];
for (let i = 0, length = data.length; i < length; i += 2) {
let encodedChar = parseInt(data.substr(i, 2), 16);
buffer.push(encodedChar);
}
return buffer;
},
/**
* Decodes a hex-encoded byte buffer into a hex-encoded string.
*
* @param data The byte buffer to decode
*/
decode(data: ByteBuffer): string {
if (data == null) {
return '';
}
let decoded = '';
for (let i = 0, length = data.length; i < length; i++) {
decoded += data[i].toString(16).toUpperCase();
}
return decoded;
}
};
/**
* Provides facilities for encoding a string into a UTF-8-encoded byte buffer and
* decoding a UTF-8-encoded byte buffer into a string.
* Inspired by the work of: https://github.com/mathiasbynens/utf8.js
*/
export const utf8: Codec = {
/**
* Encodes a string into a UTF-8-encoded byte buffer.
*
* @param data The text string to encode
*/
encode(data: string): number[] {
if (data == null) {
return [];
}
const buffer: number[] = [];
for (let i = 0, length = data.length; i < length; i++) {
let encodedChar = data.charCodeAt(i);
/**
* Surrogates
* http://en.wikipedia.org/wiki/Universal_Character_Set_characters
*/
if (encodedChar >= HIGH_SURROGATE_MIN && encodedChar <= HIGH_SURROGATE_MAX) {
let lowSurrogate = data.charCodeAt(i + 1);
if (lowSurrogate >= LOW_SURROGATE_MIN && lowSurrogate <= LOW_SURROGATE_MAX) {
encodedChar = 0x010000 + (encodedChar - HIGH_SURROGATE_MIN) * 0x0400 + (lowSurrogate - LOW_SURROGATE_MIN);
i++;
}
}
if (encodedChar < 0x80) {
buffer.push(encodedChar);
} else {
if (encodedChar < 0x800) {
buffer.push(((encodedChar >> 0x06) & 0x1F) | 0xC0);
} else if (encodedChar < 0x010000) {
if (encodedChar >= HIGH_SURROGATE_MIN && encodedChar <= LOW_SURROGATE_MAX) {
throw Error('Surrogate is not a scalar value');
}
buffer.push(((encodedChar >> 0x0C) & 0x0F) | 0xE0);
buffer.push(((encodedChar >> 0x06) & 0x3F) | 0x80);
} else if (encodedChar < 0x200000) {
buffer.push(((encodedChar >> 0x12) & 0x07) | 0xF0);
buffer.push(((encodedChar >> 0x0C) & 0x3F) | 0x80);
buffer.push(((encodedChar >> 0x06) & 0x3F) | 0x80);
}
buffer.push((encodedChar & 0x3F) | 0x80);
}
}
return buffer;
},
/**
* Decodes a UTF-8-encoded byte buffer into a string.
*
* @param data The byte buffer to decode
*/
decode(data: ByteBuffer): string {
if (data == null) {
return '';
}
let decoded = '';
for (let i = 0, length = data.length; i < length; i++) {
let byte1 = data[i] & 0xFF;
if ((byte1 & 0x80) === 0) {
decoded += decodeUtf8EncodedCodePoint(byte1);
} else if ((byte1 & 0xE0) === 0xC0) {
let byte2 = data[++i] & 0xFF;
validateUtf8EncodedCodePoint(byte2);
byte2 = byte2 & 0x3F;
let encodedByte = ((byte1 & 0x1F) << 0x06) | byte2;
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x80, Infinity]);
} else if ((byte1 & 0xF0) === 0xE0) {
let byte2 = data[++i] & 0xFF;
validateUtf8EncodedCodePoint(byte2);
byte2 = byte2 & 0x3F;
let byte3 = data[++i] & 0xFF;
validateUtf8EncodedCodePoint(byte3);
byte3 = byte3 & 0x3F;
let encodedByte = ((byte1 & 0x1F) << 0x0C) | (byte2 << 0x06) | byte3;
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x0800, Infinity], true);
} else if ((byte1 & 0xF8) === 0xF0) {
let byte2 = data[++i] & 0xFF;
validateUtf8EncodedCodePoint(byte2);
byte2 = byte2 & 0x3F;
let byte3 = data[++i] & 0xFF;
validateUtf8EncodedCodePoint(byte3);
byte3 = byte3 & 0x3F;
let byte4 = data[++i] & 0xFF;
validateUtf8EncodedCodePoint(byte4);
byte4 = byte4 & 0x3F;
let encodedByte = ((byte1 & 0x1F) << 0x0C) | (byte2 << 0x0C) | (byte3 << 0x06) | byte4;
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x010000, 0x10FFFF]);
} else {
validateUtf8EncodedCodePoint(byte1);
}
}
return decoded;
}
};

35
src/lib/common/types.ts Normal file
View File

@ -0,0 +1,35 @@
export * from './enums';
// tslint:disable-next-line:interface-name
export interface Hash<T> {
[id: string]: T;
}
// tslint:disable-next-line:interface-name
export interface List<T> {
[index: number]: T;
length: number;
}
/**
* Interface of the simple literal object with any string keys.
*/
export interface IObjectLiteral {
[key: string]: any;
}
/**
* Represents some Type of the Object.
*/
// tslint:disable-next-line:ban-types
export type ObjectType<T> = { new(): T } | (Function);
/**
* Same as Partial<T> but goes deeper and makes Partial<T> all its properties and sub-properties.
*/
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
export interface IDelimitter {
begin: string;
end: string;
}
export type JSONPathExpression = string;

67
src/lib/content/front.ts Normal file
View File

@ -0,0 +1,67 @@
import { capitalize } from "../common/strings";
export const howto_header = (title, category, image, description: string = "", tagline: string = "", config: string = "") => {
return `---
image: ${image}
category: "${category}"
title: "${title}"
tagline: ${tagline || '""' }
description: ${description || `"Precious Plastic - Howto : ${category} :: ${title} "` }
${config}
---\n`;
}
export const gallery_image = (path, title= "", alt ="") =>{
return `
- url: "${path}"
image_path: "${path}"
alt: "${alt}"
title: "${title}"`;
}
export const drawing_image = (path, pdf, title= "", alt ="") =>{
return `
- url: "${pdf}"
image_path: "${path}"
alt: "${alt}"
title: "${title}"`;
}
export const machine_header = (title, category, image, slug, rel, description: string = "", tagline: string = "", config: string = "") => {
return `---
image: ${image}
category: "${category}"
title: "${title}"
product_rel: "/${rel}"
tagline: ${tagline || '""'}
description: ${description || `"Precious Plastic - Machine : ${capitalize(category)} :: ${title}"` }
${config}
---\n`;
}
export const product_header = (title, category, image, slug, rel, description: string = "", tagline: string = "", config: string = "") => {
return `---
image: ${image}
category: "${category}"
title: "${title}"
product_rel: "/${rel}"
tagline: ${tagline || '""'}
description: ${description || `"Precious Plastic - Machine : ${title} by PlasticHub"` }
${config}
---\n`;
}
export const projects_header = (title, category, image, slug, rel, description: string = "", tagline: string = "", config: string = "") => {
return `---
image: ${image}
category: "${category}"
title: "${title}"
product_rel: "/${rel}"
tagline: ${tagline || '""'}
description: ${description || `"Precious Plastic - ${capitalize(category)} :: ${title}"` }
${config}
sidebar:
nav: "projects"
---\n`;
}

26
src/lib/content/html.ts Normal file
View File

@ -0,0 +1,26 @@
import { GIT_REPO } from '../../constants';
import { html_beautify } from 'js-beautify';
export const img = (file, label, id = '') => {
return `<div class="thumb">
<a href="${file}" _target="_blank" >
<img id="${id}" src="${file}" width="100%" />
</a>
<span class="thumb-label">${label}</span>
</div>`;
}
export const changelog_entry = (e) => {
return `<div class="change_log_entry">
<span><pre>${e.date}&nbsp;</pre></span><span><a href="${GIT_REPO}/commit/${e.hash}">${e.msg}</a></span>
<ul>
${e.files.map((f) => {
return `<li><a href="${GIT_REPO}/blob/master/${f.path}">${f.path}</a></li>`
})}
</ul>
</div>
`
}
export const changelog = (log: any[]) => {
return html_beautify(log.map(changelog_entry).join('<br/>'));
}

77
src/lib/content/md.ts Normal file
View File

@ -0,0 +1,77 @@
import * as debug from '../..';
import * as path from 'path';
import { isArray, isString } from 'util';
import { files, read, csvToMarkdown, toHTML, md2html, exists } from '../../lib/';
import { html_beautify } from 'js-beautify';
import { sync as mkdir } from '@plastichub/fs/dir';
import { substitute } from '../common/strings';
const md_tables = require('markdown-table');
export const parse_config = (config, root) => {
if (Object.keys(config)) {
for (const key in config) {
let val = config[key];
if (isArray(val)) {
if (key !== 'authors') {
config[key] = md2html(md_tables(val));
}
} else if (isString(val)) {
if (val.endsWith('.csv')) {
const parsed = path.parse(root);
let csv = path.resolve(`${parsed.dir}/${parsed.base}/${val}`) as any;
debug.info("Parsing CSV " + csv);
if (exists(csv)) {
csv = read(csv) || "";
try {
csv = md2html(csvToMarkdown(csv));
config[key] = csv;
} catch (e) {
debug.error(`Error converting csv to md ${val}`);
}
} else {
debug.error(`Can't find CSV file at ${csv}`, parsed);
}
}
}
}
}
}
export const md_edit_wrap = (content, f, prefix = '', context = '') => {
return html_beautify(`<div prefix="${prefix}" file="${path.parse(f).base}" context="${context}" class="fragment">${content}</div>`);
}
export const read_fragments = (src, config, prefix = '', context = '') => {
if (!exists(src)) {
debug.warn(`Create template folder ${src}`);
mkdir(src);
}
let fragments = files(src, '*.html');
fragments.map((f) => {
config[path.parse(f).name] = md_edit_wrap(toHTML(f, true), f, prefix, context);
});
fragments = files(src, '*.md');
fragments.map((f) => {
config[path.parse(f).name] = md_edit_wrap(toHTML(f, false), f, prefix, context);
});
return config;
}
const _resolve = (config) => {
for (const key in config) {
if (config[key] && typeof config[key] == 'string') {
const resolved = substitute(config[key], config);
config[key] = resolved;
}
}
return config;
}
export const resolveConfig = (config) => {
config = _resolve(config);
config = _resolve(config);
return config;
}

76
src/lib/content/tables.ts Normal file
View File

@ -0,0 +1,76 @@
/**
* Converts CSV to Markdown Table
*
* @param {string} csvContent - The string content of the CSV
* @param {string} delimiter - The character(s) to use as the CSV column delimiter
* @param {boolean} hasHeader - Whether to use the first row of Data as headers
* @returns {string}
*/
export function csvToMarkdown(csvContent: string, delimiter: string = ",", hasHeader: boolean = true): string {
if (delimiter != "\t") {
csvContent = csvContent.replace(/\t/g, " ");
}
const columns = csvContent.split(/\r?\n/);
const tabularData: string[][] = [];
const maxRowLen: number[] = [];
columns.forEach((e, i) => {
if (typeof tabularData[i] == "undefined") {
tabularData[i] = [];
}
const regex = new RegExp(delimiter + '(?![^"]*"\\B)');
const row = e.split(regex);
row.forEach((ee, ii) => {
if (typeof maxRowLen[ii] == "undefined") {
maxRowLen[ii] = 0;
}
// escape pipes and backslashes
ee = ee.replace(/(\||\\)/g, "\\$1");
maxRowLen[ii] = Math.max(maxRowLen[ii], ee.length);
tabularData[i][ii] = ee;
});
});
let headerOutput = "";
let seperatorOutput = "";
maxRowLen.forEach((len) => {
const sizer = Array(len + 1 + 2);
seperatorOutput += "|" + sizer.join("-");
headerOutput += "|" + sizer.join(" ");
});
headerOutput += "| \n";
seperatorOutput += "| \n";
if (hasHeader) {
headerOutput = "";
}
let rowOutput = "";
tabularData.forEach((col, i) => {
maxRowLen.forEach((len, y) => {
const row = typeof col[y] == "undefined" ? "" : col[y];
const spacing = Array((len - row.length) + 1).join(" ");
const out = `| ${row}${spacing} `;
if (hasHeader && i === 0) {
headerOutput += out;
} else {
rowOutput += out;
}
});
if (hasHeader && i === 0) {
headerOutput += "| \n";
} else {
rowOutput += "| \n";
}
});
return `\n\n ${headerOutput}${seperatorOutput}${rowOutput} \n\n`;
}

View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

21
src/lib/core/.gitattributes vendored Normal file
View File

@ -0,0 +1,21 @@
# Set the default behavior, in case users don't have core.autocrlf set
* text=auto
# Files that should always be normalized and converted to native line
# endings on checkout.
*.js text
*.json text
*.ts text
*.md text
*.yml text
LICENSE text
# Files that are truly binary and should not be modified
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.jar binary
*.zip binary
*.psd binary
*.enc binary

7
src/lib/core/.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,7 @@
# Thank You
We very much welcome contributions to Dojo 2.
Because we have so many repositories that are part of Dojo 2, we have located our [Contributing Guidelines](https://github.com/dojo/meta/blob/master/CONTRIBUTING.md) in our [Dojo 2 Meta Repository](https://github.com/dojo/meta#readme).
Look forward to working with you on Dojo 2!!!

28
src/lib/core/.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,28 @@
<!--
Thank you for contributing to Dojo 2.
Our issue tracker is for bugs for Dojo 2.
Please make sure you have read our Contributing Guidelines
available at: https://github.com/dojo/meta/blob/master/CONTRIBUTING.md
For general questions and discussion, join us on Gitter.im at: https://gitter.im/dojo/dojo2
-->
**Bug / Enhancement** <!-- delete as appropriate -->
<!-- Summary of enhancement or bug-->
Package Version: <!-- package version -->
**Code**
<!-- a self contained example of code that demonstrates the issue -->
**Expected behavior:**
<!-- What did you expect to happen -->
**Actual behavior:**
<!-- What was the actual behavior? -->

View File

@ -0,0 +1,20 @@
**Type:** bug / feature
The following has been addressed in the PR:
* [ ] There is a related issue
* [ ] All code has been formatted with [`prettier`](https://prettier.io/) as per the [readme code style guidelines](./../#code-style)
* [ ] Unit or Functional tests are included in the PR
<!--
Our bots should ensure:
* [ ] All contributors have signed a CLA
* [ ] The PR passes CI testing
* [ ] Code coverage is maintained
* [ ] The PR has been reviewed and approved
-->
**Description:**
Resolves #???

18
src/lib/core/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.sublimets
.tscache
.tsconfig*.json
*.js
*.js.map
!/*.js
dist
deploy_key
node_modules
/typings/**
bower_components
tests/typings/dist/
.baseDir.ts
html-report
coverage-final.lcov
coverage-unmapped.json
/_build
/_apidoc

17
src/lib/core/.npmignore Normal file
View File

@ -0,0 +1,17 @@
/_build
/docs
/html-report
/resources
/support
/tests
/typings
.editorconfig
.gitattributes
.gitignore
.npmignore
.travis.yml
.tscache
.vscode
Gruntfile.js
tsconfig.json
tslint.json

32
src/lib/core/.travis.yml Normal file
View File

@ -0,0 +1,32 @@
sudo: false
language: node_js
node_js:
- '6'
env:
global:
# Please get your own free key if you want to test yourself
- SAUCE_USERNAME: dojo2-ts-ci
- SAUCE_ACCESS_KEY: e92610e3-834e-4bec-a3b5-6f7b9d874601
- BROWSERSTACK_USERNAME: dylanschiemann2
- BROWSERSTACK_ACCESS_KEY: 4Q2g8YAc9qeZzB2hECnS
addons:
apt:
packages:
- openssl
before_install:
- if [ ${TRAVIS_BRANCH-""} == "master" ] && [ -n ${encrypted_12c8071d2874_key-""} ]; then openssl aes-256-cbc -K
$encrypted_12c8071d2874_key -iv $encrypted_12c8071d2874_iv
-in deploy_key.enc -out deploy_key -d; fi
install:
- travis_retry npm install grunt-cli
- travis_retry npm install
script:
- grunt
- grunt intern:browserstack --test-reporter
- grunt uploadCoverage
- grunt dist
- if [ "$TRAVIS_BRANCH" = "master" ]; then grunt doc; fi
notifications:
slack:
secure: ZuM/LrujGyUs8r/KGYtxaqRHNVyAMhQi+nrbfhxxqrfXqfFoikMqwGSlY4728MvOEj3ptJhcqjxHTp+rnajXTHkEXJ6y7yfIGekd5523dGVooESIu6oFjmh0iIVxJWLhxQ+DwABRYX0FsPAIMZCCVuK/0EM6Pw00uPwiPjmeRkylV+lUkEoWkuKCTxuaBDgY4ITDGprDVGmtJRQp7Weov4IOlfGLVdoupVd1AmXq5OyvpW4fVtBKR+aph0jAqSVOMFIas77njKKoSPzxGtqf1110MQ1QJc7yTY6J6XFZEPpv15cRjo/LRZFx3xf/hwAufxnZR0L1bxvuVG9mfn4q8m1Wgq6JyoWfg+DC12KSfo9Y2MJCIi/mAh5NkiP6wzsryhnQVLDKlnN5P611DuxeImsxYXMoZMqoZ/eT2djdm2PkJCwGilCRM1aTU2dfqpwbdM4aD88FzCfICigNuaIXhzinDrrBQtIn6EoRkqw0T1lplMxeG+lt2SLpcPsZmtjNVrFqb8lNQPm1RWOuo9toCPYeKnIBZt3wzTuEoj27nTVarNLQK1S74IyP8kKvnjtEeUGD0/a4ow9FnUCnARNfSx62ksB5sJV5DV4zi5tzuowTZsvyUM6tpt1U3SiKEIDIteurdS1G7vCNArucvP5cxCVqYbrgaFdRmJMEwvrAqXc=
on_success: change

View File

@ -0,0 +1,2 @@
The [Contributing Guidelines](https://github.com/dojo/meta/blob/master/CONTRIBUTING.md)
for all Dojo 2 packages can be found in the [Dojo 2 Meta Repository](https://github.com/dojo/meta#readme).

17
src/lib/core/Gruntfile.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = function (grunt) {
require('grunt-dojo2').initConfig(grunt, {
dtsGenerator: {
options: {
main: 'dojo-core/main'
}
},
typedoc: {
options: {
ignoreCompilerErrors: true, // Remove this once compile errors are resolved
}
},
intern: {
version: 4
}
});
};

54
src/lib/core/LICENSE Normal file
View File

@ -0,0 +1,54 @@
The "New" BSD License
*********************
Copyright (c) 2015 - 2017, [JS Foundation](https://js.foundation/)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the JS Foundation nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Code for string.codePointAt, string.fromCodePoint, and string.repeat
adapted from polyfills by Mathias Bynens, under the MIT license:
Copyright Mathias Bynens <https://mathiasbynens.be/>
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.

153
src/lib/core/README.md Normal file
View File

@ -0,0 +1,153 @@
## The `@dojo/core` repository has been deprecated and merged into [`@dojo/framework`](https://github.com/dojo/framework)
You can read more about this change on our [blog](https://dojo.io/blog/). We will continue providing patches for `core` and other Dojo 2 repositories, and a [CLI migration tool](https://github.com/dojo/cli-upgrade) is available to aid in migrating projects from v2 to v3.
***
# Dojo 2 core
[![Build Status](https://travis-ci.org/dojo/core.svg?branch=master)](https://travis-ci.org/dojo/core)
[![codecov.io](https://codecov.io/github/dojo/core/coverage.svg?branch=master)](https://codecov.io/github/dojo/core?branch=master)
[![npm version](https://badge.fury.io/js/%40dojo%2Fcore.svg)](https://badge.fury.io/js/%40dojo%2Fcore)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fdojo%2Fcore.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fdojo%2Fcore?ref=badge_shield)
This package provides a set of language helpers, utility functions, and classes for writing TypeScript applications. It includes APIs for feature detection, asynchronous operations, basic event handling,
and making HTTP requests.
## Usage
To use `@dojo/core`, install the package along with its required peer dependencies:
```bash
npm install @dojo/core
# peer dependencies
npm install @dojo/has
npm install @dojo/shim
```
## Features
- [Feature Detection](#feature-detection)
- [Language Utilities](#language-utilities)
- [lang](#lang)
- [load](#load)
- [string](#string)
- [UrlSearchParams](#urlsearchparams)
- [Event Handling](#event-handling)
- [HTTP Requests](#http-requests)
- [Promises and Asynchronous Operations](#promises-and-asynchronous-operations)
- [Promise](#promise)
- [Task](#task)
### Feature Detection
Using the latest Web technologies isn't always as straightforward as developers would like due to differing support across platforms. [`@dojo/core/has`](docs/has.md) provides a simple feature detection API that makes it easy to
detect which platforms support which features.
### Language Utilities
The core package provides modules offering language utilities. Some of these are heavily based
on methods in the ES2015 proposal; others are additional APIs for commonly-performed tasks.
#### lang
The [`@dojo/core/lang` module](docs/lang.md) contains various utility functions for tasks such as copying objects
and creating late-bound or partially applied functions.
### load
The [`@dojo/core/load` module](docs/load.md) can be used to dynamically load modules or other arbitrary resources via plugins.
#### string
The [`@dojo/core/stringExtras` module](docs/stringExtras.md) contains various string functions that are not available as part of the ES2015 String APIs.
#### UrlSearchParams
The [`@dojo/core/UrlSearchParams` class](docs/UrlSearchParams.md) can be used to parse and generate URL query strings.
#### Event handling
The [`@dojo/core/on` module](docs/on.md) contains methods to handle events across types of listeners. It also includes methods to handle different event use cases including only firing
once and pauseable events.
#### HTTP requests
The [`@dojo/core/request` module](docs/request.md) contains methods to simplify making HTTP requests. It can handle
making requests in both node and the browser through the same methods.
### Promises and Asynchronous Operations
#### Promise
The `@dojo/core/Promise` class is an implementation of the ES2015 Promise API that also includes static state inspection and a `finally` method for cleanup actions.
`@dojo/core/async` contains a number of classes and utility modules to simplify working with asynchronous operations.
#### Task
The `@dojo/core/async/Task` class is an extension of `@dojo/core/Promise` that provides cancelation support.
### Code Style
This repository uses [`prettier`](https://prettier.io/) for code styling rules and formatting. A pre-commit hook is installed automatically and configured to run `prettier` against all staged files as per the configuration in the project's `package.json`.
An additional npm script to run `prettier` (with write set to `true`) against all `src` and `test` project files is available by running:
```bash
npm run prettier
```
### Installation
To start working with this package, clone the repository and run `npm install`.
In order to build the project run `grunt dev` or `grunt dist`.
### Testing
Test cases MUST be written using [Intern](https://theintern.github.io) using the Object test interface and Assert assertion interface.
90% branch coverage MUST be provided for all code submitted to this repository, as reported by istanbuls combined coverage results for all supported platforms.
To test locally in node run:
`grunt test`
To test against browsers with a local selenium server run:
`grunt test:local`
To test against BrowserStack or Sauce Labs run:
`grunt test:browserstack`
or
`grunt test:saucelabs`
## Licensing information
© 20042018 [JS Foundation](https://js.foundation/) & contributors. [New BSD](http://opensource.org/licenses/BSD-3-Clause) license.
Some string functions (`codePointAt`, `fromCodePoint`, and `repeat`) adopted from polyfills by Mathias Bynens,
under the [MIT](http://opensource.org/licenses/MIT) license.
See [LICENSE](LICENSE) for details.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fdojo%2Fcore.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fdojo%2Fcore?ref=badge_large)
<!-- doc-viewer-config
{
"api": "docs/api.json",
"pages": [
"docs/UrlSearchParams.md",
"docs/has.md",
"docs/lang.md",
"docs/load.md",
"docs/on.md",
"docs/request.md",
"docs/stringExtras.md"
]
}
-->

11
src/lib/core/codecov.yml Normal file
View File

@ -0,0 +1,11 @@
comment:
branches:
- master
- feature/*
coverage:
notify:
slack:
default:
url: "secret:Edc+HC1cJt1JJoV0jwebOEPNfk5DXBFBMMWgjsFRUJUaymGWE8gQ/D8oCHq42U7cXaWEtKDc3fa4hBtdtVyZTsae/MXdu1x4HNkBuLx2ZI3WKOerVZRaJ6bXkA9EtvSXa6gjVouiGGIrnjkb4DxRS72m4bqnFKMHSGkkzgComjk="
threshold: 2
attachments: "sunburst, diff"

BIN
src/lib/core/deploy_key.enc Normal file

Binary file not shown.

View File

@ -0,0 +1,151 @@
# UrlSearchParams
The `UrlSearchParams` object makes working with URL query parameters a little easier.
## Creating a Url Search Params Object
### With search params string
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
```
### With object of search params
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams({
a: 'b',
b: 'c',
c: 'a'
});
```
## Instance methods
### `append`
Adds the value to the values that are associated with the key.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
const key = 'a';
const value = 'e';
searchParams.append(key, value);
```
### `delete`
Removes the key from the search params.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
const key = 'a';
searchParams.delete(key);
```
### `get`
Retrieves the first value for the key provided.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=first&a=second');
const key = 'a';
const result = searchParams.get(key);
result === 'first'; // true
```
### `getAll`
Retrieves all the values for the key provided.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=first&a=second');
const key = 'a';
const result = searchParams.getAll(key);
result[0] === 'first'; // true
result[1] === 'second'; // true
```
### `has`
Returns true if the key exists within the search params and false if it is not.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
const key = 'd';
const result = searchParams.has(key);
result === false; // true
```
### `keys`
Returns an array of the keys of the search params.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
const result = searchParams.keys();
result instanceof Array; // true
result[0] === 'a'; // true
result[1] === 'b'; // true
result[2] === 'c'; // true
```
### `set`
Sets the value of a key (clears previous values).
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
const key = 'a';
const value = 'e';
searchParams.set(key, value);
```
### `toString`
Return a string of the search params.
```
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
const result = searchParams.toString();
result === 'a=b&b=c&c=a'; // true
```

53322
src/lib/core/docs/api.json Normal file

File diff suppressed because it is too large Load Diff

40
src/lib/core/docs/has.md Normal file
View File

@ -0,0 +1,40 @@
# has
## Detecting Features
The default export of `dojo-core/has` is a function which accepts a single parameter: the name of the feature to test for.
If the feature is available, a truthy value is returned, otherwise a falsy value is returned:
```ts
import has from 'dojo-core/has';
if (has('dom-addeventlistener')) {
element.addEventListener('click', function () { /* ... */ });
}
```
## Adding Feature Detections
It's important to be able to add new feature tests that aren't provided out-of-the-box by `dojo-core/has`.
This can be done easily by using the `add` function exported by the `has` module. It accepts two parameters:
the name of the feature, and either an immediate value indicating its availability or a function that resolves to a
value.
When a function is passed, the feature will be lazily evaluated - i.e. the function is not executed until the feature is
actually requested. The return value is then cached for future calls for the same feature.
```ts
import { add as hasAdd } from 'dojo-core/has';
hasAdd('dom-queryselector', 'querySelector' in document && 'querySelectorAll' in document);
// Lazily executed; useful if a polyfill is loaded after page load
hasAdd('typedarray', function () {
return 'ArrayBuffer' in window;
});
```
## Accessing the Feature Cache
`dojo-core/has` maintains object hashes containing keys that correspond to all features that have been both
registered _and_ requested. The value associated with each feature name key corresponds to that feature's availability
in the current environment. The object hash containing evaluated features is accessible via the `cache` export.

210
src/lib/core/docs/lang.md Normal file
View File

@ -0,0 +1,210 @@
# lang
## Module Exports
### `assign`
Copies values of own properties from the source object(s) to the target object.
```ts
import { assign } from '@dojo/core/lang';
var target = {
foo: 'bar'
};
var source = {
bar: 'foo'
};
assign(target, source);
target.foo === 'bar'; // true
target.bar === 'foo'; // true
```
### `create`
Creates a new object from the given prototype, and copies all enumerable own properties of one or more source objects to the newly created target object.
```ts
import { create } from '@dojo/core/lang';
var oldObj = {
foo: 'bar',
obj: {
bar: 'foo'
}
};
var newObj = create(oldObj, {
bar: 'foo'
});
newObj.bar === 'foo'; // true
newObj.foo === 'bar'; // true
newObj.obj.bar === 'foo'; // true
oldObj.foo = 'foo';
oldObj.obj.bar = 'bar';
newObj.foo === 'bar'; // true
newObj.obj.bar === 'bar'; // true
```
### `deepAssign`
Copies the values of all enumerable own properties of one or more source objects to the target object, recursively copying all nested objects and arrays as well.
```ts
import { deepAssign } from '@dojo/core/lang';
var oldObj = {
foo: 'bar',
obj: {
bar: 'foo'
}
};
var newObj = deepAssign(oldObj, {
bar: 'foo'
});
newObj.bar === 'foo'; // true
newObj.foo === 'bar'; // true
newObj.obj.bar === 'foo'; // true
oldObj.foo = 'foo';
oldObj.obj.bar = 'bar';
newObj.foo === 'bar'; // true
newObj.obj.bar === 'bar'; // true
```
### `mixin`
Copies values of own and inherited properties from the source object(s) to the target object.
```ts
import { mixin } from '@dojo/core/lang';
const obj = {
foo: 'bar',
fooObj: {
bar: 'foo'
}
};
const result = mixin({}, obj);
result.foo === 'bar'; // true
result.fooObj.bar === 'foo'; // true
obj.fooObj.bar = 'bar';
result.fooObj.bar === 'bar'; // true
```
### `deepMixin`
Copies the values of all enumerable (own or inherited) properties of one or more source objects to the target object, recursively copying all nested objects and arrays as well.
```ts
import { deepMixin } from '@dojo/core/lang';
const obj = {
foo: 'bar',
fooObj: {
bar: 'foo'
}
};
const result = deepMixin({}, obj);
result.foo === 'bar'; // true
result.fooObj.bar === 'foo'; // true
obj.fooObj.bar = 'bar';
result.fooObj.bar === 'bar'; // false
result.fooObj.bar === 'foo'; // true
```
### `duplicate`
Creates a new object using the provided source's prototype as the prototype for the new object, and then deep copies the provided source's values into the new target.
```ts
import { duplicate } from '@dojo/core/lang';
var oldObj = {
foo: 'bar'
};
var newObj = duplicate(oldObj);
oldObj.foo = 'foo';
oldObj.foo === 'foo';
newObj.foo === 'bar';
```
### `partial`
Returns a function which invokes the given function with the given arguments prepended to its argument list. Like `Function.prototype.bind`, but does not alter execution context.
```ts
import { partial } from '@dojo/core/lang';
var add = function (a, b) {
return a + b;
}
var addFive = partial(add, 5);
var result = addFive(4);
result === 9;
```
### `isIdentical`
Determines whether two values are the same (including NaN).
```ts
import { isIdentical } from '@dojo/core/lang';
isIdentical(1, 1); // true
isIdentical(NaN, NaN); // true
```
### `lateBind`
Creates a function that calls the current method on an object with given arguments.
```ts
import { lateBind } from '@dojo/core/lang';
var person = {
speak: function (name) {
return 'hi, ' + name;
}
};
var personSpeak = lateBind(person, 'speak', 'name');
personSpeak() === 'hi, name'; // true
person.speak = function (name) {
return 'bye, ' + name;
};
personSpeak() === 'bye, name';
```

117
src/lib/core/docs/load.md Normal file
View File

@ -0,0 +1,117 @@
# load
## Module Exports
### 'isPlugin'
Tests a value to determine whether is a plugin (an object with a `load` method).
```ts
import { isPlugin } from '@dojo/core/load';
// true
isPlugin({
load() {}
normalize() {}
});
isPlugin(1); // false
isPlugin([]); // false
isPlugin([]); // false
// false
isPlugin({
observer() {}
});
```
### 'load'
Dynamically loads a module or other resource.
```ts
import load, { useDefault } from '@dojo/core/load';
// Load a single module
load('mymodule').then(([ myModule ]: [ any ]) => {
// ...
});
// Load multiple modules
load('namespace/first', 'namespace/second').then(([ first, second ]: [ any, any ]) => {
// ...
});
// Load modules with relative ids as relative to the current module
load(require, './first', './second').then(([ first, second ]: [ any, any ]) => {
// ...
});
// Automatically map modules to their default export
load('namespace/first').then(useDefault).then(([ first ]: [ any ]) => {
// ...
});
// Load a custom resource via a plugin
load(require, 'plugin!./template.html').then(([ html ]: [ string ]) => {
// ...
});
```
#### Using Plugins
AMD-style plugins can be used with `load` by passing in module ids with the format `{pluginId}!{resourceId}`. First, the plugin will be loaded, and then the resource id (the string that follows the `!` in the mid passed to `core/load`) will be normalized and passed to the plugin's `load` method. If the plugin module does not actually have a `load` method, then the resource id is ignored, and the module is returned as-is. Plugins can also expose an optional `normalize` method that is passed the resource id and a resolver method (which will be either `require.toUrl`, `require.resolve`, or an identity function, depending on the environment). Note that if no `normalize` method is provided, then the provided resource id will be resolved using `require.toUrl` or `require.resolve`, depending on the environment. Again, if the plugin module has a default export, the `normalize` method MUST exist on that object.
Note: the plugins that can be used with `load` loosely follow the [amdjs plugin API](https://github.com/amdjs/amdjs-api/blob/master/LoaderPlugins.md), with the following exceptions:
1. The plugin's `load` method does not receive a contextual `require` or a configuration object.
2. Rather than execute a callback when the resource has loaded, the plugin's `load` method instead must return a promise that resolves to that resource.
```ts
// Plugin that does not use the default export.
import { Load } from '@dojo/core/load';
export function normalize(resourceId: string, resolver: (id: string) => string): string {
return resolver(resourceId);
}
export function load(resourceId: string, load: Load) {
// This plugin does nothing more than load the resource id with `core/load`.
return load(resourceId);
}
```
```ts
// The same plugin, but using the default export
import { Load } from '@dojo/core/load';
const plugin = {
normalize(resourceId: string, resolver: (id: string) => string): string {
return resolver(resourceId);
},
load(resourceId: string, load: Load) {
// This plugin does nothing more than load the resource id with `core/load`.
return load(resourceId);
}
};
export default plugin;
```
```ts
import load from '@dojo/core/load';
// 1. The module with the id `plugin` is loaded.
// 2. If `plugin` is not actually a plugin, then `plugin` itself is returned.
// 3. If the plugin has a normalize method, then "some/resource/id" is passed to it,
// and the return value is used as the resource id.
// 4. The resource id is passed to the plugin's `load` method.
// 5. The loaded resource is used to resolve the `load` promise.
load('plugin!some/resource/id').then(([ resource ]: [ any ]) => {
// ...
});
```

65
src/lib/core/docs/on.md Normal file
View File

@ -0,0 +1,65 @@
# On
`dojo-core/on` provides event handling support with methods to attach and emit events.
## `emit`
Dispatch event to target.
```ts
import { emit } from '@dojo/core/on';
var button = document.getElementById('button');
var DOMEventObject = {
type: 'click',
bubbles: true,
cancelable: true
};
emit(button, DOMEventObject);
```
## `on`
Adds event listener to target.
```ts
import { on } from '@dojo/core/on';
var button = document.getElementById('button');
on(button, 'click', function (event) {
console.log(event.target.id);
});
```
## `once`
Attach an event that can only be called once to a target.
```ts
import { once } from '@dojo/core/on';
var button = document.getElementById('button');
once(button, 'click', function (event) {
console.log(event.target.id);
console.log('this event has been removed')
});
```
## `pausable`
Attach an event that can be paused to a target.
```ts
import { pausable } from '@dojo/core/on';
var button = document.getElementById('button');
var buttonClickHandle = pausable(button, 'click', function (event) {
console.log(event.target.id);
});
buttonClickHandle.pause(); // when paused the event will not fire
buttonClickHandle.resume(); // after resuming the event will begin to fire again if triggered
```

View File

@ -0,0 +1,75 @@
# request
This modules provides 4 methods (get, post, delete, and put) to simplify sending http requests. Each of these methods returns a promise that resolves with the response.
* request
* get
* post
* delete
* put
## Making Requests
Making requests is similar to using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
A GET request,
```typescript
const json = await request('http://www.example.com').then(response => response.json());
```
A POST request,
```typescript
const response = await request.post('http://www.example.com', { body: JSON.stringify(myValues)}).then(response => response.json());
```
## Observables
Several observables are available to provide deeper insight into the state of a request.
### Monitoring Upload Progress
Upload progress can be monitored with the `upload` observable on the `Request` object. Since upload events automatically cause a preflight request, they can be disabled by setting `includeUploadProgress: false`.
```typescript
const req = request.post('http://www.example.com/', {
body: someLargeString
});
req.upload.subscribe(totalUploadedBytes => {
// do something with uploaded bytes
})
```
Note that while the Node.js provider will emit a single upload event when it is done uploading, it cannot emit more granular upload events with `string` or `Buffer` body types. To receive more frequent upload events, you can use the `bodyStream` option to provide a `Readable` with the body content. Upload events will be emitted as the data is read from the stream.
```typescript
request.post('http://www.example.com/', {
bodyStream: fs.createReadStream('some-large-file')
});
```
### Monitoring Download Progress
You can monitor download progress by subscribing to the `download` observable on the `Response` object.
```typescript
request("http://www.example/some-large-file").then(response => {
response.download.subscribe(totalBytesDownloaded => {
// do something with totalBytesDownloaded
});
});
```
### Receiving Raw Data
You can receive the raw data from a response with the `data` observable. Depending on the provider, the value might be a `string`, or a `Buffer`.
```typescript
request("http://www.example/some-large-file").then(response => {
response.data.subscribe(chunk => {
// do something with chunk
});
});
```

View File

@ -0,0 +1,30 @@
# stringExtras
## `escapeRegExp`
Escapes a string to safely be included in regular expressions.
```ts
import { escapeRegExp } from '@dojo/core/string';
const str = 'cat file.js | grep -c';
const result = escapeRegExp(str);
result === 'cat file\\.js \\| grep -c'; // true
```
## `escapeXml`
Escapes XML (or HTML) content in a string.
```ts
import { escapeXml } from '@dojo/core/string';
const badCode = "<script>alert('hi')</script>";
const sanitized = escapeXml(badCode);
sanitized === '&lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;'; // true
```

91
src/lib/core/intern.json Normal file
View File

@ -0,0 +1,91 @@
{
"capabilities+": {
"project": "Dojo 2",
"name": "@dojo/core",
"fixSessionCapabilities": false,
"browserstack.debug": false
},
"environments": [
{ "browserName": "node" }
],
"suites": [
"./_build/tests/unit/all.js"
],
"functionalSuites": [
"./_build/tests/functional/all.js"
],
"plugins": [
"./_build/tests/plugins/echo-service.js"
],
"browser": {
"loader": "./node_modules/grunt-dojo2/lib/intern/internLoader.js",
"suites+": [
"./_build/tests/unit/all-browser.js"
]
},
"node": {
"suites+": [
"./_build/tests/unit/all-node.js"
]
},
"coverage": [
"./_build/src/**/*.js"
],
"configs": {
"browserstack": {
"tunnel": "browserstack",
"environments+": [
{ "browserName": "internet explorer", "version": "11", "os": "WINDOWS", "os_version": [ "8.1", "10" ] },
{ "browserName": "edge" },
{ "browserName": "firefox", "platform": "WINDOWS" },
{ "browserName": "chrome", "platform": "WINDOWS" },
{ "browserName": "safari", "version": "10.1", "platform": "MAC" }
],
"maxConcurrency": 5
},
"node-loader": {
"node": {
"loader": "./node_modules/grunt-dojo2/lib/intern/internLoader.js",
"suites+": [
"./_build/tests/unit/all-node-loader.js"
]
}
},
"local": {
"tunnel": "selenium",
"tunnelOptions": {
"hostname": "localhost",
"port": 4444
},
"environments+": [
{ "browserName": "chrome" }
]
},
"saucelabs": {
"tunnel": "saucelabs",
"tunnelOptions": {},
"defaultTimeout": 10000,
"environments+": [
{ "browserName": "internet explorer", "version": [ "11.0" ], "platform": "Windows 7" },
{ "browserName": "firefox", "version": "43", "platform": "Windows 10" },
{ "browserName": "chrome", "platform": "Windows 10" }
],
"maxConcurrency": 4
}
}
}

78
src/lib/core/package.json Normal file
View File

@ -0,0 +1,78 @@
{
"name": "@dojo/core",
"version": "2.0.1-pre",
"description": "Basic utilites for common TypeScript development",
"engines": {
"npm": ">=3.0.0"
},
"homepage": "https://dojo.io",
"bugs": {
"url": "https://github.com/dojo/core/issues"
},
"license": "BSD-3-Clause",
"private": true,
"main": "main.js",
"repository": {
"type": "git",
"url": "https://github.com/dojo/core.git"
},
"scripts": {
"prepublish": "grunt peerDepInstall",
"precommit": "lint-staged",
"prettier": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
"test": "grunt test"
},
"dependencies": {
"tslib": "~1.8.1"
},
"peerDependencies": {
"@dojo/has": "^2.0.0",
"@dojo/shim": "^2.0.0"
},
"devDependencies": {
"@dojo/loader": "^2.0.0",
"@theintern/leadfoot": "~2.0.2",
"@types/benchmark": "~1.0.0",
"@types/chai": "~4.0.0",
"@types/express": "~4.0.39",
"@types/glob": "~5.0.0",
"@types/grunt": "~0.4.0",
"@types/multer": "~1.3.3",
"@types/node": "~9.6.5",
"@types/sinon": "~1.16.0",
"benchmark": "^1.0.0",
"express": "~4.15.3",
"grunt": "^1.0.1",
"grunt-contrib-clean": ">=1.0.0",
"grunt-contrib-copy": ">=1.0.0",
"grunt-dojo2": "latest",
"grunt-postcss": "^0.8.0",
"grunt-text-replace": ">=0.4.0",
"grunt-ts": ">=5.0.0",
"grunt-tslint": "5.0.1",
"grunt-typings": "^0.1.5",
"husky": "0.14.3",
"intern": "~4.1.0",
"lint-staged": "6.0.0",
"multer": "~1.3.0",
"prettier": "1.9.2",
"remap-istanbul": ">=0.6.3",
"sinon": "~1.17.6",
"tslint": "5.8.0",
"typescript": "~2.6.1"
},
"lint-staged": {
"*.{ts,tsx}": [
"prettier --write",
"git add"
]
},
"prettier": {
"singleQuote": true,
"tabWidth": 4,
"useTabs": true,
"parser": "typescript",
"printWidth": 120,
"arrowParens": "always"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,350 @@
import { Hash } from './interfaces';
export interface KwArgs {
dayOfMonth?: number;
hours?: number;
milliseconds?: number;
minutes?: number;
month: number;
seconds?: number;
year: number;
}
export interface OperationKwArgs {
days?: number;
hours?: number;
milliseconds?: number;
minutes?: number;
months?: number;
seconds?: number;
years?: number;
}
/**
* The properties of a complete date
*/
export interface DateProperties {
dayOfMonth: number;
readonly dayOfWeek: number;
readonly daysInMonth: number;
hours: number;
readonly isLeapYear: boolean;
milliseconds: number;
minutes: number;
month: number;
seconds: number;
year: number;
}
const days = [NaN, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const isLeapYear = (function() {
const date = new Date();
function isLeapYear(year: number): boolean {
date.setFullYear(year, 1, 29);
return date.getDate() === 29;
}
return isLeapYear;
})();
const operationOrder = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'];
const operationHash: Hash<string> = Object.create(null, {
days: { value: 'Date' },
hours: { value: 'UTCHours' },
milliseconds: { value: 'UTCMilliseconds' },
minutes: { value: 'UTCMinutes' },
months: { value: 'Month' },
seconds: { value: 'UTCSeconds' },
years: { value: 'FullYear' }
});
export class DateObject implements DateProperties {
static parse(str: string): DateObject {
return new DateObject(Date.parse(str));
}
static now(): DateObject {
return new DateObject(Date.now());
}
private readonly _date: Date;
readonly utc: DateProperties;
constructor(value: number);
constructor(value: string);
constructor(value: Date);
constructor(value: KwArgs);
constructor();
constructor(value?: any) {
let _date: Date;
if (!arguments.length) {
_date = new Date();
} else if (value instanceof Date) {
_date = new Date(+value);
} else if (typeof value === 'number' || typeof value === 'string') {
_date = new Date(<any>value);
} else {
_date = new Date(
value.year,
value.month - 1,
value.dayOfMonth || 1,
value.hours || 0,
value.minutes || 0,
value.seconds || 0,
value.milliseconds || 0
);
}
this._date = _date;
const self = this;
this.utc = {
get isLeapYear(this: DateObject): boolean {
return isLeapYear(this.year);
},
get daysInMonth(this: DateObject): number {
const month = this.month;
if (month === 2 && this.isLeapYear) {
return 29;
}
return days[month];
},
get year(): number {
return self._date.getUTCFullYear();
},
set year(year: number) {
self._date.setUTCFullYear(year);
},
get month(): number {
return self._date.getUTCMonth() + 1;
},
set month(month: number) {
self._date.setUTCMonth(month - 1);
},
get dayOfMonth(): number {
return self._date.getUTCDate();
},
set dayOfMonth(day: number) {
self._date.setUTCDate(day);
},
get hours(): number {
return self._date.getUTCHours();
},
set hours(hours: number) {
self._date.setUTCHours(hours);
},
get minutes(): number {
return self._date.getUTCMinutes();
},
set minutes(minutes: number) {
self._date.setUTCMinutes(minutes);
},
get seconds(): number {
return self._date.getUTCSeconds();
},
set seconds(seconds: number) {
self._date.setUTCSeconds(seconds);
},
get milliseconds(): number {
return self._date.getUTCMilliseconds();
},
set milliseconds(milliseconds: number) {
self._date.setUTCMilliseconds(milliseconds);
},
get dayOfWeek(): number {
return self._date.getUTCDay();
},
toString: function(): string {
return self._date.toUTCString();
}
} as any;
}
get isLeapYear(): boolean {
return isLeapYear(this.year);
}
get daysInMonth(): number {
const month = this.month;
if (month === 2 && this.isLeapYear) {
return 29;
}
return days[month];
}
get year(): number {
return this._date.getFullYear();
}
set year(year: number) {
const dayOfMonth = this.dayOfMonth;
this._date.setFullYear(year);
if (this.dayOfMonth < dayOfMonth) {
this.dayOfMonth = 0;
}
}
get month(): number {
return this._date.getMonth() + 1;
}
set month(month: number) {
const dayOfMonth = this.dayOfMonth;
this._date.setMonth(month - 1);
if (this.dayOfMonth < dayOfMonth) {
this.dayOfMonth = 0;
}
}
get dayOfMonth(): number {
return this._date.getDate();
}
set dayOfMonth(day: number) {
this._date.setDate(day);
}
get hours(): number {
return this._date.getHours();
}
set hours(hours: number) {
this._date.setHours(hours);
}
get minutes(): number {
return this._date.getMinutes();
}
set minutes(minutes: number) {
this._date.setMinutes(minutes);
}
get seconds(): number {
return this._date.getSeconds();
}
set seconds(seconds: number) {
this._date.setSeconds(seconds);
}
get milliseconds(): number {
return this._date.getMilliseconds();
}
set milliseconds(milliseconds: number) {
this._date.setMilliseconds(milliseconds);
}
get time(): number {
return this._date.getTime();
}
set time(time: number) {
this._date.setTime(time);
}
get dayOfWeek(): number {
return this._date.getDay();
}
get timezoneOffset(): number {
return this._date.getTimezoneOffset();
}
add(value: number): DateObject;
add(value: OperationKwArgs): DateObject;
add(value: any): DateObject {
const result = new DateObject(this.time);
if (typeof value === 'number') {
result.time += value;
} else {
// Properties have to be added in a particular order to properly handle
// date overshoots in month and year calculations
operationOrder.forEach((property: string): void => {
if (!(property in value)) {
return;
}
const dateMethod = operationHash[property];
(<any>result._date)[`set${dateMethod}`]((<any>this._date)[`get${dateMethod}`]() + value[property]);
if ((property === 'years' || property === 'months') && result.dayOfMonth < this.dayOfMonth) {
// Set the day of the month to 0 to move the date to the first day of the previous
// month to fix overshoots when adding a month and the date is the 31st or adding
// a year and the date is the 29th
result.dayOfMonth = 0;
}
});
}
return result;
}
compare(value: DateObject): number {
const result = this.time - value.time;
if (result > 0) {
return 1;
}
if (result < 0) {
return -1;
}
return 0;
}
compareDate(value: KwArgs): number {
const left = new DateObject(this);
const right = new DateObject(value);
left._date.setHours(0, 0, 0, 0);
right._date.setHours(0, 0, 0, 0);
return left.compare(right);
}
compareTime(value: KwArgs): number {
const left = new DateObject(this);
const right = new DateObject(value);
left._date.setFullYear(0, 0, 0);
right._date.setFullYear(0, 0, 0);
return left.compare(right);
}
toString(): string {
return this._date.toString();
}
toDateString(): string {
return this._date.toDateString();
}
toTimeString(): string {
return this._date.toTimeString();
}
toLocaleString(): string {
return this._date.toLocaleString();
}
toLocaleDateString(): string {
return this._date.toLocaleDateString();
}
toLocaleTimeString(): string {
return this._date.toLocaleTimeString();
}
toISOString(): string {
return this._date.toISOString();
}
toJSON(key?: any): string {
return this._date.toJSON(key);
}
valueOf(): number {
return this._date.valueOf();
}
}
export default DateObject;

View File

@ -0,0 +1,67 @@
import { Handle } from './interfaces';
import { createCompositeHandle } from './lang';
import Promise from '@dojo/shim/Promise';
/**
* No operation function to replace own once instance is destoryed
*/
function noop(): Promise<boolean> {
return Promise.resolve(false);
}
/**
* No op function used to replace own, once instance has been destoryed
*/
function destroyed(): never {
throw new Error('Call made to destroyed method');
}
export class Destroyable {
/**
* register handles for the instance
*/
private handles: Handle[];
/**
* @constructor
*/
constructor() {
this.handles = [];
}
/**
* Register handles for the instance that will be destroyed when `this.destroy` is called
*
* @param {Handle} handle The handle to add for the instance
* @returns {Handle} a handle for the handle, removes the handle for the instance and calls destroy
*/
own(handles: Handle | Handle[]): Handle {
const handle = Array.isArray(handles) ? createCompositeHandle(...handles) : handles;
const { handles: _handles } = this;
_handles.push(handle);
return {
destroy() {
_handles.splice(_handles.indexOf(handle));
handle.destroy();
}
};
}
/**
* Destrpys all handers registered for the instance
*
* @returns {Promise<any} a promise that resolves once all handles have been destroyed
*/
destroy(): Promise<any> {
return new Promise((resolve) => {
this.handles.forEach((handle) => {
handle && handle.destroy && handle.destroy();
});
this.destroy = noop;
this.own = destroyed;
resolve(true);
});
}
}
export default Destroyable;

132
src/lib/core/src/Evented.ts Normal file
View File

@ -0,0 +1,132 @@
import Map from '@dojo/shim/Map';
import { Handle, EventType, EventObject } from './interfaces';
import { Destroyable } from './Destroyable';
/**
* Map of computed regular expressions, keyed by string
*/
const regexMap = new Map<string, RegExp>();
/**
* Determines is the event type glob has been matched
*
* @returns boolean that indicates if the glob is matched
*/
export function isGlobMatch(globString: string | symbol, targetString: string | symbol): boolean {
if (typeof targetString === 'string' && typeof globString === 'string' && globString.indexOf('*') !== -1) {
let regex: RegExp;
if (regexMap.has(globString)) {
regex = regexMap.get(globString)!;
} else {
regex = new RegExp(`^${globString.replace(/\*/g, '.*')}$`);
regexMap.set(globString, regex);
}
return regex.test(targetString);
} else {
return globString === targetString;
}
}
export type EventedCallback<T = EventType, E extends EventObject<T> = EventObject<T>> = {
/**
* A callback that takes an `event` argument
*
* @param event The event object
*/
(event: E): boolean | void;
};
export interface CustomEventTypes<T extends EventObject<any> = EventObject<any>> {
[index: string]: T;
}
/**
* A type which is either a targeted event listener or an array of listeners
* @template T The type of target for the events
* @template E The event type for the events
*/
export type EventedCallbackOrArray<T = EventType, E extends EventObject<T> = EventObject<T>> =
| EventedCallback<T, E>
| EventedCallback<T, E>[];
/**
* Event Class
*/
export class Evented<
M extends CustomEventTypes = {},
T = EventType,
O extends EventObject<T> = EventObject<T>
> extends Destroyable {
// The following member is purely so TypeScript remembers the type of `M` when extending so
// that the utilities in `on.ts` will work https://github.com/Microsoft/TypeScript/issues/20348
// tslint:disable-next-line
protected __typeMap__?: M;
/**
* map of listeners keyed by event type
*/
protected listenersMap: Map<T | keyof M, EventedCallback<T, O>[]> = new Map();
/**
* Emits the event object for the specified type
*
* @param event the event to emit
*/
emit<K extends keyof M>(event: M[K]): void;
emit(event: O): void;
emit(event: any): void {
this.listenersMap.forEach((methods, type) => {
if (isGlobMatch(type as any, event.type)) {
[...methods].forEach((method) => {
method.call(this, event);
});
}
});
}
/**
* Catch all handler for various call signatures. The signatures are defined in
* `BaseEventedEvents`. You can add your own event type -> handler types by extending
* `BaseEventedEvents`. See example for details.
*
* @param args
*
* @example
*
* interface WidgetBaseEvents extends BaseEventedEvents {
* (type: 'properties:changed', handler: PropertiesChangedHandler): Handle;
* }
* class WidgetBase extends Evented {
* on: WidgetBaseEvents;
* }
*
* @return {any}
*/
on<K extends keyof M>(type: K, listener: EventedCallbackOrArray<K, M[K]>): Handle;
on(type: T, listener: EventedCallbackOrArray<T, O>): Handle;
on(type: any, listener: EventedCallbackOrArray<any, any>): Handle {
if (Array.isArray(listener)) {
const handles = listener.map((listener) => this._addListener(type, listener));
return {
destroy() {
handles.forEach((handle) => handle.destroy());
}
};
}
return this._addListener(type, listener);
}
private _addListener(type: T | keyof M, listener: EventedCallback<T, O>) {
const listeners = this.listenersMap.get(type) || [];
listeners.push(listener);
this.listenersMap.set(type, listeners);
return {
destroy: () => {
const listeners = this.listenersMap.get(type) || [];
listeners.splice(listeners.indexOf(listener), 1);
}
};
}
}
export default Evented;

View File

@ -0,0 +1,180 @@
import Map from '@dojo/shim/Map';
import WeakMap from '@dojo/shim/WeakMap';
import { Iterable, IterableIterator } from '@dojo/shim/iterator';
import { Handle } from './interfaces';
import '@dojo/shim/Symbol';
import List from './List';
const noop = () => {};
interface Entry<V> {
readonly handle: Handle;
readonly value: V;
}
interface State<V extends object> {
readonly entryMap: Map<Identity, Entry<V>>;
readonly idMap: WeakMap<V, Identity>;
}
const privateStateMap = new WeakMap<IdentityRegistry<any>, State<any>>();
function getState<V extends object>(instance: IdentityRegistry<V>): State<V> {
return privateStateMap.get(instance)!;
}
/**
* Registry identities can be strings or symbols. Note that the empty string is allowed.
*/
export type Identity = string | symbol;
/**
* A registry of values, mapped by identities.
*/
export class IdentityRegistry<V extends object> implements Iterable<[Identity, V]> {
constructor() {
privateStateMap.set(this, {
entryMap: new Map<Identity, Entry<V>>(),
idMap: new WeakMap<V, Identity>()
});
}
/**
* Look up a value by its identifier.
*
* Throws if no value has been registered for the given identifier.
*
* @param id The identifier
* @return The value
*/
get(id: Identity): V {
const entry = getState(this).entryMap.get(id);
if (!entry) {
throw new Error(`Could not find a value for identity '${id.toString()}'`);
}
return entry.value;
}
/**
* Determine whether the value has been registered.
* @param value The value
* @return `true` if the value has been registered, `false` otherwise
*/
contains(value: V): boolean {
return getState(this).idMap.has(value);
}
/**
* Remove from the registry the value for a given identifier.
* @param id The identifier
* @return `true` if the value was removed, `false` otherwise
*/
delete(id: Identity): boolean {
const entry = getState(this).entryMap.get(id);
if (!entry) {
return false;
}
entry.handle.destroy();
return true;
}
/**
* Determine whether a value has been registered for the given identifier.
* @param id The identifier
* @return `true` if a value has been registered, `false` otherwise
*/
has(id: Identity): boolean {
return getState(this).entryMap.has(id);
}
/**
* Look up the identifier for which the given value has been registered.
*
* Throws if the value hasn't been registered.
*
* @param value The value
* @return The identifier otherwise
*/
identify(value: V): Identity | undefined {
if (!this.contains(value)) {
throw new Error('Could not identify non-registered value');
}
return getState(this).idMap.get(value);
}
/**
* Register a new value with a new identity.
*
* Throws if a different value has already been registered for the given identity,
* or if the value has already been registered with a different identity.
*
* @param id The identifier
* @param value The value
* @return A handle for deregistering the value. Note that when called repeatedly with
* the same identifier and value combination, the same handle is returned
*/
register(id: Identity, value: V): Handle {
const entryMap = getState<V>(this).entryMap;
const existingEntry = entryMap.get(id);
if (existingEntry && existingEntry.value !== value) {
const str = id.toString();
throw new Error(`A value has already been registered for the given identity (${str})`);
}
const existingId = this.contains(value) ? this.identify(value) : null;
if (existingId && existingId !== id) {
const str = existingId.toString();
throw new Error(`The value has already been registered with a different identity (${str})`);
}
// Adding the same value with the same id is a noop, return the original handle.
if (existingEntry && existingId) {
return existingEntry.handle;
}
const handle = {
destroy: () => {
handle.destroy = noop;
getState(this).entryMap.delete(id);
}
};
entryMap.set(id, { handle, value });
getState(this).idMap.set(value, id);
return handle;
}
entries(): IterableIterator<[Identity, V]> {
const values = new List<[Identity, V]>();
getState(this).entryMap.forEach((value: Entry<V>, key: Identity) => {
values.add([key, value.value]);
});
return values.values();
}
values(): IterableIterator<V> {
const values = new List<V>();
getState(this).entryMap.forEach((value: Entry<V>, key: Identity) => {
values.add(value.value);
});
return values.values();
}
ids(): IterableIterator<Identity> {
return getState(this).entryMap.keys();
}
[Symbol.iterator](): IterableIterator<[Identity, V]> {
return this.entries();
}
}
export default IdentityRegistry;

106
src/lib/core/src/List.ts Normal file
View File

@ -0,0 +1,106 @@
import { isArrayLike, isIterable, Iterable, IterableIterator, ShimIterator } from '@dojo/shim/iterator';
import WeakMap from '@dojo/shim/WeakMap';
const listItems: WeakMap<List<any>, any[]> = new WeakMap<List<any>, any[]>();
function getListItems<T>(list: List<T>): T[] {
return (listItems.get(list) || []) as T[];
}
export class List<T> {
[Symbol.iterator]() {
return this.values();
}
get size(): number {
return getListItems(this).length;
}
constructor(source?: Iterable<T> | ArrayLike<T>) {
listItems.set(this, []);
if (source) {
if (isArrayLike(source)) {
for (let i = 0; i < source.length; i++) {
this.add(source[i]);
}
} else if (isIterable(source)) {
for (const item of source) {
this.add(item);
}
}
}
}
add(value: T): this {
getListItems(this).push(value);
return this;
}
clear(): void {
listItems.set(this, []);
}
delete(idx: number): boolean {
if (idx < this.size) {
getListItems(this).splice(idx, 1);
return true;
}
return false;
}
entries(): IterableIterator<[number, T]> {
return new ShimIterator<[number, T]>(getListItems(this).map<[number, T]>((value, index) => [index, value]));
}
forEach(fn: (value: T, idx: number, list: this) => void, thisArg?: any): void {
getListItems(this).forEach(fn.bind(thisArg ? thisArg : this));
}
has(idx: number): boolean {
return this.size > idx;
}
includes(value: T): boolean {
return getListItems(this).indexOf(value) >= 0;
}
indexOf(value: T): number {
return getListItems(this).indexOf(value);
}
join(separator: string = ','): string {
return getListItems(this).join(separator);
}
keys(): IterableIterator<number> {
return new ShimIterator<number>(getListItems(this).map<number>((_, index) => index));
}
lastIndexOf(value: T): number {
return getListItems(this).lastIndexOf(value);
}
push(value: T): void {
this.add(value);
}
pop(): T | undefined {
return getListItems(this).pop();
}
splice(start: number, deleteCount?: number, ...newItems: T[]): T[] {
return getListItems(this).splice(
start,
deleteCount === undefined ? this.size - start : deleteCount,
...newItems
);
}
values(): IterableIterator<T> {
return new ShimIterator<T>(getListItems(this).map<T>((value) => value));
}
}
export default List;

View File

@ -0,0 +1,89 @@
import { Handle } from './interfaces';
/**
* An entry in a MatchRegistry. Each Entry contains a test to determine whether the Entry is applicable, and a value for
* the entry.
*/
interface Entry<T> {
readonly test: Test | null;
readonly value: T | null;
}
/**
* A registry of values tagged with matchers.
*/
export class MatchRegistry<T> {
protected _defaultValue: T | undefined;
private readonly _entries: Entry<T>[] | null;
/**
* Construct a new MatchRegistry, optionally containing a given default value.
*/
constructor(defaultValue?: T) {
this._defaultValue = defaultValue;
this._entries = [];
}
/**
* Return the first entry in this registry that matches the given arguments. If no entry matches and the registry
* was created with a default value, that value will be returned. Otherwise, an exception is thrown.
*
* @param ...args Arguments that will be used to select a matching value.
* @returns the matching value, or a default value if one exists.
*/
match(...args: any[]): T {
const entries = this._entries ? this._entries.slice(0) : [];
let entry: Entry<T>;
for (let i = 0; (entry = entries[i]); ++i) {
if (entry.value && entry.test && entry.test.apply(null, args)) {
return entry.value;
}
}
if (this._defaultValue !== undefined) {
return this._defaultValue;
}
throw new Error('No match found');
}
/**
* Register a test + value pair with this registry.
*
* @param test The test that will be used to determine if the registered value matches a set of arguments.
* @param value A value being registered.
* @param first If true, the newly registered test and value will be the first entry in the registry.
*/
register(test: Test | null, value: T | null, first?: boolean): Handle {
let entries = this._entries;
let entry: Entry<T> | null = {
test: test,
value: value
};
(<any>entries)[first ? 'unshift' : 'push'](entry);
return {
destroy: function(this: Handle) {
this.destroy = function(): void {};
let i = 0;
if (entries && entry) {
while ((i = entries.indexOf(entry, i)) > -1) {
entries.splice(i, 1);
}
}
test = value = entries = entry = null;
}
};
}
}
/**
* The interface that a test function must implement.
*/
export interface Test {
(...args: any[]): boolean | null;
}
export default MatchRegistry;

View File

@ -0,0 +1,237 @@
import { from as arrayFrom } from '@dojo/shim/array';
import { isArrayLike, isIterable, Iterable, IterableIterator, ShimIterator } from '@dojo/shim/iterator';
import Map from '@dojo/shim/Map';
import '@dojo/shim/Symbol';
/**
* A map implmentation that supports multiple keys for specific value.
*
* @param T Accepts the type of the value
*/
export class MultiMap<T> implements Map<any[], T> {
private _map: Map<any, any>;
private _key: symbol;
/**
* @constructor
*
* @param iterator an array or iterator of tuples to initialize the map with.
*/
constructor(iterable?: ArrayLike<[any[], T]> | Iterable<[any[], T]>) {
this._map = new Map<any, any>();
this._key = Symbol();
if (iterable) {
if (isArrayLike(iterable)) {
for (let i = 0; i < iterable.length; i++) {
const value = iterable[i];
this.set(value[0], value[1]);
}
} else if (isIterable(iterable)) {
for (const value of iterable) {
this.set(value[0], value[1]);
}
}
}
}
/**
* Sets the value for the array of keys provided
*
* @param keys The array of keys to store the value against
* @param value the value of the map entry
*
* @return the multi map instance
*/
set(keys: any[], value: T): this {
let map = this._map;
let childMap;
for (let i = 0; i < keys.length; i++) {
if (map.get(keys[i])) {
map = map.get(keys[i]);
continue;
}
childMap = new Map<any, any>();
map.set(keys[i], childMap);
map = childMap;
}
map.set(this._key, value);
return this;
}
/**
* Returns the value entry for the array of keys
*
* @param keys The array of keys to look up the value for
*
* @return The value if found otherwise `undefined`
*/
get(keys: any[]): T | undefined {
let map = this._map;
for (let i = 0; i < keys.length; i++) {
map = map.get(keys[i]);
if (!map) {
return undefined;
}
}
return map.get(this._key);
}
/**
* Returns a boolean indicating if the key exists in the map
*
* @return boolean true if the key exists otherwise false
*/
has(keys: any[]): boolean {
let map = this._map;
for (let i = 0; i < keys.length; i++) {
map = map.get(keys[i]);
if (!map) {
return false;
}
}
return true;
}
/**
* Returns the size of the map, based on the number of unique keys
*/
get size(): number {
return arrayFrom(this.keys()).length;
}
/**
* Deletes the entry for the key provided.
*
* @param keys the key of the entry to remove
* @return boolean trus if the entry was deleted, false if the entry was not found
*/
delete(keys: any[]): boolean {
let map = this._map;
const path = [this._map];
for (let i = 0; i < keys.length; i++) {
map = map.get(keys[i]);
path.push(map);
if (!map) {
return false;
}
}
map.delete(this._key);
for (let i = keys.length - 1; i >= 0; i--) {
map = path[i].get(keys[i]);
if (map.size) {
break;
}
path[i].delete(keys[i]);
}
return true;
}
/**
* Return an iterator that yields each value in the map
*
* @return An iterator containing the instance's values.
*/
values(): IterableIterator<T> {
const values: T[] = [];
const getValues = (map: Map<any, any>) => {
map.forEach((value, key) => {
if (key === this._key) {
values.push(value);
} else {
getValues(value);
}
});
};
getValues(this._map);
return new ShimIterator<T>(values);
}
/**
* Return an iterator that yields each key array in the map
*
* @return An iterator containing the instance's keys.
*/
keys(): IterableIterator<any[]> {
const finalKeys: any[][] = [];
const getKeys = (map: Map<any, any>, keys: any[] = []) => {
map.forEach((value, key) => {
if (key === this._key) {
finalKeys.push(keys);
} else {
const nextKeys = [...keys, key];
getKeys(value, nextKeys);
}
});
};
getKeys(this._map);
return new ShimIterator<any[]>(finalKeys);
}
/**
* Returns an iterator that yields each key/value pair as an array.
*
* @return An iterator for each key/value pair in the instance.
*/
entries(): IterableIterator<[any[], T]> {
const finalEntries: [any[], T][] = [];
const getKeys = (map: Map<any, any>, keys: any[] = []) => {
map.forEach((value, key) => {
if (key === this._key) {
finalEntries.push([keys, value]);
} else {
const nextKeys = [...keys, key];
getKeys(value, nextKeys);
}
});
};
getKeys(this._map);
return new ShimIterator<[any[], T]>(finalEntries);
}
/**
* Executes a given function for each map entry. The function
* is invoked with three arguments: the element value, the
* element key, and the associated Map instance.
*
* @param callback The function to execute for each map entry,
* @param context The value to use for `this` for each execution of the calback
*/
forEach(callback: (value: T, key: any[], mapInstance: MultiMap<T>) => any, context?: {}): void {
const entries = this.entries();
for (const value of entries) {
callback.call(context, value[1], value[0], this);
}
}
/**
* Deletes all keys and their associated values.
*/
clear(): void {
this._map.clear();
}
[Symbol.iterator](): IterableIterator<[any[], T]> {
return this.entries();
}
[Symbol.toStringTag] = 'MultiMap';
}
export default MultiMap;

View File

@ -0,0 +1,171 @@
import ObservableShim, { ObservableObject, Subscribable, SubscriptionObserver } from '@dojo/shim/Observable';
import Promise from '@dojo/shim/Promise';
import { Iterable } from '@dojo/shim/iterator';
function isSubscribable(object: any): object is Subscribable<any> {
return object && object.subscribe !== undefined;
}
export default class Observable<T> extends ObservableShim<T> {
static of<T>(...items: T[]): Observable<T> {
return <Observable<T>>super.of(...items);
}
static from<T>(item: Iterable<T> | ArrayLike<T> | ObservableObject): Observable<T> {
return <Observable<T>>super.from(item);
}
static defer<T>(deferFunction: () => Subscribable<T>): Observable<T> {
return new Observable<T>((observer) => {
const trueObservable = deferFunction();
return trueObservable.subscribe({
next(value: T) {
return observer.next(value);
},
error(errorValue?: any) {
return observer.error(errorValue);
},
complete(completeValue?: any) {
observer.complete(completeValue);
}
});
});
}
toPromise(): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.subscribe({
next(value: T) {
resolve(value);
},
error(error: any) {
reject(error);
}
});
});
}
map<U>(mapFunction: (x: T) => U): Observable<U> {
const self = this;
if (typeof mapFunction !== 'function') {
throw new TypeError('Map parameter must be a function');
}
return new Observable<U>((observer: SubscriptionObserver<U>) => {
self.subscribe({
next(value: T) {
try {
const result: U = mapFunction(value);
return observer.next(result);
} catch (e) {
return observer.error(e);
}
},
error(errorValue?: any) {
return observer.error(errorValue);
},
complete(completeValue?: any) {
return observer.complete(completeValue);
}
});
});
}
filter(filterFunction: (x: T) => boolean): Observable<T> {
const self = this;
if (typeof filterFunction !== 'function') {
throw new TypeError('Filter argument must be a function');
}
return new Observable<T>((observer: SubscriptionObserver<T>) => {
self.subscribe({
next(value: T) {
try {
if (filterFunction(value)) {
return observer.next(value);
}
} catch (e) {
return observer.error(e);
}
},
error(errorValue?: any) {
return observer.error(errorValue);
},
complete(completeValue?: any) {
return observer.complete(completeValue);
}
});
});
}
toArray(): Observable<T[]> {
const self = this;
return new Observable<T[]>((observer) => {
const values: T[] = [];
self.subscribe({
next(value: T) {
values.push(value);
},
error(errorValue?: any) {
return observer.error(errorValue);
},
complete(completeValue?: any) {
observer.next(values);
observer.complete(completeValue);
}
});
});
}
mergeAll(concurrent: number): Observable<any> {
const self = this;
return new Observable<Observable<any>>((observer) => {
let active: any[] = [];
let queue: any[] = [];
function checkForComplete() {
if (active.length === 0 && queue.length === 0) {
observer.complete();
} else if (queue.length > 0 && active.length < concurrent) {
const item = queue.shift();
if (isSubscribable(item)) {
const itemIndex = active.length;
active.push(item);
item.subscribe({
next(value: any) {
observer.next(value);
},
complete() {
active.splice(itemIndex, 1);
checkForComplete();
}
});
} else {
observer.next(item);
checkForComplete();
}
}
}
self.subscribe({
next(value: T) {
queue.push(value);
},
complete() {
checkForComplete();
}
});
});
}
}
// for convienence, re-export some interfaces from shim
export { Observable, Subscribable, SubscriptionObserver as Observer };

View File

@ -0,0 +1,71 @@
import { Handle, EventObject, EventType } from './interfaces';
import Map from '@dojo/shim/Map';
import Evented, { CustomEventTypes, isGlobMatch, EventedCallbackOrArray } from './Evented';
/**
* An implementation of the Evented class that queues up events when no listeners are
* listening. When a listener is subscribed, the queue will be published to the listener.
* When the queue is full, the oldest events will be discarded to make room for the newest ones.
*
* @property maxEvents The number of events to queue before old events are discarded. If zero (default), an unlimited number of events is queued.
*/
class QueuingEvented<
M extends CustomEventTypes = {},
T = EventType,
O extends EventObject<T> = EventObject<T>
> extends Evented<M, T, O> {
private _queue: Map<string | symbol, EventObject[]> = new Map();
public maxEvents = 0;
emit<K extends keyof M>(event: M[K]): void;
emit(event: O): void;
emit(event: any): void {
super.emit(event);
let hasMatch = false;
this.listenersMap.forEach((method, type) => {
// Since `type` is generic, the compiler doesn't know what type it is and `isGlobMatch` requires `string | symbol`
if (isGlobMatch(type as any, event.type)) {
hasMatch = true;
}
});
if (!hasMatch) {
let queue = this._queue.get(event.type);
if (!queue) {
queue = [];
this._queue.set(event.type, queue);
}
queue.push(event);
if (this.maxEvents > 0) {
while (queue.length > this.maxEvents) {
queue.shift();
}
}
}
}
on<K extends keyof M>(type: K, listener: EventedCallbackOrArray<K, M[K]>): Handle;
on(type: T, listener: EventedCallbackOrArray<T, O>): Handle;
on(type: any, listener: EventedCallbackOrArray<any, any>): Handle {
let handle = super.on(type, listener);
this.listenersMap.forEach((method, listenerType) => {
this._queue.forEach((events, queuedType) => {
if (isGlobMatch(listenerType as any, queuedType)) {
events.forEach((event) => this.emit(event));
this._queue.delete(queuedType);
}
});
});
return handle;
}
}
export default QueuingEvented;

View File

@ -0,0 +1,115 @@
import { Handle } from './interfaces';
import { QueueItem, queueTask } from './queue';
function getQueueHandle(item: QueueItem): Handle {
return {
destroy: function(this: Handle) {
this.destroy = function() {};
item.isActive = false;
item.callback = null;
}
};
}
export interface KwArgs {
deferWhileProcessing?: boolean;
queueFunction?: (callback: (...args: any[]) => any) => Handle;
}
export class Scheduler {
protected readonly _boundDispatch: () => void;
protected _deferred: QueueItem[] | null = null;
protected _isProcessing: boolean;
protected readonly _queue: QueueItem[];
protected _task: Handle | null = null;
/**
* Determines whether any callbacks registered during should be added to the current batch (`false`)
* or deferred until the next batch (`true`, default).
*/
deferWhileProcessing: boolean | undefined;
/**
* Allows users to specify the function that should be used to schedule callbacks.
* If no function is provided, then `queueTask` will be used.
*/
queueFunction: (callback: (...args: any[]) => any) => Handle;
protected _defer(callback: (...args: any[]) => void): Handle {
const item: QueueItem = {
isActive: true,
callback: callback
};
if (!this._deferred) {
this._deferred = [];
}
this._deferred.push(item);
return getQueueHandle(item);
}
protected _dispatch(): void {
this._isProcessing = true;
if (this._task) {
this._task.destroy();
this._task = null;
}
const queue = this._queue;
let item: QueueItem | undefined;
while ((item = queue.shift())) {
if (item.isActive && item.callback) {
item.callback();
}
}
this._isProcessing = false;
let deferred: QueueItem[] | null = this._deferred;
if (deferred && deferred.length) {
this._deferred = null;
let item: QueueItem | undefined;
while ((item = deferred.shift())) {
this._schedule(item);
}
}
}
protected _schedule(item: QueueItem): void {
if (!this._task) {
this._task = this.queueFunction(this._boundDispatch);
}
this._queue.push(item);
}
constructor(kwArgs?: KwArgs) {
this.deferWhileProcessing = kwArgs && 'deferWhileProcessing' in kwArgs ? kwArgs.deferWhileProcessing : true;
this.queueFunction = kwArgs && kwArgs.queueFunction ? kwArgs.queueFunction : queueTask;
this._boundDispatch = this._dispatch.bind(this);
this._isProcessing = false;
this._queue = [];
}
schedule(callback: (...args: any[]) => void): Handle {
if (this._isProcessing && this.deferWhileProcessing) {
return this._defer(callback);
}
const item: QueueItem = {
isActive: true,
callback: callback
};
this._schedule(item);
return getQueueHandle(item);
}
}
export default Scheduler;

View File

@ -0,0 +1,193 @@
import { Hash } from './interfaces';
import { duplicate } from './lang';
/**
* Object with string keys and string or string array values that describes a query string.
*/
export type ParamList = Hash<string | string[]>;
/**
* Parses a query string, returning a ParamList object.
*/
function parseQueryString(input: string): ParamList {
const query: Hash<string[]> = {};
const splits = input.split('&');
for (let i = 0; i < splits.length; i++) {
const entry = splits[i];
const indexOfFirstEquals = entry.indexOf('=');
let key: string;
let value = '';
if (indexOfFirstEquals >= 0) {
key = entry.slice(0, indexOfFirstEquals);
value = entry.slice(indexOfFirstEquals + 1);
} else {
key = entry;
}
key = key ? decodeURIComponent(key) : '';
value = value ? decodeURIComponent(value) : '';
if (key in query) {
query[key].push(value);
} else {
query[key] = [value];
}
}
return query;
}
/**
* Represents a set of URL query search parameters.
*/
export class UrlSearchParams {
/**
* Constructs a new UrlSearchParams from a query string, an object of parameters and values, or another
* UrlSearchParams.
*/
constructor(input?: string | ParamList | UrlSearchParams) {
let list: ParamList = {};
if (input instanceof UrlSearchParams) {
// Copy the incoming UrlSearchParam's internal list
list = <ParamList>duplicate(input._list);
} else if (typeof input === 'object') {
// Copy the incoming object, assuming its property values are either arrays or strings
list = {};
for (const key in input) {
const value = (<ParamList>input)[key];
if (Array.isArray(value)) {
list[key] = value.length ? value.slice() : [''];
} else if (value == null) {
list[key] = [''];
} else {
list[key] = [<string>value];
}
}
} else if (typeof input === 'string') {
// Parse the incoming string as a query string
list = parseQueryString(input);
}
this._list = list as Hash<string[] | undefined>;
}
/**
* Maps property keys to arrays of values. The value for any property that has been set will be an array containing
* at least one item. Properties that have been deleted will have a value of 'undefined'.
*/
protected readonly _list: Hash<string[] | undefined>;
/**
* Appends a new value to the set of values for a key.
* @param key The key to add a value for
* @param value The value to add
*/
append(key: string, value: string): void {
if (!this.has(key)) {
this.set(key, value);
} else {
const values = this._list[key];
if (values) {
values.push(value);
}
}
}
/**
* Deletes all values for a key.
* @param key The key whose values are to be removed
*/
delete(key: string): void {
// Set to undefined rather than deleting the key, for better consistency across browsers.
// If a deleted key is re-added, most browsers put it at the end of iteration order, but IE maintains
// its original position. This approach maintains the original position everywhere.
this._list[key] = undefined;
}
/**
* Returns the first value associated with a key.
* @param key The key to return the first value for
* @return The first string value for the key
*/
get(key: string): string | undefined {
if (!this.has(key)) {
return undefined;
}
const value = this._list[key];
return value ? value[0] : undefined;
}
/**
* Returns all the values associated with a key.
* @param key The key to return all values for
* @return An array of strings containing all values for the key
*/
getAll(key: string): string[] | undefined {
if (!this.has(key)) {
return undefined;
}
return this._list[key];
}
/**
* Returns true if a key has been set to any value, false otherwise.
* @param key The key to test for existence
* @return A boolean indicating if the key has been set
*/
has(key: string): boolean {
return Array.isArray(this._list[key]);
}
/**
* Returns an array of all keys which have been set.
* @return An array of strings containing all keys set in the UrlSearchParams instance
*/
keys(): string[] {
const keys: string[] = [];
for (const key in this._list) {
if (this.has(key)) {
keys.push(key);
}
}
return keys;
}
/**
* Sets the value associated with a key.
* @param key The key to set the value of
*/
set(key: string, value: string): void {
this._list[key] = [value];
}
/**
* Returns this object's data as an encoded query string.
* @return A string in application/x-www-form-urlencoded format containing all of the set keys/values
*/
toString(): string {
const query: string[] = [];
for (const key in this._list) {
if (!this.has(key)) {
continue;
}
const values = this._list[key];
if (values) {
const encodedKey = encodeURIComponent(key);
for (let i = 0; i < values.length; i++) {
query.push(encodedKey + (values[i] ? '=' + encodeURIComponent(values[i]) : ''));
}
}
}
return query.join('&');
}
}
export default UrlSearchParams;

531
src/lib/core/src/aspect.ts Normal file
View File

@ -0,0 +1,531 @@
import { Handle } from './interfaces';
import WeakMap from '@dojo/shim/WeakMap';
import { createHandle } from './lang';
/**
* An object that provides the necessary APIs to be MapLike
*/
export interface MapLike<K, V> {
get(key: K): V;
set(key: K, value?: V): this;
}
/**
* An internal type guard that determines if an value is MapLike or not
*
* @param value The value to guard against
*/
function isMapLike(value: any): value is MapLike<any, any> {
return value && typeof value.get === 'function' && typeof value.set === 'function';
}
export interface Indexable {
[method: string]: any;
}
/**
* The types of objects or maps where advice can be applied
*/
export type Targetable = MapLike<string | symbol, any> | Indexable;
type AdviceType = 'before' | 'after' | 'around';
/**
* A meta data structure when applying advice
*/
interface Advised {
readonly id?: number;
advice?: Function;
previous?: Advised;
next?: Advised;
readonly receiveArguments?: boolean;
}
/**
* A function that dispatches advice which is decorated with additional
* meta data about the advice to apply
*/
interface Dispatcher {
[type: string]: Advised | undefined;
(): any;
target: any;
before?: Advised;
around?: Advised;
after?: Advised;
}
export interface JoinPointDispatchAdvice<T> {
before?: JoinPointBeforeAdvice[];
after?: JoinPointAfterAdvice<T>[];
readonly joinPoint: Function;
}
export interface JoinPointAfterAdvice<T> {
/**
* Advice which is applied *after*, receiving the result and arguments from the join point.
*
* @param result The result from the function being advised
* @param args The arguments that were supplied to the advised function
* @returns The value returned from the advice is then the result of calling the method
*/
(result: T, ...args: any[]): T;
}
export interface JoinPointAroundAdvice<T> {
/**
* Advice which is applied *around*. The advising function receives the original function and
* needs to return a new function which will then invoke the original function.
*
* @param origFn The original function
* @returns A new function which will invoke the original function.
*/
(origFn: GenericFunction<T>): (...args: any[]) => T;
}
export interface JoinPointBeforeAdvice {
/**
* Advice which is applied *before*, receiving the original arguments, if the advising
* function returns a value, it is passed further along taking the place of the original
* arguments.
*
* @param args The arguments the method was called with
*/
(...args: any[]): any[] | void;
}
export interface GenericFunction<T> {
(...args: any[]): T;
}
/**
* A weak map of dispatchers used to apply the advice
*/
const dispatchAdviceMap = new WeakMap<Function, JoinPointDispatchAdvice<any>>();
/**
* A UID for tracking advice ordering
*/
let nextId = 0;
/**
* Internal function that advises a join point
*
* @param dispatcher The current advice dispatcher
* @param type The type of before or after advice to apply
* @param advice The advice to apply
* @param receiveArguments If true, the advice will receive the arguments passed to the join point
* @return The handle that will remove the advice
*/
function adviseObject(
dispatcher: Dispatcher | undefined,
type: AdviceType,
advice: Function | undefined,
receiveArguments?: boolean
): Handle {
let previous = dispatcher && dispatcher[type];
let advised: Advised | undefined = {
id: nextId++,
advice: advice,
receiveArguments: receiveArguments
};
if (previous) {
if (type === 'after') {
// add the listener to the end of the list
// note that we had to change this loop a little bit to workaround a bizarre IE10 JIT bug
while (previous.next && (previous = previous.next)) {}
previous.next = advised;
advised.previous = previous;
} else {
// add to the beginning
if (dispatcher) {
dispatcher.before = advised;
}
advised.next = previous;
previous.previous = advised;
}
} else {
dispatcher && (dispatcher[type] = advised);
}
advice = previous = undefined;
return createHandle(function() {
let { previous = undefined, next = undefined } = advised || {};
if (dispatcher && !previous && !next) {
dispatcher[type] = undefined;
} else {
if (previous) {
previous.next = next;
} else {
dispatcher && (dispatcher[type] = next);
}
if (next) {
next.previous = previous;
}
}
if (advised) {
delete advised.advice;
}
dispatcher = advised = undefined;
});
}
/**
* Advise a join point (function) with supplied advice
*
* @param joinPoint The function to be advised
* @param type The type of advice to be applied
* @param advice The advice to apply
*/
function adviseJoinPoint<F extends GenericFunction<T>, T>(
this: any,
joinPoint: F,
type: AdviceType,
advice: JoinPointBeforeAdvice | JoinPointAfterAdvice<T> | JoinPointAroundAdvice<T>
): F {
let dispatcher: F;
if (type === 'around') {
dispatcher = getJoinPointDispatcher(advice.apply(this, [joinPoint]));
} else {
dispatcher = getJoinPointDispatcher(joinPoint);
// cannot have undefined in map due to code logic, using !
const adviceMap = dispatchAdviceMap.get(dispatcher)!;
if (type === 'before') {
(adviceMap.before || (adviceMap.before = [])).unshift(<JoinPointBeforeAdvice>advice);
} else {
(adviceMap.after || (adviceMap.after = [])).push(advice);
}
}
return dispatcher;
}
/**
* An internal function that resolves or creates the dispatcher for a given join point
*
* @param target The target object or map
* @param methodName The name of the method that the dispatcher should be resolved for
* @return The dispatcher
*/
function getDispatcherObject(target: Targetable, methodName: string | symbol): Dispatcher {
const existing = isMapLike(target) ? target.get(methodName) : target && target[methodName];
let dispatcher: Dispatcher;
if (!existing || existing.target !== target) {
/* There is no existing dispatcher, therefore we will create one */
dispatcher = <Dispatcher>function(this: Dispatcher): any {
let executionId = nextId;
let args = arguments;
let results: any;
let before = dispatcher.before;
while (before) {
if (before.advice) {
args = before.advice.apply(this, args) || args;
}
before = before.next;
}
if (dispatcher.around && dispatcher.around.advice) {
results = dispatcher.around.advice(this, args);
}
let after = dispatcher.after;
while (after && after.id !== undefined && after.id < executionId) {
if (after.advice) {
if (after.receiveArguments) {
let newResults = after.advice.apply(this, args);
results = newResults === undefined ? results : newResults;
} else {
results = after.advice.call(this, results, args);
}
}
after = after.next;
}
return results;
};
if (isMapLike(target)) {
target.set(methodName, dispatcher);
} else {
target && (target[methodName] = dispatcher);
}
if (existing) {
dispatcher.around = {
advice: function(target: any, args: any[]): any {
return existing.apply(target, args);
}
};
}
dispatcher.target = target;
} else {
dispatcher = existing;
}
return dispatcher;
}
/**
* Returns the dispatcher function for a given joinPoint (method/function)
*
* @param joinPoint The function that is to be advised
*/
function getJoinPointDispatcher<F extends GenericFunction<T>, T>(joinPoint: F): F {
function dispatcher(this: Function, ...args: any[]): T {
// cannot have undefined in map due to code logic, using !
const { before, after, joinPoint } = dispatchAdviceMap.get(dispatcher)!;
if (before) {
args = before.reduce((previousArgs, advice) => {
const currentArgs = advice.apply(this, previousArgs);
return currentArgs || previousArgs;
}, args);
}
let result = joinPoint.apply(this, args);
if (after) {
result = after.reduce((previousResult, advice) => {
return advice.apply(this, [previousResult].concat(args));
}, result);
}
return result;
}
/* We want to "clone" the advice that has been applied already, if this
* joinPoint is already advised */
if (dispatchAdviceMap.has(joinPoint)) {
// cannot have undefined in map due to code logic, using !
const adviceMap = dispatchAdviceMap.get(joinPoint)!;
let { before, after } = adviceMap;
if (before) {
before = before.slice(0);
}
if (after) {
after = after.slice(0);
}
dispatchAdviceMap.set(dispatcher, {
joinPoint: adviceMap.joinPoint,
before,
after
});
} else {
/* Otherwise, this is a new joinPoint, so we will create the advice map afresh */
dispatchAdviceMap.set(dispatcher, { joinPoint });
}
return dispatcher as F;
}
/**
* Apply advice *after* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The after advice
*/
function afterJoinPoint<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAfterAdvice<T>): F {
return adviseJoinPoint(joinPoint, 'after', advice);
}
/**
* Attaches "after" advice to be executed after the original method.
* The advising function will receive the original method's return value and arguments object.
* The value it returns will be returned from the method when it is called (even if the return value is undefined).
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original method's return value and arguments object
* @return A handle which will remove the aspect when destroy is called
*/
function afterObject(
target: Targetable,
methodName: string | symbol,
advice: (originalReturn: any, originalArgs: IArguments) => any
): Handle {
return adviseObject(getDispatcherObject(target, methodName), 'after', advice);
}
/**
* Attaches "after" advice to be executed after the original method.
* The advising function will receive the original method's return value and arguments object.
* The value it returns will be returned from the method when it is called (even if the return value is undefined).
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original method's return value and arguments object
* @return A handle which will remove the aspect when destroy is called
*/
export function after(
target: Targetable,
methodName: string | symbol,
advice: (originalReturn: any, originalArgs: IArguments) => any
): Handle;
/**
* Apply advice *after* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The after advice
*/
export function after<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAfterAdvice<T>): F;
export function after<F extends GenericFunction<T>, T>(
joinPointOrTarget: F | Targetable,
methodNameOrAdvice: string | symbol | JoinPointAfterAdvice<T>,
objectAdvice?: (originalReturn: any, originalArgs: IArguments) => any
): Handle | F {
if (typeof joinPointOrTarget === 'function') {
return afterJoinPoint(joinPointOrTarget, <JoinPointAfterAdvice<T>>methodNameOrAdvice);
} else {
return afterObject(joinPointOrTarget, <string | symbol>methodNameOrAdvice, objectAdvice!);
}
}
/**
* Apply advice *around* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The around advice
*/
export function aroundJoinPoint<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAroundAdvice<T>): F {
return adviseJoinPoint<F, T>(joinPoint, 'around', advice);
}
/**
* Attaches "around" advice around the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original function
* @return A handle which will remove the aspect when destroy is called
*/
export function aroundObject(
target: Targetable,
methodName: string | symbol,
advice: ((previous: Function) => Function)
): Handle {
let dispatcher: Dispatcher | undefined = getDispatcherObject(target, methodName);
let previous = dispatcher.around;
let advised: Function | undefined;
if (advice) {
advised = advice(function(this: Dispatcher): any {
if (previous && previous.advice) {
return previous.advice(this, arguments);
}
});
}
dispatcher.around = {
advice: function(target: any, args: any[]): any {
return advised ? advised.apply(target, args) : previous && previous.advice && previous.advice(target, args);
}
};
return createHandle(function() {
advised = dispatcher = undefined;
});
}
/**
* Attaches "around" advice around the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the original function
* @return A handle which will remove the aspect when destroy is called
*/
export function around(
target: Targetable,
methodName: string | symbol,
advice: ((previous: Function) => Function)
): Handle;
/**
* Apply advice *around* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The around advice
*/
export function around<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAroundAdvice<T>): F;
export function around<F extends GenericFunction<T>, T>(
joinPointOrTarget: F | Targetable,
methodNameOrAdvice: string | symbol | JoinPointAroundAdvice<T>,
objectAdvice?: ((previous: Function) => Function)
): Handle | F {
if (typeof joinPointOrTarget === 'function') {
return aroundJoinPoint(joinPointOrTarget, <JoinPointAroundAdvice<T>>methodNameOrAdvice);
} else {
return aroundObject(joinPointOrTarget, <string | symbol>methodNameOrAdvice, objectAdvice!);
}
}
/**
* Apply advice *before* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The before advice
*/
export function beforeJoinPoint<F extends GenericFunction<any>>(joinPoint: F, advice: JoinPointBeforeAdvice): F {
return adviseJoinPoint(joinPoint, 'before', advice);
}
/**
* Attaches "before" advice to be executed before the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the same arguments as the original, and may return new arguments
* @return A handle which will remove the aspect when destroy is called
*/
export function beforeObject(
target: Targetable,
methodName: string | symbol,
advice: (...originalArgs: any[]) => any[] | void
): Handle {
return adviseObject(getDispatcherObject(target, methodName), 'before', advice);
}
/**
* Attaches "before" advice to be executed before the original method.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the same arguments as the original, and may return new arguments
* @return A handle which will remove the aspect when destroy is called
*/
export function before(
target: Targetable,
methodName: string | symbol,
advice: (...originalArgs: any[]) => any[] | void
): Handle;
/**
* Apply advice *before* the supplied joinPoint (function)
*
* @param joinPoint A function that should have advice applied to
* @param advice The before advice
*/
export function before<F extends GenericFunction<any>>(joinPoint: F, advice: JoinPointBeforeAdvice): F;
export function before<F extends GenericFunction<T>, T>(
joinPointOrTarget: F | Targetable,
methodNameOrAdvice: string | symbol | JoinPointBeforeAdvice,
objectAdvice?: ((...originalArgs: any[]) => any[] | void)
): Handle | F {
if (typeof joinPointOrTarget === 'function') {
return beforeJoinPoint(joinPointOrTarget, <JoinPointBeforeAdvice>methodNameOrAdvice);
} else {
return beforeObject(joinPointOrTarget, <string | symbol>methodNameOrAdvice, objectAdvice!);
}
}
/**
* Attaches advice to be executed after the original method.
* The advising function will receive the same arguments as the original method.
* The value it returns will be returned from the method when it is called *unless* its return value is undefined.
*
* @param target Object whose method will be aspected
* @param methodName Name of method to aspect
* @param advice Advising function which will receive the same arguments as the original method
* @return A handle which will remove the aspect when destroy is called
*/
export function on(target: Targetable, methodName: string | symbol, advice: (...originalArgs: any[]) => any): Handle {
return adviseObject(getDispatcherObject(target, methodName), 'after', advice, true);
}

View File

@ -0,0 +1,249 @@
import { Thenable } from '@dojo/shim/interfaces';
import { isArrayLike, isIterable, Iterable } from '@dojo/shim/iterator';
import Promise, { Executor } from '@dojo/shim/Promise';
import '@dojo/shim/Symbol';
/**
* Take a list of values, and if any are ExtensiblePromise objects, insert the wrapped Promise in its place,
* otherwise use the original object. We use this to help use the native Promise methods like `all` and `race`.
*
* @param iterable The list of objects to iterate over
* @returns {any[]} The list of objects, as an array, with ExtensiblePromises being replaced by Promises.
*/
export function unwrapPromises(iterable: Iterable<any> | any[]): any[] {
const unwrapped: any[] = [];
if (isArrayLike(iterable)) {
for (let i = 0; i < iterable.length; i++) {
const item = iterable[i];
unwrapped.push(item instanceof ExtensiblePromise ? item._promise : item);
}
} else {
for (const item of iterable) {
unwrapped.push(item instanceof ExtensiblePromise ? item._promise : item);
}
}
return unwrapped;
}
export type DictionaryOfPromises<T> = { [_: string]: T | Promise<T> | Thenable<T> };
export type ListOfPromises<T> = Iterable<T | Thenable<T>>;
/**
* An extensible base to allow Promises to be extended in ES5. This class basically wraps a native Promise object,
* giving an API like a native promise.
*/
export class ExtensiblePromise<T> {
/**
* Return a rejected promise wrapped in an ExtensiblePromise
*
* @param reason The reason for the rejection
* @returns An extensible promise
*/
static reject<T>(reason?: any): ExtensiblePromise<never>;
/**
* Return a rejected promise wrapped in an ExtensiblePromise
*
* @param reason The reason for the rejection
* @returns An extensible promise
*/
static reject<T>(reason?: any): ExtensiblePromise<T> {
return new this((resolve, reject) => reject(reason));
}
/**
* Return a resolved promise wrapped in an ExtensiblePromise
*
* @param value The value to resolve the promise with
*
* @returns An extensible promise
*/
static resolve<P extends ExtensiblePromise<void>>(): P;
/**
* Return a resolved promise wrapped in an ExtensiblePromise
*
* @param value The value to resolve the promise with
*
* @returns An extensible promise
*/
static resolve<T, P extends ExtensiblePromise<T>>(value: T | PromiseLike<T>): P;
static resolve(value?: any | PromiseLike<any>): ExtensiblePromise<any> {
return new this((resolve, reject) => resolve(value));
}
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: DictionaryOfPromises<T>): ExtensiblePromise<{ [key: string]: T }>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: (T | Thenable<T>)[]): ExtensiblePromise<T[]>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: T | Thenable<T>): ExtensiblePromise<T[]>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: ListOfPromises<T>): ExtensiblePromise<T[]>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(
iterable: DictionaryOfPromises<T> | ListOfPromises<T>
): ExtensiblePromise<T[] | { [key: string]: T }> {
if (!isArrayLike(iterable) && !isIterable(iterable)) {
const promiseKeys = Object.keys(iterable);
return new this((resolve, reject) => {
Promise.all(promiseKeys.map((key) => iterable[key])).then((promiseResults: T[]) => {
const returnValue: { [key: string]: T } = {};
promiseResults.forEach((value: T, index: number) => {
returnValue[promiseKeys[index]] = value;
});
resolve(returnValue);
}, reject);
});
}
return new this((resolve, reject) => {
Promise.all(unwrapPromises(<Iterable<T>>iterable)).then(resolve, reject);
});
}
/**
* Return a ExtensiblePromise that resolves when one of the passed in objects have resolved
*
* @param iterable An iterable of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns {ExtensiblePromise}
*/
static race<T>(iterable: Iterable<T | PromiseLike<T>> | (T | PromiseLike<T>)[]): ExtensiblePromise<T> {
return new this((resolve, reject) => {
Promise.race(unwrapPromises(iterable)).then(resolve, reject);
});
}
/**
* @type {Promise}
* The wrapped promise
*/
readonly _promise: Promise<T>;
/**
* Creates a new extended Promise.
*
* @constructor
*
* @param executor
* The executor function is called immediately when the Promise is instantiated. It is responsible for
* starting the asynchronous operation when it is invoked.
*
* The executor must call either the passed `resolve` function when the asynchronous operation has completed
* successfully, or the `reject` function when the operation fails.
*/
constructor(executor: Executor<T>) {
this._promise = new Promise<T>(executor);
}
/**
* Adds a callback to be invoked when the wrapped Promise is rejected.
*
* @param {Function} onRejected A function to call to handle the error. The parameter to the function will be the caught error.
*
* @returns {ExtensiblePromise}
*/
catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): ExtensiblePromise<T | TResult> {
return this.then(undefined, onRejected);
}
/**
* Adds a callback to be invoked when the wrapped Promise resolves or is rejected.
*
* @param {Function} onFulfilled A function to call to handle the resolution. The paramter to the function will be the resolved value, if any.
* @param {Function} onRejected A function to call to handle the error. The parameter to the function will be the caught error.
*
* @returns {ExtensiblePromise}
*/
then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2> | void) | undefined | null
): ExtensiblePromise<TResult1 | TResult2> {
const executor: Executor<TResult1> = (resolve, reject) => {
function handler(rejected: boolean, valueOrError: T | TResult1 | Error) {
const callback: ((value: any) => any) | null | undefined = rejected ? onRejected : onFulfilled;
if (typeof callback === 'function') {
try {
resolve(callback(valueOrError));
} catch (error) {
reject(error);
}
} else if (rejected) {
reject(valueOrError);
} else {
resolve(valueOrError as TResult1);
}
}
this._promise.then(handler.bind(null, false), handler.bind(null, true));
};
return new (this.constructor as typeof ExtensiblePromise)(executor);
}
readonly [Symbol.toStringTag]: 'Promise';
}
export default ExtensiblePromise;

View File

@ -0,0 +1,384 @@
import { Thenable } from '@dojo/shim/interfaces';
import { isArrayLike, isIterable, Iterable } from '@dojo/shim/iterator';
import { Executor } from '@dojo/shim/Promise';
import ExtensiblePromise, { DictionaryOfPromises, ListOfPromises, unwrapPromises } from './ExtensiblePromise';
/**
* Describe the internal state of a task.
*/
export const enum State {
Fulfilled = 0,
Pending = 1,
Rejected = 2,
Canceled = 3
}
/**
* A type guard that determines if `value` is a `Task`
* @param value The value to guard
*/
export function isTask<T>(value: any): value is Task<T> {
return Boolean(value && typeof value.cancel === 'function' && Array.isArray(value.children) && isThenable(value));
}
/**
* Returns true if a given value has a `then` method.
* @param {any} value The value to check if is Thenable
* @returns {is Thenable<T>} A type guard if the value is thenable
*/
export function isThenable<T>(value: any): value is Thenable<T> {
return value && typeof value.then === 'function';
}
/**
* Task is an extension of Promise that supports cancellation and the Task#finally method.
*/
export class Task<T> extends ExtensiblePromise<T> {
/**
* Return a Task that resolves when one of the passed in objects have resolved
*
* @param iterable An iterable of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns {Task}
*/
static race<T>(iterable: Iterable<T | Thenable<T>> | (T | Thenable<T>)[]): Task<T> {
return new this((resolve, reject) => {
Promise.race(unwrapPromises(iterable)).then(resolve, reject);
});
}
/**
* Return a rejected promise wrapped in a Task
*
* @param reason The reason for the rejection
* @returns A task
*/
static reject<T>(reason?: Error): Task<T> {
return new this((resolve, reject) => reject(reason));
}
/**
* Return a resolved task.
*
* @param value The value to resolve with
*
* @return A task
*/
public static resolve(): Task<void>;
/**
* Return a resolved task.
*
* @param value The value to resolve with
*
* @return A task
*/
public static resolve<T>(value: T | Thenable<T>): Task<T>;
/**
* Return a resolved task.
*
* @param value The value to resolve with
*
* @return A task
*/
public static resolve<T>(value?: any): Task<T> {
return new this<T>((resolve, reject) => resolve(value));
}
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: DictionaryOfPromises<T>): Task<{ [key: string]: T }>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: (T | Thenable<T>)[]): Task<T[]>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: T | Thenable<T>): Task<T[]>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: ListOfPromises<T>): Task<T[]>;
/**
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
*
* @example
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
* // { one: 1, two: 2 }
*
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
* @returns An extensible promise
*/
static all<T>(iterable: DictionaryOfPromises<T> | ListOfPromises<T>): Task<any> {
return new Task(
(resolve, reject) => {
super.all(iterable).then(resolve, reject);
},
() => {
if (isArrayLike(iterable)) {
for (let i = 0; i < iterable.length; i++) {
const promiseLike = iterable[i];
if (isTask(promiseLike)) {
promiseLike.cancel();
}
}
} else if (isIterable(iterable)) {
for (const promiseLike of iterable) {
if (isTask(promiseLike)) {
promiseLike.cancel();
}
}
} else {
Object.keys(iterable).forEach((key: any) => {
const promiseLike = iterable[key];
if (isTask(promiseLike)) {
promiseLike.cancel();
}
});
}
}
);
}
/**
* A cancelation handler that will be called if this task is canceled.
*/
private canceler: () => void;
/**
* Children of this Task (i.e., Tasks that were created from this Task with `then` or `catch`).
*/
private readonly children: Task<any>[];
/**
* The finally callback for this Task (if it was created by a call to `finally`).
*/
private _finally: undefined | (() => void);
/**
* The state of the task
*/
protected _state: State;
get state() {
return this._state;
}
/**
* @constructor
*
* Create a new task. Executor is run immediately. The canceler will be called when the task is canceled.
*
* @param executor Method that initiates some task
* @param canceler Method to call when the task is canceled
*
*/
constructor(executor: Executor<T>, canceler?: () => void) {
// we have to initialize these to avoid a compiler error of using them before they are initialized
let superResolve: (value?: T | Thenable<T> | undefined) => void = () => {};
let superReject: (reason?: any) => void = () => {};
super((resolve, reject) => {
superResolve = resolve;
superReject = reject;
});
this._state = State.Pending;
this.children = [];
this.canceler = () => {
if (canceler) {
canceler();
}
this._cancel();
};
// Don't let the Task resolve if it's been canceled
try {
executor(
(value) => {
if (this._state === State.Canceled) {
return;
}
this._state = State.Fulfilled;
superResolve(value);
},
(reason) => {
if (this._state === State.Canceled) {
return;
}
this._state = State.Rejected;
superReject(reason);
}
);
} catch (reason) {
this._state = State.Rejected;
superReject(reason);
}
}
/**
* Propagates cancellation down through a Task tree. The Task's state is immediately set to canceled. If a Thenable
* finally task was passed in, it is resolved before calling this Task's finally callback; otherwise, this Task's
* finally callback is immediately executed. `_cancel` is called for each child Task, passing in the value returned
* by this Task's finally callback or a Promise chain that will eventually resolve to that value.
*/
private _cancel(finallyTask?: void | Thenable<any>): void {
this._state = State.Canceled;
const runFinally = () => {
try {
return this._finally && this._finally();
} catch (error) {
// Any errors in a `finally` callback are completely ignored during cancelation
}
};
if (this._finally) {
if (isThenable(finallyTask)) {
finallyTask = (<Thenable<any>>finallyTask).then(runFinally, runFinally);
} else {
finallyTask = runFinally();
}
}
this.children.forEach(function(child) {
child._cancel(finallyTask);
});
}
/**
* Immediately cancels this task if it has not already resolved. This Task and any descendants are synchronously set
* to the Canceled state and any `finally` added downstream from the canceled Task are invoked.
*/
cancel(): void {
if (this._state === State.Pending) {
this.canceler();
}
}
catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined
): Task<T | TResult> {
return this.then(undefined, onRejected) as Task<T | TResult>;
}
/**
* Allows for cleanup actions to be performed after resolution of a Promise.
*/
finally(callback: () => void): Task<T> {
// if this task is already canceled, call the task
if (this._state === State.Canceled) {
callback();
return this;
}
const task = this.then<any>(
(value) => Task.resolve(callback()).then(() => value),
(reason) =>
Task.resolve(callback()).then(() => {
throw reason;
})
);
// Keep a reference to the callback; it will be called if the Task is canceled
task._finally = callback;
return task;
}
/**
* Adds a callback to be invoked when the Task resolves or is rejected.
*
* @param onFulfilled A function to call to handle the resolution. The paramter to the function will be the resolved value, if any.
* @param onRejected A function to call to handle the error. The parameter to the function will be the caught error.
*
* @returns A task
*/
then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Task<TResult1 | TResult2> {
// FIXME
// tslint:disable-next-line:no-var-keyword
var task = super.then(
// Don't call the onFulfilled or onRejected handlers if this Task is canceled
function(value) {
if (task._state === State.Canceled) {
return;
}
if (onFulfilled) {
return onFulfilled(value);
}
return <any>value;
},
function(error) {
if (task._state === State.Canceled) {
return;
}
if (onRejected) {
return onRejected(error);
}
throw error;
}
) as Task<TResult1 | TResult2>;
task.canceler = () => {
// If task's parent (this) hasn't been resolved, cancel it; downward propagation will start at the first
// unresolved parent
if (this._state === State.Pending) {
this.cancel();
} else {
// If task's parent has been resolved, propagate cancelation to the task's descendants
task._cancel();
}
};
// Keep track of child Tasks for propogating cancelation back down the chain
this.children.push(task);
return task;
}
}
export default Task;

View File

@ -0,0 +1,295 @@
import * as array from '@dojo/shim/array';
import { isArrayLike, Iterable } from '@dojo/shim/iterator';
import Promise from '@dojo/shim/Promise';
import { Thenable } from '@dojo/shim/interfaces';
function isThenable<T>(value: any): value is Thenable<T> {
return value && typeof value.then === 'function';
}
type ValuesAndResults<T, U> = { values: T[] | undefined; results: U[] | undefined };
/**
* Processes all items and then applies the callback to each item and eventually returns an object containing the
* processed values and callback results
* @param items a list of synchronous/asynchronous values to process
* @param callback a callback that maps values to synchronous/asynchronous results
* @return a list of objects holding the synchronous values and synchronous results.
*/
function processValuesAndCallback<T, U>(
items: Iterable<T | Promise<T>> | (T | Thenable<T>)[],
callback: Mapper<T, U>
): Promise<ValuesAndResults<T, U>> {
return Promise.all(items).then(function(results) {
const pass: (U | Promise<U>)[] = Array.prototype.map.call(results, callback);
return Promise.all(pass).then(function(pass) {
return { values: results, results: pass };
});
});
}
/**
* Finds the index of the next value in a sparse array-like object
* @param list the sparse array-like object
* @param offset the starting offset
* @return the offset of the next index with a value; or -1 if not found
*/
function findNextValueIndex<T>(list: ArrayLike<T>, offset: number = -1): number {
offset++;
for (let length = list.length; offset < length; offset++) {
if (offset in list) {
return offset;
}
}
return -1;
}
function findLastValueIndex(list: ArrayLike<any>, offset?: number): number {
offset = (offset === undefined ? list.length : offset) - 1;
for (; offset >= 0; offset--) {
if (offset in list) {
return offset;
}
}
return -1;
}
function generalReduce<T, U>(
findNextIndex: (list: ArrayLike<any>, offset?: number) => number,
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Reducer<T, U>,
initialValue?: U
): Promise<U> {
const hasInitialValue = arguments.length > 3;
return Promise.all(items).then(function(results) {
return new Promise<U>(function(resolve, reject) {
// As iterators do not have indices like `ArrayLike` objects, the results array
// is used to determine the next value.
const list = isArrayLike(items) ? items : results;
let i: number;
function next(currentValue: U | undefined): void {
i = findNextIndex(list, i);
if (i >= 0) {
if (results) {
if (currentValue) {
const result = callback(currentValue, results[i], i, results);
if (isThenable(result)) {
result.then(next, reject);
} else {
next(result);
}
}
}
} else {
resolve(currentValue);
}
}
let value: U | undefined;
if (hasInitialValue) {
value = initialValue;
} else {
i = findNextIndex(list);
if (i < 0) {
throw new Error('reduce array with no initial value');
}
if (results) {
value = <any>results[i];
}
}
next(value);
});
});
}
function testAndHaltOnCondition<T>(
condition: boolean,
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Filterer<T>
): Promise<boolean> {
return Promise.all(items).then(function(results) {
return new Promise<boolean>(function(resolve) {
let result: boolean | Thenable<boolean>;
let pendingCount = 0;
if (results) {
for (let i = 0; i < results.length; i++) {
result = callback(results[i], i, results);
if (result === condition) {
return resolve(result);
} else if (isThenable(result)) {
pendingCount++;
result.then(function(result) {
if (result === condition) {
resolve(result);
}
pendingCount--;
if (pendingCount === 0) {
resolve(!condition);
}
});
}
}
}
if (pendingCount === 0) {
resolve(!condition);
}
});
});
}
/**
* Test whether all elements in the array pass the provided callback
* @param items a collection of synchronous/asynchronous values
* @param callback a synchronous/asynchronous test
* @return eventually returns true if all values pass; otherwise false
*/
export function every<T>(
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Filterer<T>
): Promise<boolean> {
return testAndHaltOnCondition(false, items, callback);
}
/**
* Returns an array of elements which pass the provided callback
* @param items a collection of synchronous/asynchronous values
* @param callback a synchronous/asynchronous test
* @return eventually returns a new array with only values that have passed
*/
export function filter<T>(items: Iterable<T | Promise<T>> | (T | Promise<T>)[], callback: Filterer<T>): Promise<T[]> {
return processValuesAndCallback(items, callback).then(function(result) {
let arr: T[] = [];
if (result && result.results && result.values) {
for (let i = 0; i < result.results.length; i++) {
result.results[i] && arr.push(result.values[i]);
}
}
return arr;
});
}
/**
* Find the first value matching a filter function
* @param items a collection of synchronous/asynchronous values
* @param callback a synchronous/asynchronous test
* @return a promise eventually containing the item or undefined if a match is not found
*/
export function find<T>(
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Filterer<T>
): Promise<T | undefined> {
const list = isArrayLike(items) ? items : array.from(items);
return findIndex(list, callback).then<T | undefined>(function(i) {
return i !== undefined && i >= 0 ? list[i] : undefined;
});
}
/**
* Find the first index with a value matching the filter function
* @param items a collection of synchronous/asynchronous values
* @param callback a synchronous/asynchronous test
* @return a promise eventually containing the index of the matching item or -1 if a match is not found
*/
export function findIndex<T>(
items: Iterable<T | Promise<T>> | (T | Thenable<T>)[],
callback: Filterer<T>
): Promise<number> {
// TODO we can improve this by returning immediately
return processValuesAndCallback(items, callback).then(function(result: ValuesAndResults<T, boolean>) {
if (result && result.results) {
for (let i = 0; i < result.results.length; i++) {
if (result.results[i]) {
return i;
}
}
}
return -1;
});
}
/**
* transform a list of items using a mapper function
* @param items a collection of synchronous/asynchronous values
* @param callback a synchronous/asynchronous transform function
* @return a promise eventually containing a collection of each transformed value
*/
export function map<T, U>(
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Mapper<T, U>
): Promise<U[] | null | undefined> {
return processValuesAndCallback(items, callback).then(function(result) {
return result ? result.results : null;
});
}
/**
* reduce a list of items down to a single value
* @param items a collection of synchronous/asynchronous values
* @param callback a synchronous/asynchronous reducer function
* @param [initialValue] the first value to pass to the callback
* @return a promise eventually containing a value that is the result of the reduction
*/
export function reduce<T, U>(
this: any,
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Reducer<T, U>,
initialValue?: U
): Promise<U> {
const args: any[] = <any[]>array.from(arguments);
args.unshift(findNextValueIndex);
return generalReduce.apply(this, args);
}
export function reduceRight<T, U>(
this: any,
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
callback: Reducer<T, U>,
initialValue?: U
): Promise<U> {
const args: any[] = <any[]>array.from(arguments);
args.unshift(findLastValueIndex);
return generalReduce.apply(this, args);
}
export function series<T, U>(
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
operation: Mapper<T, U>
): Promise<U[]> {
return generalReduce(
findNextValueIndex,
items,
function(previousValue, currentValue: T, index: number, array: T[]) {
const result = operation(currentValue, index, array);
if (isThenable(result)) {
return result.then(function(value) {
previousValue.push(value);
return previousValue;
});
}
previousValue.push(result);
return previousValue;
},
[] as U[]
);
}
export function some<T>(
items: Iterable<T | Promise<T>> | Array<T | Promise<T>>,
callback: Filterer<T>
): Promise<boolean> {
return testAndHaltOnCondition(true, items, callback);
}
export interface Filterer<T> extends Mapper<T, boolean> {}
export interface Mapper<T, U> {
(value: T, index: number, array: T[]): U | Thenable<U>;
}
export interface Reducer<T, U> {
(previousValue: U, currentValue: T, index: number, array: T[]): U | Thenable<U>;
}

View File

@ -0,0 +1,63 @@
import Promise from './ExtensiblePromise';
import { Thenable } from '@dojo/shim/interfaces';
export type IdentityValue<T> = T | (() => T | Thenable<T>);
export interface Identity<T> {
(value?: IdentityValue<T>): Promise<T>;
}
/**
* Used for delaying a Promise chain for a specific number of milliseconds.
*
* @param milliseconds the number of milliseconds to delay
* @return {function (value: T | (() => T | Thenable<T>)): Promise<T>} a function producing a promise that eventually returns the value or executes the value function passed to it; usable with Thenable.then()
*/
export function delay<T>(milliseconds: number): Identity<T> {
return function(value?: IdentityValue<T>): Promise<T> {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(typeof value === 'function' ? value() : value);
}, milliseconds);
});
};
}
/**
* Reject a promise chain if a result hasn't been found before the timeout
*
* @param milliseconds after this number of milliseconds a rejection will be returned
* @param reason The reason for the rejection
* @return {function(T): Promise<T>} a function that produces a promise that is rejected or resolved based on your timeout
*/
export function timeout<T>(milliseconds: number, reason: Error): Identity<T> {
const start = Date.now();
return function(value?: IdentityValue<T>): Promise<T> {
if (Date.now() - milliseconds > start) {
return Promise.reject<T>(reason);
}
if (typeof value === 'function') {
return Promise.resolve(value());
}
return Promise.resolve(value);
};
}
/**
* A Promise that will reject itself automatically after a time.
* Useful for combining with other promises in Promise.race.
*/
export class DelayedRejection extends Promise<any> {
/**
* @param milliseconds the number of milliseconds to wait before triggering a rejection
* @param reason the reason for the rejection
*/
constructor(milliseconds: number, reason?: Error) {
super(() => {});
return new Promise(function(resolve, reject) {
setTimeout(() => {
reject(reason);
}, milliseconds);
});
}
}

View File

@ -0,0 +1,42 @@
import global from '@dojo/shim/global';
import has, { add as hasAdd } from '@dojo/has/has';
hasAdd('btoa', 'btoa' in global, true);
hasAdd('atob', 'atob' in global, true);
/**
* Take a string encoded in base64 and decode it
* @param encodedString The base64 encoded string
*/
export const decode: (encodedString: string) => string = has('atob')
? function(encodedString: string) {
/* this allows for utf8 characters to be decoded properly */
return decodeURIComponent(
Array.prototype.map
.call(
atob(encodedString),
(char: string) => '%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)
)
.join('')
);
}
: function(encodedString: string): string {
return new Buffer(encodedString.toString(), 'base64').toString('utf8');
};
/**
* Take a string and encode it to base64
* @param rawString The string to encode
*/
export const encode: (rawString: string) => string = has('btoa')
? function(decodedString: string) {
/* this allows for utf8 characters to be encoded properly */
return btoa(
encodeURIComponent(decodedString).replace(/%([0-9A-F]{2})/g, (match, code: string) =>
String.fromCharCode(Number('0x' + code))
)
);
}
: function(rawString: string): string {
return new Buffer(rawString.toString(), 'utf8').toString('base64');
};

754
src/lib/core/src/compare.ts Normal file
View File

@ -0,0 +1,754 @@
import { assign } from '@dojo/shim/object';
import { keys } from '@dojo/shim/object';
import Set from '@dojo/shim/Set';
/* Assigning to local variables to improve minification and readability */
const objectCreate = Object.create;
const hasOwnProperty = Object.prototype.hasOwnProperty;
const defineProperty = Object.defineProperty;
const isArray = Array.isArray;
const isFrozen = Object.isFrozen;
const isSealed = Object.isSealed;
export type IgnorePropertyFunction = (name: string, a: any, b: any) => boolean;
export interface DiffOptions {
/**
* Allow functions to be values. Values will be considered equal if the `typeof` both values are `function`.
* When adding or updating the property, the value of the property of `a` will be used in the record, which
* will be a reference to the function.
*/
allowFunctionValues?: boolean;
/**
* An array of strings or regular expressions which flag certain properties to be ignored. Alternatively
* a function, which returns `true` to have the property ignored or `false` to diff the property.
*/
ignoreProperties?: (string | RegExp)[] | IgnorePropertyFunction;
/**
* An array of strings or regular expressions which flag certain values to be ignored. For flagged properties,
* if the property is present in both `a` and `b` the value will be ignored. If adding the property,
* whatever the value of the property of `a` will be used, which could be a reference.
*/
ignorePropertyValues?: (string | RegExp)[] | IgnorePropertyFunction;
}
/**
* Interface for a generic constructor function
*/
export interface Constructor {
new (...args: any[]): object;
prototype: object;
}
/**
* A partial property descriptor that provides the property descriptor flags supported by the
* complex property construction of `patch()`
*
* All properties are value properties, with the value being supplied by the `ConstructRecord`
*/
export interface ConstructDescriptor {
/**
* Is the property configurable?
*/
configurable?: boolean;
/**
* Is the property enumerable?
*/
enumerable?: boolean;
/**
* Is the property configurable?
*/
writable?: boolean;
}
/**
* A record that describes a constructor function and arguments necessary to create an instance of
* an object
*/
export interface AnonymousConstructRecord {
/**
* Any arguments to pass to the constructor function
*/
args?: any[];
/**
* The constructor function to use to create the instance
*/
Ctor: Constructor;
/**
* The partial descriptor that is used to set the value of the instance
*/
descriptor?: ConstructDescriptor;
/**
* Any patches to properties that need to occur on the instance
*/
propertyRecords?: (ConstructRecord | PatchRecord)[];
}
export interface ConstructRecord extends AnonymousConstructRecord {
/**
* The name of the property on the Object
*/
name: string;
}
/**
* A record that describes the mutations necessary to a property of an object to make that property look
* like another
*/
export type PatchRecord =
| {
/**
* The name of the property on the Object
*/
name: string;
/**
* The type of the patch
*/
type: 'delete';
}
| {
/**
* A property descriptor that describes the property in `name`
*/
descriptor: PropertyDescriptor;
/**
* The name of the property on the Object
*/
name: string;
/**
* The type of the patch
*/
type: 'add' | 'update';
/**
* Additional patch records which describe the value of the property
*/
valueRecords?: (ConstructRecord | PatchRecord | SpliceRecord)[];
};
/**
* The different types of patch records supported
*/
export type PatchTypes = 'add' | 'update' | 'delete';
/**
* A record that describes a splice operation to perform on an array to make the array look like another array
*/
export interface SpliceRecord {
/**
* Any items that are being added to the array
*/
add?: any[];
/**
* The number of items in the array to delete
*/
deleteCount: number;
/**
* The type, set to `splice`
*/
type: 'splice';
/**
* The index of where to start the splice
*/
start: number;
}
/**
* A record that describes how to instantiate a new object via a constructor function
* @param Ctor The constructor function
* @param args Any arguments to be passed to the constructor function
*/
/* tslint:disable:variable-name */
export function createConstructRecord(
Ctor: Constructor,
args?: any[],
descriptor?: ConstructDescriptor
): AnonymousConstructRecord {
const record: AnonymousConstructRecord = assign(objectCreate(null), { Ctor });
if (args) {
record.args = args;
}
if (descriptor) {
record.descriptor = descriptor;
}
return record;
}
/* tslint:enable:variable-name */
/**
* An internal function that returns a new patch record
*
* @param type The type of patch record
* @param name The property name the record refers to
* @param descriptor The property descriptor to be installed on the object
* @param valueRecords Any subsequenet patch recrds to be applied to the value of the descriptor
*/
function createPatchRecord(
type: PatchTypes,
name: string,
descriptor?: PropertyDescriptor,
valueRecords?: (ConstructRecord | PatchRecord | SpliceRecord)[]
): PatchRecord {
const patchRecord = assign(objectCreate(null), {
type,
name
});
if (descriptor) {
patchRecord.descriptor = descriptor;
}
if (valueRecords) {
patchRecord.valueRecords = valueRecords;
}
return patchRecord as PatchRecord;
}
/**
* An internal function that returns a new splice record
*
* @param start Where in the array to start the splice
* @param deleteCount The number of elements to delete from the array
* @param add Elements to be added to the target
*/
function createSpliceRecord(start: number, deleteCount: number, add?: any[]): SpliceRecord {
const spliceRecord: SpliceRecord = assign(objectCreate(null), {
type: 'splice',
start,
deleteCount
});
if (add && add.length) {
spliceRecord.add = add;
}
return spliceRecord;
}
/**
* A function that produces a value property descriptor, which assumes that properties are enumerable, writable and configurable
* unless specified
*
* @param value The value for the descriptor
* @param writable Defaults to `true` if not specified
* @param enumerable Defaults to `true` if not specified
* @param configurable Defaults to `true` if not specified
*/
function createValuePropertyDescriptor(
value: any,
writable: boolean = true,
enumerable: boolean = true,
configurable: boolean = true
): PropertyDescriptor {
return assign(objectCreate(null), {
value,
writable,
enumerable,
configurable
});
}
/**
* A function that returns a constructor record or `undefined` when diffing a value
*/
export type CustomDiffFunction<T> = (
value: T,
nameOrIndex: string | number,
parent: object
) => AnonymousConstructRecord | void;
/**
* A class which is used when making a custom comparison of a non-plain object or array
*/
export class CustomDiff<T> {
private _differ: CustomDiffFunction<T>;
constructor(diff: CustomDiffFunction<T>) {
this._differ = diff;
}
/**
* Get the difference of the `value`
* @param value The value to diff
* @param nameOrIndex A `string` if comparing a property or a `number` if comparing an array element
* @param parent The outer parent that this value is part of
*/
diff(value: T, nameOrIndex: string | number, parent: object): ConstructRecord | void {
const record = this._differ(value, nameOrIndex, parent);
if (record && typeof nameOrIndex === 'string') {
return assign(record, { name: nameOrIndex });
}
}
}
/**
* Internal function that detects the differences between an array and another value and returns a set of splice records that
* describe the differences
*
* @param a The first array to compare to
* @param b The second value to compare to
* @param options An options bag that allows configuration of the behaviour of `diffArray()`
*/
function diffArray(a: any[], b: any, options: DiffOptions): SpliceRecord[] {
/* This function takes an overly simplistic approach to calculating splice records. There are many situations where
* in complicated array mutations, the splice records can be more optimised.
*
* TODO: Raise an issue for this when it is finally merged and put into core
*/
const { allowFunctionValues = false } = options;
const arrayA = a;
const lengthA = arrayA.length;
const arrayB = isArray(b) ? b : [];
const lengthB = arrayB.length;
const patchRecords: SpliceRecord[] = [];
if (!lengthA && lengthB) {
/* empty array */
patchRecords.push(createSpliceRecord(0, lengthB));
return patchRecords;
}
let add: any[] = [];
let start = 0;
let deleteCount = 0;
let last = -1;
function flushSpliceRecord() {
if (deleteCount || add.length) {
patchRecords.push(
createSpliceRecord(start, start + deleteCount > lengthB ? lengthB - start : deleteCount, add)
);
}
}
function addDifference(index: number, adding: boolean, value?: any) {
if (index > last + 1) {
/* flush the splice */
flushSpliceRecord();
start = index;
deleteCount = 0;
if (add.length) {
add = [];
}
}
if (adding) {
add.push(value);
}
deleteCount++;
last = index;
}
arrayA.forEach((valueA, index) => {
const valueB = arrayB[index];
if (
index in arrayB &&
(valueA === valueB || (allowFunctionValues && typeof valueA === 'function' && typeof valueB === 'function'))
) {
return; /* not different */
}
const isValueAArray = isArray(valueA);
const isValueAPlainObject = isPlainObject(valueA);
if (isValueAArray || isValueAPlainObject) {
const value = isValueAArray
? isArray(valueB) ? valueB : []
: isPlainObject(valueB) ? valueB : Object.create(null);
const valueRecords = diff(valueA, value, options);
if (valueRecords.length) {
/* only add if there are changes */
addDifference(index, true, diff(valueA, value, options));
}
} else if (isPrimitive(valueA)) {
addDifference(index, true, valueA);
} else if (allowFunctionValues && typeof valueA === 'function') {
addDifference(index, true, valueA);
} else {
throw new TypeError(
`Value of array element "${index}" from first argument is not a primative, plain Object, or Array.`
);
}
});
if (lengthB > lengthA) {
for (let index = lengthA; index < lengthB; index++) {
addDifference(index, false);
}
}
/* flush any deletes */
flushSpliceRecord();
return patchRecords;
}
/**
* Internal function that detects the differences between plain objects and returns a set of patch records that
* describe the differences
*
* @param a The first plain object to compare to
* @param b The second plain bject to compare to
* @param options An options bag that allows configuration of the behaviour of `diffPlainObject()`
*/
function diffPlainObject(a: any, b: any, options: DiffOptions): (ConstructRecord | PatchRecord)[] {
const { allowFunctionValues = false, ignorePropertyValues = [] } = options;
const patchRecords: (ConstructRecord | PatchRecord)[] = [];
const { comparableA, comparableB } = getComparableObjects(a, b, options);
/* look for keys in a that are different from b */
keys(comparableA).reduce((patchRecords, name) => {
const valueA = a[name];
const valueB = b[name];
const bHasOwnProperty = hasOwnProperty.call(comparableB, name);
if (
bHasOwnProperty &&
(valueA === valueB || (allowFunctionValues && typeof valueA === 'function' && typeof valueB === 'function'))
) {
/* not different */
/* when `allowFunctionValues` is true, functions are simply considered to be equal by `typeof` */
return patchRecords;
}
const type = bHasOwnProperty ? 'update' : 'add';
const isValueAArray = isArray(valueA);
const isValueAPlainObject = isPlainObject(valueA);
if (isValueAArray || isValueAPlainObject) {
/* non-primitive values we can diff */
/* this is a bit complicated, but essentially if valueA and valueB are both arrays or plain objects, then
* we can diff those two values, if not, then we need to use an empty array or an empty object and diff
* the valueA with that */
const value =
(isValueAArray && isArray(valueB)) || (isValueAPlainObject && isPlainObject(valueB))
? valueB
: isValueAArray ? [] : objectCreate(null);
const valueRecords = diff(valueA, value, options);
if (valueRecords.length) {
/* only add if there are changes */
patchRecords.push(
createPatchRecord(type, name, createValuePropertyDescriptor(value), diff(valueA, value, options))
);
}
} else if (isCustomDiff(valueA) && !isCustomDiff(valueB)) {
/* complex diff left hand */
const result = valueA.diff(valueB, name, b);
if (result) {
patchRecords.push(result);
}
} else if (isCustomDiff(valueB)) {
/* complex diff right hand */
const result = valueB.diff(valueA, name, a);
if (result) {
patchRecords.push(result);
}
} else if (
isPrimitive(valueA) ||
(allowFunctionValues && typeof valueA === 'function') ||
isIgnoredPropertyValue(name, a, b, ignorePropertyValues)
) {
/* primitive values, functions values if allowed, or ignored property values can just be copied */
patchRecords.push(createPatchRecord(type, name, createValuePropertyDescriptor(valueA)));
} else {
throw new TypeError(
`Value of property named "${name}" from first argument is not a primative, plain Object, or Array.`
);
}
return patchRecords;
}, patchRecords);
/* look for keys in b that are not in a */
keys(comparableB).reduce((patchRecords, name) => {
if (!hasOwnProperty.call(comparableA, name)) {
patchRecords.push(createPatchRecord('delete', name));
}
return patchRecords;
}, patchRecords);
return patchRecords;
}
/**
* Takes two plain objects to be compared, as well as options customizing the behavior of the comparison, and returns
* two new objects that contain only those properties that should be compared. If a property is ignored
* it will not be included in either returned object. If a property's value should be ignored it will be excluded
* if it is present in both objects.
* @param a The first object to compare
* @param b The second object to compare
* @param options An options bag indicating which properties should be ignored or have their values ignored, if any.
*/
export function getComparableObjects(a: any, b: any, options: DiffOptions) {
const { ignoreProperties = [], ignorePropertyValues = [] } = options;
const ignore = new Set<string>();
const keep = new Set<string>();
const isIgnoredProperty = Array.isArray(ignoreProperties)
? (name: string) => {
return ignoreProperties.some(
(value) => (typeof value === 'string' ? name === value : value.test(name))
);
}
: (name: string) => ignoreProperties(name, a, b);
const comparableA = keys(a).reduce(
(obj, name) => {
if (
isIgnoredProperty(name) ||
(hasOwnProperty.call(b, name) && isIgnoredPropertyValue(name, a, b, ignorePropertyValues))
) {
ignore.add(name);
return obj;
}
keep.add(name);
obj[name] = a[name];
return obj;
},
{} as { [key: string]: any }
);
const comparableB = keys(b).reduce(
(obj, name) => {
if (ignore.has(name) || (!keep.has(name) && isIgnoredProperty(name))) {
return obj;
}
obj[name] = b[name];
return obj;
},
{} as { [key: string]: any }
);
return { comparableA, comparableB, ignore };
}
/**
* A guard that determines if the value is a `ConstructRecord`
* @param value The value to check
*/
function isConstructRecord(value: any): value is ConstructRecord {
return Boolean(value && typeof value === 'object' && value !== null && value.Ctor && value.name);
}
function isIgnoredPropertyValue(
name: string,
a: any,
b: any,
ignoredPropertyValues: (string | RegExp)[] | IgnorePropertyFunction
) {
return Array.isArray(ignoredPropertyValues)
? ignoredPropertyValues.some((value) => {
return typeof value === 'string' ? name === value : value.test(name);
})
: ignoredPropertyValues(name, a, b);
}
/**
* A guard that determines if the value is a `PatchRecord`
*
* @param value The value to check
*/
function isPatchRecord(value: any): value is PatchRecord {
return Boolean(value && value.type && value.name);
}
/**
* A guard that determines if the value is an array of `PatchRecord`s
*
* @param value The value to check
*/
function isPatchRecordArray(value: any): value is PatchRecord[] {
return Boolean(isArray(value) && value.length && isPatchRecord(value[0]));
}
/**
* A guard that determines if the value is a plain object. A plain object is an object that has
* either no constructor (e.g. `Object.create(null)`) or has Object as its constructor.
*
* @param value The value to check
*/
function isPlainObject(value: any): value is Object {
return Boolean(
value && typeof value === 'object' && (value.constructor === Object || value.constructor === undefined)
);
}
/**
* A guard that determines if the value is a primitive (including `null`), as these values are
* fine to just copy.
*
* @param value The value to check
*/
function isPrimitive(value: any): value is string | number | boolean | undefined | null {
const typeofValue = typeof value;
return (
value === null ||
typeofValue === 'undefined' ||
typeofValue === 'string' ||
typeofValue === 'number' ||
typeofValue === 'boolean'
);
}
/**
* A guard that determines if the value is a `CustomDiff`
* @param value The value to check
*/
function isCustomDiff<T>(value: any): value is CustomDiff<T> {
return typeof value === 'object' && value instanceof CustomDiff;
}
/**
* A guard that determines if the value is a `SpliceRecord`
*
* @param value The value to check
*/
function isSpliceRecord(value: any): value is SpliceRecord {
return value && value.type === 'splice' && 'start' in value && 'deleteCount' in value;
}
/**
* A guard that determines if the value is an array of `SpliceRecord`s
*
* @param value The value to check
*/
function isSpliceRecordArray(value: any): value is SpliceRecord[] {
return Boolean(isArray(value) && value.length && isSpliceRecord(value[0]));
}
/**
* An internal function that patches a target with a `SpliceRecord`
*/
function patchSplice(target: any[], { add, deleteCount, start }: SpliceRecord): any {
if (add && add.length) {
const deletedItems = deleteCount ? target.slice(start, start + deleteCount) : [];
add = add.map((value, index) => resolveTargetValue(value, deletedItems[index]));
target.splice(start, deleteCount, ...add);
} else {
target.splice(start, deleteCount);
}
return target;
}
/**
* An internal function that patches a target with a `PatchRecord`
*/
function patchPatch(target: any, record: PatchRecord): any {
const { name } = record;
if (record.type === 'delete') {
delete target[name];
return target;
}
const { descriptor, valueRecords } = record;
if (valueRecords && valueRecords.length) {
descriptor.value = patch(descriptor.value, valueRecords);
}
defineProperty(target, name, descriptor);
return target;
}
const defaultConstructDescriptor = {
configurable: true,
enumerable: true,
writable: true
};
function patchConstruct(target: any, record: ConstructRecord): any {
const { args, descriptor = defaultConstructDescriptor, Ctor, name, propertyRecords } = record;
const value = new Ctor(...(args || []));
if (propertyRecords) {
propertyRecords.forEach(
(record) => (isConstructRecord(record) ? patchConstruct(value, record) : patchPatch(value, record))
);
}
defineProperty(target, name, assign({ value }, descriptor));
return target;
}
/**
* An internal function that takes a value from array being patched and the target value from the same
* index and determines the value that should actually be patched into the target array
*/
function resolveTargetValue(patchValue: any, targetValue: any): any {
const patchIsSpliceRecordArray = isSpliceRecordArray(patchValue);
return patchIsSpliceRecordArray || isPatchRecordArray(patchValue)
? patch(
patchIsSpliceRecordArray
? isArray(targetValue) ? targetValue : []
: isPlainObject(targetValue) ? targetValue : objectCreate(null),
patchValue
)
: patchValue;
}
/**
* Compares two plain objects or arrays and return a set of records which describe the differences between the two
*
* The records describe what would need to be applied to the second argument to make it look like the first argument
*
* @param a The plain object or array to compare with
* @param b The plain object or array to compare to
* @param options An options bag that allows configuration of the behaviour of `diff()`
*/
export function diff(a: any, b: any, options: DiffOptions = {}): (ConstructRecord | PatchRecord | SpliceRecord)[] {
if (typeof a !== 'object' || typeof b !== 'object') {
throw new TypeError('Arguments are not of type object.');
}
if (isArray(a)) {
return diffArray(a, b, options);
}
if (isArray(b)) {
b = objectCreate(null);
}
if (!isPlainObject(a) || !isPlainObject(b)) {
throw new TypeError('Arguments are not plain Objects or Arrays.');
}
return diffPlainObject(a, b, options);
}
/**
* Apply a set of patch records to a target.
*
* @param target The plain object or array that the patch records should be applied to
* @param records A set of patch records to be applied to the target
*/
export function patch(target: any, records: (ConstructRecord | PatchRecord | SpliceRecord)[]): any {
if (!isArray(target) && !isPlainObject(target)) {
throw new TypeError('A target for a patch must be either an array or a plain object.');
}
if (isFrozen(target) || isSealed(target)) {
throw new TypeError('Cannot patch sealed or frozen objects.');
}
records.forEach((record) => {
target = isSpliceRecord(record)
? patchSplice(isArray(target) ? target : [], record) /* patch arrays */
: isConstructRecord(record)
? patchConstruct(target, record) /* patch complex object */
: patchPatch(isPlainObject(target) ? target : {}, record); /* patch plain object */
});
return target;
}

View File

@ -0,0 +1,10 @@
import globalObject from '@dojo/shim/global';
import { deprecated } from './instrument';
deprecated({
message: 'has been replaced with @dojo/shim/global',
name: '@dojo/core/global',
url: 'https://github.com/dojo/core/issues/302'
});
export default globalObject;

70
src/lib/core/src/has.ts Normal file
View File

@ -0,0 +1,70 @@
import global from '@dojo/shim/global';
import has, { add } from '@dojo/shim/support/has';
export * from '@dojo/shim/support/has';
export default has;
add('object-assign', typeof global.Object.assign === 'function', true);
add('arraybuffer', typeof global.ArrayBuffer !== 'undefined', true);
add('formdata', typeof global.FormData !== 'undefined', true);
add('filereader', typeof global.FileReader !== 'undefined', true);
add('xhr', typeof global.XMLHttpRequest !== 'undefined', true);
add('xhr2', has('xhr') && 'responseType' in global.XMLHttpRequest.prototype, true);
add(
'blob',
function() {
if (!has('xhr2')) {
return false;
}
const request = new global.XMLHttpRequest();
request.open('GET', global.location.protocol + '//www.google.com', true);
request.responseType = 'blob';
request.abort();
return request.responseType === 'blob';
},
true
);
add('node-buffer', 'Buffer' in global && typeof global.Buffer === 'function', true);
add('fetch', 'fetch' in global && typeof global.fetch === 'function', true);
add(
'web-worker-xhr-upload',
typeof global.Promise !== 'undefined' &&
new Promise((resolve) => {
try {
if (global.Worker !== undefined && global.URL && global.URL.createObjectURL) {
const blob = new Blob(
[
`(function () {
self.addEventListener('message', function () {
var xhr = new XMLHttpRequest();
try {
xhr.upload;
postMessage('true');
} catch (e) {
postMessage('false');
}
});
})()`
],
{ type: 'application/javascript' }
);
const worker = new Worker(URL.createObjectURL(blob));
worker.addEventListener('message', ({ data: result }) => {
resolve(result === 'true');
});
worker.postMessage({});
} else {
resolve(false);
}
} catch (e) {
// IE11 on Winodws 8.1 encounters a security error.
resolve(false);
}
}),
true
);

View File

@ -0,0 +1,100 @@
import has from './has';
/**
* The default message to warn when no other is provided
*/
const DEFAULT_DEPRECATED_MESSAGE = 'This function will be removed in future versions.';
/**
* When set, globalWarn will be used instead of `console.warn`
*/
let globalWarn: ((message?: any, ...optionalParams: any[]) => void) | undefined;
export interface DeprecatedOptions {
/**
* The message to use when warning
*/
message?: string;
/**
* The name of the method or function to use
*/
name?: string;
/**
* An alternative function to log the warning to
*/
warn?: (...args: any[]) => void;
/**
* Reference an URL for more information when warning
*/
url?: string;
}
/**
* A function that will console warn that a function has been deprecated
*
* @param options Provide options which change the display of the message
*/
export function deprecated({ message, name, warn, url }: DeprecatedOptions = {}): void {
/* istanbul ignore else: testing with debug off is difficult */
if (has('debug')) {
message = message || DEFAULT_DEPRECATED_MESSAGE;
let warning = `DEPRECATED: ${name ? name + ': ' : ''}${message}`;
if (url) {
warning += `\n\n See ${url} for more details.\n\n`;
}
if (warn) {
warn(warning);
} else if (globalWarn) {
globalWarn(warning);
} else {
console.warn(warning);
}
}
}
/**
* A function that generates before advice that can be used to warn when an API has been deprecated
*
* @param options Provide options which change the display of the message
*/
export function deprecatedAdvice(options?: DeprecatedOptions): (...args: any[]) => any[] {
return function(...args: any[]): any[] {
deprecated(options);
return args;
};
}
/**
* A method decorator that will console warn when a method if invoked that is deprecated
*
* @param options Provide options which change the display of the message
*/
export function deprecatedDecorator(options?: DeprecatedOptions): MethodDecorator {
return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
if (has('debug')) {
const { value: originalFn } = descriptor;
options = options || {};
propertyKey = String(propertyKey);
/* IE 10/11 don't have the name property on functions */
options.name = target.constructor.name ? `${target.constructor.name}#${propertyKey}` : propertyKey;
descriptor.value = function(...args: any[]) {
deprecated(options);
return originalFn.apply(target, args);
};
}
return descriptor;
};
}
/**
* A function that will set the warn function that will be used instead of `console.warn` when
* logging warning messages
*
* @param warn The function (or `undefined`) to use instead of `console.warn`
*/
export function setWarn(warn?: ((message?: any, ...optionalParams: any[]) => void)): void {
globalWarn = warn;
}

View File

@ -0,0 +1,287 @@
export type EventType = string | symbol;
/**
* The base event object, which provides a `type` property
*/
export interface EventObject<T = EventType> {
/**
* The type of the event
*/
readonly type: T;
}
export interface EventErrorObject<T = EventType> extends EventObject<T> {
/**
* The error that is the subject of this event
*/
readonly error: Error;
}
/**
* An interface for an object which provides a cancelable event API. By calling the
* `.preventDefault()` method on the object, the event should be cancelled and not
* proceed any further
*/
export interface EventCancelableObject<T = EventType> extends EventObject<T> {
/**
* Can the event be canceled?
*/
readonly cancelable: boolean;
/**
* Was the event canceled?
*/
readonly defaultPrevented: boolean;
/**
* Cancel the event
*/
preventDefault(): void;
}
/**
* Used through the toolkit as a consistent API to manage how callers can "cleanup"
* when doing a function.
*/
export interface Handle {
/**
* Perform the destruction/cleanup logic associated with this handle
*/
destroy(): void;
}
/**
* A general interface that can be used to renference a general index map of values of a particular type
*/
export interface Hash<T> {
[id: string]: T;
}
/**
* A base map of styles where each key is the name of the style attribute and the value is a string
* which represents the style
*/
export interface StylesMap {
[style: string]: string;
}
/**
* The interfaces to the `@dojo/loader` AMD loader
*/
export interface AmdConfig {
/**
* The base URL that the loader will use to resolve modules
*/
baseUrl?: string;
/**
* A map of module identifiers and their replacement meta data
*/
map?: AmdModuleMap;
/**
* An array of packages that the loader should use when resolving a module ID
*/
packages?: AmdPackage[];
/**
* A map of paths to use when resolving modules names
*/
paths?: { [path: string]: string };
/**
* A map of packages that the loader should use when resolving a module ID
*/
pkgs?: { [path: string]: AmdPackage };
}
export interface AmdDefine {
/**
* Define a module
*
* @param moduleId the MID to use for the module
* @param dependencies an array of MIDs this module depends upon
* @param factory the factory function that will return the module
*/
(moduleId: string, dependencies: string[], factory: AmdFactory): void;
/**
* Define a module
*
* @param dependencies an array of MIDs this module depends upon
* @param factory the factory function that will return the module
*/
(dependencies: string[], factory: AmdFactory): void;
/**
* Define a module
*
* @param factory the factory function that will return the module
*/
(factory: AmdFactory): void;
/**
* Define a module
*
* @param value the value for the module
*/
(value: any): void;
/**
* Meta data about this particular AMD loader
*/
amd: { [prop: string]: string | number | boolean };
}
export interface AmdFactory {
/**
* The module factory
*
* @param modules The arguments that represent the resolved versions of the module dependencies
*/
(...modules: any[]): any;
}
export interface AmdHas {
/**
* Determine if a feature is present
*
* @param name the feature name to check
*/
(name: string): any;
/**
* Register a feature test
*
* @param name The name of the feature to register
* @param value The test for the feature
* @param now If `true` the test will be executed immediatly, if not, it will be lazily executed
* @param force If `true` the test value will be overwritten if already registered
*/
add(
name: string,
value: (global: Window, document?: HTMLDocument, element?: HTMLDivElement) => any,
now?: boolean,
force?: boolean
): void;
add(name: string, value: any, now?: boolean, force?: boolean): void;
}
export interface AmdModuleMap extends AmdModuleMapItem {
[sourceMid: string]: AmdModuleMapReplacement;
}
export interface AmdModuleMapItem {
[mid: string]: any;
}
export interface AmdModuleMapReplacement extends AmdModuleMapItem {
[findMid: string]: string;
}
export interface NodeRequire {
(moduleId: string): any;
resolve(moduleId: string): string;
}
export interface AmdPackage {
/**
* The path to the root of the package
*/
location?: string;
/**
* The main module of the package (defaults to `main.js`)
*/
main?: string;
/**
* The package name
*/
name?: string;
}
export interface AmdRequire {
/**
* Resolve a list of module dependencies and pass them to the callback
*
* @param dependencies The array of MIDs to resolve
* @param callback The function to invoke with the resolved dependencies
*/
(dependencies: string[], callback: AmdRequireCallback): void;
/**
* Resolve and return a single module (compatability with CommonJS `require`)
*
* @param moduleId The module ID to resolve and return
*/
<ModuleType>(moduleId: string): ModuleType;
/**
* If running in the node environment, a reference to the original NodeJS `require`
*/
nodeRequire?: NodeRequire;
/**
* Take a relative MID and return an absolute MID
*
* @param moduleId The relative module ID to resolve
*/
toAbsMid(moduleId: string): string;
/**
* Take a path and resolve the full URL for the path
*
* @param path The path to resolve and return as a URL
*/
toUrl(path: string): string;
}
export interface AmdRequireCallback {
/**
* The `require` callback
*
* @param modules The arguments that represent the resolved versions of dependencies
*/
(...modules: any[]): void;
}
export interface AmdRootRequire extends AmdRequire {
/**
* The minimalist `has` API integrated with the `@dojo/loader`
*/
has: AmdHas;
/**
* Register an event listener
*
* @param type The event type to listen for
* @param listener The listener to call when the event is emitted
*/
on(type: AmdRequireOnSignalType, listener: any): { remove: () => void };
/**
* Configure the loader
*
* @param config The configuration to apply to the loader
*/
config(config: AmdConfig): void;
/**
* Return internal values of loader for debug purposes
*
* @param name The name of the internal label
*/
inspect?(name: string): any;
/**
* Undefine a module, based on absolute MID that should be removed from the loader cache
*/
undef(moduleId: string): void;
}
/**
* The signal type for the `require.on` API
*/
export type AmdRequireOnSignalType = 'error';

390
src/lib/core/src/lang.ts Normal file
View File

@ -0,0 +1,390 @@
import { Handle } from './interfaces';
import { assign } from '@dojo/shim/object';
export { assign } from '@dojo/shim/object';
const slice = Array.prototype.slice;
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Type guard that ensures that the value can be coerced to Object
* to weed out host objects that do not derive from Object.
* This function is used to check if we want to deep copy an object or not.
* Note: In ES6 it is possible to modify an object's Symbol.toStringTag property, which will
* change the value returned by `toString`. This is a rare edge case that is difficult to handle,
* so it is not handled here.
* @param value The value to check
* @return If the value is coercible into an Object
*/
function shouldDeepCopyObject(value: any): value is Object {
return Object.prototype.toString.call(value) === '[object Object]';
}
function copyArray<T>(array: T[], inherited: boolean): T[] {
return array.map(function(item: T): T {
if (Array.isArray(item)) {
return <any>copyArray(<any>item, inherited);
}
return !shouldDeepCopyObject(item)
? item
: _mixin({
deep: true,
inherited: inherited,
sources: <Array<T>>[item],
target: <T>{}
});
});
}
interface MixinArgs<T extends {}, U extends {}> {
deep: boolean;
inherited: boolean;
sources: (U | null | undefined)[];
target: T;
copied?: any[];
}
function _mixin<T extends {}, U extends {}>(kwArgs: MixinArgs<T, U>): T & U {
const deep = kwArgs.deep;
const inherited = kwArgs.inherited;
const target: any = kwArgs.target;
const copied = kwArgs.copied || [];
const copiedClone = [...copied];
for (let i = 0; i < kwArgs.sources.length; i++) {
const source = kwArgs.sources[i];
if (source === null || source === undefined) {
continue;
}
for (let key in source) {
if (inherited || hasOwnProperty.call(source, key)) {
let value: any = source[key];
if (copiedClone.indexOf(value) !== -1) {
continue;
}
if (deep) {
if (Array.isArray(value)) {
value = copyArray(value, inherited);
} else if (shouldDeepCopyObject(value)) {
const targetValue: any = target[key] || {};
copied.push(source);
value = _mixin({
deep: true,
inherited: inherited,
sources: [value],
target: targetValue,
copied
});
}
}
target[key] = value;
}
}
}
return <T & U>target;
}
/**
* Creates a new object from the given prototype, and copies all enumerable own properties of one or more
* source objects to the newly created target object.
*
* @param prototype The prototype to create a new object from
* @param mixins Any number of objects whose enumerable own properties will be copied to the created object
* @return The new object
*/
export function create<
T extends {},
U extends {},
V extends {},
W extends {},
X extends {},
Y extends {},
Z extends {}
>(prototype: T, mixin1: U, mixin2: V, mixin3: W, mixin4: X, mixin5: Y, mixin6: Z): T & U & V & W & X & Y & Z;
export function create<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
prototype: T,
mixin1: U,
mixin2: V,
mixin3: W,
mixin4: X,
mixin5: Y
): T & U & V & W & X & Y;
export function create<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
prototype: T,
mixin1: U,
mixin2: V,
mixin3: W,
mixin4: X
): T & U & V & W & X;
export function create<T extends {}, U extends {}, V extends {}, W extends {}>(
prototype: T,
mixin1: U,
mixin2: V,
mixin3: W
): T & U & V & W;
export function create<T extends {}, U extends {}, V extends {}>(prototype: T, mixin1: U, mixin2: V): T & U & V;
export function create<T extends {}, U extends {}>(prototype: T, mixin: U): T & U;
export function create<T extends {}>(prototype: T): T;
export function create(prototype: any, ...mixins: any[]): any {
if (!mixins.length) {
throw new RangeError('lang.create requires at least one mixin object.');
}
const args = mixins.slice();
args.unshift(Object.create(prototype));
return assign.apply(null, args);
}
/**
* Copies the values of all enumerable own properties of one or more source objects to the target object,
* recursively copying all nested objects and arrays as well.
*
* @param target The target object to receive values from source objects
* @param sources Any number of objects whose enumerable own properties will be copied to the target object
* @return The modified target object
*/
export function deepAssign<
T extends {},
U extends {},
V extends {},
W extends {},
X extends {},
Y extends {},
Z extends {}
>(target: T, source1: U, source2: V, source3: W, source4: X, source5: Y, source6: Z): T & U & V & W & X & Y & Z;
export function deepAssign<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X,
source5: Y
): T & U & V & W & X & Y;
export function deepAssign<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X
): T & U & V & W & X;
export function deepAssign<T extends {}, U extends {}, V extends {}, W extends {}>(
target: T,
source1: U,
source2: V,
source3: W
): T & U & V & W;
export function deepAssign<T extends {}, U extends {}, V extends {}>(target: T, source1: U, source2: V): T & U & V;
export function deepAssign<T extends {}, U extends {}>(target: T, source: U): T & U;
export function deepAssign(target: any, ...sources: any[]): any {
return _mixin({
deep: true,
inherited: false,
sources: sources,
target: target
});
}
/**
* Copies the values of all enumerable (own or inherited) properties of one or more source objects to the
* target object, recursively copying all nested objects and arrays as well.
*
* @param target The target object to receive values from source objects
* @param sources Any number of objects whose enumerable properties will be copied to the target object
* @return The modified target object
*/
export function deepMixin<
T extends {},
U extends {},
V extends {},
W extends {},
X extends {},
Y extends {},
Z extends {}
>(target: T, source1: U, source2: V, source3: W, source4: X, source5: Y, source6: Z): T & U & V & W & X & Y & Z;
export function deepMixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X,
source5: Y
): T & U & V & W & X & Y;
export function deepMixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X
): T & U & V & W & X;
export function deepMixin<T extends {}, U extends {}, V extends {}, W extends {}>(
target: T,
source1: U,
source2: V,
source3: W
): T & U & V & W;
export function deepMixin<T extends {}, U extends {}, V extends {}>(target: T, source1: U, source2: V): T & U & V;
export function deepMixin<T extends {}, U extends {}>(target: T, source: U): T & U;
export function deepMixin(target: any, ...sources: any[]): any {
return _mixin({
deep: true,
inherited: true,
sources: sources,
target: target
});
}
/**
* Creates a new object using the provided source's prototype as the prototype for the new object, and then
* deep copies the provided source's values into the new target.
*
* @param source The object to duplicate
* @return The new object
*/
export function duplicate<T extends {}>(source: T): T {
const target = Object.create(Object.getPrototypeOf(source));
return deepMixin(target, source);
}
/**
* Determines whether two values are the same value.
*
* @param a First value to compare
* @param b Second value to compare
* @return true if the values are the same; false otherwise
*/
export function isIdentical(a: any, b: any): boolean {
return (
a === b ||
/* both values are NaN */
(a !== a && b !== b)
);
}
/**
* Returns a function that binds a method to the specified object at runtime. This is similar to
* `Function.prototype.bind`, but instead of a function it takes the name of a method on an object.
* As a result, the function returned by `lateBind` will always call the function currently assigned to
* the specified property on the object as of the moment the function it returns is called.
*
* @param instance The context object
* @param method The name of the method on the context object to bind to itself
* @param suppliedArgs An optional array of values to prepend to the `instance[method]` arguments list
* @return The bound function
*/
export function lateBind(instance: {}, method: string, ...suppliedArgs: any[]): (...args: any[]) => any {
return suppliedArgs.length
? function() {
const args: any[] = arguments.length ? suppliedArgs.concat(slice.call(arguments)) : suppliedArgs;
// TS7017
return (<any>instance)[method].apply(instance, args);
}
: function() {
// TS7017
return (<any>instance)[method].apply(instance, arguments);
};
}
/**
* Copies the values of all enumerable (own or inherited) properties of one or more source objects to the
* target object.
*
* @return The modified target object
*/
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}, Z extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X,
source5: Y,
source6: Z
): T & U & V & W & X & Y & Z;
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X,
source5: Y
): T & U & V & W & X & Y;
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
target: T,
source1: U,
source2: V,
source3: W,
source4: X
): T & U & V & W & X;
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}>(
target: T,
source1: U,
source2: V,
source3: W
): T & U & V & W;
export function mixin<T extends {}, U extends {}, V extends {}>(target: T, source1: U, source2: V): T & U & V;
export function mixin<T extends {}, U extends {}>(target: T, source: U): T & U;
export function mixin(target: any, ...sources: any[]): any {
return _mixin({
deep: false,
inherited: true,
sources: sources,
target: target
});
}
/**
* Returns a function which invokes the given function with the given arguments prepended to its argument list.
* Like `Function.prototype.bind`, but does not alter execution context.
*
* @param targetFunction The function that needs to be bound
* @param suppliedArgs An optional array of arguments to prepend to the `targetFunction` arguments list
* @return The bound function
*/
export function partial(targetFunction: (...args: any[]) => any, ...suppliedArgs: any[]): (...args: any[]) => any {
return function(this: any) {
const args: any[] = arguments.length ? suppliedArgs.concat(slice.call(arguments)) : suppliedArgs;
return targetFunction.apply(this, args);
};
}
/**
* Returns an object with a destroy method that, when called, calls the passed-in destructor.
* This is intended to provide a unified interface for creating "remove" / "destroy" handlers for
* event listeners, timers, etc.
*
* @param destructor A function that will be called when the handle's `destroy` method is invoked
* @return The handle object
*/
export function createHandle(destructor: () => void): Handle {
let called = false;
return {
destroy: function(this: Handle) {
if (!called) {
called = true;
destructor();
}
}
};
}
/**
* Returns a single handle that can be used to destroy multiple handles simultaneously.
*
* @param handles An array of handles with `destroy` methods
* @return The handle object
*/
export function createCompositeHandle(...handles: Handle[]): Handle {
return createHandle(function() {
for (let i = 0; i < handles.length; i++) {
handles[i].destroy();
}
});
}

110
src/lib/core/src/load.ts Normal file
View File

@ -0,0 +1,110 @@
import Promise from '@dojo/shim/Promise';
import { AmdRequire, AmdDefine, NodeRequire } from './interfaces';
import { isPlugin, useDefault } from './load/util';
export type Require = AmdRequire | NodeRequire;
export interface Load {
(require: Require, ...moduleIds: string[]): Promise<any[]>;
(...moduleIds: string[]): Promise<any[]>;
}
declare const require: Require;
declare const define: AmdDefine;
export function isAmdRequire(object: any): object is AmdRequire {
return typeof object.toUrl === 'function';
}
export function isNodeRequire(object: any): object is NodeRequire {
return typeof object.resolve === 'function';
}
const load: Load = (function(): Load {
const resolver = isAmdRequire(require)
? require.toUrl
: isNodeRequire(require) ? require.resolve : (resourceId: string) => resourceId;
function pluginLoad(moduleIds: string[], load: Load, loader: (modulesIds: string[]) => Promise<any>) {
const pluginResourceIds: string[] = [];
moduleIds = moduleIds.map((id: string, i: number) => {
const parts = id.split('!');
pluginResourceIds[i] = parts[1];
return parts[0];
});
return loader(moduleIds).then((modules: any[]) => {
pluginResourceIds.forEach((resourceId: string, i: number) => {
if (typeof resourceId === 'string') {
const module = modules[i];
const defaultExport = module['default'] || module;
if (isPlugin(defaultExport)) {
resourceId =
typeof defaultExport.normalize === 'function'
? defaultExport.normalize(resourceId, resolver)
: resolver(resourceId);
modules[i] = defaultExport.load(resourceId, load);
}
}
});
return Promise.all(modules);
});
}
if (typeof module === 'object' && typeof module.exports === 'object') {
return function load(contextualRequire: any, ...moduleIds: string[]): Promise<any[]> {
if (typeof contextualRequire === 'string') {
moduleIds.unshift(contextualRequire);
contextualRequire = require;
}
return pluginLoad(moduleIds, load, (moduleIds: string[]) => {
try {
return Promise.resolve(
moduleIds.map(function(moduleId): any {
return contextualRequire(moduleId.split('!')[0]);
})
);
} catch (error) {
return Promise.reject(error);
}
});
};
} else if (typeof define === 'function' && define.amd) {
return function load(contextualRequire: any, ...moduleIds: string[]): Promise<any[]> {
if (typeof contextualRequire === 'string') {
moduleIds.unshift(contextualRequire);
contextualRequire = require;
}
return pluginLoad(moduleIds, load, (moduleIds: string[]) => {
return new Promise(function(resolve, reject) {
let errorHandle: { remove: () => void };
if (typeof contextualRequire.on === 'function') {
errorHandle = contextualRequire.on('error', (error: Error) => {
errorHandle.remove();
reject(error);
});
}
contextualRequire(moduleIds, function(...modules: any[]) {
errorHandle && errorHandle.remove();
resolve(modules);
});
});
});
};
} else {
return function() {
return Promise.reject(new Error('Unknown loader'));
};
}
})();
export default load;
export { isPlugin, useDefault };

View File

@ -0,0 +1,59 @@
import { isArrayLike, isIterable } from '@dojo/shim/iterator';
import { Load } from '../load';
export interface LoadPlugin<T> {
/**
* An optional method that normmalizes a resource id.
*
* @param resourceId
* The raw resource id.
*
* @param resolver
* A method that can resolve an id to an absolute path. Depending on the environment, this will
* usually be either `require.toUrl` or `require.resolve`.
*/
normalize?: (resourceId: string, resolver: (resourceId: string) => string) => string;
/**
* A method that loads the specified resource.
*
* @param resourceId
* The id of the resource to load.
*
* @param load
* The `load` method that was used to load and execute the plugin.
*
* @return
* A promise that resolves to the loaded resource.
*/
load(resourceId: string, load: Load): Promise<T>;
}
export function isPlugin(value: any): value is LoadPlugin<any> {
return Boolean(value) && typeof value.load === 'function';
}
export function useDefault(modules: any[]): any[];
export function useDefault(module: any): any;
export function useDefault(modules: any | any[]): any[] | any {
if (isArrayLike(modules)) {
let processedModules: any[] = [];
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
processedModules.push(module.__esModule && module.default ? module.default : module);
}
return processedModules;
} else if (isIterable(modules)) {
let processedModules: any[] = [];
for (const module of modules) {
processedModules.push(module.__esModule && module.default ? module.default : module);
}
return processedModules;
} else {
return modules.__esModule && modules.default ? modules.default : modules;
}
}

View File

@ -0,0 +1,146 @@
import Promise from '@dojo/shim/Promise';
import { isPlugin, useDefault } from './util';
interface ModuleIdMap {
[path: string]: { id: number; lazy: boolean };
}
interface BundleLoaderCallback<T> {
(value: T): void;
}
interface WebpackRequire<T> {
(id: number): T | BundleLoaderCallback<T>;
}
/**
* A global map (set by the build) of resolved module paths to webpack-specific module data.
*/
declare const __modules__: ModuleIdMap;
/**
* The webpack-specific require function, set globally by webpack.
*/
declare const __webpack_require__: WebpackRequire<any>;
/**
* @private
* Resolves an absolute path from an absolute base path and relative module ID.
*
* @param base
* The absolute base path.
*
* @param mid
* The relative module ID
*
* @return
* The resolved absolute module path.
*/
function resolveRelative(base: string, mid: string): string {
const isRelative = mid.match(/^\.+\//);
let result = base;
if (isRelative) {
if (mid.match(/^\.\//)) {
mid = mid.replace(/\.\//, '');
}
const up = mid.match(/\.\.\//g);
if (up) {
const chunks = base.split('/');
if (up.length > chunks.length) {
throw new Error('Path cannot go beyond root directory.');
}
chunks.splice(chunks.length - up.length);
result = chunks.join('/');
mid = mid.replace(/\.\.\//g, '');
}
mid = result + '/' + mid;
}
return mid;
}
/**
* @private
* Returns the parent directory for the specified module ID.
*
* @param context
* A function that returns the context module ID.
*
* @return
* The parent directory of the path returned by the context function.
*/
function getBasePath(context: () => string): string {
return context()
.split('/')
.slice(0, -1)
.join('/');
}
/**
* A webpack-specific function that replaces `@dojo/core/load` in its builds. In order for a module to be loaded,
* it must first be included in a webpack chunk, whether that chunk is included in the main build, or lazy-loaded.
* Note that this module is not intended for direct use, but rather is intended for use by a webpack plugin
* that sets the module ID map used to translate resolved module paths to webpack module IDs.
*
* @param contextRequire
* An optional function that returns the base path to use when resolving relative module IDs.
*
* @param ...mids
* One or more IDs for modules to load.
*
* @return
* A promise to the loaded module values.
*/
export default function load(contextRequire: () => string, ...mids: string[]): Promise<any[]>;
export default function load(...mids: string[]): Promise<any[]>;
export default function load(...args: any[]): Promise<any[]> {
const req = __webpack_require__;
const context =
typeof args[0] === 'function'
? args[0]
: function() {
return '';
};
const modules = __modules__ || {};
const base = getBasePath(context);
const results = args
.filter((mid: string | Function) => typeof mid === 'string')
.map((mid: string) => resolveRelative(base, mid))
.map((mid: string) => {
let [moduleId, pluginResourceId] = mid.split('!');
const moduleMeta = modules[mid] || modules[moduleId];
if (!moduleMeta) {
return Promise.reject(new Error(`Missing module: ${mid}`));
}
if (moduleMeta.lazy) {
return new Promise((resolve) => req(moduleMeta.id)(resolve));
}
const module = req(moduleMeta.id);
const defaultExport = module['default'] || module;
if (isPlugin(defaultExport)) {
pluginResourceId =
typeof defaultExport.normalize === 'function'
? defaultExport.normalize(pluginResourceId, (mid: string) => resolveRelative(base, mid))
: resolveRelative(base, pluginResourceId);
return Promise.resolve(defaultExport.load(pluginResourceId, <any>load));
}
return Promise.resolve(module);
});
return Promise.all(results);
}
export { isPlugin, useDefault };

47
src/lib/core/src/main.ts Normal file
View File

@ -0,0 +1,47 @@
import * as aspect from './aspect';
import DateObject from './DateObject';
import Evented from './Evented';
import IdentityRegistry from './IdentityRegistry';
import * as lang from './lang';
import * as base64 from './base64';
import load from './load';
import MatchRegistry from './MatchRegistry';
import on, { emit } from './on';
import * as queue from './queue';
import request from './request';
import Scheduler from './Scheduler';
import * as stringExtras from './stringExtras';
import * as text from './text';
import UrlSearchParams from './UrlSearchParams';
import * as util from './util';
import * as iteration from './async/iteration';
import Task from './async/Task';
import * as timing from './async/timing';
const async = {
iteration,
Task,
timing
};
export {
aspect,
async,
base64,
DateObject,
emit,
Evented,
IdentityRegistry,
lang,
load,
MatchRegistry,
on,
queue,
request,
Scheduler,
stringExtras,
text,
UrlSearchParams,
util
};

230
src/lib/core/src/on.ts Normal file
View File

@ -0,0 +1,230 @@
import { Handle, EventObject } from './interfaces';
import { createHandle, createCompositeHandle } from './lang';
import Evented, { CustomEventTypes } from './Evented';
export interface EventCallback<O = EventObject<string>> {
(event: O): void;
}
export interface EventEmitter {
on(event: string, listener: EventCallback): EventEmitter;
removeListener(event: string, listener: EventCallback): EventEmitter;
}
interface DOMEventObject extends EventObject {
bubbles: boolean;
cancelable: boolean;
}
/**
* Provides a normalized mechanism for dispatching events for event emitters, Evented objects, or DOM nodes.
* @param target The target to emit the event from
* @param event The event object to emit
* @return Boolean indicating if preventDefault was called on the event object (only relevant for DOM events;
* always false for other event emitters)
*/
export function emit<
M extends CustomEventTypes,
T,
O extends EventObject<T> = EventObject<T>,
K extends keyof M = keyof M
>(target: Evented<M, T, O>, event: M[K]): boolean;
export function emit<T, O extends EventObject<T> = EventObject<T>>(target: Evented<any, T, O>, event: O): boolean;
export function emit<O extends EventObject<string> = EventObject<string>>(
target: EventTarget | EventEmitter,
event: O
): boolean;
export function emit(target: any, event: EventObject<any>): boolean {
if (
target.dispatchEvent /* includes window and document */ &&
((target.ownerDocument && target.ownerDocument.createEvent) /* matches nodes */ ||
(target.document && target.document.createEvent) /* matches window */ ||
target.createEvent) /* matches document */
) {
const nativeEvent = (target.ownerDocument || target.document || target).createEvent('HTMLEvents');
nativeEvent.initEvent(
event.type,
Boolean((<DOMEventObject>event).bubbles),
Boolean((<DOMEventObject>event).cancelable)
);
for (let key in event) {
if (!(key in nativeEvent)) {
nativeEvent[key] = (<any>event)[key];
}
}
return target.dispatchEvent(nativeEvent);
}
if (target.emit) {
if (target.removeListener) {
// Node.js EventEmitter
target.emit(event.type, event);
return false;
} else if (target.on) {
// Dojo Evented or similar
target.emit(event);
return false;
}
}
throw new Error('Target must be an event emitter');
}
/**
* Provides a normalized mechanism for listening to events from event emitters, Evented objects, or DOM nodes.
* @param target Target to listen for event on
* @param type Event event type(s) to listen for; may a string or an array of strings
* @param listener Callback to handle the event when it fires
* @param capture Whether the listener should be registered in the capture phase (DOM events only)
* @return A handle which will remove the listener when destroy is called
*/
export default function on<
M extends CustomEventTypes,
T,
K extends keyof M = keyof M,
O extends EventObject<T> = EventObject<T>
>(target: Evented<M, T, O>, type: K | K[], listener: EventCallback<M[K]>): Handle;
export default function on<T, O extends EventObject<T> = EventObject<T>>(
target: Evented<any, T, O>,
type: T | T[],
listener: EventCallback<O>
): Handle;
export default function on(target: EventEmitter, type: string | string[], listener: EventCallback): Handle;
export default function on(
target: EventTarget,
type: string | string[],
listener: EventCallback,
capture?: boolean
): Handle;
export default function on(target: any, type: any, listener: any, capture?: boolean): Handle {
if (Array.isArray(type)) {
let handles: Handle[] = type.map(function(type: string): Handle {
return on(target, type, listener, capture);
});
return createCompositeHandle(...handles);
}
const callback = function(this: any) {
listener.apply(this, arguments);
};
// DOM EventTarget
if (target.addEventListener && target.removeEventListener) {
target.addEventListener(type, callback, capture);
return createHandle(function() {
target.removeEventListener(type, callback, capture);
});
}
if (target.on) {
// EventEmitter
if (target.removeListener) {
target.on(type, callback);
return createHandle(function() {
target.removeListener(type, callback);
});
} else if (target.emit) {
// Evented
return target.on(type, listener);
}
}
throw new TypeError('Unknown event emitter object');
}
/**
* Provides a mechanism for listening to the next occurrence of an event from event
* emitters, Evented objects, or DOM nodes.
* @param target Target to listen for event on
* @param type Event event type(s) to listen for; may be a string or an array of strings
* @param listener Callback to handle the event when it fires
* @param capture Whether the listener should be registered in the capture phase (DOM events only)
* @return A handle which will remove the listener when destroy is called
*/
export function once<
M extends CustomEventTypes,
T,
K extends keyof M = keyof M,
O extends EventObject<T> = EventObject<T>
>(target: Evented<M, T, O>, type: K | K[], listener: EventCallback<M[K]>): Handle;
export function once<T, O extends EventObject<T> = EventObject<T>>(
target: Evented<any, T, O>,
type: T | T[],
listener: EventCallback<O>
): Handle;
export function once(target: EventTarget, type: string | string[], listener: EventCallback, capture?: boolean): Handle;
export function once(target: EventEmitter, type: string | string[], listener: EventCallback): Handle;
export function once(target: any, type: any, listener: any, capture?: boolean): Handle {
// FIXME
// tslint:disable-next-line:no-var-keyword
var handle = on(
target,
type,
function(this: any) {
handle.destroy();
return listener.apply(this, arguments);
},
capture
);
return handle;
}
export interface PausableHandle extends Handle {
pause(): void;
resume(): void;
}
/**
* Provides a mechanism for creating pausable listeners for events from event emitters, Evented objects, or DOM nodes.
* @param target Target to listen for event on
* @param type Event event type(s) to listen for; may a string or an array of strings
* @param listener Callback to handle the event when it fires
* @param capture Whether the listener should be registered in the capture phase (DOM events only)
* @return A handle with additional pause and resume methods; the listener will never fire when paused
*/
export function pausable<
M extends CustomEventTypes,
T,
K extends keyof M = keyof M,
O extends EventObject<T> = EventObject<T>
>(target: Evented<M, T, O>, type: K | K[], listener: EventCallback<M[K]>): PausableHandle;
export function pausable<T, O extends EventObject<T> = EventObject<T>>(
target: Evented<any, T, O>,
type: T | T[],
listener: EventCallback<O>
): PausableHandle;
export function pausable(
target: EventTarget,
type: string | string[],
listener: EventCallback,
capture?: boolean
): PausableHandle;
export function pausable(target: EventEmitter, type: string | string[], listener: EventCallback): PausableHandle;
export function pausable(target: any, type: any, listener: any, capture?: boolean): PausableHandle {
let paused: boolean;
const handle = <PausableHandle>on(
target,
type,
function(this: any) {
if (!paused) {
return listener.apply(this, arguments);
}
},
capture
);
handle.pause = function() {
paused = true;
};
handle.resume = function() {
paused = false;
};
return handle;
}

View File

@ -0,0 +1 @@
export * from '@dojo/shim/support/queue';

View File

@ -0,0 +1,66 @@
import has from '@dojo/has/has';
import Task from './async/Task';
import { RequestOptions, Response, Provider, UploadObservableTask } from './request/interfaces';
import ProviderRegistry from './request/ProviderRegistry';
import xhr from './request/providers/xhr';
export const providerRegistry = new ProviderRegistry();
const request: {
(url: string, options?: RequestOptions): Task<Response>;
delete(url: string, options?: RequestOptions): Task<Response>;
get(url: string, options?: RequestOptions): Task<Response>;
head(url: string, options?: RequestOptions): Task<Response>;
options(url: string, options?: RequestOptions): Task<Response>;
post(url: string, options?: RequestOptions): UploadObservableTask<Response>;
put(url: string, options?: RequestOptions): UploadObservableTask<Response>;
setDefaultProvider(provider: Provider): void;
} = <any>function request(url: string, options: RequestOptions = {}): Task<Response> {
try {
return providerRegistry.match(url, options)(url, options);
} catch (error) {
return Task.reject<Response>(error);
}
};
['DELETE', 'GET', 'HEAD', 'OPTIONS'].forEach((method) => {
Object.defineProperty(request, method.toLowerCase(), {
value(url: string, options: RequestOptions = {}): Task<Response> {
options = Object.create(options);
options.method = method;
return request(url, options);
}
});
});
['POST', 'PUT'].forEach((method) => {
Object.defineProperty(request, method.toLowerCase(), {
value(url: string, options: RequestOptions = {}): UploadObservableTask<Response> {
options = Object.create(options);
options.method = method;
return <UploadObservableTask<Response>>request(url, options);
}
});
});
Object.defineProperty(request, 'setDefaultProvider', {
value(provider: Provider) {
providerRegistry.setDefaultProvider(provider);
}
});
providerRegistry.setDefaultProvider(xhr);
if (has('host-node')) {
// tslint:disable-next-line
import('./request/providers/node').then((node) => {
providerRegistry.setDefaultProvider(node.default);
});
}
export default request;
export * from './request/interfaces';
export { default as Headers } from './request/Headers';
export { default as TimeoutError } from './request/TimeoutError';
export { ResponseData } from './request/Response';

View File

@ -0,0 +1,101 @@
import { Headers as HeadersInterface } from './interfaces';
import { IterableIterator, ShimIterator } from '@dojo/shim/iterator';
import Map from '@dojo/shim/Map';
function isHeadersLike(object: any): object is HeadersInterface {
return (
typeof object.append === 'function' &&
typeof object.entries === 'function' &&
typeof object[Symbol.iterator] === 'function'
);
}
export class Headers implements HeadersInterface {
protected map = new Map<string, string[]>();
constructor(headers?: { [key: string]: string } | HeadersInterface) {
if (headers) {
if (headers instanceof Headers) {
this.map = new Map(headers.map);
} else if (isHeadersLike(headers)) {
for (const [key, value] of headers) {
this.append(key, value);
}
} else {
for (let key in headers) {
this.set(key, headers[key]);
}
}
}
}
append(name: string, value: string) {
const values = this.map.get(name.toLowerCase());
if (values) {
values.push(value);
} else {
this.set(name, value);
}
}
delete(name: string) {
this.map.delete(name.toLowerCase());
}
entries(): IterableIterator<[string, string]> {
const entries: [string, string][] = [];
for (const [key, values] of this.map.entries()) {
values.forEach((value) => {
entries.push([key, value]);
});
}
return new ShimIterator(entries);
}
get(name: string): string | null {
const values = this.map.get(name.toLowerCase());
if (values) {
return values[0];
} else {
return null;
}
}
getAll(name: string): string[] {
const values = this.map.get(name.toLowerCase());
if (values) {
return values.slice(0);
} else {
return [];
}
}
has(name: string): boolean {
return this.map.has(name.toLowerCase());
}
keys(): IterableIterator<string> {
return this.map.keys();
}
set(name: string, value: string) {
this.map.set(name.toLowerCase(), [value]);
}
values(): IterableIterator<string> {
const values: string[] = [];
for (const value of this.map.values()) {
values.push(...value);
}
return new ShimIterator(values);
}
[Symbol.iterator]() {
return this.entries();
}
}
export default Headers;

View File

@ -0,0 +1,27 @@
import { Provider, ProviderTest } from './interfaces';
import MatchRegistry, { Test } from '../MatchRegistry';
import { Handle } from '../interfaces';
export class ProviderRegistry extends MatchRegistry<Provider> {
setDefaultProvider(provider: Provider) {
this._defaultValue = provider;
}
register(test: string | RegExp | ProviderTest | null, value: Provider, first?: boolean): Handle {
let entryTest: Test | null;
if (typeof test === 'string') {
entryTest = (url, options) => test === url;
} else if (test instanceof RegExp) {
entryTest = (url, options) => {
return test ? test.test(url) : null;
};
} else {
entryTest = test;
}
return super.register(entryTest, value, first);
}
}
export default ProviderRegistry;

View File

@ -0,0 +1,75 @@
## Features
This module allows the consumer to make HTTP requests using Node, XMLHttpRequest, or the Fetch API. The details of
these implementations are exposed through a common interface.
With this module you can,
* Easily make simple HTTP requests
* Convert response bodies to common formats like text, json, or html
* Access response headers of a request before the body is downloaded
* Monitor progress of a request
* Stream response data
## How do I use this package?
### Quick Start
To make simple GET requests, you must register your provider (node, or XHR) then make the request. The overall
format of the API resembles the [Fetch Standard](https://fetch.spec.whatwg.org/).
```ts
import request from 'dojo-request';
import node from 'dojo-request/providers/node';
request.setDefaultProvider(node);
request.get('http://www.example.com').then(response => {
return response.text();
}).then(html => {
console.log(html);
});
```
Responses can be returned as an `ArrayBuffer`, `Blob`, XML document, JSON object, or text string.
You can also easily send request data,
```ts
request.post('http://www.example.com', {
body: 'request data here'
}).then(response => {
// ...
});
```
## Advanced Usage
### Reading Response Headers
This approach allows for processing of response headers _before_ the response body is available.
```ts
request.get('http://www.example.com').then(response => {
const expectedSize = Number(response.headers.get('content-length') || 0);
});
```
### Response Events
`Response` objects also emit `start`, `end`, `progress`, and `data` events. You can use these events to monitor download progress
or stream a response directly to a file.
```ts
request.get('http://www.example.com').then(response => {
response.on('progress', (event: ProgressEvent) => {
console.log(`Total downloaded: ${event.totalBytesDownloaded}`);
});
return response.blob();
}).then(blob => {
// do something with the data
});
```
Note that there are some caveats when using these events. XHR cannot stream data (a final `data` event is sent at the end however).

View File

@ -0,0 +1,69 @@
import Promise from '@dojo/shim/Promise';
import Task from '../async/Task';
import { Headers, Response as ResponseInterface, RequestOptions } from './interfaces';
import Observable from '../Observable';
export interface ResponseData {
task: Task<any>;
used: boolean;
}
abstract class Response implements ResponseInterface {
abstract readonly headers: Headers;
abstract readonly ok: boolean;
abstract readonly status: number;
abstract readonly statusText: string;
abstract readonly url: string;
abstract readonly bodyUsed: boolean;
abstract readonly requestOptions: RequestOptions;
abstract readonly download: Observable<number>;
abstract readonly data: Observable<any>;
json<T>(): Task<T> {
return <any>this.text().then(JSON.parse);
}
abstract arrayBuffer(): Task<ArrayBuffer>;
abstract blob(): Task<Blob>;
abstract formData(): Task<FormData>;
abstract text(): Task<string>;
}
export default Response;
export function getFileReaderPromise<T>(reader: FileReader): Promise<T> {
return new Promise((resolve, reject) => {
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function() {
reject(reader.error);
};
});
}
export function getTextFromBlob(blob: Blob) {
const reader = new FileReader();
const promise = getFileReaderPromise<string>(reader);
reader.readAsText(blob);
return promise;
}
export function getArrayBufferFromBlob(blob: Blob) {
const reader = new FileReader();
const promise = getFileReaderPromise<ArrayBuffer>(reader);
reader.readAsArrayBuffer(blob);
return promise;
}
export function getTextFromArrayBuffer(buffer: ArrayBuffer) {
const view = new Uint8Array(buffer);
const chars: string[] = [];
view.forEach((charCode, index) => {
chars[index] = String.fromCharCode(charCode);
});
return chars.join('');
}

View File

@ -0,0 +1,46 @@
import { SubscriptionObserver } from '@dojo/shim/Observable';
export class SubscriptionPool<T> {
private _observers: SubscriptionObserver<T>[] = [];
private _queue: T[] = [];
private _queueMaxLength: number;
constructor(maxLength = 10) {
this._queueMaxLength = maxLength;
}
add(subscription: SubscriptionObserver<T>) {
this._observers.push(subscription);
while (this._queue.length > 0) {
this.next(this._queue.shift()!);
}
return () => {
this._observers.splice(this._observers.indexOf(subscription), 1);
};
}
next(value: T) {
if (this._observers.length === 0) {
this._queue.push(value);
// when the queue is full, get rid of the first ones
while (this._queue.length > this._queueMaxLength) {
this._queue.shift();
}
}
this._observers.forEach((observer) => {
observer.next(value);
});
}
complete() {
this._observers.forEach((observer) => {
observer.complete();
});
}
}
export default SubscriptionPool;

View File

@ -0,0 +1,14 @@
export class TimeoutError implements Error {
readonly message: string;
get name(): string {
return 'TimeoutError';
}
constructor(message?: string) {
message = message || 'The request timed out';
this.message = message;
}
}
export default TimeoutError;

View File

@ -0,0 +1,84 @@
import { IterableIterator } from '@dojo/shim/iterator';
import Task from '../async/Task';
import UrlSearchParams, { ParamList } from '../UrlSearchParams';
import Observable from '../Observable';
export interface Body {
readonly bodyUsed: boolean;
arrayBuffer(): Task<ArrayBuffer>;
blob(): Task<Blob>;
formData(): Task<FormData>;
json<T>(): Task<T>;
text(): Task<string>;
}
export interface Headers {
append(name: string, value: string): void;
delete(name: string): void;
entries(): IterableIterator<[string, string]>;
get(name: string): string | null;
getAll(name: string): string[];
has(name: string): boolean;
keys(): IterableIterator<string>;
set(name: string, value: string): void;
values(): IterableIterator<string>;
[Symbol.iterator](): IterableIterator<[string, string]>;
}
export interface UploadObservableTask<T> extends Task<T> {
upload: Observable<number>;
}
export type Provider = (url: string, options?: RequestOptions) => UploadObservableTask<Response>;
export type ProviderTest = (url: string, options?: RequestOptions) => boolean | null;
export interface RequestOptions {
/**
* Enable cache busting (default false). Cache busting will make a new URL by appending a parameter to the
* requested URL
*/
cacheBust?: boolean;
credentials?: 'omit' | 'same-origin' | 'include';
/**
* Body to send along with the http request
*/
body?: Blob | BufferSource | FormData | UrlSearchParams | string;
/**
* Headers to send along with the http request
*/
headers?: Headers | { [key: string]: string };
/**
* HTTP method
*/
method?: string;
/**
* Password for HTTP authentication
*/
password?: string;
/**
* Number of milliseconds before the request times out and is canceled
*/
timeout?: number;
/**
* User for HTTP authentication
*/
user?: string;
/**
* Optional query parameter(s) for the URL. The requested url will have these query parameters appended.
*/
query?: string | ParamList;
}
export interface Response extends Body {
readonly headers: Headers;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
readonly requestOptions: RequestOptions;
readonly download: Observable<number>;
readonly data: Observable<any>;
}

View File

@ -0,0 +1,677 @@
import { Handle } from '../../interfaces';
import Set from '@dojo/shim/Set';
import WeakMap from '@dojo/shim/WeakMap';
import * as http from 'http';
import * as https from 'https';
import * as urlUtil from 'url';
import * as zlib from 'zlib';
import Task from '../../async/Task';
import { deepAssign } from '../../lang';
import { queueTask } from '../../queue';
import { createTimer } from '../../util';
import Headers from '../Headers';
import { RequestOptions, UploadObservableTask } from '../interfaces';
import Response from '../Response';
import TimeoutError from '../TimeoutError';
import { Readable } from 'stream';
import Observable from '../../Observable';
import SubscriptionPool from '../SubscriptionPool';
/**
* Request options specific to a node request. For HTTPS options, see
* https://nodejs.org/api/tls.html#tls_tls_connect_options_callback for more details.
*/
export interface NodeRequestOptions extends RequestOptions {
/**
* User-agent header
*/
agent?: any;
/**
* If specified, the request body is read from the stream specified here, rather than from the `body` field.
*/
bodyStream?: Readable;
/**
* HTTPS optionally override the trusted CA certificates
*/
ca?: any;
/**
* HTTPS optional cert chains in PEM format. One cert chain should be provided per private key.
*/
cert?: string;
/**
* HTTPS optional cipher suite specification
*/
ciphers?: string;
dataEncoding?: string;
/**
* Whether or not to automatically follow redirects (default true)
*/
followRedirects?: boolean;
/**
* HTTPS optional private key in PEM format.
*/
key?: string;
/**
* Local interface to bind for network connections.
*/
localAddress?: string;
/**
* HTTPS optional shared passphrase used for a single private key and/or a PFX.
*/
passphrase?: string;
/**
* HTTPS optional PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: any;
/**
* Optional proxy address. If specified, requests will be sent through this url.
*/
proxy?: string;
/**
* HTTPS If not false the server will reject any connection which is not authorized with the list of supplied CAs
*/
rejectUnauthorized?: boolean;
/**
* HTTPS optional SSL method to use, default is "SSLv23_method"
*/
secureProtocol?: string;
/**
* Unix Domain Socket (use one of host:port or socketPath)
*/
socketPath?: string;
/**
* Whether or not to add the gzip and deflate accept headers (default true)
*/
acceptCompression?: boolean;
/**
* A set of options to set on the HTTP request
*/
socketOptions?: {
/**
* Enable/disable keep-alive functionality, and optionally set the initial delay before the first keepalive probe is sent on an idle socket.
*/
keepAlive?: number;
/**
* Disables the Nagle algorithm. By default TCP connections use the Nagle algorithm, they buffer data before sending it off.
*/
noDelay?: boolean;
/**
* Number of milliseconds before the HTTP request times out
*/
timeout?: number;
};
/**
* Stream encoding on incoming HTTP response
*/
streamEncoding?: string;
/**
* Options to control redirect follow logic
*/
redirectOptions?: {
/**
* The limit to the number of redirects that will be followed (default 15). This is used to prevent infinite
* redirect loops.
*/
limit?: number;
count?: number;
/**
* Whether or not to keep the original HTTP method during 301 redirects (default false).
*/
keepOriginalMethod?: boolean;
};
}
// TODO: This should be read from the package and not hard coded!
let version = '2.0.0-pre';
/**
* If not overridden, redirects will only be processed this many times before aborting (per request).
* @type {number}
*/
const DEFAULT_REDIRECT_LIMIT = 15;
/**
* Options to be passed to node's request
*/
interface Options {
agent?: any;
auth?: string;
headers?: { [name: string]: string };
host?: string;
hostname?: string;
localAddress?: string;
method?: string;
path?: string;
port?: number;
socketPath?: string;
}
/**
* HTTPS specific options for node
*/
interface HttpsOptions extends Options {
ca?: any;
cert?: string;
ciphers?: string;
key?: string;
passphrase?: string;
pfx?: any;
rejectUnauthorized?: boolean;
secureProtocol?: string;
}
interface RequestData {
task: Task<http.IncomingMessage>;
buffer: any[];
data: Buffer;
size: number;
used: boolean;
nativeResponse: http.IncomingMessage;
requestOptions: NodeRequestOptions;
url: string;
downloadObservable: Observable<number>;
dataObservable: Observable<any>;
}
const dataMap = new WeakMap<NodeResponse, RequestData>();
const discardedDuplicates = new Set<string>([
'age',
'authorization',
'content-length',
'content-type',
'etag',
'expires',
'from',
'host',
'if-modified-since',
'if-unmodified-since',
'last-modified',
'location',
'max-forwards',
'proxy-authorization',
'referer',
'retry-after',
'user-agent'
]);
function getDataTask(response: NodeResponse): Task<RequestData> {
const data = dataMap.get(response)!;
if (data.used) {
return Task.reject<any>(new TypeError('Body already read'));
}
data.used = true;
return <Task<RequestData>>data.task.then((_) => data);
}
/**
* Turn a node native response object into something that resembles the fetch api
*/
export class NodeResponse extends Response {
readonly headers: Headers;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
downloadBody = true;
get bodyUsed(): boolean {
return dataMap.get(this)!.used;
}
get nativeResponse(): http.IncomingMessage {
return dataMap.get(this)!.nativeResponse;
}
get requestOptions(): NodeRequestOptions {
return dataMap.get(this)!.requestOptions;
}
get url(): string {
return dataMap.get(this)!.url;
}
get download(): Observable<number> {
return dataMap.get(this)!.downloadObservable;
}
get data(): Observable<any> {
return dataMap.get(this)!.dataObservable;
}
constructor(response: http.IncomingMessage) {
super();
const headers = (this.headers = new Headers());
for (let key in response.headers) {
const value = response.headers[key];
if (value) {
if (discardedDuplicates.has(key) && !Array.isArray(value)) {
headers.append(key, value);
}
(Array.isArray(value) ? value : value.split(/\s*,\s*/)).forEach((v) => {
headers.append(key, v);
});
}
}
this.status = response.statusCode || 0;
this.ok = this.status >= 200 && this.status < 300;
this.statusText = response.statusMessage || '';
}
arrayBuffer(): Task<ArrayBuffer> {
return <any>getDataTask(this).then((data) => {
if (data) {
return data.data;
}
return new Buffer([]);
});
}
blob(): Task<Blob> {
// Node doesn't support Blobs
return Task.reject<Blob>(new Error('Blob not supported'));
}
formData(): Task<FormData> {
return Task.reject<FormData>(new Error('FormData not supported'));
}
text(): Task<string> {
return <any>getDataTask(this).then((data) => {
return String(data ? data.data : '');
});
}
}
function redirect(
resolve: (p?: any) => void,
reject: (_?: Error) => void,
originalUrl: string,
redirectUrl: string | null,
options: NodeRequestOptions
): boolean {
if (!options.redirectOptions) {
options.redirectOptions = {};
}
const { limit: redirectLimit = DEFAULT_REDIRECT_LIMIT, count: redirectCount = 0 } = options.redirectOptions;
const { followRedirects = true } = options;
if (!followRedirects) {
return false;
}
// we only check for undefined here because empty string redirects are now allowed
// (they'll resolve to the current url)
if (redirectUrl === undefined || redirectUrl === null) {
reject(new Error('asked to redirect but no location header was found'));
return true;
}
if (redirectCount > redirectLimit) {
reject(new Error(`too many redirects, limit reached at ${redirectLimit}`));
return true;
}
options.redirectOptions.count = redirectCount + 1;
// we wrap the url in a call to node's URL.resolve which will handle relative and partial
// redirects (like "/another-page" on the same domain).
resolve(node(urlUtil.resolve(originalUrl, redirectUrl), options));
return true;
}
export function getAuth(proxyAuth: string | undefined, options: NodeRequestOptions): string | undefined {
if (proxyAuth) {
return proxyAuth;
}
if (options.user || options.password) {
return `${options.user || ''}:${options.password || ''}`;
}
return undefined;
}
export default function node(url: string, options: NodeRequestOptions = {}): UploadObservableTask<NodeResponse> {
const parsedUrl = urlUtil.parse(options.proxy || url);
const requestOptions: HttpsOptions = {
agent: options.agent,
auth: getAuth(parsedUrl.auth, options),
ca: options.ca,
cert: options.cert,
ciphers: options.ciphers,
host: parsedUrl.host,
hostname: parsedUrl.hostname,
key: options.key,
localAddress: options.localAddress,
method: options.method ? options.method.toUpperCase() : 'GET',
passphrase: options.passphrase,
path: parsedUrl.path,
pfx: options.pfx,
port: Number(parsedUrl.port),
rejectUnauthorized: options.rejectUnauthorized,
secureProtocol: options.secureProtocol,
socketPath: options.socketPath
};
requestOptions.headers = <{ [key: string]: string }>options.headers || {};
if (
!Object.keys(requestOptions.headers)
.map((headerName) => headerName.toLowerCase())
.some((headerName) => headerName === 'user-agent')
) {
requestOptions.headers['user-agent'] = 'dojo/' + version + ' Node.js/' + process.version.replace(/^v/, '');
}
if (options.proxy) {
requestOptions.path = url;
if (parsedUrl.auth) {
requestOptions.headers['proxy-authorization'] = 'Basic ' + new Buffer(parsedUrl.auth).toString('base64');
}
const parsedProxyUrl = urlUtil.parse(url);
if (parsedProxyUrl.host) {
requestOptions.headers['host'] = parsedProxyUrl.host;
}
if (parsedProxyUrl.auth) {
requestOptions.auth = parsedProxyUrl.auth;
}
}
const { acceptCompression = true } = options;
if (acceptCompression) {
requestOptions.headers['Accept-Encoding'] = 'gzip, deflate';
}
const request = parsedUrl.protocol === 'https:' ? https.request(requestOptions) : http.request(requestOptions);
const uploadObserverPool = new SubscriptionPool<number>();
const requestTask = <UploadObservableTask<NodeResponse>>new Task<NodeResponse>(
(resolve, reject) => {
let timeoutHandle: Handle;
let timeoutReject: Function = reject;
if (options.socketOptions) {
if (options.socketOptions.timeout) {
request.setTimeout(options.socketOptions.timeout);
}
if ('noDelay' in options.socketOptions) {
request.setNoDelay(options.socketOptions.noDelay);
}
if ('keepAlive' in options.socketOptions) {
const initialDelay: number | undefined = options.socketOptions.keepAlive;
if (initialDelay !== undefined) {
request.setSocketKeepAlive(initialDelay >= 0, initialDelay);
}
}
}
request.once('response', (message: http.IncomingMessage) => {
const response = new NodeResponse(message);
// Redirection handling defaults to true in order to harmonise with the XHR provider, which will always
// follow redirects
if (response.status >= 300 && response.status < 400) {
const redirectOptions = options.redirectOptions || {};
const newOptions = deepAssign({}, options);
switch (response.status) {
case 300:
/**
* Note about 300 redirects. RFC 2616 doesn't specify what to do with them, it is up to the client to "pick
* the right one". We're picking like Chrome does, just don't pick any.
*/
break;
case 301:
case 302:
/**
* RFC 2616 says,
*
* If the 301 status code is received in response to a request other
* than GET or HEAD, the user agent MUST NOT automatically redirect the
* request unless it can be confirmed by the user, since this might
* change the conditions under which the request was issued.
*
* Note: When automatically redirecting a POST request after
* receiving a 301 status code, some existing HTTP/1.0 user agents
* will erroneously change it into a GET request.
*
* We're going to be one of those erroneous agents, to prevent the request from failing..
*/
if (
requestOptions.method !== 'GET' &&
requestOptions.method !== 'HEAD' &&
!redirectOptions.keepOriginalMethod
) {
newOptions.method = 'GET';
}
if (redirect(resolve, reject, url, response.headers.get('location'), newOptions)) {
return;
}
break;
case 303:
/**
* The response to the request can be found under a different URI and
* SHOULD be retrieved using a GET method on that resource.
*/
if (requestOptions.method !== 'GET') {
newOptions.method = 'GET';
}
if (redirect(resolve, reject, url, response.headers.get('location'), newOptions)) {
return;
}
break;
case 304:
// do nothing so this can fall through and return the response as normal. Nothing more can
// be done for 304
break;
case 305:
if (!response.headers.get('location')) {
reject(new Error('expected Location header to contain a proxy url'));
} else {
newOptions.proxy = response.headers.get('location') || '';
if (redirect(resolve, reject, url, '', newOptions)) {
return;
}
}
break;
case 307:
/**
* If the 307 status code is received in response to a request other
* than GET or HEAD, the user agent MUST NOT automatically redirect the
* request unless it can be confirmed by the user, since this might
* change the conditions under which the request was issued.
*/
if (redirect(resolve, reject, url, response.headers.get('location'), newOptions)) {
return;
}
break;
default:
reject(new Error('unhandled redirect status ' + response.status));
return;
}
}
options.streamEncoding && message.setEncoding(options.streamEncoding);
const downloadSubscriptionPool = new SubscriptionPool<number>();
const dataSubscriptionPool = new SubscriptionPool<any>();
/*
[RFC 2616](https://tools.ietf.org/html/rfc2616#page-118) says that content-encoding can have multiple
values, so we split them here and put them in a list to process later.
*/
const contentEncodings = response.headers.getAll('content-encoding');
const task = new Task<http.IncomingMessage>(
(resolve, reject) => {
timeoutReject = reject;
// we queue this up for later to allow listeners to register themselves before we start receiving data
queueTask(() => {
/*
* Note that this is the raw data, if your input stream is zipped, and you are piecing
* together the downloaded data, you'll have to decompress it yourself
*/
message.on('data', (chunk: any) => {
dataSubscriptionPool.next(chunk);
if (response.downloadBody) {
data.buffer.push(chunk);
}
data.size +=
typeof chunk === 'string'
? Buffer.byteLength(chunk, options.streamEncoding)
: chunk.length;
downloadSubscriptionPool.next(data.size);
});
message.once('end', () => {
timeoutHandle && timeoutHandle.destroy();
let dataAsBuffer = options.streamEncoding
? new Buffer(data.buffer.join(''), 'utf8')
: Buffer.concat(data.buffer, data.size);
const handleEncoding = function() {
/*
Content encoding is ordered by the order in which they were applied to the
content, so do undo the encoding we have to start at the end and work backwards.
*/
if (contentEncodings.length) {
const encoding = contentEncodings.pop()!.trim().toLowerCase();
if (encoding === '' || encoding === 'none' || encoding === 'identity') {
// do nothing, response stream is as-is
handleEncoding();
} else if (encoding === 'gzip') {
zlib.gunzip(dataAsBuffer, function(err: Error | null, result: Buffer) {
if (err) {
reject(err);
}
dataAsBuffer = result;
handleEncoding();
});
} else if (encoding === 'deflate') {
zlib.inflate(dataAsBuffer, function(err: Error | null, result: Buffer) {
if (err) {
reject(err);
}
dataAsBuffer = result;
handleEncoding();
});
} else {
reject(new Error('Unsupported content encoding, ' + encoding));
}
} else {
data.data = dataAsBuffer;
resolve(message);
}
};
handleEncoding();
});
});
},
() => {
request.abort();
}
);
const data: RequestData = {
task,
buffer: [],
data: Buffer.alloc(0),
size: 0,
used: false,
url: url,
requestOptions: options,
nativeResponse: message,
downloadObservable: new Observable<number>((observer) => downloadSubscriptionPool.add(observer)),
dataObservable: new Observable<any>((observer) => dataSubscriptionPool.add(observer))
};
dataMap.set(response, data);
resolve(response);
});
request.once('error', reject);
if (options.bodyStream) {
options.bodyStream.pipe(request);
let uploadedSize = 0;
options.bodyStream.on('data', (chunk: any) => {
uploadedSize += chunk.length;
uploadObserverPool.next(uploadedSize);
});
options.bodyStream.on('end', () => {
uploadObserverPool.complete();
request.end();
});
} else if (options.body) {
const body = options.body.toString();
request.on('response', () => {
uploadObserverPool.next(body.length);
});
request.end(body);
} else {
request.end();
}
if (options.timeout && options.timeout > 0 && options.timeout !== Infinity) {
timeoutHandle = createTimer(() => {
timeoutReject && timeoutReject(new TimeoutError('The request timed out'));
}, options.timeout);
}
},
() => {
request.abort();
}
).catch(function(error: Error): any {
const parsedUrl = urlUtil.parse(url);
if (parsedUrl.auth) {
parsedUrl.auth = '(redacted)';
}
const sanitizedUrl = urlUtil.format(parsedUrl);
error.message = '[' + requestOptions.method + ' ' + sanitizedUrl + '] ' + error.message;
throw error;
});
requestTask.upload = new Observable<number>((observer) => uploadObserverPool.add(observer));
return requestTask;
}

View File

@ -0,0 +1,333 @@
import { Handle } from '../../interfaces';
import global from '@dojo/shim/global';
import WeakMap from '@dojo/shim/WeakMap';
import Task, { State } from '../../async/Task';
import has from '../../has';
import Observable from '../../Observable';
import { createTimer } from '../../util';
import Headers from '../Headers';
import { RequestOptions, UploadObservableTask } from '../interfaces';
import Response, { getArrayBufferFromBlob, getTextFromBlob } from '../Response';
import SubscriptionPool from '../SubscriptionPool';
import TimeoutError from '../TimeoutError';
import { generateRequestUrl } from '../util';
/**
* Request options specific to an XHR request
*/
export interface XhrRequestOptions extends RequestOptions {
/**
* Controls whether or not the request is synchronous (blocks the main thread) or asynchronous (default).
*/
blockMainThread?: boolean;
/**
* Controls whether or not the X-Requested-With header is added to the request (default true). Set to false to not
* include the header.
*/
includeRequestedWithHeader?: boolean;
/**
* Controls whether or not to subscribe to events on `XMLHttpRequest.upload`, if available. This causes all requests
* to be preflighted (https://xhr.spec.whatwg.org/#request)
*/
includeUploadProgress?: boolean;
}
interface RequestData {
task: Task<XMLHttpRequest>;
used: boolean;
requestOptions: XhrRequestOptions;
nativeResponse: XMLHttpRequest;
url: string;
downloadObservable: Observable<number>;
dataObservable: Observable<any>;
}
const dataMap = new WeakMap<XhrResponse, RequestData>();
function getDataTask(response: XhrResponse): Task<XMLHttpRequest> {
const data = dataMap.get(response)!;
if (data.used) {
return Task.reject<any>(new TypeError('Body already read'));
}
data.used = true;
return data.task;
}
/**
* Wraps an XHR request in a response that mimics the fetch API
*/
export class XhrResponse extends Response {
readonly headers: Headers;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
get bodyUsed(): boolean {
return dataMap.get(this)!.used;
}
get nativeResponse(): XMLHttpRequest {
return dataMap.get(this)!.nativeResponse;
}
get requestOptions(): XhrRequestOptions {
return dataMap.get(this)!.requestOptions;
}
get url(): string {
return dataMap.get(this)!.url;
}
get download(): Observable<number> {
return dataMap.get(this)!.downloadObservable;
}
get data(): Observable<any> {
return dataMap.get(this)!.dataObservable;
}
constructor(request: XMLHttpRequest) {
super();
const headers = (this.headers = new Headers());
const responseHeaders = request.getAllResponseHeaders();
if (responseHeaders) {
const lines = responseHeaders.split(/\r\n/g);
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/^(.*?): (.*)$/);
if (match) {
headers.append(match[1], match[2]);
}
}
}
this.status = request.status;
this.ok = this.status >= 200 && this.status < 300;
this.statusText = request.statusText || 'OK';
}
arrayBuffer(): Task<ArrayBuffer> {
return Task.reject<ArrayBuffer>(new Error('ArrayBuffer not supported'));
}
blob(): Task<Blob> {
return Task.reject<Blob>(new Error('Blob not supported'));
}
formData(): Task<FormData> {
return Task.reject<FormData>(new Error('FormData not supported'));
}
text(): Task<string> {
return <any>getDataTask(this).then((request: XMLHttpRequest) => {
return String(request.responseText);
});
}
xml(): Task<Document> {
return <any>this.text().then((text: string) => {
const parser = new DOMParser();
return parser.parseFromString(text, this.headers.get('content-type') || 'text/html');
});
}
}
if (has('blob')) {
XhrResponse.prototype.blob = function(this: XhrResponse): Task<Blob> {
return <any>getDataTask(this).then((request: XMLHttpRequest) => request.response);
};
XhrResponse.prototype.text = function(this: XhrResponse): Task<string> {
return <any>this.blob().then(getTextFromBlob);
};
if (has('arraybuffer')) {
XhrResponse.prototype.arrayBuffer = function(this: XhrResponse): Task<ArrayBuffer> {
return <any>this.blob().then(getArrayBufferFromBlob);
};
}
}
if (has('formdata')) {
XhrResponse.prototype.formData = function(this: XhrResponse): Task<FormData> {
return <any>this.text().then((text: string) => {
const data = new FormData();
text
.trim()
.split('&')
.forEach((keyValues) => {
if (keyValues) {
const pairs = keyValues.split('=');
const name = (pairs.shift() || '').replace(/\+/, ' ');
const value = pairs.join('=').replace(/\+/, ' ');
data.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return data;
});
};
}
function noop() {}
function setOnError(request: XMLHttpRequest, reject: Function) {
request.addEventListener('error', function(event) {
reject(new TypeError(event.error || 'Network request failed'));
});
}
export default function xhr(url: string, options: XhrRequestOptions = {}): UploadObservableTask<XhrResponse> {
const request = new XMLHttpRequest();
const requestUrl = generateRequestUrl(url, options);
options = Object.create(options);
if (!options.method) {
options.method = 'GET';
}
let isAborted = false;
function abort() {
isAborted = true;
if (request) {
request.abort();
request.onreadystatechange = noop;
}
}
let timeoutHandle: Handle;
let timeoutReject: Function;
const task = <UploadObservableTask<XhrResponse>>new Task<XhrResponse>((resolve, reject) => {
timeoutReject = reject;
request.onreadystatechange = function() {
if (isAborted) {
return;
}
if (request.readyState === 2) {
const response = new XhrResponse(request);
const downloadSubscriptionPool = new SubscriptionPool<number>();
const dataSubscriptionPool = new SubscriptionPool<any>();
const task = new Task<XMLHttpRequest>((resolve, reject) => {
timeoutReject = reject;
request.onprogress = function(event: any) {
if (isAborted) {
return;
}
downloadSubscriptionPool.next(event.loaded);
};
request.onreadystatechange = function() {
if (isAborted) {
return;
}
if (request.readyState === 4) {
request.onreadystatechange = noop;
timeoutHandle && timeoutHandle.destroy();
dataSubscriptionPool.next(request.response);
dataSubscriptionPool.complete();
resolve(request);
}
};
setOnError(request, reject);
}, abort);
dataMap.set(response, {
task,
used: false,
nativeResponse: request,
requestOptions: options,
url: requestUrl,
downloadObservable: new Observable<number>((observer) => downloadSubscriptionPool.add(observer)),
dataObservable: new Observable<any>((observer) => dataSubscriptionPool.add(observer))
});
resolve(response);
}
};
setOnError(request, reject);
}, abort);
request.open(options.method, requestUrl, !options.blockMainThread, options.user, options.password);
if (has('filereader') && has('blob')) {
request.responseType = 'blob';
}
if (options.timeout && options.timeout > 0 && options.timeout !== Infinity) {
timeoutHandle = createTimer(() => {
// Reject first, since aborting will also fire onreadystatechange which would reject with a
// less specific error. (This is also why we set up our own timeout rather than using
// native timeout and ontimeout, because that aborts and fires onreadystatechange before ontimeout.)
timeoutReject && timeoutReject(new TimeoutError('The XMLHttpRequest request timed out'));
abort();
}, options.timeout);
}
let hasContentTypeHeader = false;
let hasRequestedWithHeader = false;
const { includeRequestedWithHeader = true, includeUploadProgress = true } = options;
if (options.headers) {
const requestHeaders = new Headers(options.headers);
hasRequestedWithHeader = requestHeaders.has('x-requested-with');
hasContentTypeHeader = requestHeaders.has('content-type');
for (const [key, value] of requestHeaders) {
request.setRequestHeader(key, value);
}
}
if (!hasRequestedWithHeader && includeRequestedWithHeader) {
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
if (!hasContentTypeHeader && has('formdata') && options.body instanceof global.FormData) {
// Assume that most forms do not contain large binary files. If that is not the case,
// then "multipart/form-data" should be manually specified as the "Content-Type" header.
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
task.finally(() => {
if (task.state !== State.Fulfilled) {
request.onreadystatechange = noop;
timeoutHandle && timeoutHandle.destroy();
}
});
if (includeUploadProgress) {
const uploadObserverPool = new SubscriptionPool<number>();
task.upload = new Observable<number>((observer) => uploadObserverPool.add(observer));
if (has('host-browser') || has('web-worker-xhr-upload')) {
request.upload.addEventListener('progress', (event) => {
uploadObserverPool.next(event.loaded);
});
}
}
request.send(options.body || null);
return task;
}

View File

@ -0,0 +1,28 @@
import { RequestOptions } from './interfaces';
import UrlSearchParams from '../UrlSearchParams';
/**
* Returns a URL formatted with optional query string and cache-busting segments.
*
* @param url The base URL.
* @param options The RequestOptions used to generate the query string or cacheBust.
*/
export function generateRequestUrl(url: string, options: RequestOptions = {}): string {
let query = new UrlSearchParams(options.query).toString();
if (options.cacheBust) {
const bustString = String(Date.now());
query += query ? `&${bustString}` : bustString;
}
const separator = url.indexOf('?') > -1 ? '&' : '?';
return query ? `${url}${separator}${query}` : url;
}
export function getStringFromFormData(formData: any): string {
const fields: string[] = [];
for (const key of formData.keys()) {
fields.push(encodeURIComponent(key) + '=' + encodeURIComponent(formData.get(key)));
}
return fields.join('&');
}

View File

@ -0,0 +1,39 @@
import { Hash } from './interfaces';
const escapeRegExpPattern = /[[\]{}()|\/\\^$.*+?]/g;
const escapeXmlPattern = /[&<]/g;
const escapeXmlForPattern = /[&<>'"]/g;
const escapeXmlMap: Hash<string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
/**
* Escapes a string so that it can safely be passed to the RegExp constructor.
* @param text The string to be escaped
* @return The escaped string
*/
export function escapeRegExp(text: string): string {
return !text ? text : text.replace(escapeRegExpPattern, '\\$&');
}
/**
* Sanitizes a string to protect against tag injection.
* @param xml The string to be escaped
* @param forAttribute Whether to also escape ', ", and > in addition to < and &
* @return The escaped string
*/
export function escapeXml(xml: string, forAttribute: boolean = true): string {
if (!xml) {
return xml;
}
const pattern = forAttribute ? escapeXmlForPattern : escapeXmlPattern;
return xml.replace(pattern, function(character: string): string {
return escapeXmlMap[character];
});
}

116
src/lib/core/src/text.ts Normal file
View File

@ -0,0 +1,116 @@
import Promise from '@dojo/shim/Promise';
import has from './has';
import request from './request';
import { NodeRequire, AmdRequire, AmdConfig } from './interfaces';
import { Require, isAmdRequire } from './load';
declare const require: Require;
/*
* Strips <?xml ...?> declarations so that external SVG and XML
* documents can be added to a document without worry. Also, if the string
* is an HTML document, only the part inside the body tag is returned.
*/
function strip(text: string): string {
if (!text) {
return '';
}
text = text.replace(/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, '');
let matches = text.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im);
text = matches ? matches[1] : text;
return text;
}
/*
* Host-specific method to retrieve text
*/
let getText: (url: string, callback: (value: string | null) => void) => void;
if (has('host-browser')) {
getText = function(url: string, callback: (value: string | null) => void): void {
request(url).then((response) => {
response.text().then((data) => {
callback(data);
});
});
};
} else if (has('host-node')) {
let fs = isAmdRequire(require) && require.nodeRequire ? require.nodeRequire('fs') : (<NodeRequire>require)('fs');
getText = function(url: string, callback: (value: string) => void): void {
fs.readFile(url, { encoding: 'utf8' }, function(error: Error, data: string): void {
if (error) {
throw error;
}
callback(data);
});
};
} else {
getText = function(): void {
throw new Error('dojo/text not supported on this platform');
};
}
/*
* Cache of previously-loaded text resources
*/
let textCache: { [key: string]: any } = {};
/*
* Cache of pending text resources
*/
let pending: { [key: string]: any } = {};
export function get(url: string): Promise<string | null> {
let promise = new Promise<string | null>(function(resolve, reject) {
getText(url, function(text) {
resolve(text);
});
});
return promise;
}
export function normalize(id: string, toAbsMid: (moduleId: string) => string): string {
let parts = id.split('!');
let url = parts[0];
return (/^\./.test(url) ? toAbsMid(url) : url) + (parts[1] ? '!' + parts[1] : '');
}
export function load(id: string, require: AmdRequire, load: (value?: any) => void, config?: AmdConfig): void {
let parts = id.split('!');
let stripFlag = parts.length > 1;
let mid = parts[0];
let url = require.toUrl(mid);
let text: string | undefined;
function finish(text: string): void {
load(stripFlag ? strip(text) : text);
}
if (mid in textCache) {
text = textCache[mid];
} else if (url in textCache) {
text = textCache[url];
}
if (!text) {
if (pending[url]) {
pending[url].push(finish);
} else {
let pendingList = (pending[url] = [finish]);
getText(url, function(value) {
textCache[mid] = textCache[url] = value;
for (let i = 0; i < pendingList.length; ) {
pendingList[i++](value || '');
}
delete pending[url];
});
}
} else {
finish(text);
}
}

122
src/lib/core/src/util.ts Normal file
View File

@ -0,0 +1,122 @@
import { Handle } from './interfaces';
import { createHandle } from './lang';
/**
* Wraps a setTimeout call in a handle, allowing the timeout to be cleared by calling destroy.
*
* @param callback Callback to be called when the timeout elapses
* @param delay Number of milliseconds to wait before calling the callback
* @return Handle which can be destroyed to clear the timeout
*/
export function createTimer(callback: (...args: any[]) => void, delay?: number): Handle {
let timerId: number | null = setTimeout(callback, delay);
return createHandle(function() {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
});
}
/**
* Wraps a callback, returning a function which fires after no further calls are received over a set interval.
*
* @param callback Callback to wrap
* @param delay Number of milliseconds to wait after any invocations before calling the original callback
* @return Debounced function
*/
export function debounce<T extends (this: any, ...args: any[]) => void>(callback: T, delay: number): T {
// node.d.ts clobbers setTimeout/clearTimeout with versions that return/receive NodeJS.Timer,
// but browsers return/receive a number
let timer: Handle | null;
return <T>function() {
timer && timer.destroy();
let context = this;
let args: IArguments | null = arguments;
timer = guaranteeMinimumTimeout(function() {
callback.apply(context, args);
args = context = timer = null;
}, delay);
};
}
/**
* Wraps a callback, returning a function which fires at most once per set interval.
*
* @param callback Callback to wrap
* @param delay Number of milliseconds to wait before allowing the original callback to be called again
* @return Throttled function
*/
export function throttle<T extends (this: any, ...args: any[]) => void>(callback: T, delay: number): T {
let ran: boolean | null;
return <T>function() {
if (ran) {
return;
}
ran = true;
callback.apply(this, arguments);
guaranteeMinimumTimeout(function() {
ran = null;
}, delay);
};
}
/**
* Like throttle, but calls the callback at the end of each interval rather than the beginning.
* Useful for e.g. resize or scroll events, when debounce would appear unresponsive.
*
* @param callback Callback to wrap
* @param delay Number of milliseconds to wait before calling the original callback and allowing it to be called again
* @return Throttled function
*/
export function throttleAfter<T extends (this: any, ...args: any[]) => void>(callback: T, delay: number): T {
let ran: boolean | null;
return <T>function() {
if (ran) {
return;
}
ran = true;
let context = this;
let args: IArguments | null = arguments;
guaranteeMinimumTimeout(function() {
callback.apply(context, args);
args = context = ran = null;
}, delay);
};
}
export function guaranteeMinimumTimeout(callback: (...args: any[]) => void, delay?: number): Handle {
const startTime = Date.now();
let timerId: number | null;
function timeoutHandler() {
const delta = Date.now() - startTime;
if (delay == null || delta >= delay) {
callback();
} else {
// Cast setTimeout return value to fix TypeScript parsing bug. Without it,
// it thinks we are using the Node version of setTimeout.
// Revisit this with the next TypeScript update.
// Set another timer for the mount of time that we came up short.
timerId = <any>setTimeout(timeoutHandler, delay - delta);
}
}
timerId = setTimeout(timeoutHandler, delay);
return createHandle(() => {
if (timerId != null) {
clearTimeout(timerId);
timerId = null;
}
});
}

12
src/lib/core/src/uuid.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* Returns a v4 compliant UUID.
*
* @returns {string}
*/
export default function uuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = (Math.random() * 16) | 0,
v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

View File

@ -0,0 +1,2 @@
import './lang';
import './stringExtras';

View File

@ -0,0 +1,269 @@
import Benchmark = require('benchmark');
import lang = require('../../src/lang');
function onComplete(this: any) {
console.log(this.name + ': ' + this.hz + ' with a margin of error of ' + this.stats.moe);
}
const simpleSource = { a: 1, b: 'Lorem ipsum', c: 4 };
const simpleSourceWithArray = {
a: 1,
b: 'Dolor sit amet.',
c: [1, 2, 3],
d: 5
};
const sourceFromConstructor = (function() {
function Answers(this: any, kwArgs: { [key: string]: any }) {
Object.keys(kwArgs).forEach(function(this: any, key: string): void {
(<any>this)[key] = kwArgs[key];
}, this);
}
return new (<any>Answers)({
universe: 42
});
})();
const sourceWithInherited = Object.create(
Object.create(null, {
x: {
value: '1234567890'.split('')
},
y: {
enumerable: true,
value: /\s/
}
}),
{
a: {
value: 1,
enumerable: true,
configurable: true,
writable: true
},
b: {
value: 'Lorem ipsum',
enumerable: true,
configurable: true,
writable: true
},
c: {
value: [],
enumerable: true,
configurable: true,
writable: true
},
d: {
value: 4,
enumerable: true,
configurable: true,
writable: true
}
}
);
let benchmarks: Benchmark[] = [];
benchmarks.push(
new Benchmark(
'lang.assign (single source)',
function() {
lang.assign(Object.create(null), simpleSource);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.assign (multiple sources)',
function() {
lang.assign(
Object.create(null),
simpleSource,
simpleSourceWithArray,
sourceWithInherited,
sourceFromConstructor
);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.deepAssign (single source)',
function() {
lang.deepAssign(Object.create(null), simpleSource);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.deepAssign (multiple sources)',
function() {
lang.deepAssign(
Object.create(null),
simpleSource,
simpleSourceWithArray,
sourceWithInherited,
sourceFromConstructor
);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.mixin (single source)',
function() {
lang.mixin(Object.create(null), simpleSource);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.mixin (multiple sources)',
function() {
lang.mixin(
Object.create(null),
simpleSource,
simpleSourceWithArray,
sourceWithInherited,
sourceFromConstructor
);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.deepMixin (single source)',
function() {
lang.deepMixin(Object.create(null), simpleSource);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.deepMixin (multiple sources)',
function() {
lang.deepMixin(
Object.create(null),
simpleSource,
simpleSourceWithArray,
sourceWithInherited,
sourceFromConstructor
);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.create',
function() {
lang.create(simpleSource, simpleSource, simpleSource, simpleSource);
},
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.isIdentical',
(function() {
let a = Number('asdfx{}');
let b = Number('xkcd');
return function() {
lang.isIdentical(a, b);
};
})(),
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.lateBind',
(function() {
let object: any = {
method: function() {}
};
return function() {
lang.lateBind(object, 'method');
};
})(),
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.lateBind (partial application)',
(function() {
let object: any = {
method: function() {}
};
return function() {
lang.lateBind(object, 'method', 1, 2, 3);
};
})(),
{
onComplete: onComplete
}
)
);
benchmarks.push(
new Benchmark(
'lang.partial',
(function() {
function f() {}
return function() {
lang.partial(f, 1, 2, 3);
};
})(),
{
onComplete: onComplete
}
)
);
benchmarks.forEach(function(benchmark: Benchmark): void {
benchmark.run();
});

View File

@ -0,0 +1,11 @@
import Benchmark = require('benchmark');
let benchmarks: any[] = [];
benchmarks.forEach(function(benchmark: any): void {
new Benchmark(benchmark.name, benchmark.fn, {
onComplete: function(this: any) {
console.log(this.name + ': ' + this.hz + ' with a margin of error of ' + this.stats.moe);
}
}).run();
});

View File

@ -0,0 +1 @@
import './text/textPlugin';

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Text Plugin Test</title>
</head>
<body>
<script src="../../../../node_modules/@dojo/shim/util/amd.js"></script>
<script src="../../../../node_modules/@dojo/loader/loader.js"></script>
<script>
require.config(shimAmdDependencies({
baseUrl: '../../../../',
packages: [
{ name: 'src', location: '_build/src' },
{ name: 'tests', location: '_build/tests' }
]
}));
require(['@dojo/shim/main'], function () {
require([
'src/text!./_build/tests/support/data/correctText.txt'
], function (result) {
window.loaderTestResults = {
text: result
};
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,44 @@
const { registerSuite } = intern.getInterface('object');
const { assert } = intern.getPlugin('chai');
import Test from 'intern/lib/Test';
import pollUntil from '@theintern/leadfoot/helpers/pollUntil';
async function executeTest(test: Test, htmlTestPath: string, timeout = 10000) {
try {
return await test.remote.get(htmlTestPath).then(
pollUntil<{ text: string }>(
function() {
return (<any>window).loaderTestResults || null;
},
undefined,
timeout
)
);
} catch (e) {
throw new Error('loaderTestResult was not set.');
}
}
const text = 'abc';
registerSuite('text plugin', {
async 'correct text'() {
const results = await executeTest(this, `${__dirname}/textPlugin.html`);
assert.strictEqual(results.text, text);
},
async 'strips XML'(this: any) {
const results = await executeTest(this, `${__dirname}/textPluginXML.html`);
assert.strictEqual(results.text, text);
},
async 'strips HTML'(this: any) {
const results = await executeTest(this, `${__dirname}/textPluginHTML.html`);
assert.strictEqual(results.text, text);
},
async 'strips empty file'(this: any) {
const results = await executeTest(this, `${__dirname}/textPluginEmpty.html`);
assert.strictEqual(results.text, '');
}
});

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Text Plugin Test</title>
</head>
<body>
<script src="../../../../node_modules/@dojo/shim/util/amd.js"></script>
<script src="../../../../node_modules/@dojo/loader/loader.js"></script>
<script>
require.config(shimAmdDependencies({
baseUrl: '../../../../',
packages: [
{ name: 'src', location: '_build/src' },
{ name: 'tests', location: '_build/tests' }
]
}));
require(['@dojo/shim/main'], function () {
require([
'src/text!./_build/tests/support/data/stripEmpty.xml!strip'
], function (result) {
window.loaderTestResults = {
text: result
};
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Text Plugin Test</title>
</head>
<body>
<script src="../../../../node_modules/@dojo/shim/util/amd.js"></script>
<script src="../../../../node_modules/@dojo/loader/loader.js"></script>
<script>
require.config(shimAmdDependencies({
baseUrl: '../../../../',
packages: [
{ name: 'src', location: '_build/src' },
{ name: 'tests', location: '_build/tests' }
]
}));
require(['@dojo/shim/main'], function () {
require([
'src/text!./_build/tests/support/data/strip.html!strip'
], function (result) {
window.loaderTestResults = {
text: result
};
});
});
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More