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
JavaScript
/* global commandOptions */
;
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;