Skip to content

Commit

Permalink
fix: Typescript config invalid with node v22.7.0 with ESM (#30099)
Browse files Browse the repository at this point in the history
* add failing system binary test [run ci]

* fix: leverage the --no-experimental-detect-module when node 22.7.0 and above is detected [run ci]

* Update cli/CHANGELOG.md

* use environment variable to mock stubbing out of typescript install in order to unit test as mocking the module seems near impossible to do correctly given the context [run ci]

* update changelog to include mention of experimental-detect-module

* make sure node version is set before comparing versions

* Revert "update changelog to include mention of experimental-detect-module"

This reverts commit 5ef8ef0.

---------

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
  • Loading branch information
AtofStryker and jennifer-shehane committed Aug 27, 2024
1 parent 0480480 commit 5a6b8c4
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 19 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ _Released 8/27/2024 (PENDING)_
**Bugfixes:**

- Fixed an issue where files outside the Cypress project directory were not calculating the bundle output path correctly for the `file:preprocessor`. Addresses [#8599](https://github.com/cypress-io/cypress/issues/8599).
- Fixed an issue where Cypress would not run if Node.js version `22.7.0` was being used with TypeScript and ES Modules. Fixes [#30084](https://github.com/cypress-io/cypress/issues/30084).
- Correctly determines current browser family when choosing between `unload` and `pagehide` options in App Runner. Fixes [#29880](https://github.com/cypress-io/cypress/issues/29880).

**Misc:**
Expand Down
12 changes: 11 additions & 1 deletion packages/data-context/src/data/ProjectConfigIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { autoBindDebug, hasTypeScriptInstalled, toPosix } from '../util'
import _ from 'lodash'
import { pathToFileURL } from 'url'
import os from 'os'
import semver from 'semver'
import type { OTLPTraceExporterCloud } from '@packages/telemetry'
import { telemetry, encodeTelemetryContext } from '@packages/telemetry'

Expand Down Expand Up @@ -58,6 +59,7 @@ export class ProjectConfigIpc extends EventEmitter {

constructor (
readonly nodePath: string | undefined | null,
readonly nodeVersion: string | undefined | null,
readonly projectRoot: string,
readonly configFilePath: string,
readonly configFile: string | false,
Expand Down Expand Up @@ -301,7 +303,15 @@ export class ProjectConfigIpc extends EventEmitter {
// best option that leverages the existing modules we bundle in the binary.
// @see ts-node esm loader https://typestrong.org/ts-node/docs/usage/#node-flags-and-other-tools
// @see Node.js Loader API https://nodejs.org/api/esm.html#customizing-esm-specifier-resolution-algorithm
const tsNodeEsmLoader = `--experimental-specifier-resolution=node --loader ${tsNodeEsm}`
let tsNodeEsmLoader = `--experimental-specifier-resolution=node --loader ${tsNodeEsm}`

// in nodejs 22.7.0, the --experimental-detect-module option is now enabled by default.
// We need to disable it with the --no-experimental-detect-module flag.
// @see https://github.com/cypress-io/cypress/issues/30084
if (this.nodeVersion && semver.gte(this.nodeVersion, '22.7.0')) {
debug(`detected node version ${this.nodeVersion}, adding --no-experimental-detect-module option to child_process NODE_OPTIONS.`)
tsNodeEsmLoader = `${tsNodeEsmLoader} --no-experimental-detect-module`
}

if (childOptions.env.NODE_OPTIONS) {
childOptions.env.NODE_OPTIONS += ` ${tsNodeEsmLoader}`
Expand Down
1 change: 1 addition & 0 deletions packages/data-context/src/data/ProjectConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export class ProjectConfigManager {

this._eventsIpc = new ProjectConfigIpc(
this.options.ctx.coreData.app.nodePath,
this.options.ctx.coreData.app.nodeVersion,
this.options.projectRoot,
this.configFilePath,
this.options.configFile,
Expand Down
2 changes: 2 additions & 0 deletions packages/data-context/src/data/coreDataShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface AppDataShape {
browsers: ReadonlyArray<FoundBrowser> | null
projects: ProjectShape[]
nodePath: Maybe<string>
nodeVersion: Maybe<string>
browserStatus: BrowserStatus
browserUserAgent: string | null
relaunchBrowser: boolean
Expand Down Expand Up @@ -195,6 +196,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
browsers: null,
projects: [],
nodePath: modeOptions.userNodePath,
nodeVersion: modeOptions.userNodeVersion,
browserStatus: 'closed',
browserUserAgent: null,
relaunchBrowser: false,
Expand Down
6 changes: 6 additions & 0 deletions packages/data-context/src/util/hasTypescript.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export function hasTypeScriptInstalled (projectRoot: string) {
try {
// mocking this module is fairly difficult under unit test. We need to mock this for the ProjectConfigIpc unit tests
// as the scaffolded projects in the data-context package do not install dependencies related to the project.
if (process.env.CYPRESS_INTERNAL_MOCK_TYPESCRIPT_INSTALL === 'true') {
return true
}

require.resolve('typescript', { paths: [projectRoot] })

return true
Expand Down
144 changes: 126 additions & 18 deletions packages/data-context/test/unit/data/ProjectConfigIpc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,140 @@
import childProcess from 'child_process'
import { expect } from 'chai'
import sinon from 'sinon'
import { scaffoldMigrationProject as scaffoldProject } from '../helper'
import { ProjectConfigIpc } from '../../../src/data/ProjectConfigIpc'

describe('ProjectConfigIpc', () => {
let projectConfigIpc

beforeEach(async () => {
const projectPath = await scaffoldProject('e2e')

projectConfigIpc = new ProjectConfigIpc(
undefined,
projectPath,
'cypress.config.js',
false,
(error) => {},
() => {},
)
})
context('#eventProcessPid', () => {
let projectConfigIpc

afterEach(() => {
projectConfigIpc.cleanupIpc()
})
beforeEach(async () => {
const projectPath = await scaffoldProject('e2e')

projectConfigIpc = new ProjectConfigIpc(
undefined,
undefined,
projectPath,
'cypress.config.js',
false,
(error) => {},
() => {},
)
})

afterEach(() => {
projectConfigIpc.cleanupIpc()
})

context('#eventProcessPid', () => {
it('returns id for child process', () => {
const expectedId = projectConfigIpc._childProcess.pid

expect(projectConfigIpc.childProcessPid).to.eq(expectedId)
})
})

context('forkChildProcess', () => {
const NODE_VERSIONS = ['18.20.4', '20.17.0']
const NODE_VERSIONS_22_7_0_AND_UP = ['22.7.0', '22.11.4']

let projectConfigIpc
let forkSpy

beforeEach(() => {
process.env.CYPRESS_INTERNAL_MOCK_TYPESCRIPT_INSTALL = 'true'
forkSpy = sinon.spy(childProcess, 'fork')
})

afterEach(() => {
delete process.env.CYPRESS_INTERNAL_MOCK_TYPESCRIPT_INSTALL
forkSpy.restore()
projectConfigIpc.cleanupIpc()
})

context('typescript', () => {
[...NODE_VERSIONS, ...NODE_VERSIONS_22_7_0_AND_UP].forEach((nodeVersion) => {
context(`node v${nodeVersion}`, () => {
context('ESM', () => {
it('uses the experimental module loader if ESM is being used with typescript', async () => {
// @ts-expect-error
const projectPath = await scaffoldProject('config-cjs-and-esm/config-with-ts-module')

const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node`
const MOCK_NODE_VERSION = nodeVersion

projectConfigIpc = new ProjectConfigIpc(
MOCK_NODE_PATH,
MOCK_NODE_VERSION,
projectPath,
'cypress.config.js',
false,
(error) => {},
() => {},
)

expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
NODE_OPTIONS: sinon.match('--experimental-specifier-resolution=node --loader'),
},
}))
})

// @see https://github.com/cypress-io/cypress/issues/30084
// at time of writing, 22.11.4 is a node version that does not exist. We are using this version to test the logic for future proofing.
if (NODE_VERSIONS_22_7_0_AND_UP.includes(nodeVersion)) {
it(`additionally adds --no-experimental-detect-module for node versions 22.7.0 and up if ESM is being used with typescript`, async () => {
// @ts-expect-error
const projectPath = await scaffoldProject('config-cjs-and-esm/config-with-ts-module')

const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node`
const MOCK_NODE_VERSION = nodeVersion

projectConfigIpc = new ProjectConfigIpc(
MOCK_NODE_PATH,
MOCK_NODE_VERSION,
projectPath,
'cypress.config.js',
false,
(error) => {},
() => {},
)

expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
NODE_OPTIONS: sinon.match('--no-experimental-detect-module'),
},
}))
})
}
})

context('CommonJS', () => {
it('uses the ts_node commonjs loader if CommonJS is being used with typescript', async () => {
// @ts-expect-error
const projectPath = await scaffoldProject('config-cjs-and-esm/config-with-module-resolution-bundler')

const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node`
const MOCK_NODE_VERSION = nodeVersion

projectConfigIpc = new ProjectConfigIpc(
MOCK_NODE_PATH,
MOCK_NODE_VERSION,
projectPath,
'cypress.config.js',
false,
(error) => {},
() => {},
)

expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
NODE_OPTIONS: sinon.match('--require'),
},
}))
})
})
})
})
})
})
})
2 changes: 2 additions & 0 deletions system-tests/test-binary/node_versions_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('binary node versions', () => {
'cypress/base:18.16.1',
'cypress/base:20.12.2',
'cypress/base:22.0.0',
'cypress/base:22.7.0',
].forEach(smokeTestDockerImage)
})

Expand All @@ -37,6 +38,7 @@ describe('type: module', () => {
'cypress/base:18.16.1',
'cypress/base:20.12.2',
'cypress/base:22.0.0',
'cypress/base:22.7.0',
].forEach((dockerImage) => {
systemTests.it(`can run in ${dockerImage}`, {
withBinary: true,
Expand Down

5 comments on commit 5a6b8c4

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5a6b8c4 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.14.0/linux-arm64/develop-5a6b8c49cf2e3bd28f855278e96319ec53e8d7d3/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5a6b8c4 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.14.0/linux-x64/develop-5a6b8c49cf2e3bd28f855278e96319ec53e8d7d3/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5a6b8c4 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.14.0/win32-x64/develop-5a6b8c49cf2e3bd28f855278e96319ec53e8d7d3/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5a6b8c4 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.14.0/darwin-x64/develop-5a6b8c49cf2e3bd28f855278e96319ec53e8d7d3/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5a6b8c4 Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.14.0/darwin-arm64/develop-5a6b8c49cf2e3bd28f855278e96319ec53e8d7d3/cypress.tgz

Please sign in to comment.