Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ _Released 3/25/2025 (PENDING)_

- Applies a fix from [#30730](https://github.com/cypress-io/cypress/pull/30730) and [#30099](https://github.com/cypress-io/cypress/pull/30099) related to Node.js turning on ESM flags by default in Node.js version `20.19.0`. Fixed in [#31308](https://github.com/cypress-io/cypress/pull/31308).
- Fixed an issue where Firefox BiDi was not correctly removing prerequests on expected network request failures. Fixes [#31325](https://github.com/cypress-io/cypress/issues/31325).
- Fixed an issue in `@cypress/webpack-batteries-included-preprocessor` and `@cypress/webpack-preprocessor` where sourceMaps were not being set correctly in TypeScript 5. This should now make error code frames accurate for TypeScript 5 users. Fixes [#29614](https://github.com/cypress-io/cypress/issues/29614).

**Dependency Updates:**

Expand Down
52 changes: 12 additions & 40 deletions npm/webpack-batteries-included-preprocessor/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
const path = require('path')
const fs = require('fs-extra')
const JSON5 = require('json5')
const webpack = require('webpack')
const Debug = require('debug')
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
Expand All @@ -17,38 +15,6 @@ const hasTsLoader = (rules) => {
})
}

const getTSCompilerOptionsForUser = (configFilePath) => {
const compilerOptions = {
sourceMap: false,
inlineSourceMap: true,
inlineSources: true,
downlevelIteration: true,
}

if (!configFilePath) {
return compilerOptions
}

try {
// If possible, try to read the user's tsconfig.json and see if sourceMap is configured
// eslint-disable-next-line no-restricted-syntax
const tsconfigJSON = fs.readFileSync(configFilePath, 'utf8')
// file might have trailing commas, new lines, etc. JSON5 can parse those correctly
const parsedJSON = JSON5.parse(tsconfigJSON)

// if the user has sourceMap's configured, set the option to true and turn off inlineSourceMaps
if (parsedJSON?.compilerOptions?.sourceMap) {
compilerOptions.sourceMap = true
compilerOptions.inlineSourceMap = false
compilerOptions.inlineSources = false
}
} catch (e) {
debug(`error in getTSCompilerOptionsForUser. Returning default...`, e)
} finally {
return compilerOptions
}
}

const addTypeScriptConfig = (file, options) => {
// shortcut if we know we've already added typescript support
if (options.__typescriptSupportAdded) return options
Expand All @@ -60,14 +26,21 @@ const addTypeScriptConfig = (file, options) => {
if (!rules || !Array.isArray(rules)) return options

// if we find ts-loader configured, don't add it again
if (hasTsLoader(rules)) return options
if (hasTsLoader(rules)) {
debug('ts-loader already configured, not adding again')

return options
}

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
// node will try to load a projects tsconfig.json instead of the node
// package using require('tsconfig'), so we alias it as 'tsconfig-aliased-for-wbip'
const configFile = require('tsconfig-aliased-for-wbip').findSync(path.dirname(file.filePath))

const compilerOptions = getTSCompilerOptionsForUser(configFile)
const getTsConfig = require('get-tsconfig')

// returns null if tsconfig cannot be found in the path/parent hierarchy
const configFile = getTsConfig.getTsconfig(file.filePath)

configFile ? debug(`found user tsconfig.json at ${configFile?.path} with compilerOptions: ${JSON.stringify(configFile?.config?.compilerOptions)}`) : debug('no user tsconfig.json found')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we aren't doing anything with this until #31282 except feeding the webpack plugin the tsconfig file path


webpackOptions.module.rules.push({
test: /\.tsx?$/,
Expand All @@ -77,7 +50,6 @@ const addTypeScriptConfig = (file, options) => {
loader: require.resolve('ts-loader'),
options: {
compiler: options.typescript,
compilerOptions,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

delegated to @cypress/webpack-preprocessor

logLevel: 'error',
silent: true,
transpileOnly: true,
Expand All @@ -88,7 +60,7 @@ const addTypeScriptConfig = (file, options) => {

webpackOptions.resolve.extensions = webpackOptions.resolve.extensions.concat(['.ts', '.tsx'])
webpackOptions.resolve.plugins = [new TsconfigPathsPlugin({
configFile,
configFile: configFile?.path,
silent: true,
})]

Expand Down
6 changes: 2 additions & 4 deletions npm/webpack-batteries-included-preprocessor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@
"debug": "^4.3.4",
"domain-browser": "^4.22.0",
"events": "^3.3.0",
"fs-extra": "^9.1.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

"get-tsconfig": "^4.10.0",
"https-browserify": "^1.0.0",
"json5": "2.2.3",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
Expand All @@ -39,8 +38,7 @@
"stream-http": "^3.2.0",
"string_decoder": "1.3.0",
"timers-browserify": "^2.0.12",
"ts-loader": "9.4.4",
"tsconfig-aliased-for-wbip": "npm:tsconfig@^7.0.0",
"ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"tty-browserify": "^0.0.1",
"url": "^0.11.1",
Expand Down
101 changes: 9 additions & 92 deletions npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,24 @@ describe('webpack-batteries-included-preprocessor', () => {
})

context('#getTSCompilerOptionsForUser', () => {
const mockTsconfigPath = '/path/to/tsconfig.json'
let readFileTsConfigMock
let getTsConfigMock
let preprocessor
let readFileTsConfigStub
let webpackOptions

beforeEach(() => {
const tsConfigPathSpy = sinon.spy()

readFileTsConfigMock = () => {
throw new Error('Could not read file!')
}

mock('tsconfig-paths-webpack-plugin', tsConfigPathSpy)
mock('@cypress/webpack-preprocessor', (options) => {
return (file) => undefined
})

const tsconfig = require('tsconfig-aliased-for-wbip')
const getTsConfig = require('get-tsconfig')

sinon.stub(tsconfig, 'findSync').callsFake(() => mockTsconfigPath)
getTsConfigMock = sinon.stub(getTsConfig, 'getTsconfig')

preprocessor = require('../../index')

const fs = require('fs-extra')

readFileTsConfigStub = sinon.stub(fs, 'readFileSync').withArgs(mockTsconfigPath, 'utf8').callsFake(() => {
return readFileTsConfigMock()
})

webpackOptions = {
module: {
rules: [],
Expand All @@ -74,12 +62,14 @@ describe('webpack-batteries-included-preprocessor', () => {
})

afterEach(() => {
// Remove the mock
// Remove the mock
mock.stop('tsconfig-paths-webpack-plugin')
mock.stop('@cypress/webpack-preprocessor')
})

it('always returns compilerOptions even if there is an error discovering the user\'s tsconfig.json', () => {
it('always returns loader options even if there is an error discovering the user\'s tsconfig.json', () => {
getTsConfigMock.returns(null)

const preprocessorCB = preprocessor({
typescript: true,
webpackOptions,
Expand All @@ -90,7 +80,6 @@ describe('webpack-batteries-included-preprocessor', () => {
outputPath: '.js',
})

sinon.assert.calledOnce(readFileTsConfigStub)
const tsLoader = webpackOptions.module.rules[0].use[0]

expect(tsLoader.loader).to.contain('ts-loader')
Expand All @@ -100,80 +89,8 @@ describe('webpack-batteries-included-preprocessor', () => {
expect(tsLoader.options.silent).to.be.true
expect(tsLoader.options.transpileOnly).to.be.true

const compilerOptions = tsLoader.options.compilerOptions

expect(compilerOptions.downlevelIteration).to.be.true
expect(compilerOptions.inlineSources).to.be.true
expect(compilerOptions.inlineSources).to.be.true
expect(compilerOptions.sourceMap).to.be.false
})

it('turns inlineSourceMaps on by default even if none are configured', () => {
// make json5 compat schema
const mockTsConfig = `{
"compilerOptions": {
"sourceMap": false,
"someConfigWithTrailingComma": true,
}
}`

readFileTsConfigMock = () => mockTsConfig

const preprocessorCB = preprocessor({
typescript: true,
webpackOptions,
})

preprocessorCB({
filePath: 'foo.ts',
outputPath: '.js',
})

sinon.assert.calledOnce(readFileTsConfigStub)
const tsLoader = webpackOptions.module.rules[0].use[0]

expect(tsLoader.loader).to.contain('ts-loader')

const compilerOptions = tsLoader.options.compilerOptions

expect(compilerOptions.downlevelIteration).to.be.true
expect(compilerOptions.inlineSources).to.be.true
expect(compilerOptions.inlineSources).to.be.true
expect(compilerOptions.sourceMap).to.be.false
})

it('turns on sourceMaps and disables inlineSourceMap and inlineSources if the sourceMap configuration option is set by the user', () => {
// make json5 compat schema
const mockTsConfig = `{
"compilerOptions": {
"sourceMap": true,
"someConfigWithTrailingComma": true,
}
}`

readFileTsConfigMock = () => mockTsConfig

const preprocessorCB = preprocessor({
typescript: true,
webpackOptions,
})

preprocessorCB({
filePath: 'foo.ts',
outputPath: '.js',
})

sinon.assert.calledOnce(readFileTsConfigStub)
const tsLoader = webpackOptions.module.rules[0].use[0]

expect(tsLoader.loader).to.contain('ts-loader')

const compilerOptions = tsLoader.options.compilerOptions

expect(compilerOptions.downlevelIteration).to.be.true
expect(compilerOptions.inlineSources).to.be.false
expect(compilerOptions.inlineSources).to.be.false
expect(compilerOptions.sourceMap).to.be.true
// compilerOptions are set by `@cypress/webpack-preprocessor` if ts-loader is present
expect(tsLoader.options.compilerOptions).to.be.undefined
})
})
})
97 changes: 97 additions & 0 deletions npm/webpack-preprocessor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,72 @@ import webpack from 'webpack'
import utils from './lib/utils'
import { overrideSourceMaps } from './lib/typescript-overrides'

const getTsLoaderIfExists = (rules) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

any idea if webpack has some type of config utility parsing? This is a bit tedious

Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately, not that I can find. I was hoping to find a JOI or Zod schema, but wasn't able to find anything obvious

let tsLoaderRule

rules.some((rule) => {
if (!rule.use && !rule.loader) return false

if (Array.isArray(rule.use)) {
const foundRule = rule.use.find((use) => {
return use.loader && use.loader.includes('ts-loader')
})

/**
* If the rule is found, it will look like this:
* rules: [
* {
* test: /\.tsx?$/,
* exclude: [/node_modules/],
* use: [{
* loader: 'ts-loader'
* }]
* }
* ]
*/
tsLoaderRule = foundRule

return tsLoaderRule
}

if (_.isObject(rule.use) && rule.use.loader && rule.use.loader.includes('ts-loader')) {
/**
* If the rule is found, it will look like this:
* rules: [
* {
* test: /\.tsx?$/,
* exclude: [/node_modules/],
* use: {
* loader: 'ts-loader'
* }
* }
* ]
*/
tsLoaderRule = rule.use

return tsLoaderRule
}

tsLoaderRule = rules.find((rule) => {
/**
* If the rule is found, it will look like this:
* rules: [
* {
* test: /\.tsx?$/,
* exclude: [/node_modules/],
* loader: 'ts-loader'
* }
* ]
*/
return rule.loader && rule.loader.includes('ts-loader')
})

return tsLoaderRule
})

return tsLoaderRule
}

const debug = Debug('cypress:webpack')
const debugStats = Debug('cypress:webpack:stats')

Expand Down Expand Up @@ -208,6 +274,37 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
filename: path.basename(outputPath),
},
})
.tap((opts) => {
try {
const tsLoaderRule = getTsLoaderIfExists(opts?.module?.rules)

if (!tsLoaderRule) {
debug('ts-loader not detected')

return
}

// FIXME: To prevent disruption, we are only passing in these 4 options to the ts-loader.
// We will be passing in the entire compilerOptions object from the tsconfig.json in Cypress 15.
// @see https://github.com/cypress-io/cypress/issues/29614#issuecomment-2722071332
// @see https://github.com/cypress-io/cypress/issues/31282
// Cypress ALWAYS wants sourceMap set to true, regardless of the user configuration.
// This is because we want to display a correct code frame in the test runner.
debug(`ts-loader detected: overriding tsconfig to use sourceMap:true, inlineSourceMap:false, inlineSources:false, downlevelIteration:true`)

tsLoaderRule.options = tsLoaderRule?.options || {}
tsLoaderRule.options.compilerOptions = tsLoaderRule.options?.compilerOptions || {}

tsLoaderRule.options.compilerOptions.sourceMap = true
tsLoaderRule.options.compilerOptions.inlineSourceMap = false
tsLoaderRule.options.compilerOptions.inlineSources = false
tsLoaderRule.options.compilerOptions.downlevelIteration = true
} catch (e) {
debug('ts-loader not detected', e)

return
}
})
.tap((opts) => {
if (opts.devtool === false) {
// disable any overrides if we've explicitly turned off sourcemaps
Expand Down
Loading
Loading