diff --git a/examples/example-app-angular-cli/package.json b/examples/example-app-angular-cli/package.json index b767c487..86551e33 100644 --- a/examples/example-app-angular-cli/package.json +++ b/examples/example-app-angular-cli/package.json @@ -7,11 +7,12 @@ "ng": "ng", "playground:copy": "node ../../scripts/copy.js", "playground:run": "node ./node_modules/angular-playground/dist/bin/cli.js", + "playground:test": "node ./node_modules/angular-playground/dist/bin/cli.js --check-errors --random-scenario", "playground": "npm run playground:copy && npm run playground:run", "playground:dev": "npm run build --prefix ../../ && npm run playground", "playground2:run": "node ./node_modules/angular-playground/dist/bin/cli.js angular-playground2.json", "cli": "ts-node --project ../../src/cli ../../src/cli/cli.ts", - "cli:build": "ts-node --project ../../src/cli ../../src/cli/cli.ts -no-watch -no-serve", + "cli:build": "ts-node --project ../../src/cli ../../src/cli/cli.ts --no-watch --no-serve", "start": "ng serve -no-progress", "build": "ng build -prod -a=app", "lint": "ng lint", diff --git a/package-lock.json b/package-lock.json index 446eda33..2ff8ea78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,24 @@ "integrity": "sha1-5+E068Z0rm7ZPDbHZ3ObEQ0sV/w=", "dev": true }, + "@types/puppeteer": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-0.13.3.tgz", + "integrity": "sha512-QPzng6tTcWPnjG51EATK74TRz5dtst8BqXdZt9QgbZAaY+2bq0t6m5Mj3hJ8hJV9UFlXOq3B6PYBwWIh7qZX5Q==", + "dev": true, + "requires": { + "@types/node": "6.0.60" + } + }, + "agent-base": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.1.2.tgz", + "integrity": "sha512-VE6QoEdaugY86BohRtfGmTDabxdU5sCKOkbcPA6PXKJsRzEi/7A3RCTxJal1ft/4qSfPht5/iQLhMh/wzSkkNw==", + "dev": true, + "requires": { + "es6-promisify": "5.0.0" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -155,6 +173,12 @@ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "ast-types": { "version": "0.9.6", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", @@ -184,6 +208,12 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -289,6 +319,21 @@ "sprintf-js": "1.1.1" } }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", @@ -301,6 +346,49 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, "copyfiles": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-1.2.0.tgz", @@ -353,6 +441,15 @@ "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", "dev": true }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -426,6 +523,21 @@ } } }, + "es6-promise": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", + "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "4.1.1" + } + }, "es6-templates": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/es6-templates/-/es6-templates-0.2.3.tgz", @@ -496,6 +608,35 @@ "is-extglob": "1.0.0" } }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "fancy-log": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", @@ -512,6 +653,15 @@ "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", "dev": true }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -901,6 +1051,12 @@ "ansi-regex": "2.1.1" } }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, "has-gulplog": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", @@ -919,6 +1075,16 @@ "parse-passwd": "1.0.0" } }, + "https-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.1.0.tgz", + "integrity": "sha512-/DTVSUCbRc6AiyOV4DBRvPDpKKCJh4qQJNaCgypX0T41quD9hp/PB5iUyx/60XobuMPQa9ce1jNV9UOUq6PnTg==", + "dev": true, + "requires": { + "agent-base": "4.1.2", + "debug": "2.6.9" + } + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1307,6 +1473,12 @@ "integrity": "sha1-Wrh60dTB2rjowIu/A37gwZAih88=", "dev": true }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", + "dev": true + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -1334,6 +1506,12 @@ "regex-cache": "0.4.4" } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1366,6 +1544,12 @@ } } }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "multipipe": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", @@ -1573,6 +1757,12 @@ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", "dev": true }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -1597,6 +1787,34 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", "dev": true }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "puppeteer": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-0.13.0.tgz", + "integrity": "sha512-M52SA/WmW54YMLzFtCLGslhr9tntzfTgJIZnx3QnaDXn9F5q2BlTosywSBEKj8aVVd6al0WNfiu14MUQW3wjaw==", + "dev": true, + "requires": { + "debug": "2.6.9", + "extract-zip": "1.6.6", + "https-proxy-agent": "2.1.0", + "mime": "1.6.0", + "progress": "2.0.0", + "proxy-from-env": "1.0.0", + "rimraf": "2.6.2", + "ws": "3.3.2" + } + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -1805,6 +2023,15 @@ "global-modules": "0.2.3" } }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, "rxjs": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.2.tgz", @@ -1911,6 +2138,12 @@ "is-utf8": "0.2.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -1986,6 +2219,82 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "ts-node": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", + "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "chalk": "2.3.0", + "diff": "3.3.1", + "make-error": "1.3.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18", + "tsconfig": "6.0.0", + "v8flags": "3.0.1", + "yn": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "v8flags": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz", + "integrity": "sha1-3Oj8N5wX2fLJ6e142JzgAFKxt2s=", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } + } + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "dev": true, + "requires": { + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tsickle": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.24.1.tgz", @@ -2030,12 +2339,24 @@ "tslib": "1.7.1" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, "typescript": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", "dev": true }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -2160,12 +2481,38 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "ws": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz", + "integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==", + "dev": true, + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, "zone.js": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.4.tgz", diff --git a/package.json b/package.json index c4968032..a795bfed 100644 --- a/package.json +++ b/package.json @@ -56,13 +56,16 @@ "@angular/platform-browser": "^5.0.0", "@angular/platform-browser-dynamic": "^5.0.0", "@types/node": "6.0.60", + "@types/puppeteer": "^0.13.3", "codelyzer": "3.0.1", "copyfiles": "^1.2.0", "fs-extra": "^4.0.1", "glob": "^7.1.2", "gulp": "^3.9.1", "gulp-inline-ng2-template": "^4.0.0", + "puppeteer": "^0.13.0", "rxjs": "^5.5.2", + "ts-node": "^3.3.0", "tslint": "5.3.2", "typescript": "2.4.2", "zone.js": "0.8.4" diff --git a/src/cli/build.ts b/src/cli/build.ts index cab8ee7c..d90a8cc9 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -3,57 +3,62 @@ import { StringBuilder } from './string-builder'; import * as fs from 'fs'; import * as path from 'path'; -export const build = (rootPath) => { - let content = new StringBuilder(); - let home = path.resolve(rootPath); - let sandboxes = []; - - fromDir(home, /\.sandbox.ts$/, (filename) => { - let sandboxPath = filename.replace(home, '.').replace(/.ts$/, '').replace(/\\/g, '/'); - const contents = fs.readFileSync(filename, 'utf8'); - - const matchSandboxOf = /\s?sandboxOf\s*\(\s*([^)]+?)\s*\)/g.exec(contents); - if (matchSandboxOf) { - const typeName = matchSandboxOf[1].split(',')[0].trim(); - const labelText = /label\s*:\s*['"](.+)['"]/g.exec(matchSandboxOf[0]); - - let scenarioMenuItems = []; - const scenarioRegex = /\.add\s*\(['"](.+)['"]\s*,\s*{/g; - let scenarioMatches; - let scenarioIndex = 1; - while ((scenarioMatches = scenarioRegex.exec(contents)) !== null) { - scenarioMenuItems.push({key: scenarioIndex, description: scenarioMatches[1]}); - scenarioIndex++; - } - - let label = labelText ? labelText[1] : ''; - sandboxes.push({ - key: sandboxPath, - searchKey: `${typeName}${label}`, - name: typeName, - label: label, - scenarioMenuItems - }); - } - }); - - content.addLine(`export function getSandboxMenuItems() {`); - content.addLine(`return ${JSON.stringify(sandboxes)};`); - content.addLine(`}`); - - content.addLine(`export function getSandbox(path: string) {`); - content.addLine(`switch(path) {`); - sandboxes.forEach(({key}) => { - content.addLine(`case '${key}':`); - content.addLine(`return import('${key}').then(sandbox => { return sandbox.default.serialize('${key}'); });`); - }); - content.addLine(`}}`); - - let filePath = path.resolve(home, './sandboxes.ts'); - fs.writeFile(filePath, content.dump(), function (err) { - if (err) { - return console.log(err); - } - console.log(`Created file: ${filePath}`); - }); -}; +export async function build(rootPath): Promise { + let content = new StringBuilder(); + let home = path.resolve(rootPath); + let sandboxes = []; + + fromDir(home, /\.sandbox.ts$/, (filename) => { + let sandboxPath = filename.replace(home, '.').replace(/.ts$/, '').replace(/\\/g, '/'); + const contents = fs.readFileSync(filename, 'utf8'); + + const matchSandboxOf = /\s?sandboxOf\s*\(\s*([^)]+?)\s*\)/g.exec(contents); + if (matchSandboxOf) { + const typeName = matchSandboxOf[1].split(',')[0].trim(); + const labelText = /label\s*:\s*['"](.+)['"]/g.exec(matchSandboxOf[0]); + + let scenarioMenuItems = []; + const scenarioRegex = /\.add\s*\(['"](.+)['"]\s*,\s*{/g; + let scenarioMatches; + let scenarioIndex = 1; + while ((scenarioMatches = scenarioRegex.exec(contents)) !== null) { + scenarioMenuItems.push({ key: scenarioIndex, description: scenarioMatches[1] }); + scenarioIndex++; + } + + let label = labelText ? labelText[1] : ''; + sandboxes.push({ + key: sandboxPath, + searchKey: `${typeName}${label}`, + name: typeName, + label: label, + scenarioMenuItems + }); + } + }); + + content.addLine(`export function getSandboxMenuItems() {`); + content.addLine(`return ${JSON.stringify(sandboxes)};`); + content.addLine(`}`); + + content.addLine(`export function getSandbox(path: string) {`); + content.addLine(`switch(path) {`); + sandboxes.forEach(({ key }) => { + content.addLine(`case '${key}':`); + content.addLine(`return import('${key}').then(sandbox => { return sandbox.default.serialize('${key}'); });`); + }); + content.addLine(`}}`); + + let filePath = path.resolve(home, './sandboxes.ts'); + + return new Promise((resolve, reject) => { + fs.writeFile(filePath, content.dump(), function (err) { + if (err) { + reject(err); + } else { + console.log(`Created file: ${filePath}`); + resolve(filePath); + } + }); + }); +} diff --git a/src/cli/cli.ts b/src/cli/cli.ts index adbd41ed..cbb54877 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,35 +1,40 @@ #! /usr/bin/env node -const path = require('path'); -import {build} from './build'; -import {startWatch} from './start-watch'; -import {runAngularCli} from './run-angular-cli'; +import * as path from 'path'; +import { build } from './build'; +import { startWatch } from './start-watch'; +import { runAngularCli } from './run-angular-cli'; +import { Configuration } from './shared/configuration'; +import { verifySandboxes } from './verify-sandboxes'; -const supportedFlags = ['-no-watch', '-no-serve']; -let args = process.argv.slice(2); -let flags = []; -args = args.reduce((accr, value) => { - supportedFlags.indexOf(value) > -1 ? flags.push(value) : accr.push(value); - return accr; -}, []); +(async () => { + await run(); +})(); -const runWatch = flags.indexOf('-no-watch') === -1; -const runAngularCliServe = flags.indexOf('-no-serve') === -1; +async function run() { + const rawArgs = process.argv.slice(2); + const config = new Configuration(rawArgs); -const configFilePath = args[0] || 'angular-playground.json'; + let configFile = path.resolve(config.configFilePath); + let playgroundConfig; + try { + playgroundConfig = require(configFile.replace(/.json$/, '')); + } catch (e) { + process.stdout.write(`[angular-playground]: \x1b[31mFailed to load config file ${configFile}\x1b[0m\n`); + process.exit(1); + } -let configFile = path.resolve(configFilePath); -let config; -try { - config = require(configFile.replace(/.json$/, '')); -} catch(e) { - process.stdout.write(`[angular-playground]: \x1b[31mFailed to load config file ${configFile}\x1b[0m\n`); - process.exit(1); -} + const sandboxesPath = await build(playgroundConfig.sourceRoot); + config.port = playgroundConfig.angularCli.port ? playgroundConfig.angularCli.port : 4201; -build(config.sourceRoot); -if (runWatch) { - startWatch(config, () => build(config.sourceRoot)); -} -if (runAngularCliServe && config.angularCli) { - runAngularCli(config.angularCli); + if (config.runWatch) { + startWatch(playgroundConfig, () => build(playgroundConfig.sourceRoot)); + } + + if (config.runAngularCliServe && playgroundConfig.angularCli) { + runAngularCli(playgroundConfig.angularCli); + } + + if (config.runCheckErrors) { + verifySandboxes(config, sandboxesPath); + } } diff --git a/src/cli/from-dir.ts b/src/cli/from-dir.ts index 8dd40b03..3dfd465b 100644 --- a/src/cli/from-dir.ts +++ b/src/cli/from-dir.ts @@ -2,17 +2,17 @@ import * as fs from 'fs'; import * as path from 'path'; export const fromDir = (startPath, filter, callback) => { - if (!fs.existsSync(startPath)) { - console.log("no dir ", startPath); - return; - } - let files = fs.readdirSync(startPath); - for (let i = 0; i < files.length; i++) { - let filename = path.join(startPath, files[i]); - let stat = fs.lstatSync(filename); - if (stat.isDirectory()) { - fromDir(filename, filter, callback); //recurse + if (!fs.existsSync(startPath)) { + console.log("no dir ", startPath); + return; + } + let files = fs.readdirSync(startPath); + for (let i = 0; i < files.length; i++) { + let filename = path.join(startPath, files[i]); + let stat = fs.lstatSync(filename); + if (stat.isDirectory()) { + fromDir(filename, filter, callback); //recurse + } + else if (filter.test(filename)) callback(filename); } - else if (filter.test(filename)) callback(filename); - } }; diff --git a/src/cli/run-angular-cli.ts b/src/cli/run-angular-cli.ts index 2ddd4bc4..34113a47 100644 --- a/src/cli/run-angular-cli.ts +++ b/src/cli/run-angular-cli.ts @@ -3,36 +3,36 @@ const path = require('path'); const childProcess = require('child_process'); export const runAngularCli = (angularCliConfig) => { - let port = angularCliConfig.port ? angularCliConfig.port : 4201; - let cliName = '@angular/cli'; - try{ - fs.accessSync(path.resolve('node_modules/@angular/cli/bin/ng')); - } catch (e) { - cliName = 'angular-cl'; - } - let cliPath = `node_modules/${cliName}/bin/ng`; - let args = [cliPath, 'serve', '-no-progress']; - args.push('--port'); - args.push(port.toString()); - if (angularCliConfig.appName) { - args.push(`-a=${angularCliConfig.appName}`); - } - if (angularCliConfig.environment) { - args.push(`-e=${angularCliConfig.environment}`); - } - if (angularCliConfig.args) { - args = args.concat(angularCliConfig.args); - } - const ngServe = childProcess.spawn('node', args, {maxBuffer: 1024 * 500}); - ngServe.stdout.on('data', (data) => { - write(process.stdout, data); - }); - ngServe.stderr.on('data', (data) => { - write(process.stderr, data); - }); + let port = angularCliConfig.port ? angularCliConfig.port : 4201; + let cliName = '@angular/cli'; + try { + fs.accessSync(path.resolve('node_modules/@angular/cli/bin/ng')); + } catch (e) { + cliName = 'angular-cl'; + } + let cliPath = `node_modules/${cliName}/bin/ng`; + let args = [cliPath, 'serve', '-no-progress']; + args.push('--port'); + args.push(port.toString()); + if (angularCliConfig.appName) { + args.push(`-a=${angularCliConfig.appName}`); + } + if (angularCliConfig.environment) { + args.push(`-e=${angularCliConfig.environment}`); + } + if (angularCliConfig.args) { + args = args.concat(angularCliConfig.args); + } + const ngServe = childProcess.spawn('node', args, { maxBuffer: 1024 * 500 }); + ngServe.stdout.on('data', (data) => { + write(process.stdout, data); + }); + ngServe.stderr.on('data', (data) => { + write(process.stderr, data); + }); - function write(handler, data) { - let message = data.toString(); - handler.write(`[ng serve]: ${message}\n`); - } + function write(handler, data) { + let message = data.toString(); + handler.write(`[ng serve]: ${message}\n`); + } }; diff --git a/src/cli/shared/configuration.ts b/src/cli/shared/configuration.ts new file mode 100644 index 00000000..d05d64fc --- /dev/null +++ b/src/cli/shared/configuration.ts @@ -0,0 +1,82 @@ +/** + * Configuration object used to parse and assign command line arguments + */ +export class Configuration { + private supportedFlags = { + noWatch: '--no-watch', + noServe: '--no-serve', + checkErrs: '--check-errors', + randomScenario: '--random-scenario' + }; + + private supportedArguments = { + config: '--config' + }; + + runWatch: boolean; + runAngularCliServe: boolean; + runCheckErrors: boolean; + randomScenario: boolean; + configFilePath: string; + port: number; + timeoutAttempts = 20; + chromeArguments = [ '--disable-gpu', '--no-sandbox' ]; + + constructor(rawArgv: string[]) { + const { flags, args } = this.getParsedArguments(rawArgv); + this.configureFlags(flags); + this.configureArguments(args); + } + + get baseUrl(): string { + return `http://localhost:${this.port}`; + } + + // Boolean flags + private configureFlags(flags: string[]) { + this.runWatch = flags.indexOf(this.supportedFlags.noWatch) === -1; + this.runAngularCliServe = flags.indexOf(this.supportedFlags.noServe) === -1; + this.runCheckErrors = flags.indexOf(this.supportedFlags.checkErrs) !== -1; + this.randomScenario = flags.indexOf(this.supportedFlags.randomScenario) !== -1; + } + + // Arguments that may have values attached + private configureArguments(args: string[]) { + const configIndex = args.indexOf(this.supportedArguments.config); + if (configIndex !== -1) { + this.configFilePath = this.getArgValue(configIndex, args); + } else if (args.length > 0) { + this.configFilePath = args[0]; + } else { + this.configFilePath = 'angular-playground.json'; + } + } + + /** + * Separates accepted command line arguments from other ts-node arguments + * @param supportedFlags - Accepted command line flags + * @param args - Process arguments + */ + private getParsedArguments(args: string[]): { flags: string[], args: string[] } { + const flags: string[] = []; + + args = args.reduce((accr, value) => { + Object.keys(this.supportedFlags) + .map(key => this.supportedFlags[key]) + .indexOf(value) > -1 ? flags.push(value) : accr.push(value); + return accr; + }, []); + + return { flags, args }; + } + + /** + * Gets the value of an argument from list of args (next consecutive argument) + * e.g. --config ./src/ + * @param startingIndex - Index of argument + * @param args - list of args + */ + private getArgValue(startingIndex: number, args: string[]): string { + return args[startingIndex + 1]; + } +} diff --git a/src/cli/shared/error-reporter.ts b/src/cli/shared/error-reporter.ts new file mode 100644 index 00000000..3b350b8c --- /dev/null +++ b/src/cli/shared/error-reporter.ts @@ -0,0 +1,26 @@ +export enum ReportType { + Log +} + +export class ErrorReporter { + private _errors: { error: any, scenario: string }[] = []; + + constructor(public type = ReportType.Log) {} + + get errors() { + return this._errors; + } + + addError(error: any, scenario: string) { + this._errors.push({ error, scenario }); + } + + compileReport() { + switch (this.type) { + case ReportType.Log: + console.log('Found errors in the following scenarios:'); + this._errors.forEach(e => console.log(e.scenario)); + } + } + +} diff --git a/src/cli/start-watch.ts b/src/cli/start-watch.ts index 6659d30a..94b16a88 100644 --- a/src/cli/start-watch.ts +++ b/src/cli/start-watch.ts @@ -2,12 +2,12 @@ const path = require('path'); const watch = require('node-watch'); export const startWatch = (config, cb) => { - let filter = (fn) => { - return (filename) => { - if (!/node_modules/.test(filename) && /\.sandbox.ts$/.test(filename)) { - fn(filename); - } + let filter = (fn) => { + return (filename) => { + if (!/node_modules/.test(filename) && /\.sandbox.ts$/.test(filename)) { + fn(filename); + } + }; }; - }; - watch([path.resolve(config.sourceRoot)], filter(cb)); + watch([path.resolve(config.sourceRoot)], filter(cb)); }; diff --git a/src/cli/string-builder.ts b/src/cli/string-builder.ts index c03e153d..1db6d145 100644 --- a/src/cli/string-builder.ts +++ b/src/cli/string-builder.ts @@ -1,14 +1,14 @@ export class StringBuilder { - private lines = []; + private lines = []; - addLine(line) { - this.lines.push(line); - } + addLine(line) { + this.lines.push(line); + } - dump() { - let data = this.lines.join('\n'); - data += '\n'; - this.lines = []; - return data; - } + dump() { + let data = this.lines.join('\n'); + data += '\n'; + this.lines = []; + return data; + } } diff --git a/src/cli/tsconfig.json b/src/cli/tsconfig.json index fcca0924..1d315c1a 100644 --- a/src/cli/tsconfig.json +++ b/src/cli/tsconfig.json @@ -7,7 +7,7 @@ "moduleResolution": "node", "outDir": "../../dist/bin", "sourceMap": false, - "target": "es5", + "target": "es2015", "newLine": "LF", "typeRoots": [ "../../node_modules/@types" diff --git a/src/cli/verify-sandboxes.ts b/src/cli/verify-sandboxes.ts new file mode 100644 index 00000000..399a7f5d --- /dev/null +++ b/src/cli/verify-sandboxes.ts @@ -0,0 +1,155 @@ +import * as puppeteer from 'puppeteer'; +import * as process from 'process'; +import * as path from 'path'; +import { ErrorReporter, ReportType } from './shared/error-reporter'; +import { Configuration } from './shared/configuration'; +// ts-node required for runtime typescript compilation of sandboxes.ts +require('ts-node/register'); + + +interface ScenarioSummary { + url: string; + name: string; + description: string; +} + +let browser: any; +let currentScenario = ''; +const reporter = new ErrorReporter(); + +// Ensure Chromium instances are destroyed on err +process.on('unhandledRejection', () => { + if (browser) browser.close(); +}); + +export async function verifySandboxes(configuration: Configuration, sandboxesPath: string) { + await main(configuration, sandboxesPath); +} + +///////////////////////////////// + +async function main (configuration: Configuration, sandboxesPath: string) { + let timeoutAttempts = configuration.timeoutAttempts; + browser = await puppeteer.launch({ + headless: true, + handleSIGINT: false, + args: configuration.chromeArguments + }); + + const scenarios = getSandboxMetadata(configuration.baseUrl, configuration.randomScenario, sandboxesPath); + console.log(`Retrieved ${scenarios.length} scenarios.\n`); + for (let i = 0; i < scenarios.length; i++) { + await openScenarioInNewPage(scenarios[i], configuration.timeoutAttempts); + } + + browser.close(); + + if (reporter.errors.length > 0) { + reporter.compileReport(); + process.exit(1); + } else { + process.exit(0); + } +} + +/** + * Creates a Chromium page and navigates to a scenario (URL). + * If Chromium is not able to connect to the provided page, it will issue a series + * of retries before it finally fails. + * @param scenario - Scenario to visit + */ +async function openScenarioInNewPage(scenario: ScenarioSummary, timeoutAttempts: number) { + if (timeoutAttempts === 0) { + await browser.close(); + process.exit(1); + } + + const page = await browser.newPage(); + page.on('console', (msg: any) => onConsoleErr(msg)); + currentScenario = scenario.name; + + try { + console.log(`Checking: ${currentScenario}: ${scenario.description}`); + await page.goto(scenario.url); + } catch (e) { + await page.close(); + await delay(5000); + console.log(`Attempting to connect. (Attempts Remaining: ${timeoutAttempts})`); + await openScenarioInNewPage(scenario, timeoutAttempts - 1); + } +} + +/** + * Retrieves Sandbox scenario URLs, descriptions, and names + * @param baseUrl - Base URL of scenario path e.g. http://localhost:4201 + * @param selectRandomScenario - Whether or not to select one random scenario of all availalble scenarios for a component + * @param path - Path to sandboxes.ts + */ +function getSandboxMetadata(baseUrl: string, selectRandomScenario: boolean, path: string): ScenarioSummary[] { + const scenarios: ScenarioSummary[] = []; + + loadSandboxMenuItems(path).forEach((scenario: any) => { + if (selectRandomScenario) { + const randomItemKey = getRandomKey(scenario.scenarioMenuItems.length); + scenario.scenarioMenuItems + .forEach((item: any) => { + if (item.key === randomItemKey) { + const url = `${baseUrl}?scenario=${encodeURIComponent(scenario.key)}/${encodeURIComponent(item.description)}`; + scenarios.push({ url, name: scenario.key, description: item.description }); + } + }); + } else { + // Grab all scenarios + scenario.scenarioMenuItems + .forEach((item: any) => { + const url = `${baseUrl}?scenario=${encodeURIComponent(scenario.key)}/${encodeURIComponent(item.description)}`; + scenarios.push({ url, name: scenario.key, description: item.description }); + }); + } + }); + + return scenarios; +} + +/** + * Attemtp to load sandboxes.ts and provide menu items + * @param path - Path to sandboxes.ts + */ +function loadSandboxMenuItems(path: string): any[] { + try { + return require(path).getSandboxMenuItems(); + } catch (err) { + console.error('Failed to load sandboxes.ts file.'); + console.error(err); + console.log('Terminating process.'); + process.exit(1); + } +} + +/** + * Callback when Chromium page encounters a console error + * @param msg - Error message + */ +function onConsoleErr(msg: any) { + if (msg.type === 'error') { + console.error(`ERROR Found in ${currentScenario}`); + reporter.addError(msg, currentScenario); + } +} + +/** + * Returns a random value between 1 and the provided length. + * Note: indexing of keys starts at 1, not 0 + * @param menuItemsLength - Maximum number of items + */ +function getRandomKey(menuItemsLength: number): number { + return Math.floor(Math.random() * (menuItemsLength - 1) + 1); +} + +function delay(ms: number) { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, ms); + }); +} diff --git a/tsconfig.json b/tsconfig.json index c8ae687b..64f15ffe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "lib": ["es6", "dom"], "module": "es6", "moduleResolution": "node", - "noImplicitAny": true, + "noImplicitAny": false, "target": "es5", "newLine": "LF", "typeRoots": [ diff --git a/tslint.json b/tslint.json index 078efc1f..e061baf2 100644 --- a/tslint.json +++ b/tslint.json @@ -8,7 +8,7 @@ true, "check-space" ], - "curly": true, + "curly": false, "eofline": true, "forin": true, "indent": [