nativescript
Version:
Command-line interface for building NativeScript projects
476 lines • 25.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginsService = void 0;
const path = require("path");
const shelljs = require("shelljs");
const semver = require("semver");
const constants = require("../constants");
const _ = require("lodash");
const yok_1 = require("../common/yok");
const package_path_helper_1 = require("../helpers/package-path-helper");
const color_1 = require("../color");
class PluginsService {
get $platformsDataService() {
return this.$injector.resolve("platformsDataService");
}
get $projectDataService() {
return this.$injector.resolve("projectDataService");
}
get npmInstallOptions() {
return _.merge({
disableNpmInstall: this.$options.disableNpmInstall,
frameworkPath: this.$options.frameworkPath,
ignoreScripts: this.$options.ignoreScripts,
path: this.$options.path,
}, PluginsService.NPM_CONFIG);
}
constructor($packageManager, $fs, $options, $logger, $errors, $filesHashService, $injector, $mobileHelper, $nodeModulesDependenciesBuilder) {
this.$packageManager = $packageManager;
this.$fs = $fs;
this.$options = $options;
this.$logger = $logger;
this.$errors = $errors;
this.$filesHashService = $filesHashService;
this.$injector = $injector;
this.$mobileHelper = $mobileHelper;
this.$nodeModulesDependenciesBuilder = $nodeModulesDependenciesBuilder;
this.ensureValidProductionPlugins = _.memoize(this._ensureValidProductionPlugins, (productionDependencies, projectDir, platform) => {
let key = _.sortBy(productionDependencies, (p) => p.directory)
.map((d) => JSON.stringify(d, null, 2))
.join("\n");
key += projectDir + platform;
return key;
});
}
async add(plugin, projectData) {
await this.ensure(projectData);
const possiblePackageName = path.resolve(plugin);
if (possiblePackageName.indexOf(".tgz") !== -1 &&
this.$fs.exists(possiblePackageName)) {
plugin = possiblePackageName;
}
const name = (await this.$packageManager.install(plugin, projectData.projectDir, this.npmInstallOptions)).name;
const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule(name, projectData.projectDir);
const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson);
if (realNpmPackageJson.nativescript) {
const pluginData = this.convertToPluginData(realNpmPackageJson, projectData.projectDir);
// Validate
const action = async (pluginDestinationPath, platform, platformData) => {
this.isPluginDataValidForPlatform(pluginData, platform, projectData);
};
await this.executeForAllInstalledPlatforms(action, projectData);
this.$logger.info(`Successfully installed plugin ${realNpmPackageJson.name}.`);
}
else {
await this.$packageManager.uninstall(realNpmPackageJson.name, { save: true }, projectData.projectDir);
this.$errors.fail(`${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`);
}
}
async remove(pluginName, projectData) {
const removePluginNativeCodeAction = async (modulesDestinationPath, platform, platformData) => {
const pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir);
await platformData.platformProjectService.removePluginNativeCode(pluginData, projectData);
};
await this.executeForAllInstalledPlatforms(removePluginNativeCodeAction, projectData);
await this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName, projectData);
let showMessage = true;
const action = async (modulesDestinationPath, platform, platformData) => {
shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName));
this.$logger.info(`Successfully removed plugin ${pluginName} for ${platform}.`);
showMessage = false;
};
await this.executeForAllInstalledPlatforms(action, projectData);
if (showMessage) {
this.$logger.info(`Successfully removed plugin ${pluginName}`);
}
}
addToPackageJson(plugin, version, isDev, projectDir) {
const packageJsonPath = this.getPackageJsonFilePath(projectDir);
let packageJsonContent = this.$fs.readJson(packageJsonPath);
const collectionKey = isDev ? "devDependencies" : "dependencies";
const oppositeCollectionKey = isDev ? "dependencies" : "devDependencies";
if (packageJsonContent[oppositeCollectionKey] &&
packageJsonContent[oppositeCollectionKey][plugin]) {
const result = this.removeDependencyFromPackageJsonContent(plugin, packageJsonContent);
packageJsonContent = result.packageJsonContent;
}
packageJsonContent[collectionKey] = packageJsonContent[collectionKey] || {};
packageJsonContent[collectionKey][plugin] = version;
this.$fs.writeJson(packageJsonPath, packageJsonContent);
}
removeFromPackageJson(plugin, projectDir) {
const packageJsonPath = this.getPackageJsonFilePath(projectDir);
const packageJsonContent = this.$fs.readJson(packageJsonPath);
const result = this.removeDependencyFromPackageJsonContent(plugin, packageJsonContent);
if (result.hasModifiedPackageJson) {
this.$fs.writeJson(packageJsonPath, result.packageJsonContent);
}
}
async preparePluginNativeCode({ pluginData, platform, projectData, }) {
const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platform);
if (this.$fs.exists(pluginPlatformsFolderPath)) {
const pathToPluginsBuildFile = path.join(platformData.projectRoot, constants.PLUGINS_BUILD_DATA_FILENAME);
const allPluginsNativeHashes = this.getAllPluginsNativeHashes(pathToPluginsBuildFile);
const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name];
const currentPluginNativeHashes = await this.getPluginNativeHashes(pluginPlatformsFolderPath);
if (!oldPluginNativeHashes ||
this.$filesHashService.hasChangesInShasums(oldPluginNativeHashes, currentPluginNativeHashes)) {
await platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData);
const updatedPluginNativeHashes = await this.getPluginNativeHashes(pluginPlatformsFolderPath);
this.setPluginNativeHashes({
pathToPluginsBuildFile,
pluginData,
currentPluginNativeHashes: updatedPluginNativeHashes,
allPluginsNativeHashes,
});
}
}
}
async ensureAllDependenciesAreInstalled(projectData) {
const packageJsonContent = this.$fs.readJson(this.getPackageJsonFilePath(projectData.projectDir));
const allDependencies = _.keys(packageJsonContent.dependencies).concat(_.keys(packageJsonContent.devDependencies));
const notInstalledDependencies = allDependencies
.map((dep) => {
this.$logger.trace(`Checking if ${dep} is installed...`);
const pathToPackage = (0, package_path_helper_1.resolvePackagePath)(dep, {
paths: [projectData.projectDir],
});
if (pathToPackage) {
// return false if the dependency is installed - we'll filter out boolean values
// and end up with an array of dep names that are not installed if we end up
// inside the catch block.
return false;
}
this.$logger.trace(`${dep} is not installed, or couldn't be found`);
return dep;
})
.filter(Boolean);
if (this.$options.force || notInstalledDependencies.length) {
this.$logger.trace("Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", notInstalledDependencies);
await this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
disableNpmInstall: this.$options.disableNpmInstall,
frameworkPath: this.$options.frameworkPath,
ignoreScripts: this.$options.ignoreScripts,
path: this.$options.path,
});
}
}
async getAllInstalledPlugins(projectData) {
const nodeModules = (await this.getAllInstalledModules(projectData)).map((nodeModuleData) => this.convertToPluginData(nodeModuleData, projectData.projectDir));
return _.filter(nodeModules, (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin);
}
getAllProductionPlugins(projectData, platform, dependencies) {
dependencies =
dependencies ||
this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir, projectData.ignoredDependencies);
if (_.isEmpty(dependencies)) {
return [];
}
let productionPlugins = dependencies.filter((d) => !!d.nativescript);
productionPlugins = this.ensureValidProductionPlugins(productionPlugins, projectData.projectDir, platform);
return productionPlugins
.map((plugin) => this.convertToPluginData(plugin, projectData.projectDir))
.filter((item, idx, self) => {
// Filter out duplicates to speed up build times by not building the same dependency
// multiple times. One possible downside is that if there are different versions
// of the same native dependency only the first one in the array will be built
return self.findIndex((p) => p.name === item.name) === idx;
});
}
getDependenciesFromPackageJson(projectDir) {
const packageJson = this.$fs.readJson(this.getPackageJsonFilePath(projectDir));
const dependencies = this.getBasicPluginInformation(packageJson.dependencies);
const devDependencies = this.getBasicPluginInformation(packageJson.devDependencies);
return {
dependencies,
devDependencies,
};
}
isNativeScriptPlugin(pluginPackageJsonPath) {
const pluginPackageJsonContent = this.$fs.readJson(pluginPackageJsonPath);
return pluginPackageJsonContent && pluginPackageJsonContent.nativescript;
}
_ensureValidProductionPlugins(productionDependencies, projectDir, platform) {
let clonedProductionDependencies = _.cloneDeep(productionDependencies);
platform = platform.toLowerCase();
if (this.$mobileHelper.isAndroidPlatform(platform)) {
this.ensureValidProductionPluginsForAndroid(clonedProductionDependencies);
}
else if (this.$mobileHelper.isiOSPlatform(platform)) {
clonedProductionDependencies = this.ensureValidProductionPluginsForIOS(clonedProductionDependencies, projectDir, platform);
}
return clonedProductionDependencies;
}
ensureValidProductionPluginsForAndroid(productionDependencies) {
const dependenciesGroupedByName = _.groupBy(productionDependencies, (p) => p.name);
_.each(dependenciesGroupedByName, (dependencyOccurrences, dependencyName) => {
if (dependencyOccurrences.length > 1) {
// the dependency exists multiple times in node_modules
const dependencyOccurrencesGroupedByVersion = _.groupBy(dependencyOccurrences, (g) => g.version);
const versions = _.keys(dependencyOccurrencesGroupedByVersion);
if (versions.length === 1) {
// all dependencies with this name have the same version
this.$logger.trace(`Detected same versions (${_.first(versions)}) of the ${dependencyName} installed at locations: ${_.map(dependencyOccurrences, (d) => d.directory).join(", ")}`);
}
else {
this.$logger.trace(`Detected different versions of the ${dependencyName} installed at locations: ${_.map(dependencyOccurrences, (d) => d.directory).join(", ")}\nThis can cause build failures.`);
}
}
});
}
ensureValidProductionPluginsForIOS(productionDependencies, projectDir, platform) {
const dependenciesWithFrameworks = [];
_.each(productionDependencies, (d) => {
const pathToPlatforms = path.join(d.directory, "platforms", platform);
if (this.$fs.exists(pathToPlatforms)) {
const contents = this.$fs.readDirectory(pathToPlatforms);
_.each(contents, (file) => {
if (path.extname(file) === ".framework") {
dependenciesWithFrameworks.push({
...d,
frameworkName: path.basename(file),
frameworkLocation: path.join(pathToPlatforms, file),
});
}
});
}
});
if (dependenciesWithFrameworks.length > 0) {
const dependenciesGroupedByFrameworkName = _.groupBy(dependenciesWithFrameworks, (d) => d.frameworkName);
_.each(dependenciesGroupedByFrameworkName, (dependencyOccurrences, frameworkName) => {
if (dependencyOccurrences.length > 1) {
// A framework exists multiple times in node_modules
const groupedByName = _.groupBy(dependencyOccurrences, (d) => d.name);
const pluginsNames = _.keys(groupedByName);
if (pluginsNames.length > 1) {
// fail - the same framework is installed by different dependencies.
const locations = dependencyOccurrences.map((d) => d.frameworkLocation);
let msg = `Detected the framework ${frameworkName} is installed from multiple plugins at locations:\n${locations.join("\n")}\n`;
msg += this.getHelpMessage(projectDir);
this.$errors.fail(msg);
}
const dependencyName = _.first(pluginsNames);
const dependencyOccurrencesGroupedByVersion = _.groupBy(dependencyOccurrences, (g) => g.version);
const versions = _.keys(dependencyOccurrencesGroupedByVersion);
if (versions.length === 1) {
// all dependencies with this name have the same version
this.$logger.warn(`Detected the framework ${frameworkName} is installed multiple times from the same versions of plugin (${_.first(versions)}) at locations: ${_.map(dependencyOccurrences, (d) => d.directory).join(", ")}`);
const selectedPackage = _.minBy(dependencyOccurrences, (d) => d.depth);
this.$logger.info(color_1.color.green(`CLI will use only the native code from '${selectedPackage.directory}'.`));
_.each(dependencyOccurrences, (dependency) => {
if (dependency !== selectedPackage) {
productionDependencies.splice(productionDependencies.indexOf(dependency), 1);
}
});
}
else {
const message = this.getFailureMessageForDifferentDependencyVersions(dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, projectDir);
this.$errors.fail(message);
}
}
});
}
return productionDependencies;
}
getFailureMessageForDifferentDependencyVersions(dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, projectDir) {
let message = `Cannot use the same framework ${frameworkName} multiple times in your application.
This framework comes from ${dependencyName} plugin, which is installed multiple times in node_modules:\n`;
_.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => {
message += dependencies.map((d) => `* Path: ${d.directory}, version: ${d.version}\n`);
});
message += this.getHelpMessage(projectDir);
return message;
}
getHelpMessage(projectDir) {
const existingLockFiles = [];
PluginsService.LOCK_FILES.forEach((lockFile) => {
if (this.$fs.exists(path.join(projectDir, lockFile))) {
existingLockFiles.push(lockFile);
}
});
let msgForLockFiles = "";
if (existingLockFiles.length) {
msgForLockFiles += ` and ${existingLockFiles.join(", ")}`;
}
return `\nProbably you need to update your dependencies, remove node_modules${msgForLockFiles} and try again.`;
}
convertToPluginData(cacheData, projectDir) {
try {
const pluginData = {};
pluginData.name = cacheData.name;
pluginData.version = cacheData.version;
pluginData.fullPath =
cacheData.directory ||
path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
pluginData.isPlugin = !!cacheData.nativescript;
pluginData.pluginPlatformsFolderPath = (platform) => {
if (this.$mobileHelper.isvisionOSPlatform(platform)) {
platform = "ios" /* constants.PlatformTypes.ios */;
}
return path.join(pluginData.fullPath, "platforms", platform.toLowerCase());
};
const data = cacheData.nativescript;
if (pluginData.isPlugin) {
pluginData.platformsData = data.platforms;
pluginData.pluginVariables = data.variables;
}
return pluginData;
}
catch (err) {
this.$logger.trace("NOTE: There appears to be a problem with this dependency:", cacheData.name);
this.$logger.trace(err);
return null;
}
}
removeDependencyFromPackageJsonContent(dependency, packageJsonContent) {
let hasModifiedPackageJson = false;
if (packageJsonContent.devDependencies &&
packageJsonContent.devDependencies[dependency]) {
delete packageJsonContent.devDependencies[dependency];
hasModifiedPackageJson = true;
}
if (packageJsonContent.dependencies &&
packageJsonContent.dependencies[dependency]) {
delete packageJsonContent.dependencies[dependency];
hasModifiedPackageJson = true;
}
return {
hasModifiedPackageJson,
packageJsonContent,
};
}
getBasicPluginInformation(dependencies) {
return _.map(dependencies, (version, key) => ({
name: key,
version: version,
}));
}
getNodeModulesPath(projectDir) {
return path.join(projectDir, "node_modules");
}
getPackageJsonFilePath(projectDir) {
return path.join(projectDir, "package.json");
}
getPackageJsonFilePathForModule(moduleName, projectDir) {
const pathToJsonFile = (0, package_path_helper_1.resolvePackageJSONPath)(moduleName, {
paths: [projectDir],
});
return pathToJsonFile;
}
getDependencies(projectDir) {
const packageJsonFilePath = this.getPackageJsonFilePath(projectDir);
return _.keys(require(packageJsonFilePath).dependencies);
}
getNodeModuleData(module, projectDir) {
// module can be modulePath or moduleName
if (!this.$fs.exists(module) || path.basename(module) !== "package.json") {
module = this.getPackageJsonFilePathForModule(module, projectDir);
}
const data = this.$fs.readJson(module);
return {
name: data.name,
version: data.version,
fullPath: path.dirname(module),
isPlugin: data.nativescript !== undefined,
nativescript: data.nativescript,
};
}
async ensure(projectData) {
await this.ensureAllDependenciesAreInstalled(projectData);
this.$fs.ensureDirectoryExists(this.getNodeModulesPath(projectData.projectDir));
}
async getAllInstalledModules(projectData) {
await this.ensure(projectData);
const nodeModules = this.getDependencies(projectData.projectDir);
return _.map(nodeModules, (nodeModuleName) => this.getNodeModuleData(nodeModuleName, projectData.projectDir));
}
async executeNpmCommand(npmCommandName, npmCommandArguments, projectData) {
if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) {
await this.$packageManager.install(npmCommandArguments, projectData.projectDir, this.npmInstallOptions);
}
else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) {
await this.$packageManager.uninstall(npmCommandArguments, PluginsService.NPM_CONFIG, projectData.projectDir);
}
return this.parseNpmCommandResult(npmCommandArguments);
}
parseNpmCommandResult(npmCommandResult) {
return npmCommandResult.split("@")[0]; // returns plugin name
}
async executeForAllInstalledPlatforms(action, projectData) {
const availablePlatforms = this.$mobileHelper.platformNames.map((p) => p.toLowerCase());
for (const platform of availablePlatforms) {
const isPlatformInstalled = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase()));
if (isPlatformInstalled) {
const platformData = this.$platformsDataService.getPlatformData(platform.toLowerCase(), projectData);
const pluginDestinationPath = path.join(platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, "tns_modules");
await action(pluginDestinationPath, platform.toLowerCase(), platformData);
}
}
}
getInstalledFrameworkVersion(platform, projectData) {
const runtimePackage = this.$projectDataService.getRuntimePackage(projectData.projectDir, platform);
// const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
// const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName);
return runtimePackage.version;
}
isPluginDataValidForPlatform(pluginData, platform, projectData) {
let isValid = true;
const installedFrameworkVersion = this.getInstalledFrameworkVersion(platform, projectData);
const pluginPlatformsData = pluginData.platformsData;
if (pluginPlatformsData) {
const versionRequiredByPlugin = pluginPlatformsData[platform];
if (!versionRequiredByPlugin) {
this.$logger.warn(`${pluginData.name} is not supported for ${platform}.`);
isValid = false;
}
else if (semver.gt(versionRequiredByPlugin, installedFrameworkVersion)) {
this.$logger.warn(`${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.`);
isValid = false;
}
}
return isValid;
}
async getPluginNativeHashes(pluginPlatformsDir) {
let data = {};
if (this.$fs.exists(pluginPlatformsDir)) {
const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginPlatformsDir);
data = await this.$filesHashService.generateHashes(pluginNativeDataFiles);
}
return data;
}
getAllPluginsNativeHashes(pathToPluginsBuildFile) {
if (this.$options.hostProjectPath) {
// TODO: force rebuild plugins for now until we decide where to put .ns-plugins-build-data.json when embedding
return {};
}
let data = {};
if (this.$fs.exists(pathToPluginsBuildFile)) {
data = this.$fs.readJson(pathToPluginsBuildFile);
}
return data;
}
setPluginNativeHashes(opts) {
if (this.$options.hostProjectPath) {
// TODO: force rebuild plugins for now until we decide where to put .ns-plugins-build-data.json when embedding
return;
}
opts.allPluginsNativeHashes[opts.pluginData.name] =
opts.currentPluginNativeHashes;
this.$fs.writeJson(opts.pathToPluginsBuildFile, opts.allPluginsNativeHashes);
}
}
exports.PluginsService = PluginsService;
PluginsService.INSTALL_COMMAND_NAME = "install";
PluginsService.UNINSTALL_COMMAND_NAME = "uninstall";
PluginsService.NPM_CONFIG = {
save: true,
};
PluginsService.LOCK_FILES = [
"package-lock.json",
"npm-shrinkwrap.json",
"yarn.lock",
"pnpm-lock.yaml",
];
yok_1.injector.register("pluginsService", PluginsService);
//# sourceMappingURL=plugins-service.js.map
;