diff --git a/lib/definitions/android-plugin-migrator.d.ts b/lib/definitions/android-plugin-migrator.d.ts index 1836931b0f..f5ad873307 100644 --- a/lib/definitions/android-plugin-migrator.d.ts +++ b/lib/definitions/android-plugin-migrator.d.ts @@ -44,9 +44,9 @@ interface IBuildAndroidPluginData extends Partial { * Optional custom Gradle path. */ gradlePath?: string; - - /** + + /** * Optional custom Gradle arguments. */ - gradleArgs?: string, + gradleArgs?: string; } diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 03fc61d512..f19636a248 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -258,7 +258,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { await this.setupGradle( pluginTempDir, options.platformsAndroidDirPath, - options.projectDir + options.projectDir, + options.pluginName ); await this.buildPlugin({ gradlePath: options.gradlePath, @@ -398,7 +399,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private async setupGradle( pluginTempDir: string, platformsAndroidDirPath: string, - projectDir: string + projectDir: string, + pluginName: string ): Promise { const gradleTemplatePath = path.resolve( path.join(__dirname, "../../vendor/gradle-plugin") @@ -419,6 +421,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { buildGradlePath, runtimeGradleVersions.gradleAndroidPluginVersion ); + this.replaceFileContent(buildGradlePath, "{{pluginName}}", pluginName); } private async getRuntimeGradleVersions( @@ -729,7 +732,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { `-PcompileSdk=android-${pluginBuildSettings.androidToolsInfo.compileSdkVersion}`, `-PbuildToolsVersion=${pluginBuildSettings.androidToolsInfo.buildToolsVersion}`, `-PappPath=${this.$projectData.getAppDirectoryPath()}`, - `-PappResourcesPath=${this.$projectData.getAppResourcesDirectoryPath()}` + `-PappResourcesPath=${this.$projectData.getAppResourcesDirectoryPath()}`, ]; if (pluginBuildSettings.gradleArgs) { localArgs.push(pluginBuildSettings.gradleArgs); diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index c621cd3418..648eedec6c 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -48,6 +48,71 @@ import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import { INotConfiguredEnvOptions } from "../common/definitions/commands"; +interface NativeDependency { + name: string; + directory: string; + dependencies: string[]; +} + +// +// we sort the native dependencies topologically to make sure they are processed in the right order +// native dependenciess need to be sorted so the deepst dependencies are built before it's parents +// +// for example, given this dep structure (assuming these are all native dependencies that need to be built) +// |- dep1 +// |- dep2 +// |- dep3 +// |- dep4 +// |-dep5 +// |- dep6 +// +// It is sorted: +// |- dep1 - doesn't depend on anything, so the order stays the same as in the input list +// |- dep3 - doesn't depend on anything, so the order stays the same as in the input list +// |- dep5 - doesn't depend on anything, so the order stays the same as in the input list +// |- dep6 - doesn't depend on anything, so the order stays the same as in the input list +// |- dep4 - depends on dep6, so dep6 must be built first, ie above ^ +// |- dep2 - depends on dep3, dep4, dep5 and dep6, so all of them must be built first +// +// for more details see: https://wikiless.org/wiki/Topological_sorting?lang=en +// +function topologicalSortNativeDependencies( + nativeDeps: NativeDependency[], + start: NativeDependency[] = [], + depth = 0 +): NativeDependency[] { + const processedDeps = nativeDeps.reduce( + (accumulator, nativeDep: NativeDependency) => { + if ( + nativeDep.dependencies.every( + Array.prototype.includes, + accumulator.map((n) => n.name) + ) + ) { + accumulator.push(nativeDep); + } + return accumulator; + }, + start + ); + + const remainingDeps = nativeDeps.filter( + (nativeDep) => !processedDeps.includes(nativeDep) + ); + + // recurse if we still have unprocessed deps + // the second condition here prevents infinite recursion + if (remainingDeps.length && depth <= nativeDeps.length) { + return topologicalSortNativeDependencies( + remainingDeps, + processedDeps, + depth + 1 + ); + } + + return processedDeps; +} + export class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { private static VALUES_DIRNAME = "values"; private static VALUES_VERSION_DIRNAME_PREFIX = @@ -635,10 +700,10 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async beforePrepareAllPlugins( projectData: IProjectData, dependencies?: IDependencyData[] - ): Promise { + ): Promise { if (dependencies) { dependencies = this.filterUniqueDependencies(dependencies); - this.provideDependenciesJson(projectData, dependencies); + return this.provideDependenciesJson(projectData, dependencies); } } @@ -666,7 +731,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private provideDependenciesJson( projectData: IProjectData, dependencies: IDependencyData[] - ): void { + ): IDependencyData[] { const platformDir = path.join( projectData.platformsDir, AndroidProjectService.ANDROID_PLATFORM_NAME @@ -675,15 +740,37 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject platformDir, constants.DEPENDENCIES_JSON_NAME ); - const nativeDependencies = dependencies - .filter(AndroidProjectService.isNativeAndroidDependency) - .map(({ name, directory }) => ({ - name, - directory: path.relative(platformDir, directory), - })); - const jsonContent = JSON.stringify(nativeDependencies, null, 4); + let nativeDependencyData = dependencies.filter( + AndroidProjectService.isNativeAndroidDependency + ); + let nativeDependencies = nativeDependencyData.map( + ({ name, directory, dependencies }) => { + return { + name, + directory: path.relative(platformDir, directory), + dependencies: dependencies.filter((dep) => { + // filter out transient dependencies that don't have native dependencies + return ( + nativeDependencyData.findIndex( + (nativeDep) => nativeDep.name === dep + ) !== -1 + ); + }), + } as NativeDependency; + } + ); + nativeDependencies = topologicalSortNativeDependencies(nativeDependencies); + const jsonContent = JSON.stringify(nativeDependencies, null, 4); this.$fs.writeFile(dependenciesJsonPath, jsonContent); + + // we sort all the dependencies to respect the topological sorting of the native dependencies + return dependencies.sort(function (a, b) { + return ( + nativeDependencies.findIndex((n) => n.name === a.name) - + nativeDependencies.findIndex((n) => n.name === b.name) + ); + }); } private static isNativeAndroidDependency({ diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 11892f7fe9..fc427f1442 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -13,7 +13,11 @@ import { IOSProvisionService } from "./ios-provision-service"; import { IOSEntitlementsService } from "./ios-entitlements-service"; import { IOSBuildData } from "../data/build-data"; import { IOSPrepareData } from "../data/prepare-data"; -import { BUILD_XCCONFIG_FILE_NAME, CONFIG_FILE_NAME_DISPLAY, IosProjectConstants } from "../constants"; +import { + BUILD_XCCONFIG_FILE_NAME, + CONFIG_FILE_NAME_DISPLAY, + IosProjectConstants, +} from "../constants"; import { hook } from "../common/helpers"; import { IPlatformData, @@ -29,8 +33,14 @@ import { IIOSNativeTargetService, IValidatePlatformOutput, } from "../definitions/project"; + import { IBuildData } from "../definitions/build"; -import { IXcprojService, IXcconfigService, IOptions } from "../declarations"; +import { + IXcprojService, + IXcconfigService, + IDependencyData, + IOptions, +} from "../declarations"; import { IPluginData, IPluginsService } from "../definitions/plugins"; import { IFileSystem, @@ -976,8 +986,11 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ await this.addExtensions(projectData, pluginsData); } - public beforePrepareAllPlugins(): Promise { - return Promise.resolve(); + public beforePrepareAllPlugins( + projectData: IProjectData, + dependencies?: IDependencyData[] + ): Promise { + return Promise.resolve(dependencies); } public async checkForChanges( diff --git a/lib/services/platform-environment-requirements.ts b/lib/services/platform-environment-requirements.ts index f4c73f55a8..c3bd8e2019 100644 --- a/lib/services/platform-environment-requirements.ts +++ b/lib/services/platform-environment-requirements.ts @@ -24,9 +24,8 @@ export class PlatformEnvironmentRequirements // private $staticConfig: IStaticConfig, private $analyticsService: IAnalyticsService, // @ts-ignore - required by the hook helper! - private $injector: IInjector - ) // private $previewQrCodeService: IPreviewQrCodeService - {} + private $injector: IInjector // private $previewQrCodeService: IPreviewQrCodeService + ) {} // public get $previewAppController(): IPreviewAppController { // return this.$injector.resolve("previewAppController"); diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 91fcf3a054..ba16acdd36 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -93,7 +93,10 @@ export class ProjectChangesService implements IProjectChangesService { ); this.$nodeModulesDependenciesBuilder - .getProductionDependencies(projectData.projectDir, projectData.ignoredDependencies) + .getProductionDependencies( + projectData.projectDir, + projectData.ignoredDependencies + ) .filter( (dep) => dep.nativescript && diff --git a/lib/services/webpack/webpack.d.ts b/lib/services/webpack/webpack.d.ts index a9f6fa4eb4..e3a8dddd8b 100644 --- a/lib/services/webpack/webpack.d.ts +++ b/lib/services/webpack/webpack.d.ts @@ -163,7 +163,7 @@ declare global { beforePrepareAllPlugins( projectData: IProjectData, dependencies?: IDependencyData[] - ): Promise; + ): Promise; handleNativeDependenciesChange( projectData: IProjectData, diff --git a/lib/tools/node-modules/node-modules-builder.ts b/lib/tools/node-modules/node-modules-builder.ts index 6be5874213..a7ba048de4 100644 --- a/lib/tools/node-modules/node-modules-builder.ts +++ b/lib/tools/node-modules/node-modules-builder.ts @@ -18,10 +18,10 @@ export class NodeModulesBuilder implements INodeModulesBuilder { platformData, projectData, }: IPrepareNodeModulesData): Promise { - const dependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies( + let dependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies( projectData.projectDir, projectData.ignoredDependencies ); - await platformData.platformProjectService.beforePrepareAllPlugins( + dependencies = await platformData.platformProjectService.beforePrepareAllPlugins( projectData, dependencies ); diff --git a/lib/tools/node-modules/node-modules-dependencies-builder.ts b/lib/tools/node-modules/node-modules-dependencies-builder.ts index 4ceb820708..24abbb426d 100644 --- a/lib/tools/node-modules/node-modules-dependencies-builder.ts +++ b/lib/tools/node-modules/node-modules-dependencies-builder.ts @@ -5,6 +5,7 @@ import { IDependencyData } from "../../declarations"; import { IFileSystem } from "../../common/declarations"; import * as _ from "lodash"; import { injector } from "../../common/yok"; +import { resolvePackagePath } from "@rigor789/resolve-package-path"; interface IDependencyDescription { parent: IDependencyDescription; @@ -43,7 +44,8 @@ export class NodeModulesDependenciesBuilder const currentModule = queue.shift(); const resolvedDependency = this.findModule( currentModule, - resolvedDependencies + resolvedDependencies, + projectPath ); if ( @@ -86,16 +88,29 @@ export class NodeModulesDependenciesBuilder private findModule( depDescription: IDependencyDescription, - resolvedDependencies: IDependencyData[] + resolvedDependencies: IDependencyData[], + rootPath: string ): IDependencyData { try { const parentModulesPath = depDescription?.parentDir ?? depDescription?.parent?.parentDir; - const modulePath = require - .resolve(`${depDescription.name}/package.json`, { - paths: [parentModulesPath], - }) - .replace(/[\\/]+package\.json$/, ""); + + let modulePath: string = resolvePackagePath(depDescription.name, { + paths: [parentModulesPath], + }); + + // perhaps traverse up the tree here? + if (!modulePath) { + // fallback to searching in the root path + modulePath = resolvePackagePath(depDescription.name, { + paths: [rootPath], + }); + } + + // if we failed to find the module... + if (!modulePath) { + return null; + } // if we already resolved this dependency, we return null to avoid a duplicate resolution if ( diff --git a/test/services/test-execution-service.ts b/test/services/test-execution-service.ts index 03c8d468d4..ad6ad5c62a 100644 --- a/test/services/test-execution-service.ts +++ b/test/services/test-execution-service.ts @@ -9,113 +9,114 @@ const karmaPluginName = "karma"; const unitTestsPluginName = "@nativescript/unit-test-runner"; function getTestExecutionService(): ITestExecutionService { - const injector = new InjectorStub(); - injector.register("testExecutionService", TestExecutionService); - injector.register("runController", {}); + const injector = new InjectorStub(); + injector.register("testExecutionService", TestExecutionService); + injector.register("runController", {}); - return injector.resolve("testExecutionService"); + return injector.resolve("testExecutionService"); } function getDependenciesObj(deps: string[]): IDictionary { - const depsObj: IDictionary = {}; - deps.forEach((dep) => { - depsObj[dep] = "1.0.0"; - }); + const depsObj: IDictionary = {}; + deps.forEach((dep) => { + depsObj[dep] = "1.0.0"; + }); - return depsObj; + return depsObj; } describe("testExecutionService", () => { - const testCases = [ - { - name: - "should return false when the project has no dependencies and dev dependencies", - expectedCanStartKarmaServer: false, - projectData: { dependencies: {}, devDependencies: {} }, - }, - { - name: "should return false when the project has no karma", - expectedCanStartKarmaServer: false, - projectData: { - dependencies: getDependenciesObj([unitTestsPluginName]), - devDependencies: {}, - }, - }, - { - name: "should return false when the project has no unit test runner", - expectedCanStartKarmaServer: false, - projectData: { - dependencies: getDependenciesObj([karmaPluginName]), - devDependencies: {}, - }, - }, - { - name: - "should return true when the project has the required plugins as dependencies", - expectedCanStartKarmaServer: true, - projectData: { - dependencies: getDependenciesObj([ - karmaPluginName, - unitTestsPluginName, - ]), - devDependencies: {}, - }, - }, - { - name: - "should return true when the project has the required plugins as dev dependencies", - expectedCanStartKarmaServer: true, - projectData: { - dependencies: {}, - devDependencies: getDependenciesObj([ - karmaPluginName, - unitTestsPluginName, - ]), - }, - }, - { - name: - "should return true when the project has the required plugins as dev and normal dependencies", - expectedCanStartKarmaServer: true, - projectData: { - dependencies: getDependenciesObj([karmaPluginName]), - devDependencies: getDependenciesObj([unitTestsPluginName]), - }, - }, - ]; + const testCases = [ + { + name: + "should return false when the project has no dependencies and dev dependencies", + expectedCanStartKarmaServer: false, + projectData: { dependencies: {}, devDependencies: {} }, + }, + { + name: "should return false when the project has no karma", + expectedCanStartKarmaServer: false, + projectData: { + dependencies: getDependenciesObj([unitTestsPluginName]), + devDependencies: {}, + }, + }, + { + name: "should return false when the project has no unit test runner", + expectedCanStartKarmaServer: false, + projectData: { + dependencies: getDependenciesObj([karmaPluginName]), + devDependencies: {}, + }, + }, + { + name: + "should return true when the project has the required plugins as dependencies", + expectedCanStartKarmaServer: true, + projectData: { + dependencies: getDependenciesObj([ + karmaPluginName, + unitTestsPluginName, + ]), + devDependencies: {}, + }, + }, + { + name: + "should return true when the project has the required plugins as dev dependencies", + expectedCanStartKarmaServer: true, + projectData: { + dependencies: {}, + devDependencies: getDependenciesObj([ + karmaPluginName, + unitTestsPluginName, + ]), + }, + }, + { + name: + "should return true when the project has the required plugins as dev and normal dependencies", + expectedCanStartKarmaServer: true, + projectData: { + dependencies: getDependenciesObj([karmaPluginName]), + devDependencies: getDependenciesObj([unitTestsPluginName]), + }, + }, + ]; - describe("canStartKarmaServer", () => { - _.each(testCases, (testCase: any) => { - it(`${testCase.name}`, async () => { - const testExecutionService = getTestExecutionService(); + describe("canStartKarmaServer", () => { + _.each(testCases, (testCase: any) => { + it(`${testCase.name}`, async () => { + const testExecutionService = getTestExecutionService(); - // todo: cleanup monkey-patch with a friendlier syntax (util?) - // MOCK require.resolve - const Module = require('module'); - const originalResolveFilename = Module._resolveFilename + // todo: cleanup monkey-patch with a friendlier syntax (util?) + // MOCK require.resolve + const Module = require("module"); + const originalResolveFilename = Module._resolveFilename; - Module._resolveFilename = function (...args: any) { - if (args[0].startsWith(karmaPluginName) - && (testCase.projectData.dependencies[karmaPluginName] - || testCase.projectData.devDependencies[karmaPluginName]) - ) { - // override with a "random" built-in module to - // ensure the module can be resolved - args[0] = 'fs' - } + Module._resolveFilename = function (...args: any) { + if ( + args[0].startsWith(karmaPluginName) && + (testCase.projectData.dependencies[karmaPluginName] || + testCase.projectData.devDependencies[karmaPluginName]) + ) { + // override with a "random" built-in module to + // ensure the module can be resolved + args[0] = "fs"; + } - return originalResolveFilename.apply(this, args) - } - // END MOCK + return originalResolveFilename.apply(this, args); + }; + // END MOCK - const canStartKarmaServer = await testExecutionService.canStartKarmaServer( - testCase.projectData - ); - assert.equal(canStartKarmaServer, testCase.expectedCanStartKarmaServer); + const canStartKarmaServer = await testExecutionService.canStartKarmaServer( + testCase.projectData + ); + assert.equal(canStartKarmaServer, testCase.expectedCanStartKarmaServer); - // restore mock - Module._resolveFilename = originalResolveFilename; - }); - }); - }); + // restore mock + Module._resolveFilename = originalResolveFilename; + }); + }); + }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 4bf5ce54c4..5e599f1671 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -49,6 +49,9 @@ import { IDeviceDebugService, IDebugResultInfo, } from "../lib/definitions/debug"; +import { + IDependencyData, +} from "../lib/declarations"; import { IBuildData } from "../lib/definitions/build"; import { IFileSystem, @@ -827,8 +830,11 @@ export class PlatformProjectServiceStub return Promise.resolve(); } - async beforePrepareAllPlugins(): Promise { - return Promise.resolve(); + async beforePrepareAllPlugins( + projectData: IProjectData, + dependencies?: IDependencyData[] + ): Promise { + return Promise.resolve(dependencies); } async cleanDeviceTempFolder(deviceIdentifier: string): Promise { diff --git a/vendor/gradle-plugin/build.gradle b/vendor/gradle-plugin/build.gradle index b7f03c8ce7..4199a42fad 100644 --- a/vendor/gradle-plugin/build.gradle +++ b/vendor/gradle-plugin/build.gradle @@ -7,8 +7,10 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' - buildscript { + def getDepPlatformDir = { dep -> + file("${project.ext.USER_PROJECT_ROOT}/${project.ext.PLATFORMS_ANDROID}/${dep.directory}/$PLATFORMS_ANDROID") + } def computeKotlinVersion = { -> project.hasProperty("kotlinVersion") ? kotlinVersion : "1.6.0" } def kotlinVersion = computeKotlinVersion() repositories { @@ -24,10 +26,18 @@ buildscript { } // Set up styled logger + project.ext.getDepPlatformDir = getDepPlatformDir project.ext.outLogger = services.get(StyledTextOutputFactory).create("colouredOutputLogger") project.ext.USER_PROJECT_ROOT = "$rootDir/../../.." - + project.ext.PLATFORMS_ANDROID = "platforms/android" + project.ext.PLUGIN_NAME = "{{pluginName}}" + + // the build script will not work with previous versions of the CLI (3.1 or earlier) + def dependenciesJson = file("${project.ext.USER_PROJECT_ROOT}/${project.ext.PLATFORMS_ANDROID}/dependencies.json") + def appDependencies = new JsonSlurper().parseText(dependenciesJson.text) + def pluginData = appDependencies.find { it.name == project.ext.PLUGIN_NAME } + project.ext.nativescriptDependencies = appDependencies.findAll{pluginData.dependencies.contains(it.name)} project.ext.getAppPath = { -> def relativePathToApp = "app" def nsConfigFile = file("$USER_PROJECT_ROOT/nsconfig.json") @@ -86,6 +96,15 @@ buildscript { apply from: pathToBuildScriptGradle, to: buildscript } + nativescriptDependencies.each { dep -> + def pathToPluginBuildScriptGradle = "${getDepPlatformDir(dep)}/buildscript.gradle" + def pluginBuildScriptGradle = file(pathToPluginBuildScriptGradle) + if (pluginBuildScriptGradle.exists()) { + outLogger.withStyle(Style.SuccessHeader).println "\t + applying user-defined buildscript from dependency ${pluginBuildScriptGradle}" + apply from: pathToPluginBuildScriptGradle, to: buildscript + } + } + def pathToPluginBuildScriptGradle = "$rootDir/buildscript.gradle" def pluginBuildScriptGradle = file(pathToPluginBuildScriptGradle) if (pluginBuildScriptGradle.exists()) { @@ -97,8 +116,19 @@ buildscript { } +def pluginDependencies + allprojects { repositories { + // used for local *.AAR files + pluginDependencies = nativescriptDependencies.collect { + getDepPlatformDir(it) + } + + // some plugins may have their android dependencies in a /libs subdirectory + pluginDependencies.addAll(nativescriptDependencies.collect { + "${getDepPlatformDir(it)}/libs" + }) mavenLocal() mavenCentral() maven { @@ -106,6 +136,11 @@ allprojects { name 'Google' } jcenter() + if (pluginDependencies.size() > 0) { + flatDir { + dirs pluginDependencies + } + } } } @@ -116,7 +151,16 @@ def computeBuildToolsVersion = { -> } android { + def applyPluginGradleConfigurations = { -> + nativescriptDependencies.each { dep -> + def includeGradlePath = "${getDepPlatformDir(dep)}/include.gradle" + if (file(includeGradlePath).exists()) { + apply from: includeGradlePath + } + } + } applyBeforePluginGradleConfiguration() + applyPluginGradleConfigurations() compileSdkVersion computeCompileSdkVersion() buildToolsVersion computeBuildToolsVersion() @@ -137,4 +181,25 @@ def applyBeforePluginGradleConfiguration() { outLogger.withStyle(Style.SuccessHeader).println "\t ~ applying user-defined configuration from ${beforePluginGradle}" apply from: pathToBeforePluginGradle } +} + +task addDependenciesFromNativeScriptPlugins { + nativescriptDependencies.each { dep -> + def aarFiles = fileTree(dir: getDepPlatformDir(dep), include: ["**/*.aar"]) + aarFiles.each { aarFile -> + def length = aarFile.name.length() - 4 + def fileName = aarFile.name[0.. + def jarFileAbsolutePath = jarFile.getAbsolutePath() + outLogger.withStyle(Style.SuccessHeader).println "\t + adding jar plugin dependency: $jarFileAbsolutePath" + pluginsJarLibraries.add(jarFile.getAbsolutePath()) + } + + project.dependencies.add("implementation", jarFiles) + } } \ No newline at end of file