diff --git a/lib/cli.js b/lib/cli.js index e89ecc19..19664d5d 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -36,7 +36,7 @@ exports.run = async function () { settings.lintingPath = process.cwd(); if (settings.coverage) { - Modules.coverage.instrument(settings); + await Modules.coverage.instrument(settings); } else if (settings.transform) { Modules.transform.install(settings); diff --git a/lib/index.js b/lib/index.js index 7cf3875e..a8ea4fdd 100755 --- a/lib/index.js +++ b/lib/index.js @@ -21,7 +21,6 @@ for (const module of ['coverage', 'leaks', 'types']) { Object.defineProperty(exports, module, Object.getOwnPropertyDescriptor(Modules, module)); } - /* experiment('Utilities', () => { diff --git a/lib/linter/index.js b/lib/linter/index.js index b4c156e9..0c476d53 100755 --- a/lib/linter/index.js +++ b/lib/linter/index.js @@ -10,7 +10,7 @@ const Hoek = require('@hapi/hoek'); const internals = {}; -exports.lint = function () { +exports.lint = async function () { const configuration = { ignore: true @@ -24,7 +24,7 @@ exports.lint = function () { !Fs.existsSync('.eslintrc.yml') && !Fs.existsSync('.eslintrc.json') && !Fs.existsSync('.eslintrc')) { - configuration.configFile = Path.join(__dirname, '.eslintrc.js'); + configuration.overrideConfigFile = Path.join(__dirname, '.eslintrc.js'); } if (options) { @@ -33,17 +33,14 @@ exports.lint = function () { let results; try { - const engine = new Eslint.CLIEngine(configuration); - results = engine.executeOnFiles(['.']); + const eslint = new Eslint.ESLint(configuration); + results = await eslint.lintFiles(['.']); } catch (ex) { - results = { - results: [{ messages: [ex] }] - }; + results = [{ messages: [ex] }]; } - - return results.results.map((result) => { + return results.map((result) => { const transformed = { filename: result.filePath @@ -68,4 +65,10 @@ exports.lint = function () { }); }; -process.send(exports.lint()); +const main = async () => { + + const results = await exports.lint(); + process.send(results); +}; + +main(); diff --git a/lib/modules/coverage.js b/lib/modules/coverage.js index b42c61d2..ab2da633 100755 --- a/lib/modules/coverage.js +++ b/lib/modules/coverage.js @@ -18,7 +18,7 @@ const Transform = require('./transform'); const internals = { ext: Symbol.for('@hapi/lab/coverage/initialize'), _state: Symbol.for('@hapi/lab/coverage/_state'), - EslintEngine: new ESLint.CLIEngine({ baseConfig: Eslintrc }) + eslint: new ESLint.ESLint({ baseConfig: Eslintrc }) }; @@ -69,7 +69,7 @@ if (typeof global.__$$labCov === 'undefined') { // $lab:coverage:on$ -exports.instrument = function (options) { +exports.instrument = async function (options) { if (options['coverage-module']) { for (const name of options['coverage-module']) { @@ -77,8 +77,10 @@ exports.instrument = function (options) { } } + const ctx = await internals.context(options); + internals.state.patterns.unshift(internals.pattern(options)); - Transform.install(options, internals.prime); + Transform.install(options, (ext) => internals.prime(ext, ctx)); }; @@ -114,7 +116,7 @@ internals.escape = function (string) { }; -internals.prime = function (extension) { +internals.prime = function (extension, ctx) { require.extensions[extension] = function (localModule, filename) { @@ -122,7 +124,7 @@ internals.prime = function (extension) { if (Path.basename(filename, extension) !== '.eslintrc') { for (let i = 0; i < internals.state.patterns.length; ++i) { if (internals.state.patterns[i].test(filename.replace(/\\/g, '/'))) { - return localModule._compile(internals.instrument(filename), filename); + return localModule._compile(internals.instrument(filename, ctx), filename); } } } @@ -133,7 +135,7 @@ internals.prime = function (extension) { }; -internals.instrument = function (filename) { +internals.instrument = function (filename, ctx) { filename = filename.replace(/\\/g, '/'); @@ -331,9 +333,8 @@ internals.instrument = function (filename) { // Parse tree - const eslintConfig = internals.EslintEngine.getConfigForFile(filename); const tree = ESLintParser.parse(content, { - ...eslintConfig.parserOptions, + ...ctx.parserOptions, loc: true, range: true, comment: true @@ -520,6 +521,7 @@ exports.analyze = async function (options) { const report = internals.state.files; const pattern = internals.pattern(options); + const ctx = await internals.context(options); const cov = { sloc: 0, @@ -537,7 +539,7 @@ exports.analyze = async function (options) { const filename = file.replace(/\\/g, '/'); if (pattern.test(filename)) { if (!report[filename]) { - internals.instrument(filename); + internals.instrument(filename, ctx); } report[filename].source = internals.state.sources[filename] || []; @@ -778,3 +780,14 @@ internals.external = function (filename) { return reports.length ? reports : null; }; + +internals.context = async (options) => { + + // The parserOptions are shared by all files for coverage purposes, based on + // the effective eslint config for a hypothetical file {coveragePath}/x.js + const { parserOptions } = await internals.eslint.calculateConfigForFile( + Path.join(options.coveragePath || '', 'x.js') + ); + + return { parserOptions }; +}; diff --git a/lib/modules/typescript.js b/lib/modules/typescript.js index 37497495..364be6f4 100755 --- a/lib/modules/typescript.js +++ b/lib/modules/typescript.js @@ -13,14 +13,15 @@ internals.transform = function (content, fileName) { if (!internals.configs.has(configFile)) { try { var { config, error } = Typescript.readConfigFile(configFile, Typescript.sys.readFile); - if (error) { - throw new Error(`TypeScript config error in ${configFile}: ${error.messageText}`); - } } catch (err) { throw new Error(`Cannot find a tsconfig file for ${fileName}`); } + if (error) { + throw new Error(`TypeScript config error in ${configFile}: ${error.messageText}`); + } + const { options } = Typescript.parseJsonConfigFileContent(config, Typescript.sys, Typescript.getDirectoryPath(configFile), {}, configFile); options.sourceMap = false; options.inlineSourceMap = true; diff --git a/package.json b/package.json index 2731733f..27918ba5 100755 --- a/package.json +++ b/package.json @@ -19,13 +19,13 @@ ] }, "dependencies": { - "@babel/core": "^7.14.3", - "@babel/eslint-parser": "^7.14.3", + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.0", "@hapi/bossy": "5.x.x", "@hapi/eslint-plugin": "^5.1.0", "@hapi/hoek": "9.x.x", "diff": "^5.0.0", - "eslint": "7.x.x", + "eslint": "8.x.x", "find-rc": "4.x.x", "globby": "10.x.x", "handlebars": "4.x.x", diff --git a/test/coverage.js b/test/coverage.js index e63b142d..c0a88e77 100755 --- a/test/coverage.js +++ b/test/coverage.js @@ -29,6 +29,7 @@ const internals = { const lab = exports.lab = _Lab.script(); +const before = lab.before; const describe = lab.describe; const it = lab.it; const expect = Code.expect; @@ -36,7 +37,10 @@ const expect = Code.expect; describe('Coverage', () => { - Lab.coverage.instrument({ coveragePath: Path.join(__dirname, 'coverage'), coverageExclude: 'exclude' }); + before({}, async () => { + + await Lab.coverage.instrument({ coveragePath: Path.join(__dirname, 'coverage'), coverageExclude: 'exclude' }); + }); it('computes sloc without comments', async () => { @@ -132,7 +136,7 @@ describe('Coverage', () => { expect(cov.percent).to.equal(100); }); - it('logs to stderr when coverageExclude file has fs.stat issue', async () => { + it('logs to stderr when coverageExclude file has fs.stat issue', async (flags) => { const Test = require('./coverage/test-folder/test-name.js'); @@ -141,8 +145,15 @@ describe('Coverage', () => { const origStatSync = Fs.statSync; const origErrorLog = console.error; + let calls = 0; Fs.statSync = () => { + calls++; + if (calls === 3) { + // Only mock for first 3 calls + Fs.statSync = origStatSync; + } + const err = new Error(); err.code = 'BOOM'; throw err; @@ -153,9 +164,13 @@ describe('Coverage', () => { expect(data.code).to.equal('BOOM'); }; + flags.onCleanup = () => { + + Fs.statSync = origStatSync; + console.error = origErrorLog; + }; + const cov = await Lab.coverage.analyze({ coveragePath: Path.join(__dirname, 'coverage/test-folder'), coverageExclude: ['test', 'node_modules', 'test-name.js'] }); - Fs.statSync = origStatSync; - console.error = origErrorLog; expect(cov.percent).to.equal(100); }); @@ -577,7 +592,7 @@ describe('Coverage', () => { it('reports external coverage', async () => { const coveragePath = Path.join(__dirname, 'coverage/module'); - Lab.coverage.instrument({ coveragePath, 'coverage-module': ['@hapi/lab-external-module-test'] }); + await Lab.coverage.instrument({ coveragePath, 'coverage-module': ['@hapi/lab-external-module-test'] }); require(coveragePath); @@ -615,12 +630,12 @@ describe('Coverage', () => { describe('Coverage via Transform API', () => { - lab.before(() => { + lab.before(async () => { internals.js = require.extensions['.js']; internals.inl = require.extensions['.inl']; - Lab.coverage.instrument({ coveragePath: Path.join(__dirname, 'coverage'), coverageExclude: 'exclude', transform: internals.transform }); + await Lab.coverage.instrument({ coveragePath: Path.join(__dirname, 'coverage'), coverageExclude: 'exclude', transform: internals.transform }); }); lab.after(() => { diff --git a/test/reporters.js b/test/reporters.js index 8c1ada37..a98af60e 100755 --- a/test/reporters.js +++ b/test/reporters.js @@ -20,6 +20,7 @@ const internals = {}; const lab = exports.lab = _Lab.script(); +const before = lab.before; const describe = lab.describe; const it = lab.it; const expect = Code.expect; @@ -27,7 +28,10 @@ const expect = Code.expect; describe('Reporter', () => { - Lab.coverage.instrument({ coveragePath: Path.join(__dirname, './coverage/'), coverageExclude: 'exclude' }); + before({}, async () => { + + await Lab.coverage.instrument({ coveragePath: Path.join(__dirname, './coverage/'), coverageExclude: 'exclude' }); + }); it('outputs to a stream', async () => { diff --git a/test/transform.js b/test/transform.js index 16d7c75b..647cb679 100755 --- a/test/transform.js +++ b/test/transform.js @@ -38,13 +38,17 @@ const internals = { // Test shortcuts const lab = exports.lab = _Lab.script(); +const before = lab.before; const describe = lab.describe; const it = lab.it; const expect = Code.expect; describe('Transform', () => { - Lab.coverage.instrument({ coveragePath: Path.join(__dirname, './transform/'), coverageExclude: 'exclude', transform: internals.transform }); + before({}, async () => { + + await Lab.coverage.instrument({ coveragePath: Path.join(__dirname, './transform/'), coverageExclude: 'exclude', transform: internals.transform }); + }); it('instruments and measures coverage', async () => { diff --git a/test/typescript.js b/test/typescript.js index 40e702ea..bccfe736 100755 --- a/test/typescript.js +++ b/test/typescript.js @@ -6,6 +6,7 @@ const Path = require('path'); const Code = require('@hapi/code'); const _Lab = require('../test_runner'); const RunCli = require('./run_cli'); +const Ts = require('typescript'); const Typescript = require('../lib/modules/typescript'); @@ -53,6 +54,31 @@ describe('TypeScript', () => { describe('transform', () => { + it('errors when failing to find a tsconfig file', () => { + + const path = Path.join(__dirname, 'cli', 'simple.js'); + + expect( + () => Typescript.extensions[0].transform(Fs.readFileSync(path, { encoding: 'utf8' }), path) + ).to.throw(/^Cannot find a tsconfig file for .+cli[\/\\]simple\.js/); + }); + + it('errors when unable to read a tsconfig file', (flags) => { + + const path = Path.join(__dirname, 'cli_typescript', 'simple.ts'); + + const origReadFile = Ts.sys.readFile; + flags.onCleanup = () => Object.assign(Ts.sys, { readFile: origReadFile }); + Ts.sys.readFile = () => { + + throw new Error('Oops!'); + }; + + expect( + () => Typescript.extensions[0].transform(Fs.readFileSync(path, { encoding: 'utf8' }), path) + ).to.throw(/^TypeScript config error in .+?cli_typescript\/tsconfig\.json: Cannot read file \'.+?\/cli_typescript\/tsconfig\.json\': Oops!/); + }); + it('generates embedded sourcemap with sourcesContent', () => { const smre = /\/\/\#\s*sourceMappingURL=data:application\/json[^,]+base64,(.*)\r?\n?$/; @@ -65,5 +91,12 @@ describe('TypeScript', () => { expect(sourcemap.sourcesContent).to.exist(); expect(sourcemap.sourcesContent).to.have.length(1); }); + + it('transforms identically when called multiple times', () => { + // This covers config file caching behavior, which is not directly observable by consumers. + const path = Path.join(__dirname, 'cli_typescript', 'simple.ts'); + const transform = () => Typescript.extensions[0].transform(Fs.readFileSync(path, { encoding: 'utf8' }), path); + expect(transform()).to.equal(transform()); + }); }); });