'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var child_process = require('child_process');
var os = require('os');
var url = require('url');

class NodeLogger {
    constructor() {
        this._level = 'info';
        this.writeLogQueue = [];
        this.buildLogFilePath = null;
        const rootDir = path.join(__dirname, '..', '..', '..');
        const distDir = path.join(rootDir, 'dist');
        const sysUtil = require(path.join(distDir, 'sys', 'node', 'sys-util.js'));
        this.chalk = sysUtil.chalk;
    }
    get level() {
        return this._level;
    }
    set level(l) {
        if (typeof l === 'string') {
            l = l.toLowerCase().trim();
            if (LOG_LEVELS.indexOf(l) === -1) {
                this.error(`Invalid log level '${this.chalk.bold(l)}' (choose from: ${LOG_LEVELS.map(l => this.chalk.bold(l)).join(', ')})`);
            }
            else {
                this._level = l;
            }
        }
    }
    info(...msg) {
        if (this.shouldLog('info')) {
            const lines = wordWrap(msg, getColumns());
            this.infoPrefix(lines);
            console.log(lines.join('\n'));
        }
        this.queueWriteLog('I', msg);
    }
    infoPrefix(lines) {
        if (lines.length) {
            const d = new Date();
            const prefix = '[' +
                ('0' + d.getMinutes()).slice(-2) + ':' +
                ('0' + d.getSeconds()).slice(-2) + '.' +
                Math.floor((d.getMilliseconds() / 1000) * 10) + ']';
            lines[0] = this.dim(prefix) + lines[0].substr(prefix.length);
        }
    }
    warn(...msg) {
        if (this.shouldLog('warn')) {
            const lines = wordWrap(msg, getColumns());
            this.warnPrefix(lines);
            console.warn('\n' + lines.join('\n') + '\n');
        }
        this.queueWriteLog('W', msg);
    }
    warnPrefix(lines) {
        if (lines.length) {
            const prefix = '[ WARN  ]';
            lines[0] = this.bold(this.chalk.yellow(prefix)) + lines[0].substr(prefix.length);
        }
    }
    error(...msg) {
        for (let i = 0; i < msg.length; i++) {
            if (msg[i] instanceof Error) {
                const err = msg[i];
                msg[i] = err.message;
                if (err.stack) {
                    msg[i] += '\n' + err.stack;
                }
            }
        }
        if (this.shouldLog('error')) {
            const lines = wordWrap(msg, getColumns());
            this.errorPrefix(lines);
            console.error('\n' + lines.join('\n') + '\n');
        }
        this.queueWriteLog('E', msg);
    }
    errorPrefix(lines) {
        if (lines.length) {
            const prefix = '[ ERROR ]';
            lines[0] = this.bold(this.chalk.red(prefix)) + lines[0].substr(prefix.length);
        }
    }
    debug(...msg) {
        if (this.shouldLog('debug')) {
            msg.push(this.dim(` MEM: ${(process.memoryUsage().rss / 1000000).toFixed(1)}MB`));
            const lines = wordWrap(msg, getColumns());
            this.debugPrefix(lines);
            console.log(lines.join('\n'));
        }
        this.queueWriteLog('D', msg);
    }
    debugPrefix(lines) {
        if (lines.length) {
            const d = new Date();
            const prefix = '[' +
                ('0' + d.getMinutes()).slice(-2) + ':' +
                ('0' + d.getSeconds()).slice(-2) + '.' +
                Math.floor((d.getMilliseconds() / 1000) * 10) + ']';
            lines[0] = this.chalk.cyan(prefix) + lines[0].substr(prefix.length);
        }
    }
    timespanStart(startMsg, debug) {
        const msg = [`${startMsg} ${this.dim('...')}`];
        if (debug) {
            if (this.shouldLog('debug')) {
                const lines = wordWrap(msg, getColumns());
                this.debugPrefix(lines);
                console.log(lines.join('\n'));
                this.queueWriteLog('D', [`${startMsg} ...`]);
            }
        }
        else {
            const lines = wordWrap(msg, getColumns());
            this.infoPrefix(lines);
            console.log(lines.join('\n'));
            this.queueWriteLog('I', [`${startMsg} ...`]);
        }
    }
    timespanFinish(finishMsg, timeSuffix, color, bold, newLineSuffix, debug) {
        let msg = finishMsg;
        if (color) {
            msg = this.color(finishMsg, color);
        }
        if (bold) {
            msg = this.bold(msg);
        }
        msg += ' ' + this.dim(timeSuffix);
        if (debug) {
            if (this.shouldLog('debug')) {
                const lines = wordWrap([msg], getColumns());
                this.debugPrefix(lines);
                console.log(lines.join('\n'));
            }
            this.queueWriteLog('D', [`${finishMsg} ${timeSuffix}`]);
        }
        else {
            const lines = wordWrap([msg], getColumns());
            this.infoPrefix(lines);
            console.log(lines.join('\n'));
            this.queueWriteLog('I', [`${finishMsg} ${timeSuffix}`]);
        }
        if (newLineSuffix) {
            console.log('');
        }
    }
    queueWriteLog(prefix, msg) {
        if (this.buildLogFilePath) {
            const d = new Date();
            const log = '' +
                ('0' + d.getHours()).slice(-2) + ':' +
                ('0' + d.getMinutes()).slice(-2) + ':' +
                ('0' + d.getSeconds()).slice(-2) + '.' +
                ('0' + Math.floor((d.getMilliseconds() / 1000) * 10)) +
                '  ' +
                ('000' + (process.memoryUsage().rss / 1000000).toFixed(1)).slice(-6) + 'MB' +
                '  ' + prefix +
                '  ' +
                msg.join(', ');
            this.writeLogQueue.push(log);
        }
    }
    writeLogs(append) {
        if (this.buildLogFilePath) {
            try {
                this.queueWriteLog('F', ['--------------------------------------']);
                const log = this.writeLogQueue.join('\n');
                if (append) {
                    try {
                        fs.accessSync(this.buildLogFilePath);
                    }
                    catch (e) {
                        append = false;
                    }
                }
                if (append) {
                    fs.appendFileSync(this.buildLogFilePath, log);
                }
                else {
                    fs.writeFileSync(this.buildLogFilePath, log);
                }
            }
            catch (e) { }
        }
        this.writeLogQueue.length = 0;
    }
    color(msg, color) {
        return this.chalk[color](msg);
    }
    red(msg) {
        return this.chalk.red(msg);
    }
    green(msg) {
        return this.chalk.green(msg);
    }
    yellow(msg) {
        return this.chalk.yellow(msg);
    }
    blue(msg) {
        return this.chalk.blue(msg);
    }
    magenta(msg) {
        return this.chalk.magenta(msg);
    }
    cyan(msg) {
        return this.chalk.cyan(msg);
    }
    gray(msg) {
        return this.chalk.gray(msg);
    }
    bold(msg) {
        return this.chalk.bold(msg);
    }
    dim(msg) {
        return this.chalk.dim(msg);
    }
    shouldLog(level) {
        return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.level);
    }
    createTimeSpan(startMsg, debug = false) {
        return new CmdTimeSpan(this, startMsg, debug);
    }
    printDiagnostics(diagnostics) {
        if (!diagnostics || !diagnostics.length)
            return;
        let outputLines = [''];
        diagnostics.forEach(d => {
            outputLines = outputLines.concat(this.printDiagnostic(d));
        });
        console.log(outputLines.join('\n'));
    }
    printDiagnostic(d) {
        const outputLines = wordWrap([d.messageText], getColumns());
        let header = '';
        if (d.header) {
            header += d.header;
        }
        if (d.relFilePath) {
            if (header.length > 0) {
                header += ': ';
            }
            header += this.chalk.cyan(d.relFilePath);
            if (typeof d.lineNumber === 'number' && d.lineNumber > -1) {
                header += this.chalk.dim(`:`);
                header += this.chalk.yellow(`${d.lineNumber}`);
                if (typeof d.columnNumber === 'number' && d.columnNumber > -1) {
                    header += this.chalk.dim(`:`);
                    header += this.chalk.yellow(`${d.columnNumber}`);
                }
            }
        }
        if (header.length > 0) {
            outputLines.unshift(INDENT + header);
        }
        outputLines.push('');
        if (d.lines && d.lines.length) {
            const lines = prepareLines(d.lines);
            lines.forEach(l => {
                if (!isMeaningfulLine(l.text)) {
                    return;
                }
                let msg = `L${l.lineNumber}:  `;
                while (msg.length < INDENT.length) {
                    msg = ' ' + msg;
                }
                let text = l.text;
                if (l.errorCharStart > -1) {
                    text = this.highlightError(text, l.errorCharStart, l.errorLength);
                }
                msg = this.dim(msg);
                if (d.language === 'typescript' || d.language === 'javascript') {
                    msg += this.javaScriptSyntaxHighlight(text);
                }
                else if (d.language === 'scss' || d.language === 'css') {
                    msg += this.cssSyntaxHighlight(text);
                }
                else {
                    msg += text;
                }
                outputLines.push(msg);
            });
            outputLines.push('');
        }
        if (d.level === 'error') {
            this.errorPrefix(outputLines);
        }
        else if (d.level === 'warn') {
            this.warnPrefix(outputLines);
        }
        else if (d.level === 'debug') {
            this.debugPrefix(outputLines);
        }
        else {
            this.infoPrefix(outputLines);
        }
        return outputLines;
    }
    highlightError(errorLine, errorCharStart, errorLength) {
        let rightSideChars = errorLine.length - errorCharStart + errorLength - 1;
        while (errorLine.length + INDENT.length > MAX_COLUMNS) {
            if (errorCharStart > (errorLine.length - errorCharStart + errorLength) && errorCharStart > 5) {
                // larger on left side
                errorLine = errorLine.substr(1);
                errorCharStart--;
            }
            else if (rightSideChars > 1) {
                // larger on right side
                errorLine = errorLine.substr(0, errorLine.length - 1);
                rightSideChars--;
            }
            else {
                break;
            }
        }
        const lineChars = [];
        const lineLength = Math.max(errorLine.length, errorCharStart + errorLength);
        for (var i = 0; i < lineLength; i++) {
            var chr = errorLine.charAt(i);
            if (i >= errorCharStart && i < errorCharStart + errorLength) {
                chr = this.chalk.bgRed(chr === '' ? ' ' : chr);
            }
            lineChars.push(chr);
        }
        return lineChars.join('');
    }
    javaScriptSyntaxHighlight(text) {
        if (text.trim().startsWith('//')) {
            return this.dim(text);
        }
        const words = text.split(' ').map(word => {
            if (JS_KEYWORDS.indexOf(word) > -1) {
                return this.chalk.cyan(word);
            }
            return word;
        });
        return words.join(' ');
    }
    cssSyntaxHighlight(text) {
        let cssProp = true;
        const safeChars = 'abcdefghijklmnopqrstuvwxyz-_';
        const notProp = '.#,:}@$[]/*';
        const chars = [];
        for (var i = 0; i < text.length; i++) {
            const c = text.charAt(i);
            if (c === ';' || c === '{') {
                cssProp = true;
            }
            else if (notProp.indexOf(c) > -1) {
                cssProp = false;
            }
            if (cssProp && safeChars.indexOf(c.toLowerCase()) > -1) {
                chars.push(this.chalk.cyan(c));
                continue;
            }
            chars.push(c);
        }
        return chars.join('');
    }
}
class CmdTimeSpan {
    constructor(logger, startMsg, debug) {
        this.debug = debug;
        this.logger = logger;
        this.start = Date.now();
        this.logger.timespanStart(startMsg, debug);
    }
    finish(msg, color, bold, newLineSuffix) {
        const duration = Date.now() - this.start;
        let time;
        if (duration > 1000) {
            time = 'in ' + (duration / 1000).toFixed(2) + ' s';
        }
        else {
            const ms = parseFloat((duration).toFixed(3));
            if (ms > 0) {
                time = 'in ' + duration + ' ms';
            }
            else {
                time = 'in less than 1 ms';
            }
        }
        this.logger.timespanFinish(msg, time, color, bold, newLineSuffix, this.debug);
    }
}
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
function getColumns() {
    const terminalWidth = (process.stdout && process.stdout.columns) || 80;
    return Math.max(Math.min(MAX_COLUMNS, terminalWidth), MIN_COLUMNS);
}
function wordWrap(msg, columns) {
    const lines = [];
    const words = [];
    msg.forEach(m => {
        if (m === null) {
            words.push('null');
        }
        else if (typeof m === 'undefined') {
            words.push('undefined');
        }
        else if (typeof m === 'string') {
            m.replace(/\s/gm, ' ').split(' ').forEach(strWord => {
                if (strWord.trim().length) {
                    words.push(strWord.trim());
                }
            });
        }
        else if (typeof m === 'number' || typeof m === 'boolean' || typeof m === 'function') {
            words.push(m.toString());
        }
        else if (Array.isArray(m)) {
            words.push(() => {
                return m.toString();
            });
        }
        else if (Object(m) === m) {
            words.push(() => {
                return m.toString();
            });
        }
        else {
            words.push(m.toString());
        }
    });
    let line = INDENT;
    words.forEach(word => {
        if (lines.length > 25) {
            return;
        }
        if (typeof word === 'function') {
            if (line.trim().length) {
                lines.push(line);
            }
            lines.push(word());
            line = INDENT;
        }
        else if (INDENT.length + word.length > columns - 1) {
            // word is too long to play nice, just give it its own line
            if (line.trim().length) {
                lines.push(line);
            }
            lines.push(INDENT + word);
            line = INDENT;
        }
        else if ((word.length + line.length) > columns - 1) {
            // this word would make the line too long
            // print the line now, then start a new one
            lines.push(line);
            line = INDENT + word + ' ';
        }
        else {
            line += word + ' ';
        }
    });
    if (line.trim().length) {
        lines.push(line);
    }
    return lines.map(line => {
        return line.trimRight();
    });
}
function prepareLines(orgLines) {
    const lines = JSON.parse(JSON.stringify(orgLines));
    for (let i = 0; i < 100; i++) {
        if (!eachLineHasLeadingWhitespace(lines)) {
            return lines;
        }
        for (let i = 0; i < lines.length; i++) {
            lines[i].text = lines[i].text.substr(1);
            lines[i].errorCharStart--;
            if (!(lines[i]).text.length) {
                return lines;
            }
        }
    }
    return lines;
}
function eachLineHasLeadingWhitespace(lines) {
    if (!lines.length) {
        return false;
    }
    for (var i = 0; i < lines.length; i++) {
        if (!lines[i].text || lines[i].text.length < 1) {
            return false;
        }
        const firstChar = lines[i].text.charAt(0);
        if (firstChar !== ' ' && firstChar !== '\t') {
            return false;
        }
    }
    return true;
}
function isMeaningfulLine(line) {
    if (line) {
        line = line.trim();
        if (line.length) {
            return (MEH_LINES.indexOf(line) < 0);
        }
    }
    return false;
}
const MEH_LINES = [';', ':', '{', '}', '(', ')', '/**', '/*', '*/', '*', '({', '})'];
const JS_KEYWORDS = [
    'abstract', 'any', 'as', 'break', 'boolean', 'case', 'catch', 'class',
    'console', 'const', 'continue', 'debugger', 'declare', 'default', 'delete',
    'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'from',
    'function', 'get', 'if', 'import', 'in', 'implements', 'Infinity',
    'instanceof', 'let', 'module', 'namespace', 'NaN', 'new', 'number', 'null',
    'public', 'private', 'protected', 'require', 'return', 'static', 'set',
    'string', 'super', 'switch', 'this', 'throw', 'try', 'true', 'type',
    'typeof', 'undefined', 'var', 'void', 'with', 'while', 'yield',
];
const INDENT = '           ';
const MIN_COLUMNS = 60;
const MAX_COLUMNS = 120;

