Skip to content

Commit

Permalink
fix: correctly resolve dependencies for CT onboarding when using Yarn…
Browse files Browse the repository at this point in the history
… Plug n Play (#26452)

* patch resolve package and use corret path for Yarn PnP module resolution

* add test

* fix logic

* changelog

* log

* Add link to pnp docs

* recursively search upwards for pnp.cjs

* use require.resolve no matter what

---------

Co-authored-by: Mike Plummer <[email protected]>
  • Loading branch information
lmiller1990 and mike-plummer committed Apr 11, 2023
1 parent fdb5642 commit 7a33f5c
Show file tree
Hide file tree
Showing 12 changed files with 14,831 additions and 106 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ _Released 04/11/2023 (PENDING)_
- Fixed an issue in the onboarding wizard where project framework & bundler would not be auto-detected when opening directly into component testing mode using the `--component` CLI flag. Fixes [#22777](https://github.com/cypress-io/cypress/issues/22777) and [#26388](https://github.com/cypress-io/cypress/issues/26388).
- Updated to use the `SEMAPHORE_GIT_WORKING_BRANCH` [Semphore](https://docs.semaphoreci.com) CI environment variable to correctly associate a Cloud run to the current branch. Previously this was incorrectly associating a run to the target branch. Fixes [#26309](https://github.com/cypress-io/cypress/issues/26309).
- Fix an edge case in Component Testing where a custom `baseUrl` in `tsconfig.json` for Next.js 13.2.0+ is not respected. This was partially fixed in [#26005](https://github.com/cypress-io/cypress/pull/26005), but an edge case was missed. Fixes [#25951](https://github.com/cypress-io/cypress/issues/25951).
- Correctly detect and resolve dependencies when configuring Component Testing in projects using Yarn's [Plug'n'Play feature](https://yarnpkg.com/features/pnp). Fixes [#25960](https://github.com/cypress-io/cypress/issues/25960).
- Fixed an issue where `click` events fired on `.type('{enter}')` did not propagate through shadow roots. Fixes [#26392](https://github.com/cypress-io/cypress/issues/26392).

**Misc:**
Expand Down
12 changes: 12 additions & 0 deletions packages/launchpad/cypress/e2e/project-setup.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,18 @@ describe('Launchpad: Setup Project', () => {
cy.findByDisplayValue('pnpm install -D react-scripts react-dom react')
})

it('works with Yarn 3 Plug n Play', () => {
scaffoldAndOpenProject('yarn-v3.1.1-pnp')

cy.visitLaunchpad()

cy.get('[data-cy-testingtype="component"]').click()
cy.get('button').should('be.visible').contains('Vue.js 3(detected)')
cy.get('button').should('be.visible').contains('Vite(detected)')
cy.findByText('Next step').click()
cy.findByTestId('alert').contains(`You've successfully installed all required dependencies.`)
})

it('makes the right command for npm', () => {
scaffoldAndOpenProject('pristine-npm')

Expand Down
41 changes: 1 addition & 40 deletions packages/scaffold-config/src/ct-detect-third-party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from 'zod'
import fs from 'fs-extra'
import Debug from 'debug'
import findUp from 'find-up'
import { isRepositoryRoot } from './searchUtils'

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

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

const ROOT_PATHS = [
'.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 Down
31 changes: 28 additions & 3 deletions packages/scaffold-config/src/frameworks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import fs from 'fs-extra'
import * as dependencies from './dependencies'
import componentIndexHtmlGenerator from './component-index-template'
import debugLib from 'debug'
import semver from 'semver'
import { isThirdPartyDefinition } from './ct-detect-third-party'
import resolvePackagePath from 'resolve-package-path'
import { tryToFindPnpFile } from './searchUtils'

const debug = debugLib('cypress:scaffold-config:frameworks')

Expand All @@ -14,10 +13,36 @@ export type WizardBundler = typeof dependencies.WIZARD_BUNDLERS[number]

export type CodeGenFramework = Cypress.ResolvedComponentFrameworkDefinition['codeGenFramework']

const yarnPnpRegistrationPath = new Map<string, boolean>()

async function readPackageJson (packageFilePath: string, projectPath: string): Promise<PkgJson> {
return require(require.resolve(packageFilePath))
}

export async function isDependencyInstalled (dependency: Cypress.CypressComponentDependency, projectPath: string): Promise<Cypress.DependencyToInstall> {
try {
debug('detecting %s in %s', dependency.package, projectPath)

// we only need to register this once, when the project check dependencies for the first time.
if (!yarnPnpRegistrationPath.get(projectPath)) {
const pnpFile = await tryToFindPnpFile(projectPath)

if (pnpFile) {
const pnpapi = require(pnpFile)

pnpapi.setup()
yarnPnpRegistrationPath.set(projectPath, true)
} else {
// not using Yarn PnP
yarnPnpRegistrationPath.set(projectPath, false)
}
}

// NOTE: this *must* be required **after** the call to `pnpapi.setup()`
// or the pnpapi module that is added at runtime by Yarn PnP will not be correctly used
// for module resolution.
const resolvePackagePath = require('resolve-package-path')

const packageFilePath = resolvePackagePath(dependency.package, projectPath)

if (!packageFilePath) {
Expand All @@ -30,7 +55,7 @@ export async function isDependencyInstalled (dependency: Cypress.CypressComponen
}
}

const pkg = await fs.readJson(packageFilePath) as PkgJson
const pkg = await readPackageJson(packageFilePath, projectPath)

debug('found package.json %o', pkg)

Expand Down
71 changes: 71 additions & 0 deletions packages/scaffold-config/src/searchUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import findUp from 'find-up'
import path from 'path'
import fs from 'fs-extra'
import Debug from 'debug'
const debug = Debug('cypress:scaffold-config:searchUtils')

const ROOT_PATHS = [
'.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)
}

/**
* Recursing search upwards from projectPath until the repository root looking for .pnp.cjs.
* If `.pnp.cjs` is found, return it
*/
export async function tryToFindPnpFile (projectPath: string): Promise<string | undefined> {
return findUp(async (directory: string) => {
const isCurrentRepositoryRoot = await isRepositoryRoot(directory)

const file = path.join(directory, '.pnp.cjs')
const hasPnpCjs = await fs.pathExists(file)

if (hasPnpCjs) {
return file
}

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: projectPath })
}
62 changes: 1 addition & 61 deletions packages/scaffold-config/test/unit/ct-detect-third-party.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { scaffoldMigrationProject, fakeDepsInNodeModules } from './detect.spec'
import fs from 'fs-extra'
import path from 'path'
import { detectThirdPartyCTFrameworks, validateThirdPartyModule, isThirdPartyDefinition, isRepositoryRoot } from '../../src'
import { detectThirdPartyCTFrameworks, validateThirdPartyModule, isThirdPartyDefinition } from '../../src'
import { expect } from 'chai'
import os from 'os'
import solidJs from './fixtures'

async function copyNodeModule (root, moduleName) {
Expand Down Expand Up @@ -54,65 +53,6 @@ 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 Down
105 changes: 105 additions & 0 deletions packages/scaffold-config/test/unit/searchUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import fs from 'fs-extra'
import path from 'path'
import { expect } from 'chai'
import os from 'os'
import { isRepositoryRoot, tryToFindPnpFile } from '../../src/searchUtils'
import dedent from 'dedent'

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 })
})

describe('isRepositoryRoot', () => {
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('tryToFindPnpFile', () => {
it('finds pnp.cjs at repo root', async () => {
const projectPath = path.join(TEMP_DIR, 'packages', 'tests')
const pnpcjs = path.join(TEMP_DIR, '.pnp.cjs')

await Promise.all([
fs.ensureFile(path.join(projectPath, 'package.json')),
fs.writeFile(pnpcjs, '/* pnp api */'),
fs.writeFile(path.join(TEMP_DIR, 'package.json'), dedent`
{
"workspaces": [
"packages/*"
]
}
`),
])

const pnpPath = await tryToFindPnpFile(projectPath)

expect(pnpPath).to.eq(pnpcjs)
})

it('does not find pnp.cjs at repo root', async () => {
const projectPath = path.join(TEMP_DIR, 'packages', 'tests')

await fs.ensureFile(path.join(projectPath, 'package.json'))
await fs.writeFile(path.join(TEMP_DIR, 'package.json'), dedent`
{
"workspaces": [
"packages/*"
]
}
`)

const pnpPath = await tryToFindPnpFile(projectPath)

expect(pnpPath).to.eq(undefined)
})
})
Loading

5 comments on commit 7a33f5c

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 7a33f5c Apr 11, 2023

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/12.9.1/linux-x64/develop-7a33f5c1a87f5a3aa3b8da575b39e42f93bfd5db/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 7a33f5c Apr 11, 2023

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/12.9.1/linux-arm64/develop-7a33f5c1a87f5a3aa3b8da575b39e42f93bfd5db/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 7a33f5c Apr 11, 2023

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/12.9.1/darwin-arm64/develop-7a33f5c1a87f5a3aa3b8da575b39e42f93bfd5db/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 7a33f5c Apr 11, 2023

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/12.9.1/darwin-x64/develop-7a33f5c1a87f5a3aa3b8da575b39e42f93bfd5db/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 7a33f5c Apr 11, 2023

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/12.9.1/win32-x64/develop-7a33f5c1a87f5a3aa3b8da575b39e42f93bfd5db/cypress.tgz

Please sign in to comment.