Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: discover CT community definitions in monorepos with hoisted dependencies #26066

Merged
merged 29 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
00fa9f4
WIP find a way to resolve dependencies that does not rely on exports
astone123 Mar 6, 2023
6b4ff05
use `resolvePackagePath` package
astone123 Mar 6, 2023
1b7f578
revert schema changes
astone123 Mar 6, 2023
4c60b60
something is going on with Percy
astone123 Mar 7, 2023
5bffe16
remove loc
astone123 Mar 7, 2023
8dcef02
Merge branch 'develop' into launchpad-dep-resolution-exports
astone123 Mar 7, 2023
c2dbdce
feedback
astone123 Mar 7, 2023
21c8f71
Merge branch 'launchpad-dep-resolution-exports' of github.com:cypress…
astone123 Mar 7, 2023
d95d3e5
upgrade vite react plugin
astone123 Mar 7, 2023
ac054d1
update yarn lock
astone123 Mar 7, 2023
da65e43
Merge branch 'develop' into launchpad-dep-resolution-exports
astone123 Mar 7, 2023
98a10d6
fix unit test
astone123 Mar 8, 2023
59f89ca
Merge branch 'launchpad-dep-resolution-exports' of github.com:cypress…
astone123 Mar 8, 2023
a9ca93c
chore: discover CT community definitions in monorepos with hoisted de…
astone123 Mar 8, 2023
7403bd2
[run ci]
astone123 Mar 8, 2023
5897943
use find-up v5 because cjs
astone123 Mar 8, 2023
2fcde94
delete solid yarn lock [run ci]
astone123 Mar 8, 2023
ac1373e
improve repo root detection [run ci]
astone123 Mar 8, 2023
c43348e
Merge branch 'develop' of github.com:cypress-io/cypress into ct-third…
astone123 Mar 8, 2023
bd15692
[run ci]
astone123 Mar 8, 2023
9449b10
add some unit tests
astone123 Mar 9, 2023
eba120d
update comments
astone123 Mar 9, 2023
e669582
update links
astone123 Mar 9, 2023
d1ea474
use os.tmpdir
astone123 Mar 10, 2023
541098c
add test for monorepo third-party dependency resolution
astone123 Mar 13, 2023
371efb2
Merge branch 'develop' of github.com:cypress-io/cypress into ct-third…
astone123 Mar 13, 2023
43792f5
add changelog entry
astone123 Mar 13, 2023
b6acb62
add e2e test
astone123 Mar 13, 2023
b172a09
fix paths for windows
lmiller1990 Mar 14, 2023
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 @@ -12,6 +12,7 @@ _Released 03/14/2023 (PENDING)_

