Skip to content

Commit 6df7a70

Browse files
authored
chore: capture versions of relevant dependencies with x-dependencies header (#26814)
1 parent f524670 commit 6df7a70

File tree

7 files changed

+255
-58
lines changed

7 files changed

+255
-58
lines changed

packages/data-context/src/sources/UtilDataSource.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fetch from 'cross-fetch'
22
import type { DataContext } from '../DataContext'
3-
import { isDependencyInstalled } from '@packages/scaffold-config'
3+
import { isDependencyInstalled, isDependencyInstalledByName } from '@packages/scaffold-config'
44

55
// Require rather than import since data-context is stricter than network and there are a fair amount of errors in agent.
66
const { agent } = require('@packages/network')
@@ -23,4 +23,8 @@ export class UtilDataSource {
2323
isDependencyInstalled (dependency: Cypress.CypressComponentDependency, projectPath: string) {
2424
return isDependencyInstalled(dependency, projectPath)
2525
}
26+
27+
isDependencyInstalledByName (packageName: string, projectPath: string) {
28+
return isDependencyInstalledByName(packageName, projectPath)
29+
}
2630
}

packages/data-context/src/sources/VersionsDataSource.ts

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import type { DataContext } from '..'
33
import type { TestingType } from '@packages/types'
44
import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types'
55
import Debug from 'debug'
6-
import { WIZARD_DEPENDENCIES } from '@packages/scaffold-config'
7-
import semver from 'semver'
6+
import { dependencyNamesToDetect } from '@packages/scaffold-config'
87

98
const debug = Debug('cypress:data-context:sources:VersionsDataSource')
109

@@ -161,45 +160,43 @@ export class VersionsDataSource {
161160
}
162161
}
163162

