diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 3a31bc6ddf4d..77e1bed3f50b 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -32,7 +32,7 @@ mainBuildFilters: &mainBuildFilters - 'update-v8-snapshot-cache-on-develop' - 'jquery-patch-remove-unload' - 'publish-binary' - - 'chore/use_build_docker_file_for_centos7' + - 'fix/8599' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -43,7 +43,7 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'fix/webpack-batteries-included-for-yarn-pnp', << pipeline.git.branch >> ] + - equal: [ 'fix/8599', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -54,7 +54,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'chore/use_build_docker_file_for_centos7', << pipeline.git.branch >> ] + - equal: [ 'fix/8599', << pipeline.git.branch >> ] - equal: [ 'jquery-patch-remove-unload', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ @@ -78,7 +78,7 @@ windowsWorkflowFilters: &windows-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'fix/webpack-batteries-included-for-yarn-pnp', << pipeline.git.branch >> ] + - equal: [ 'fix/8599', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -154,7 +154,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/use_build_docker_file_for_centos7" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "fix/8599" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index bd2e581d08d0..b77c46ccfdda 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -11,9 +11,13 @@ _Released 8/27/2024 (PENDING)_ - Added a `CYPRESS_SKIP_VERIFY` flag to enable suppressing Cypress verification checks. Addresses [#22243](https://github.com/cypress-io/cypress/issues/22243). +**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). + **Dependency Updates:** -- Updated `detect-port` from `1.3.0` to `1.6.1`. Addressed in [#30038](https://github.com/cypress-io/cypress/pull/30038). +- Updated `detect-port` from `1.3.0` to `1.6.1`. Addressed in [#30038](https://github.com/cypress-io/cypress/pull/30038). ## 13.13.3 diff --git a/packages/server/lib/util/app_data.js b/packages/server/lib/util/app_data.js index efe9cb21f1de..a2f3c7b11021 100644 --- a/packages/server/lib/util/app_data.js +++ b/packages/server/lib/util/app_data.js @@ -10,11 +10,38 @@ const { fs } = require('../util/fs') const cwd = require('../cwd') const md5 = require('md5') const sanitize = require('sanitize-filename') +const replace = require('lodash/replace') const PRODUCT_NAME = pkg.productName || pkg.name -const OS_DATA_PATH = ospath.data() -const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME) +// original source code: https://github.com/isaacs/common-ancestor-path +// but modified to be more testable +const findCommonAncestor = (a, b) => { + const sep = os.platform() === 'win32' ? '\\' : '/' + + function* commonArrayMembers (a, b) { + const [l, s] = a.length > b.length ? [a, b] : [b, a] + + for (const x of s) { + if (x === l.shift()) { + yield x + } else { + break + } + } + } + + return a === b ? a + : path.parse(a).root !== path.parse(b).root ? null + : [...commonArrayMembers(path.normalize(a).split(sep), path.normalize(b).split(sep))].join(sep) +} + +const getElectronAppDataPath = () => { + const OS_DATA_PATH = ospath.data() + const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME) + + return ELECTRON_APP_DATA_PATH +} if (!PRODUCT_NAME) { throw new Error('Root package is missing name') @@ -47,11 +74,45 @@ const toHashName = (projectRoot) => { return `${name}-${hash}` } +const modifyFileIfOutsideProjectDirectory = (projectRoot, filePath) => { + /** + * files that live outside of the project directory + * do not resolve correctly on Windows as we are trying to resolve the file to the project directory. + * This issue is only noticeable on windows since the absolute path gets appended to the project bundle + * path. In Unix based systems, this goes unnoticed because: + * /Users/foo/project/nested/hash-bundle/Users/foo/project/file.js + * is a valid path in Unix, but + * C:\\Users\\foo\\project\\nested\\hash-bundleC:\\Users\\foo\\project\\file.js + * is not a valid path in Windows + * + * To resolve this issue, we find the common ancestor directory between the project and file, + * take the path AFTER the common ancestor directory of the file, and append it to the project bundle directory. + * Effectively: + * C:\\Users\\foo\\project\\nested\\hash-bundleC:\\Users\\foo\\project\\file.js + * will become + * C:\\Users\\foo\\project\\nested\\hash-bundle\\file.js + * @see https://github.com/cypress-io/cypress/issues/8599 + */ + + const relative = path.relative(projectRoot, filePath) + const isSubDirectory = relative && !relative.startsWith('..') && !path.isAbsolute(relative) + + // if the file does NOT live inside the project directory, + // find the common ancestor of the project and file to get the file subpath to append to the project bundle directory + if (!isSubDirectory) { + const commonDirectoryPath = findCommonAncestor(projectRoot, filePath) + + filePath = replace(filePath, commonDirectoryPath, '') + } + + return filePath +} + module.exports = { toHashName, - + findCommonAncestor, getBundledFilePath (projectRoot, filePath) { - return this.projectsPath(toHashName(projectRoot), 'bundles', filePath) + return this.projectsPath(toHashName(projectRoot), 'bundles', modifyFileIfOutsideProjectDirectory(projectRoot, filePath)) }, ensure () { @@ -98,7 +159,7 @@ module.exports = { folder = `${folder}-e2e-test` } - const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths) + const p = path.join(getElectronAppDataPath(), 'cy', folder, ...paths) log('path: %s', p) @@ -106,7 +167,7 @@ module.exports = { }, electronPartitionsPath () { - return path.join(ELECTRON_APP_DATA_PATH, 'Partitions') + return path.join(getElectronAppDataPath(), 'Partitions') }, projectsPath (...paths) { diff --git a/packages/server/test/unit/util/app_data_spec.js b/packages/server/test/unit/util/app_data_spec.js index f05405245fe4..35c93fb8d3d5 100644 --- a/packages/server/test/unit/util/app_data_spec.js +++ b/packages/server/test/unit/util/app_data_spec.js @@ -1,4 +1,7 @@ require('../../spec_helper') +const os = require('os') +const osPath = require('ospath') +const path = require('path') const AppData = require(`../../../lib/util/app_data`) @@ -28,6 +31,28 @@ describe('lib/util/app_data', () => { }) }) + context('#findCommonAncestor', () => { + it('posix', () => { + expect(AppData.findCommonAncestor('/a/b/c/d', '/a/b/c/d/')).to.equal('/a/b/c/d') + expect(AppData.findCommonAncestor('/a/b/c', '/a/x/y')).to.equal('/a') + expect(AppData.findCommonAncestor('/a/b/', '/a/b/')).to.equal('/a/b/') + expect(AppData.findCommonAncestor('/a', '/a/b/c')).to.equal('/a') + expect(AppData.findCommonAncestor('/a/b/c', '/a')).to.equal('/a') + }) + + it('win32', () => { + sinon.stub(os, 'platform') + os.platform.returns('win32') + + expect(AppData.findCommonAncestor('c:\\a\\b\\c\\d', 'c:\\a\\b\\c\\d\\')).to.equal('c:\\a\\b\\c\\d') + expect(AppData.findCommonAncestor('c:\\a\\b\\c', 'c:\\a\\x\\y')).to.equal('c:\\a') + expect(AppData.findCommonAncestor('c:\\a\\b\\', 'c:\\a\\b\\')).to.equal('c:\\a\\b\\') + expect(AppData.findCommonAncestor('c:\\a\\b\\', 'd:\\a\\b\\')).to.equal('') + expect(AppData.findCommonAncestor('c:\\a', 'c:\\a\\b\\c')).to.equal('c:\\a') + expect(AppData.findCommonAncestor('c:\\a\\b\\c', 'c:\\a')).to.equal('c:\\a') + }) + }) + context('#getBundledFilePath', () => { it('provides an absolute path to the bundled file', () => { const projectRoot = '/foo/bar' @@ -38,5 +63,69 @@ describe('lib/util/app_data', () => { expect(result).to.contain(expectedPrefix) expect(result).to.contain(imagePath) }) + + // @see https://github.com/cypress-io/cypress/issues/8599 + describe('issue #8599: can find a path to bundle preprocessor files that live outside the project directory', () => { + it('on windows', () => { + // mock / stub out path and os variables as if we were on Windows + sinon.stub(os, 'platform') + sinon.stub(osPath, 'data') + sinon.stub(path, 'basename') + sinon.stub(path, 'dirname') + sinon.stub(path, 'isAbsolute') + sinon.stub(path, 'join') + sinon.stub(path, 'parse') + sinon.stub(path, 'normalize') + + os.platform.returns('win32') + osPath.data.returns(`C:\\Users\\foo\\AppData\\Roaming`) + + path.basename.callsFake((...args) => path.win32.basename(...args)) + path.dirname.callsFake((...args) => path.win32.dirname(...args)) + path.isAbsolute.callsFake((...args) => path.win32.isAbsolute(...args)) + path.join.callsFake((...args) => path.win32.join(...args)) + path.parse.callsFake((...args) => path.win32.parse(...args)) + path.normalize.callsFake((...args) => path.win32.normalize(...args)) + const filePathNotInProjectDirectory = `C:\\Users\\foo\\project\\support\\index.js` + + const projectRoot = `C:\\Users\\foo\\project\\nested-project` + + const result = AppData.getBundledFilePath(projectRoot, filePathNotInProjectDirectory) + + // currently is: 'C:\\Users\\foo\\AppData\\Roaming\\Cypress\\cy\\test\\projects\\nested-project-5ddfc54488859fd4a685e789cc5259c9\\bundles\\C:\\Users\\foo\\project\\support\\index.js' + // likely should be something like this: 'C:\\Users\\foo\\AppData\\Roaming\\Cypress\\cy\\test\\projects\\nested-project-5ddfc54488859fd4a685e789cc5259c9\\bundles\\support\\index.js' + expect(result).to.equal(`C:\\Users\\foo\\AppData\\Roaming\\Cypress\\cy\\test\\projects\\nested-project-5ddfc54488859fd4a685e789cc5259c9\\bundles\\support\\index.js`) + }) + + it('on linux', () => { + sinon.stub(os, 'platform') + sinon.stub(osPath, 'data') + + os.platform.returns('linux') + osPath.data.returns(`/Users/foo/.cache`) + + const filePathNotInProjectDirectory = `/Users/foo/project/support/index.js` + const projectRoot = `/Users/foo/project/nested-project` + + const result = AppData.getBundledFilePath(projectRoot, filePathNotInProjectDirectory) + + expect(result).to.equal(`/Users/foo/.cache/Cypress/cy/test/projects/nested-project-48bc0cf1ee4dff159065e8a5813d3c7f/bundles/support/index.js`) + }) + + it('on darwin/mac', () => { + sinon.stub(os, 'platform') + sinon.stub(osPath, 'data') + + os.platform.returns('linux') + osPath.data.returns(`/Users/foo/Library/Caches`) + + const filePathNotInProjectDirectory = `/Users/foo/project/support/index.js` + const projectRoot = `/Users/foo/project/nested-project` + + const result = AppData.getBundledFilePath(projectRoot, filePathNotInProjectDirectory) + + expect(result).to.equal(`/Users/foo/Library/Caches/Cypress/cy/test/projects/nested-project-48bc0cf1ee4dff159065e8a5813d3c7f/bundles/support/index.js`) + }) + }) }) }) diff --git a/yarn.lock b/yarn.lock index f304d58603b7..a66ea3d335ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12443,7 +12443,7 @@ commander@~2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== -common-ancestor-path@^1.0.1: +common-ancestor-path@1.0.1, common-ancestor-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==