diff --git a/lib/definitions/libnpmconfig.d.ts b/lib/definitions/libnpmconfig.d.ts new file mode 100644 index 0000000000..63d2ea0313 --- /dev/null +++ b/lib/definitions/libnpmconfig.d.ts @@ -0,0 +1,3 @@ +declare module "libnpmconfig" { + export function read(options?: Object): Object; +} diff --git a/lib/services/pacote-service.ts b/lib/services/pacote-service.ts index 212bb706b4..290a598034 100644 --- a/lib/services/pacote-service.ts +++ b/lib/services/pacote-service.ts @@ -2,12 +2,20 @@ import * as pacote from "pacote"; import * as tar from "tar"; import * as path from "path"; import { cache } from "../common/decorators"; +import * as npmconfig from "libnpmconfig"; export class PacoteService implements IPacoteService { + private npmConfig: { [index: string]: any } = {}; + constructor(private $fs: IFileSystem, private $injector: IInjector, private $logger: ILogger, - private $proxyService: IProxyService) { } + private $proxyService: IProxyService) { + npmconfig.read().forEach((value: any, key: string) => { + // replace env ${VARS} in strings with the process.env value + this.npmConfig[key] = typeof value !== 'string' ? value : value.replace(/\${([^}]+)}/, (_, envVar) => process.env[envVar] ); + }); + } @cache() public get $packageManager(): INodePackageManager { @@ -18,6 +26,7 @@ export class PacoteService implements IPacoteService { public async manifest(packageName: string, options?: IPacoteManifestOptions): Promise { this.$logger.trace(`Calling pacoteService.manifest for packageName: '${packageName}' and options: ${options}`); const manifestOptions: IPacoteBaseOptions = await this.getPacoteBaseOptions(); + if (options) { _.extend(manifestOptions, options); } @@ -67,7 +76,9 @@ export class PacoteService implements IPacoteService { private async getPacoteBaseOptions(): Promise { // In case `tns create myapp --template https://github.com/NativeScript/template-hello-world.git` command is executed, pacote module throws an error if cache option is not provided. const cachePath = await this.$packageManager.getCachePath(); - const pacoteOptions = { cache: cachePath }; + + // Add NPM Configuration to our Manifest options + const pacoteOptions = _.extend( this.npmConfig, {cache: cachePath }); const proxySettings = await this.$proxyService.getCache(); if (proxySettings) { _.extend(pacoteOptions, proxySettings); diff --git a/package.json b/package.json index 9b934c7cfb..c0aef722cf 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,11 @@ "nativescript-dev-xcode": "0.2.0", "nativescript-doctor": "1.10.0", "nativescript-preview-sdk": "0.3.4", + "libnpmconfig": "1.2.1", "open": "0.0.5", "ora": "2.0.0", "osenv": "0.1.3", - "pacote": "8.1.6", + "pacote": "9.5.4", "pako": "1.0.6", "pbxproj-dom": "1.2.0", "plist": "1.1.0", @@ -139,4 +140,4 @@ "engines": { "node": ">=10.0.0 <13.0.0" } -} \ No newline at end of file +} diff --git a/test/services/pacote-service.ts b/test/services/pacote-service.ts index acdbaa96b4..e5280f9ab3 100644 --- a/test/services/pacote-service.ts +++ b/test/services/pacote-service.ts @@ -4,15 +4,17 @@ import { PacoteService } from '../../lib/services/pacote-service'; import { LoggerStub } from "../stubs"; import { sandbox, SinonSandbox, SinonStub } from "sinon"; import { EventEmitter } from "events"; + +const npmconfig = require("libnpmconfig"); const pacote = require("pacote"); const tar = require("tar"); const path = require("path"); -const npmCachePath = "npmCachePath"; +const defaultPacoteOpts: IPacoteBaseOptions = createPacoteOptions({}); +const npmCachePath = defaultPacoteOpts['cache']; const packageName = "testPackage"; const fullPath = `/Users/username/${packageName}`; const destinationDir = "destinationDir"; -const defaultPacoteOpts: IPacoteBaseOptions = { cache: npmCachePath }; const errorMessage = "error message"; const proxySettings: IProxySettings = { hostname: "hostname", @@ -36,6 +38,20 @@ interface ITestCase extends ITestSetup { expectedArgs: any[]; } +function createPacoteOptions(source: Object): Object { + const options: { [index: string]: any } = {}; + npmconfig.read().forEach((value: any, key: string) => { + // replace env ${VARS} in strings with the process.env value + options[key] = typeof value !== 'string' ? value : value.replace(/\${([^}]+)}/, (_, envVar) => process.env[envVar] ); + }); + + // Copy any original source keys over our defaults + for (const key in source) { + options[key] = source[key]; + } + return options; +} + const createTestInjector = (opts?: ITestSetup): IInjector => { opts = opts || {}; @@ -103,8 +119,15 @@ describe("pacoteService", () => { const setupTest = (opts?: ITestSetup): IPacoteService => { opts = opts || {}; const testInjector = createTestInjector(opts); + if (opts.isLocalPackage) { - sandboxInstance.stub(path, "resolve").withArgs(packageName).returns(fullPath); + const oldPath = path.resolve; + sandboxInstance.stub(path, "resolve").callsFake((value:string) => { + if (value === packageName) { + return fullPath; + } + return oldPath(value); + }); } return testInjector.resolve("pacoteService"); @@ -116,7 +139,7 @@ describe("pacoteService", () => { const testData: ITestCase[] = [ { name: "with 'cache' only when no opts are passed", - expectedArgs: [packageName, defaultPacoteOpts] + expectedArgs: [packageName, _.extend({}, defaultPacoteOpts)] }, { name: "with 'cache' and passed options", @@ -137,7 +160,7 @@ describe("pacoteService", () => { { name: "with full path to file when it is local one", isLocalPackage: true, - expectedArgs: [fullPath, defaultPacoteOpts] + expectedArgs: [fullPath, _.extend({}, defaultPacoteOpts)] }, { name: "with full path to file, 'cache' and passed options when local path is passed",