diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e699be9..009c0920c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ ## [Unreleased] -- Fixed a crash when using `--template` from the same repository but from two different paths. +## [2.11.0] **BETA** - 2023-10-16 + +- Add an `offline` mode to prevent `elm-review` from making any HTTP requests. This is useful for CI environments that should not have access to the internet, where you only want to run `elm-review` without arguments. +- Replaced the internally used `elm-json` dependency with `elm-solve-deps-wasm`, which should be more reliable, re-enable support for old MacOS versions as well as improve performance. +- Fixed a crash when using `--template` from the same repository but from two different paths. + +This is a **BETA** release, so I expect things to break. Please report any issues you encounter on the #elm-review channel on the Elm Slack. ## [2.10.3] - 2023-09-26 diff --git a/documentation/tooling-integration.md b/documentation/tooling-integration.md index 9afa5b431..37ef07b1f 100644 --- a/documentation/tooling-integration.md +++ b/documentation/tooling-integration.md @@ -18,7 +18,7 @@ The CLI creates a bunch of cache inside `elm-stuff/generated-code/jfmengels/elm- - `file-cache/`: Caching of the file's ASTs. - `review-applications/`: Caching of the project's configuration. This is the application we build by compiling the source code in the CLI's `template/` directory. - `result-cache/`: Caching of the results of each module's analysis for every rule. -- `dependencies-cache/`: Caching of the dependencies of the project's configuration computed by `elm-json`. `elm-json` is a bit slow, and doesn't work great offline. This is done so we don't have to compute the dependencies again if the configuration changed but not `review/elm.json`. +- `dependencies-cache/`: Caching of data related to Elm dependencies computed by `elm-solve-deps-wasm` (or `elm-json` prior to `2.11.0`). - `elm-parser/`: Caching of the parser application. This is the application we build by compiling with the user's version of `stil4m/elm-syntax`, and the name of the file reflect the version. This is used to parallelize the parsing of the files, which is kind of slow, at startup. Namespacing things means that data will unfortunately be duplicated, but it is meant to prevent different tools from stepping on each other's toes by saving the same files at the same time as another, potentially corrupting the files. diff --git a/lib/build.js b/lib/build.js index 1159c7c93..2b563b934 100644 --- a/lib/build.js +++ b/lib/build.js @@ -22,6 +22,7 @@ const TemplateDependencies = require('./template-dependencies'); * @typedef { import("./types/build").BuildResult } BuildResult * @typedef { import("./types/build").AppHash } AppHash * @typedef { import("./types/build").ReviewElmJson } ReviewElmJson + * @typedef { import("./types/elm-version").ElmVersion } ElmVersion * @typedef { import("./types/path").Path } Path */ @@ -159,13 +160,7 @@ I can help set you up with an initial configuration if you run ${chalk.magenta(' const buildResult = await Promise.all([ getElmBinary(options), - createTemplateProject( - options, - reviewElmJsonPath, - userSrc, - buildFolder, - reviewElmJson - ) + createTemplateProject(options, userSrc, buildFolder, reviewElmJson) ]).then(([elmBinary]) => { Debug.log('Compiling review application'); Benchmark.start(options, 'Compile review project'); @@ -257,7 +252,6 @@ async function buildFromGitHubTemplate(options, template) { getElmBinary(options), createTemplateProject( options, - reviewElmJsonPath, buildFolder, path.join(buildFolder, 'project'), reviewElmJsonWithReplacedParentDirectories @@ -294,7 +288,6 @@ async function buildFromGitHubTemplate(options, template) { /** * Create a local temporary project to build an Elm application in. * @param {Options} options - * @param {Path} reviewElmJsonPath * @param {Path} userSrc * @param {Path} projectFolder * @param {ReviewElmJson} reviewElmJson @@ -302,7 +295,6 @@ async function buildFromGitHubTemplate(options, template) { */ async function createTemplateProject( options, - reviewElmJsonPath, userSrc, projectFolder, reviewElmJson @@ -312,17 +304,14 @@ async function createTemplateProject( const elmJsonPath = path.join(projectFolder, 'elm.json'); // Load review project's elm.json file contents - const [dependencies, previousElmJson] = await Promise.all([ - TemplateDependencies.get( - options, - reviewElmJson.dependencies, - reviewElmJsonPath - ), - FS.readFile(elmJsonPath).catch(() => null) + const [previousElmJson, dependencies] = await Promise.all([ + FS.readFile(elmJsonPath).catch(() => null), + TemplateDependencies.addRequiredDependencies(options, reviewElmJson) ]); const finalElmJson = updateSourceDirectories(options, userSrc, { ...reviewElmJson, - dependencies + dependencies, + 'test-dependencies': {direct: {}, indirect: {}} }); const finalElmJsonAsString = JSON.stringify(finalElmJson, null, 4); if (previousElmJson !== finalElmJsonAsString) { @@ -558,29 +547,6 @@ async function buildElmParser(options, reviewElmJson) { Debug.log(`Building parser app for elm-syntax v${elmSyntaxVersion}`); - const parseElmElmJsonPath = path.resolve(parseElmFolder, 'elm.json'); - const parseElmElmJson = await FS.readJsonFile(parseElmElmJsonPath).catch( - (error) => { - if (error.code === 'ENOENT') { - return Promise.reject( - new ErrorMessage.CustomError( - // prettier-ignore - 'UNEXPECTED INTERNAL ERROR', - // prettier-ignore - `I was expecting to find the "parseElm" project at ${chalk.cyan(parseElmElmJsonPath)} but could not find it. - -Please open an issue at the following link: -https://github.com/jfmengels/node-elm-review/issues/new -`, - options.elmJsonPath - ) - ); - } - - return Promise.reject(error); - } - ); - const buildFolder = options.buildFolderForParserApp(); const [elmBinary] = await Promise.all([ @@ -588,8 +554,7 @@ https://github.com/jfmengels/node-elm-review/issues/new createParserElmJsonFile( options, buildFolder, - parseElmElmJson, - parseElmElmJsonPath, + reviewElmJson['elm-version'], elmSyntaxVersion ), // Needed when the user has `"type": "module"` in their package.json. @@ -612,33 +577,36 @@ https://github.com/jfmengels/node-elm-review/issues/new ); } +/** Create `elm.json` file for the parser application, which will use the exact same version + * of `stil4m/elm-syntax` as the review application. + * + * @param {Options} options + * @param {Path} buildFolder + * @param {ElmVersion} elmVersion + * @param {ElmVersion} elmSyntaxVersion + * @return {Promise} + */ async function createParserElmJsonFile( options, buildFolder, - parseElmElmJson, - parseElmElmJsonPath, + elmVersion, elmSyntaxVersion ) { - const dependencies = await TemplateDependencies.addElmSyntax( + /** @type {ReviewElmJson} */ + const parseElmElmJson = await TemplateDependencies.addElmSyntax( options, - parseElmElmJsonPath, + elmVersion, elmSyntaxVersion ); + parseElmElmJson['source-directories'] = parseElmElmJson[ + 'source-directories' + ].map((dir) => path.resolve(parseElmFolder, dir)); + await FS.mkdirp(buildFolder); return FS.writeFile( path.resolve(buildFolder, 'elm.json'), - JSON.stringify( - { - ...parseElmElmJson, - dependencies, - 'source-directories': parseElmElmJson['source-directories'].map((dir) => - path.resolve(parseElmFolder, dir) - ) - }, - null, - 2 - ) + JSON.stringify(parseElmElmJson, null, 2) ); } diff --git a/lib/dependency-provider.js b/lib/dependency-provider.js new file mode 100644 index 000000000..3bcb26172 --- /dev/null +++ b/lib/dependency-provider.js @@ -0,0 +1,483 @@ +const fs = require('fs'); +const path = require('path'); +const wasm = require('elm-solve-deps-wasm'); +const FS = require('./fs-wrapper'); +const SyncGet = require('./sync-get'); +const ProjectJsonFiles = require('./project-json-files'); + +/** + * @typedef { import("./types/options").Options } Options + * @typedef { import("./types/path").Path } Path + */ + +/** @type {boolean} */ +let wasmWasInitialized = false; +/** @type {{ get: (string) => string, shutDown: () => void } | null} */ +let syncGetWorker = null; + +class DependencyProvider { + /** @type {OnlineVersionsCache} */ + cache; + + constructor() { + this.cache = new OnlineVersionsCache(); + if (!wasmWasInitialized) { + wasm.init(); + wasmWasInitialized = true; + } + + syncGetWorker = SyncGet.startWorker(); + } + + /** Solve dependencies completely offline, without any http request. + * + * @param {Options} options + * @param {string} elmVersion + * @param {string} elmJson + * @param {Record} extra + * @return {string} + */ + solveOffline(options, elmVersion, elmJson, extra) { + const lister = new OfflineAvailableVersionLister(); + try { + return wasm.solve_deps( + elmJson, + false, + extra, + (pkg, version) => + fetchElmJsonOffline(options, elmVersion, pkg, version), + (/** @type {string} */ pkg) => lister.list(elmVersion, pkg) + ); + } catch (errorMessage) { + throw new Error(errorMessage); + } + } + + /** Solve dependencies with http requests when required. + * + * @param {Options} options + * @param {string} elmVersion + * @param {string} elmJson + * @param {Record} extra + * @return {string} + */ + solveOnline(options, elmVersion, elmJson, extra) { + const lister = new OnlineAvailableVersionLister( + options, + this.cache, + elmVersion + ); + try { + return wasm.solve_deps( + elmJson, + false, + extra, + (pkg, version) => fetchElmJsonOnline(options, elmVersion, pkg, version), + (pkg) => lister.list(elmVersion, pkg) + ); + } catch (errorMessage) { + throw new Error(errorMessage); + } + } + + /** + * @returns {void} + */ + tearDown() { + if (!syncGetWorker) { + return; + } + + syncGetWorker.shutDown(); + syncGetWorker = null; + } +} + +class OfflineAvailableVersionLister { + /** Memoization cache to avoid doing the same work twice in list. + * @type {Map} + */ + cache = new Map(); + + /** + * @param {string} elmVersion + * @param {string} pkg + * @return {string[]} + */ + list(elmVersion, pkg) { + const memoVersions = this.cache.get(pkg); + if (memoVersions !== undefined) { + return memoVersions; + } + + const offlineVersions = readVersionsInElmHomeAndSort(elmVersion, pkg); + + this.cache.set(pkg, offlineVersions); + return offlineVersions; + } +} + +/** Cache of existing versions according to the package website. */ +class OnlineVersionsCache { + /** @type {Map>} */ + map = new Map(); + + /** + * @param {Options} options + * @param {string} elmVersion + * @returns {void} + */ + update(options, elmVersion) { + const cachePath = dependenciesCachePath( + options.packageJsonVersion, + elmVersion + ); + + const remotePackagesUrl = 'https://package.elm-lang.org/all-packages'; + if (this.map.size === 0) { + let cacheFile; + try { + // Read from disk existing versions which are already cached. + cacheFile = fs.readFileSync(cachePath, 'utf8'); + } catch (_) { + // The cache file does not exist so let's reset it. + this.map = onlineVersionsFromScratch(cachePath, remotePackagesUrl); + return; + } + + try { + this.map = parseOnlineVersions(JSON.parse(cacheFile)); + } catch (error) { + throw new Error( + `Failed to parse the cache file ${cachePath}.\n${error.message}` + ); + } + } + + this.updateWithRequestSince(cachePath, remotePackagesUrl); + } + + /** Update the cache with a request to the package server. + * + * @param {string} cachePath + * @param {string} remotePackagesUrl + * @returns {void} + */ + updateWithRequestSince(cachePath, remotePackagesUrl) { + // Count existing versions. + let versionsCount = 0; + for (const versions of this.map.values()) { + versionsCount += versions.length; + } + + // Complete cache with a remote call to the package server. + const remoteUrl = remotePackagesUrl + '/since/' + (versionsCount - 1); // -1 to check if no package was deleted. + if (!syncGetWorker) { + return; + } + + const newVersions = JSON.parse(syncGetWorker.get(remoteUrl)); + if (newVersions.length === 0) { + // Reload from scratch since it means at least one package was deleted from the registry. + this.map = onlineVersionsFromScratch(cachePath, remotePackagesUrl); + return; + } + + // Check that the last package in the list was already in cache + // since the list returned by the package server is sorted newest first. + const {pkg, version} = splitPkgVersion(newVersions.pop()); + const cachePkgVersions = this.map.get(pkg); + if ( + cachePkgVersions !== undefined && + cachePkgVersions[cachePkgVersions.length - 1] === version + ) { + // Insert (in reverse) newVersions into onlineVersionsCache map. + for (const pkgVersion of newVersions.reverse()) { + const {pkg, version} = splitPkgVersion(pkgVersion); + const versionsOfPkg = this.map.get(pkg); + if (versionsOfPkg === undefined) { + this.map.set(pkg, [version]); + } else { + versionsOfPkg.push(version); + } + } + + // Save the updated onlineVersionsCache to disk. + const onlineVersions = Object.fromEntries(this.map.entries()); + fs.writeFileSync(cachePath, JSON.stringify(onlineVersions)); + } else { + // There was a problem and a package got deleted from the server. + this.map = onlineVersionsFromScratch(cachePath, remotePackagesUrl); + } + } + + /** List the versions for a package. + * + * @param {string} pkg + * @returns {string[]} + */ + getVersions(pkg) { + const versions = this.map.get(pkg); + return versions === undefined ? [] : versions; + } +} + +class OnlineAvailableVersionLister { + /** Memoization cache to avoid doing the same work twice in list. + * @type {Map>} + */ + memoCache = new Map(); + /** @type {OnlineVersionsCache} */ + onlineCache; + + /** + * @param {Options} options + * @param {OnlineVersionsCache} onlineCache + * @param {string} elmVersion + */ + constructor(options, onlineCache, elmVersion) { + onlineCache.update(options, elmVersion); + this.onlineCache = onlineCache; + } + + /** + * @param {string} elmVersion + * @param {string} pkg + * @return {string[]} + */ + list(elmVersion, pkg) { + const memoVersions = this.memoCache.get(pkg); + if (memoVersions !== undefined) { + return memoVersions; + } + + const offlineVersions = readVersionsInElmHomeAndSort(elmVersion, pkg); + const allVersionsSet = new Set(this.onlineCache.getVersions(pkg)); + // Combine local and online versions. + for (const version of offlineVersions) { + allVersionsSet.add(version); + } + + const allVersions = [...allVersionsSet].sort(flippedSemverCompare); + this.memoCache.set(pkg, allVersions); + return allVersions; + } +} + +/** + * @param {string} elmVersion + * @param {string} pkg + * @return {string[]} + */ +function readVersionsInElmHomeAndSort(elmVersion, pkg) { + const pkgPath = ProjectJsonFiles.getPackagePathInElmHome(elmVersion, pkg); + let offlineVersions; + try { + offlineVersions = fs.readdirSync(pkgPath); + } catch (_) { + // The directory doesn't exist or we don't have permissions. + // It's fine to catch all cases and return an empty list. + offlineVersions = []; + } + + return offlineVersions.sort(flippedSemverCompare); +} + +/** Solve dependencies completely offline, without any http request. + * + * @param {Options} options + * @param {string} elmVersion + * @param {string} pkg + * @param {string} version + * @returns {string} + */ +function fetchElmJsonOnline(options, elmVersion, pkg, version) { + try { + return fetchElmJsonOffline(options, elmVersion, pkg, version); + } catch (_) { + // `fetchElmJsonOffline` can only fail in ways that are either expected + // (such as file does not exist or no permissions) + // or because there was an error parsing `pkg` and `version`. + // In such case, this will throw again with `cacheElmJsonPath()` so it's fine. + const remoteUrl = remoteElmJsonUrl(pkg, version); + if (!syncGetWorker) { + return ''; + } + + const elmJson = syncGetWorker.get(remoteUrl); + const cachePath = ProjectJsonFiles.elmReviewDependencyCache( + options, + elmVersion, + pkg, + version, + 'elm.json' + ); + const parentDir = path.dirname(cachePath); + fs.mkdirSync(parentDir, {recursive: true}); + fs.writeFileSync(cachePath, elmJson); + return elmJson; + } +} + +/** + * @param {Options} options + * @param {string} elmVersion + * @param {string} pkg + * @param {string} version + * @returns {string} + */ +function fetchElmJsonOffline(options, elmVersion, pkg, version) { + try { + return fs.readFileSync( + ProjectJsonFiles.getElmJsonFromElmHomePath(elmVersion, pkg, version), + 'utf8' + ); + } catch (_) { + // The read can only fail if the elm.json file does not exist + // or if we don't have the permissions to read it so it's fine to catch all. + // Otherwise, it means that `homeElmJsonPath()` failed while processing `pkg` and `version`. + // In such case, again, it's fine to catch all since the next call to `cacheElmJsonPath()` + // will fail the same anyway. + const cachePath = ProjectJsonFiles.elmReviewDependencyCache( + options, + elmVersion, + pkg, + version, + 'elm.json' + ); + return fs.readFileSync(cachePath, 'utf8'); + } +} + +/** Reset the cache of existing versions from scratch with a request to the package server. + * + * @param {string} cachePath + * @param {string} remotePackagesUrl + * @return {Map} + */ +function onlineVersionsFromScratch(cachePath, remotePackagesUrl) { + if (!syncGetWorker) { + return new Map(); + } + + const onlineVersionsJson = syncGetWorker.get(remotePackagesUrl); + FS.mkdirpSync(path.dirname(cachePath)); + fs.writeFileSync(cachePath, onlineVersionsJson); + const onlineVersions = JSON.parse(onlineVersionsJson); + try { + return parseOnlineVersions(onlineVersions); + } catch (error) { + throw new Error( + `Failed to parse the response from the request to ${remotePackagesUrl}.\n${error.message}` + ); + } +} + +// Helper functions ################################################## + +/** Compares two versions so that newer versions appear first when sorting with this function. + * + * @param {string} a + * @param {string} b + * @return {number} + */ +function flippedSemverCompare(a, b) { + const vA = a.split('.'); + const vB = b.split('.'); + return ( + compareNumber(vA[0], vB[0]) || + compareNumber(vA[1], vB[1]) || + compareNumber(vA[2], vB[2]) + ); +} + +/** COmpare 2 numbers as string + * + * @param {string} a + * @param {string} b + * @return {number} + */ +function compareNumber(a, b) { + return parseInt(b, 10) - parseInt(a, 10); +} + +/** + * @param {unknown} json + * @return {Map} + */ +function parseOnlineVersions(json) { + if (typeof json !== 'object' || json === null || Array.isArray(json)) { + throw new Error( + `Expected an object, but got: ${ + json === null ? 'null' : Array.isArray(json) ? 'Array' : typeof json + }` + ); + } + + const result = new Map(); + + for (const [key, value] of Object.entries(json)) { + result.set(key, parseVersions(key, value)); + } + + return result; +} + +/** + * @param {string} key + * @param {unknown} json + * @return {string[]} + */ +function parseVersions(key, json) { + if (!Array.isArray(json)) { + throw new Error( + `Expected ${JSON.stringify(key)} to be an array, but got: ${typeof json}` + ); + } + + for (const [index, item] of json.entries()) { + if (typeof item !== 'string') { + throw new Error( + `Expected${JSON.stringify( + key + )}->${index} to be a string, but got: ${typeof item}` + ); + } + } + + return json; +} + +/** Cache in which we'll store information related Elm dependencies, computed by elm-solve-deps-wasm. + * + * @param {string} packageJsonVersion + * @param {string} elmVersion + * @return {Path} + */ +function dependenciesCachePath(packageJsonVersion, elmVersion) { + return path.join( + ProjectJsonFiles.elmHomeCache(packageJsonVersion), + elmVersion, + 'versions-cache.json' + ); +} + +/** + * @param {string} pkg + * @param {string} version + * @return {string} + */ +function remoteElmJsonUrl(pkg, version) { + return `https://package.elm-lang.org/packages/${pkg}/${version}/elm.json`; +} + +/** + * @param {string} str + * @return {{pkg: string, version: string}} + */ +function splitPkgVersion(str) { + const [pkg, version] = str.split('@'); + return {pkg, version}; +} + +module.exports = DependencyProvider; diff --git a/lib/elm-binary.js b/lib/elm-binary.js index 752836dd5..dd1cdcb96 100644 --- a/lib/elm-binary.js +++ b/lib/elm-binary.js @@ -5,6 +5,16 @@ const which = require('which'); const spawn = require('cross-spawn'); const ErrorMessage = require('./error-message'); +/** + * @typedef { import("./types/options").Options } Options + * @typedef { import("./types/path").Path } Path + */ + +/** Get the path to the Elm binary + * + * @param {Options} options + * @return {Promise} + */ function getElmBinary(options) { const whichAsync = util.promisify(which); if (options.compiler === undefined) { @@ -35,6 +45,11 @@ A few options: }); } +/** Get the version of the Elm compiler + * + * @param {Path} elmBinary + * @return {Promise} + */ async function getElmVersion(elmBinary) { const result = spawn.sync(elmBinary, ['--version'], { silent: true, @@ -48,6 +63,33 @@ async function getElmVersion(elmBinary) { return trimVersion(result.stdout.toString()); } +/** Download the dependencies of the project to analyze. + * + * @param {Path} elmBinary + * @param {Path} elmJsonPath + * @return {Promise} + */ +async function downloadDependenciesOfElmJson(elmBinary, elmJsonPath) { + const result = spawn.sync(elmBinary, ['make', '--report=json'], { + cwd: path.dirname(elmJsonPath), + silent: false, + env: process.env + }); + + if (result.status !== 0) { + const error = JSON.parse(result.stderr.toString()); + // TODO Check for other kinds of errors + if (error.title !== 'NO INPUT') { + // TODO Print error nicely + throw new Error(error); + } + } +} + +/** + * @param {string} version + * @return {string} + */ function trimVersion(version) { const index = version.indexOf('-'); if (index === -1) { @@ -59,5 +101,6 @@ function trimVersion(version) { module.exports = { getElmBinary, - getElmVersion + getElmVersion, + downloadDependenciesOfElmJson }; diff --git a/lib/flags.js b/lib/flags.js index 0f8c996c4..94c088256 100644 --- a/lib/flags.js +++ b/lib/flags.js @@ -193,7 +193,7 @@ const flags = [ mayBeUsedSeveralTimes: false, usesEquals: false, color: chalk.cyan, - sections: ['regular', 'init', 'new-package'], + sections: ['regular', 'init', 'new-package', 'prepare-offline'], description: [ `Specify the path to the ${chalk.magentaBright('elm')} compiler.` ], @@ -380,6 +380,18 @@ const flags = [ boolean: true, sections: null }, + { + name: 'offline', + boolean: true, + color: chalk.cyan, + sections: ['regular'], + description: [ + 'Prevent making network calls. You might need to run', + `${chalk.yellow( + 'elm-review prepare-offline' + )} beforehand to avoid problems.` + ] + }, gitHubAuthFlag, { name: 'namespace', @@ -484,7 +496,7 @@ ${description /** * @param {Subcommand | null} subcommand - * @returns {"initDescription" | "newPackageDescription" | "description" } + * @returns {"initDescription" | "newPackageDescription" | "description"} */ function preferredDescriptionFieldFor(subcommand) { switch (subcommand) { @@ -496,6 +508,8 @@ function preferredDescriptionFieldFor(subcommand) { return 'description'; case 'suppress': return 'description'; + case 'prepare-offline': + return 'description'; case null: return 'description'; } diff --git a/lib/help.js b/lib/help.js index 38ff8fffb..9dba33ba5 100644 --- a/lib/help.js +++ b/lib/help.js @@ -43,6 +43,9 @@ function review(options) { ${chalk.yellow('elm-review new-rule [RULE-NAME]')} Adds a new rule to your review configuration or review package. + ${chalk.yellow('elm-review prepare-offline')} + Prepares running ${chalk.greenBright('elm-review')} in offline mode using ${chalk.cyan('--offline')}. + You can customize the review command with the following flags: ${Flags.buildFlags('regular', null)} @@ -159,10 +162,27 @@ ${Flags.buildFlags('new-rule', 'new-rule')} `); } +function prepareOffline() { + console.log(`The prepare-offline command allows the tool to run in offline mode using +the ${chalk.cyan('--offline')} flag. + +This will build the review configuration application and download the +dependencies of the project to review. It requires network access. + +If you change your the review configuration, you might need to re-run this +command to work again in offline mode. + +You can customize the new-rule command with the following flags: + +${Flags.buildFlags('prepare-offline', 'prepare-offline')} +`); +} + module.exports = { review, suppress, init, newRule, - newPackage + newPackage, + prepareOffline }; diff --git a/lib/init.js b/lib/init.js index c93ffa585..bf32ecd1f 100644 --- a/lib/init.js +++ b/lib/init.js @@ -144,26 +144,14 @@ async function createElmJson(options, directory) { const elmBinary = await getElmBinary(options); const elmVersion = await getElmVersion(elmBinary); - const elmJson = { - type: 'application', - 'source-directories': ['src'], - 'elm-version': elmVersion || '0.19.1', - dependencies: { - direct: {}, - indirect: {} - }, - 'test-dependencies': { - direct: {}, - indirect: {} - } - }; - - const pathToElmJson = path.join(directory, 'elm.json'); - fs.writeFileSync(pathToElmJson, JSON.stringify(elmJson, null, 4)); - await TemplateDependencies.add(options, pathToElmJson); - - const elmJsonWithDeps = FS.readJsonFileSync(pathToElmJson); - fs.writeFileSync(pathToElmJson, JSON.stringify(elmJsonWithDeps, null, 4)); + fs.writeFileSync( + path.join(directory, 'elm.json'), + JSON.stringify( + TemplateDependencies.createNewReviewElmJson(options, elmVersion), + null, + 4 + ) + ); } function createReviewConfig(directory, template) { diff --git a/lib/main.js b/lib/main.js index c5b96349f..e380afc00 100644 --- a/lib/main.js +++ b/lib/main.js @@ -6,6 +6,7 @@ if (!process.argv.includes('--no-color')) { } const path = require('path'); +const chalk = require('chalk'); const Help = require('./help'); const Init = require('./init'); const Builder = require('./build'); @@ -16,11 +17,19 @@ const NewRule = require('./new-rule'); const Anonymize = require('./anonymize'); const newPackage = require('./new-package'); const AppWrapper = require('./app-wrapper'); +const ElmBinary = require('./elm-binary'); const ResultCache = require('./result-cache'); const ErrorMessage = require('./error-message'); const SuppressedErrors = require('./suppressed-errors'); const Watch = require('./watch'); +/** + * @typedef { import("./types/options").Options } Options + */ + +/** + * @type {Options} + */ const options = AppState.getOptions(); process.on('uncaughtException', errorHandler); @@ -98,6 +107,30 @@ async function runElmReviewInWatchMode() { await Runner.runReview(options, initialization.app).catch(errorHandler); } +async function prepareOffline() { + const elmBinary = await ElmBinary.getElmBinary(options); + await ElmBinary.downloadDependenciesOfElmJson(elmBinary, options.elmJsonPath); + + const {elmModulePath, reviewElmJson} = await Builder.build(options); + + if (!elmModulePath) { + AppState.exitRequested(1); + return; + } + + await Builder.buildElmParser(options, reviewElmJson); + + console.log(`${chalk.greenBright( + 'elm-review' + )} is now ready to be run ${chalk.cyan('--offline')}. + +You will need to run ${chalk.yellow( + 'elm-review prepare-offline' + )} to keep the offline mode working +if either your review configuration or your project's dependencies change.`); + process.exit(1); +} + module.exports = () => { if (options.version) { console.log(Anonymize.version(options)); @@ -138,6 +171,14 @@ module.exports = () => { } } + if (options.subcommand === 'prepare-offline') { + if (options.help) { + return Help.prepareOffline(); + } + + return prepareOffline(); + } + if (options.help) { return Help.review(options); } diff --git a/lib/min-version.js b/lib/min-version.js index 144cce7fd..73485b494 100644 --- a/lib/min-version.js +++ b/lib/min-version.js @@ -16,6 +16,7 @@ const supportedRange = `${minimalVersion.major}.${minimalVersion.minor}.0 <= v < module.exports = { updateToAtLeastMinimalVersion, validate, + minimalVersion, supportedRange }; diff --git a/lib/options.js b/lib/options.js index fe8cf324b..da5e42a8b 100644 --- a/lib/options.js +++ b/lib/options.js @@ -1,5 +1,6 @@ const path = require('path'); const chalk = require('chalk'); +const wrap = require('wrap-ansi'); const findUp = require('find-up'); const minimist = require('minimist'); const levenshtein = require('fastest-levenshtein'); @@ -27,7 +28,13 @@ course of action is and how to get forward. /** * @type {Subcommand[]} */ -const availableSubcommands = ['init', 'new-package', 'new-rule', 'suppress']; +const availableSubcommands = [ + 'init', + 'new-package', + 'new-rule', + 'suppress', + 'prepare-offline' +]; let containsHelp = false; @@ -199,6 +206,7 @@ try re-running it with ${chalk.cyan('--elmjson ')}.`, packageJsonVersion: packageJson.version, localElmReviewSrc, forceBuild: args['force-build'], + offline: args.offline, report: args.report === 'json' || args.report === 'ndjson' ? 'json' : null, reportOnOneLine: args.report === 'ndjson', rulesFilter: listOfStrings(args.rules), @@ -270,8 +278,6 @@ try re-running it with ${chalk.cyan('--elmjson ')}.`, 'elm.json' ); }, - dependenciesCachePath: () => - path.join(elmStuffFolder(), 'dependencies-cache'), // PATHS - THINGS TO REVIEW elmJsonPath, @@ -347,7 +353,8 @@ function findElmJsonPath(args, subcommand) { subcommand && subcommand !== 'new-rule' && subcommand !== 'init' && - subcommand !== 'suppress' + subcommand !== 'suppress' && + subcommand !== 'prepare-offline' ) { return null; } @@ -517,6 +524,44 @@ ${Flags.buildFlag(subcommand, Flags.reportFlag)}` ) ); } + + if (args.template && args.offline) { + reportErrorAndExit( + new ErrorMessage.CustomError( + 'COMMAND REQUIRES NETWORK ACCESS', + wrap( + `I can't use ${chalk.cyan('--template')} in ${chalk.cyan( + 'offline' + )} mode, as I need network access to download the external template. + +If you have the configuration locally on your computer, you can run it by pointing to it with ${chalk.yellow( + '--config' + )}. + +Otherwise, I recommend you try to gain network access and initialize your configuration to be able to run it offline afterwards: + +`, + 80 + ) + chalk.yellow(' elm-review init --template ' + args.template) + ) + ); + } + + if (args.offline && subcommand === 'new-package') { + reportErrorAndExit( + new ErrorMessage.CustomError( + 'COMMAND REQUIRES NETWORK ACCESS', + wrap( + `I can't use ${chalk.yellow('new-package')} in ${chalk.cyan( + 'offline' + )} mode, as I need network access to perform a number of steps. + +I recommend you try to gain network access and try again.`, + 80 + ) + ) + ); + } } function parseGitHubAuth(subcommand, gitHubAuth) { diff --git a/lib/project-json-files.js b/lib/project-json-files.js index e083f87e6..1fe3b3a58 100644 --- a/lib/project-json-files.js +++ b/lib/project-json-files.js @@ -35,15 +35,20 @@ function getElmJson(options, elmVersion, name, packageVersion) { packageVersion, 'elm.json' ); - return FS.readJsonFile(cacheLocation).catch(() => + return FS.readJsonFile(cacheLocation).catch((error) => { // Finally, try to download it from the packages website - readFromPackagesWebsite( + if (options.offline) { + // Unless we're in offline mode + throw error; + } + + return readFromPackagesWebsite( cacheLocation, name, packageVersion, 'elm.json' - ) - ); + ); + }); }) ); } @@ -58,18 +63,28 @@ function getElmJsonFromElmHome(elmVersion, name, packageVersion) { return promise; } - const directory = path.join( - elmRoot, + const elmJsonPath = getElmJsonFromElmHomePath( elmVersion, - 'packages', name, packageVersion ); - promise = FS.readJsonFile(path.join(directory, 'elm.json')); + promise = FS.readJsonFile(elmJsonPath); elmJsonInElmHomePromises.set(key, promise); return promise; } +function getElmJsonFromElmHomePath(elmVersion, name, packageVersion) { + return path.join( + getPackagePathInElmHome(elmVersion, name), + packageVersion, + 'elm.json' + ); +} + +function getPackagePathInElmHome(elmVersion, name) { + return path.join(elmRoot, elmVersion, 'packages', name); +} + /** Get the docs.json file for a dependency. * * @param {Options} options @@ -96,9 +111,20 @@ function getDocsJson(options, elmVersion, name, packageVersion) { packageVersion, 'docs.json' ); - return FS.readJsonFile(cacheLocation).catch(() => - readFromPackagesWebsite(cacheLocation, name, packageVersion, 'docs.json') - ); + return FS.readJsonFile(cacheLocation).catch((error) => { + // Finally, try to download it from the packages website + if (options.offline) { + // Unless we're in offline mode + throw error; + } + + return readFromPackagesWebsite( + cacheLocation, + name, + packageVersion, + 'docs.json' + ); + }); }); } @@ -146,19 +172,25 @@ function elmReviewDependencyCache( file ) { return path.join( - elmRoot, - 'elm-review', - options.packageJsonVersion, - 'packages', + elmHomeCache(options.packageJsonVersion), elmVersion, + 'packages', name, packageVersion, file ); } +function elmHomeCache(packageJsonVersion) { + return path.join(elmRoot, 'elm-review', packageJsonVersion); +} + module.exports = { getElmJson, + getPackagePathInElmHome, + getElmJsonFromElmHomePath, getElmJsonFromElmHome, - getDocsJson + elmReviewDependencyCache, + getDocsJson, + elmHomeCache }; diff --git a/lib/remote-template.js b/lib/remote-template.js index ba75b5d8b..b34efe6e5 100644 --- a/lib/remote-template.js +++ b/lib/remote-template.js @@ -80,23 +80,25 @@ async function getRemoteElmJson( } const elmJson = await downloadTemplateElmJson(template, commit); + if ( elmJson.dependencies && elmJson.dependencies.direct && elmJson.dependencies.direct['jfmengels/elm-review'] ) { + const packageVersion = elmJson.dependencies.direct['jfmengels/elm-review']; + const [major] = packageVersion.split('.'); + + if (Number.parseInt(major, 10) !== MinVersion.minimalVersion.major) { + // Major version for which the configuration exists is not compatible + MinVersion.validate(options, reviewElmJsonPath, packageVersion); + } + elmJson.dependencies.direct['jfmengels/elm-review'] = - MinVersion.updateToAtLeastMinimalVersion( - elmJson.dependencies.direct['jfmengels/elm-review'] - ); + MinVersion.updateToAtLeastMinimalVersion(packageVersion); } - elmJson['test-dependencies'].direct = {}; - elmJson['test-dependencies'].indirect = {}; - - await FS.mkdirp(path.dirname(reviewElmJsonPath)); - await FS.writeJson(reviewElmJsonPath, elmJson, 4); - return TemplateDependencies.update(options, reviewElmJsonPath); + return TemplateDependencies.update(options, elmJson); } async function downloadTemplateElmJson(template, commit) { diff --git a/lib/state.js b/lib/state.js index 72ac32ee7..5f2a52b87 100644 --- a/lib/state.js +++ b/lib/state.js @@ -221,6 +221,7 @@ function updateFilesInCache(files) { // ACCESS +/** @returns {Options} */ function getOptions() { return options; } diff --git a/lib/sync-get-worker.js b/lib/sync-get-worker.js new file mode 100644 index 000000000..0cb137797 --- /dev/null +++ b/lib/sync-get-worker.js @@ -0,0 +1,40 @@ +const {parentPort, workerData} = require('worker_threads'); +const https = require('https'); + +const {sharedLock, requestPort} = workerData; +const sharedLockArray = new Int32Array(sharedLock); + +if (parentPort) { + parentPort.on('message', async (url) => { + try { + const response = await getBody(url); + requestPort.postMessage(response); + } catch (error) { + requestPort.postMessage({error}); + } + + Atomics.notify(sharedLockArray, 0, Infinity); + }); +} + +/** + * @param {string} url + * @return {Promise} + */ +async function getBody(url) { + return new Promise((resolve, reject) => { + https + .get(url, (res) => { + let body = ''; + res.on('data', (chunk) => { + body += chunk; + }); + res.on('end', () => { + resolve(body); + }); + }) + .on('error', (err) => { + reject(err); + }); + }); +} diff --git a/lib/sync-get.js b/lib/sync-get.js new file mode 100644 index 000000000..e38f14cc5 --- /dev/null +++ b/lib/sync-get.js @@ -0,0 +1,56 @@ +const path = require('path'); +const { + Worker, + MessageChannel, + receiveMessageOnPort +} = require('worker_threads'); + +module.exports = { + startWorker +}; + +/** Start a worker thread and return a `syncGetWorker` + * capable of making sync requests until shut down. + * + * @return {{get: (url : string) => string, shutDown: () => void}} + */ +function startWorker() { + const {port1: localPort, port2: workerPort} = new MessageChannel(); + const sharedLock = new SharedArrayBuffer(4); + const sharedLockArray = new Int32Array(sharedLock); + const workerPath = path.resolve(__dirname, 'sync-get-worker.js'); + const worker = new Worker(workerPath, { + workerData: {sharedLock, requestPort: workerPort}, + transferList: [workerPort] + }); + + /** + * @param {string} url + * @return {string} + */ + function get(url) { + worker.postMessage(url); + Atomics.wait(sharedLockArray, 0, 0); // Blocks until notified at index 0. + const response = receiveMessageOnPort(localPort); + if (!response || !response.message) { + return ''; + } + + if (response.message.error) { + throw response.message.error; + } else { + return response.message; + } + } + + /** Shut down the worker thread. + * + * @returns {void} + */ + function shutDown() { + localPort.close(); + worker.terminate(); + } + + return {get, shutDown}; +} diff --git a/lib/template-dependencies.js b/lib/template-dependencies.js index 1ff989d82..ad90bb910 100644 --- a/lib/template-dependencies.js +++ b/lib/template-dependencies.js @@ -1,308 +1,324 @@ -/* - * Credit goes to @zwilias, from his PR here https://github.com/rtfeldman/node-test-runner/pull/356/files - */ - -const spawnAsync = require('cross-spawn'); -const Hash = require('./hash'); -const Cache = require('./cache'); -const FS = require('./fs-wrapper'); +const path = require('path'); +const chalk = require('chalk'); +const MinVersion = require('./min-version'); const ErrorMessage = require('./error-message'); -const getExecutable = require('elm-tooling/getExecutable'); +const DependencyProvider = require('./dependency-provider'); +const FS = require('./fs-wrapper'); + +/** @type {DependencyProvider | null} */ +let dependencyProvider = null; + +const parseElmFolder = path.join(__dirname, '../parseElm'); /** * @typedef { import("./types/options").Options } Options + * @typedef { import("./types/elm-version").ElmVersion } ElmVersion + * @typedef { import("./types/build").ReviewElmJson } ReviewElmJson + * @typedef { import("./types/build").ApplicationDependencies } ApplicationDependencies * @typedef { import("./types/template-dependencies").TemplateDependenciesError } TemplateDependenciesError */ module.exports = { - get, - add, + addRequiredDependencies, + createNewReviewElmJson, update, addElmSyntax }; -// GET +/** Add required dependencies for the application elm.json file. + * + * @param {Options} options + * @param {ReviewElmJson} elmJson + * @return {ApplicationDependencies} + */ +function addRequiredDependencies(options, elmJson) { + const extra = { + 'elm/json': '1.0.0 <= v < 2.0.0', + 'stil4m/elm-syntax': '7.0.0 <= v < 8.0.0', + 'elm/project-metadata-utils': '1.0.0 <= v < 2.0.0' + }; + + try { + const dependencies = solve( + options, + elmJson['elm-version'], + JSON.stringify(elmJson), + extra, + false + ); + if (options.localElmReviewSrc) { + delete dependencies.direct['jfmengels/elm-review']; + delete dependencies.indirect['jfmengels/elm-review']; + } -async function get(options, elmJsonDependencies, pathToElmJson) { - const dependencyHash = Hash.hash(JSON.stringify(elmJsonDependencies)); - const cacheKey = `${dependencyHash}${ - options.localElmReviewSrc ? '-local' : '' - }`; - return Cache.getOrCompute(options.dependenciesCachePath(), cacheKey, () => - computeDependencies(options, pathToElmJson) - ); + return dependencies; + } catch (error) { + throw new ErrorMessage.CustomError( + 'CONFIGURATION COMPILATION ERROR', + `I encountered a problem when solving dependencies for creating the parser application: + +${error.toString().replace(/^Error: /, '')}`, + null + ); + } } -function computeDependencies(options, pathToElmJson) { - return spawnElmJsonAsync( - options, - [ - 'solve', - '--extra', - 'elm/json@1', - 'stil4m/elm-syntax@7', - 'elm/project-metadata-utils@1', - '--', - pathToElmJson - ], - (error) => { - throw new ErrorMessage.CustomError( - 'CONFIGURATION COMPILATION ERROR', - `I encountered a problem when solving dependencies: - -${formatElmJsonError(error, options)}`, - null +// ADD ELM-SYNTAX + +/** Compute the dependencies if we were to replace the version of `stil4m/elm-syntax` with the given one. + * + * @param {Options} options + * @param {ElmVersion} elmVersion + * @param {ElmVersion} elmSyntaxVersion + * @return {Promise} + */ +async function addElmSyntax(options, elmVersion, elmSyntaxVersion) { + const elmJsonPath = path.resolve(parseElmFolder, 'elm.json'); + const elmJson = await FS.readJsonFile(elmJsonPath).catch((error) => { + if (error.code === 'ENOENT') { + return Promise.reject( + new ErrorMessage.CustomError( + // prettier-ignore + 'UNEXPECTED INTERNAL ERROR', + // prettier-ignore + `I was expecting to find the "parseElm" project at ${chalk.cyan(elmJsonPath)} but could not find it. + +Please open an issue at the following link: +https://github.com/jfmengels/node-elm-review/issues/new +`, + options.elmJsonPath + ) ); } - ) - .then(JSON.parse) - .then((dependencies) => { - if (options.localElmReviewSrc) { - delete dependencies.direct['jfmengels/elm-review']; - delete dependencies.indirect['jfmengels/elm-review']; - } - - return dependencies; - }); + + return Promise.reject(error); + }); + + elmJson['elm-version'] = elmVersion; + delete elmJson.dependencies.direct['stil4m/elm-syntax']; + elmJson.dependencies = solve( + options, + elmVersion, + JSON.stringify(elmJson), + { + 'stil4m/elm-syntax': `${elmSyntaxVersion} <= v < ${nextPatchVersion( + elmSyntaxVersion + )}` + }, + false + ); + return elmJson; } -function formatElmJsonError(error, options) { - if (error.stderr === undefined) { - return error.message; - } +/** + * @param {Options} options + * @param {ElmVersion} elmVersion + * @param {string} elmJson + * @param {Record}extra + * @param {boolean} onlineFirst + * @return {ApplicationDependencies} + */ +function solve(options, elmVersion, elmJson, extra, onlineFirst) { + dependencyProvider = dependencyProvider || new DependencyProvider(); + + try { + return JSON.parse( + onlineFirst && !options.offline + ? dependencyProvider.solveOnline(options, elmVersion, elmJson, extra) + : dependencyProvider.solveOffline(options, elmVersion, elmJson, extra) + ); + } catch (error) { + if (options.offline) { + throw error; + } - const stderrMessage = error.stderr.toString().trim(); - const exec = /^([^]+\n)?--\s([A-Z ]+)\s-*\n\n([^]+)$/.exec(stderrMessage); - if (exec === null) { - return `${error.message}\n\n${ - stderrMessage === '' ? '(empty stderr)' : stderrMessage - }`; + return JSON.parse( + onlineFirst + ? dependencyProvider.solveOffline(options, elmVersion, elmJson, extra) + : dependencyProvider.solveOnline(options, elmVersion, elmJson, extra) + ); } - - const [, before, title, message] = exec; - return ErrorMessage.formatHuman( - options.debug, - new ErrorMessage.CustomError( - title, - before === undefined ? message : `${message}\n\n${before}` - ) - ); } -// ADD ELM-SYNTAX +/** Returns the next major version. + * Ex: 2.13.0 -> 3.0.0 + * + * @param {ElmVersion} version + * @return {ElmVersion} + */ +function nextMajorVersion(version) { + const [major] = version.split('.'); + return `${parseInt(major, 10) + 1}.0.0`; +} -async function addElmSyntax(options, pathToElmJson, elmSyntaxVersion) { - return spawnElmJsonAsync( - options, - [ - 'solve', - '--extra', - `stil4m/elm-syntax@${elmSyntaxVersion}`, - '--', - pathToElmJson - ], - (error) => { - throw new ErrorMessage.CustomError( - 'CONFIGURATION COMPILATION ERROR', - `I encountered a problem when solving dependencies for creating the parser application: - -${formatElmJsonError(error, options)}`, - null - ); - } - ).then(JSON.parse); +/** Returns the next patch version. + * Ex: 2.13.0 -> 2.13.1 + * + * @param {ElmVersion} version + * @return {ElmVersion} + */ +function nextPatchVersion(version) { + const [major, minor, patch] = version.split('.'); + return `${major}.${minor}.${parseInt(patch, 10) + 1}`; } -// ADD +/** Create a new elm.json with basic `elm-review` dependencies. + * + * @param {Options} options + * @param {ElmVersion} elmVersion + * @return {ReviewElmJson} + */ +function createNewReviewElmJson(options, elmVersion) { + /** @type {ReviewElmJson} */ + const elmJson = { + type: 'application', + 'source-directories': ['src'], + 'elm-version': elmVersion, + dependencies: { + direct: {}, + indirect: {} + }, + 'test-dependencies': { + direct: {}, + indirect: {} + } + }; + const stringifiedElmJson = JSON.stringify(elmJson); -async function add(options, pathToElmJson) { - await spawnElmJsonAsync( + elmJson.dependencies = solve( options, - [ - 'install', - '--yes', - 'elm/core@1', - 'jfmengels/elm-review@2', - 'stil4m/elm-syntax@7', - '--', - pathToElmJson - ], - (error) => { - throw new ErrorMessage.CustomError( - 'CONFIGURATION COMPILATION ERROR', - `I encountered a problem when adding base dependencies: - -${formatElmJsonError(error, options)}`, - null - ); - } + elmVersion, + stringifiedElmJson, + { + 'elm/core': '1.0.0 <= v < 2.0.0', + 'stil4m/elm-syntax': '7.0.0 <= v < 8.0.0', + 'jfmengels/elm-review': MinVersion.supportedRange + }, + true ); - return spawnElmJsonAsync( + const testDependencies = solve( options, - [ - 'install', - '--test', - '--yes', - 'elm-explorations/test@2', - '--', - pathToElmJson - ], - (error) => { - throw new ErrorMessage.CustomError( - 'CONFIGURATION COMPILATION ERROR', - `I encountered a problem when adding test dependencies: - -${formatElmJsonError(error, options)}`, - null - ); - } + elmVersion, + stringifiedElmJson, + { + 'elm/core': '1.0.0 <= v < 2.0.0', + 'elm-explorations/test': '2.0.0 <= v < 3.0.0' + }, + false ); -} - -// UPDATE -async function update(options, pathToElmJson) { - await spawnElmJsonAsync( - options, - ['upgrade', '--yes', pathToElmJson], - (error) => { - throw new ErrorMessage.CustomError( - 'CONFIGURATION COMPILATION ERROR', - `I encountered a problem when attempting to update the dependencies: - -${formatElmJsonError(error, options)}`, - null - ); - } + elmJson['test-dependencies'] = filterOutDuplicateDependencies( + testDependencies, + elmJson.dependencies ); - const elmJson = await FS.readJsonFile(pathToElmJson); - if (options.subcommand === 'init') { - await FS.writeJson(pathToElmJson, elmJson, 4); - } + teardownDependenciesProvider(); return elmJson; } -// SPAWNING +/** Filter out test-dependencies that are already in the regular dependencies. + * + * @param {{direct: Record, indirect: Record}} testDependencies + * @param {{direct: Record, indirect: Record}} regularDependencies + * @return {{direct: Record, indirect: Record}} + */ +function filterOutDuplicateDependencies(testDependencies, regularDependencies) { + return { + direct: filterOutDuplicateDependenciesForSection( + testDependencies.direct, + regularDependencies.direct + ), + indirect: filterOutDuplicateDependenciesForSection( + testDependencies.indirect, + {...regularDependencies.direct, ...regularDependencies.indirect} + ) + }; +} -let elmJsonPromise; +/** Filter out test-dependencies that are already in the regular dependencies (only on a section of the dependencies). + * + * @param {Record} testDependencies + * @param {Record} regularDependencies + * @return {Record} + */ +function filterOutDuplicateDependenciesForSection( + testDependencies, + regularDependencies +) { + return Object.fromEntries( + Object.entries(testDependencies).filter( + ([pkg, _]) => !regularDependencies[pkg] + ) + ); +} -/** - * Run elm-json +/** Update versions of dependencies to their latest (compatible) version. + * * @param {Options} options - * @param {string[]} args - * @param {(Error) => Error} onError - * @returns {Promise} + * @param {ReviewElmJson} elmJson + * @return {Promise} */ -function spawnElmJsonAsync(options, args, onError) { - if (elmJsonPromise === undefined) { - elmJsonPromise = getExecutable({ - name: 'elm-json', - version: '^0.2.10', - onProgress: (percentage) => { - const message = `Downloading elm-json... ${Math.round( - percentage * 100 - )}%`; - - if (options.report !== 'json' || options.debug) { - process.stderr.write( - percentage >= 1 - ? `${'Working...'.padEnd(message.length, ' ')}\r` - : `${message}\r` - ); - } - } - }).catch((error) => { - throw new ErrorMessage.CustomError( - // prettier-ignore - 'PROBLEM INSTALLING elm-json', - // prettier-ignore - `I need a tool called elm-json for some of my inner workings, -but there was some trouble installing it. This is what we know: - -${error.message}` - ); - }); - } +async function update(options, elmJson) { + const extra = { + 'stil4m/elm-syntax': '7.0.0 <= v < 8.0.0', + 'jfmengels/elm-review': MinVersion.supportedRange + }; + + Object.entries(elmJson.dependencies.direct).forEach(([pkg, version]) => { + extra[pkg] = `${version} <= v < ${nextMajorVersion(version)}`; + }); + + delete elmJson.dependencies.direct['jfmengels/elm-review']; + delete elmJson.dependencies.direct['stil4m/elm-syntax']; + + const stringifiedElmJson = JSON.stringify({ + ...elmJson, + dependencies: {direct: {}, indirect: {}}, + 'test-dependencies': {direct: {}, indirect: {}} + }); + + elmJson.dependencies = solve( + options, + elmJson['elm-version'], + stringifiedElmJson, + extra, + true + ); - return elmJsonPromise - .then( - (elmJsonCLI) => - new Promise((resolve, reject) => { - const child = spawnAsync(elmJsonCLI, args, { - silent: true, - env: process.env - }); - let stdout = ''; - let stderr = ''; - - child.on('error', reject); - - child.stdout.on('data', (chunk) => { - stdout += chunk.toString(); - }); - - child.stderr.on('data', (chunk) => { - stderr += chunk.toString(); - }); - - child.on('close', (code, signal) => { - if (code === 0) { - resolve(stdout); - } else { - /** @type {TemplateDependenciesError} */ - const error = new Error( - `elm-json exited with ${exitReason( - code, - signal - )}\n\n${stdout}\n\n${stderr}` - ); - error.stderr = stderr; - reject(error); - } - }); - }) - ) - .catch((error) => { - if ( - error && - error.message && - error.message.startsWith('phase: retrieve') - ) { - return Promise.reject( - new ErrorMessage.CustomError( - // prettier-ignore - 'MISSING INTERNET ACCESS', - // prettier-ignore - `I’m sorry, but it looks like you don’t have Internet access at the moment. -I require it for some of my inner workings. - -Please connect to the Internet and try again. After that, as long as you don’t -change your configuration or remove \`elm-stuff/\`, you should be able to go -offline again.` - ) - ); - } - - return Promise.reject(onError(error)); + const testDependenciesEntries = Object.entries( + elmJson['test-dependencies'].direct + ); + if (testDependenciesEntries.length !== 0) { + /** @type {Record} */ + const packagesToAdd = {}; + testDependenciesEntries.forEach(([pkg, version]) => { + packagesToAdd[pkg] = `${version} <= v < ${nextMajorVersion(version)}`; }); -} -/** - * @param {number | null} code - * @param {string | null} signal - * @returns {string} - */ -function exitReason(code, signal) { - if (code !== null) { - return `exit code ${code}`; + const testDependencies = solve( + options, + elmJson['elm-version'], + stringifiedElmJson, + packagesToAdd, + true + ); + + elmJson['test-dependencies'] = filterOutDuplicateDependencies( + testDependencies, + elmJson.dependencies + ); } - if (signal !== null) { - return `signal ${signal}`; - } + teardownDependenciesProvider(); - return 'unknown reason'; + return elmJson; +} + +function teardownDependenciesProvider() { + if (dependencyProvider) { + dependencyProvider.tearDown(); + dependencyProvider = null; + } } diff --git a/lib/types/build.d.ts b/lib/types/build.d.ts index fc4b2d595..016683420 100644 --- a/lib/types/build.d.ts +++ b/lib/types/build.d.ts @@ -1,4 +1,5 @@ import type {Path} from './types/path'; +import type {ElmVersion} from './types/elm-version'; export type BuildResult = { elmModulePath: Path | null; @@ -10,6 +11,14 @@ export type BuildResult = { export type AppHash = string; export type ReviewElmJson = { + type: 'application'; + 'elm-version': ElmVersion; 'source-directories': Array; - dependencies: Record; + dependencies: ApplicationDependencies; + 'test-dependencies': ApplicationDependencies; +}; + +export type ApplicationDependencies = { + direct: Record; + indirect: Record; }; diff --git a/lib/types/flag.d.ts b/lib/types/flag.d.ts index 0f2370982..91587fced 100644 --- a/lib/types/flag.d.ts +++ b/lib/types/flag.d.ts @@ -7,7 +7,8 @@ export type Section = | 'init' | 'new-rule' | 'new-package' - | 'suppress-subcommand'; + | 'suppress-subcommand' + | 'prepare-offline'; export type Flag = BaseFlag & SingleOrMulti & Display; diff --git a/lib/types/options.d.ts b/lib/types/options.d.ts index 301350612..30b1ae867 100644 --- a/lib/types/options.d.ts +++ b/lib/types/options.d.ts @@ -24,6 +24,7 @@ export type Options = { packageJsonVersion: string; localElmReviewSrc: string | undefined; forceBuild: boolean; + offline: boolean; report: ReportMode; reportOnOneLine: boolean; rulesFilter: string[]; @@ -47,7 +48,6 @@ export type Options = { generatedCodePackageJson: () => Path; templateElmModulePath: (string) => Path; pathToTemplateElmJson: (string) => Path; - dependenciesCachePath: () => Path; elmJsonPath: Path; elmJsonPathWasSpecified: boolean; readmePath: Path; @@ -78,4 +78,9 @@ export type Template = { reference: string | null; }; -export type Subcommand = 'init' | 'new-package' | 'new-rule' | 'suppress'; +export type Subcommand = + | 'init' + | 'new-package' + | 'new-rule' + | 'suppress' + | 'prepare-offline'; diff --git a/package-lock.json b/package-lock.json index 8797d6e22..8320b0bed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "elm-review", - "version": "2.10.3", + "version": "2.11.0-beta.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "elm-review", - "version": "2.10.3", + "version": "2.11.0-beta.3", "license": "BSD-3-Clause", "dependencies": { "chalk": "^4.0.0", "chokidar": "^3.5.2", "cross-spawn": "^7.0.3", + "elm-solve-deps-wasm": "^1.0.2", "elm-tooling": "^1.14.1", "fastest-levenshtein": "^1.0.16", "find-up": "^4.1.0", @@ -2621,9 +2622,9 @@ "dev": true }, "node_modules/elm-solve-deps-wasm": { - "version": "1.0.1", - "dev": true, - "license": "MPL-2.0" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/elm-solve-deps-wasm/-/elm-solve-deps-wasm-1.0.2.tgz", + "integrity": "sha512-qnwo7RO9IO7jd9SLHvIy0rSOEIlc/tNMTE9Cras0kl+b161PVidW4FvXo0MtXU8GAKi/2s/HYvhcnpR/NNQ1zw==" }, "node_modules/elm-test": { "version": "0.19.1-revision10", @@ -8357,8 +8358,9 @@ "dev": true }, "elm-solve-deps-wasm": { - "version": "1.0.1", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/elm-solve-deps-wasm/-/elm-solve-deps-wasm-1.0.2.tgz", + "integrity": "sha512-qnwo7RO9IO7jd9SLHvIy0rSOEIlc/tNMTE9Cras0kl+b161PVidW4FvXo0MtXU8GAKi/2s/HYvhcnpR/NNQ1zw==" }, "elm-test": { "version": "0.19.1-revision10", diff --git a/package.json b/package.json index 492069a4e..ec3003e35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elm-review", - "version": "2.10.3", + "version": "2.11.0-beta.3", "description": "Run elm-review from Node.js", "engines": { "node": ">=10.0.0" @@ -79,6 +79,7 @@ "chalk": "^4.0.0", "chokidar": "^3.5.2", "cross-spawn": "^7.0.3", + "elm-solve-deps-wasm": "^1.0.2", "elm-tooling": "^1.14.1", "fastest-levenshtein": "^1.0.16", "find-up": "^4.1.0", diff --git a/test/flags.test.js b/test/flags.test.js index 8b6017b92..aa3bc8d4f 100644 --- a/test/flags.test.js +++ b/test/flags.test.js @@ -115,3 +115,22 @@ test('Using the same flag twice', async () => { const output = await TestCli.runAndExpectError('--config a/ --config b/'); expect(output).toMatchFile(testName('duplicate-flags')); }); + +test('Using both --template and --offline (regular run)', async () => { + const output = await TestCli.runAndExpectError( + '--template jfmengels/elm-review-unused/example --offline' + ); + expect(output).toMatchFile(testName('template-and-offline-run')); +}); + +test('Using both --template and --offline (init)', async () => { + const output = await TestCli.runAndExpectError( + 'init --template jfmengels/elm-review-unused/example --offline' + ); + expect(output).toMatchFile(testName('template-and-offline-init')); +}); + +test('Using both new-package and --offline', async () => { + const output = await TestCli.runAndExpectError('new-package --offline'); + expect(output).toMatchFile(testName('offline-new-package')); +}); diff --git a/test/help.test.js b/test/help.test.js index 201c750f4..7426e7075 100644 --- a/test/help.test.js +++ b/test/help.test.js @@ -28,3 +28,8 @@ test('new-rule --help', async () => { const output = await TestCli.run('new-rule --help'); expect(output).toMatchFile(testName('new-rule')); }); + +test('prepare-offline --help', async () => { + const output = await TestCli.run('prepare-offline --help'); + expect(output).toMatchFile(testName('prepare-offline')); +}); diff --git a/test/run-snapshots/elm-review-something-for-new-rule/package.json b/test/run-snapshots/elm-review-something-for-new-rule/package.json index 29af827a7..c6cbe9da0 100644 --- a/test/run-snapshots/elm-review-something-for-new-rule/package.json +++ b/test/run-snapshots/elm-review-something-for-new-rule/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "elm-doc-preview": "^5.0.5", - "elm-review": "^2.10.3", + "elm-review": "^2.11.0-beta.3", "elm-test": "^0.19.1-revision10", "elm-tooling": "^1.13.1", "fs-extra": "^9.0.0", diff --git a/test/run-snapshots/elm-review-something-for-new-rule/preview/elm.json b/test/run-snapshots/elm-review-something-for-new-rule/preview/elm.json index 9f9783aad..923f55ea8 100644 --- a/test/run-snapshots/elm-review-something-for-new-rule/preview/elm.json +++ b/test/run-snapshots/elm-review-something-for-new-rule/preview/elm.json @@ -20,7 +20,7 @@ "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", - "elm-explorations/test": "2.1.1", + "elm-explorations/test": "2.1.2", "miniBill/elm-unicode": "1.0.3", "rtfeldman/elm-hex": "1.0.0", "stil4m/structured-writer": "1.0.3" @@ -28,7 +28,7 @@ }, "test-dependencies": { "direct": { - "elm-explorations/test": "2.1.1" + "elm-explorations/test": "2.1.2" }, "indirect": {} } diff --git a/test/run-snapshots/elm-review-something-for-new-rule/review/elm.json b/test/run-snapshots/elm-review-something-for-new-rule/review/elm.json index e184a325e..51cdf0de3 100644 --- a/test/run-snapshots/elm-review-something-for-new-rule/review/elm.json +++ b/test/run-snapshots/elm-review-something-for-new-rule/review/elm.json @@ -15,7 +15,7 @@ "jfmengels/elm-review-common": "1.3.3", "jfmengels/elm-review-debug": "1.0.8", "jfmengels/elm-review-documentation": "2.0.4", - "jfmengels/elm-review-simplify": "2.1.2", + "jfmengels/elm-review-simplify": "2.1.3", "jfmengels/elm-review-unused": "1.2.0", "sparksp/elm-review-forbidden-words": "1.0.1", "stil4m/elm-syntax": "7.3.2" @@ -28,7 +28,7 @@ "elm/regex": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", - "elm-explorations/test": "2.1.1", + "elm-explorations/test": "2.1.2", "miniBill/elm-unicode": "1.0.3", "pzp1997/assoc-list": "1.0.0", "rtfeldman/elm-hex": "1.0.0", @@ -36,7 +36,9 @@ } }, "test-dependencies": { - "direct": {}, + "direct": { + "elm-explorations/test": "2.1.2" + }, "indirect": {} } } \ No newline at end of file diff --git a/test/run-snapshots/elm-review-something/package.json b/test/run-snapshots/elm-review-something/package.json index 29af827a7..c6cbe9da0 100644 --- a/test/run-snapshots/elm-review-something/package.json +++ b/test/run-snapshots/elm-review-something/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "elm-doc-preview": "^5.0.5", - "elm-review": "^2.10.3", + "elm-review": "^2.11.0-beta.3", "elm-test": "^0.19.1-revision10", "elm-tooling": "^1.13.1", "fs-extra": "^9.0.0", diff --git a/test/run-snapshots/elm-review-something/preview/elm.json b/test/run-snapshots/elm-review-something/preview/elm.json index 9f9783aad..923f55ea8 100644 --- a/test/run-snapshots/elm-review-something/preview/elm.json +++ b/test/run-snapshots/elm-review-something/preview/elm.json @@ -20,7 +20,7 @@ "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", - "elm-explorations/test": "2.1.1", + "elm-explorations/test": "2.1.2", "miniBill/elm-unicode": "1.0.3", "rtfeldman/elm-hex": "1.0.0", "stil4m/structured-writer": "1.0.3" @@ -28,7 +28,7 @@ }, "test-dependencies": { "direct": { - "elm-explorations/test": "2.1.1" + "elm-explorations/test": "2.1.2" }, "indirect": {} } diff --git a/test/run-snapshots/elm-review-something/review/elm.json b/test/run-snapshots/elm-review-something/review/elm.json index e184a325e..51cdf0de3 100644 --- a/test/run-snapshots/elm-review-something/review/elm.json +++ b/test/run-snapshots/elm-review-something/review/elm.json @@ -15,7 +15,7 @@ "jfmengels/elm-review-common": "1.3.3", "jfmengels/elm-review-debug": "1.0.8", "jfmengels/elm-review-documentation": "2.0.4", - "jfmengels/elm-review-simplify": "2.1.2", + "jfmengels/elm-review-simplify": "2.1.3", "jfmengels/elm-review-unused": "1.2.0", "sparksp/elm-review-forbidden-words": "1.0.1", "stil4m/elm-syntax": "7.3.2" @@ -28,7 +28,7 @@ "elm/regex": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", - "elm-explorations/test": "2.1.1", + "elm-explorations/test": "2.1.2", "miniBill/elm-unicode": "1.0.3", "pzp1997/assoc-list": "1.0.0", "rtfeldman/elm-hex": "1.0.0", @@ -36,7 +36,9 @@ } }, "test-dependencies": { - "direct": {}, + "direct": { + "elm-explorations/test": "2.1.2" + }, "indirect": {} } } \ No newline at end of file diff --git a/test/run-snapshots/init-project/review/elm.json b/test/run-snapshots/init-project/review/elm.json index 1acdd7ae0..c71176db0 100644 --- a/test/run-snapshots/init-project/review/elm.json +++ b/test/run-snapshots/init-project/review/elm.json @@ -19,7 +19,7 @@ "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", - "elm-explorations/test": "2.1.1", + "elm-explorations/test": "2.1.2", "miniBill/elm-unicode": "1.0.3", "rtfeldman/elm-hex": "1.0.0", "stil4m/structured-writer": "1.0.3" @@ -27,7 +27,7 @@ }, "test-dependencies": { "direct": { - "elm-explorations/test": "2.1.1" + "elm-explorations/test": "2.1.2" }, "indirect": {} } diff --git a/test/run-snapshots/init-template-project/review/elm.json b/test/run-snapshots/init-template-project/review/elm.json index 574029ff5..c4e261de7 100644 --- a/test/run-snapshots/init-template-project/review/elm.json +++ b/test/run-snapshots/init-template-project/review/elm.json @@ -20,7 +20,7 @@ "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", - "elm-explorations/test": "2.1.1", + "elm-explorations/test": "2.1.2", "miniBill/elm-unicode": "1.0.3", "rtfeldman/elm-hex": "1.0.0", "stil4m/structured-writer": "1.0.3" diff --git a/test/run-snapshots/remote-without-elm-review-json.txt b/test/run-snapshots/remote-without-elm-review-json.txt index 3a3a472d5..1973276ea 100644 --- a/test/run-snapshots/remote-without-elm-review-json.txt +++ b/test/run-snapshots/remote-without-elm-review-json.txt @@ -1,8 +1,63 @@ { - "type": "error", - "title": "MISSING ELM-REVIEW DEPENDENCY", - "path": "/test/project-with-errors/review/elm.json", - "message": [ - "The template's configuration does not include jfmengels/elm-review in its direct dependencies.\n\nMaybe you chose the wrong template, or the template is malformed. If the latter is the case, please inform the template author." - ] + "type": "review-errors", + "errors": [ + { + "path": "src/Main.elm", + "errors": [ + { + "rule": "NoUnused.Variables", + "message": "Imported variable `span` is not used", + "ruleLink": "https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.0/NoUnused-Variables", + "details": [ + "You should either use this value somewhere, or remove it at the location I pointed at." + ], + "region": { + "start": { + "line": 11, + "column": 11 + }, + "end": { + "line": 11, + "column": 15 + } + }, + "fix": [ + { + "range": { + "start": { + "line": 9, + "column": 14 + }, + "end": { + "line": 11, + "column": 15 + } + }, + "string": "" + } + ], + "formatted": [ + { + "string": "(fix) ", + "color": "#33BBC8" + }, + { + "string": "NoUnused.Variables", + "color": "#FF0000", + "href": "https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.0/NoUnused-Variables" + }, + ": Imported variable `span` is not used\n\n10| -- span is unused\n11| , span\n ", + { + "string": "^^^^", + "color": "#FF0000" + }, + "\n12| , text\n\nYou should either use this value somewhere, or remove it at the location I pointed at." + ], + "suppressed": false, + "originallySuppressed": false + } + ] + } + ], + "extracts": {} } diff --git a/test/run-snapshots/remote-without-elm-review-ndjson.txt b/test/run-snapshots/remote-without-elm-review-ndjson.txt index 3a3a472d5..f42c6599d 100644 --- a/test/run-snapshots/remote-without-elm-review-ndjson.txt +++ b/test/run-snapshots/remote-without-elm-review-ndjson.txt @@ -1,8 +1 @@ -{ - "type": "error", - "title": "MISSING ELM-REVIEW DEPENDENCY", - "path": "/test/project-with-errors/review/elm.json", - "message": [ - "The template's configuration does not include jfmengels/elm-review in its direct dependencies.\n\nMaybe you chose the wrong template, or the template is malformed. If the latter is the case, please inform the template author." - ] -} +{"path":"src/Main.elm","rule":"NoUnused.Variables","message":"Imported variable `span` is not used","ruleLink":"https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.0/NoUnused-Variables","details":["You should either use this value somewhere, or remove it at the location I pointed at."],"region":{"start":{"line":11,"column":11},"end":{"line":11,"column":15}},"fix":[{"range":{"start":{"line":9,"column":14},"end":{"line":11,"column":15}},"string":""}],"formatted":[{"string":"(fix) ","color":"#33BBC8"},{"string":"NoUnused.Variables","color":"#FF0000","href":"https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.0/NoUnused-Variables"},": Imported variable `span` is not used\n\n10| -- span is unused\n11| , span\n ",{"string":"^^^^","color":"#FF0000"},"\n12| , text\n\nYou should either use this value somewhere, or remove it at the location I pointed at."],"suppressed":false,"originallySuppressed":false} diff --git a/test/run-snapshots/remote-without-elm-review.txt b/test/run-snapshots/remote-without-elm-review.txt index fe8259b8e..8f0377f31 100644 --- a/test/run-snapshots/remote-without-elm-review.txt +++ b/test/run-snapshots/remote-without-elm-review.txt @@ -1,8 +1,17 @@ - Fetching template information -✖ Fetching template information --- MISSING ELM-REVIEW DEPENDENCY ----------------------------------------------- +✔ Build finished! Now reviewing your project... +-- ELM-REVIEW ERROR ----------------------------------------- src/Main.elm:11:11 -The template's configuration does not include jfmengels/elm-review in its direct dependencies. +(fix) NoUnused.Variables: Imported variable `span` is not used -Maybe you chose the wrong template, or the template is malformed. If the latter is the case, please inform the template author. +10| -- span is unused +11| , span + ^^^^ +12| , text +You should either use this value somewhere, or remove it at the location I +pointed at. + +Errors marked with (fix) can be fixed automatically using `elm-review --fix`. + +I found 1 error in 1 file. diff --git a/test/run.sh b/test/run.sh index 978cd82b5..eb08ee1e9 100755 --- a/test/run.sh +++ b/test/run.sh @@ -5,6 +5,7 @@ set -e CWD=$(pwd) CMD="elm-review --no-color" TMP="$CWD/temporary" +ELM_HOME="$TMP/elm-home" SNAPSHOTS="$CWD/run-snapshots" SUBCOMMAND="$1" REPLACE_SCRIPT="node $CWD/replace-local-path.js" @@ -35,7 +36,6 @@ function runCommandAndCompareToSnapshot { eval "$LOCAL_COMMAND$AUTH --FOR-TESTS $ARGS" 2>&1 \ | $REPLACE_SCRIPT \ - | grep -v "Downloading elm-json" \ > "$TMP/$FILE" if [ "$(diff "$TMP/$FILE" "$SNAPSHOTS/$FILE")" != "" ] then @@ -58,9 +58,8 @@ function runAndRecord { local ARGS=$3 local FILE=$4 echo -e "\x1B[33m- $TITLE\x1B[0m: \x1B[34m elm-review --FOR-TESTS $ARGS\x1B[0m" - eval "$LOCAL_COMMAND$AUTH --FOR-TESTS $ARGS" 2>&1 \ + eval "ELM_HOME=$ELM_HOME $LOCAL_COMMAND$AUTH --FOR-TESTS $ARGS" 2>&1 \ | $REPLACE_SCRIPT \ - | grep -v "Downloading elm-json" \ > "$SNAPSHOTS/$FILE" } @@ -412,3 +411,5 @@ createTestSuiteWithDifferentReportFormats "$CMD" \ "Using both --config and --template" \ "--config ../config-that-triggers-no-errors --template jfmengels/test-node-elm-review" \ "remote-configuration-with-config-flag" + +rm -rf "$TMP" diff --git a/test/snapshots/flags/offline-new-package.txt b/test/snapshots/flags/offline-new-package.txt new file mode 100644 index 000000000..3d1dfed36 --- /dev/null +++ b/test/snapshots/flags/offline-new-package.txt @@ -0,0 +1,7 @@ +-- COMMAND REQUIRES NETWORK ACCESS --------------------------------------------- + +I can't use new-package in offline mode, as I need network access to perform a +number of steps. + +I recommend you try to gain network access and try again. + diff --git a/test/snapshots/flags/template-and-offline-init.txt b/test/snapshots/flags/template-and-offline-init.txt new file mode 100644 index 000000000..4553285a0 --- /dev/null +++ b/test/snapshots/flags/template-and-offline-init.txt @@ -0,0 +1,13 @@ +-- COMMAND REQUIRES NETWORK ACCESS --------------------------------------------- + +I can't use --template in offline mode, as I need network access to download the +external template. + +If you have the configuration locally on your computer, you can run it by +pointing to it with --config. + +Otherwise, I recommend you try to gain network access and initialize your +configuration to be able to run it offline afterwards: + + elm-review init --template jfmengels/elm-review-unused/example + diff --git a/test/snapshots/flags/template-and-offline-run.txt b/test/snapshots/flags/template-and-offline-run.txt new file mode 100644 index 000000000..4553285a0 --- /dev/null +++ b/test/snapshots/flags/template-and-offline-run.txt @@ -0,0 +1,13 @@ +-- COMMAND REQUIRES NETWORK ACCESS --------------------------------------------- + +I can't use --template in offline mode, as I need network access to download the +external template. + +If you have the configuration locally on your computer, you can run it by +pointing to it with --config. + +Otherwise, I recommend you try to gain network access and initialize your +configuration to be able to run it offline afterwards: + + elm-review init --template jfmengels/elm-review-unused/example + diff --git a/test/snapshots/help/default.txt b/test/snapshots/help/default.txt index 880bfb383..02327a45c 100644 --- a/test/snapshots/help/default.txt +++ b/test/snapshots/help/default.txt @@ -26,6 +26,9 @@ You are using elm-review . elm-review new-rule [RULE-NAME] Adds a new rule to your review configuration or review package. + elm-review prepare-offline + Prepares running elm-review in offline mode using --offline. + You can customize the review command with the following flags: --unsuppress @@ -91,6 +94,10 @@ You can customize the review command with the following flags: --no-details Hide the details from error reports for a more compact view. + --offline + Prevent making network calls. You might need to run + elm-review prepare-offline beforehand to avoid problems. + --ignore-dirs Ignore the reports of all rules for the specified directories. diff --git a/test/snapshots/help/prepare-offline.txt b/test/snapshots/help/prepare-offline.txt new file mode 100644 index 000000000..f80ede464 --- /dev/null +++ b/test/snapshots/help/prepare-offline.txt @@ -0,0 +1,14 @@ +The prepare-offline command allows the tool to run in offline mode using +the --offline flag. + +This will build the review configuration application and download the +dependencies of the project to review. It requires network access. + +If you change your the review configuration, you might need to re-run this +command to work again in offline mode. + +You can customize the new-rule command with the following flags: + + --compiler + Specify the path to the elm compiler. + diff --git a/tsconfig.json b/tsconfig.json index e84783d5f..dfa804c03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "node_modules", "**/elm-stuff/", ".eslintrc.js", - "new-package/elm-review-package-tests/check-previews-compile.js" + "new-package/elm-review-package-tests/check-previews-compile.js", + "test/temporary" ] } diff --git a/tsconfig.no-implicit-any.json b/tsconfig.no-implicit-any.json index a797fce0b..4f5584136 100644 --- a/tsconfig.no-implicit-any.json +++ b/tsconfig.no-implicit-any.json @@ -6,6 +6,8 @@ "include": [ "lib/anonymize.js", "lib/benchmark.js", + "lib/sync-get.js", + "lib/sync-get-worker.js", "lib/error-message.js", "lib/flags.js", "lib/hash.js",