function createServer(compilerCtx, outputTarget) {
    if (compilerCtx.localPrerenderServer)
        return;
    const fs$$1 = require('fs');
    const http = require('http');
    compilerCtx.localPrerenderServer = http.createServer((request, response) => {
        const filePath = getFilePath(outputTarget, request.url);
        fs$$1.readFile(filePath, 'utf8', (err, data) => {
            if (err) {
                response.write(`<!--localPrerenderServer, error fetching: ${request.url} : ${err} -->`);
            }
            else {
                response.write(data);
            }
            response.end();
        });
    });
    compilerCtx.localPrerenderServer.listen(PORT);
}
function getFilePath(outputTarget, url$$1) {
    const path$$1 = require('path');
    const Url = require('url');
    const parsedUrl = Url.parse(url$$1);
    let pathname = parsedUrl.pathname;
    if (pathname.charAt(0) !== '/') {
        pathname = '/' + pathname;
    }
    if (pathname.startsWith(outputTarget.baseUrl)) {
        pathname = pathname.substring(outputTarget.baseUrl.length);
    }
    const filePath = path$$1.join(outputTarget.dir, pathname);
    return filePath;
}
function normalizeUrl(url$$1) {
    const Url = require('url');
    const parsedUrl = Url.parse(url$$1);
    if (!parsedUrl.protocol || !parsedUrl.hostname) {
        parsedUrl.protocol = 'http:';
        parsedUrl.host = 'localhost:' + PORT;
        url$$1 = Url.format(parsedUrl);
    }
    return url$$1;
}
const PORT = 53536;

