UNPKG

angular-ide

Version:

Provides a seamless integration with the Angular IDE from the command-line for developers looking for an enhanced development experience with Angular.

558 lines (456 loc) 21.3 kB
/* global commandOptions */ 'use strict'; const getNGCLIManifest = require('../../lib/CLUtils').getNGCLIManifest; const getNGCLIPath = require('../../lib/CLUtils').getNGCLIPath; const ngCLIPKG = getNGCLIManifest(); const path = require('path'); const ngCLIPKGName = ngCLIPKG.name; const ngCLIPath = getNGCLIPath(ngCLIPKGName); const Rx = require('rxjs'); let devServer; try { devServer = require('webpack-dev-server'); } catch (e) { try { devServer = require('@angular/cli/node_modules/webpack-dev-server/lib/Server'); require.cache['webpack-dev-server'] = require('@angular/cli/node_modules/webpack-dev-server/lib/Server'); } catch (er) { try { devServer = require('@angular-devkit/build-angular/node_modules/webpack-dev-server/lib/Server'); require.cache['webpack-dev-server'] = require('@angular-devkit/build-angular/node_modules/webpack-dev-server/lib/Server'); } catch (err) { // Do nothing, this might be the case of an empty project where no application/library has been created } } } const Serve = require(`${ngCLIPath}/commands/serve`).default; const intercept = require('intercept-stdout'); const semver = require('semver'); const extractNGEvent = require('../utils/extractNGEvent'); const wsManager = require('../services/WSConnectionsManager'); const NGEventType = require('../utils/consts').NGEventType; const BuildStatus = require('../utils/consts').BuildStatus; const NGServerStatus = require('../utils/consts').NGServerStatus; const getRunningInstallations = require('../../lib/utils').getRunningInstallations; const getNPMPackage = require('../../lib/utils').getNPMPackage; const injectComponentMetadata = require('../../lib/CLUtils').injectComponentMetadata; const injectCLBootstrapScript = require('../../lib/CLUtils').injectCLBootstrapScript; const CLEnablementStatusManager = require('../../lib/CLEnablementStatusManager').CLEnablementStatusManager; const supportedVersions = require('../../supported_versions.json').supported_versions; const GET_RUNNING_INSTALLATIONS_TIMEOUT = 15000; const debug = require('debug')('angular-ide'); const chalk = require('chalk'); let middleware = null; const Q = require('q'); const aliveMessageData = { serverId: Date.now(), projectPath: null, commandOptions: null, command: 'registerServer', method: 'registerServer', }; const ngServeStatus = { serverStatus: NGServerStatus.STARTING, build: { status: BuildStatus.NOT_READY, errors: [], }, }; module.exports.patchServeCommand = function patchServeCommand(cliVersion) { if (semver.satisfies(semver.coerce(cliVersion), '>=8.0.0')) { // MonkeyPatch ts compiler target to support old versions of chromium on windows if (process.platform === 'win32' && process.env['SKIP_FORCE_ES5_TARGET'] === undefined) { try { const readTSConfigModule = require('@angular-devkit/build-angular/src/angular-cli-files/utilities/read-tsconfig'); const orig_readTsconfig = readTSConfigModule.readTsconfig; readTSConfigModule.readTsconfig = function readTsconfig() { const result = orig_readTsconfig.apply(this, arguments); result.options.target = 1; // Set target to es5 return result; }; } catch (e) { console.log('[angular-ide] Unable to change tsc target'); } } } if (semver.satisfies(semver.coerce(cliVersion), '>=6.0.0')) { /** * TODO: Extend Serve command so it doesn't throw an error when webclipse-launch-id is passed. * Let just filter the argument for now as it only used as a way to identify the process by Eclipse */ process.argv = process.argv.filter((argument) => argument.indexOf('--webclipse-launch-id') === -1); let ServeModule = null; if (semver.satisfies(semver.coerce(cliVersion), '>=6.2.0')) { ServeModule = require('../../../@angular/cli/commands/serve-impl'); require.cache['@angular/cli/commands/serve'] = require('../../../@angular/cli/commands/serve-impl'); } else { ServeModule = require('../../../@angular/cli/commands/serve'); require.cache['@angular/cli/commands/serve'] = require('../../../@angular/cli/commands/serve'); } let ServeCommand = null; // ServeCommand is not available in ServeModule as "default" property in 6.1.0>= if (ServeModule.default) { ServeCommand = ServeModule.default; } else { ServeCommand = ServeModule.ServeCommand; } class PatchedServeCommand extends ServeCommand { run(commandOptions) { const _superRun = super.run.bind(this); const superRun = function (args) { return _superRun(args) .then(() => { // Do nothing if application launch went fine }, (e) => { // There was an error when trying to launch the application, print it out console.log(e); process.exit(); }); }.bind(this); return new Promise((resolve) => { const originalListen = devServer.prototype.listen; devServer.prototype.listen = function () { middleware = this.middleware; return originalListen.apply(this, arguments); }; const ngCLIPackage = ngCLIPKG; const ngIDEPackage = getNPMPackage('angular-ide'); debug(`angular-cli version detected: ${ngCLIPackage.version}`); debug(`angular-ide version detected: ${ngIDEPackage.version}`); const prereleaseParts = semver.prerelease(ngCLIPackage.version); if (prereleaseParts && prereleaseParts.length > 1 && typeof prereleaseParts[1] === 'string' ) { ngCLIPackage.version = ngCLIPackage.version.replace(`${prereleaseParts[0]}.${prereleaseParts[1]}`, `${prereleaseParts[0]}.${parseInt(prereleaseParts[1])}` ); debug('Normalizing package version with', ngCLIPackage.version); } const angularCLIVersionSupported = supportedVersions.find((versionRules) => { return semver.satisfies(ngCLIPackage.version, versionRules['angular-cli'], false) && semver.satisfies(ngIDEPackage.version, versionRules['angular-ide'], false); }); if (angularCLIVersionSupported) { debug('angular-cli version supported!'); } else { debug(chalk.red('angular-cli version not supported supported!')); } aliveMessageData.commandOptions = commandOptions; // Available @angular@cli@7.0.0< if (this.project) { aliveMessageData.projectPath = this.project.root; // Available @angular@cli@7.0.0>= } else if (this.workspace) { aliveMessageData.projectPath = this.workspace.root || this.workspace.basePath; // process command options if (commandOptions['--'] && commandOptions['--'].length > 0) { try { const parser = require('@angular/cli/models/parser'); // Creating a copy of commandOptions as parser.parseFreeFormArguments mutates // the param passed const commandOptionsCopy = JSON.parse(JSON.stringify(commandOptions['--'])); const newCommandOptions = parser.parseFreeFormArguments(commandOptionsCopy); aliveMessageData.commandOptions = newCommandOptions; } catch (requireParserError) { debug(chalk.red('Command option parser not found')); } } } wsManager.onConnect(JSON.stringify(aliveMessageData)); wsManager.registerServerStatus(ngServeStatus); if (process.env.NG_IDE_PORT) { wsManager.register(process.env.NG_IDE_PORT); } else { debug('Server port env part not set, falling back to port detection in locations file'); Rx.Observable.race( getRunningInstallations().filter((installations) => installations.length > 0), Rx.Observable.timer(GET_RUNNING_INSTALLATIONS_TIMEOUT).map(() => { throw new Error('Unable to find ports to connect to');}) ) .subscribe(installations => { installations.forEach(installation => { debug(`Installation found, connecting to port: ${installation.port}`); wsManager.register(installation.port); }); }, (e) => { console.log('[angular-ide] Error while trying find a port to connect to', e); superRun(commandOptions); }); } /* * Intercepting stdout to extract relevant info * e.g. Where the dev server is running, error build, etc. */ intercept((text) => { const ngEvent = extractNGEvent(text); if (ngEvent) { switch (ngEvent.type) { case NGEventType.NG_SERVER_STATUS: ngServeStatus.serverStatus = ngEvent.params.status; wsManager.sendEventToAll('ng-serve-status-update'); break; case NGEventType.BUILD_STATUS: // Resetting build error when a new build starts if (ngEvent.params.status === BuildStatus.BUILD_STARTED) { ngServeStatus.build.errors = []; } ngServeStatus.build.status = ngEvent.params.status; wsManager.sendEventToAll('ng-serve-status-update'); break; case NGEventType.BUILD_ERROR: ngServeStatus.build.errors.push(ngEvent.params); break; default: // no-op } } return text; }); let timeoutOnProgress = null; wsManager.on('message', function (ws, data) { const message = JSON.parse(data); switch (message.command) { case 'terminate': process.exit(); break; case 'injectCL': if (commandOptions.prod == null) { CLEnablementStatusManager.setStatus(message.commandOptions.inject); if (CLEnablementStatusManager.getStatus()) { const bootstrapScriptConfig = { host: '127.0.0.1', port: null, }; if (process.env.NG_IDE_PORT) { debug(`[inject-cl] Using env var port: ${process.env.NG_IDE_PORT}`); bootstrapScriptConfig.port = process.env.NG_IDE_PORT; try { injectCLBootstrapScript(ngCLIPackage.version, bootstrapScriptConfig); } catch (e) { console.log('[angular-ide] Error while injecting bootstrap script'); } try { injectComponentMetadata(ngCLIPackage.version); } catch (e) { console.log('[angular-ide] Error while injecting component metadata'); } superRun(commandOptions); } else { debug(`[inject-cl] Environment var not set`); const runningInstallTimeout = setTimeout(() => { console.log('[angular-ide] Unable to find an installation to connect to'); }, GET_RUNNING_INSTALLATIONS_TIMEOUT); getRunningInstallations() .first((installations) => { if (installations.length > 0) { return installations.find(install => install.used); } else { return false; } }) .subscribe(installations => { clearTimeout(runningInstallTimeout); if (installations.length > 0) { const sortedInstallations = installations.sort((installA, installB) => { return installA.used < installB.used; }); const installation = sortedInstallations[0]; bootstrapScriptConfig.port = installation.port; debug(`[inject-cl] Running installation detected with port ${bootstrapScriptConfig.port}`); try { injectCLBootstrapScript(ngCLIPackage.version, bootstrapScriptConfig); } catch (e) { console.log('[angular-ide] Error while injecting bootstrap script'); } try { injectComponentMetadata(ngCLIPackage.version); } catch (e) { console.log('[angular-ide] Error while injecting component metadata'); } superRun(commandOptions); } }, (e) => { console.log('[angular-ide] Error while trying find a port to connect to', e); superRun(commandOptions); }); } } else { superRun(commandOptions); } } else { superRun(commandOptions); } break; default: // TODO: Handle unknown command } }.bind(this)); }); } } if (ServeModule.default) { ServeModule.default = PatchedServeCommand; } else { ServeModule.ServeCommand = PatchedServeCommand; } } else { // add webclipse-launch-id to supported options Serve.prototype.availableOptions.push( { name: 'webclipse-launch-id', type: String } ); const ServeCommandPatched = Serve.extend({ run(commandOptions) { let ServeTask = require(`@angular/cli/tasks/serve`).default; ServeTask.prototype.originalRun = ServeTask.prototype.run; ServeTask.prototype.run = function monkeyPatchedRun(serveTaskOptions, rebuildDoneCb) { // Disabling evalSourcemaps, required until proper evalSourcemaps is implemented // If evalSourcemaps exists and it is true, make it false if (serveTaskOptions.evalSourcemaps) { serveTaskOptions.evalSourcemaps = false; } return this.originalRun(serveTaskOptions, rebuildDoneCb); }; const originalListen = devServer.prototype.listen; devServer.prototype.listen = function () { middleware = this.middleware; return originalListen.apply(this, arguments); }; const ngCLIPackage = ngCLIPKG; const ngIDEPackage = getNPMPackage('angular-ide'); debug(`angular-cli version detected: ${ngCLIPackage.version}`); debug(`angular-ide version detected: ${ngIDEPackage.version}`); const prereleaseParts = semver.prerelease(ngCLIPackage.version); if (prereleaseParts && prereleaseParts.length > 1 && typeof prereleaseParts[1] === 'string' ) { ngCLIPackage.version = ngCLIPackage.version.replace(`${prereleaseParts[0]}.${prereleaseParts[1]}`, `${prereleaseParts[0]}.${parseInt(prereleaseParts[1])}` ); debug('Normalizing package version with', ngCLIPackage.version); } const angularCLIVersionSupported = supportedVersions.find((versionRules) => { return semver.satisfies(ngCLIPackage.version, versionRules['angular-cli'], false) && semver.satisfies(ngIDEPackage.version, versionRules['angular-ide'], false); }); if (angularCLIVersionSupported) { debug('angular-cli version supported!'); } else { debug(chalk.red('angular-cli version not supported supported!')); } try { injectComponentMetadata(ngCLIPackage.version); } catch (e) { console.log('[angular-ide] Error while injecting component metadata'); } aliveMessageData.commandOptions = commandOptions; aliveMessageData.projectPath = this.project.root; wsManager.onConnect(JSON.stringify(aliveMessageData)); wsManager.registerServerStatus(ngServeStatus); if (process.env.NG_IDE_PORT) { wsManager.register(process.env.NG_IDE_PORT); } else { debug('Server port env part not set, falling back to port detection in locations file'); getRunningInstallations() .subscribe(installations => { installations.forEach(installation => { debug(`Installation found, connecting to port: ${installation.port}`); wsManager.register(installation.port); }); }); } /* * Intercepting stdout to extract relevant info * e.g. Where the dev server is running, error build, etc. */ intercept((text) => { const ngEvent = extractNGEvent(text); if (ngEvent) { switch (ngEvent.type) { case NGEventType.NG_SERVER_STATUS: ngServeStatus.serverStatus = ngEvent.params.status; wsManager.sendEventToAll('ng-serve-status-update'); break; case NGEventType.BUILD_STATUS: // Resetting build error when a new build starts if (ngEvent.params.status === BuildStatus.BUILD_STARTED) { ngServeStatus.build.errors = []; } ngServeStatus.build.status = ngEvent.params.status; wsManager.sendEventToAll('ng-serve-status-update'); break; case NGEventType.BUILD_ERROR: ngServeStatus.build.errors.push(ngEvent.params); break; default: // TODO: Handle unknown NGEventType } } return text; }); const promise = Q.Promise(function (resolve) { const bootstrapScriptConfig = { host: '127.0.0.1', port: null, }; if (process.env.NG_IDE_PORT) { debug(`[inject] Using env var port: ${process.env.NG_IDE_PORT}`); bootstrapScriptConfig.port = process.env.NG_IDE_PORT; try { injectCLBootstrapScript(ngCLIPackage.version, bootstrapScriptConfig); } catch (e) { console.log('[angular-ide] Error while injecting bootstrap script'); } resolve(this._super.run.apply(this, [commandOptions])); } else { debug(`[inject] Environment var not set`); const _super = this._super; getRunningInstallations() .subscribe(installations => { if (installations.length > 0) { const sortedInstallations = installations.sort((installA, installB) => { return installA.used < installB.used; }); const installation = installations[0]; bootstrapScriptConfig.port = installation.port; debug(`[inject-cl] Running installation detected with port ${bootstrapScriptConfig.port}`); try { injectCLBootstrapScript(ngCLIPackage.version, bootstrapScriptConfig); } catch (e) { console.log('[angular-ide] Error while injecting bootstrap script'); } resolve(_super.run.apply(this, [commandOptions])); } }); } }.bind(this)); wsManager.on('message', function (ws, data) { const message = JSON.parse(data); switch (message.command) { case 'terminate': process.exit(); break; case 'injectCL': // Only inject CodeLive in "development" environment if (commandOptions.environment === 'dev' || commandOptions.target === 'development') { CLEnablementStatusManager.setStatus(message.commandOptions.inject); middleware.invalidate(); } break; default: // TODO: Handle unknown command } }.bind(this)); return promise; }, }); const CLI = require(path.normalize(path.resolve(__dirname, `../../../${ngCLIPath}/ember-cli/lib/cli/cli`))); CLI.prototype.originalRun = CLI.prototype.run; CLI.prototype.run = function monkeyRun(environment) { environment.commands['serve'] = ServeCommandPatched; return CLI.prototype.originalRun.apply(this, arguments); }; module.exports = ServeCommandPatched; } } module.exports.overrideCore = true;