diff --git a/src/sys/node/bundles/glob.js b/src/sys/node/bundles/glob.js new file mode 100644 index 00000000000..29973fc267a --- /dev/null +++ b/src/sys/node/bundles/glob.js @@ -0,0 +1 @@ +module.exports = require('glob'); diff --git a/src/sys/node/bundles/prompts.js b/src/sys/node/bundles/prompts.js new file mode 100644 index 00000000000..02e9a974df4 --- /dev/null +++ b/src/sys/node/bundles/prompts.js @@ -0,0 +1,3 @@ +// we already know node will be greater than 8.6.0 +// https://github.com/terkelg/prompts/blob/master/index.js +module.exports = require('prompts/lib/index.js'); diff --git a/src/sys/node/index.ts b/src/sys/node/index.ts new file mode 100644 index 00000000000..4998e385565 --- /dev/null +++ b/src/sys/node/index.ts @@ -0,0 +1,5 @@ +export { createNodeLogger } from './node-logger'; +export { createNodeSysWithWatch } from './node-sys-watch'; +export { createNodeSys } from './node-sys'; +export { checkVersion } from './node-stencil-version-checker'; +export { setupNodeProcess } from './node-setup-process'; diff --git a/src/sys/node/node-copy-tasks.ts b/src/sys/node/node-copy-tasks.ts index b1d93606b84..8f890c58285 100644 --- a/src/sys/node/node-copy-tasks.ts +++ b/src/sys/node/node-copy-tasks.ts @@ -177,7 +177,8 @@ const IGNORE = ['.ds_store', '.gitignore', 'desktop.ini', 'thumbs.db']; export function asyncGlob(pattern: string, opts: any) { return new Promise((resolve, reject) => { - glob(pattern, opts, (err: any, files: string[]) => { + const g: typeof glob = (glob as any).glob; + g(pattern, opts, (err: any, files: string[]) => { if (err) { reject(err); } else { diff --git a/src/sys/node/node-fs-promisify.ts b/src/sys/node/node-fs-promisify.ts index a910f5c6de3..1a13adbf050 100644 --- a/src/sys/node/node-fs-promisify.ts +++ b/src/sys/node/node-fs-promisify.ts @@ -4,4 +4,5 @@ import { promisify } from 'util'; export const copyFile = promisify(fs.copyFile); export const mkdir = promisify(fs.mkdir); export const readdir = promisify(fs.readdir); +export const readFile = promisify(fs.readFile); export const stat = promisify(fs.stat); diff --git a/src/sys/node/node-lazy-require.ts b/src/sys/node/node-lazy-require.ts index d89905efee7..9dff860ede2 100644 --- a/src/sys/node/node-lazy-require.ts +++ b/src/sys/node/node-lazy-require.ts @@ -1,17 +1,14 @@ import * as d from '../../declarations'; -import { dirname } from 'path'; +import path from 'path'; import { NodeResolveModule } from './node-resolve-module'; -import { readFile } from 'graceful-fs'; +import { readFile } from './node-fs-promisify'; import { SpawnOptions, spawn } from 'child_process'; import semiver from 'semiver'; export class NodeLazyRequire implements d.LazyRequire { private moduleData = new Map(); - constructor( - private nodeResolveModule: NodeResolveModule, - private lazyDependencies: {[dep: string]: [string, string]} - ) {} + constructor(private nodeResolveModule: NodeResolveModule, private lazyDependencies: { [dep: string]: [string, string] }) {} async ensure(logger: d.Logger, fromDir: string, ensureModuleIds: string[]) { const depsToInstall: DepToInstall[] = []; @@ -35,7 +32,7 @@ export class NodeLazyRequire implements d.LazyRequire { if (semiver(installedPkgJson.version, minVersion) >= 0) { this.moduleData.set(ensureModuleId, { fromDir: fromDir, - modulePath: dirname(resolvedPkgJsonPath), + modulePath: path.dirname(resolvedPkgJsonPath), }); return; } @@ -93,7 +90,7 @@ export class NodeLazyRequire implements d.LazyRequire { if (!moduleData.modulePath) { const modulePkgJsonPath = this.nodeResolveModule.resolveModule(moduleData.fromDir, moduleId); - moduleData.modulePath = dirname(modulePkgJsonPath); + moduleData.modulePath = path.dirname(modulePkgJsonPath); this.moduleData.set(moduleId, moduleData); } @@ -109,7 +106,7 @@ export class NodeLazyRequire implements d.LazyRequire { if (!moduleData.modulePath) { const modulePkgJsonPath = this.nodeResolveModule.resolveModule(moduleData.fromDir, moduleId); - moduleData.modulePath = dirname(modulePkgJsonPath); + moduleData.modulePath = path.dirname(modulePkgJsonPath); this.moduleData.set(moduleId, moduleData); } @@ -130,7 +127,7 @@ function npmInstall(logger: d.Logger, fromDir: string, moduleIds: string[]) { }; opts.env.NODE_ENV = 'development'; - if (logger.level === 'debug') { + if (logger.getLevel() === 'debug') { args.push('--verbose'); } @@ -156,7 +153,7 @@ function npmInstall(logger: d.Logger, fromDir: string, moduleIds: string[]) { } childProcess.once('exit', exitCode => { - if (logger.level === 'debug') { + if (logger.getLevel() === 'debug') { logger.debug(`${cmd}, exit ${exitCode}`); } @@ -169,20 +166,9 @@ function npmInstall(logger: d.Logger, fromDir: string, moduleIds: string[]) { }); } -function readPackageJson(pkgJsonPath: string) { - return new Promise((resolve, reject) => { - readFile(pkgJsonPath, 'utf8', (err, data) => { - if (err) { - reject(err); - } else { - try { - resolve(JSON.parse(data)); - } catch (e) { - reject(e); - } - } - }); - }); +async function readPackageJson(pkgJsonPath: string) { + const data = await readFile(pkgJsonPath, 'utf8'); + return JSON.parse(data) as d.PackageJsonData; } interface DepToInstall { diff --git a/src/sys/node/node-load-typescript.ts b/src/sys/node/node-load-typescript.ts new file mode 100644 index 00000000000..50e34949281 --- /dev/null +++ b/src/sys/node/node-load-typescript.ts @@ -0,0 +1,6 @@ +import { requireFunc } from '@utils'; + +export const nodeLoadTypeScript = (typeScriptPath: string) => { + const nodeModuleId = typeScriptPath || 'typescript'; + return requireFunc(nodeModuleId); +}; diff --git a/src/sys/node/node-logger.ts b/src/sys/node/node-logger.ts index b4223786bb9..b18b1ee44f0 100644 --- a/src/sys/node/node-logger.ts +++ b/src/sys/node/node-logger.ts @@ -1,688 +1,85 @@ -import { Diagnostic, Logger, LoggerTimeSpan, PrintLine } from '../../declarations'; -import color from 'ansi-colors'; +import { createTerminalLogger, ColorType, TerminalLoggerSys } from '../../compiler/sys/logger/terminal-logger'; +import ansiColor from 'ansi-colors'; import fs from 'graceful-fs'; import path from 'path'; -export function createNodeLogger(prcs: NodeJS.Process) { - return new NodeLogger(prcs); -} +export const createNodeLogger = (c: { process: any }) => { + let useColors = true; + const prcs: NodeJS.Process = c.process; + const minColumns = 60; + const maxColumns = 120; -export class NodeLogger implements Logger { - colors = true; - private _level = 'info'; - private writeLogQueue: string[] = []; - buildLogFilePath: string = null; + const color = (msg: string, colorType: ColorType) => (useColors ? (ansiColor as any)[colorType](msg) : msg); - constructor(private prcs: NodeJS.Process) {} + const cwd = () => prcs.cwd(); - get level() { - return this._level; - } + const emoji = (e: string) => (prcs.platform !== 'win32' ? e : ''); - set level(l: string) { - if (typeof l === 'string') { - l = l.toLowerCase().trim(); + const enableColors = (uc: boolean) => (useColors = uc); - if (LOG_LEVELS.indexOf(l) === -1) { - this.error(`Invalid log level '${this.bold(l)}' (choose from: ${LOG_LEVELS.map(l => this.bold(l)).join(', ')})`); - } else { - this._level = l; - } - } - } - - info(...msg: any[]) { - if (this.shouldLog('info')) { - const lines = wordWrap(msg, getColumns(this.prcs)); - this.infoPrefix(lines); - console.log(lines.join('\n')); - } - this.queueWriteLog('I', msg); - } - - infoPrefix(lines: string[]) { - 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: any[]) { - if (this.shouldLog('warn')) { - const lines = wordWrap(msg, getColumns(this.prcs)); - this.warnPrefix(lines); - console.warn('\n' + lines.join('\n') + '\n'); - } - this.queueWriteLog('W', msg); - } - - warnPrefix(lines: string[]) { - if (lines.length) { - const prefix = '[ WARN ]'; - lines[0] = this.bold(this.yellow(prefix)) + lines[0].substr(prefix.length); - } - } - - error(...msg: any[]) { - for (let i = 0; i < msg.length; i++) { - if (msg[i] instanceof Error) { - const err: Error = msg[i]; - msg[i] = err.message; - if (err.stack) { - msg[i] += '\n' + err.stack; - } - } - } - - if (this.shouldLog('error')) { - const lines = wordWrap(msg, getColumns(this.prcs)); - this.errorPrefix(lines); - console.error('\n' + lines.join('\n') + '\n'); - } - this.queueWriteLog('E', msg); - } - - errorPrefix(lines: string[]) { - if (lines.length) { - const prefix = '[ ERROR ]'; - lines[0] = this.bold(this.red(prefix)) + lines[0].substr(prefix.length); - } - } - - debug(...msg: any[]) { - if (this.shouldLog('debug')) { - msg.push(this.dim(` MEM: ${(this.prcs.memoryUsage().rss / 1000000).toFixed(1)}MB`)); - const lines = wordWrap(msg, getColumns(this.prcs)); - this.debugPrefix(lines); - console.log(lines.join('\n')); - } - this.queueWriteLog('D', msg); - } + const getColumns = () => { + const terminalWidth = (prcs.stdout && (prcs.stdout as any).columns) || 80; + return Math.max(Math.min(maxColumns, terminalWidth), minColumns); + }; - debugPrefix(lines: string[]) { - if (lines.length) { - const d = new Date(); + const memoryUsage = () => prcs.memoryUsage().rss; - const prefix = '[' + ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2) + '.' + Math.floor((d.getMilliseconds() / 1000) * 10) + ']'; + const relativePath = (from: string, to: string) => path.relative(from, to); - lines[0] = this.cyan(prefix) + lines[0].substr(prefix.length); - } - } - - timespanStart(startMsg: string, debug: boolean, appendTo: string[]) { - const msg = [`${startMsg} ${this.dim('...')}`]; - - if (debug) { - if (this.shouldLog('debug')) { - msg.push(this.dim(` MEM: ${(this.prcs.memoryUsage().rss / 1000000).toFixed(1)}MB`)); - const lines = wordWrap(msg, getColumns(this.prcs)); - this.debugPrefix(lines); - console.log(lines.join('\n')); - this.queueWriteLog('D', [`${startMsg} ...`]); - } - } else { - const lines = wordWrap(msg, getColumns(this.prcs)); - this.infoPrefix(lines); - console.log(lines.join('\n')); - this.queueWriteLog('I', [`${startMsg} ...`]); - if (appendTo) { - appendTo.push(`${startMsg} ...`); - } - } - } - - timespanFinish(finishMsg: string, timeSuffix: string, color: 'red', bold: boolean, newLineSuffix: boolean, debug: boolean, appendTo: string[]) { - 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 m = [msg]; - m.push(this.dim(` MEM: ${(this.prcs.memoryUsage().rss / 1000000).toFixed(1)}MB`)); - const lines = wordWrap(m, getColumns(this.prcs)); - this.debugPrefix(lines); - console.log(lines.join('\n')); - } - this.queueWriteLog('D', [`${finishMsg} ${timeSuffix}`]); - } else { - const lines = wordWrap([msg], getColumns(this.prcs)); - this.infoPrefix(lines); - console.log(lines.join('\n')); - this.queueWriteLog('I', [`${finishMsg} ${timeSuffix}`]); - - if (appendTo) { - appendTo.push(`${finishMsg} ${timeSuffix}`); - } - } - - if (newLineSuffix) { - console.log(''); - } - } - - private queueWriteLog(prefix: string, msg: any[]) { - 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' + (this.prcs.memoryUsage().rss / 1000000).toFixed(1)).slice(-6) + - 'MB' + - ' ' + - prefix + - ' ' + - msg.join(', '); - - this.writeLogQueue.push(log); - } - } - - writeLogs(append: boolean) { - if (this.buildLogFilePath) { + const writeLogs = (logFilePath: string, log: string, append: boolean) => { + if (append) { 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: string, colorName: 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'gray') { - return this.colors ? (color as any)[colorName](msg) : msg; - } - - red(msg: string) { - return this.colors ? color.red(msg) : msg; - } - - green(msg: string) { - return this.colors ? color.green(msg) : msg; - } - - yellow(msg: string) { - return this.colors ? color.yellow(msg) : msg; - } - - blue(msg: string) { - return this.colors ? color.blue(msg) : msg; - } - - magenta(msg: string) { - return this.colors ? color.magenta(msg) : msg; - } - - cyan(msg: string) { - return this.colors ? color.cyan(msg) : msg; - } - - gray(msg: string) { - return this.colors ? color.gray(msg) : msg; - } - - bold(msg: string) { - return this.colors ? color.bold(msg) : msg; - } - - dim(msg: string) { - return this.colors ? color.dim(msg) : msg; - } - - bgRed(msg: string) { - return this.colors ? color.bgRed(msg) : msg; - } - - private shouldLog(level: string): boolean { - return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.level); - } - - createTimeSpan(startMsg: string, debug = false, appendTo?: string[]): LoggerTimeSpan { - return new CmdTimeSpan(this, startMsg, debug, appendTo); - } - - printDiagnostics(diagnostics: Diagnostic[], cwd?: string) { - if (!diagnostics || diagnostics.length === 0) return; - - let outputLines: string[] = ['']; - - diagnostics.forEach(d => { - outputLines = outputLines.concat(this.printDiagnostic(d, cwd)); - }); - - console.log(outputLines.join('\n')); - } - - printDiagnostic(diagnostic: Diagnostic, cwd?: string) { - const outputLines = wordWrap([diagnostic.messageText], getColumns(this.prcs)); - - let header = ''; - - if (diagnostic.header && diagnostic.header !== 'Build Error') { - header += diagnostic.header; - } - - if (typeof diagnostic.absFilePath === 'string' && typeof diagnostic.relFilePath !== 'string') { - if (typeof cwd !== 'string') { - cwd = this.prcs.cwd(); - } - - diagnostic.relFilePath = path.relative(cwd, diagnostic.absFilePath); - if (!diagnostic.relFilePath.includes('/')) { - diagnostic.relFilePath = './' + diagnostic.relFilePath; + fs.accessSync(logFilePath); + } catch (e) { + append = false; } } - let filePath = diagnostic.relFilePath; - if (typeof filePath !== 'string') { - filePath = diagnostic.absFilePath; - } - - if (typeof filePath === 'string') { - if (header.length > 0) { - header += ': '; - } - - header += this.cyan(filePath); - - if (typeof diagnostic.lineNumber === 'number' && diagnostic.lineNumber > -1) { - header += this.dim(`:`); - header += this.yellow(`${diagnostic.lineNumber}`); - - if (typeof diagnostic.columnNumber === 'number' && diagnostic.columnNumber > -1) { - header += this.dim(`:`); - header += this.yellow(`${diagnostic.columnNumber}`); - } - } - } - - if (header.length > 0) { - outputLines.unshift(INDENT + header); - } - - outputLines.push(''); - - if (diagnostic.lines && diagnostic.lines.length) { - const lines = prepareLines(diagnostic.lines); - - lines.forEach(l => { - if (!isMeaningfulLine(l.text)) { - return; - } - - let msg = ``; - - if (l.lineNumber > -1) { - 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 (diagnostic.language === 'typescript' || diagnostic.language === 'javascript') { - msg += this.javaScriptSyntaxHighlight(text); - } else if (diagnostic.language === 'scss' || diagnostic.language === 'css') { - msg += this.cssSyntaxHighlight(text); - } else { - msg += text; - } - - outputLines.push(msg); - }); - - outputLines.push(''); - } - - if (diagnostic.level === 'error') { - this.errorPrefix(outputLines); - } else if (diagnostic.level === 'warn') { - this.warnPrefix(outputLines); - } else if (diagnostic.level === 'debug') { - this.debugPrefix(outputLines); - } else { - this.infoPrefix(outputLines); - } - - if (diagnostic.debugText != null && this.level === 'debug') { - outputLines.push(diagnostic.debugText); - this.debugPrefix(wordWrap([diagnostic.debugText], getColumns(this.prcs))); - } - - return outputLines; - } - - highlightError(errorLine: string, errorCharStart: number, errorLength: number) { - 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: string[] = []; - 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.bgRed(chr === '' ? ' ' : chr); - } - lineChars.push(chr); - } - - return lineChars.join(''); - } - - javaScriptSyntaxHighlight(text: string) { - if (text.trim().startsWith('//')) { - return this.dim(text); - } - - const words = text.split(' ').map(word => { - if (JS_KEYWORDS.indexOf(word) > -1) { - return this.cyan(word); - } - return word; - }); - - return words.join(' '); - } - - cssSyntaxHighlight(text: string) { - let cssProp = true; - const safeChars = 'abcdefghijklmnopqrstuvwxyz-_'; - const notProp = '.#,:}@$[]/*'; - - const chars: string[] = []; - - 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.cyan(c)); - continue; - } - - chars.push(c); - } - - return chars.join(''); - } -} - -class CmdTimeSpan { - private logger: NodeLogger; - private start: number; - - constructor(logger: NodeLogger, startMsg: string, private debug: boolean, private appendTo: string[]) { - this.logger = logger; - this.start = Date.now(); - this.logger.timespanStart(startMsg, debug, this.appendTo); - } - - duration() { - return Date.now() - this.start; - } - - finish(msg: string, color?: 'red', bold?: boolean, newLineSuffix?: boolean) { - const duration = this.duration(); - let time: string; - - if (duration > 1000) { - time = 'in ' + (duration / 1000).toFixed(2) + ' s'; + if (append) { + fs.appendFileSync(logFilePath, log); } 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, this.appendTo); - - return duration; - } -} - -const LOG_LEVELS = ['debug', 'info', 'warn', 'error']; - -function getColumns(prcs: NodeJS.Process) { - const terminalWidth = (prcs.stdout && (prcs.stdout as any).columns) || 80; - return Math.max(Math.min(MAX_COLUMNS, terminalWidth), MIN_COLUMNS); -} - -export function wordWrap(msg: any[], columns: number) { - const lines: string[] = []; - const words: any[] = []; - - 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()); - } + fs.writeFileSync(logFilePath, log); + } + }; + + const loggerSys: TerminalLoggerSys = { + color, + cwd, + emoji, + enableColors, + getColumns, + memoryUsage, + relativePath, + writeLogs, + }; + + const logger = createTerminalLogger(loggerSys); + + logger.createLineUpdater = async () => { + const readline = await import('readline'); + let promise = Promise.resolve(); + const update = (text: string) => { + text = text.substr(0, prcs.stdout.columns - 5) + '\x1b[0m'; + return (promise = promise.then(() => { + return new Promise(resolve => { + readline.clearLine(prcs.stdout, 0); + readline.cursorTo(prcs.stdout, 0, null); + prcs.stdout.write(text, resolve); }); - } 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 as any).trimRight(); - }); -} - -function prepareLines(orgLines: PrintLine[]) { - const lines: PrintLine[] = 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: PrintLine[]) { - 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: string) { - if (line) { - line = line.trim(); - return line.length > 0; - } - return false; -} - -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; + })); + }; + + const stop = () => { + return update('\x1B[?25h'); + }; + + // hide cursor + prcs.stdout.write('\x1B[?25l'); + return { + update, + stop, + }; + }; + + return logger; +}; diff --git a/src/sys/node/node-setup-process.ts b/src/sys/node/node-setup-process.ts new file mode 100644 index 00000000000..c8435008c06 --- /dev/null +++ b/src/sys/node/node-setup-process.ts @@ -0,0 +1,20 @@ +import { Logger } from '../../declarations'; +import { shouldIgnoreError } from '@utils'; + +export function setupNodeProcess(c: { process: any, logger: Logger }) { + c.process.on(`unhandledRejection`, (e: any) => { + if (!shouldIgnoreError(e)) { + let msg = 'unhandledRejection'; + if (e != null) { + if (typeof e === 'string') { + msg += ': ' + e; + } else if (e.stack) { + msg += ': ' + e.stack; + } else if (e.message) { + msg += ': ' + e.message; + } + } + c.logger.error(msg); + } + }); +} diff --git a/src/sys/node/node-stencil-version-checker.ts b/src/sys/node/node-stencil-version-checker.ts new file mode 100644 index 00000000000..560c6688783 --- /dev/null +++ b/src/sys/node/node-stencil-version-checker.ts @@ -0,0 +1,164 @@ +import { Config, PackageJsonData } from '../../declarations'; +import { isString, noop } from '@utils'; +import semiver from 'semiver'; +import path from 'path'; + +const REGISTRY_URL = `https://registry.npmjs.org/@stencil/core`; +const CHECK_INTERVAL = 1000 * 60 * 60 * 24 * 7; + +export async function checkVersion(config: Config, currentVersion: string): Promise<() => void> { + if (config.devMode && !config.flags.ci) { + try { + const latestVersion = await getLatestCompilerVersion(config); + if (latestVersion != null) { + return () => { + if (semiver(currentVersion, latestVersion) < 0) { + printUpdateMessage(config, currentVersion, latestVersion); + } else { + console.debug(`${config.logger.cyan('@stencil/core')} version ${config.logger.green(currentVersion)} is the latest version`); + } + }; + } + } catch (e) { + config.logger.debug(`unable to load latest compiler version: ${e}`); + } + } + return noop; +} + +async function getLatestCompilerVersion(config: Config) { + try { + const lastCheck = await getLastCheck(config); + if (lastCheck == null) { + // we've never check before, so probably first install, so don't bother + // save that we did just do a check though + setLastCheck(config); + return null; + } + + if (!requiresCheck(Date.now(), lastCheck, CHECK_INTERVAL)) { + // within the range that we did a check recently, so don't bother + return null; + } + + // remember we just did a check + const setPromise = setLastCheck(config); + + const body = await requestUrl(REGISTRY_URL); + const data = JSON.parse(body) as PackageJsonData; + + await setPromise; + + return data['dist-tags'].latest; + } catch (e) { + // quietly catch, could have no network connection which is fine + config.logger.debug(`getLatestCompilerVersion error: ${e}`); + } + + return null; +} + +async function requestUrl(url: string) { + const https = await import('https'); + + return new Promise((resolve, reject) => { + const req = https.request(url, res => { + if (res.statusCode > 299) { + reject(`url: ${url}, staus: ${res.statusCode}`); + return; + } + + res.once('error', reject); + + const ret: any = []; + res.once('end', () => { + resolve(ret.join('')); + }); + + res.on('data', data => { + ret.push(data); + }); + }); + req.once('error', reject); + req.end(); + }); +} + +function requiresCheck(now: number, lastCheck: number, checkInterval: number) { + return lastCheck + checkInterval < now; +} + +async function getLastCheck(config: Config) { + try { + const data = await config.sys.readFile(getLastCheckStoragePath(config)); + if (isString(data)) { + return JSON.parse(data); + } + } catch (e) {} + return null; +} + +async function setLastCheck(config: Config) { + try { + const now = JSON.stringify(Date.now()); + await config.sys.writeFile(getLastCheckStoragePath(config), now); + } catch (e) {} +} + +function getLastCheckStoragePath(config: Config) { + return path.join(config.sys.tmpdir(), 'stencil_last_version_check.json'); +} + +function printUpdateMessage(config: Config, currentVersion: string, latestVersion: string) { + const installMessage = `npm install @stencil/core`; + const msg = [`Update available: ${currentVersion} ${ARROW} ${latestVersion}`, `To get the latest, please run:`, installMessage]; + + const lineLength = msg[0].length; + + const o: string[] = []; + + let top = BOX_TOP_LEFT; + while (top.length <= lineLength + PADDING * 2) { + top += BOX_HORIZONTAL; + } + top += BOX_TOP_RIGHT; + o.push(top); + + msg.forEach(m => { + let line = BOX_VERTICAL; + for (let i = 0; i < PADDING; i++) { + line += ` `; + } + line += m; + while (line.length <= lineLength + PADDING * 2) { + line += ` `; + } + line += BOX_VERTICAL; + o.push(line); + }); + + let bottom = BOX_BOTTOM_LEFT; + while (bottom.length <= lineLength + PADDING * 2) { + bottom += BOX_HORIZONTAL; + } + bottom += BOX_BOTTOM_RIGHT; + o.push(bottom); + + let output = `\n${INDENT}${o.join(`\n${INDENT}`)}\n`; + + output = output.replace(currentVersion, config.logger.red(currentVersion)); + output = output.replace(latestVersion, config.logger.green(latestVersion)); + output = output.replace(installMessage, config.logger.cyan(installMessage)); + + console.log(output); +} + +const ARROW = `→`; +const BOX_TOP_LEFT = `╭`; +const BOX_TOP_RIGHT = `╮`; +const BOX_BOTTOM_LEFT = `╰`; +const BOX_BOTTOM_RIGHT = `╯`; +const BOX_VERTICAL = `│`; +const BOX_HORIZONTAL = `─`; +const PADDING = 2; +const INDENT = ` `; diff --git a/src/sys/node/node-sys-watch.ts b/src/sys/node/node-sys-watch.ts index f4a5c809dea..53b49c63b02 100644 --- a/src/sys/node/node-sys-watch.ts +++ b/src/sys/node/node-sys-watch.ts @@ -1,12 +1,12 @@ -import { CompilerSystem } from '../../declarations'; +import { CompilerSystem, Logger } from '../../declarations'; import { buildEvents } from '../../compiler/events'; import { createNodeSys } from './node-sys'; import { normalizePath } from '@utils'; -import tsTypes from 'typescript'; +import type TypeScript from 'typescript'; -export function createNodeSysWithWatch(prcs: NodeJS.Process): CompilerSystem { - const ts = require('typescript') as typeof tsTypes; - const sys = createNodeSys(prcs); +export function createNodeSysWithWatch(c: { process: any; logger: Logger }): CompilerSystem { + const ts = require('typescript') as typeof TypeScript; + const sys = createNodeSys(c); const tsWatchFile = ts.sys.watchFile; const tsWatchDirectory = ts.sys.watchDirectory; diff --git a/src/sys/node/node-sys.ts b/src/sys/node/node-sys.ts index 004c21c9e4e..af95847c61c 100644 --- a/src/sys/node/node-sys.ts +++ b/src/sys/node/node-sys.ts @@ -1,17 +1,37 @@ -import { CompilerSystem, SystemDetails, CompilerSystemUnlinkResults, CompilerSystemMakeDirectoryResults, CompilerSystemWriteFileResults, CompilerSystemRealpathResults } from '../../declarations'; +import type { + CompilerSystem, + CompilerSystemMakeDirectoryResults, + CompilerSystemRealpathResults, + CompilerSystemUnlinkResults, + CompilerSystemWriteFileResults, + Logger, + TranspileOnlyResults, +} from '../../declarations'; import { asyncGlob, nodeCopyTasks } from './node-copy-tasks'; import { cpus, freemem, platform, release, tmpdir, totalmem } from 'os'; import { createHash } from 'crypto'; +import exit from 'exit'; import fs from 'graceful-fs'; -import { normalizePath } from '@utils'; -import path from 'path'; import { NodeLazyRequire } from './node-lazy-require'; import { NodeResolveModule } from './node-resolve-module'; +import { NodeWorkerController } from './node-worker-controller'; +import { normalizePath, requireFunc, catchError } from '@utils'; +import path from 'path'; +import type TypeScript from 'typescript'; -export function createNodeSys(prcs: NodeJS.Process) { +export function createNodeSys(c: { process: any; logger: Logger }) { + const prcs: NodeJS.Process = c.process; + const logger = c.logger; const destroys = new Set<() => Promise | void>(); + const onInterruptsCallbacks: (() => void)[] = []; + + const sysCpus = cpus(); + const hardwareConcurrency = sysCpus.length; + const osPlatform = platform(); const sys: CompilerSystem = { + name: 'node', + version: prcs.versions.node, access(p) { return new Promise(resolve => { fs.access(p, err => { @@ -41,6 +61,10 @@ export function createNodeSys(prcs: NodeJS.Process) { }); }); }, + createWorkerController(maxConcurrentWorkers) { + const forkModulePath = path.join(__dirname, 'worker.js'); + return new NodeWorkerController(logger, forkModulePath, maxConcurrentWorkers); + }, async destroy() { const waits: Promise[] = []; destroys.forEach(cb => { @@ -50,21 +74,45 @@ export function createNodeSys(prcs: NodeJS.Process) { waits.push(rtn); } } catch (e) { - console.error(`node sys destroy: ${e}`); + logger.error(`node sys destroy: ${e}`); } }); - await Promise.all(waits); + if (waits.length > 0) { + await Promise.all(waits); + } destroys.clear(); }, + dynamicImport(p) { + return Promise.resolve(requireFunc(p)); + }, encodeToBase64(str) { return Buffer.from(str).toString('base64'); }, + exit(exitCode) { + exit(exitCode); + }, getCurrentDirectory() { return normalizePath(prcs.cwd()); }, + getCompilerExecutingPath() { + return path.join(__dirname, '..', '..', 'compiler', 'stencil.js'); + }, + getDevServerExecutingPath() { + return path.join(__dirname, '..', '..', 'dev-server', 'index.js'); + }, + getEnvironmentVar(key) { + return process.env[key]; + }, + getLocalModulePath() { + return null; + }, + getRemoteModuleUrl() { + return null; + }, glob: asyncGlob, - isSymbolicLink: (p: string) => - new Promise(resolve => { + hardwareConcurrency, + isSymbolicLink(p: string) { + return new Promise(resolve => { try { fs.lstat(p, (err, stats) => { if (err) { @@ -76,9 +124,8 @@ export function createNodeSys(prcs: NodeJS.Process) { } catch (e) { resolve(false); } - }), - getCompilerExecutingPath: null, - normalizePath, + }); + }, mkdir(p, opts) { return new Promise(resolve => { if (opts) { @@ -119,6 +166,14 @@ export function createNodeSys(prcs: NodeJS.Process) { } return results; }, + nextTick(cb) { + prcs.nextTick(cb); + }, + normalizePath, + onProcessInterrupt(cb) { + onInterruptsCallbacks.push(cb); + }, + platformPath: path, readdir(p) { return new Promise(resolve => { fs.readdir(p, (err, files) => { @@ -160,7 +215,7 @@ export function createNodeSys(prcs: NodeJS.Process) { fs.realpath(p, 'utf8', (e, data) => { resolve({ path: data, - error: e + error: e, }); }); }); @@ -168,8 +223,8 @@ export function createNodeSys(prcs: NodeJS.Process) { realpathSync(p) { const results: CompilerSystemRealpathResults = { path: undefined, - error: null - } + error: null, + }; try { results.path = fs.realpathSync(p, 'utf8'); } catch (e) { @@ -231,6 +286,27 @@ export function createNodeSys(prcs: NodeJS.Process) { } catch (e) {} return undefined; }, + tmpdir() { + return tmpdir(); + }, + async transpile(input, filePath, compilerOptions) { + const results: TranspileOnlyResults = { + diagnostics: [], + output: input, + sourceMap: null, + }; + + try { + const ts: typeof TypeScript = require('typescript'); + const tsResults = ts.transpileModule(input, { fileName: filePath, compilerOptions }); + results.output = tsResults.outputText; + results.sourceMap = tsResults.sourceMapText; + } catch (e) { + catchError(results.diagnostics, e); + } + + return results; + }, unlink(p) { return new Promise(resolve => { fs.unlink(p, err => { @@ -285,7 +361,15 @@ export function createNodeSys(prcs: NodeJS.Process) { return Promise.resolve(hash); }, copy: nodeCopyTasks, - details: getDetails(), + details: { + cpuModel: sysCpus[0].model, + freemem() { + return freemem(); + }, + platform: osPlatform === 'darwin' || osPlatform === 'linux' ? osPlatform : osPlatform === 'win32' ? 'windows' : '', + release: release(), + totalmem: totalmem(), + }, }; const nodeResolve = new NodeResolveModule(); @@ -303,29 +387,3 @@ export function createNodeSys(prcs: NodeJS.Process) { return sys; } - -const getDetails = () => { - const details: SystemDetails = { - cpuModel: '', - cpus: -1, - freemem() { - return freemem(); - }, - platform: '', - release: '', - runtime: 'node', - runtimeVersion: '', - tmpDir: tmpdir(), - totalmem: -1, - }; - try { - const sysCpus = cpus(); - details.cpuModel = sysCpus[0].model; - details.cpus = sysCpus.length; - details.platform = platform(); - details.release = release(); - details.runtimeVersion = process.version; - details.totalmem = totalmem(); - } catch (e) {} - return details; -}; diff --git a/src/sys/node/worker/index.ts b/src/sys/node/node-worker-controller.ts similarity index 86% rename from src/sys/node/worker/index.ts rename to src/sys/node/node-worker-controller.ts index 745802ea55d..d9e2c297253 100755 --- a/src/sys/node/worker/index.ts +++ b/src/sys/node/node-worker-controller.ts @@ -1,7 +1,7 @@ -import * as d from '../../../declarations'; +import type * as d from '../../declarations'; import { EventEmitter } from 'events'; import { TASK_CANCELED_MSG } from '@utils'; -import { NodeWorkerMain } from './worker-main'; +import { NodeWorkerMain } from './node-worker-main'; import { cpus } from 'os'; export class NodeWorkerController extends EventEmitter implements d.WorkerMainController { @@ -10,16 +10,16 @@ export class NodeWorkerController extends EventEmitter implements d.WorkerMainCo isEnding = false; taskQueue: d.CompilerWorkerTask[] = []; workers: NodeWorkerMain[] = []; - totalWorkers: number; + maxWorkers: number; useForkedWorkers: boolean; mainThreadRunner: { [fnName: string]: (...args: any[]) => Promise }; - constructor(public workerDomain: string, public forkModulePath: string, maxConcurrentWorkers: number, public logger: d.Logger) { + constructor(private logger: d.Logger, public forkModulePath: string, maxConcurrentWorkers: number) { super(); const osCpus = cpus().length; this.useForkedWorkers = maxConcurrentWorkers > 0; - this.totalWorkers = Math.max(Math.min(maxConcurrentWorkers, osCpus), 2) - 1; + this.maxWorkers = Math.max(Math.min(maxConcurrentWorkers, osCpus), 2) - 1; if (this.useForkedWorkers) { // start up the forked child processes @@ -62,14 +62,14 @@ export class NodeWorkerController extends EventEmitter implements d.WorkerMainCo } startWorkers() { - while (this.workers.length < this.totalWorkers) { + while (this.workers.length < this.maxWorkers) { this.startWorker(); } } startWorker() { const workerId = this.workerIds++; - const worker = new NodeWorkerMain(this.workerDomain, workerId, this.forkModulePath); + const worker = new NodeWorkerMain(workerId, this.forkModulePath); worker.on('response', this.processTaskQueue.bind(this)); @@ -203,9 +203,3 @@ export function getNextWorker(workers: NodeWorkerMain[]) { return sorted[0]; } - -export function setupWorkerController(sys: d.CompilerSystem, logger: d.Logger, workerDomain: string) { - sys.createWorkerController = function(compilerPath, maxConcurrentWorkers) { - return new NodeWorkerController(workerDomain, compilerPath, maxConcurrentWorkers, logger); - }; -} diff --git a/src/sys/node/worker/worker-main.ts b/src/sys/node/node-worker-main.ts similarity index 93% rename from src/sys/node/worker/worker-main.ts rename to src/sys/node/node-worker-main.ts index 7a666368904..6a83bea4839 100644 --- a/src/sys/node/worker/worker-main.ts +++ b/src/sys/node/node-worker-main.ts @@ -1,4 +1,4 @@ -import * as d from '../../../declarations'; +import * as d from '../../declarations'; import * as cp from 'child_process'; import { EventEmitter } from 'events'; import { TASK_CANCELED_MSG } from '@utils'; @@ -13,7 +13,7 @@ export class NodeWorkerMain extends EventEmitter { successfulMessage = false; totalTasksAssigned = 0; - constructor(public workerDomain: string, public id: number, forkModulePath: string) { + constructor(public id: number, forkModulePath: string) { super(); this.fork(forkModulePath); } @@ -21,8 +21,6 @@ export class NodeWorkerMain extends EventEmitter { fork(forkModulePath: string) { const filteredArgs = process.execArgv.filter(v => !/^--(debug|inspect)/.test(v)); - const args = [this.workerDomain]; - const options: cp.ForkOptions = { execArgv: filteredArgs, env: process.env, @@ -30,7 +28,7 @@ export class NodeWorkerMain extends EventEmitter { silent: true, }; - this.childProcess = cp.fork(forkModulePath, args, options); + this.childProcess = cp.fork(forkModulePath, [], options); this.childProcess.stdout.setEncoding('utf8'); this.childProcess.stdout.on('data', data => { diff --git a/src/sys/node/worker/worker-child.ts b/src/sys/node/node-worker-thread.ts similarity index 89% rename from src/sys/node/worker/worker-child.ts rename to src/sys/node/node-worker-thread.ts index 9c4591aed7c..71ab79aefb7 100755 --- a/src/sys/node/worker/worker-child.ts +++ b/src/sys/node/node-worker-thread.ts @@ -1,5 +1,4 @@ -import * as d from '../../../declarations'; -import { isNumber, isString } from '@utils'; +import type * as d from '../../declarations'; export const initNodeWorkerThread = (prcs: NodeJS.Process, msgHandler: d.WorkerMsgHandler) => { const sendHandle = (err: NodeJS.ErrnoException) => { @@ -14,7 +13,7 @@ export const initNodeWorkerThread = (prcs: NodeJS.Process, msgHandler: d.WorkerM stencilRtnValue: null, stencilRtnError: 'Error', }; - if (isString(err)) { + if (typeof err === 'string') { errMsgBackToMain.stencilRtnError += ': ' + err; } else if (err) { if (err.stack) { @@ -28,7 +27,7 @@ export const initNodeWorkerThread = (prcs: NodeJS.Process, msgHandler: d.WorkerM prcs.on('message', async (msgToWorker: d.MsgToWorker) => { // message from the main thread - if (msgToWorker && isNumber(msgToWorker.stencilId)) { + if (msgToWorker && typeof msgToWorker.stencilId === 'number') { try { // run the handler to get the data const msgFromWorker: d.MsgFromWorker = { diff --git a/src/sys/node/worker/test/test-worker-main.ts b/src/sys/node/test/test-worker-main.ts similarity index 64% rename from src/sys/node/worker/test/test-worker-main.ts rename to src/sys/node/test/test-worker-main.ts index ecdf8eed7ff..4423b9b6e3a 100644 --- a/src/sys/node/worker/test/test-worker-main.ts +++ b/src/sys/node/test/test-worker-main.ts @@ -1,8 +1,8 @@ -import { NodeWorkerMain } from '../worker-main'; +import { NodeWorkerMain } from '../node-worker-main'; export class TestWorkerMain extends NodeWorkerMain { constructor(workerId: number) { - super('TestWorker', workerId, null); + super(workerId, null); this.fork(); } diff --git a/src/sys/node/worker/test/tsconfig.json b/src/sys/node/test/tsconfig.json similarity index 100% rename from src/sys/node/worker/test/tsconfig.json rename to src/sys/node/test/tsconfig.json diff --git a/src/sys/node/worker/test/worker-manager.spec.ts b/src/sys/node/test/worker-manager.spec.ts similarity index 96% rename from src/sys/node/worker/test/worker-manager.spec.ts rename to src/sys/node/test/worker-manager.spec.ts index 0386ccd4091..25180de5886 100644 --- a/src/sys/node/worker/test/worker-manager.spec.ts +++ b/src/sys/node/test/worker-manager.spec.ts @@ -1,4 +1,4 @@ -import { getNextWorker } from '..'; +import { getNextWorker } from '../node-worker-controller'; import { TestWorkerMain } from './test-worker-main'; describe('getNextWorker', () => { diff --git a/src/sys/node/worker.ts b/src/sys/node/worker.ts new file mode 100644 index 00000000000..f24732668c2 --- /dev/null +++ b/src/sys/node/worker.ts @@ -0,0 +1,10 @@ +import '@stencil/core/compiler'; +import * as nodeApi from '@sys-api-node'; +import { initNodeWorkerThread } from './node-worker-thread'; + +const coreCompiler = (global as any).stencil as typeof import('@stencil/core/compiler'); +const nodeLogger = nodeApi.createNodeLogger({ process: process }); +const nodeSys = nodeApi.createNodeSys({ process: process, logger: nodeLogger }); +const msgHandler = coreCompiler.createWorkerMessageHandler(nodeSys); + +initNodeWorkerThread(process, msgHandler);