Skip to content

Commit 9f948a1

Browse files
feat: improve detection of NativeScript plugins
Currently in case you have multiple occurences of a NativeScript plugin in node_modules, most of the time the build of application fails. For iOS Xcode fails with duplicate resources (most commonly frameworks) and for Android the Static Binding Generator will fail in case the JavaScript of a plugin differs. Improve the handling by checking the versions of NativeScript plugins. In case the same version is installed multiple times, show warning to the user and link only one of the versions to the native build. In case multiple versions are detected, throw an error - currently we cannot support this case.
1 parent 7d4d628 commit 9f948a1

File tree

7 files changed

+427
-69
lines changed

7 files changed

+427
-69
lines changed

lib/declarations.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ interface IDependencyData {
368368
* Dependencies of the current module.
369369
*/
370370
dependencies?: string[];
371+
372+
version: string;
371373
}
372374

373375
interface INpmsResult {
@@ -1071,4 +1073,4 @@ interface IWatchIgnoreListService {
10711073

10721074
interface INpmConfigService {
10731075
getConfig(): IDictionary<any>;
1074-
}
1076+
}

lib/definitions/plugins.d.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ interface IPluginsService {
1414
*/
1515
getDependenciesFromPackageJson(projectDir: string): IPackageJsonDepedenciesResult;
1616
preparePluginNativeCode(preparePluginNativeCodeData: IPreparePluginNativeCodeData): Promise<void>;
17-
convertToPluginData(cacheData: any, projectDir: string): IPluginData;
1817
isNativeScriptPlugin(pluginPackageJsonPath: string): boolean;
1918
}
2019

@@ -44,7 +43,7 @@ interface IPluginData extends INodeModuleData {
4443
interface INodeModuleData extends IBasePluginData {
4544
fullPath: string;
4645
isPlugin: boolean;
47-
moduleInfo: any;
46+
nativescript: any;
4847
}
4948

5049
interface IPluginPlatformsData {

lib/services/plugins-service.ts

+70-17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export class PluginsService implements IPluginsService {
99
private static NPM_CONFIG = {
1010
save: true
1111
};
12+
13+
private static LOCK_FILES = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml"];
14+
1215
private get $platformsDataService(): IPlatformsDataService {
1316
return this.$injector.resolve("platformsDataService");
1417
}
@@ -169,24 +172,17 @@ export class PluginsService implements IPluginsService {
169172
return _.filter(nodeModules, nodeModuleData => nodeModuleData && nodeModuleData.isPlugin);
170173
}
171174

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

177178
if (_.isEmpty(dependencies)) {
178-
return allProductionPlugins;
179+
return [];
179180
}
180181

181-
_.forEach(dependencies, dependency => {
182-
const isPlugin = !!dependency.nativescript;
183-
if (isPlugin) {
184-
const pluginData = this.convertToPluginData(dependency, projectData.projectDir);
185-
allProductionPlugins.push(pluginData);
186-
}
187-
});
188-
189-
return allProductionPlugins;
182+
let productionPlugins: IDependencyData[] = dependencies.filter(d => !!d.nativescript);
183+
productionPlugins = this.ensureValidProductionPlugins(productionPlugins, projectData.projectDir);
184+
const pluginData = productionPlugins.map(plugin => this.convertToPluginData(plugin, projectData.projectDir));
185+
return pluginData;
190186
}
191187

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

209-
public convertToPluginData(cacheData: any, projectDir: string): IPluginData {
205+
private ensureValidProductionPlugins = _.memoize<(productionDependencies: IDependencyData[], projectDir: string) => IDependencyData[]>(this._ensureValidProductionPlugins, (productionDependencies: IDependencyData[], projectDir: string) => {
206+
let key = _.sortBy(productionDependencies, p => p.directory).map(d => JSON.stringify(d, null, 2)).join("\n");
207+
key += projectDir;
208+
return key;
209+
});
210+
211+
private _ensureValidProductionPlugins(productionDependencies: IDependencyData[], projectDir: string): IDependencyData[] {
212+
const clonedProductionDependencies = _.cloneDeep(productionDependencies);
213+
const dependenciesGroupedByName = _.groupBy(clonedProductionDependencies, p => p.name);
214+
_.each(dependenciesGroupedByName, (dependencyOccurrences, dependencyName) => {
215+
if (dependencyOccurrences.length > 1) {
216+
// the dependency exists multiple times in node_modules
217+
const dependencyOccurrencesGroupedByVersion = _.groupBy(dependencyOccurrences, g => g.version);
218+
const versions = _.keys(dependencyOccurrencesGroupedByVersion);
219+
if (versions.length === 1) {
220+
// all dependencies with this name have the same version
221+
this.$logger.warn(`Detected same versions (${_.first(versions)}) of the ${dependencyName} installed at locations: ${_.map(dependencyOccurrences, d => d.directory).join(", ")}`);
222+
const selectedPackage = _.minBy(dependencyOccurrences, d => d.depth);
223+
this.$logger.info(`CLI will use only the native code from '${selectedPackage.directory}'.`.green);
224+
_.each(dependencyOccurrences, dependency => {
225+
if (dependency !== selectedPackage) {
226+
clonedProductionDependencies.splice(clonedProductionDependencies.indexOf(dependency), 1);
227+
}
228+
});
229+
} else {
230+
const message = this.getFailureMessageForDifferentDependencyVersions(dependencyName, dependencyOccurrencesGroupedByVersion, projectDir);
231+
this.$errors.fail(message);
232+
}
233+
}
234+
});
235+
236+
return clonedProductionDependencies;
237+
}
238+
239+
private getFailureMessageForDifferentDependencyVersions(dependencyName: string, dependencyOccurrencesGroupedByVersion: IDictionary<IDependencyData[]>, projectDir: string): string {
240+
let message = `Cannot use different versions of a NativeScript plugin in your application.
241+
${dependencyName} plugin occurs multiple times in node_modules:\n`;
242+
_.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => {
243+
message += dependencies.map(d => `* Path: ${d.directory}, version: ${d.version}\n`);
244+
});
245+
246+
const existingLockFiles: string[] = [];
247+
PluginsService.LOCK_FILES.forEach(lockFile => {
248+
if (this.$fs.exists(path.join(projectDir, lockFile))) {
249+
existingLockFiles.push(lockFile);
250+
}
251+
});
252+
253+
let msgForLockFiles: string = "";
254+
if (existingLockFiles.length) {
255+
msgForLockFiles += ` and ${existingLockFiles.join(", ")}`;
256+
}
257+
258+
message += `Probably you need to update your dependencies, remove node_modules${msgForLockFiles} and try again.`;
259+
return message;
260+
}
261+
262+
private convertToPluginData(cacheData: IDependencyData | INodeModuleData, projectDir: string): IPluginData {
210263
const pluginData: any = {};
211264
pluginData.name = cacheData.name;
212265
pluginData.version = cacheData.version;
213-
pluginData.fullPath = cacheData.directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
214-
pluginData.isPlugin = !!cacheData.nativescript || !!cacheData.moduleInfo;
266+
pluginData.fullPath = (<IDependencyData>cacheData).directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
267+
pluginData.isPlugin = !!cacheData.nativescript;
215268
pluginData.pluginPlatformsFolderPath = (platform: string) => path.join(pluginData.fullPath, "platforms", platform.toLowerCase());
216-
const data = cacheData.nativescript || cacheData.moduleInfo;
269+
const data = cacheData.nativescript;
217270

218271
if (pluginData.isPlugin) {
219272
pluginData.platformsData = data.platforms;
@@ -280,7 +333,7 @@ export class PluginsService implements IPluginsService {
280333
version: data.version,
281334
fullPath: path.dirname(module),
282335
isPlugin: data.nativescript !== undefined,
283-
moduleInfo: data.nativescript
336+
nativescript: data.nativescript
284337
};
285338
}
286339

lib/tools/node-modules/node-modules-dependencies-builder.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesB
9393
const dependency: IDependencyData = {
9494
name,
9595
directory,
96-
depth
96+
depth,
97+
version: null
9798
};
9899

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

106+
dependency.version = packageJsonContents.version;
105107
if (!!packageJsonContents.nativescript) {
106108
// add `nativescript` property, necessary for resolving plugins
107109
dependency.nativescript = packageJsonContents.nativescript;

0 commit comments

Comments
 (0)