function createContext(compilerCtx, outputTarget, sandbox) {
    const vm = require('vm');
    // https://github.com/tmpvar/jsdom/issues/1724
    // manually adding a fetch polyfill until jsdom adds it
    patchFetch(compilerCtx, outputTarget, sandbox);
    patchRaf(sandbox);
    return vm.createContext(sandbox);
}
function patchFetch(compilerCtx, outputTarget, sandbox) {
    function fetch(input, init) {
        const path$$1 = require('path');
        const nf = require(path$$1.join(__dirname, './node-fetch.js'));
        createServer(compilerCtx, outputTarget);
        if (typeof input === 'string') {
            // fetch(url)
            return nf.nodeFetch(normalizeUrl(input), init);
        }
        else {
            // fetch(Request)
            input.url = normalizeUrl(input.url);
            return nf.nodeFetch(input, init);
        }
    }
    sandbox.fetch = fetch;
}
function patchRaf(sandbox) {
    if (!sandbox.requestAnimationFrame) {
        sandbox.requestAnimationFrame = function (callback) {
            const id = sandbox.setTimeout(function () {
                callback(Date.now());
            }, 0);
            return id;
        };
        sandbox.cancelAnimationFrame = function (id) {
            clearTimeout(id);
        };
    }
}
function runInContext(code, contextifiedSandbox, options) {
    const vm = require('vm');
    vm.runInContext(code, contextifiedSandbox, options);
}

