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 (cypress-io#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
2 people authored and astone123 committed Apr 19, 2023
1 parent 049b007 commit 28c226a
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

0 comments on commit 28c226a

Please sign in to comment.