From 383fa22f10b5805f6970eeaa219bb132f2fec457 Mon Sep 17 00:00:00 2001 From: Chris Breiding Date: Mon, 24 Aug 2020 17:02:26 -0400 Subject: [PATCH] fix: Resolve tsconfig.json for plugins file from the plugins directory (#8377) --- .../server/lib/plugins/child/run_plugins.js | 11 +++- packages/server/lib/plugins/preprocessor.js | 5 +- packages/server/lib/project.js | 5 -- .../server/lib/{plugins => util}/resolve.js | 14 ++--- packages/server/lib/util/ts-node.js | 19 ++++--- .../test/e2e/1_typescript_plugins_spec.ts | 7 +++ .../test/integration/http_requests_spec.js | 2 +- .../ts-proj-tsconfig-in-plugins/cypress.json | 3 ++ .../cypress/integration/passing_spec.ts | 3 ++ .../cypress/plugins/index.ts | 6 +++ .../cypress/plugins/tsconfig.json | 5 ++ .../projects/ts-proj/cypress/plugins/index.ts | 2 +- .../fixtures/projects/ts-proj/tsconfig.json | 1 + .../unit/plugins/child/run_plugins_spec.js | 42 ++++++++++++++- packages/server/test/unit/project_spec.js | 52 ------------------- 15 files changed, 95 insertions(+), 82 deletions(-) rename packages/server/lib/{plugins => util}/resolve.js (67%) create mode 100644 packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress.json create mode 100644 packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/integration/passing_spec.ts create mode 100644 packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/index.ts create mode 100644 packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/tsconfig.json create mode 100644 packages/server/test/support/fixtures/projects/ts-proj/tsconfig.json diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index 894ad24a2331..5ce9151df815 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -157,7 +157,7 @@ const execute = (ipc, event, ids, args = []) => { let tsRegistered = false -module.exports = (ipc, pluginsFile, projectRoot) => { +const runPlugins = (ipc, pluginsFile, projectRoot) => { debug('pluginsFile:', pluginsFile) debug('project root:', projectRoot) if (!projectRoot) { @@ -181,7 +181,7 @@ module.exports = (ipc, pluginsFile, projectRoot) => { }) if (!tsRegistered) { - registerTsNode(projectRoot) + registerTsNode(projectRoot, pluginsFile) // ensure typescript is only registered once tsRegistered = true @@ -219,3 +219,10 @@ module.exports = (ipc, pluginsFile, projectRoot) => { execute(ipc, event, ids, args) }) } + +// for testing purposes +runPlugins.__reset = () => { + tsRegistered = false +} + +module.exports = runPlugins diff --git a/packages/server/lib/plugins/preprocessor.js b/packages/server/lib/plugins/preprocessor.js index 5ea1ba77cca3..8568baeb3be9 100644 --- a/packages/server/lib/plugins/preprocessor.js +++ b/packages/server/lib/plugins/preprocessor.js @@ -7,7 +7,7 @@ const debug = require('debug')('cypress:server:preprocessor') const Promise = require('bluebird') const appData = require('../util/app_data') const plugins = require('../plugins') -const resolve = require('./resolve') +const resolve = require('../util/resolve') const errorMessage = function (err = {}) { return (err.stack || err.annotated || err.message || err.toString()) @@ -46,8 +46,7 @@ const createPreprocessor = function (options) { const setDefaultPreprocessor = function (config) { debug('set default preprocessor') - const tsPath = resolve.typescript(config) - + const tsPath = resolve.typescript(config.projectRoot) const options = { typescript: tsPath, } diff --git a/packages/server/lib/project.js b/packages/server/lib/project.js index bde157744fbb..1d9e598ea28a 100644 --- a/packages/server/lib/project.js +++ b/packages/server/lib/project.js @@ -29,7 +29,6 @@ const keys = require('./util/keys') const settings = require('./util/settings') const specsUtil = require('./util/specs') const { escapeFilenameInUrl } = require('./util/escape_filename') -const { registerTsNode } = require('./util/ts-node') const localCwd = cwd() @@ -100,10 +99,6 @@ class Project extends EE { return scaffold.plugins(path.dirname(cfg.pluginsFile), cfg) } - }).then((cfg) => { - registerTsNode(this.projectRoot) - - return cfg }).then((cfg) => { return this._initPlugins(cfg, options) .then((modifiedCfg) => { diff --git a/packages/server/lib/plugins/resolve.js b/packages/server/lib/util/resolve.js similarity index 67% rename from packages/server/lib/plugins/resolve.js rename to packages/server/lib/util/resolve.js index ddc216644b7a..3e241644606a 100644 --- a/packages/server/lib/plugins/resolve.js +++ b/packages/server/lib/util/resolve.js @@ -1,26 +1,22 @@ const resolve = require('resolve') -const env = require('../util/env') +const env = require('./env') const debug = require('debug')('cypress:server:plugins') module.exports = { /** * Resolves the path to 'typescript' module. * - * @param {Config} cypress config object + * @param {projectRoot} path to the project root * @returns {string|null} path if typescript exists, otherwise null */ - typescript: (config) => { - if (env.get('CYPRESS_INTERNAL_NO_TYPESCRIPT') === '1') { + typescript: (projectRoot) => { + if (env.get('CYPRESS_INTERNAL_NO_TYPESCRIPT') === '1' || !projectRoot) { return null } try { const options = { - basedir: config.projectRoot, - } - - if (!config.projectRoot) { - throw new Error('Config is missing projet root') + basedir: projectRoot, } debug('resolving typescript with options %o', options) diff --git a/packages/server/lib/util/ts-node.js b/packages/server/lib/util/ts-node.js index fa81234c9b24..d44d07c4c0df 100644 --- a/packages/server/lib/util/ts-node.js +++ b/packages/server/lib/util/ts-node.js @@ -1,23 +1,28 @@ const debug = require('debug')('cypress:server:ts-node') +const path = require('path') const tsnode = require('ts-node') -const resolve = require('resolve') +const resolve = require('./resolve') -const getTsNodeOptions = (tsPath) => { +const getTsNodeOptions = (tsPath, pluginsFile) => { return { compiler: tsPath, // use the user's installed typescript compilerOptions: { module: 'CommonJS', }, + // resolves tsconfig.json starting from the plugins directory + // instead of the cwd (the project root) + dir: path.dirname(pluginsFile), transpileOnly: true, // transpile only (no type-check) for speed } } -const registerTsNode = (projectRoot) => { +const registerTsNode = (projectRoot, pluginsFile) => { try { - const tsPath = resolve.sync('typescript', { - basedir: projectRoot, - }) - const tsOptions = getTsNodeOptions(tsPath) + const tsPath = resolve.typescript(projectRoot) + + if (!tsPath) return + + const tsOptions = getTsNodeOptions(tsPath, pluginsFile) debug('typescript path: %s', tsPath) debug('registering project TS with options %o', tsOptions) diff --git a/packages/server/test/e2e/1_typescript_plugins_spec.ts b/packages/server/test/e2e/1_typescript_plugins_spec.ts index 5954530e9367..17db9bcd1e69 100644 --- a/packages/server/test/e2e/1_typescript_plugins_spec.ts +++ b/packages/server/test/e2e/1_typescript_plugins_spec.ts @@ -23,4 +23,11 @@ describe('e2e typescript in plugins file', function () { project: Fixtures.projectPath('ts-proj-esmoduleinterop-true'), }) }) + + // https://github.com/cypress-io/cypress/issues/8359 + it('loads tsconfig.json from plugins directory', function () { + return e2e.exec(this, { + project: Fixtures.projectPath('ts-proj-tsconfig-in-plugins'), + }) + }) }) diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 63c925783132..4f7b000f1ea4 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -23,7 +23,7 @@ const Project = require(`${root}lib/project`) const Watchers = require(`${root}lib/watchers`) const pluginsModule = require(`${root}lib/plugins`) const preprocessor = require(`${root}lib/plugins/preprocessor`) -const resolve = require(`${root}lib/plugins/resolve`) +const resolve = require(`${root}lib/util/resolve`) const fs = require(`${root}lib/util/fs`) const glob = require(`${root}lib/util/glob`) const CacheBuster = require(`${root}lib/util/cache_buster`) diff --git a/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress.json b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress.json new file mode 100644 index 000000000000..0c2bdde8665b --- /dev/null +++ b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress.json @@ -0,0 +1,3 @@ +{ + "supportFile": false +} diff --git a/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/integration/passing_spec.ts b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/integration/passing_spec.ts new file mode 100644 index 000000000000..99a13400edf9 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/integration/passing_spec.ts @@ -0,0 +1,3 @@ +it('passes', () => { + expect(true).to.be.true +}) diff --git a/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/index.ts b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/index.ts new file mode 100644 index 000000000000..4b4b1affda01 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/index.ts @@ -0,0 +1,6 @@ +// this tests that the tsconfig.json is loaded from the plugins directory. +// if it isn't, the lack of "downlevelIteration" support will cause this to +// fail at runtime with "RangeError: Invalid array length" +[...Array(100).keys()].map((x) => `${x}`) + +export default () => {} diff --git a/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/tsconfig.json b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/tsconfig.json new file mode 100644 index 000000000000..6936fe051166 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/ts-proj-tsconfig-in-plugins/cypress/plugins/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "downlevelIteration": true + } +} diff --git a/packages/server/test/support/fixtures/projects/ts-proj/cypress/plugins/index.ts b/packages/server/test/support/fixtures/projects/ts-proj/cypress/plugins/index.ts index 7f27d46b4c1a..ae0a98c0cb83 100644 --- a/packages/server/test/support/fixtures/projects/ts-proj/cypress/plugins/index.ts +++ b/packages/server/test/support/fixtures/projects/ts-proj/cypress/plugins/index.ts @@ -1,6 +1,6 @@ /// -import fn from './commonjs-export-function' +import * as fn from './commonjs-export-function' // if esModuleInterop is forced to be true, this will error // with 'fn is // not a function'. instead, we allow the tsconfig.json to determine the value diff --git a/packages/server/test/support/fixtures/projects/ts-proj/tsconfig.json b/packages/server/test/support/fixtures/projects/ts-proj/tsconfig.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/ts-proj/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.js b/packages/server/test/unit/plugins/child/run_plugins_spec.js index 1510c427e523..bb34605afd76 100644 --- a/packages/server/test/unit/plugins/child/run_plugins_spec.js +++ b/packages/server/test/unit/plugins/child/run_plugins_spec.js @@ -2,11 +2,13 @@ require('../../../spec_helper') const _ = require('lodash') const snapshot = require('snap-shot-it') +const tsnode = require('ts-node') const preprocessor = require(`${root}../../lib/plugins/child/preprocessor`) const task = require(`${root}../../lib/plugins/child/task`) const runPlugins = require(`${root}../../lib/plugins/child/run_plugins`) const util = require(`${root}../../lib/plugins/util`) +const resolve = require(`${root}../../lib/util/resolve`) const browserUtils = require(`${root}../../lib/browsers/utils`) const Fixtures = require(`${root}../../test/support/helpers/fixtures`) @@ -31,8 +33,7 @@ describe('lib/plugins/child/run_plugins', () => { afterEach(() => { mockery.deregisterMock('plugins-file') - - return mockery.deregisterSubstitute('plugins-file') + mockery.deregisterSubstitute('plugins-file') }) it('sends error message if pluginsFile is missing', function () { @@ -77,6 +78,43 @@ describe('lib/plugins/child/run_plugins', () => { return snapshot(JSON.stringify(this.ipc.send.lastCall.args[3])) }) + describe('typescript registration', () => { + beforeEach(function () { + runPlugins.__reset() + + this.register = sinon.stub(tsnode, 'register') + sinon.stub(resolve, 'typescript').returns('/path/to/typescript.js') + }) + + it('registers ts-node if typescript is installed', function () { + runPlugins(this.ipc, '/path/to/plugins/file.js', 'proj-root') + + expect(this.register).to.be.calledWith({ + transpileOnly: true, + compiler: '/path/to/typescript.js', + dir: '/path/to/plugins', + compilerOptions: { + module: 'CommonJS', + }, + }) + }) + + it('only registers ts-node once', function () { + runPlugins(this.ipc, '/path/to/plugins/file.js', 'proj-root') + runPlugins(this.ipc, '/path/to/plugins/file.js', 'proj-root') + + expect(this.register).to.be.calledOnce + }) + + it('does not register ts-node if typescript is not installed', function () { + resolve.typescript.returns(null) + + runPlugins(this.ipc, '/path/to/plugins/file.js', 'proj-root') + + expect(this.register).not.to.be.called + }) + }) + describe('on \'load\' message', () => { it('sends error if pluginsFile function rejects the promise', function (done) { const err = new Error('foo') diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js index 1cae5ac3428c..f107fcc09ef0 100644 --- a/packages/server/test/unit/project_spec.js +++ b/packages/server/test/unit/project_spec.js @@ -3,7 +3,6 @@ require('../spec_helper') const mockedEnv = require('mocked-env') const path = require('path') const commitInfo = require('@cypress/commit-info') -const tsnode = require('ts-node') const Fixtures = require('../support/helpers/fixtures') const api = require(`${root}lib/api`) const user = require(`${root}lib/user`) @@ -316,57 +315,6 @@ This option will not have an effect in Some-other-name. Tests that rely on web s expect(config).ok }) }) - - describe('out-of-the-box typescript setup', () => { - const tsProjPath = Fixtures.projectPath('ts-installed') - // Root path is used because resolve finds server typescript path when we use a project under `suppert/projects` folder. - const rootPath = path.join(__dirname, '../../../../..') - const projTsPath = path.join(tsProjPath, 'node_modules/typescript/index.js') - - let cfg - - beforeEach(() => { - return config.get(tsProjPath, {}) - .then((c) => { - cfg = c - }) - }) - - const setupProject = (typescript, projectRoot) => { - const proj = new Project(projectRoot) - - sinon.stub(proj, 'watchSettingsAndStartWebsockets').resolves() - sinon.stub(proj, 'checkSupportFile').resolves() - sinon.stub(proj, 'scaffold').resolves() - sinon.stub(proj, 'getConfig').resolves({ ...cfg, typescript }) - - const register = sinon.stub(tsnode, 'register') - - return { proj, register } - } - - it('ts installed', () => { - const { proj, register } = setupProject('default', tsProjPath) - - return proj.open().then(() => { - expect(register).to.be.calledWith({ - transpileOnly: true, - compiler: projTsPath, - compilerOptions: { - module: 'CommonJS', - }, - }) - }) - }) - - it('ts not installed', () => { - const { proj, register } = setupProject('default', rootPath) - - return proj.open().then(() => { - expect(register).not.called - }) - }) - }) }) context('#close', () => {