function createDom() {
    let dom = null;
    return {
        parse(opts) {
            if (dom) {
                dom.window.close();
            }
            const jsdom = require('jsdom');
            const jsdomOptions = {
                url: opts.url,
                referrer: opts.referrer,
                userAgent: opts.userAgent
            };
            if (opts.console) {
                jsdomOptions.virtualConsole = new jsdom.VirtualConsole();
                jsdomOptions.virtualConsole.sendTo(opts.console);
            }
            dom = new jsdom.JSDOM(opts.html, jsdomOptions);
            polyfillJsDom(dom.window);
            return dom.window;
        },
        serialize() {
            return dom.serialize();
        },
        destroy() {
            dom && dom.window && dom.window.close();
            dom = null;
        }
    };
}
function polyfillJsDom(window) {
    if (!window.Element.prototype.closest) {
        window.Element.prototype.closest = function (selector) {
            let el = this;
            while (el) {
                if (el.matches(selector)) {
                    return el;
                }
                el = el.parentElement;
            }
        };
    }
}

class NodeFs {
    copyFile(src, dest) {
        return new Promise((resolve, reject) => {
            const readStream = fs.createReadStream(src);
            readStream.on('error', reject);
            const writeStream = fs.createWriteStream(dest);
            writeStream.on('error', reject);
            writeStream.on('close', resolve);
            readStream.pipe(writeStream);
        });
    }
    createReadStream(filePath) {
        return fs.createReadStream(filePath);
    }
    mkdir(dirPath) {
        return new Promise((resolve, reject) => {
            fs.mkdir(dirPath, (err) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve();
                }
            });
        });
    }
    mkdirSync(dirPath) {
        fs.mkdirSync(dirPath);
    }
    readdir(dirPath) {
        return new Promise((resolve, reject) => {
            fs.readdir(dirPath, (err, files) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(files);
                }
            });
        });
    }
    readFile(filePath) {
        return new Promise((resolve, reject) => {
            fs.readFile(filePath, 'utf8', (err, content) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(content);
                }
            });
        });
    }
    readFileSync(filePath) {
        return fs.readFileSync(filePath, 'utf8');
    }
    rmdir(dirPath) {
        return new Promise((resolve, reject) => {
            fs.rmdir(dirPath, (err) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve();
                }
            });
        });
    }
    stat(itemPath) {
        return new Promise((resolve, reject) => {
            fs.stat(itemPath, (err, stats) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(stats);
                }
            });
        });
    }
    statSync(itemPath) {
        return fs.statSync(itemPath);
    }
    unlink(filePath) {
        return new Promise((resolve, reject) => {
            fs.unlink(filePath, (err) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve();
                }
            });
        });
    }
    writeFile(filePath, content) {
        return new Promise((resolve, reject) => {
            fs.writeFile(filePath, content, { encoding: 'utf8' }, (err) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve();
                }
            });
        });
    }
    writeFileSync(filePath, content) {
        return fs.writeFileSync(filePath, content, { encoding: 'utf8' });
    }
}