- Fixed an issue where using `Cypress.require()` would throw the error `Cannot find module 'typescript'`. Fixes [#25885](https://github.com/cypress-io/cypress/issues/25885).
- The [`before:spec`](https://docs.cypress.io/api/plugins/before-spec-api) API was updated to correctly support async event handlers in `run` mode. Fixes [#24403](https://github.com/cypress-io/cypress/issues/24403).
- Updated the Component Testing community framework definition detection logic to take into account monorepo structures that hoist dependencies. Fixes [#25993](https://github.com/cypress-io/cypress/issues/25993)

**Misc:**

Expand Down
10 changes: 9 additions & 1 deletion packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,15 @@ async function makeE2ETasks () {
}
},
async __internal_openProject ({ argv, projectName }: InternalOpenProjectArgs): Promise<ResetOptionsResult> {
if (!scaffoldedProjects.has(projectName)) {
let projectMatched = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Had to re-work this in order to call cy.openProject with a directory inside of a project like <projectName>/packages/foo to test monorepos

Copy link
Contributor

Choose a reason for hiding this comment

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

Shame we lose the auto completion :(

Let's do it for now, but I wonder if we can find a way to keep the completion 🤔


for (const scaffoldedProject of scaffoldedProjects.keys()) {
if (projectName.startsWith(scaffoldedProject)) {
projectMatched = true
}
}

if (!projectMatched) {
throw new Error(`${projectName} has not been scaffolded. Be sure to call cy.scaffoldProject('${projectName}') in the test, a before, or beforeEach hook`)
}

Expand Down
6 changes: 4 additions & 2 deletions packages/frontend-shared/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,10 @@ function openGlobalMode (options: OpenGlobalModeOptions = {}) {
})
}

function openProject (projectName: ProjectFixtureDir, argv: string[] = []) {
if (!fixtureDirs.includes(projectName)) {
type WithPrefix<T extends string> = `${T}${string}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here, this allows us to open Cypress from within a project to support monorepo testing


function openProject (projectName: WithPrefix<ProjectFixtureDir>, argv: string[] = []) {
if (!fixtureDirs.some((dir) => projectName.startsWith(dir))) {
throw new Error(`Unknown project ${projectName}`)
}

Expand Down
20 changes: 20 additions & 0 deletions packages/launchpad/cypress/e2e/scaffold-component-testing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,25 @@ describe('scaffolding component testing', {

verifyConfigFile('cypress.config.js')
})

it('Scaffolds component testing for monorepos with hoisted dependencies', () => {
cy.scaffoldProject('ct-monorepo-unconfigured')
cy.openProject('ct-monorepo-unconfigured/packages/foo')
Copy link
Contributor

Choose a reason for hiding this comment

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

Random idea to keep the autocompletion on the project name:

cy.openProject('ct-monorepo-unconfigured', { package: 'packages/foo' })


cy.withCtx(async (ctx) => {
await ctx.actions.file.removeFileInProject(ctx.path.join('..', '..', 'node_modules', 'cypress-ct-qwik'))
await ctx.actions.file.moveFileInProject(
ctx.path.join('..', '..', 'cypress-ct-qwik'),
ctx.path.join('..', '..', 'node_modules', 'cypress-ct-qwik'),
)
})

cy.visitLaunchpad()
cy.skipWelcome()

cy.contains('Component Testing').click()
cy.get(`[data-testid="select-framework"]`).click()
cy.contains('Qwik').should('be.visible')
})
})
})
1 change: 1 addition & 0 deletions packages/scaffold-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"compare-versions": "4.1.3",
"debug": "^4.3.4",
"find-up": "^5.0.0",
"fs-extra": "^9.1.0",
"globby": "^11.0.1",
"resolve-package-path": "^4.0.3",
Expand Down
87 changes: 80 additions & 7 deletions packages/scaffold-config/src/ct-detect-third-party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import globby from 'globby'
import { z } from 'zod'
import fs from 'fs-extra'
import Debug from 'debug'
import findUp from 'find-up'

const debug = Debug('cypress:scaffold-config:ct-detect-third-party')

Expand All @@ -25,6 +26,46 @@ const thirdPartyDefinitionPrefixes = {
globalPrefix: 'cypress-ct-',
}

const ROOT_PATHS = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit... should this be ROOT_FILES?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used the term paths because some of them are directories and some are files, for example .git is a directory.

'.git',

// https://pnpm.io/workspaces
'pnpm-workspace.yaml',

// https://rushjs.io/pages/advanced/config_files/
'rush.json',

// https://nx.dev/deprecated/workspace-json#workspace.json
// https://nx.dev/reference/nx-json#nx.json
'workspace.json',
'nx.json',

// https://lerna.js.org/docs/api-reference/configuration
'lerna.json',
]

async function hasWorkspacePackageJson (directory: string) {
try {
const pkg = await fs.readJson(path.join(directory, 'package.json'))

debug('package file for %s: %o', directory, pkg)

return !!pkg.workspaces
} catch (e) {
debug('error reading package.json in %s. this is not the repository root', directory)

return false
}
}

export async function isRepositoryRoot (directory: string) {
if (ROOT_PATHS.some((rootPath) => fs.existsSync(path.join(directory, rootPath)))) {
return true
}

return hasWorkspacePackageJson(directory)
}

export function isThirdPartyDefinition (definition: Cypress.ComponentFrameworkDefinition | Cypress.ThirdPartyComponentFrameworkDefinition): boolean {
return definition.type.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) ||
thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(definition.type)
Expand All @@ -46,14 +87,46 @@ export async function detectThirdPartyCTFrameworks (
projectRoot: string,
): Promise<Cypress.ThirdPartyComponentFrameworkDefinition[]> {
try {
const fullPathGlobs = [
path.join(projectRoot, CT_FRAMEWORK_GLOBAL_GLOB),
path.join(projectRoot, CT_FRAMEWORK_NAMESPACED_GLOB),
].map((x) => x.replaceAll('\\', '/'))
let fullPathGlobs
let packageJsonPaths: string[] = []

// Start at the project root and check each directory above it until we see
// an indication that the current directory is the root of the repository.
await findUp(async (directory: string) => {
fullPathGlobs = [
path.join(directory, CT_FRAMEWORK_GLOBAL_GLOB),
path.join(directory, CT_FRAMEWORK_NAMESPACED_GLOB),
].map((x) => x.replaceAll('\\', '/'))

debug('searching for third-party dependencies with globs %o', fullPathGlobs)

const newPackagePaths = await globby(fullPathGlobs)

if (newPackagePaths.length > 0) {
debug('found third-party dependencies %o', newPackagePaths)
}

packageJsonPaths = [...packageJsonPaths, ...newPackagePaths]

const isCurrentRepositoryRoot = await isRepositoryRoot(directory)
Copy link
Contributor

Choose a reason for hiding this comment

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

I can give this PR a test later, but definitely will be good to see some tests around this full functionality (whether it's Cy-in-Cy, or just an elaborate unit/integration test in this package).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a unit test in this package, let me know what you think


if (isCurrentRepositoryRoot) {
debug('stopping search at %s because it is believed to be the repository root', directory)

return findUp.stop
}

// Return undefined to keep searching
return undefined
}, { cwd: projectRoot })

if (packageJsonPaths.length === 0) {
debug('no third-party dependencies detected')

const packageJsonPaths = await globby(fullPathGlobs)
return []
}

debug('Found packages matching %s glob: %o', fullPathGlobs, packageJsonPaths)
debug('found third-party dependencies %o', packageJsonPaths)

const modules = await Promise.all(
packageJsonPaths.map(async (packageJsonPath) => {
Expand Down Expand Up @@ -96,7 +169,7 @@ export async function detectThirdPartyCTFrameworks (

return defaultEntry
} catch (e) {
debug('Ignoring %s due to error resolving: %o', e)
debug('Ignoring %s due to error resolving module', e)
}
}),
).then((modules) => {
Expand Down
88 changes: 83 additions & 5 deletions packages/scaffold-config/test/unit/ct-detect-third-party.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { scaffoldMigrationProject, fakeDepsInNodeModules } from './detect.spec'
import fs from 'fs-extra'
import path from 'path'
import { detectThirdPartyCTFrameworks, validateThirdPartyModule, isThirdPartyDefinition } from '../../src'
import { detectThirdPartyCTFrameworks, validateThirdPartyModule, isThirdPartyDefinition, isRepositoryRoot } from '../../src'
import { expect } from 'chai'
import os from 'os'
import solidJs from './fixtures'

async function copyNodeModule (root, moduleName) {
const nodeModulePath = path.join(root, 'node_modules', moduleName)

await fs.remove(nodeModulePath)
await fs.copy(path.join(root, moduleName), nodeModulePath)
}

async function scaffoldQwikApp (thirdPartyModuleNames: Array<'cypress-ct-qwik' | '@org/cypress-ct-qwik' | 'misconfigured-cypress-ct-qwik'>) {
const projectRoot = await scaffoldMigrationProject('qwik-app')

fakeDepsInNodeModules(projectRoot, [{ dependency: '@builder.io/qwik', version: '0.17.5' }])
for (const thirdPartyModuleName of thirdPartyModuleNames) {
const nodeModulePath = path.join(projectRoot, 'node_modules', thirdPartyModuleName)

await fs.remove(nodeModulePath)
await fs.copy(path.join(projectRoot, thirdPartyModuleName), nodeModulePath)
await copyNodeModule(projectRoot, thirdPartyModuleName)
}

return projectRoot
Expand Down Expand Up @@ -49,6 +54,65 @@ describe('isThirdPartyDefinition', () => {
})
})

describe('isRepositoryRoot', () => {
const TEMP_DIR = path.join(os.tmpdir(), 'is-repository-root-tmp')

beforeEach(async () => {
await fs.mkdir(TEMP_DIR)
})

afterEach(async () => {
await fs.rm(TEMP_DIR, { recursive: true })
})

it('returns false if there is nothing in the directory', async () => {
const isCurrentRepositoryRoot = await isRepositoryRoot(TEMP_DIR)

expect(isCurrentRepositoryRoot).to.be.false
})

it('returns true if there is a Git directory', async () => {
await fs.mkdir(path.join(TEMP_DIR, '.git'))

const isCurrentRepositoryRoot = await isRepositoryRoot(TEMP_DIR)

expect(isCurrentRepositoryRoot).to.be.true
})

it('returns false if there is a package.json without workspaces field', async () => {
await fs.writeFile(path.join(TEMP_DIR, 'package.json'), `{
"name": "@packages/foo",
"private": true,
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
`)

const isCurrentRepositoryRoot = await isRepositoryRoot(TEMP_DIR)

expect(isCurrentRepositoryRoot).to.be.false
})

it('returns true if there is a package.json with workspaces field', async () => {
await fs.writeFile(path.join(TEMP_DIR, 'package.json'), `{
"name": "monorepo-repo",
"private": true,
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"workspaces": [
"packages/*"
]
}
`)

const isCurrentRepositoryRoot = await isRepositoryRoot(TEMP_DIR)

expect(isCurrentRepositoryRoot).to.be.true
})
})

describe('detectThirdPartyCTFrameworks', () => {
it('detects third party frameworks in global namespace', async () => {
const projectRoot = await scaffoldQwikApp(['cypress-ct-qwik'])
Expand All @@ -75,6 +139,20 @@ describe('detectThirdPartyCTFrameworks', () => {
expect(thirdPartyFrameworks[0].type).eq('cypress-ct-qwik')
})

it('detects third party frameworks in monorepos with hoisted dependencies', async () => {
const repositoryRoot = await scaffoldMigrationProject('ct-monorepo-unconfigured')

// Copy 'cypress-ct-qwik' third-party module into node_modules in the monorepo root
await copyNodeModule(repositoryRoot, 'cypress-ct-qwik')

const projectRoot = path.join(repositoryRoot, 'packages', 'foo')

// Look for third-party modules in packages/foo (where Cypress was launched from)
const thirdPartyFrameworks = await detectThirdPartyCTFrameworks(projectRoot)

expect(thirdPartyFrameworks[0].type).eq('cypress-ct-qwik')
})

it('validates third party module', () => {
expect(() => validateThirdPartyModule(solidJs)).to.not.throw()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const qwikDep = {
type: 'qwik',
name: 'Qwik',
package: '@builder.io/qwik',
installer: '@builder.io/qwik',
description:
'An Open-Source sub-framework designed with a focus on server-side-rendering, lazy-loading, and styling/animation.',
minVersion: '^0.17.5',
}

module.exports = {
type: 'cypress-ct-qwik',

category: 'library',

name: 'Qwik',

supportedBundlers: ['vite'],

detectors: [qwikDep],

// Cypress will include the bundler dependency here, if they selected one.
dependencies: () => {
return [qwikDep]
},

icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 274"><defs><linearGradient id="logosQwik0" x1="22.347%" x2="77.517%" y1="49.545%" y2="50.388%"><stop offset="0%" stop-color="#4340C4"/><stop offset="12%" stop-color="#4642C8"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik1" x1="38.874%" x2="60.879%" y1="49.845%" y2="50.385%"><stop offset="0%" stop-color="#4340C4"/><stop offset="74%" stop-color="#534ADB"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik2" x1="-.004%" x2="100.123%" y1="49.529%" y2="50.223%"><stop offset="0%" stop-color="#4340C4"/><stop offset="23%" stop-color="#4340C4"/><stop offset="60%" stop-color="#4F48D5"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik3" x1="35.4%" x2="64.895%" y1="49.459%" y2="50.085%"><stop offset="0%" stop-color="#0080FF"/><stop offset="100%" stop-color="#00B9FF"/></linearGradient><linearGradient id="logosQwik4" x1="-.243%" x2="100.411%" y1="49.366%" y2="50.467%"><stop offset="0%" stop-color="#0080FF"/><stop offset="17%" stop-color="#008BFF"/><stop offset="47%" stop-color="#00A7FF"/><stop offset="63%" stop-color="#00B9FF"/><stop offset="100%" stop-color="#00B9FF"/></linearGradient><linearGradient id="logosQwik5" x1="-.125%" x2="100.225%" y1="49.627%" y2="50.101%"><stop offset="0%" stop-color="#00B9FF"/><stop offset="30%" stop-color="#0080FF"/><stop offset="60%" stop-color="#2D67F1"/><stop offset="86%" stop-color="#4D55E8"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik6" x1="4.557%" x2="99.354%" y1="50.184%" y2="51.298%"><stop offset="0%" stop-color="#4340C4"/><stop offset="12%" stop-color="#4642C8"/><stop offset="100%" stop-color="#594EE4"/></linearGradient></defs><path fill="url(#logosQwik0)" d="m175.051 236.859l25.162-15.071l49.298-86.929l-76.287 89.097z"/><path fill="url(#logosQwik1)" d="m242.337 80.408l-4.926-9.4l-1.932-3.663l-.2.196l-25.818-47.015C202.984 8.65 190.631 1.231 177.01 1.074l-25.074.206L188.15 114.8l-23.958 23.331l8.924 86.245l73.769-84.021c10.005-11.587 11.97-28.09 4.92-41.646l-9.466-18.302h-.002Z"/><path fill="url(#logosQwik2)" d="m201.113 72.256l-43.18-70.907L83.41.003C70.165-.15 57.83 6.573 50.88 17.87L7.01 101.747l34.443-33.334L84.701 8.356l97.894 112.153l18.3-18.626c8.397-8.142 5.54-19.558.22-29.625l-.002-.002Z"/><path fill="url(#logosQwik3)" d="M97.784 95.26L84.522 8.796l-73.148 88.03c-12.328 11.935-14.897 30.662-6.419 45.49l42.98 74.727c6.553 11.464 18.755 18.577 32.024 18.729l42.945.49L71.46 119.607L97.784 95.26Z"/><path fill="url(#logosQwik4)" d="M173.227 223.9L71.38 119.022l-13.196 12.59c-10.812 10.248-11.106 27.332-.728 37.921l43.99 66.384l70.65.907l1.127-12.926h.003Z"/><path fill="url(#logosQwik5)" d="m101.584 235.903l72.292-11.599l47.704 49.464z"/><path fill="url(#logosQwik6)" d="m173.111 224.483l27.168-3.457l24.096 49.915c1.06 2.06-1.719 3.977-3.373 2.302l-47.89-48.76Z"/><path fill="#FFF" d="M182.708 120.058L84.681 8.601l12.502 85.958L71.16 118.78l101.772 105.372l-7.595-85.905l17.371-18.192z"/></svg>',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function mount () {
return 'Legit mount function'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "cypress-ct-qwik",
"version": "1.0.0",
"main": "index.js",
"exports": {
"node": "./definition.cjs",
"default": "./index.mjs"
}
}
10 changes: 10 additions & 0 deletions system-tests/projects/ct-monorepo-unconfigured/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "ct-monorepo-unconfigured",
"version": "1.0.0",
"private": true,
"main": "index.js",
"license": "MIT",
"workspaces": [
"packages/*"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@packages/bar",
"version": "1.0.0",
"private": true,
"main": "index.js",
"license": "MIT"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@packages/foo",
"version": "1.0.0",
"private": true,
"main": "index.js",
"license": "MIT"
}
Loading