diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index c1d6060f..00000000 --- a/.flowconfig +++ /dev/null @@ -1,7 +0,0 @@ -[options] -include_warnings=true -exact_by_default=true - -[lints] -all=error -untyped-import=off diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index bd05e9c2..f565a80c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -47,8 +47,8 @@ jobs: - name: elm-tooling install run: npx --no-install elm-tooling install - - name: Flow - run: npx --no-install flow check + - name: TypeScript + run: npx --no-install tsc - name: ESLint run: npx --no-install eslint --report-unused-disable-directives . diff --git a/eslint.config.mjs b/eslint.config.mjs index 9c9eecda..89d59854 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,12 +9,12 @@ export default [ '**/elm-stuff', '**/fixtures', '**/templates', - '**/flow-typed', ], }, { rules: { ...js.configs.recommended.rules, + 'no-fallthrough': 'off', // Caught by TypeScript instead. 'no-inner-declarations': 'off', 'no-prototype-builtins': 'off', 'no-unused-vars': ['error', { caughtErrorsIgnorePattern: '^_' }], diff --git a/lib/Compile.js b/lib/Compile.js index 307335df..4d0717ce 100644 --- a/lib/Compile.js +++ b/lib/Compile.js @@ -1,17 +1,17 @@ -//@flow - const { supportsColor } = require('./chalk'); const spawn = require('cross-spawn'); const ElmCompiler = require('./ElmCompiler'); const Report = require('./Report'); -function compile( - cwd /*: string */, - testFile /*: string */, - dest /*: string */, - pathToElmBinary /*: string */, - report /*: typeof Report.Report */ -) /*: Promise */ { +/** + * @param { string } cwd + * @param { string } testFile + * @param { string } dest + * @param { string } pathToElmBinary + * @param { import('./Report').Report } report + * @returns { Promise } + */ +function compile(cwd, testFile, dest, pathToElmBinary, report) { return new Promise((resolve, reject) => { const compileProcess = ElmCompiler.compile([testFile], { output: dest, @@ -30,21 +30,26 @@ function compile( }); } +/** + * @param { Array } testFilePaths + * @param { string } projectRootDir + * @param { string } pathToElmBinary + * @param { import('./Report').Report } report + * @returns { Promise } + */ function compileSources( - testFilePaths /*: Array */, - projectRootDir /*: string */, - pathToElmBinary /*: string */, - report /*: typeof Report.Report */ -) /*: Promise */ { + testFilePaths, + projectRootDir, + pathToElmBinary, + report +) { return new Promise((resolve, reject) => { - const compilerReport = report === 'json' ? report : undefined; - const compileProcess = ElmCompiler.compile(testFilePaths, { output: '/dev/null', cwd: projectRootDir, spawn: spawnCompiler({ ignoreStdout: false, cwd: projectRootDir }), pathToElm: pathToElmBinary, - report: compilerReport, + reportJson: report === 'json', processOpts: processOptsForReporter(report), }); @@ -58,12 +63,16 @@ function compileSources( }); } +/** + * @param { { ignoreStdout: boolean, cwd: string } } options + * @returns { ( + pathToElm: string, + processArgs: Array, + processOpts: import('child_process').SpawnOptions + ) => import('child_process').ChildProcess } + */ function spawnCompiler({ ignoreStdout, cwd }) { - return ( - pathToElm /*: string */, - processArgs /*: Array */, - processOpts /*: child_process$spawnOpts */ - ) => { + return (pathToElm, processArgs, processOpts) => { // It might seem useless to specify 'pipe' and then just write all data to // `process.stdout`/`process.stderr`, but it does make a difference: The Elm // compiler turns off colors when it’s used in a pipe. In summary: @@ -73,6 +82,7 @@ function spawnCompiler({ ignoreStdout, cwd }) { const stdout = ignoreStdout ? 'ignore' : supportsColor ? 'inherit' : 'pipe'; const stderr = supportsColor ? 'inherit' : 'pipe'; + /** @type { import('child_process').SpawnOptions } */ const finalOpts = { env: process.env, ...processOpts, @@ -82,11 +92,11 @@ function spawnCompiler({ ignoreStdout, cwd }) { const child = spawn(pathToElm, processArgs, finalOpts); - if (stdout === 'pipe') { + if (stdout === 'pipe' && child.stdout !== null) { child.stdout.on('data', (data) => process.stdout.write(data)); } - if (stderr === 'pipe') { + if (stderr === 'pipe' && child.stderr !== null) { child.stderr.on('data', (data) => process.stderr.write(data)); } @@ -94,9 +104,11 @@ function spawnCompiler({ ignoreStdout, cwd }) { }; } -function processOptsForReporter( - report /*: typeof Report.Report */ -) /*: child_process$spawnOpts */ { +/** + * @param { import('./Report').Report } report + * @returns { import('child_process').SpawnOptions } + */ +function processOptsForReporter(report) { if (Report.isMachineReadable(report)) { return { env: process.env, stdio: ['ignore', 'ignore', process.stderr] }; } else { diff --git a/lib/DependencyProvider.js b/lib/DependencyProvider.js index afd1fbcd..a0954e63 100644 --- a/lib/DependencyProvider.js +++ b/lib/DependencyProvider.js @@ -1,5 +1,3 @@ -// @flow - const fs = require('fs'); const path = require('path'); const wasm = require('elm-solve-deps-wasm'); @@ -11,8 +9,12 @@ const collator = new Intl.Collator('en', { numeric: true }); // for sorting SemV wasm.init(); // Lazily start the worker until needed. // This is important for the tests, which never exit otherwise. -let syncGetWorker_ /*: void | typeof SyncGet.SyncGetWorker */ = undefined; -function syncGetWorker() /*: typeof SyncGet.SyncGetWorker */ { +/** @type { undefined | import('./SyncGet').SyncGetWorker } */ +let syncGetWorker_ = undefined; +/** + * @returns { import('./SyncGet').SyncGetWorker } + */ +function syncGetWorker() { if (syncGetWorker_ === undefined) { syncGetWorker_ = SyncGet.startWorker(); } @@ -21,8 +23,12 @@ function syncGetWorker() /*: typeof SyncGet.SyncGetWorker */ { // Cache of existing versions according to the package website. class OnlineVersionsCache { - map /*: Map> */ = new Map(); + /** @type { Map> } */ + map = new Map(); + /** + * @returns { void } + */ update() { const pubgrubHome = path.join(ElmHome.elmHome(), 'pubgrub'); fs.mkdirSync(pubgrubHome, { recursive: true }); @@ -49,11 +55,14 @@ class OnlineVersionsCache { this.updateWithRequestSince(cachePath, remotePackagesUrl); } - // Update the cache with a request to the package server. - updateWithRequestSince( - cachePath /*: string */, - remotePackagesUrl /*: string */ - ) /*: void */ { + /** + * 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()) { @@ -95,26 +104,39 @@ class OnlineVersionsCache { } } - getVersions(pkg /*: string */) /*: Array */ { + /** + * @param { string } pkg + * @returns { Array } + */ + 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. - memoCache /*: Map> */ = new Map(); - onlineCache /*: OnlineVersionsCache */; - - constructor(onlineCache /*: OnlineVersionsCache */) { + /** + * Memoization cache to avoid doing the same work twice in list. + * @type { Map> } + */ + memoCache = new Map(); + /** @type { OnlineVersionsCache } */ + onlineCache; + + /** + * @param {OnlineVersionsCache} onlineCache + */ + constructor(onlineCache) { onlineCache.update(); this.onlineCache = onlineCache; } - list( - pkg /*: string */, - pinnedVersion /*: void | string */ - ) /*: Array */ { + /** + * @param { string } pkg + * @param { undefined | string } pinnedVersion + * @returns { Array } + */ + list(pkg, pinnedVersion) { const memoVersions = this.memoCache.get(pkg); if (memoVersions !== undefined) { return prioritizePinnedIndirectVersion(memoVersions, pinnedVersion); @@ -132,13 +154,18 @@ class OnlineAvailableVersionLister { } class OfflineAvailableVersionLister { - // Memoization cache to avoid doing the same work twice in list. - cache /*: Map> */ = new Map(); - - list( - pkg /*: string */, - pinnedVersion /*: void | string */ - ) /*: Array */ { + /** + * Memoization cache to avoid doing the same work twice in list. + * @type { Map> } + */ + cache = new Map(); + + /** + * @param { string } pkg + * @param { undefined | string } pinnedVersion + * @returns { Array } + */ + list(pkg, pinnedVersion) { const memoVersions = this.cache.get(pkg); if (memoVersions !== undefined) { return prioritizePinnedIndirectVersion(memoVersions, pinnedVersion); @@ -151,8 +178,13 @@ class OfflineAvailableVersionLister { } } -function readVersionsInElmHomeAndSort(pkg /*: string */) /*: Array */ { +/** + * @param { string } pkg + * @returns { Array } + */ +function readVersionsInElmHomeAndSort(pkg) { const pkgPath = ElmHome.packagePath(pkg); + /** @type {Array} */ let offlineVersions; try { offlineVersions = fs.readdirSync(pkgPath); @@ -165,15 +197,26 @@ function readVersionsInElmHomeAndSort(pkg /*: string */) /*: Array */ { return offlineVersions.sort(flippedSemverCompare); } +/** + * When doing `import('./DependencyProvider').DependencyProvider`, + * TypeScript says `DependencyProvider` is not exported for some reason. + * But `import('./DependencyProvider').DependencyProviderType` works. + * @typedef {DependencyProvider} DependencyProviderType + */ + class DependencyProvider { - cache /*: OnlineVersionsCache */ = new OnlineVersionsCache(); - - // Solve dependencies completely offline, without any http request. - solveOffline( - elmJson /*: string */, - useTest /*: boolean */, - extra /*: { [string]: string } */ - ) /*: string */ { + /** @type { OnlineVersionsCache } */ + cache = new OnlineVersionsCache(); + + /** + * Solve dependencies completely offline, without any http request. + * + * @param { string } elmJson + * @param { boolean } useTest + * @param { Record } extra + * @returns { string } + */ + solveOffline(elmJson, useTest, extra) { const lister = new OfflineAvailableVersionLister(); const dependencies = JSON.parse(elmJson).dependencies; const indirectDeps = @@ -185,6 +228,7 @@ class DependencyProvider { useTest, extra, fetchElmJsonOffline, + /** @type { (pkg: string) => Array } */ (pkg) => lister.list( pkg, @@ -196,12 +240,15 @@ class DependencyProvider { } } - // Solve dependencies with http requests when required. - solveOnline( - elmJson /*: string */, - useTest /*: boolean */, - extra /*: { [string]: string } */ - ) /*: string */ { + /** + * Solve dependencies with http requests when required. + * + * @param { string } elmJson + * @param { boolean } useTest + * @param { Record } extra + * @returns { string } + */ + solveOnline(elmJson, useTest, extra) { const lister = new OnlineAvailableVersionLister(this.cache); const dependencies = JSON.parse(elmJson).dependencies; const indirectDeps = @@ -213,6 +260,7 @@ class DependencyProvider { useTest, extra, fetchElmJsonOnline, + /** @type { (pkg: string) => Array } */ (pkg) => lister.list( pkg, @@ -225,10 +273,12 @@ class DependencyProvider { } } -function fetchElmJsonOnline( - pkg /*: string */, - version /*: string */ -) /*: string */ { +/** + * @param { string } pkg + * @param { string } version + * @returns { string } + */ +function fetchElmJsonOnline(pkg, version) { try { return fetchElmJsonOffline(pkg, version); } catch (_) { @@ -246,10 +296,12 @@ function fetchElmJsonOnline( } } -function fetchElmJsonOffline( - pkg /*: string */, - version /*: string */ -) /*: string */ { +/** + * @param { string } pkg + * @param { string } version + * @returns { string } + */ +function fetchElmJsonOffline(pkg, version) { try { return fs.readFileSync(homeElmJsonPath(pkg, version), 'utf8'); } catch (_) { @@ -262,12 +314,15 @@ function fetchElmJsonOffline( } } -// Reset the cache of existing versions from scratch -// with a request to the package server. -function onlineVersionsFromScratch( - cachePath /*: string */, - remotePackagesUrl /*: string */ -) /*: Map> */ { +/** + * Reset the cache of existing versions from scratch + * with a request to the package server. + * + * @param { string } cachePath + * @param { string } remotePackagesUrl + * @returns { Map> } + */ +function onlineVersionsFromScratch(cachePath, remotePackagesUrl) { const onlineVersionsJson = syncGetWorker().get(remotePackagesUrl); fs.writeFileSync(cachePath, onlineVersionsJson); const onlineVersions = JSON.parse(onlineVersionsJson); @@ -298,11 +353,12 @@ function onlineVersionsFromScratch( * the real live application. * * Assumes versions is sorted descending (newest -> oldest). + * + * @param { Array } versions + * @param { void | string } pinnedVersion + * @returns { Array } */ -function prioritizePinnedIndirectVersion( - versions /*: Array */, - pinnedVersion /*: void | string */ -) /*: Array */ { +function prioritizePinnedIndirectVersion(versions, pinnedVersion) { if (pinnedVersion === undefined || !versions.includes(pinnedVersion)) { return versions; } @@ -318,14 +374,22 @@ function prioritizePinnedIndirectVersion( return desirableVersions.concat(olderVersions); } -/* Compares two versions so that newer versions appear first when sorting with this function. */ -function flippedSemverCompare(a /*: string */, b /*: string */) /*: number */ { +/** + * Compares two versions so that newer versions appear first when sorting with this function. + * + * @param { string } a + * @param { string } b + * @returns { number } + */ +function flippedSemverCompare(a, b) { return collator.compare(b, a); } -function parseOnlineVersions( - json /*: mixed */ -) /*: Map> */ { +/** + * @param { unknown } json + * @returns { Map> } + */ +function parseOnlineVersions(json) { if (typeof json !== 'object' || json === null || Array.isArray(json)) { throw new Error( `Expected an object, but got: ${ @@ -343,10 +407,12 @@ function parseOnlineVersions( return result; } -function parseVersions( - key /*: string */, - json /*: mixed */ -) /*: Array */ { +/** + * @param { string } key + * @param { unknown } json + * @returns { Array } + */ +function parseVersions(key, json) { if (!Array.isArray(json)) { throw new Error( `Expected ${JSON.stringify(key)} to be an array, but got: ${typeof json}` @@ -363,21 +429,24 @@ function parseVersions( } } - // $FlowFixMe[incompatible-return]: We dynamically checked that `json` is an `Array`. return json; } -function remoteElmJsonUrl( - pkg /*: string */, - version /*: string */ -) /*: string */ { +/** + * @param { string } pkg + * @param { string } version + * @returns { string } + */ +function remoteElmJsonUrl(pkg, version) { return `https://package.elm-lang.org/packages/${pkg}/${version}/elm.json`; } -function cacheElmJsonPath( - pkg /*: string */, - version /*: string */ -) /*: string */ { +/** + * @param { string } pkg + * @param { string } version + * @returns { string } + */ +function cacheElmJsonPath(pkg, version) { const parts = ElmHome.splitAuthorPkg(pkg); return path.join( ElmHome.elmHome(), @@ -390,19 +459,25 @@ function cacheElmJsonPath( ); } -function homeElmJsonPath( - pkg /*: string */, - version /*: string */ -) /*: string */ { +/** + * @param { string } pkg + * @param { string } version + * @returns { string } + */ +function homeElmJsonPath(pkg, version) { return path.join(ElmHome.packagePath(pkg), version, 'elm.json'); } -function splitPkgVersion(str /*: string */) /*: { - pkg: string, - version: string, -} */ { - const parts = str.split('@'); - return { pkg: parts[0], version: parts[1] }; +/** + * @param { string } str + * @returns { { + pkg: string, + version: string, + } } + */ +function splitPkgVersion(str) { + const [pkg = 'unknown', version = '99.99.99'] = str.split('@'); + return { pkg, version }; } module.exports = { diff --git a/lib/ElmCompiler.js b/lib/ElmCompiler.js index ad2e450e..05edb647 100644 --- a/lib/ElmCompiler.js +++ b/lib/ElmCompiler.js @@ -1,20 +1,35 @@ -// @flow - 'use strict'; var spawn = require('cross-spawn'); +/** + * @typedef { { + spawn: ( + cmd: string, + args: Array, + options: import('child_process').SpawnOptions + ) => import('child_process').ChildProcess, + cwd?: string, + pathToElm: string, + output?: string, + reportJson?: boolean, + processOpts?: import('child_process').SpawnOptions, + } } Options + */ + +/** @type { Options } */ var defaultOptions = { spawn: spawn, - cwd: undefined, pathToElm: 'elm', - output: undefined, - report: undefined, - processOpts: undefined, }; -// Converts an object of key/value pairs to an array of arguments suitable -// to be passed to child_process.spawn for elm-make. +/** + * Converts an object of key/value pairs to an array of arguments suitable + * to be passed to child_process.spawn for elm-make. + * + * @param { Options } options + * @returns { Array } + */ function compilerArgsFromOptions(options) { var args = []; @@ -22,13 +37,18 @@ function compilerArgsFromOptions(options) { args.push('--output', options.output); } - if (options.report != null) { - args.push('--report', options.report); + if (options.reportJson === true) { + args.push('--report', 'json'); } return args; } +/** + * @param { Array } sources + * @param { Options } options + * @returns { import('child_process').ChildProcess } + */ function runCompiler(sources, options) { var pathToElm = options.pathToElm; var processArgs = ['make'].concat(sources, compilerArgsFromOptions(options)); @@ -46,8 +66,18 @@ function runCompiler(sources, options) { return options.spawn(pathToElm, processArgs, processOpts); } +/** + * @param { unknown } err + * @param { string } pathToElm + * @returns { string } + */ function compilerErrorToString(err, pathToElm) { - if (typeof err === 'object' && typeof err.code === 'string') { + if ( + typeof err === 'object' && + err !== null && + 'code' in err && + typeof err.code === 'string' + ) { switch (err.code) { case 'ENOENT': return ( @@ -66,7 +96,12 @@ function compilerErrorToString(err, pathToElm) { 'Error attempting to run Elm compiler "' + pathToElm + '":\n' + err ); } - } else if (typeof err === 'object' && typeof err.message === 'string') { + } else if ( + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' + ) { return JSON.stringify(err.message); } else { return ( @@ -76,21 +111,13 @@ function compilerErrorToString(err, pathToElm) { } } -function compile( - sources /*: Array */, - options /*: {| - spawn?: ( - string, - Array, - child_process$spawnOpts - ) => child_process$ChildProcess, - cwd?: string, - pathToElm?: string, - output?: string, - report?: 'json', - processOpts?: child_process$spawnOpts, - |} */ -) /*: child_process$ChildProcess */ { +/** + * + * @param { Array } sources + * @param { Options } options + * @returns { import('child_process').ChildProcess } + */ +function compile(sources, options) { var optionsWithDefaults = Object.assign({}, defaultOptions, options); try { return runCompiler(sources, optionsWithDefaults).on( diff --git a/lib/ElmHome.js b/lib/ElmHome.js index 12a0b36e..dd76a0ff 100644 --- a/lib/ElmHome.js +++ b/lib/ElmHome.js @@ -1,25 +1,35 @@ -// @flow - const path = require('path'); const os = require('os'); -function elmHome() /*: string */ { +/** + * @returns { string } + */ +function elmHome() { const elmHomeEnv = process.env['ELM_HOME']; return elmHomeEnv === undefined ? defaultElmHome() : elmHomeEnv; } -function defaultElmHome() /*: string */ { +/** + * @returns { string } + */ +function defaultElmHome() { return process.platform === 'win32' ? defaultWindowsElmHome() : defaultUnixElmHome(); } -function defaultUnixElmHome() /*: string */ { +/** + * @returns { string } + */ +function defaultUnixElmHome() { return path.join(os.homedir(), '.elm'); } -function defaultWindowsElmHome() /*: string */ { - const appData = process.env.APPDATA; +/** + * @returns { string } + */ +function defaultWindowsElmHome() { + const appData = process.env['APPDATA']; const dir = appData === undefined ? path.join(os.homedir(), 'AppData', 'Roaming') @@ -27,17 +37,25 @@ function defaultWindowsElmHome() /*: string */ { return path.join(dir, 'elm'); } -function packagePath(pkg /*: string */) /*: string */ { +/** + * @param { string } pkg + * @returns { string } + */ +function packagePath(pkg) { const parts = splitAuthorPkg(pkg); return path.join(elmHome(), '0.19.2', 'packages', parts.author, parts.pkg); } -function splitAuthorPkg(pkgIdentifier /*: string */) /*: { - author: string, - pkg: string, -} */ { - const parts = pkgIdentifier.split('/'); - return { author: parts[0], pkg: parts[1] }; +/** + * @param { string } pkgIdentifier + * @returns { { + author: string, + pkg: string, + } } + */ +function splitAuthorPkg(pkgIdentifier) { + const [author = 'unknown', pkg = 'unknown'] = pkgIdentifier.split('/'); + return { author, pkg }; } module.exports = { diff --git a/lib/ElmJson.js b/lib/ElmJson.js index afc3544e..be479a86 100644 --- a/lib/ElmJson.js +++ b/lib/ElmJson.js @@ -1,41 +1,45 @@ -// @flow - const fs = require('fs'); const path = require('path'); -// Poor man’s type alias. We can’t use /*:: type Dependencies = ... */ because of: -// https://github.com/prettier/prettier/issues/2597 -const Dependencies /*: { [string]: string } */ = {}; - -const DirectAndIndirectDependencies /*: { - direct: typeof Dependencies, - indirect: typeof Dependencies, -} */ = { direct: {}, indirect: {} }; - -const ElmJson /*: +/** + * @typedef { Record } Dependencies + * + * @typedef { { + direct: Dependencies, + indirect: Dependencies, + } } DirectAndIndirectDependencies + * + * @typedef { | { type: 'application', 'source-directories': Array, - dependencies: typeof DirectAndIndirectDependencies, - 'test-dependencies': typeof DirectAndIndirectDependencies, - [string]: mixed, + dependencies: DirectAndIndirectDependencies, + 'test-dependencies': DirectAndIndirectDependencies, + [key: string]: unknown, } | { type: 'package', - dependencies: typeof Dependencies, - 'test-dependencies': typeof Dependencies, - [string]: mixed, - } */ = { - type: 'package', - dependencies: Dependencies, - 'test-dependencies': Dependencies, -}; + dependencies: Dependencies, + 'test-dependencies': Dependencies, + [key: string]: unknown, + } + } ElmJson + */ -function getPath(dir /*: string */) /*: string */ { +/** + * @param { string } dir + * @returns { string } + */ +function getPath(dir) { return path.join(dir, 'elm.json'); } -function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { +/** + * @param { string } dir + * @param { ElmJson } elmJson + * @returns { void } + */ +function write(dir, elmJson) { const elmJsonPath = getPath(dir); try { @@ -47,7 +51,11 @@ function write(dir /*: string */, elmJson /*: typeof ElmJson */) /*: void */ { } } -function read(dir /*: string */) /*: typeof ElmJson */ { +/** + * @param { string } dir + * @returns { ElmJson } + */ +function read(dir) { const elmJsonPath = getPath(dir); try { @@ -59,13 +67,17 @@ function read(dir /*: string */) /*: typeof ElmJson */ { } } -function readHelper(elmJsonPath /*: string */) /*: typeof ElmJson */ { +/** + * @param { string } elmJsonPath + * @returns { ElmJson } + */ +function readHelper(elmJsonPath) { const json = parseObject( JSON.parse(fs.readFileSync(elmJsonPath, 'utf8')), 'the file' ); - switch (json.type) { + switch (json['type']) { case 'application': return { ...json, @@ -74,7 +86,7 @@ function readHelper(elmJsonPath /*: string */) /*: typeof ElmJson */ { json['source-directories'] ), dependencies: parseDirectAndIndirectDependencies( - json.dependencies, + json['dependencies'], 'dependencies' ), 'test-dependencies': parseDirectAndIndirectDependencies( @@ -87,7 +99,7 @@ function readHelper(elmJsonPath /*: string */) /*: typeof ElmJson */ { return { ...json, type: 'package', - dependencies: parseDependencies(json.dependencies, 'dependencies'), + dependencies: parseDependencies(json['dependencies'], 'dependencies'), 'test-dependencies': parseDependencies( json['test-dependencies'], 'test-dependencies' @@ -97,13 +109,17 @@ function readHelper(elmJsonPath /*: string */) /*: typeof ElmJson */ { default: throw new Error( `Expected "type" to be "application" or "package", but got: ${stringify( - json.type + json['type'] )}` ); } } -function parseSourceDirectories(json /*: mixed */) /*: Array */ { +/** + * @param { unknown } json + * @returns { Array } + */ +function parseSourceDirectories(json) { if (!Array.isArray(json)) { throw new Error( `Expected "source-directories" to be an array, but got: ${stringify( @@ -134,22 +150,27 @@ function parseSourceDirectories(json /*: mixed */) /*: Array */ { return result; } -function parseDirectAndIndirectDependencies( - json /*: mixed */, - what /*: string */ -) /*: typeof DirectAndIndirectDependencies */ { +/** + * @param { unknown } json + * @param { string } what + * @returns { DirectAndIndirectDependencies } + */ +function parseDirectAndIndirectDependencies(json, what) { const jsonObject = parseObject(json, what); return { - direct: parseDependencies(jsonObject.direct, `${what}->"direct"`), - indirect: parseDependencies(jsonObject.indirect, `${what}->"indirect"`), + direct: parseDependencies(jsonObject['direct'], `${what}->"direct"`), + indirect: parseDependencies(jsonObject['indirect'], `${what}->"indirect"`), }; } -function parseDependencies( - json /*: mixed */, - what /*: string */ -) /*: typeof Dependencies */ { +/** + * @param { unknown } json + * @param { string } what + * @returns { Dependencies } + */ +function parseDependencies(json, what) { const jsonObject = parseObject(json, what); + /** @type { Dependencies } */ const result = {}; for (const [key, value] of Object.entries(jsonObject)) { @@ -166,29 +187,37 @@ function parseDependencies( return result; } -function parseObject( - json /*: mixed */, - what /*: string */ -) /*: { +[string]: mixed } */ { +/** + * @param { unknown } json + * @param { string } what + * @returns { Record } + */ +function parseObject(json, what) { if (json == null || typeof json !== 'object' || Array.isArray(json)) { throw new Error( `Expected ${what} to be an object, but got: ${stringify(json)}` ); } - return json; + return /** @type { Record } */ (json); } -function stringify(json /*: mixed */) /*: string */ { +/** + * @param { unknown } json + * @returns { string } + */ +function stringify(json) { const maybeString = JSON.stringify(json); return maybeString === undefined ? 'undefined' : maybeString; } const ELM_TEST_PACKAGE = 'elm-explorations/test'; -function requireElmTestPackage( - dir /*: string */, - elmJson /*: typeof ElmJson */ -) /*: void */ { +/** + * @param { string } dir + * @param { ElmJson } elmJson + * @returns { void } + */ +function requireElmTestPackage(dir, elmJson) { const elmJsonPath = getPath(dir); const versionOrRange = getElmExplorationsTestPackageVersionOrRange(elmJson); @@ -205,9 +234,11 @@ function requireElmTestPackage( } } -function getElmExplorationsTestPackageVersionOrRange( - elmJson /*: typeof ElmJson */ -) /*: string | void */ { +/** + * @param { ElmJson } elmJson + * @returns { string | undefined } + */ +function getElmExplorationsTestPackageVersionOrRange(elmJson) { switch (elmJson.type) { case 'application': return ( @@ -224,9 +255,6 @@ function getElmExplorationsTestPackageVersionOrRange( module.exports = { ELM_TEST_PACKAGE, - Dependencies, - DirectAndIndirectDependencies, - ElmJson, getPath, parseDirectAndIndirectDependencies, read, diff --git a/lib/FindTests.js b/lib/FindTests.js index d8b5076d..2e883aeb 100644 --- a/lib/FindTests.js +++ b/lib/FindTests.js @@ -1,13 +1,8 @@ -// @flow - const gracefulFs = require('graceful-fs'); const fs = require('fs'); const { globSync } = require('tinyglobby'); const path = require('path'); const Parser = require('./Parser'); -const Project = require('./Project'); - -void Project; // Double stars at the start and end is the correct way to ignore directories in // the `glob` package. @@ -15,10 +10,12 @@ void Project; // https://github.com/isaacs/node-glob/blob/f5a57d3d6e19b324522a3fa5bdd5075fd1aa79d1/common.js#L222-L231 const ignoredDirsGlobs = ['**/elm-stuff/**', '**/node_modules/**']; -function resolveGlobs( - fileGlobs /*: Array */, - projectRootDir /*: string */ -) /*: Array */ { +/** + * @param { Array } fileGlobs + * @param { string } projectRootDir + * @returns { Array } + */ +function resolveGlobs(fileGlobs, projectRootDir) { return Array.from( new Set( fileGlobs.flatMap((fileGlob) => { @@ -48,10 +45,12 @@ function resolveGlobs( ); } -function resolveCliArgGlob( - fileGlob /*: string */, - projectRootDir /*: string */ -) /*: Array */ { +/** + * @param { string } fileGlob + * @param { string } projectRootDir + * @returns { Array } + */ +function resolveCliArgGlob(fileGlob, projectRootDir) { // Globs passed as CLI arguments are relative to CWD, while elm-test // operates from the project root dir. const globRelativeToProjectRoot = path.relative( @@ -84,8 +83,13 @@ function resolveCliArgGlob( ); } -// Recursively search for *.elm files. -function findAllElmFilesInDir(dir /*: string */) /*: Array */ { +/** + * Recursively search for *.elm files. + * + * @param { string } dir + * @returns { Array } + */ +function findAllElmFilesInDir(dir) { return globSync('**/*.elm', { cwd: dir, caseSensitiveMatch: false, @@ -95,10 +99,12 @@ function findAllElmFilesInDir(dir /*: string */) /*: Array */ { }); } -function findTests( - testFilePaths /*: Array */, - project /*: typeof Project.Project */ -) /*: Promise }>> */ { +/** + * @param { Array } testFilePaths + * @param { import('./Project').Project } project + * @returns { Promise }>> } + */ +function findTests(testFilePaths, project) { return Promise.all( testFilePaths.map((filePath) => { const matchingSourceDirs = project.testsSourceDirs.filter((dir) => @@ -164,6 +170,11 @@ function findTests( ); } +/** + * @param { string } filePath + * @param { boolean } isPackageProject + * @returns { string } + */ function missingSourceDirectoryError(filePath, isPackageProject) { return ` This file: @@ -180,6 +191,12 @@ ${ `.trim(); } +/** + * @param { string } filePath + * @param { Array } matchingSourceDirs + * @param { string } testsDir + * @returns { string } + */ function multipleSourceDirectoriesError( filePath, matchingSourceDirs, @@ -204,6 +221,12 @@ ${note} `.trim(); } +/** + * @param { string } filePath + * @param { string } sourceDir + * @param { string } moduleName + * @returns { string } + */ function badModuleNameError(filePath, sourceDir, moduleName) { return ` This file: @@ -227,10 +250,12 @@ Make sure that all parts start with an uppercase letter and don't contain any sp `.trim(); } -function noFilesFoundError( - projectRootDir /*: string */, - testFileGlobs /*: Array */ -) /*: string */ { +/** + * @param { string } projectRootDir + * @param { Array } testFileGlobs + * @returns { string } + */ +function noFilesFoundError(projectRootDir, testFileGlobs) { return testFileGlobs.length === 0 ? ` ${noFilesFoundInTestsDir(projectRootDir)} @@ -249,6 +274,10 @@ Are the above patterns correct? Maybe try running elm-test with no arguments? `.trim(); } +/** + * @param { string } projectRootDir + * @returns { string } + */ function noFilesFoundInTestsDir(projectRootDir) { const testsDir = path.join(projectRootDir, 'tests'); try { diff --git a/lib/Generate.js b/lib/Generate.js index 5874f079..f38e5b0e 100644 --- a/lib/Generate.js +++ b/lib/Generate.js @@ -1,20 +1,9 @@ -// @flow - const { supportsColor } = require('./chalk'); const fs = require('fs'); const path = require('path'); -const { DependencyProvider } = require('./DependencyProvider.js'); const ElmJson = require('./ElmJson'); -const Project = require('./Project'); -const Report = require('./Report'); const Solve = require('./Solve'); -// These values are used _only_ in flow types. 'use' them with the javascript -// void operator to keep eslint happy. -void DependencyProvider; -void Project; -void Report; - const before = fs.readFileSync( path.join(__dirname, '..', 'templates', 'before.js'), 'utf8' @@ -25,10 +14,12 @@ const after = fs.readFileSync( 'utf8' ); -function prepareCompiledJsFile( - pipeFilename /*: string */, - dest /*: string */ -) /*: void */ { +/** + * @param { string } pipeFilename + * @param { string } dest + * @returns { void } + */ +function prepareCompiledJsFile(pipeFilename, dest) { const content = fs.readFileSync(dest, 'utf8'); const finalContent = ` ${before} @@ -61,8 +52,13 @@ const testVariantDefinition = const checkDefinition = /^(var\s+\$author\$project\$Test\$Runner\$Node\$check)\s*=\s*\$author\$project\$Test\$Runner\$Node\$checkHelperReplaceMe___;?$/m; -// Create a symbol, tag all `Test` constructors with it and make the `check` -// function look for it. +/** + * Create a symbol, tag all `Test` constructors with it and make the `check` + * function look for it. + * + * @param { string } content + * @returns { string } + */ function addKernelTestChecking(content) { return ( 'var __elmTestSymbol = Symbol("elmTestSymbol");\n' + @@ -75,14 +71,20 @@ function addKernelTestChecking(content) { ); } -function getGeneratedSrcDir(generatedCodeDir /*: string */) /*: string */ { +/** + * @param { string } generatedCodeDir + * @returns { string } + */ +function getGeneratedSrcDir(generatedCodeDir) { return path.join(generatedCodeDir, 'src'); } -function generateElmJson( - dependencyProvider /*: DependencyProvider */, - project /*: typeof Project.Project */ -) /*: void */ { +/** + * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { import('./Project').Project } project + * @returns { void } + */ +function generateElmJson(dependencyProvider, project) { const generatedSrc = getGeneratedSrcDir(project.generatedCodeDir); fs.mkdirSync(generatedSrc, { recursive: true }); @@ -138,10 +140,14 @@ function generateElmJson( } } -function getMainModule(generatedCodeDir /*: string */) /*: { - moduleName: string, - path: string, -} */ { +/** + * @param { string } generatedCodeDir + * @returns { { + moduleName: string, + path: string, + } } + */ +function getMainModule(generatedCodeDir) { const moduleName = ['Test', 'Generated', 'Main']; return { moduleName: moduleName.join('.'), @@ -153,19 +159,30 @@ function getMainModule(generatedCodeDir /*: string */) /*: { }; } -function generateMainModule( - fuzz /*: number */, - seed /*: number */, - report /*: typeof Report.Report */, - testFileGlobs /*: Array */, - testFilePaths /*: Array */, - testModules /*: Array<{ +/** + * @param { number } fuzz + * @param { number } seed + * @param { import('./Report').Report } report + * @param { Array } testFileGlobs + * @param { Array } testFilePaths + * @param { Array<{ moduleName: string, possiblyTests: Array, - }> */, - mainModule /*: { moduleName: string, path: string } */, - processes /*: number */ -) /*: void */ { + }> } testModules + * @param { { moduleName: string, path: string } } mainModule + * @param { number } processes + * @returns { void } + */ +function generateMainModule( + fuzz, + seed, + report, + testFileGlobs, + testFilePaths, + testModules, + mainModule, + processes +) { const testFileBody = makeTestFileBody( testModules, makeOptsCode(fuzz, seed, report, testFileGlobs, testFilePaths, processes) @@ -178,13 +195,15 @@ function generateMainModule( fs.writeFileSync(mainModule.path, testFileContents); } -function makeTestFileBody( - testModules /*: Array<{ +/** + * @param { Array<{ moduleName: string, possiblyTests: Array, - }> */, - optsCode /*: string */ -) /*: string */ { + }> } testModules + * @param { string } optsCode + * @returns { string } + */ +function makeTestFileBody(testModules, optsCode) { const imports = testModules.map((mod) => `import ${mod.moduleName}`); const possiblyTestsList = makeList(testModules.map(makeModuleTuple)); @@ -205,10 +224,14 @@ main = `.trim(); } -function makeModuleTuple(mod /*: { - moduleName: string, - possiblyTests: Array, -} */) /*: string */ { +/** + * @param { { + moduleName: string, + possiblyTests: Array, + } } mod + * @returns { string } + */ +function makeModuleTuple(mod) { const list = mod.possiblyTests.map( (test) => `Test.Runner.Node.check ${mod.moduleName}.${test}` ); @@ -220,7 +243,11 @@ function makeModuleTuple(mod /*: { `.trim(); } -function makeList(parts /*: Array */) /*: string */ { +/** + * @param { Array } parts + * @returns { string } + */ +function makeList(parts) { if (parts.length === 0) { return '[]'; } @@ -236,6 +263,11 @@ function makeList(parts /*: Array */) /*: string */ { `.trim(); } +/** + * @param { string } indent + * @param { string } string + * @returns { string } + */ function indentAllButFirstLine(indent, string) { return string .split('\n') @@ -243,14 +275,23 @@ function indentAllButFirstLine(indent, string) { .join('\n'); } +/** + * @param { number } fuzz + * @param { number } seed + * @param { import('./Report').Report } report + * @param { Array } testFileGlobs + * @param { Array } testFilePaths + * @param { number } processes + * @returns { string } + */ function makeOptsCode( - fuzz /*: number */, - seed /*: number */, - report /*: typeof Report.Report */, - testFileGlobs /*: Array */, - testFilePaths /*: Array */, - processes /*: number */ -) /*: string */ { + fuzz, + seed, + report, + testFileGlobs, + testFilePaths, + processes +) { return ` { runs = ${fuzz} , report = ${generateElmReportVariant(report)} @@ -264,9 +305,11 @@ function makeOptsCode( `.trim(); } -function generateElmReportVariant( - report /*: typeof Report.Report */ -) /*: string */ { +/** + * @param { import('./Report').Report } report + * @returns { string } + */ +function generateElmReportVariant(report) { switch (report) { case 'json': return 'JsonReport'; @@ -281,6 +324,10 @@ function generateElmReportVariant( } } +/** + * @param { string } string + * @returns { string } + */ function makeElmString(string) { return `"${string .replace(/[\\"]/g, '\\$&') diff --git a/lib/Install.js b/lib/Install.js index dee30a25..9709b38a 100644 --- a/lib/Install.js +++ b/lib/Install.js @@ -1,14 +1,13 @@ -// @flow - const spawn = require('cross-spawn'); const fs = require('fs'); const path = require('path'); const ElmJson = require('./ElmJson'); -const Project = require('./Project'); - -void Project; -function rmDirSync(dir /*: string */) /*: void */ { +/** + * @param { string } dir + * @returns { void } + */ +function rmDirSync(dir) { // 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 @@ -17,15 +16,19 @@ function rmDirSync(dir /*: string */) /*: void */ { if (fs.rmSync !== undefined) { fs.rmSync(dir, { recursive: true, force: true }); } else if (fs.existsSync(dir)) { + // @ts-expect-error TypeScript says that `rmdirSync` only takes one argument – because + // legacy versions of Node.js do – but we need to support those. fs.rmdirSync(dir, { recursive: true }); } } -function install( - project /*: typeof Project.Project */, - pathToElmBinary /*: string */, - packageName /*: string */ -) /*: 'SuccessfullyInstalled' | 'AlreadyInstalled' */ { +/** + * @param { import('./Project').Project } project + * @param { string } pathToElmBinary + * @param { string } packageName + * @returns { 'SuccessfullyInstalled' | 'AlreadyInstalled' } + */ +function install(project, pathToElmBinary, packageName) { const installationScratchDir = path.join(project.generatedCodeDir, 'install'); try { @@ -83,8 +86,15 @@ function install( } }); } else { + // This variable is needed for TypeScript to remember that `newElmJson` has + // been narrowed to `application` inside the `moveToTestDeps` function. + const newElmJson_ = newElmJson; + /** + * @param { 'direct' | 'indirect' } directness + * @returns { void } + */ function moveToTestDeps(directness) { - Object.keys(newElmJson['dependencies'][directness]).forEach(function ( + Object.keys(newElmJson_['dependencies'][directness]).forEach(function ( key ) { // If we didn't have this dep before, move it to test-dependencies. @@ -94,13 +104,13 @@ function install( // https://github.com/rtfeldman/node-test-runner/issues/282 if ( directness === 'direct' || - !newElmJson['test-dependencies']['direct'].hasOwnProperty(key) + !newElmJson_['test-dependencies']['direct'].hasOwnProperty(key) ) { - newElmJson['test-dependencies'][directness][key] = - newElmJson['dependencies'][directness][key]; + newElmJson_['test-dependencies'][directness][key] = + newElmJson_['dependencies'][directness][key]; } - delete newElmJson['dependencies'][directness][key]; + delete newElmJson_['dependencies'][directness][key]; } }); } diff --git a/lib/Parser.js b/lib/Parser.js index 270c14d1..62685936 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -1,40 +1,44 @@ -// @flow - // Useful for debugging. const LOG_ERRORS = 'ELM_TEST_LOG_PARSE_ERRORS' in process.env; -// For valid Elm files, this extracts _all_ (no more, no less) names that: -// 1. Are exposed. -// 2. _Might_ be tests. So capitalized names are excluded, for example. -// -// For invalid Elm files, this probably returns an empty list. It could also -// return a list of things it _thinks_ are exposed values, but it doesn’t -// matter. The idea is to bail early and still import the file. Then Elm gets a -// chance to show its nice error messages. -// -// The only times the returned promise is rejected are: -// -// - When there’s a problem reading the file from disk. -// - If there’s a bug and an unexpected exception is thrown somewhere. -// - If `effect module` is encountered. That’s a real edge case. The gist of it -// is that applications can’t contain effect modules and since the tests are -// compiled as an application we can’t include effect modules. -// -// The tokenizer reads the file character by character. As soon as it’s produced -// a whole token it feeds it to the parser, which works token by token. Both -// parse just enough to be able to extract all exposed names that could be tests -// without false positives. -function extractExposedPossiblyTests( - filePath /*: string */, - createReadStream /*: ( +/** + * For valid Elm files, this extracts _all_ (no more, no less) names that: + * 1. Are exposed. + * 2. _Might_ be tests. So capitalized names are excluded, for example. + * + * For invalid Elm files, this probably returns an empty list. It could also + * return a list of things it _thinks_ are exposed values, but it doesn’t + * matter. The idea is to bail early and still import the file. Then Elm gets a + * chance to show its nice error messages. + * + * The only times the returned promise is rejected are: + * + * - When there’s a problem reading the file from disk. + * - If there’s a bug and an unexpected exception is thrown somewhere. + * - If `effect module` is encountered. That’s a real edge case. The gist of it + * is that applications can’t contain effect modules and since the tests are + * compiled as an application we can’t include effect modules. + * + * The tokenizer reads the file character by character. As soon as it’s produced + * a whole token it feeds it to the parser, which works token by token. Both + * parse just enough to be able to extract all exposed names that could be tests + * without false positives. + * + * @param { string } filePath + * @param { ( path: string, - options?: { encoding?: string, ... } - ) => stream$Readable & { close(): void } */ -) /*: Promise> */ { + options?: { encoding?: BufferEncoding } + ) => import('stream').Readable & { close(): void } } createReadStream + * @returns { Promise> } + */ +function extractExposedPossiblyTests(filePath, createReadStream) { return new Promise((resolve, reject) => { - const exposedPossiblyTests /*: Array */ = []; - let tokenizerState /*: typeof TokenizerState */ = { tag: 'MaybeNewChunk' }; - let parserState /*: typeof ParserState */ = { + /** @type { Array } */ + const exposedPossiblyTests = []; + /** @type { TokenizerState } */ + let tokenizerState = { tag: 'MaybeNewChunk' }; + /** @type { ParserState } */ + let parserState = { tag: 'ModuleDeclaration', lastToken: 'Nothing', }; @@ -51,7 +55,11 @@ function extractExposedPossiblyTests( resolve(exposedPossiblyTests); }); - function onData(chunk /*: string */) /*: void */ { + /** + * @param { string } chunk + * @returns { void } + */ + function onData(chunk) { for (let index = 0; index < chunk.length; index++) { const char = chunk[index]; @@ -95,7 +103,7 @@ function extractExposedPossiblyTests( } break; default: - unreachable(flushCommand.tag); + unreachable(flushCommand, 'tag'); } } @@ -113,9 +121,11 @@ function extractExposedPossiblyTests( } } - function flush( - token /*: typeof Token | void */ - ) /*: typeof OnParserTokenResult | void */ { + /** + * @param { Token } [token] + * @returns { OnParserTokenResult | undefined } + */ + function flush(token) { if ( tokenizerState.tag === 'Initial' && tokenizerState.otherTokenChars !== '' @@ -139,9 +149,11 @@ function extractExposedPossiblyTests( return undefined; } - function stop( - result /*: typeof OnParserTokenResult */ - ) /*: Array */ { + /** + * @param { OnParserTokenResult } result + * @returns { Array } + */ + function stop(result) { switch (result.tag) { case 'ParseError': if (LOG_ERRORS) { @@ -162,22 +174,24 @@ function extractExposedPossiblyTests( } } - function onParserToken( - token /*: typeof Token */ - ) /*: typeof OnParserTokenResult | void */ { + /** + * @param { Token } token + * @returns { OnParserTokenResult | undefined } + */ + function onParserToken(token) { if (token.tag === 'LowerName') { lastLowerName = token.value; } switch (parserState.tag) { case 'ModuleDeclaration': { - const rawResult = parseModuleDeclaration( - token, - parserState.lastToken - ); - const result = - typeof rawResult === 'string' - ? { tag: 'Token', token: rawResult } - : rawResult; + const result = parseModuleDeclaration(token, parserState.lastToken); + if (typeof result === 'string') { + parserState.lastToken = result; + if (result === 'LowerName') { + exposedPossiblyTests.push(lastLowerName); + } + return undefined; + } switch (result.tag) { case 'ParseError': case 'CriticalParseError': @@ -185,42 +199,32 @@ function extractExposedPossiblyTests( return result; case 'NextParserState': parserState = { tag: 'Rest', lastToken: 'Initial' }; - break; - case 'Token': - parserState.lastToken = result.token; - if (result.token === 'LowerName') { - exposedPossiblyTests.push(lastLowerName); - } - break; + return undefined; default: unreachable(result); } - break; } case 'Rest': { - const rawResult = parseRest(token, parserState.lastToken); - const result = - typeof rawResult === 'string' - ? { tag: 'Token', token: rawResult } - : rawResult; + const result = parseRest(token, parserState.lastToken); + if (typeof result === 'string') { + parserState.lastToken = result; + if (result === 'PotentialTestDeclaration=') { + exposedPossiblyTests.push(lastLowerName); + } + return undefined; + } switch (result.tag) { case 'ParseError': return result; - case 'Token': - parserState.lastToken = result.token; - if (result.token === 'PotentialTestDeclaration=') { - exposedPossiblyTests.push(lastLowerName); - } - break; default: - unreachable(result); + // Type assertion needed here due to there only being one error variant. + unreachable(/** @type { never } */ (result), 'tag'); } - break; } default: - unreachable(parserState.tag); + unreachable(parserState, 'tag'); } } }); @@ -273,61 +277,76 @@ const backslashableChars = new Set([ 'u', ]); +/** + * @param { string } string + * @returns { boolean } + */ function isLowerName(string) { return lowerName.test(string) && !reservedWords.has(string); } -function isUpperName(string /*: string */) /*: boolean */ { +/** + * @param { string } string + * @returns { boolean } + */ +function isUpperName(string) { return upperName.test(string); } -function unreachable(value /*: empty */) /*: empty */ { - throw new Error(`Unreachable: ${value}`); +/** + * @param { never } value + * @param { string } [property] + * @returns { never } + */ +function unreachable(value, property) { + throw new Error( + `Unreachable: ${property === undefined ? value : value[property]}` + ); } -// Poor man’s type alias. We can’t use /*:: type ParseError = ... */ because of: -// https://github.com/prettier/prettier/issues/2597 -// There are a couple of more of this workaround throughout the file. -const ParseError /*: { - tag: 'ParseError', - message: string, - -[mixed]: empty, // https://github.com/facebook/flow/issues/7859 -} */ = { - tag: 'ParseError', - message: '', -}; -void ParseError; - -const CriticalParseError /*: { - tag: 'CriticalParseError', - message: string, -} */ = { - tag: 'CriticalParseError', - message: '', -}; -void CriticalParseError; - -const OnParserTokenResult /*: - | typeof ParseError - | typeof CriticalParseError - | { tag: 'StopParsing' } */ = ParseError; -void OnParserTokenResult; - -function expected( - expectedDescription /*: string */, - actual /*: mixed */ -) /*: typeof ParseError */ { +/** + * @typedef { { + tag: 'ParseError', + message: string, + } } ParseError + * + * @typedef { { + tag: 'CriticalParseError', + message: string, + } } CriticalParseError + * + * @typedef { + | ParseError + | CriticalParseError + | { tag: 'StopParsing' } + } OnParserTokenResult + */ + +/** + * @param { string } expectedDescription + * @param { unknown } actual + * @returns { ParseError } + */ +function expected(expectedDescription, actual) { return { tag: 'ParseError', message: `Expected ${expectedDescription} but got: ${stringify(actual)}`, }; } -function stringify(json /*: mixed */) /*: string */ { +/** + * @param { unknown } json + * @returns { string } + */ +function stringify(json) { const maybeString = JSON.stringify(json); return maybeString === undefined ? 'undefined' : maybeString; } +/** + * @param { unknown } actual + * @returns { ParseError } + */ function backslashError(actual) { return expected( `one of \`${Array.from(backslashableChars).join(' ')}\``, @@ -335,57 +354,58 @@ function backslashError(actual) { ); } -const Token /*: - | { tag: '(' } - | { tag: ')' } - | { tag: ',' } - | { tag: '=' } - | { tag: '.' } - | { tag: '..' } - | { tag: 'Char' } - | { tag: 'String' } - | { tag: 'NewChunk' } - | { tag: 'LowerName', value: string } - | { tag: 'UpperName', value: string } - | { tag: 'Other', value: string } */ = { tag: '(' }; -void Token; - -const TokenizerState /*: - | { tag: 'Initial', otherTokenChars: string } - | { tag: 'MaybeNewChunk' } - | { tag: 'MaybeMultilineComment{' } - | { tag: 'MultilineComment', level: number } - | { tag: 'MultilineComment{', level: number } - | { tag: 'MultilineComment-', level: number } - | { tag: 'MaybeSinglelineComment-' } - | { tag: 'SinglelineComment' } - | { tag: 'Maybe..' } - | { tag: 'CharStart' } - | { tag: 'CharBackslash' } - | { tag: 'CharUnicodeEscape' } - | { tag: 'CharEnd' } - | { tag: 'StringStart' } - | { tag: 'StringContent' } - | { tag: 'StringBackslash' } - | { tag: 'EmptyStringMaybeTriple' } - | { tag: 'MultilineString' } - | { tag: 'MultilineStringBackslash' } - | { tag: 'MultilineString"' } - | { tag: 'MultilineString""' } */ = { - tag: 'Initial', - otherTokenChars: '', -}; -void TokenizerState; - -function tokenize( - char /*: string */, - tokenizerState /*: typeof TokenizerState */ -) /*: - | [ - typeof TokenizerState, - Array<{ tag: 'Flush' } | { tag: 'FlushToken', token: typeof Token }> - ] - | typeof ParseError */ { +/** + * @typedef { + | { tag: '(' } + | { tag: ')' } + | { tag: ',' } + | { tag: '=' } + | { tag: '.' } + | { tag: '..' } + | { tag: 'Char' } + | { tag: 'String' } + | { tag: 'NewChunk' } + | { tag: 'LowerName', value: string } + | { tag: 'UpperName', value: string } + | { tag: 'Other', value: string } + } Token + * + * @typedef { + | { tag: 'Initial', otherTokenChars: string } + | { tag: 'MaybeNewChunk' } + | { tag: 'MaybeMultilineComment{' } + | { tag: 'MultilineComment', level: number } + | { tag: 'MultilineComment{', level: number } + | { tag: 'MultilineComment-', level: number } + | { tag: 'MaybeSinglelineComment-' } + | { tag: 'SinglelineComment' } + | { tag: 'Maybe..' } + | { tag: 'CharStart' } + | { tag: 'CharBackslash' } + | { tag: 'CharUnicodeEscape' } + | { tag: 'CharEnd' } + | { tag: 'StringStart' } + | { tag: 'StringContent' } + | { tag: 'StringBackslash' } + | { tag: 'EmptyStringMaybeTriple' } + | { tag: 'MultilineString' } + | { tag: 'MultilineStringBackslash' } + | { tag: 'MultilineString"' } + | { tag: 'MultilineString""' } + } TokenizerState + * + * @typedef { + | { tag: 'Flush' } + | { tag: 'FlushToken', token: Token } + } Cmd + */ + +/** + * @param { string } char + * @param { TokenizerState } tokenizerState + * @returns { [ TokenizerState, Array ] | ParseError } + */ +function tokenize(char, tokenizerState) { switch (tokenizerState.tag) { case 'Initial': switch (char) { @@ -690,58 +710,69 @@ function tokenize( } default: - return unreachable(tokenizerState.tag); + return unreachable(tokenizerState, 'tag'); } } +/** + * @param { string } previousChar + * @param { string } char + * @param { Array } cmds + * @returns { [ TokenizerState, Array ] | ParseError } + */ function tokenizeInitial(previousChar, char, cmds) { const result = tokenize(char, { tag: 'Initial', otherTokenChars: previousChar, }); - if (result.tag === 'ParseError') { + if (!Array.isArray(result)) { return result; } const [nextTokenizerState, nextCmds] = result; return [nextTokenizerState, cmds.concat(nextCmds)]; } -const ParserState /*: - | { - tag: 'ModuleDeclaration', - lastToken: typeof ModuleDeclarationLastToken, - } - | { - tag: 'Rest', - lastToken: typeof RestLastToken, - } */ = { tag: 'ModuleDeclaration', lastToken: 'Nothing' }; -void ParserState; - -const ModuleDeclarationLastToken /*: - | 'Nothing' - | 'NewChunk' - | 'port' - | 'module' - | 'ModuleName' - | 'ModuleName.' - | 'exposing' - | 'exposing(' - | 'exposing..' - | 'LowerName' - | 'UpperName' - | 'UpperName(' - | 'UpperName..' - | 'UpperName)' - | ',' */ = 'Nothing'; -void ModuleDeclarationLastToken; - -function parseModuleDeclaration( - token /*: typeof Token */, - lastToken /*: typeof ModuleDeclarationLastToken */ -) /*: - | typeof ModuleDeclarationLastToken - | { tag: 'NextParserState' } - | typeof OnParserTokenResult */ { +/** + * @typedef { + | { + tag: 'ModuleDeclaration', + lastToken: ModuleDeclarationLastToken, + } + | { + tag: 'Rest', + lastToken: RestLastToken, + } + } ParserState + * + * @typedef { + | 'Nothing' + | 'NewChunk' + | 'port' + | 'module' + | 'ModuleName' + | 'ModuleName.' + | 'exposing' + | 'exposing(' + | 'exposing..' + | 'LowerName' + | 'UpperName' + | 'UpperName(' + | 'UpperName..' + | 'UpperName)' + | ',' + } ModuleDeclarationLastToken + */ + +/** + * @param { Token } token + * @param { ModuleDeclarationLastToken } lastToken + * @returns { + | ModuleDeclarationLastToken + | { tag: 'NextParserState' } + | OnParserTokenResult + } + */ +function parseModuleDeclaration(token, lastToken) { switch (lastToken) { case 'Nothing': if (token.tag === 'NewChunk') { @@ -875,18 +906,22 @@ function parseModuleDeclaration( } } -const RestLastToken /*: - | 'Initial' - | 'NewChunk' - | 'PotentialTestDeclarationName' - | 'PotentialTestDeclaration=' - | 'Ignore' */ = 'Initial'; -void RestLastToken; - -function parseRest( - token /*: typeof Token */, - lastToken /*: typeof RestLastToken */ -) /*: typeof RestLastToken | typeof ParseError */ { +/** + * @typedef { + | 'Initial' + | 'NewChunk' + | 'PotentialTestDeclarationName' + | 'PotentialTestDeclaration=' + | 'Ignore' + } RestLastToken + */ + +/** + * @param { Token } token + * @param { RestLastToken } lastToken + * @returns { RestLastToken | ParseError } + */ +function parseRest(token, lastToken) { switch (lastToken) { case 'Initial': if (token.tag === 'NewChunk') { diff --git a/lib/Project.js b/lib/Project.js index 1e15ae0b..d614273d 100644 --- a/lib/Project.js +++ b/lib/Project.js @@ -1,33 +1,31 @@ -// @flow - const fs = require('fs'); const path = require('path'); const ElmJson = require('./ElmJson'); -// Poor man’s type alias. We can’t use /*:: type Project = ... */ because of: -// https://github.com/prettier/prettier/issues/2597 -const Project /*: { - rootDir: string, - testsDir: string, - generatedCodeDir: string, - testsSourceDirs: Array, - elmJson: typeof ElmJson.ElmJson, -} */ = { - rootDir: '', - testsDir: '', - generatedCodeDir: '', - testsSourceDirs: [], - elmJson: ElmJson.ElmJson, -}; - -function getTestsDir(rootDir /*: string */) /*: string */ { +/** + * @typedef { { + rootDir: string, + testsDir: string, + generatedCodeDir: string, + testsSourceDirs: Array, + elmJson: import('./ElmJson').ElmJson, + } } Project + */ + +/** + * @param { string } rootDir + * @returns { string } + */ +function getTestsDir(rootDir) { return path.join(rootDir, 'tests'); } -function init( - rootDir /*: string */, - version /*: string */ -) /*: typeof Project */ { +/** + * @param { string } rootDir + * @param { string } version + * @returns { Project } + */ +function init(rootDir, version) { const testsDir = getTestsDir(rootDir); // The tests/ directory is not required. You can also co-locate tests with @@ -78,7 +76,11 @@ following directory: I cannot find it though. Is it missing? Is there a typo? */ -function validateTestsSourceDirs(project /*: typeof Project */) /*: void */ { +/** + * @param { Project } project + * @returns { void } + */ +function validateTestsSourceDirs(project) { for (const dir of project.testsSourceDirs) { const fullDir = path.resolve(project.rootDir, dir); let stats; @@ -105,6 +107,11 @@ function validateTestsSourceDirs(project /*: typeof Project */) /*: void */ { } } +/** + * @param { string } dir + * @param { string } message + * @returns { string } + */ function validateTestsSourceDirsError(dir, message) { return ` The "source-directories" field in your elm.json lists the following directory: @@ -116,7 +123,6 @@ ${message} } module.exports = { - Project, getTestsDir, init, validateTestsSourceDirs, diff --git a/lib/Report.js b/lib/Report.js index 09582148..488f005b 100644 --- a/lib/Report.js +++ b/lib/Report.js @@ -1,12 +1,14 @@ -// @flow - -// Poor man’s type alias. We can’t use /*:: type Report = ... */ because of: -// https://github.com/prettier/prettier/issues/2597 -const Report /*: 'console' | 'json' | 'junit' */ = 'console'; +/** + * @typedef { 'console' | 'json' | 'junit' } Report + */ const all = ['json', 'junit', 'console']; -function isMachineReadable(report /*: typeof Report */) /*: boolean */ { +/** + * @param { Report } report + * @returns { boolean } + */ +function isMachineReadable(report) { switch (report) { case 'json': case 'junit': @@ -17,7 +19,6 @@ function isMachineReadable(report /*: typeof Report */) /*: boolean */ { } module.exports = { - Report, all, isMachineReadable, }; diff --git a/lib/RunTests.js b/lib/RunTests.js index 6128f5b7..5d9b8af3 100644 --- a/lib/RunTests.js +++ b/lib/RunTests.js @@ -1,11 +1,8 @@ -// @flow - const chalk = require('./chalk'); const path = require('path'); const readline = require('readline'); const packageInfo = require('../package.json'); const Compile = require('./Compile'); -const { DependencyProvider } = require('./DependencyProvider.js'); const ElmJson = require('./ElmJson'); const FindTests = require('./FindTests'); const Generate = require('./Generate'); @@ -13,39 +10,48 @@ const Project = require('./Project'); const Report = require('./Report'); const Supervisor = require('./Supervisor'); -// This value is used _only_ in flow types. 'use' it with the javascript -// void operator to keep eslint happy. -void DependencyProvider; - -// Incorporate the process PID into the socket name, so elm-test processes can -// be run parallel without accidentally sharing each others' sockets. -// -// See https://github.com/rtfeldman/node-test-runner/pull/231 -// Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - -// see https://github.com/rtfeldman/node-test-runner/issues/275 - because the -// alternative approach of deleting the file before creating a new one doesn't -// work on Windows. We have to let Windows clean up the named pipe. This is -// essentially a band-aid fix. The alternative is to rewrite a ton of stuff. -function getPipeFilename(runsExecuted /*: number */) /*: string */ { +/** + * Incorporate the process PID into the socket name, so elm-test processes can + * be run parallel without accidentally sharing each others' sockets. + * + * See https://github.com/rtfeldman/node-test-runner/pull/231 + * Also incorporate a salt number into it on Windows, to avoid EADDRINUSE - + * see https://github.com/rtfeldman/node-test-runner/issues/275 - because the + * alternative approach of deleting the file before creating a new one doesn't + * work on Windows. We have to let Windows clean up the named pipe. This is + * essentially a band-aid fix. The alternative is to rewrite a ton of stuff. + * + * @param { number } runsExecuted + * @returns { string } + */ +function getPipeFilename(runsExecuted) { return process.platform === 'win32' ? `\\\\.\\pipe\\elm_test-${process.pid}-${runsExecuted}` : `/tmp/elm_test-${process.pid}.sock`; } -// This could have been a class, but I couldn’t figure out how to type it with -// Flow comments and Prettier. -// This lets you log something like `elm.json changed > Compiling > Starting tests` -// where each segment appears over time. -// `\r` moves the cursor to the start of the line. This is important when there -// are Elm compilation errors - they overwrite the progress text rather than -// weirdly ending up with stuff like: -// `elm.json changed > Compiling-- NAMING ERROR - File.elm` -// Also note that using too much `clearLine` or `clearConsole` causes flickering -// on Windows, so it's nicer to cleverly overwrite old output when possible. -function makeProgressLogger( - report /*: typeof Report.Report */, - clearConsole /*: boolean */ -) { +/** + * This lets you log something like `elm.json changed > Compiling > Starting tests` + * where each segment appears over time. + * `\r` moves the cursor to the start of the line. This is important when there + * are Elm compilation errors - they overwrite the progress text rather than + * weirdly ending up with stuff like: + * `elm.json changed > Compiling-- NAMING ERROR - File.elm` + * Also note that using too much `clearLine` or `clearConsole` causes flickering + * on Windows, so it's nicer to cleverly overwrite old output when possible. + * + * @param { import('./Report').Report } report + * @param { boolean } clearConsole + * @returns { { + log: (message: string) => void, + newLine: () => void, + overwrite: (message: string) => void, + clearLine: () => void, + clearConsole: () => void, + } } + */ +function makeProgressLogger(report, clearConsole) { + /** @type { Array } */ const items = []; return { log(message) { @@ -86,29 +92,41 @@ function makeProgressLogger( }; } -function diffArrays/*:: */( - from /*: Array */, - to /*: Array */ -) /*: { added: Array, removed: Array } */ { +/** + * @template T + * @param { Array } from + * @param { Array } to + * @returns { { added: Array, removed: Array } } + */ +function diffArrays(from, to) { return { added: to.filter((item) => !from.includes(item)), removed: from.filter((item) => !to.includes(item)), }; } -function delay(ms /*: number */) /*: Promise */ { +/** + * @param { number } ms + * @returns { Promise } + */ +function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } -const Queue /*: Array<{ - event: 'added' | 'changed' | 'removed', - filePath: string, -}> */ = []; -void Queue; - -function watcherEventMessage(queue /*: typeof Queue */) /*: string */ { +/** + * @typedef { Array<{ + event: 'added' | 'changed' | 'removed', + filePath: string, + }> } Queue + */ + +/** + * @param { Queue } queue + * @returns { string } + */ +function watcherEventMessage(queue) { const filePaths = Array.from(new Set(queue.map(({ filePath }) => filePath))); if (filePaths.length === 1) { const { event, filePath } = queue[0]; @@ -119,35 +137,48 @@ function watcherEventMessage(queue /*: typeof Queue */) /*: string */ { return `${filePaths.length} files ${events.join('/')}`; } -function runTests( - dependencyProvider /*: DependencyProvider */, - projectRootDir /*: string */, - pathToElmBinary /*: string */, - testFileGlobs /*: Array */, - processes /*: number */, - { - watch, - clearConsole, - report, - seed, - fuzz, - } /*: { +/** + * @typedef { { watch: boolean, clearConsole: boolean, - report: typeof Report.Report, + report: import('./Report').Report, seed: number, fuzz: number, - } */ -) /*: Promise */ { + } } Options + + * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { string } projectRootDir + * @param { string } pathToElmBinary + * @param { Array } testFileGlobs + * @param { number } processes + * @param { Options } options + * @returns { Promise } + */ +function runTests( + dependencyProvider, + projectRootDir, + pathToElmBinary, + testFileGlobs, + processes, + { watch, clearConsole, report, seed, fuzz } +) { + /** @type { import('chokidar').FSWatcher | undefined } */ let watcher = undefined; - let watchedPaths /*: Array */ = []; - let runsExecuted /*: number */ = 0; - let currentRun /*: Promise | void */ = undefined; - let queue /*: typeof Queue */ = []; + /** @type { Array } */ + let watchedPaths = []; + /** @type { number } */ + let runsExecuted = 0; + /** @type { Promise | void } */ + let currentRun = undefined; + /** @type { Queue } */ + let queue = []; const progressLogger = makeProgressLogger(report, clearConsole); - async function run() /*: Promise */ { + /** + * @returns { Promise } + */ + async function run() { try { // Don’t delay the first run (that’s the only time the queue is empty). // Otherwise, wait for a little bit to batch events that happened roughly @@ -211,7 +242,7 @@ function runTests( const mainModule = Generate.getMainModule(project.generatedCodeDir); const dest = path.join(project.generatedCodeDir, 'elmTestOutput.js'); - await Generate.generateElmJson(dependencyProvider, project); + Generate.generateElmJson(dependencyProvider, project); progressLogger.log('Compiling'); @@ -278,6 +309,7 @@ function runTests( Project.getTestsDir(projectRootDir), ]; + /** @type { (event: 'added' | 'changed' | 'removed') => (absoluteFilePath: string) => void } */ const rerun = (event) => (absoluteFilePath) => { if ( absoluteFilePath.endsWith('.elm') || diff --git a/lib/Solve.js b/lib/Solve.js index 21a7e397..b8e35401 100644 --- a/lib/Solve.js +++ b/lib/Solve.js @@ -1,25 +1,22 @@ -// @flow - const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const ElmJson = require('./ElmJson'); -const Project = require('./Project'); -const { DependencyProvider } = require('./DependencyProvider.js'); - -// These value are used _only_ in flow types. 'use' them with the javascript -// void operator to keep eslint happy. -void Project; -void DependencyProvider; +/** + * @param { string } string + * @returns { string } + */ function sha256(string) { return crypto.createHash('sha256').update(string).digest('hex'); } -function getDependenciesCached( - dependencyProvider /*: DependencyProvider */, - project /*: typeof Project.Project */ -) /*: typeof ElmJson.DirectAndIndirectDependencies */ { +/** + * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { import('./Project').Project } project + * @returns { import('./ElmJson').DirectAndIndirectDependencies } + */ +function getDependenciesCached(dependencyProvider, project) { const hash = sha256( JSON.stringify({ dependencies: project.elmJson.dependencies, @@ -52,10 +49,12 @@ function getDependenciesCached( ); } -function getDependencies( - dependencyProvider /*: DependencyProvider */, - elmJson /*: typeof ElmJson.ElmJson */ -) /*: string */ { +/** + * @param { import('./DependencyProvider').DependencyProviderType } dependencyProvider + * @param { import('./ElmJson').ElmJson } elmJson + * @returns { string } + */ +function getDependencies(dependencyProvider, elmJson) { const useTest = true; // Note: These are the dependencies listed in `elm/elm.json`, except // `elm-explorations/test`. `elm/elm.json` is only used during development of diff --git a/lib/Supervisor.js b/lib/Supervisor.js index fc651678..e67bff62 100644 --- a/lib/Supervisor.js +++ b/lib/Supervisor.js @@ -1,5 +1,3 @@ -// @flow - const chalk = require('./chalk'); const child_process = require('child_process'); const fs = require('fs'); @@ -7,26 +5,35 @@ const net = require('net'); const split = require('split'); const Report = require('./Report'); -function run( - elmTestVersion /*: string */, - pipeFilename /*: string */, - report /*: typeof Report.Report */, - processes /*: number */, - dest /*: string */, - watch /*: boolean */ -) /*: Promise */ { +/** + * @param { string } elmTestVersion + * @param { string } pipeFilename + * @param { import('./Report').Report } report + * @param { number } processes + * @param { string } dest + * @param { boolean } watch + * @returns { Promise } + */ +function run(elmTestVersion, pipeFilename, report, processes, dest, watch) { return new Promise(function (resolve) { + /** @type { number | null } */ var nextResultToPrint = null; var finishedWorkers = 0; var closedWorkers = 0; var results = new Map(); var failures = 0; + /** @type { Array<{ labels: Array, todo: string }> } */ var todos = []; var testsToRun = -1; var initializedWorkers = -1; var startingTime = Date.now(); + /** @type { Array } */ var workers = []; + /** + * @param { any } result This `any` became explicit instead of implicit when migrating from Flow to TypeScript. + * @returns { void } + */ function printResult(result) { switch (report) { case 'console': @@ -96,6 +103,10 @@ function run( ); } + /** + * @param { any } response This `any` became explicit instead of implicit when migrating from Flow to TypeScript. + * @returns { void } + */ function handleResults(response) { // TODO print progress bar - e.g. "Running test 5 of 20" on a bar! // -- yikes, be careful though...test the scenario where test @@ -144,6 +155,10 @@ function run( flushResults(); } + /** + * @param { import('net').Socket } socket + * @returns { void } + */ function initWorker(socket) { socket.setEncoding('utf8'); socket.setNoDelay(true); @@ -208,10 +223,11 @@ function run( // hex-encoded unicode codepoint for the invalid character. For // example, the start of a terminal escape (`\u{001B}` in Elm) will be output as a // literal `\u{001B}`. + /** @type { (char: string) => string } */ var invalidCharReplacement = function (char) { return ( '\\u{' + - char.codePointAt(0).toString(16).padStart(4, '0') + + (char.codePointAt(0) || 0).toString(16).padStart(4, '0') + '}' ); }; @@ -221,6 +237,7 @@ function run( // when using the junit report, so `require` it late. require('xmlbuilder') .create(xml, { + // @ts-expect-error The type annotation says that `invalidCharReplacement` should be a string, but we have always passed a function. This got noticed when migrating from Flow to TypeScript. invalidCharReplacement: invalidCharReplacement, }) .end() @@ -323,11 +340,18 @@ function run( }); } +/** + * @param { string } text + * @returns { string } + */ function makeWindowsSafe(text) { return process.platform === 'win32' ? windowsify(text) : text; } -// Fix Windows Unicode problems. Credit to https://github.com/sindresorhus/figures for the Windows compat idea! +/** + * Fix Windows Unicode problems. Credit to https://github.com/sindresorhus/figures for the Windows compat idea! + * @type { Array<[RegExp, string]> } + */ var windowsSubstitutions = [ [/[↓✗►]/g, '>'], [/╵│╷╹┃╻/g, '|'], @@ -336,8 +360,12 @@ var windowsSubstitutions = [ [/✔/g, '√'], ]; +/** + * @param { string } str + * @returns { string } + */ function windowsify(str) { - return windowsSubstitutions.reduce(function (result /*: string */, sub) { + return windowsSubstitutions.reduce(function (result, sub) { return result.replace(sub[0], sub[1]); }, str); } diff --git a/lib/SyncGet.js b/lib/SyncGet.js index 32f5f427..04106d12 100644 --- a/lib/SyncGet.js +++ b/lib/SyncGet.js @@ -1,40 +1,43 @@ -// @flow - const path = require('path'); const { Worker, MessageChannel, receiveMessageOnPort, - // $FlowFixMe[cannot-resolve-module]: Flow doesn’t seem to know about the `worker_threads` module yet. } = require('worker_threads'); -// Poor man’s type alias. We can’t use /*:: type SyncGetWorker = ... */ because of: -// https://github.com/prettier/prettier/issues/2597 -const SyncGetWorker /*: { - get: (string) => string, - shutDown: () => void, -} */ = { - get: (string) => string, - shutDown: () => {}, -}; +/** + * @typedef { { + get: (key: string) => string, + shutDown: () => void, + } } SyncGetWorker + */ -// Start a worker thread and return a `syncGetWorker` -// capable of making sync requests until shut down. -function startWorker() /*: typeof SyncGetWorker */ { +/** + * Start a worker thread and return a `syncGetWorker` + * capable of making sync requests until shut down. + * + * @returns { SyncGetWorker } + */ +function startWorker() { const { port1: localPort, port2: workerPort } = new MessageChannel(); const sharedLock = new SharedArrayBuffer(4); - // $FlowFixMe[incompatible-call]: Flow is wrong and says `sharedLock` is not an accepted parameter here. const sharedLockArray = new Int32Array(sharedLock); const workerPath = path.resolve(__dirname, 'SyncGetWorker.js'); const worker = new Worker(workerPath, { workerData: { sharedLock, requestPort: workerPort }, transferList: [workerPort], }); + /** + * @param { string } url + * @returns { string } + */ function get(url) { worker.postMessage(url); Atomics.wait(sharedLockArray, 0, 0); // blocks until notified at index 0. const response = receiveMessageOnPort(localPort); - if (response.message.error) { + if (response === undefined) { + throw new Error(`No message on port ${localPort} available.`); + } else if (response.message.error) { throw response.message.error; } else { return response.message; @@ -48,6 +51,5 @@ function startWorker() /*: typeof SyncGetWorker */ { } module.exports = { - SyncGetWorker, startWorker, }; diff --git a/lib/SyncGetWorker.js b/lib/SyncGetWorker.js index 0b7f547d..216c41b0 100644 --- a/lib/SyncGetWorker.js +++ b/lib/SyncGetWorker.js @@ -1,12 +1,13 @@ -// @flow - -// $FlowFixMe[cannot-resolve-module]: Flow doesn’t seem to know about the `worker_threads` module yet. const { parentPort, workerData } = require('worker_threads'); const https = require('https'); const { sharedLock, requestPort } = workerData; const sharedLockArray = new Int32Array(sharedLock); +if (parentPort === null) { + throw new Error('parentPort is null!'); +} + parentPort.on('message', async (url) => { try { const response = await getBody(url); @@ -17,7 +18,11 @@ parentPort.on('message', async (url) => { Atomics.notify(sharedLockArray, 0, Infinity); }); -async function getBody(url /*: string */) /*: Promise */ { +/** + * @param { string } url + * @returns { Promise } + */ +async function getBody(url) { return new Promise(function (resolve, reject) { https .get(url, function (res) { diff --git a/lib/chalk.js b/lib/chalk.js index 2f439597..6ce8465b 100644 --- a/lib/chalk.js +++ b/lib/chalk.js @@ -1,14 +1,21 @@ -// @flow const supportsColor = require('./supports-color'); // Find more colors/styles in // https://github.com/chalk/ansi-styles/blob/main/index.js -function red(string /*: string */) /*: string */ { +/** + * @param { string } string + * @returns { string } + */ +function red(string) { return supportsColor ? `\x1B[31m${string}\x1B[39m` : string; } -function blue(string /*: string */) /*: string */ { +/** + * @param { string } string + * @returns { string } + */ +function blue(string) { return supportsColor ? `\x1B[34m${string}\x1B[39m` : string; } diff --git a/lib/elm-test.js b/lib/elm-test.js index 1127b77c..85a8c3c7 100644 --- a/lib/elm-test.js +++ b/lib/elm-test.js @@ -1,5 +1,3 @@ -// @flow - const { InvalidOptionArgumentError, Option, program } = require('commander'); const fs = require('fs'); const os = require('os'); @@ -16,24 +14,25 @@ const Project = require('./Project'); const Report = require('./Report'); const RunTests = require('./RunTests'); -void Report; - -const parsePositiveInteger = - (minimum /*: number */) => - (string /*: string */) /*: number */ => { - const number = Number(string); - if (!/^\d+$/.test(string)) { - throw new InvalidOptionArgumentError('Expected one or more digits.'); - } else if (!Number.isFinite(number)) { - throw new InvalidOptionArgumentError('Expected a finite number.'); - } else if (number < minimum) { - throw new InvalidOptionArgumentError(`Expected at least ${minimum}.`); - } else { - return number; - } - }; +/** @type { (minimum: number) => (string: string) => number } */ +const parsePositiveInteger = (minimum) => (string) => { + const number = Number(string); + if (!/^\d+$/.test(string)) { + throw new InvalidOptionArgumentError('Expected one or more digits.'); + } else if (!Number.isFinite(number)) { + throw new InvalidOptionArgumentError('Expected a finite number.'); + } else if (number < minimum) { + throw new InvalidOptionArgumentError(`Expected at least ${minimum}.`); + } else { + return number; + } +}; -function findClosestElmJson(dir /*: string */) /*: string | void */ { +/** + * @param { string } dir + * @returns { string | void } + */ +function findClosestElmJson(dir) { const entry = ElmJson.getPath(dir); return fs.existsSync(entry) ? entry @@ -42,7 +41,11 @@ function findClosestElmJson(dir /*: string */) /*: string | void */ { : findClosestElmJson(path.dirname(dir)); } -function getProjectRootDir(subcommand /*: string */) /*: string */ { +/** + * @param { string } subcommand + * @returns { string } + */ +function getProjectRootDir(subcommand) { const elmJsonPath = findClosestElmJson(process.cwd()); if (elmJsonPath === undefined) { const command = @@ -55,7 +58,11 @@ function getProjectRootDir(subcommand /*: string */) /*: string */ { return path.dirname(elmJsonPath); } -function getProject(subcommand /*: string */) /*: typeof Project.Project */ { +/** + * @param { string } subcommand + * @returns { import('./Project').Project } + */ +function getProject(subcommand) { try { return Project.init(getProjectRootDir(subcommand), packageInfo.version); } catch (error) { @@ -64,7 +71,11 @@ function getProject(subcommand /*: string */) /*: typeof Project.Project */ { } } -function getPathToElmBinary(compiler /*: string | void */) /*: string */ { +/** + * @param { string | void } compiler + * @returns { string } + */ +function getPathToElmBinary(compiler) { const name = compiler === undefined ? 'elm' : compiler; try { return path.resolve(which.sync(name)); @@ -173,7 +184,7 @@ function main() { ) .action(() => { const options = program.opts(); - const pathToElmBinary = getPathToElmBinary(options.compiler); + const pathToElmBinary = getPathToElmBinary(options['compiler']); const project = getProject('init'); try { Install.install(project, pathToElmBinary, ElmJson.ELM_TEST_PACKAGE); @@ -199,7 +210,7 @@ function main() { ) .action((packageName) => { const options = program.opts(); - const pathToElmBinary = getPathToElmBinary(options.compiler); + const pathToElmBinary = getPathToElmBinary(options['compiler']); const project = getProject('install'); try { const result = Install.install(project, pathToElmBinary, packageName); @@ -220,7 +231,7 @@ function main() { .description('Check files matching the globs for compilation errors') .action((testFileGlobs) => { const options = program.opts(); - const pathToElmBinary = getPathToElmBinary(options.compiler); + const pathToElmBinary = getPathToElmBinary(options['compiler']); const project = getProject('make'); const make = async () => { Generate.generateElmJson(dependencyProvider, project); @@ -231,7 +242,7 @@ function main() { ), project.generatedCodeDir, pathToElmBinary, - options.report + options['report'] ); }; make().then( @@ -249,7 +260,7 @@ function main() { .command('__elmTestCommand__ [globs...]', { hidden: true, isDefault: true }) .action((testFileGlobs) => { const options = program.opts(); - const pathToElmBinary = getPathToElmBinary(options.compiler); + const pathToElmBinary = getPathToElmBinary(options['compiler']); const projectRootDir = getProjectRootDir('tests'); const processes = Math.max(1, os.cpus().length); RunTests.runTests( @@ -258,7 +269,8 @@ function main() { pathToElmBinary, testFileGlobs, processes, - options + // The flag validations are supposed to make this type assertion safe here. + /** @type { import('./RunTests').Options } */ (options) ).then( (code) => process.exit(code), (error) => { diff --git a/lib/supports-color.js b/lib/supports-color.js index b37762b4..cfe01e9c 100644 --- a/lib/supports-color.js +++ b/lib/supports-color.js @@ -1,4 +1,3 @@ -// @flow /** Based on https://github.com/chalk/supports-color v10.2.2 but adapted to use CommonJs and simplified to our needs (only knowing whether stdout supports colors). @@ -15,9 +14,12 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI */ const { env } = process; -function supportsColor() /*: boolean */ { - if ('FORCE_COLOR' in env && env.FORCE_COLOR !== '') { - return env.FORCE_COLOR !== 'false' && env.FORCE_COLOR !== '0'; +/** + * @returns { boolean } + */ +function supportsColor() { + if ('FORCE_COLOR' in env && env['FORCE_COLOR'] !== '') { + return env['FORCE_COLOR'] !== 'false' && env['FORCE_COLOR'] !== '0'; } if (process.argv.includes('--no-color')) { @@ -57,7 +59,7 @@ function supportsColor() /*: boolean */ { return true; } - if (env.TERM === 'dumb') { + if (env['TERM'] === 'dumb') { return false; } @@ -73,4 +75,4 @@ function supportsColor() /*: boolean */ { return false; } -module.exports = (supportsColor() /*: boolean */); +module.exports = supportsColor(); diff --git a/package-lock.json b/package-lock.json index 5ddb33e8..1cd4388b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,15 +24,22 @@ }, "devDependencies": { "@eslint/js": "9.20.0", + "@types/cross-spawn": "6.0.6", + "@types/graceful-fs": "4.1.9", + "@types/mocha": "10.0.10", + "@types/node": "26.0.1", + "@types/split": "1.0.5", + "@types/which": "3.0.4", + "@types/xml2js": "0.4.14", "elm-review": "2.13.1", "elm-tooling": "1.18.0", "eslint": "9.20.1", "eslint-plugin-mocha": "10.5.0", - "flow-bin": "0.180.0", "globals": "15.15.0", "mocha": "11.1.0", "prettier": "2.8.1", "strip-ansi": "6.0.0", + "typescript": "7.0.2", "xml2js": "0.5.0" }, "engines": { @@ -395,12 +402,32 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -422,13 +449,21 @@ "@types/node": "*" } }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/responselike": { @@ -440,6 +475,384 @@ "@types/node": "*" } }, + "node_modules/@types/split": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/split/-/split-1.0.5.tgz", + "integrity": "sha512-gMiDr4vA6YofTpAkPQtP+5pvStIf3CMYphf32YAG/3RwogNL8ii1CQKDc+sxN62KuxPoRaJXcf2zDCDkEBH4FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/through": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/which": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz", + "integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -1388,18 +1801,6 @@ "dev": true, "license": "ISC" }, - "node_modules/flow-bin": { - "version": "0.180.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.180.0.tgz", - "integrity": "sha512-jEZoIwOxzrtQ0erUu94nEzlqUoX7OAMeVs0CjO0rN6b7SDBhI5IysVRvGSQkkFWBJpy5VQ9lvzBYzq5Sq9vcmg==", - "dev": true, - "bin": { - "flow": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/folder-hash": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.5.tgz", @@ -2689,11 +3090,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", @@ -3121,12 +3558,30 @@ "@types/responselike": "^1.0.0" } }, + "@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -3148,13 +3603,19 @@ "@types/node": "*" } }, + "@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "dev": true, "requires": { - "undici-types": "~6.20.0" + "undici-types": "~8.3.0" } }, "@types/responselike": { @@ -3166,6 +3627,180 @@ "@types/node": "*" } }, + "@types/split": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/split/-/split-1.0.5.tgz", + "integrity": "sha512-gMiDr4vA6YofTpAkPQtP+5pvStIf3CMYphf32YAG/3RwogNL8ii1CQKDc+sxN62KuxPoRaJXcf2zDCDkEBH4FA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/through": "*" + } + }, + "@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/which": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz", + "integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==", + "dev": true + }, + "@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "dev": true, + "optional": true + }, + "@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "dev": true, + "optional": true + }, + "@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "dev": true, + "optional": true + }, + "@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "dev": true, + "optional": true + }, + "@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "dev": true, + "optional": true + }, + "@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "dev": true, + "optional": true + }, + "@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "dev": true, + "optional": true + }, "acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3824,12 +4459,6 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, - "flow-bin": { - "version": "0.180.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.180.0.tgz", - "integrity": "sha512-jEZoIwOxzrtQ0erUu94nEzlqUoX7OAMeVs0CjO0rN6b7SDBhI5IysVRvGSQkkFWBJpy5VQ9lvzBYzq5Sq9vcmg==", - "dev": true - }, "folder-hash": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.5.tgz", @@ -4729,10 +5358,38 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, + "typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "requires": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, "undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index 23c7ae09..821e57cb 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,11 @@ "scripts": { "prepare": "elm-tooling install", "test": "npm run check && npm run test-only", - "flow": "flow", "lint": "eslint --report-unused-disable-directives .", "review": "cd elm && elm-review", "elm-test": "cd elm && node ../bin/elm-test", "test-only": "mocha tests && npm run elm-test", - "check": "flow check && npm run lint && npm run format:check && npm run review", + "check": "tsc && npm run lint && npm run format:check && npm run review", "format:check": "prettier --check . && elm-format elm --validate", "format:write": "prettier --write . && elm-format elm --yes" }, @@ -55,15 +54,22 @@ }, "devDependencies": { "@eslint/js": "9.20.0", + "@types/cross-spawn": "6.0.6", + "@types/graceful-fs": "4.1.9", + "@types/mocha": "10.0.10", + "@types/node": "26.0.1", + "@types/split": "1.0.5", + "@types/which": "3.0.4", + "@types/xml2js": "0.4.14", "elm-review": "2.13.1", "elm-tooling": "1.18.0", "eslint": "9.20.1", "eslint-plugin-mocha": "10.5.0", - "flow-bin": "0.180.0", "globals": "15.15.0", "mocha": "11.1.0", "prettier": "2.8.1", "strip-ansi": "6.0.0", + "typescript": "7.0.2", "xml2js": "0.5.0" } } diff --git a/prettier.config.js b/prettier.config.js index d48f083b..da98b339 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,12 +1,4 @@ module.exports = { singleQuote: true, proseWrap: 'never', - overrides: [ - { - files: '*.js', - options: { - parser: 'flow', - }, - }, - ], }; diff --git a/tests/DependencyProvider.js b/tests/DependencyProvider.js index 3ce7f395..4719bc1f 100644 --- a/tests/DependencyProvider.js +++ b/tests/DependencyProvider.js @@ -9,6 +9,10 @@ describe('DependencyProvider', () => { describe('prioritizePinnedIndirectVersion', () => { const versions = ['1.0.5', '1.0.4', '1.0.3', '1.0.2', '1.0.1', '1.0.0']; + /** + * @param { string | undefined } pinnedVersion + * @param { Array } expectedResult + */ const testPinning = (pinnedVersion, expectedResult) => { assert.deepStrictEqual( prioritizePinnedIndirectVersion(versions, pinnedVersion), diff --git a/tests/ElmJson.js b/tests/ElmJson.js index 296218a7..4e0c37ee 100644 --- a/tests/ElmJson.js +++ b/tests/ElmJson.js @@ -46,7 +46,7 @@ describe('handling invalid elm.json', () => { }, (error) => { assert.strictEqual( - error.message.replace( + /** @type { Error } */ (error).message.replace( // Handle slightly different JSON.parse error messages on different Node.js versions. /^.+ in JSON at position .+$/gm, '(the JSON parse error)' diff --git a/tests/Parser.js b/tests/Parser.js index 0a2bf531..095d99a1 100644 --- a/tests/Parser.js +++ b/tests/Parser.js @@ -4,14 +4,21 @@ const assert = require('assert'); const stream = require('stream'); const Parser = require('../lib/Parser'); +/** + * @param { string } elmCode + * @param { Array } expectedExposedNames + */ async function testParser(elmCode, expectedExposedNames) { const exposed = await Parser.extractExposedPossiblyTests( 'SomeFile.elm', (_, options) => { - const readable = stream.Readable.from([elmCode], { - ...options, - autoDestroy: true, - }); + const readable = + /** @type { import('stream').Readable & { close(): void } } */ ( + stream.Readable.from([elmCode], { + ...options, + autoDestroy: true, + }) + ); readable.close = readable.destroy; return readable; } diff --git a/tests/ci.js b/tests/ci.js index fab04477..e113901c 100755 --- a/tests/ci.js +++ b/tests/ci.js @@ -17,14 +17,27 @@ const resultSuccess = 0; const resultErrored = 1; const resultFailureThreshold = 2; +/** + * @param { Array } args + * @param { string } cwd + * @returns { import('child_process').SpawnSyncReturns } + */ function execElmTest(args, cwd = '.') { return spawn.sync( elmtestPath, args, - Object.assign({ encoding: 'utf-8', cwd }, spawnOpts) + Object.assign( + /** @type { const } */ ({ encoding: 'utf-8', cwd }), + spawnOpts + ) ); } +/** + * @param { string } message + * @param { import('child_process').SpawnSyncReturns } runResult + * @returns { string } + */ function getDetailedMessage(message, runResult) { return ( message + @@ -37,6 +50,10 @@ function getDetailedMessage(message, runResult) { ); } +/** + * @param { import('child_process').SpawnSyncReturns } runResult + * @returns { void } + */ function assertTestSuccess(runResult) { const msg = 'Expected success (exit code ' + @@ -50,6 +67,10 @@ function assertTestSuccess(runResult) { ); } +/** + * @param { import('child_process').SpawnSyncReturns } runResult + * @returns { void } + */ function assertTestErrored(runResult) { const msg = 'Expected error (exit code ' + @@ -63,6 +84,10 @@ function assertTestErrored(runResult) { ); } +/** + * @param { import('child_process').SpawnSyncReturns } runResult + * @returns { void } + */ function assertTestFailure(runResult) { const msg = 'Expected failure (exit code >= ' + @@ -70,11 +95,16 @@ function assertTestFailure(runResult) { '), but got ' + runResult.status; assert.ok( - runResult.status >= resultFailureThreshold, + runResult.status !== null && runResult.status >= resultFailureThreshold, getDetailedMessage(msg, runResult) ); } +/** + * @param { import('../lib/Report').Report} reporter + * @param { import('child_process').SpawnSyncReturns } runResult + * @returns { void } + */ function assertDistributionShown(reporter, runResult) { const msg = getDetailedMessage( 'Expected to show distribution table', @@ -95,6 +125,11 @@ function assertDistributionShown(reporter, runResult) { } } +/** + * @param { import('../lib/Report').Report} reporter + * @param { import('child_process').SpawnSyncReturns } runResult + * @returns { void } + */ function assertDistributionNotShown(reporter, runResult) { const msg = getDetailedMessage( 'Expected to show distribution table', @@ -115,6 +150,10 @@ function assertDistributionNotShown(reporter, runResult) { } } +/** + * @param { string } dir + * @returns { Array } + */ function readdir(dir) { return fs .readdirSync(dir) @@ -309,6 +348,7 @@ describe('Testing elm-test on single Elm files', () => { describe('Distribution report tests', () => { const cwd = fixturesDir; + /** @type { Record> } */ const distributionReportFiles = { console: { 'ReportDistributionPassing.elm': { showDistribution: true }, @@ -333,7 +373,10 @@ describe('Distribution report tests', () => { }, }; - for (const [reporter, tests] of Object.entries(distributionReportFiles)) { + for (const [reporterKey, tests] of Object.entries(distributionReportFiles)) { + const reporter = /** @type { import('../lib/Report').Report } */ ( + reporterKey + ); for (const [test, { showDistribution }] of Object.entries(tests)) { const testFile = path.join(cwd, 'tests', 'Distribution', test); it(`Distribution report test for test: ${test}, reporter: ${reporter}`, () => { diff --git a/tests/flags.js b/tests/flags.js index 9b49751c..2a8bc350 100644 --- a/tests/flags.js +++ b/tests/flags.js @@ -16,6 +16,10 @@ const elmTestPath = path.join(rootDir, 'bin', 'elm-test'); const scratchDir = path.join(fixturesDir, 'scratch'); const scratchElmJsonPath = path.join(scratchDir, 'elm.json'); +/** + * @param { Array } args + * @param { (code: number | null) => void } callback + */ function elmTestWithYes(args, callback) { const child = spawn( elmTestPath, @@ -23,6 +27,11 @@ function elmTestWithYes(args, callback) { Object.assign({ encoding: 'utf-8', cwd: scratchDir }, spawnOpts) ); + if (child.stdin === null) { + throw new Error('child.stdin is null!'); + } + + // @ts-expect-error TypeScript claims this method does not exist, but the code runs and the tests pass. child.stdin.setEncoding('utf-8'); child.stdin.write(os.EOL); child.stdin.end(); @@ -31,14 +40,28 @@ function elmTestWithYes(args, callback) { }); } +/** + * @param { Array } args + * @param { string } [cwd] + * @param { import('child_process').SpawnOptions } extraOpts + * @returns + */ function execElmTest(args, cwd = fixturesDir, extraOpts = {}) { return spawn.sync( elmTestPath, args, - Object.assign({ encoding: 'utf-8', cwd }, spawnOpts, extraOpts) + Object.assign( + /** @type { const } */ ({ encoding: 'utf-8', cwd }), + spawnOpts, + extraOpts + ) ); } +/** + * @param { string } dirPath + * @returns { void } + */ function rimraf(dirPath) { // We can replace this with just `fs.rmSync(dirPath, { recursive: true, force: true })` // when Node.js 12 is EOL 2022-04-30 and support for Node.js 12 is dropped. @@ -48,20 +71,34 @@ function rimraf(dirPath) { if (fs.rmSync !== undefined) { fs.rmSync(dirPath, { recursive: true, force: true }); } else if (fs.existsSync(dirPath)) { + // @ts-expect-error TypeScript says that `rmdirSync` only takes one argument – because + // legacy versions of Node.js do – but we need to support those. fs.rmdirSync(dirPath, { recursive: true }); } } +/** + * @param { string } dirPath + * @returns { void } + */ function ensureEmptyDir(dirPath) { rimraf(dirPath); fs.mkdirSync(dirPath, { recursive: true }); } +/** + * @param { string } filePath + * @returns { void } + */ function touch(filePath) { const now = new Date(); fs.utimesSync(filePath, now, now); } +/** + * @param { string } filePath + * @returns { any } + */ function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } @@ -169,7 +206,11 @@ describe('flags', () => { elmTestPath, ['install', "elm/regex; printf 'FINDME'; printf 'TWICE'"], Object.assign( - { encoding: 'utf-8', input: 'y\n', cwd: fixturesDir }, + /** @type { const } */ ({ + encoding: 'utf-8', + input: 'y\n', + cwd: fixturesDir, + }), spawnOpts ) ); @@ -485,6 +526,10 @@ describe('flags', () => { Object.assign({ encoding: 'utf-8', cwd: fixturesDir }, spawnOpts) ); + if (child.stdout === null) { + throw new Error('child.stdout is null!'); + } + child.on('close', (code, signal) => { // don't send error when killed after test passed if (code !== null || signal !== 'SIGTERM') { @@ -563,6 +608,14 @@ describe('flags', () => { Object.assign({ encoding: 'utf-8', cwd: scratchDir }, spawnOpts) ); + if (child.stdout === null) { + throw new Error('child.stdout is null!'); + } + + if (child.stderr === null) { + throw new Error('child.stderr is null!'); + } + child.on('close', (code, signal) => { // don't send error when killed after test passed if (code !== null || signal !== 'SIGTERM') { @@ -634,7 +687,12 @@ describe('flags', () => { // Run with a constant seed so we can compare outputs (the seed is printed). const baseArgs = ['--seed=1', path.join('tests', 'Passing', 'One.elm')]; - // Replace printed duration with a fixed value so we can compare outputs. + /** + * Replace printed duration with a fixed value so we can compare outputs. + * + * @param { string } string + * @returns { string } + */ const fixDuration = (string) => string.replace(/\d+ ms/g, '123 ms'); // This has no colors because in the tests `elm-test` is not connected to a terminal. @@ -649,6 +707,12 @@ describe('flags', () => { 'colorFlag.stdout should have color' ); + /** + * @param { string } name + * @param { Array } args + * @param { Record } env + * @returns { void } + */ const shouldNotHaveColor = (name, args, env) => { const runResult = execElmTest(baseArgs.concat(args), fixturesDir, { env: Object.assign({}, spawnOpts.env, env), @@ -661,6 +725,12 @@ describe('flags', () => { ); }; + /** + * @param { string } name + * @param { Array } args + * @param { Record } env + * @returns { void } + */ const shouldHaveColor = (name, args, env) => { const runResult = execElmTest(baseArgs.concat(args), fixturesDir, { env: Object.assign({}, spawnOpts.env, env), diff --git a/tests/test-quality.js b/tests/test-quality.js index 0dd5555d..c404d1a6 100644 --- a/tests/test-quality.js +++ b/tests/test-quality.js @@ -7,19 +7,35 @@ const spawn = require('cross-spawn'); const { spawnOpts } = require('./util'); +/** + * @param { Array } args + * @param { string } cwd + * @returns { import('child_process').SpawnSyncReturns } + */ function execElmJson(args, cwd) { return spawn.sync( 'elm-json', args, - Object.assign({ encoding: 'utf-8', cwd: cwd }, spawnOpts) + Object.assign( + /** @type { const } */ ({ encoding: 'utf-8', cwd: cwd }), + spawnOpts + ) ); } +/** + * @param { Array } args + * @param { string } cwd + * @returns { import('child_process').SpawnSyncReturns } + */ function execElm(args, cwd) { return spawn.sync( 'elm', args, - Object.assign({ encoding: 'utf-8', cwd: cwd }, spawnOpts) + Object.assign( + /** @type { const } */ ({ encoding: 'utf-8', cwd: cwd }), + spawnOpts + ) ); } diff --git a/tests/util.js b/tests/util.js index cfead24b..1549e0c2 100644 --- a/tests/util.js +++ b/tests/util.js @@ -3,7 +3,7 @@ const path = require('path'); const fixturesDir = path.join(__dirname, 'fixtures'); const dummyBinPath = path.join(fixturesDir, 'dummy-bin'); -const newPath = process.env.PATH + path.delimiter + dummyBinPath; +const newPath = process.env['PATH'] + path.delimiter + dummyBinPath; const spawnOpts = { silent: true, env: Object.assign({}, process.env, { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..db9aea79 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "lib": ["esnext"], + "types": ["node", "mocha"], + "noUncheckedIndexedAccess": false, // Disabled during the migration from Flow to TypeScript. + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "useUnknownInCatchVariables": false, // Disabled during the migration from Flow to TypeScript. + "allowJs": true, + "checkJs": true, + "noEmit": true, + }, + "include": ["lib/**/*", "tests/**/*"], + "exclude": ["tests/fixtures/**/*"], +}