diff --git a/lib/DependencyProvider.js b/lib/DependencyProvider.js index 4f1ac4df..09f5bd32 100644 --- a/lib/DependencyProvider.js +++ b/lib/DependencyProvider.js @@ -1,9 +1,9 @@ // @flow const fs = require('fs'); -const os = require('os'); const path = require('path'); const wasm = require('elm-solve-deps-wasm'); +const ElmHome = require('./ElmHome.js'); const SyncGet = require('./SyncGet.js'); const collator = new Intl.Collator('en', { numeric: true }); // for sorting SemVer strings @@ -17,7 +17,7 @@ class OnlineVersionsCache { map /*: Map> */ = new Map(); update() { - const pubgrubHome = path.join(elmHome(), 'pubgrub'); + const pubgrubHome = path.join(ElmHome.elmHome(), 'pubgrub'); fs.mkdirSync(pubgrubHome, { recursive: true }); const cachePath = path.join(pubgrubHome, 'versions_cache.json'); const remotePackagesUrl = 'https://package.elm-lang.org/all-packages'; @@ -139,7 +139,7 @@ class OfflineAvailableVersionLister { } function readVersionsInElmHomeAndSort(pkg /*: string */) /*: Array */ { - const pkgPath = homePkgPath(pkg); + const pkgPath = ElmHome.packagePath(pkg); let offlineVersions; try { offlineVersions = fs.readdirSync(pkgPath); @@ -316,9 +316,9 @@ function cacheElmJsonPath( pkg /*: string */, version /*: string */ ) /*: string */ { - const parts = splitAuthorPkg(pkg); + const parts = ElmHome.splitAuthorPkg(pkg); return path.join( - elmHome(), + ElmHome.elmHome(), 'pubgrub', 'elm_json_cache', parts.author, @@ -332,20 +332,7 @@ function homeElmJsonPath( pkg /*: string */, version /*: string */ ) /*: string */ { - return path.join(homePkgPath(pkg), version, 'elm.json'); -} - -function homePkgPath(pkg /*: string */) /*: string */ { - const parts = splitAuthorPkg(pkg); - return path.join(elmHome(), '0.19.1', 'packages', parts.author, parts.pkg); -} - -function splitAuthorPkg(pkgIdentifier /*: string */) /*: { - author: string, - pkg: string, -} */ { - const parts = pkgIdentifier.split('/'); - return { author: parts[0], pkg: parts[1] }; + return path.join(ElmHome.packagePath(pkg), version, 'elm.json'); } function splitPkgVersion(str /*: string */) /*: { @@ -356,28 +343,4 @@ function splitPkgVersion(str /*: string */) /*: { return { pkg: parts[0], version: parts[1] }; } -function elmHome() /*: string */ { - const elmHomeEnv = process.env['ELM_HOME']; - return elmHomeEnv === undefined ? defaultElmHome() : elmHomeEnv; -} - -function defaultElmHome() /*: string */ { - return process.platform === 'win32' - ? defaultWindowsElmHome() - : defaultUnixElmHome(); -} - -function defaultUnixElmHome() /*: string */ { - return path.join(os.homedir(), '.elm'); -} - -function defaultWindowsElmHome() /*: string */ { - const appData = process.env.APPDATA; - const dir = - appData === undefined - ? path.join(os.homedir(), 'AppData', 'Roaming') - : appData; - return path.join(dir, 'elm'); -} - module.exports = DependencyProvider; diff --git a/lib/ElmHome.js b/lib/ElmHome.js new file mode 100644 index 00000000..787eaee4 --- /dev/null +++ b/lib/ElmHome.js @@ -0,0 +1,47 @@ +// @flow + +const path = require('path'); +const os = require('os'); + +function elmHome() /*: string */ { + const elmHomeEnv = process.env['ELM_HOME']; + return elmHomeEnv === undefined ? defaultElmHome() : elmHomeEnv; +} + +function defaultElmHome() /*: string */ { + return process.platform === 'win32' + ? defaultWindowsElmHome() + : defaultUnixElmHome(); +} + +function defaultUnixElmHome() /*: string */ { + return path.join(os.homedir(), '.elm'); +} + +function defaultWindowsElmHome() /*: string */ { + const appData = process.env.APPDATA; + const dir = + appData === undefined + ? path.join(os.homedir(), 'AppData', 'Roaming') + : appData; + return path.join(dir, 'elm'); +} + +function packagePath(pkg /*: string */) /*: string */ { + const parts = splitAuthorPkg(pkg); + return path.join(elmHome(), '0.19.1', 'packages', parts.author, parts.pkg); +} + +function splitAuthorPkg(pkgIdentifier /*: string */) /*: { + author: string, + pkg: string, +} */ { + const parts = pkgIdentifier.split('/'); + return { author: parts[0], pkg: parts[1] }; +} + +module.exports = { + elmHome, + packagePath, + splitAuthorPkg, +}; diff --git a/lib/ElmJson.js b/lib/ElmJson.js index c9784220..96e1608d 100644 --- a/lib/ElmJson.js +++ b/lib/ElmJson.js @@ -184,6 +184,7 @@ function stringify(json /*: mixed */) /*: string */ { } module.exports = { + Dependencies, DirectAndIndirectDependencies, ElmJson, getPath, diff --git a/lib/Install.js b/lib/Install.js index 99c753da..f4187ee5 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -2,12 +2,30 @@ const spawn = require('cross-spawn'); const fs = require('fs'); +const https = require('https'); const path = require('path'); +const chalk = require('chalk'); +const ElmHome = require('./ElmHome'); const ElmJson = require('./ElmJson'); const Project = require('./Project'); void Project; +function rmDirSync(dir /*: string */) /*: void */ { + // We can replace this with just `fs.rmSync(dir, { recursive: true, force: true })` + // when Node.js 12 is EOL 2022-04-30 and support for Node.js 12 is dropped. + // `fs.rmSync` was added in Node.js 14.14.0, which is also when the + // `recursive` option of `fs.rmdirSync` was deprecated. The `if` avoids + // printing a deprecation message. + // $FlowFixMe[prop-missing]: Flow does not know of `fs.rmSync` yet. + if (fs.rmSync !== undefined) { + fs.rmSync(dir, { recursive: true, force: true }); + } else if (fs.existsSync(dir)) { + // $FlowFixMe[extra-arg]: Flow does not know of the options argument yet. + fs.rmdirSync(dir, { recursive: true }); + } +} + function install( project /*: typeof Project.Project */, pathToElmBinary /*: string */, @@ -19,18 +37,7 @@ function install( // Recreate the directory to remove any artifacts from the last time // someone ran `elm-test install`. We do not delete this directory after // the installation finishes in case the user needs to debug the test run. - // We can replace this with just `fs.rmSync(installationScratchDir, { recursive: true, force: true })` - // when Node.js 12 is EOL 2022-04-30 and support for Node.js 12 is dropped. - // `fs.rmSync` was added in Node.js 14.14.0, which is also when the - // `recursive` option of `fs.rmdirSync` was deprecated. The `if` avoids - // printing a deprecation message. - // $FlowFixMe[prop-missing]: Flow does not know of `fs.rmSync` yet. - if (fs.rmSync !== undefined) { - fs.rmSync(installationScratchDir, { recursive: true, force: true }); - } else if (fs.existsSync(installationScratchDir)) { - // $FlowFixMe[extra-arg]: Flow does not know of the options argument yet. - fs.rmdirSync(installationScratchDir, { recursive: true }); - } + rmDirSync(installationScratchDir); fs.mkdirSync(installationScratchDir, { recursive: true }); } catch (error) { throw new Error( @@ -117,6 +124,154 @@ function install( return 'SuccessfullyInstalled'; } +async function downloadFileNative( + url /*: string */, + filePath /*: string */ +) /*: Promise */ { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + + const request = https.get(url, (response) => { + if (response.statusCode !== 200) { + fs.unlink(filePath, () => { + reject(new Error(`Failed to get '${url}' (${response.statusCode})`)); + }); + } else { + response.pipe(file); + } + }); + + // The destination stream is ended by the time it's called + file.on('finish', () => resolve()); + + request.on('error', (err) => { + fs.unlink(filePath, () => reject(err)); + }); + + file.on('error', (err) => { + fs.unlink(filePath, () => reject(err)); + }); + + request.end(); + }); +} + +function getDirectTestDependencies( + project /*: typeof Project.Project */ +) /*: typeof ElmJson.Dependencies */ { + switch (project.elmJson.type) { + case 'application': + return project.elmJson['test-dependencies'].direct; + case 'package': + return project.elmJson['test-dependencies']; + } +} + +const PKG = 'elm-explorations/test'; +const VERSION = '1.2.2'; + +async function installUnstableTestMaster( + project /*: typeof Project.Project */ +) /*: Promise */ { + const directTestDependencies = getDirectTestDependencies(project); + const actualVersion = directTestDependencies[PKG]; + if (actualVersion !== VERSION) { + throw new Error( + ` +Could not find ${JSON.stringify(PKG)}: ${JSON.stringify( + VERSION + )} in your elm.json file here: + +${ElmJson.getPath(project.rootDir)} + +This command only works if you have ${PKG} as a (direct) test-dependency, +and only if you use version ${VERSION}. + +${ + actualVersion === undefined + ? 'I could not find it at all.' + : `You seem to be using version ${actualVersion}.` +} + `.trim() + ); + } + + console.log( + chalk.yellow(`Using the master version of ${PKG} in place of ${VERSION}.`) + ); + console.log( + chalk.yellow( + `Note: You will need to use the \`elm-test uninstall-unstable-test-master\` command afterwards to get back to the ${VERSION} version.` + ) + ); + + const pkgWithDash = PKG.replace('/', '-'); + + const tempPath = project.generatedCodeDir; + const zipballUrl = `https://codeload.github.com/${PKG}/zip/refs/heads/master`; + const zipballPath = path.join(tempPath, `${pkgWithDash}.zip`); + + const packagePath = path.join(ElmHome.packagePath(PKG), VERSION); + + console.log(chalk.dim.yellow(`Removing ${tempPath}`)); + rmDirSync(tempPath); + + console.log(chalk.dim.yellow(`Removing ${packagePath}`)); + rmDirSync(packagePath); + + fs.mkdirSync(tempPath, { recursive: true }); + fs.mkdirSync(packagePath, { recursive: true }); + + console.log(chalk.dim.yellow(`Downloading ${zipballUrl}`)); + await downloadFileNative(zipballUrl, zipballPath); + + console.log(chalk.dim.yellow(`Unzipping ${zipballPath}`)); + const unzipResult = spawn.sync('unzip', [ + '-o', // overwrite + zipballPath, // file to unzip + '-d', + tempPath, // directory where to extract files + ]); + + if (unzipResult.status === 0) { + console.log(chalk.dim.yellow(`Moving to ELM_HOME: ${packagePath}`)); + fs.renameSync(path.join(tempPath, 'test-master'), packagePath); + } else { + // Windows does not have `unzip`, but BSD `tar`. On Windows, we have to extract + // straight into `packagePath` (instead of `tempPath`), because `fs.renameSync` + // gives an EPERM error otherwise, which seems to be due to how antivirus works + // on Windows. + const tarResult = spawn.sync('tar', [ + 'zxf', // eXtract Zipped File + zipballPath, // file to unzip + '-C', + packagePath, // directory where to extract files + '--strip-components=1', // strip the inner 'test-master' folder + ]); + if (tarResult.status !== 0) { + throw new Error('Failed to unzip the elm-explorations/test repo zipfile'); + } + } + + console.log(chalk.dim.yellow(`Removing ${zipballPath}`)); + fs.unlinkSync(zipballPath); +} + +function uninstallUnstableTestMaster( + project /*: typeof Project.Project */ +) /*: void */ { + const { generatedCodeDir } = project; + const packagePath = path.join(ElmHome.packagePath(PKG), VERSION); + + console.log(chalk.dim.yellow(`Removing ${generatedCodeDir}`)); + rmDirSync(generatedCodeDir); + + console.log(chalk.dim.yellow(`Removing ${packagePath}`)); + rmDirSync(packagePath); +} + module.exports = { install, + installUnstableTestMaster, + uninstallUnstableTestMaster, }; diff --git a/lib/elm-test.js b/lib/elm-test.js index 3ff462c9..a426ba2b 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -217,7 +217,7 @@ function main() { const pathToElmBinary = getPathToElmBinary(options.compiler); const project = getProject('make'); const make = async () => { - await Generate.generateElmJson(dependencyProvider, project); + Generate.generateElmJson(dependencyProvider, project); await Compile.compileSources( FindTests.resolveGlobs( testFileGlobs.length === 0 ? [project.testsDir] : testFileGlobs, @@ -262,6 +262,56 @@ function main() { ); }); + program + .command('install-unstable-test-master') + .description( + 'Use the `master` version of the elm-explorations/test library' + ) + .action(() => { + const project = getProject('install-unstable-test-master'); + Install.installUnstableTestMaster(project).then( + () => process.exit(0), + (error) => { + console.error(error.message); + process.exit(1); + } + ); + }); + + program + .command('uninstall-unstable-test-master') + .description( + 'Stop using the `master` version of the elm-explorations/test library' + ) + .action(() => { + const options = program.opts(); + const pathToElmBinary = getPathToElmBinary(options.compiler); + const project = getProject('uninstall-unstable-test-master'); + const run = async () => { + Install.uninstallUnstableTestMaster(project); + // Install project elm-explorations/test again. This is based on the `make` command. + Generate.generateElmJson(dependencyProvider, project); + const dummyFile = path.join(project.generatedCodeDir, 'Dummy.elm'); + fs.writeFileSync( + dummyFile, + `module Dummy exposing (dummy)\ndummy = ()` + ); + await Compile.compileSources( + [dummyFile], + project.generatedCodeDir, + pathToElmBinary, + options.report + ); + }; + run().then( + () => process.exit(0), + (error) => { + console.error(error.message); + process.exit(1); + } + ); + }); + program.parse(process.argv); }