diff --git a/packages/@angular/cli/lib/config/schema.json b/packages/@angular/cli/lib/config/schema.json index ccd71182dbeb..7a8b2c08e4c5 100644 --- a/packages/@angular/cli/lib/config/schema.json +++ b/packages/@angular/cli/lib/config/schema.json @@ -83,6 +83,12 @@ "type": "string", "description": "Base url for the application being built." }, + "platform": { + "type": "string", + "enum": ["browser", "server"], + "default": "browser", + "description": "The runtime platform of the app." + }, "index": { "type": "string", "default": "index.html", diff --git a/packages/@angular/cli/models/webpack-config.ts b/packages/@angular/cli/models/webpack-config.ts index 8d084801e48c..4e7e259cf868 100644 --- a/packages/@angular/cli/models/webpack-config.ts +++ b/packages/@angular/cli/models/webpack-config.ts @@ -7,6 +7,7 @@ import { getDevConfig, getProdConfig, getStylesConfig, + getServerConfig, getNonAotConfig, getAotConfig } from './webpack-configs'; @@ -37,9 +38,12 @@ export class NgCliWebpackConfig { } public buildConfig() { + const platformConfig = this.wco.appConfig.platform === 'server' ? + getServerConfig(this.wco) : getBrowserConfig(this.wco); + let webpackConfigs = [ getCommonConfig(this.wco), - getBrowserConfig(this.wco), + platformConfig, getStylesConfig(this.wco), this.getTargetConfig(this.wco) ]; diff --git a/packages/@angular/cli/models/webpack-configs/index.ts b/packages/@angular/cli/models/webpack-configs/index.ts index 7c985a1e093d..70560367f79f 100644 --- a/packages/@angular/cli/models/webpack-configs/index.ts +++ b/packages/@angular/cli/models/webpack-configs/index.ts @@ -2,6 +2,7 @@ export * from './browser'; export * from './common'; export * from './development'; export * from './production'; +export * from './server'; export * from './styles'; export * from './test'; export * from './typescript'; diff --git a/packages/@angular/cli/models/webpack-configs/server.ts b/packages/@angular/cli/models/webpack-configs/server.ts new file mode 100644 index 000000000000..262849877de2 --- /dev/null +++ b/packages/@angular/cli/models/webpack-configs/server.ts @@ -0,0 +1,38 @@ +import { WebpackConfigOptions } from '../webpack-config'; + +/** + * Returns a partial specific to creating a bundle for node + * @param _wco Options which are include the build options and app config + */ +export const getServerConfig = function (_wco: WebpackConfigOptions) { + return { + target: 'node', + output: { + libraryTarget: 'commonjs' + }, + externals: [ + /^@angular/, + function (_: any, request: any, callback: (error?: any, result?: any) => void) { + // Absolute & Relative paths are not externals + if (request.match(/^\.{0,2}\//)) { + return callback(); + } + + try { + // Attempt to resolve the module via Node + const e = require.resolve(request); + if (/node_modules/.test(e)) { + // It's a node_module + callback(null, request); + } else { + // It's a system thing (.ie util, fs...) + callback(); + } + } catch (e) { + // Node couldn't find it, so it must be user-aliased + callback(); + } + } + ] + }; +}; diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts index ba58d224ba64..39c7a7295687 100644 --- a/packages/@angular/cli/models/webpack-configs/typescript.ts +++ b/packages/@angular/cli/models/webpack-configs/typescript.ts @@ -68,6 +68,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) { i18nFile: buildOptions.i18nFile, i18nFormat: buildOptions.i18nFormat, locale: buildOptions.locale, + replaceExport: appConfig.platform === 'server', hostReplacementPaths, // If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`. exclude: [] diff --git a/packages/@angular/cli/tasks/e2e.ts b/packages/@angular/cli/tasks/e2e.ts index 3b2cec98991e..40228aafeb83 100644 --- a/packages/@angular/cli/tasks/e2e.ts +++ b/packages/@angular/cli/tasks/e2e.ts @@ -4,6 +4,7 @@ import { stripIndents } from 'common-tags'; import { E2eTaskOptions } from '../commands/e2e'; import { CliConfig } from '../models/config'; import { requireProjectModule } from '../utilities/require-project-module'; +import { getAppFromConfig } from '../utilities/app-utils'; const Task = require('../ember-cli/lib/models/task'); const SilentError = require('silent-error'); @@ -14,10 +15,14 @@ export const E2eTask = Task.extend({ const projectConfig = CliConfig.fromProject().config; const projectRoot = this.project.root; const protractorLauncher = requireProjectModule(projectRoot, 'protractor/built/launcher'); + const appConfig = getAppFromConfig(e2eTaskOptions.app); if (projectConfig.project && projectConfig.project.ejected) { throw new SilentError('An ejected project cannot use the build command anymore.'); } + if (appConfig.platform === 'server') { + throw new SilentError('ng test for platform server applications is coming soon!'); + } return new Promise(function () { let promise = Promise.resolve(); diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts index c9f6cdce8f5b..8be7ae7a3f0b 100644 --- a/packages/@angular/cli/tasks/eject.ts +++ b/packages/@angular/cli/tasks/eject.ts @@ -436,6 +436,9 @@ export default Task.extend({ if (project.root === path.resolve(outputPath)) { throw new SilentError ('Output path MUST not be project root directory!'); } + if (appConfig.platform === 'server') { + throw new SilentError('ng eject for platform server applications is coming soon!'); + } const webpackConfig = new NgCliWebpackConfig(runTaskOptions, appConfig).buildConfig(); const serializer = new JsonWebpackSerializer(process.cwd(), outputPath, appConfig.root); diff --git a/packages/@angular/cli/tasks/serve.ts b/packages/@angular/cli/tasks/serve.ts index 750c3c66e5db..618ea14419ba 100644 --- a/packages/@angular/cli/tasks/serve.ts +++ b/packages/@angular/cli/tasks/serve.ts @@ -31,6 +31,9 @@ export default Task.extend({ if (projectConfig.project && projectConfig.project.ejected) { throw new SilentError('An ejected project cannot use the build command anymore.'); } + if (appConfig.platform === 'server') { + throw new SilentError('ng serve for platform server applications is coming soon!'); + } if (serveTaskOptions.deleteOutputPath) { fs.removeSync(path.resolve(this.project.root, outputPath)); } diff --git a/packages/@angular/cli/tasks/test.ts b/packages/@angular/cli/tasks/test.ts index 9b1fca73b009..a41ec55ac4f5 100644 --- a/packages/@angular/cli/tasks/test.ts +++ b/packages/@angular/cli/tasks/test.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { TestOptions } from '../commands/test'; import { CliConfig } from '../models/config'; import { requireProjectModule } from '../utilities/require-project-module'; +import { getAppFromConfig } from '../utilities/app-utils'; const Task = require('../ember-cli/lib/models/task'); const SilentError = require('silent-error'); @@ -12,10 +13,14 @@ export default Task.extend({ run: function (options: TestOptions) { const projectConfig = CliConfig.fromProject().config; const projectRoot = this.project.root; + const appConfig = getAppFromConfig(options.app); if (projectConfig.project && projectConfig.project.ejected) { throw new SilentError('An ejected project cannot use the build command anymore.'); } + if (appConfig.platform === 'server') { + throw new SilentError('ng test for platform server applications is coming soon!'); + } return new Promise((resolve) => { const karma = requireProjectModule(projectRoot, 'karma'); diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts index bd1a3ff3b8a2..60124c1dbee5 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -435,6 +435,83 @@ function _diagnoseDeps(reasons: ModuleReason[], plugin: AotPlugin, checked: Set< } +export function _getModuleExports(plugin: AotPlugin, + refactor: TypeScriptFileRefactor): ts.Identifier[] { + const exports = refactor + .findAstNodes(refactor.sourceFile, ts.SyntaxKind.ExportDeclaration, true); + + return exports + .filter(node => { + + const identifiers = refactor.findAstNodes(node, ts.SyntaxKind.Identifier, false); + + identifiers + .filter(node => node.getText() === plugin.entryModule.className); + + return identifiers.length > 0; + }) as ts.Identifier[]; +} + + +export function _replaceExport(plugin: AotPlugin, refactor: TypeScriptFileRefactor) { + if (!plugin.replaceExport) { + return; + } + _getModuleExports(plugin, refactor) + .forEach(node => { + const factoryPath = _getNgFactoryPath(plugin, refactor); + const factoryClassName = plugin.entryModule.className + 'NgFactory'; + const exportStatement = `export \{ ${factoryClassName} \} from '${factoryPath}'`; + refactor.appendAfter(node, exportStatement); + }); +} + + +export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) { + if (!plugin.replaceExport) { + return; + } + + const dirName = path.normalize(path.dirname(refactor.fileName)); + const classNameAppend = plugin.skipCodeGeneration ? '' : 'NgFactory'; + const modulePathAppend = plugin.skipCodeGeneration ? '' : '.ngfactory'; + + _getModuleExports(plugin, refactor) + .forEach(node => { + const modules = Object.keys(plugin.discoveredLazyRoutes) + .map((loadChildrenString) => { + let [lazyRouteKey, moduleName] = loadChildrenString.split('#'); + + if (!lazyRouteKey || !moduleName) { + throw new Error(`${loadChildrenString} was not a proper loadChildren string`); + } + + moduleName += classNameAppend; + lazyRouteKey += modulePathAppend; + const modulePath = plugin.lazyRoutes[lazyRouteKey]; + + return { + modulePath, + moduleName, + loadChildrenString + }; + }); + + modules.forEach((module, index) => { + const relativePath = path.relative(dirName, module.modulePath).replace(/\\/g, '/'); + refactor.prependBefore(node, `import * as __lazy_${index}__ from './${relativePath}'`); + }); + + const jsonContent: string = modules + .map((module, index) => + `"${module.loadChildrenString}": __lazy_${index}__.${module.moduleName}`) + .join(); + + refactor.appendAfter(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`); + }); +} + + // Super simple TS transpiler loader for testing / isolated usage. does not type check! export function ngcLoader(this: LoaderContext & { _compilation: any }, source: string | null) { const cb = this.async(); @@ -464,11 +541,14 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s if (!plugin.skipCodeGeneration) { return Promise.resolve() .then(() => _removeDecorators(refactor)) - .then(() => _refactorBootstrap(plugin, refactor)); + .then(() => _refactorBootstrap(plugin, refactor)) + .then(() => _replaceExport(plugin, refactor)) + .then(() => _exportModuleMap(plugin, refactor)); } else { return Promise.resolve() .then(() => _replaceResources(refactor)) - .then(() => _removeModuleId(refactor)); + .then(() => _removeModuleId(refactor)) + .then(() => _exportModuleMap(plugin, refactor)); } }) .then(() => { diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 16aa9542dd49..0535aa3a9aff 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -25,6 +25,7 @@ export interface AotPluginOptions { mainPath?: string; typeChecking?: boolean; skipCodeGeneration?: boolean; + replaceExport?: boolean; hostOverrideFileSystem?: { [path: string]: string }; hostReplacementPaths?: { [path: string]: string }; i18nFile?: string; @@ -49,6 +50,7 @@ export class AotPlugin implements Tapable { private _rootFilePath: string[]; private _compilerHost: WebpackCompilerHost; private _resourceLoader: WebpackResourceLoader; + private _discoveredLazyRoutes: LazyRouteMap; private _lazyRoutes: LazyRouteMap = Object.create(null); private _tsConfigPath: string; private _entryModule: string; @@ -59,6 +61,7 @@ export class AotPlugin implements Tapable { private _typeCheck = true; private _skipCodeGeneration = false; + private _replaceExport = false; private _basePath: string; private _genDir: string; @@ -89,11 +92,14 @@ export class AotPlugin implements Tapable { get genDir() { return this._genDir; } get program() { return this._program; } get skipCodeGeneration() { return this._skipCodeGeneration; } + get replaceExport() { return this._replaceExport; } get typeCheck() { return this._typeCheck; } get i18nFile() { return this._i18nFile; } get i18nFormat() { return this._i18nFormat; } get locale() { return this._locale; } get firstRun() { return this._firstRun; } + get lazyRoutes() { return this._lazyRoutes; } + get discoveredLazyRoutes() { return this._discoveredLazyRoutes; } private _setupOptions(options: AotPluginOptions) { // Fill in the missing options. @@ -232,6 +238,9 @@ export class AotPlugin implements Tapable { if (options.hasOwnProperty('locale')) { this._locale = options.locale; } + if (options.hasOwnProperty('replaceExport')) { + this._replaceExport = options.replaceExport || this._replaceExport; + } } private _findLazyRoutesInAst(): LazyRouteMap { @@ -510,14 +519,14 @@ export class AotPlugin implements Tapable { .then(() => { // We need to run the `listLazyRoutes` the first time because it also navigates libraries // and other things that we might miss using the findLazyRoutesInAst. - let discoveredLazyRoutes: LazyRouteMap = this.firstRun + this._discoveredLazyRoutes = this.firstRun ? this._getLazyRoutesFromNgtools() : this._findLazyRoutesInAst(); // Process the lazy routes discovered. - Object.keys(discoveredLazyRoutes) + Object.keys(this.discoveredLazyRoutes) .forEach(k => { - const lazyRoute = discoveredLazyRoutes[k]; + const lazyRoute = this.discoveredLazyRoutes[k]; k = k.split('#')[0]; if (lazyRoute === null) { return; diff --git a/tests/e2e/assets/webpack/test-server-app/app/app.module.ts b/tests/e2e/assets/webpack/test-server-app/app/app.module.ts index 7ef819b3a46e..7c8a0c296448 100644 --- a/tests/e2e/assets/webpack/test-server-app/app/app.module.ts +++ b/tests/e2e/assets/webpack/test-server-app/app/app.module.ts @@ -1,7 +1,10 @@ import { NgModule, Component } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; +import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; + import { AppComponent } from './app.component'; +import { MyInjectable } from './injectable'; @Component({ selector: 'home-view', @@ -16,12 +19,16 @@ export class HomeView {} HomeView ], imports: [ + BrowserModule.withServerTransition({ + appId: 'app' + }), ServerModule, RouterModule.forRoot([ {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, {path: '', component: HomeView} ]) ], + providers: [MyInjectable], bootstrap: [AppComponent] }) export class AppModule { diff --git a/tests/e2e/assets/webpack/test-server-app/app/injectable.ts b/tests/e2e/assets/webpack/test-server-app/app/injectable.ts index 04d8486586c4..b357678ae77a 100644 --- a/tests/e2e/assets/webpack/test-server-app/app/injectable.ts +++ b/tests/e2e/assets/webpack/test-server-app/app/injectable.ts @@ -4,5 +4,5 @@ import {DOCUMENT} from '@angular/platform-browser'; @Injectable() export class MyInjectable { - constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {} + constructor(@Inject(DOCUMENT) public doc) {} } diff --git a/tests/e2e/assets/webpack/test-server-app/app/main.commonjs.ts b/tests/e2e/assets/webpack/test-server-app/app/main.commonjs.ts new file mode 100644 index 000000000000..ce26d93a11de --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/app/main.commonjs.ts @@ -0,0 +1 @@ +export { AppModule } from './app.module'; diff --git a/tests/e2e/assets/webpack/test-server-app/index.js b/tests/e2e/assets/webpack/test-server-app/index.js new file mode 100644 index 000000000000..bdfb2e792acd --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/index.js @@ -0,0 +1,12 @@ +const fs = require('fs'); +const { AppModuleNgFactory } = require('./dist/app.main'); +const { renderModuleFactory } = require('@angular/platform-server'); + +require('zone.js/dist/zone-node'); + +renderModuleFactory(AppModuleNgFactory, { + url: '/', + document: '' +}).then(html => { + fs.writeFileSync('dist/index.html', html); +}) diff --git a/tests/e2e/assets/webpack/test-server-app/webpack.commonjs.config.js b/tests/e2e/assets/webpack/test-server-app/webpack.commonjs.config.js new file mode 100644 index 000000000000..b23f63ee54a6 --- /dev/null +++ b/tests/e2e/assets/webpack/test-server-app/webpack.commonjs.config.js @@ -0,0 +1,33 @@ +const ngToolsWebpack = require('@ngtools/webpack'); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + target: 'web', + entry: './app/main.commonjs.ts', + output: { + path: './dist', + publicPath: 'dist/', + filename: 'app.main.js', + libraryTarget: 'commonjs' + }, + plugins: [ + new ngToolsWebpack.AotPlugin({ + tsConfigPath: './tsconfig.json', + replaceExport: true + }) + ], + externals: /^@angular/, + module: { + loaders: [ + { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader'] }, + { test: /\.css$/, loader: 'raw-loader' }, + { test: /\.html$/, loader: 'raw-loader' }, + { test: /\.ts$/, loader: '@ngtools/webpack' } + ] + }, + devServer: { + historyApiFallback: true + } +}; diff --git a/tests/e2e/tests/build/platform-server.ts b/tests/e2e/tests/build/platform-server.ts new file mode 100644 index 000000000000..314435b13dd3 --- /dev/null +++ b/tests/e2e/tests/build/platform-server.ts @@ -0,0 +1,71 @@ +import { normalize } from 'path'; + +import { updateJsonFile, updateTsConfig } from '../../utils/project'; +import { expectFileToMatch, writeFile, replaceInFile, prependToFile } from '../../utils/fs'; +import { ng, silentNpm, silentExec } from '../../utils/process'; +import { getGlobalVariable } from '../../utils/env'; + +export default function () { + // Skip this in Appveyor tests. + if (getGlobalVariable('argv').appveyor) { + return Promise.resolve(); + } + + // Skip this for ejected tests. + if (getGlobalVariable('argv').eject) { + return Promise.resolve(); + } + + return Promise.resolve() + .then(() => updateJsonFile('.angular-cli.json', configJson => { + const app = configJson['apps'][0]; + delete app['polyfills']; + delete app['styles']; + app['platform'] = 'server'; + })) + .then(() => updateJsonFile('package.json', packageJson => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/platform-server'] = '^4.0.0'; + })) + .then(() => updateTsConfig(tsConfig => { + tsConfig['angularCompilerOptions'] = { + entryModule: 'app/app.module#AppModule' + }; + })) + .then(() => writeFile('./src/main.ts', 'export { AppModule } from \'./app/app.module\';')) + .then(() => prependToFile('./src/app/app.module.ts', + 'import { ServerModule } from \'@angular/platform-server\';')) + .then(() => replaceInFile('./src/app/app.module.ts', /\[\s*BrowserModule/g, + `[BrowserModule.withServerTransition(\{ appId: 'app' \}), ServerModule`)) + .then(() => silentNpm('install')) + .then(() => ng('build')) + // files were created successfully + .then(() => expectFileToMatch('dist/main.bundle.js', + /__webpack_exports__, "AppModule"/)) + .then(() => writeFile('./index.js', ` + require('zone.js/dist/zone-node'); + require('reflect-metadata'); + const fs = require('fs'); + const \{ AppModule \} = require('./dist/main.bundle'); + const \{ renderModule \} = require('@angular/platform-server'); + + renderModule(AppModule, \{ + url: '/', + document: '' + \}).then(html => \{ + fs.writeFileSync('dist/index.html', html); + \}); + `)) + .then(() => silentExec(normalize('node'), 'index.js')) + .then(() => expectFileToMatch('dist/index.html', + new RegExp('

Here are some links to help you start:

'))) + .then(() => ng('build', '--aot')) + // files were created successfully + .then(() => expectFileToMatch('dist/main.bundle.js', + /__webpack_exports__, "AppModuleNgFactory"/)) + .then(() => replaceInFile('./index.js', /AppModule/g, 'AppModuleNgFactory')) + .then(() => replaceInFile('./index.js', /renderModule/g, 'renderModuleFactory')) + .then(() => silentExec(normalize('node'), 'index.js')) + .then(() => expectFileToMatch('dist/index.html', + new RegExp('

Here are some links to help you start:

'))); +} diff --git a/tests/e2e/tests/misc/platform-server.ts b/tests/e2e/tests/misc/platform-server.ts new file mode 100644 index 000000000000..d5c251294c9a --- /dev/null +++ b/tests/e2e/tests/misc/platform-server.ts @@ -0,0 +1,15 @@ +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default function () { + return Promise.resolve() + .then(() => updateJsonFile('.angular-cli.json', configJson => { + const app = configJson['apps'][0]; + app['platform'] = 'server'; + })) + .then(() => expectToFail(() => ng('serve'))) + .then(() => expectToFail(() => ng('test'))) + .then(() => expectToFail(() => ng('e2e'))) + .then(() => expectToFail(() => ng('eject'))); +} diff --git a/tests/e2e/tests/packages/webpack/server.ts b/tests/e2e/tests/packages/webpack/server.ts index 2143a4920e76..be50abae69a3 100644 --- a/tests/e2e/tests/packages/webpack/server.ts +++ b/tests/e2e/tests/packages/webpack/server.ts @@ -1,7 +1,7 @@ import {normalize} from 'path'; import {createProjectFromAsset} from '../../../utils/assets'; import {exec} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; +import {expectFileToMatch, rimraf} from '../../../utils/fs'; export default function(skipCleaning: () => void) { @@ -12,13 +12,22 @@ export default function(skipCleaning: () => void) { new RegExp('.bootstrapModuleFactory')) .then(() => expectFileToMatch('dist/app.main.js', new RegExp('MyInjectable.ctorParameters = .*' - + 'type: .*ViewContainerRef.*' + 'type: undefined, decorators.*Inject.*args: .*DOCUMENT.*')) .then(() => expectFileToMatch('dist/app.main.js', new RegExp('AppComponent.ctorParameters = .*MyInjectable')) .then(() => expectFileToMatch('dist/app.main.js', /AppModule \*\/\].*\.testProp = \'testing\'/)) .then(() => expectFileToMatch('dist/app.main.js', - /renderModuleFactory \*\/\].*\/\* AppModuleNgFactory \*\/\]/)) + /renderModuleFactory \*\/\].*\/\* AppModuleNgFactory \*\/\]/)) + .then(() => rimraf('dist')) + .then(() => exec(normalize('node_modules/.bin/webpack'), + '--config', 'webpack.commonjs.config.js')) + .then(() => expectFileToMatch('dist/app.main.js', + /__webpack_exports__, "AppModuleNgFactory"/)) + .then(() => expectFileToMatch('dist/app.main.js', + /var LAZY_MODULE_MAP = { ".\/lazy\.module#LazyModule": /)) + .then(() => exec(normalize('node'), 'index.js')) + .then(() => expectFileToMatch('dist/index.html', + new RegExp('lazy'))) .then(() => skipCleaning()); } diff --git a/tests/e2e/utils/process.ts b/tests/e2e/utils/process.ts index 26f662ccf4f0..49a47e4e114e 100644 --- a/tests/e2e/utils/process.ts +++ b/tests/e2e/utils/process.ts @@ -128,6 +128,10 @@ export function exec(cmd: string, ...args: string[]) { return _exec({}, cmd, args); } +export function silentExec(cmd: string, ...args: string[]) { + return _exec({ silent: true }, cmd, args); +} + export function execAndWaitForOutputToMatch(cmd: string, args: string[], match: RegExp) { return _exec({ waitForMatch: match }, cmd, args); }