Skip to content

feat: improve detection of NativeScript plugins #5221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ interface IDependencyData {
* Dependencies of the current module.
*/
dependencies?: string[];

version: string;
}

interface INpmsResult {
Expand Down Expand Up @@ -1071,4 +1073,4 @@ interface IWatchIgnoreListService {

interface INpmConfigService {
getConfig(): IDictionary<any>;
}
}
3 changes: 1 addition & 2 deletions lib/definitions/plugins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ interface IPluginsService {
*/
getDependenciesFromPackageJson(projectDir: string): IPackageJsonDepedenciesResult;
preparePluginNativeCode(preparePluginNativeCodeData: IPreparePluginNativeCodeData): Promise<void>;
convertToPluginData(cacheData: any, projectDir: string): IPluginData;
isNativeScriptPlugin(pluginPackageJsonPath: string): boolean;
}

Expand Down Expand Up @@ -44,7 +43,7 @@ interface IPluginData extends INodeModuleData {
interface INodeModuleData extends IBasePluginData {
fullPath: string;
isPlugin: boolean;
moduleInfo: any;
nativescript: any;
}

interface IPluginPlatformsData {
Expand Down
87 changes: 70 additions & 17 deletions lib/services/plugins-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export class PluginsService implements IPluginsService {
private static NPM_CONFIG = {
save: true
};

private static LOCK_FILES = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml"];

private get $platformsDataService(): IPlatformsDataService {
return this.$injector.resolve("platformsDataService");
}
Expand Down Expand Up @@ -169,24 +172,17 @@ export class PluginsService implements IPluginsService {
return _.filter(nodeModules, nodeModuleData => nodeModuleData && nodeModuleData.isPlugin);
}

//This method will traverse all non dev dependencies (not only the root/installed ones) and filter the plugins.
public getAllProductionPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): IPluginData[] {
const allProductionPlugins: IPluginData[] = [];
dependencies = dependencies || this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir);

if (_.isEmpty(dependencies)) {
return allProductionPlugins;
return [];
}

_.forEach(dependencies, dependency => {
const isPlugin = !!dependency.nativescript;
if (isPlugin) {
const pluginData = this.convertToPluginData(dependency, projectData.projectDir);
allProductionPlugins.push(pluginData);
}
});

return allProductionPlugins;
let productionPlugins: IDependencyData[] = dependencies.filter(d => !!d.nativescript);
productionPlugins = this.ensureValidProductionPlugins(productionPlugins, projectData.projectDir);
const pluginData = productionPlugins.map(plugin => this.convertToPluginData(plugin, projectData.projectDir));
return pluginData;
}

public getDependenciesFromPackageJson(projectDir: string): IPackageJsonDepedenciesResult {
Expand All @@ -206,14 +202,71 @@ export class PluginsService implements IPluginsService {
return pluginPackageJsonContent && pluginPackageJsonContent.nativescript;
}

public convertToPluginData(cacheData: any, projectDir: string): IPluginData {
private ensureValidProductionPlugins = _.memoize<(productionDependencies: IDependencyData[], projectDir: string) => IDependencyData[]>(this._ensureValidProductionPlugins, (productionDependencies: IDependencyData[], projectDir: string) => {
let key = _.sortBy(productionDependencies, p => p.directory).map(d => JSON.stringify(d, null, 2)).join("\n");
key += projectDir;
return key;
});

private _ensureValidProductionPlugins(productionDependencies: IDependencyData[], projectDir: string): IDependencyData[] {
const clonedProductionDependencies = _.cloneDeep(productionDependencies);
const dependenciesGroupedByName = _.groupBy(clonedProductionDependencies, 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.warn(`Detected same versions (${_.first(versions)}) of the ${dependencyName} installed at locations: ${_.map(dependencyOccurrences, d => d.directory).join(", ")}`);
const selectedPackage = _.minBy(dependencyOccurrences, d => d.depth);
this.$logger.info(`CLI will use only the native code from '${selectedPackage.directory}'.`.green);
_.each(dependencyOccurrences, dependency => {
if (dependency !== selectedPackage) {
clonedProductionDependencies.splice(clonedProductionDependencies.indexOf(dependency), 1);
}
});
} else {
const message = this.getFailureMessageForDifferentDependencyVersions(dependencyName, dependencyOccurrencesGroupedByVersion, projectDir);
this.$errors.fail(message);
}
}
});

return clonedProductionDependencies;
}

private getFailureMessageForDifferentDependencyVersions(dependencyName: string, dependencyOccurrencesGroupedByVersion: IDictionary<IDependencyData[]>, projectDir: string): string {
let message = `Cannot use different versions of a NativeScript plugin in your application.
${dependencyName} plugin occurs multiple times in node_modules:\n`;
_.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => {
message += dependencies.map(d => `* Path: ${d.directory}, version: ${d.version}\n`);
});

const existingLockFiles: string[] = [];
PluginsService.LOCK_FILES.forEach(lockFile => {
if (this.$fs.exists(path.join(projectDir, lockFile))) {
existingLockFiles.push(lockFile);
}
});

let msgForLockFiles: string = "";
if (existingLockFiles.length) {
msgForLockFiles += ` and ${existingLockFiles.join(", ")}`;
}

message += `Probably you need to update your dependencies, remove node_modules${msgForLockFiles} and try again.`;
return message;
}

private convertToPluginData(cacheData: IDependencyData | INodeModuleData, projectDir: string): IPluginData {
const pluginData: any = {};
pluginData.name = cacheData.name;
pluginData.version = cacheData.version;
pluginData.fullPath = cacheData.directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
pluginData.isPlugin = !!cacheData.nativescript || !!cacheData.moduleInfo;
pluginData.fullPath = (<IDependencyData>cacheData).directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
pluginData.isPlugin = !!cacheData.nativescript;
pluginData.pluginPlatformsFolderPath = (platform: string) => path.join(pluginData.fullPath, "platforms", platform.toLowerCase());
const data = cacheData.nativescript || cacheData.moduleInfo;
const data = cacheData.nativescript;

if (pluginData.isPlugin) {
pluginData.platformsData = data.platforms;
Expand Down Expand Up @@ -280,7 +333,7 @@ export class PluginsService implements IPluginsService {
version: data.version,
fullPath: path.dirname(module),
isPlugin: data.nativescript !== undefined,
moduleInfo: data.nativescript
nativescript: data.nativescript
};
}

Expand Down
4 changes: 3 additions & 1 deletion lib/tools/node-modules/node-modules-dependencies-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesB
const dependency: IDependencyData = {
name,
directory,
depth
depth,
version: null
};

const packageJsonPath = path.join(directory, PACKAGE_JSON_FILE_NAME);
Expand All @@ -102,6 +103,7 @@ export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesB
if (packageJsonExists) {
const packageJsonContents = this.$fs.readJson(packageJsonPath);

dependency.version = packageJsonContents.version;
if (!!packageJsonContents.nativescript) {
// add `nativescript` property, necessary for resolving plugins
dependency.nativescript = packageJsonContents.nativescript;
Expand Down
Loading