164-
try {
165-
const projectPath = this.ctx.currentProject
166-
167-
if (projectPath) {
168-
const dependenciesToCheck = WIZARD_DEPENDENCIES
169-
170-
debug('Checking %d dependencies in project', dependenciesToCheck.length)
171-
// Check all dependencies of interest in parallel
172-
const dependencyResults = await Promise.allSettled(
173-
dependenciesToCheck.map(async (dependency) => {
174-
const result = await this.ctx.util.isDependencyInstalled(dependency, projectPath)
175-
176-
// If a dependency isn't satisfied then we are no longer interested in it,
177-
// exclude from further processing by rejecting promise
178-
if (!result.satisfied) {
179-
throw new Error('Unsatisfied dependency')
180-
}
181-
182-
// We only want major version, fallback to `-1` if we couldn't detect version
183-
const majorVersion = result.detectedVersion ? semver.major(result.detectedVersion) : -1
184-
185-
// For any satisfied dependencies, build a `package@version` string
186-
return `${result.dependency.package}@${majorVersion}`
187-
}),
188-
)
189-
// Take any dependencies that were found and combine into comma-separated string
190-
const headerValue = dependencyResults
191-
.filter(this.isFulfilled)
192-
.map((result) => result.value)
193-
.join(',')
194-
195-
if (headerValue) {
196-
manifestHeaders['x-dependencies'] = headerValue
163+
if (this._initialLaunch) {
164+
try {
165+
const projectPath = this.ctx.currentProject
166+
167+
if (projectPath) {
168+
debug('Checking %d dependencies in project', dependencyNamesToDetect.length)
169+
// Check all dependencies of interest in parallel
170+
const dependencyResults = await Promise.allSettled(
171+
dependencyNamesToDetect.map(async (dependency) => {
172+
const result = await this.ctx.util.isDependencyInstalledByName(dependency, projectPath)
173+
174+
if (!result.detectedVersion) {
175+
throw new Error(`Could not resolve dependency version for ${dependency}`)
176+
}
177+
178+
// For any satisfied dependencies, build a `package@version` string
179+
return `${result.dependency}@${result.detectedVersion}`
180+
}),
181+
)
182+
183+
// Take any dependencies that were found and combine into comma-separated string
184+
const headerValue = dependencyResults
185+
.filter(this.isFulfilled)
186+
.map((result) => result.value)
187+
.join(',')
188+
189+
if (headerValue) {
190+
manifestHeaders['x-dependencies'] = headerValue
191+
}
192+
} else {
193+
debug('No project path, skipping dependency check')
197194
}
198-
} else {
199-
debug('No project path, skipping dependency check')
195+
} catch (err) {
196+
debug('Failed to detect project dependencies', err)
200197
}
201-
} catch (err) {
202-
debug('Failed to detect project dependencies', err)
198+
} else {
199+
debug('Not initial launch of Cypress, skipping dependency check')
203200
}
204201

205202
try {

packages/data-context/test/unit/sources/VersionsDataSource.spec.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('VersionsDataSource', () => {
1414
context('.versions', () => {
1515
let ctx: DataContext
1616
let fetchStub: sinon.SinonStub
17-
let isDependencyInstalledStub: sinon.SinonStub
17+
let isDependencyInstalledByNameStub: sinon.SinonStub
1818
let mockNow: Date = new Date()
1919
let versionsDataSource: VersionsDataSource
2020
let currentCypressVersion: string = pkg.version
@@ -36,12 +36,12 @@ describe('VersionsDataSource', () => {
3636
ctx.coreData.currentTestingType = 'e2e'
3737

3838
fetchStub = sinon.stub()
39-
isDependencyInstalledStub = sinon.stub()
39+
isDependencyInstalledByNameStub = sinon.stub()
4040
})
4141

4242
beforeEach(() => {
4343
sinon.stub(ctx.util, 'fetch').callsFake(fetchStub)
44-
sinon.stub(ctx.util, 'isDependencyInstalled').callsFake(isDependencyInstalledStub)
44+
sinon.stub(ctx.util, 'isDependencyInstalledByName').callsFake(isDependencyInstalledByNameStub)
4545
sinon.stub(os, 'platform').returns('darwin')
4646
sinon.stub(os, 'arch').returns('x64')
4747
sinon.useFakeTimers({ now: mockNow })
@@ -194,31 +194,41 @@ describe('VersionsDataSource', () => {
194194
})
195195

196196
it('generates x-framework, x-bundler, and x-dependencies headers', async () => {
197-
isDependencyInstalledStub.callsFake(async (dependency) => {
197+
isDependencyInstalledByNameStub.callsFake(async (packageName) => {
198198
// Should include any resolved dependency with a valid version
199-
if (dependency.package === 'react') {
199+
if (packageName === 'react') {
200200
return {
201-
dependency,
201+
dependency: packageName,
202202
detectedVersion: '1.2.3',
203-
satisfied: true,
204203
} as Cypress.DependencyToInstall
205204
}
206205

207-
// Not satisfied dependency should be excluded
208-
if (dependency.package === 'vue') {
206+
if (packageName === 'vue') {
209207
return {
210-
dependency,
208+
dependency: packageName,
211209
detectedVersion: '4.5.6',
212-
satisfied: false,
213210
}
214211
}
215212

216-
// Satisfied dependency without resolved version should result in -1
217-
if (dependency.package === 'typescript') {
213+
if (packageName === '@builder.io/qwik') {
218214
return {
219-
dependency,
215+
dependency: packageName,
216+
detectedVersion: '1.1.4',
217+
}
218+
}
219+
220+
if (packageName === '@playwright/experimental-ct-core') {
221+
return {
222+
dependency: packageName,
223+
detectedVersion: '1.33.0',
224+
}
225+
}
226+
227+
// Dependency without resolved version should be excluded
228+
if (packageName === 'typescript') {
229+
return {
230+
dependency: packageName,
220231
detectedVersion: null,
221-
satisfied: true,
222232
}
223233
}
224234

@@ -238,7 +248,7 @@ describe('VersionsDataSource', () => {
238248
headers: sinon.match({
239249
'x-framework': 'react',
240250
'x-dev-server': 'vite',
241-
'x-dependencies': 'typescript@-1,react@1',
251+
'x-dependencies': 'react@1.2.3,vue@4.5.6,@builder.io/qwik@1.1.4,@playwright/experimental-ct-core@1.33.0',
242252
}),
243253
},
244254
)

packages/launchpad/cypress/e2e/open-mode.cy.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,30 @@ describe('Launchpad: Open Mode', () => {
6565
cy.openProject('todos', ['--e2e'])
6666
})
6767

68-
it('includes `x-framework`, `x-dev-server`, and `x-dependencies` headers, even when launched in e2e mode', () => {
68+
it('includes `x-framework`, `x-dev-server`, and `x-dependencies` headers, even when launched in e2e mode if this is the initial launch of Cypress', () => {
69+
cy.withCtx((ctx) => {
70+
ctx.versions['_initialLaunch'] = true
71+
})
72+
73+
cy.visitLaunchpad()
74+
cy.skipWelcome()
75+
cy.get('h1').should('contain', 'Choose a browser')
76+
cy.withCtx((ctx, o) => {
77+
expect(ctx.util.fetch).to.have.been.calledWithMatch('https://download.cypress.io/desktop.json', {
78+
headers: {
79+
'x-framework': 'react',
80+
'x-dev-server': 'webpack',
81+
'x-dependencies': 'typescript@4.7.4',
82+
},
83+
})
84+
})
85+
})
86+
87+
it('does not include `x-dependencies` header, if this is not the initial launch of Cypress', () => {
88+
cy.withCtx((ctx) => {
89+
ctx.versions['_initialLaunch'] = false
90+
})
91+
6992
cy.visitLaunchpad()
7093
cy.skipWelcome()
7194
cy.get('h1').should('contain', 'Choose a browser')
@@ -74,7 +97,6 @@ describe('Launchpad: Open Mode', () => {
7497
headers: {
7598
'x-framework': 'react',
7699
'x-dev-server': 'webpack',
77-
'x-dependencies': 'typescript@4',
78100
},
79101
})
80102
})

packages/scaffold-config/src/dependencies.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,78 @@ export const WIZARD_BUNDLERS = [
175175
WIZARD_DEPENDENCY_WEBPACK,
176176
WIZARD_DEPENDENCY_VITE,
177177
] as const
178+
179+
const componentDependenciesOfInterest = [
180+
'@angular/cli',
181+
'@angular-devkit/build-angular',
182+
'@angular/core',
183+
'@angular/common',
184+
'@angular/platform-browser-dynamic',
185+
'react',
186+
'react-dom',
187+
'react-scripts',
188+
'vue',
189+
'@vue/cli-service',
190+
'svelte',
191+
'solid-js',
192+
'lit',
193+
'preact',
194+
'preact-cli',
195+
'ember',
196+
'@stencil/core',
197+
'@builder.io/qwik',
198+
'alpinejs',
199+
'@glimmer/component',
200+
'typescript',
201+
]
202+
203+
const bundlerDependenciesOfInterest = [
204+
'vite',
205+
'webpack',
206+
'parcel',
207+
'rollup',
208+
'snowpack',
209+
]
210+
211+
const testingDependenciesOfInterest = [
212+
'jest',
213+
'jsdom',
214+
'jest-preview',
215+
'storybook',
216+
'@storybook/addon-interactions',
217+
'@storybook/addon-a11y',
218+
'chromatic',
219+
'@testing-library/react',
220+
'@testing-library/react-hooks',
221+
'@testing-library/dom',
222+
'@testing-library/jest-dom',
223+
'@testing-library/cypress',
224+
'@testing-library/user-event',
225+
'@testing-library/vue',
226+
'@testing-library/svelte',
227+
'@testing-library/preact',
228+
'happy-dom',
229+
'vitest',
230+
'vitest-preview',
231+
'selenium-webdriver',
232+
'nightwatch',
233+
'karma',
234+
'playwright',
235+
'playwright-core',
236+
'@playwright/experimental-ct-core',
237+
'@playwright/experimental-ct-react',
238+
'@playwright/experimental-ct-svelte',
239+
'@playwright/experimental-ct-vue',
240+
'@playwright/experimental-ct-vue2',
241+
'@playwright/experimental-ct-solid',
242+
'@playwright/experimental-ct-react17',
243+
'axe-core',
244+
'jest-axe',
245+
'enzyme',
246+
]
247+
248+
export const dependencyNamesToDetect = [
249+
...componentDependenciesOfInterest,
250+
...bundlerDependenciesOfInterest,
251+
...testingDependenciesOfInterest,
252+
]

packages/scaffold-config/src/frameworks.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@ export type WizardBundler = typeof dependencies.WIZARD_BUNDLERS[number]
1414

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

17+
export async function isDependencyInstalledByName (packageName: string, projectPath: string): Promise<{dependency: string, detectedVersion: string | null}> {
18+
let detectedVersion: string | null = null
19+
20+
try {
21+
debug('detecting %s in %s', packageName, projectPath)
22+
23+
const packageFilePath = resolvePackagePath(packageName, projectPath, false)
24+
25+
if (!packageFilePath) {
26+
throw new Error('unable to resolve package file')
27+
}
28+
29+
const pkg = await fs.readJson(packageFilePath) as PkgJson
30+
31+
debug('found package.json %o', pkg)
32+
33+
if (!pkg.version) {
34+
throw Error(`${pkg.version} for ${packageName} is not a valid semantic version.`)
35+
}
36+
37+
detectedVersion = pkg.version
38+
} catch (e) {
39+
debug('error when detecting %s: %s', packageName, e.message)
40+
}
41+
42+
return {
43+
dependency: packageName,
44+
detectedVersion,
45+
}
46+
}
47+
1748
export async function isDependencyInstalled (dependency: Cypress.CypressComponentDependency, projectPath: string): Promise<Cypress.DependencyToInstall> {
1849
try {
1950
debug('detecting %s in %s', dependency.package, projectPath)

0 commit comments

Comments
 (0)