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: correctly resolve dependencies for CT onboarding when using Yarn Plug n Play #26452

Merged
merged 11 commits into from
Apr 11, 2023
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')
Copy link
Contributor

Choose a reason for hiding this comment

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

omg, never would have figured this one out. Very nice!


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