import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges, ViewChild } from '@angular/core'; import * as ts from 'typescript'; import { FileConfig } from '../interfaces/file-config'; import { LoopProtectionService } from '../services/loop-protection.service'; import { ScriptLoaderService } from '../services/script-loader.service'; import * as babylon from 'babylon'; import * as babel_types from 'babel-types'; import babel_traverse from 'babel-traverse'; declare const require; function jsScriptInjector(iframe) { return function (code) { const script = document.createElement('script'); script.type = 'text/javascript'; script.innerHTML = code; iframe.contentWindow.document.head.appendChild(script); }; } function cssInjector(iframe) { return function (css) { const s = iframe.contentDocument.createElement('style'); s.innerHTML = css; iframe.contentDocument.getElementsByTagName('head')[0].appendChild(s); }; } interface IframeConfig { id: string; url: string; restart?: boolean; hidden?: boolean; } function createIframe(config: IframeConfig) { const iframe = document.createElement('iframe'); iframe.setAttribute('sandbox', 'allow-modals allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts'); iframe.setAttribute('frameBorder', '0'); iframe.setAttribute('src', config.url); iframe.setAttribute('class', config.id); return iframe; } function injectIframe(element: any, config: IframeConfig, runner: RunnerComponent): Promise<{ setHtml: Function, runCss: Function, register: Function, runMultipleFiles: Function, runSingleFile: Function, runSingleScriptFile: Function, loadSystemJS: Function, injectSystemJs: Function }> { if (runner.cachedIframes[config.id]) { runner.cachedIframes[config.id].iframe.remove(); delete runner.cachedIframes[config.id]; } const iframe = createIframe(config); runner.cachedIframes[config.id] = { iframe: iframe }; element.appendChild(iframe); const runJs = jsScriptInjector(iframe); const runCss = cssInjector(iframe); let index = 0; return new Promise((resolve, reject) => { if (!iframe.contentWindow) { return reject('iframe is gone'); } iframe.contentWindow.onload = () => { iframe.contentWindow.console.log = function () { console.log.apply(console, arguments); }; const setHtml = (html) => { iframe.contentDocument.body.innerHTML = html; }; const displayError = (error, info) => { /*if (!runner.appConfig.config.noerrors) { console.log(info, error); } */ const escaped = (document.createElement('a').appendChild( document.createTextNode(error)).parentNode as any).innerHTML; setHtml(`
Check out your browser console to see the full error!
${escaped}
`); }; iframe.contentWindow.console.error = function (error, message) { // handle Angular error 1/3 displayError(error, 'Angular Error'); }; function register(name, code) { (iframe.contentWindow as any).System.register(name, [], function (exports) { return { setters: [], execute: function () { exports(code); } }; }); } resolve({ register: register, injectSystemJs: () => { const systemCode = runner.scriptLoaderService.getScript('SystemJS'); // SystemJS expects document.baseURI to be set on the document. // Since it's a readonly property, I'm faking whole document property. const wrappedSystemCode = ` (function(document){ ${systemCode} }({ getElementsByTagName: document.getElementsByTagName.bind(document), head:document.head, body: document.body, documentElement: document.documentElement, createElement: document.createElement.bind(document), baseURI: '${document.baseURI}' })); `; jsScriptInjector(iframe)(wrappedSystemCode); }, runSingleScriptFile: jsScriptInjector(iframe), runSingleFile: runJs, runSingleCssFile: runCss, setHtml: setHtml, loadSystemJS: (name) => { (iframe.contentWindow as any).loadSystemModule(name, runner.scriptLoaderService.getScript(name)); }, runCss: runCss, runMultipleFiles: (files: Array) => { index++; (iframe.contentWindow as any).System.register('code', [], function (exports) { return { setters: [], execute: function () { exports('ts', ts); exports('babylon', babylon); exports('babel_traverse', babel_traverse); exports('babel_types', babel_types); files.forEach((file) => { exports(file.path.replace(/[\/\.-]/gi, '_'), file.code); exports(file.path.replace(/[\/\.-]/gi, '_') + '_AST', ts.createSourceFile(file.path, file.code, ts.ScriptTarget.ES5)); }); } }; }); files.map(file => { if (!file.path) { // tslint:disable-next-line:no-debugger debugger; } }); files.filter(file => file.path.indexOf('index.html') >= 0).map((file => { setHtml(file.code); })); files.filter(file => file.type === 'css').map((file) => { runCss(file.code); }); const compiled = files.filter(file => file.type === 'typescript').map((file) => { // Update module names let code = file.code; code = runner.loopProtectionService.protect(file.path, code); if (file.before) { code = file.before + ';\n' + code; } if (file.after) { code = ';\n' + code + file.after; } const moduleName = file.moduleName; // TODO(kirjs): Add source maps. return ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.System, target: ts.ScriptTarget.ES5, experimentalDecorators: true, emitDecoratorMetadata: true, noImplicitAny: true, declaration: true, // TODO: figure out why this doesn't work inlineSourceMap: true, inlineSources: true }, fileName: moduleName, moduleName: moduleName, reportDiagnostics: true }); }); const diagnostics = compiled.reduce((result, file) => { return result.concat(file.diagnostics); }, []); if (diagnostics.length) { runCss(` body { font-family: Roboto, sans-serif; } h2 { font-size: 50px; background-color: red; color: white; margin-bottom: 0; } b { color: #c04e22; font-weight: 300; background-color: #ffe; } `); const diagnosticsHtml = diagnostics.map(diagnostic => `
  • Error in file ${diagnostic.file.fileName}: ` + diagnostic.messageText + '
  • ').join(''); setHtml(`

    Errors when compiling

    Look in the editor for hints to fix it.
    `); } else { compiled.map((result) => { runJs(result.outputText); }); files.filter((file) => file.bootstrap).map((file) => { runJs(`System.import('${file.moduleName}')`); }); } } }); }; if (config.url === 'about:blank') { iframe.contentWindow.onload({} as any); } }); } @Component({ selector: 'slides-runner', templateUrl: './runner.component.html', styleUrls: ['./runner.component.css'] }) export class RunnerComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() browserUseConsole: boolean; @Input() browserWidth: string; @Input() browserHeight: string; @Input() files: Array; @Input() runnerType: string; @Output() onTestUpdate = new EventEmitter(); cachedIframes = {}; html = ``; @ViewChild('runner') runnerElement: ElementRef; @ViewChild('runnerConsole') runnerConsoleElement: ElementRef; private handleMessageBound: any; public System: any; constructor(public loopProtectionService: LoopProtectionService, public scriptLoaderService: ScriptLoaderService) { this.handleMessageBound = this.handleMessage.bind(this); window.addEventListener('message', this.handleMessageBound, false); } ngOnChanges(changes: SimpleChanges): void { if (this.runnerElement != null || this.runnerConsoleElement != null) { this.runCode(changes.files.currentValue, this.runnerType); } } handleMessage(event): void { this.onTestUpdate.emit(event); } runCode(files: Array, runner: string): void { const time = (new Date()).getTime(); if (runner === 'Angular') { injectIframe(this.runnerElement.nativeElement, { id: 'preview', 'url': 'about:blank' }, this).then((sandbox) => { sandbox.injectSystemJs(); sandbox.runCss(require('./inner.css')); sandbox.setHtml(this.html); sandbox.runSingleFile(this.scriptLoaderService.getScript('shim')); sandbox.runSingleFile(this.scriptLoaderService.getScript('zone')); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('system-config')); sandbox.loadSystemJS('ng-bundle'); sandbox.register('reflect-metadata', Reflect); sandbox.runMultipleFiles(files.filter(file => !file.test)); }); injectIframe(this.runnerElement.nativeElement, { id: 'testing', 'url': 'about:blank' }, this).then((sandbox) => { sandbox.injectSystemJs(); sandbox.runCss(require('./inner.css')); sandbox.setHtml(this.html); sandbox.runSingleFile(this.scriptLoaderService.getScript('shim')); sandbox.runSingleFile(this.scriptLoaderService.getScript('zone')); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('chai')); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('system-config')); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('mocha')); sandbox.runSingleFile(this.scriptLoaderService.getScript('test-bootstrap')); sandbox.loadSystemJS('ng-bundle'); sandbox.register('reflect-metadata', Reflect); const testFiles = files .filter(file => !file.excludeFromTesting); sandbox.runMultipleFiles(testFiles); }); } else if (runner === 'TypeScript') { injectIframe(this.runnerElement.nativeElement, { id: 'preview', 'url': 'about:blank' }, this).then((sandbox) => { sandbox.runCss(require('./inner.css')); sandbox.injectSystemJs(); sandbox.runMultipleFiles(files.filter(file => !file.test)); }); injectIframe(this.runnerElement.nativeElement, { id: 'testing', 'url': 'about:blank' }, this).then((sandbox) => { sandbox.runCss(require('./inner.css')); console.log('FRAME CREATED', (new Date()).getTime() - time); sandbox.injectSystemJs(); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('mocha')); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('chai')); sandbox.runSingleScriptFile(this.scriptLoaderService.getScript('test-bootstrap')); const testFiles = files .filter(file => !file.excludeFromTesting); sandbox.runMultipleFiles(testFiles); }); } else if (runner === 'Vue') { injectIframe(this.runnerElement.nativeElement, { id: 'preview', 'url': 'about:blank' }, this).then((sandbox) => { sandbox.runCss(require('./inner.css')); sandbox.setHtml('
    '); sandbox.runSingleFile(this.scriptLoaderService.getScript('vue')); sandbox.runSingleFile(files[0].code); }); } else if (runner === 'React') { injectIframe(this.runnerElement.nativeElement, { id: 'preview', 'url': 'about:blank' }, this).then((sandbox) => { sandbox.runCss(require('./inner.css')); sandbox.setHtml('
    '); sandbox.runSingleFile(this.scriptLoaderService.getScript('react')); sandbox.runSingleFile(this.scriptLoaderService.getScript('react-dom')); sandbox.runSingleFile(files[0].code); }); } else { throw new Error('No runner specified'); } } ngOnDestroy(): void { Object.keys(this.cachedIframes).map(key => { if (this.cachedIframes[key].canBeDeleted) { delete this.cachedIframes[key]; } }); window.removeEventListener('message', this.handleMessageBound, false); } ngAfterViewInit() { if (this.runnerElement == null && this.runnerConsoleElement != null) { this.runnerElement = this.runnerConsoleElement; } if (this.runnerElement != null) { this.runCode(this.files, this.runnerType); } } }