/**
 * SSR Attribute Names
 */

function normalizePath(str) {
    // Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
    // https://github.com/sindresorhus/slash MIT
    // By Sindre Sorhus
    if (typeof str !== 'string') {
        throw new Error(`invalid path to normalize`);
    }
    str = str.trim();
    if (EXTENDED_PATH_REGEX.test(str) || NON_ASCII_REGEX.test(str)) {
        return str;
    }
    str = str.replace(SLASH_REGEX, '/');
    // always remove the trailing /
    // this makes our file cache look ups consistent
    if (str.charAt(str.length - 1) === '/') {
        const colonIndex = str.indexOf(':');
        if (colonIndex > -1) {
            if (colonIndex < str.length - 2) {
                str = str.substring(0, str.length - 1);
            }
        }
        else if (str.length > 1) {
            str = str.substring(0, str.length - 1);
        }
    }
    return str;
}
const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
const NON_ASCII_REGEX = /[^\x00-\x80]+/;
const SLASH_REGEX = /\\/g;

class WorkerFarm {
    constructor(modulePath, options = {}) {
        this.workers = [];
        this.taskQueue = [];
        this.isExisting = false;
        this.options = Object.assign({}, DEFAULT_OPTIONS, options);
        this.modulePath = modulePath;
        this.logger = {
            error: function () {
                console.error.apply(console, arguments);
            }
        };
        if (this.options.maxConcurrentWorkers > 1) {
            this.startWorkers();
        }
        else {
            this.workerModule = require(modulePath);
            this.singleThreadRunner = new this.workerModule.createRunner();
        }
    }
    run(methodName, args, opts = {}) {
        if (this.isExisting) {
            return Promise.reject(`process exited`);
        }
        if (this.singleThreadRunner) {
            return this.singleThreadRunner(methodName, args);
        }
        return new Promise((resolve, reject) => {
            const task = {
                methodName: methodName,
                args: args,
                isLongRunningTask: !!opts.isLongRunningTask,
                resolve: resolve,
                reject: reject
            };
            if (typeof opts.workerKey === 'string') {
                // this tasl has a worker key so that it always uses
                // the same worker, this way it can reuse that worker's cache again
                // let's figure out its worker id which should always be
                // the same id number for the same worker key string
                const workerId = getWorkerIdFromKey(opts.workerKey, this.workers);
                const worker = this.workers.find(w => w.workerId === workerId);
                if (!worker) {
                    task.reject(`invalid worker id for task: ${task}`);
                }
                else {
                    this.send(worker, task);
                }
            }
            else {
                // add this task to the queue to be processed
                // and assigned to the next available worker
                this.taskQueue.push(task);
                this.processTaskQueue();
            }
        });
    }
    startWorkers() {
        for (let workerId = 0; workerId < this.options.maxConcurrentWorkers; workerId++) {
            const worker = this.createWorker(workerId);
            worker.tasks = [];
            worker.totalTasksAssigned = 0;
            this.workers.push(worker);
        }
        process.once('exit', this.destroy.bind(this));
    }
    createWorker(workerId) {
        const argv = [
            `--start-worker`,
            `--worker-id=${workerId}`
        ];
        const options = {
            env: process.env,
            cwd: process.cwd()
        };
        const childProcess = child_process.fork(this.modulePath, argv, options);
        const worker = {
            workerId: workerId,
            taskIds: 0,
            send: (msg) => childProcess.send(msg),
            kill: () => childProcess.kill('SIGKILL')
        };
        childProcess.on('message', this.receiveMessageFromWorker.bind(this));
        childProcess.once('exit', code => {
            this.onWorkerExit(workerId, code);
        });
        childProcess.on('error', err => {
            this.receiveMessageFromWorker({
                workerId: workerId,
                error: {
                    message: `Worker (${workerId}) process error: ${err.message}`,
                    stack: err.stack
                }
            });
        });
        return worker;
    }
    onWorkerExit(workerId, exitCode) {
        const worker = this.workers.find(w => w.workerId === workerId);
        if (!worker) {
            return;
        }
        worker.exitCode = exitCode;
        setTimeout(() => {
            this.stopWorker(workerId);
        }, 10);
    }
    stopWorker(workerId) {
        const worker = this.workers.find(w => w.workerId === workerId);
        if (worker && !worker.isExisting) {
            worker.isExisting = true;
            worker.tasks.forEach(task => {
                task.reject(WORKER_EXITED_MSG);
            });
            worker.tasks.length = 0;
            worker.send({
                exitProcess: true
            });
            const tmr = setTimeout(() => {
                if (worker.exitCode == null) {
                    worker.kill();
                }
            }, this.options.forcedKillTime);
            tmr.unref && tmr.unref();
            const index = this.workers.indexOf(worker);
            if (index > -1) {
                this.workers.splice(index, 1);
            }
        }
    }
    receiveMessageFromWorker(msg) {
        // message sent back from a worker process
        if (this.isExisting) {
            // already exiting, don't bother
            return;
        }
        const worker = this.workers.find(w => w.workerId === msg.workerId);
        if (!worker) {
            this.logger.error(`Received message for unknown worker (${msg.workerId})`);
            return;
        }
        const task = worker.tasks.find(w => w.taskId === msg.taskId);
        if (!task) {
            this.logger.error(`Worker (${worker.workerId}) received message for unknown taskId (${msg.taskId})`);
            return;
        }
        if (task.timer) {
            clearTimeout(task.timer);
        }
        const index = worker.tasks.indexOf(task);
        if (index > -1) {
            worker.tasks.splice(index, 1);
        }
        process.nextTick(() => {
            if (msg.error) {
                task.reject(msg.error.message);
            }
            else {
                task.resolve(msg.value);
            }
            // overkill yes, but let's ensure we've cleaned up this task
            task.args = null;
            task.reject = null;
            task.resolve = null;
            task.timer = null;
        });
        // allow any outstanding tasks to be processed
        this.processTaskQueue();
    }
    workerTimeout(workerId) {
        const worker = this.workers.find(w => w.workerId === workerId);
        if (!worker) {
            return;
        }
        worker.tasks.forEach(task => {
            this.receiveMessageFromWorker({
                taskId: task.taskId,
                workerId: workerId,
                error: {
                    message: `Worker (${workerId}) timed out! Canceled "${task.methodName}" task.`
                }
            });
        });
        this.stopWorker(workerId);
    }
    processTaskQueue() {
        while (this.taskQueue.length > 0) {
            const worker = nextAvailableWorker(this.workers, this.options.maxConcurrentTasksPerWorker);
            if (worker) {
                // we found a worker to send this task to
                this.send(worker, this.taskQueue.shift());
            }
            else {
                // no worker available ATM, we'll try again later
                break;
            }
        }
    }
    send(worker, task) {
        if (!worker || !task) {
            return;
        }
        task.taskId = worker.taskIds++;
        worker.tasks.push(task);
        worker.totalTasksAssigned++;
        worker.send({
            workerId: worker.workerId,
            taskId: task.taskId,
            methodName: task.methodName,
            args: task.args
        });
        // no need to keep these args in memory at this point
        task.args = null;
        if (this.options.maxTaskTime !== Infinity) {
            task.timer = setTimeout(this.workerTimeout.bind(this, worker.workerId), this.options.maxTaskTime);
        }
    }
    destroy() {
        if (!this.isExisting) {
            this.isExisting = true;
            // workers may already be getting removed
            // so doing it this way cuz we don't know if the
            // order of the workers array is consistent
            const workerIds = this.workers.map(worker => worker.workerId);
            workerIds.forEach(workerId => {
                this.stopWorker(workerId);
            });
        }
    }
}
function nextAvailableWorker(workers, maxConcurrentTasksPerWorker) {
    const availableWorkers = workers.filter(w => {
        if (w.tasks.length >= maxConcurrentTasksPerWorker) {
            // do not use this worker if it's at its max
            return false;
        }
        if (w.tasks.some(t => t && t.isLongRunningTask)) {
            // one of the tasks for this worker is a long running task
            // so leave this worker alone and let it focus
            // basically so the many little tasks don't have to wait up on the long task
            return false;
        }
        // let's use this worker for this task
        return true;
    });
    if (availableWorkers.length === 0) {
        // all workers are pretty tasked at the moment, please come back later. Thank you.
        return null;
    }
    const sorted = availableWorkers.sort((a, b) => {
        // worker with the fewest active tasks first
        if (a.tasks.length < b.tasks.length)
            return -1;
        if (a.tasks.length > b.tasks.length)
            return 1;
        // all workers have the same number of active takss, so next sort
        // by worker with the fewest total tasks that have been assigned
        if (a.totalTasksAssigned < b.totalTasksAssigned)
            return -1;
        if (a.totalTasksAssigned > b.totalTasksAssigned)
            return 1;
        return 0;
    });
    return sorted[0];
}
function getWorkerIdFromKey(workerKey, workers) {
    const hashChar = crypto.createHash('sha1')
        .update(workerKey)
        .digest('base64')
        .charAt(0);
    const workerIds = workers.map(w => w.workerId);
    const b64Int = B64_TABLE[hashChar];
    const dv = b64Int / 64;
    const mt = (workerIds.length - 1) * dv;
    const workerIndex = Math.round(mt);
    return workerIds[workerIndex];
}
const B64_TABLE = {
    'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'J': 9, 'K': 10, 'L': 11, 'M': 12,
    'N': 13, 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20, 'V': 21, 'W': 22, 'X': 23,
    'Y': 24, 'Z': 25, 'a': 26, 'b': 27, 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34,
    'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41, 'q': 42, 'r': 43, 's': 44, 't': 45,
    'u': 46, 'v': 47, 'w': 48, 'x': 49, 'y': 50, 'z': 51, '0': 52, '1': 53, '2': 54, '3': 55, '4': 56,
    '5': 57, '6': 58, '7': 59, '8': 60, '9': 61, '+': 62, '/': 63,
};
const DEFAULT_OPTIONS = {
    maxConcurrentWorkers: 1,
    maxConcurrentTasksPerWorker: 5,
    maxTaskTime: 120000,
    forcedKillTime: 100
};
const WORKER_EXITED_MSG = `worker has exited`;

