From 21b15803aeb654df5956c26a0f68f37ccdc67676 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 20 Nov 2017 13:38:25 +0000 Subject: [PATCH] In tests, restore NPM dependencies using yarn if installed --- .../Node/install-via-yarn-with-shrinkwrap.js | 97 +++++++++++++++++++ .../Helpers/TemplateTestBase.cs | 49 ++++++++-- .../SpaTemplateTest/SpaTemplateTestBase.cs | 16 ++- 3 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 test/Templates.Test/Helpers/Node/install-via-yarn-with-shrinkwrap.js diff --git a/test/Templates.Test/Helpers/Node/install-via-yarn-with-shrinkwrap.js b/test/Templates.Test/Helpers/Node/install-via-yarn-with-shrinkwrap.js new file mode 100644 index 0000000000..280d7e2d96 --- /dev/null +++ b/test/Templates.Test/Helpers/Node/install-via-yarn-with-shrinkwrap.js @@ -0,0 +1,97 @@ +/* +============================================================================================== +Implementation based on https://github.com/dabroek/shrinkwrap-to-lockfile/ by Matthijs Dabroek + +License for original package: +MIT License + +Copyright (c) 2017 Matthijs Dabroek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +============================================================================================== + +Modified to support execution across multiple directories simultaneously, +to provide console output while executing, and to eliminate lodash dependency. +*/ + +const fs = require('fs'); +const path = require('path'); +const execSync = require('child_process').execSync; + +function parseJsonFile(filename) { + let obj = {}; + + try { + const file = fs.readFileSync(path.resolve(process.cwd(), filename), 'utf8'); + obj = JSON.parse(file); + } catch (error) { + throw new Error(`${filename} could not be parsed.`); + } + + return obj; +} + +function getDependencyVersions(dependencies) { + const result = {}; + for (const name in dependencies) { + if (dependencies.hasOwnProperty(name)) { + const pkg = dependencies[name]; + const version = pkg.version || pkg; + result[name] = version.match(/(\d+\.\d+\.\d+(?:-.+)?)/)[0]; + } + } + return result; +} + +function objectMergeLeft(a, b) { + return _.reduce(a, (result, value, key) => { + result[key] = value; + + if (!_.isEqual(value, b[key])) { + result[key] = b[key]; + } + + return result; + }, {}); +} + +function updatePackageJson(shrinkwrapFile, packageFile) { + const packageFilePath = path.resolve(process.cwd(), packageFile); + const origPackageFileContents = fs.readFileSync(packageFilePath); + const shrinkwrapJson = parseJsonFile(shrinkwrapFile); + const packageJson = parseJsonFile(packageFile); + + packageJson.dependencies = getDependencyVersions(shrinkwrapJson.dependencies); + delete packageJson.devDependencies; + + fs.writeFileSync(packageFilePath, JSON.stringify(packageJson, null, 2)); + return { + dispose: () => { + fs.writeFileSync(packageFilePath, origPackageFileContents); + } + }; +} + +const temporaryPackageJson = updatePackageJson('npm-shrinkwrap.json', 'package.json'); +try { + console.log('Generating yarn.lock from npm-shrinkwrap.json...'); + execSync('yarn install --mutex network', { stdio: 'inherit' }); +} finally { + temporaryPackageJson.dispose(); +} diff --git a/test/Templates.Test/Helpers/TemplateTestBase.cs b/test/Templates.Test/Helpers/TemplateTestBase.cs index f8466e1ea8..68c7a6cdc2 100644 --- a/test/Templates.Test/Helpers/TemplateTestBase.cs +++ b/test/Templates.Test/Helpers/TemplateTestBase.cs @@ -73,15 +73,52 @@ namespace Templates.Test } } - protected void RunNpmInstall() + protected void InstallNpmPackages(string relativePath) { - // The first time this runs on any given CI agent it may take several minutes. - // If the agent has NPM 5+ installed, it should be quite a lot quicker on - // subsequent runs because of package caching. - var (exe, args) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + var fullPath = Path.Combine(TemplateOutputDir, relativePath); + + if (!HasYarnInstalled()) + { + Output.WriteLine($"Restoring NPM packages in '{relativePath}' using npm because yarn is not installed..."); + RunViaShell(fullPath, "npm install"); + } + else + { + // Current versions of NPM produce random errors when run on Windows + // (e.g., https://github.com/npm/npm/issues/19004). Plus, it's very slow to + // restore the SPA template packages. To make CI faster and more reliable, use + // Yarn to install the packages. To make Yarn respect the npm-shrinkwrap.json + // files, run a script that temporarily replaces the package.json content using + // dependency information from npm-shrinkwrap.json. + var installViaYarnWithShrinkwrapScriptPath = Path.Combine( + Path.GetDirectoryName(typeof(TemplateTestBase).Assembly.Location), + @"..\..\..\Helpers\Node\install-via-yarn-with-shrinkwrap.js"); + Output.WriteLine($"Restoring NPM packages in '{relativePath}' using yarn..."); + RunViaShell(fullPath, $"node {installViaYarnWithShrinkwrapScriptPath}"); + } + } + + private void RunViaShell(string workingDirectory, string commandAndArgs) + { + var (shellExe, argsPrefix) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ("cmd", "/c") : ("bash", "-c"); - ProcessEx.Run(Output, TemplateOutputDir, exe, args + " \"npm install\"").WaitForExit(assertSuccess: true); + ProcessEx + .Run(Output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"") + .WaitForExit(assertSuccess: true); + } + + private bool HasYarnInstalled() + { + try + { + RunViaShell(TemplateOutputDir, "yarn --version"); + return true; + } + catch + { + return false; + } } protected void AssertDirectoryExists(string path, bool shouldExist) diff --git a/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs b/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs index eb9fa7070a..c61cbdaa3e 100644 --- a/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs +++ b/test/Templates.Test/SpaTemplateTest/SpaTemplateTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using OpenQA.Selenium; +using System.IO; using System.Net; using Templates.Test.Helpers; using Xunit; @@ -21,7 +22,20 @@ namespace Templates.Test.SpaTemplateTest protected void SpaTemplateImpl(string targetFrameworkOverride, string template) { RunDotNetNew(template, targetFrameworkOverride); - RunNpmInstall(); + + // For some SPA templates, the NPM root directory is './ClientApp'. In other + // templates it's at the project root. Strictly speaking we shouldn't have + // to do the NPM restore in tests because it should happen automatically at + // build time, but the tests run a lot faster this way because we can use Yarn. + if (File.Exists(Path.Combine(TemplateOutputDir, "ClientApp", "package.json"))) + { + InstallNpmPackages("ClientApp"); + } + else if (File.Exists(Path.Combine(TemplateOutputDir, "package.json"))) + { + InstallNpmPackages("."); + } + TestApplication(targetFrameworkOverride, publish: false); TestApplication(targetFrameworkOverride, publish: true); }