var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
class NodeSystem {
    constructor(fs$$1) {
        this.resolveModuleCache = {};
        this.destroys = [];
        this.fs = fs$$1 || new NodeFs();
        this.path = path;
        const rootDir = path.join(__dirname, '..', '..', '..');
        this.distDir = path.join(rootDir, 'dist');
        this.sysUtil = require(path.join(this.distDir, 'sys', 'node', 'sys-util.js'));
        try {
            this.packageJsonData = require(path.join(rootDir, 'package.json'));
        }
        catch (e) {
            throw new Error(`unable to resolve "package.json" from: ${rootDir}`);
        }
        try {
            this.typescriptPackageJson = require(this.resolveModule(rootDir, 'typescript'));
        }
        catch (e) {
            throw new Error(`unable to resolve "typescript" from: ${rootDir}`);
        }
    }
    initWorkers(maxConcurrentWorkers) {
        if (this.sysWorker) {
            return maxConcurrentWorkers;
        }
        const workerModulePath = require.resolve(path.join(this.distDir, 'sys', 'node', 'sys-worker.js'));
        const availableCpus = os.cpus().length;
        if (typeof maxConcurrentWorkers === 'number') {
            maxConcurrentWorkers = Math.max(1, Math.min(availableCpus, maxConcurrentWorkers));
        }
        else {
            maxConcurrentWorkers = availableCpus;
        }
        this.sysWorker = new WorkerFarm(workerModulePath, {
            maxConcurrentWorkers: maxConcurrentWorkers
        });
        this.addDestroy(() => {
            if (this.sysWorker && this.sysWorker.destroy) {
                this.sysWorker.destroy();
            }
        });
        return maxConcurrentWorkers;
    }
    destroy() {
        this.destroys.forEach(destroyFn => {
            destroyFn();
        });
        this.destroys.length = 0;
    }
    addDestroy(fn) {
        this.destroys.push(fn);
    }
    get compiler() {
        return {
            name: this.packageJsonData.name,
            version: this.packageJsonData.version,
            runtime: path.join(this.distDir, 'compiler', 'index.js'),
            typescriptVersion: this.typescriptPackageJson.version
        };
    }
    autoprefixCss(input, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.sysWorker.run('autoprefixCss', [input, opts]);
        });
    }
    copy(copyTasks) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.sysWorker.run('copy', [copyTasks], { isLongRunningTask: true });
        });
    }
    get createDom() {
        if (this._existingDom) {
            return this._existingDom;
        }
        return createDom;
    }
    set createDom(val) {
        this._existingDom = val;
    }
    createWatcher(events, paths, opts) {
        const chokidar = require('chokidar');
        const watcher = chokidar.watch(paths, opts);
        watcher
            .on('change', (path$$1) => {
            events.emit('fileUpdate', path$$1);
        })
            .on('add', (path$$1) => {
            events.emit('fileAdd', path$$1);
        })
            .on('unlink', (path$$1) => {
            events.emit('fileDelete', path$$1);
        })
            .on('addDir', (path$$1) => {
            events.emit('dirAdd', path$$1);
        })
            .on('unlinkDir', (path$$1) => {
            events.emit('dirDelete', path$$1);
        })
            .on('error', (err) => {
            console.error(err);
        });
        return watcher;
    }
    generateContentHash(content, length) {
        return crypto.createHash('sha1')
            .update(content)
            .digest('base64')
            .replace(/\W/g, '')
            .substr(0, length)
            .toLowerCase();
    }
    getClientCoreFile(opts) {
        const filePath = normalizePath(path.join(this.distDir, 'client', opts.staticName));
        return this.fs.readFile(filePath);
    }
    glob(pattern, opts) {
        return new Promise((resolve, reject) => {
            this.sysUtil.glob(pattern, opts, (err, files) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(files);
                }
            });
        });
    }
    gzipSize(text) {
        return this.sysWorker.run('gzipSize', [text]);
    }
    isGlob(str) {
        return this.sysUtil.isGlob(str);
    }
    loadConfigFile(configPath, process) {
        let config;
        let cwd = '';
        if (process) {
            if (process.cwd) {
                cwd = process.cwd();
            }
            if (process.env && typeof process.env.PWD === 'string') {
                cwd = process.env.PWD;
            }
        }
        let hasConfigFile = false;
        if (typeof configPath === 'string') {
            if (!path.isAbsolute(configPath)) {
                throw new Error(`Stencil configuration file "${configPath}" must be an absolute path.`);
            }
            try {
                let fileStat = this.fs.statSync(configPath);
                if (fileStat.isFile()) {
                    hasConfigFile = true;
                }
                else if (fileStat.isDirectory()) {
                    // this is only a directory, so let's just assume we're looking for in stencil.config.js
                    // otherwise they could pass in an absolute path if it was somewhere else
                    configPath = path.join(configPath, 'stencil.config.js');
                    fileStat = this.fs.statSync(configPath);
                    hasConfigFile = fileStat.isFile();
                }
            }
            catch (e) { }
        }
        if (hasConfigFile) {
            // the passed in config was a string, so it's probably a path to the config we need to load
            // first clear the require cache so we don't get the same file
            delete require.cache[path.resolve(configPath)];
            const configFileData = require(configPath);
            if (!configFileData.config) {
                throw new Error(`Invalid Stencil configuration file "${configPath}". Missing "config" property.`);
            }
            config = configFileData.config;
            config.configPath = configPath;
            if (!config.rootDir && configPath) {
                config.rootDir = path.dirname(configPath);
            }
        }
        else {
            // no stencil.config.js file, which is fine
            config = {
                rootDir: cwd
            };
        }
        if (!config.sys) {
            config.sys = this;
        }
        config.cwd = cwd;
        return config;
    }
    minifyCss(input, filePath, opts = {}) {
        return this.sysWorker.run('minifyCss', [input, filePath, opts]);
    }
    minifyJs(input, opts) {
        return this.sysWorker.run('minifyJs', [input, opts]);
    }
    minimatch(filePath, pattern, opts) {
        return this.sysUtil.minimatch(filePath, pattern, opts);
    }
    open(p) {
        return this.sysUtil.opn(p);
    }
    get details() {
        const details = {
            cpuModel: '',
            cpus: -1,
            freemem: -1,
            platform: '',
            release: '',
            runtime: 'node',
            runtimeVersion: ''
        };
        try {
            const cpus = os.cpus();
            details.cpuModel = cpus[0].model;
            details.cpus = cpus.length;
            details.freemem = os.freemem();
            details.platform = os.platform();
            details.release = os.release();
            details.runtimeVersion = process.version;
        }
        catch (e) { }
        return details;
    }
    resolveModule(fromDir, moduleId) {
        const cacheKey = `${fromDir}:${moduleId}`;
        if (this.resolveModuleCache[cacheKey]) {
            return this.resolveModuleCache[cacheKey];
        }
        const Module = require('module');
        fromDir = path.resolve(fromDir);
        const fromFile = path.join(fromDir, 'noop.js');
        let dir = Module._resolveFilename(moduleId, {
            id: fromFile,
            filename: fromFile,
            paths: Module._nodeModulePaths(fromDir)
        });
        const root = path.parse(fromDir).root;
        let packageJsonFilePath;
        while (dir !== root) {
            dir = path.dirname(dir);
            packageJsonFilePath = path.join(dir, 'package.json');
            try {
                fs.accessSync(packageJsonFilePath);
            }
            catch (e) {
                continue;
            }
            this.resolveModuleCache[cacheKey] = packageJsonFilePath;
            return packageJsonFilePath;
        }
        throw new Error(`error loading "${moduleId}" from "${fromDir}"`);
    }
    get rollup() {
        const rollup = require('rollup');
        rollup.plugins = {
            commonjs: require('rollup-plugin-commonjs'),
            nodeResolve: require('rollup-plugin-node-resolve')
        };
        return rollup;
    }
    scopeCss(cssText, scopeAttribute, hostScopeAttr, slotScopeAttr) {
        return this.sysWorker.run('scopeCss', [cssText, scopeAttribute, hostScopeAttr, slotScopeAttr]);
    }
    get semver() {
        return this.sysUtil.semver;
    }
    transpileToEs5(cwd, input) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.sysWorker.run('transpileToEs5', [cwd, input]);
        });
    }
    get url() {
        return url;
    }
    validateTypes(compilerOptions, emitDtsFiles, currentWorkingDir, collectionNames, rootTsFiles) {
        return this.sysWorker.run('validateTypes', [compilerOptions, emitDtsFiles, currentWorkingDir, collectionNames, rootTsFiles], { isLongRunningTask: true, workerKey: 'validateTypes' });
    }
    get vm() {
        return {
            createContext,
            runInContext
        };
    }
    get workbox() {
        return require('workbox-build');
    }
}

exports.NodeLogger = NodeLogger;
exports.NodeSystem = NodeSystem;
