Skip to content

Commit

Permalink
Merge branch 'develop' into 21931-styles
Browse files Browse the repository at this point in the history
  • Loading branch information
mjhenkes authored Jun 1, 2022
2 parents 7fb449e + e12e0d9 commit 3509e87
Show file tree
Hide file tree
Showing 16 changed files with 150 additions and 33 deletions.
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test": "yarn test-unit",
"test:clean": "find ./test/__fixtures__ -depth -name 'output.*' -type f -exec rm {} \\;",
"test-debug": "yarn test-unit --inspect-brk=5566",
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register 'test/**/*.spec.ts' --exit"
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register 'test/**/*.spec.ts' --exit --timeout 5000"
},
"dependencies": {
"@babel/core": "^7",
Expand Down
54 changes: 44 additions & 10 deletions packages/config/src/ast-utils/addToCypressConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface AddTestingTypeToCypressConfigOptions {
info: ASTComponentDefinitionConfig | {
testingType: 'e2e'
}
projectRoot: string
}

export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCypressConfigOptions): Promise<AddToCypressConfigResult> {
Expand All @@ -114,7 +115,7 @@ export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCy
// gracefully by adding some default code to use as the AST here, based on the extension
if (!result || result.trim() === '') {
resultStatus = 'ADDED'
result = getEmptyCodeBlock({ outputType: pathExt as OutputExtension, isProjectUsingESModules: options.isProjectUsingESModules })
result = getEmptyCodeBlock({ outputType: pathExt as OutputExtension, isProjectUsingESModules: options.isProjectUsingESModules, projectRoot: options.projectRoot })
}

const toPrint = await addToCypressConfig(options.filePath, result, toAdd)
Expand All @@ -133,27 +134,60 @@ export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCy
}
}

// If they are running Cypress that isn't installed in their
// project's node_modules, we don't want to include
// defineConfig(/***/) in their cypress.config.js,
// since it won't exist.
export function defineConfigAvailable (projectRoot: string) {
try {
const cypress = require.resolve('cypress', {
paths: [projectRoot],
})
const api = require(cypress)

return 'defineConfig' in api
} catch (e) {
return false
}
}

type OutputExtension = '.ts' | '.mjs' | '.js'

// Necessary to handle the edge case of them deleting the contents of their Cypress
// config file, just before we merge in the testing type
function getEmptyCodeBlock ({ outputType, isProjectUsingESModules }: { outputType: OutputExtension, isProjectUsingESModules: boolean}) {
if (outputType === '.ts' || outputType === '.mjs' || isProjectUsingESModules) {
function getEmptyCodeBlock ({ outputType, isProjectUsingESModules, projectRoot }: { outputType: OutputExtension, isProjectUsingESModules: boolean, projectRoot: string}) {
if (defineConfigAvailable(projectRoot)) {
if (outputType === '.ts' || outputType === '.mjs' || isProjectUsingESModules) {
return dedent`
import { defineConfig } from 'cypress'
export default defineConfig({
})
`
}

return dedent`
import { defineConfig } from 'cypress'
const { defineConfig } = require('cypress')
module.exports = defineConfig({
export default defineConfig({
})
`
}

if (outputType === '.ts' || outputType === '.mjs' || isProjectUsingESModules) {
return dedent`
export default {
}
`
}

return dedent`
const { defineConfig } = require('cypress')
module.exports = {
module.exports = defineConfig({
})
}
`
}

Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// babel transforms, etc. into client-side usage of the config code
export * from './browser'

export { addProjectIdToCypressConfig, addToCypressConfig, addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions } from './ast-utils/addToCypressConfig'
export { addProjectIdToCypressConfig, addToCypressConfig, addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions, defineConfigAvailable } from './ast-utils/addToCypressConfig'
51 changes: 51 additions & 0 deletions packages/config/test/ast-utils/addToCypressConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})

expect(stub.firstCall.args[1].trim()).to.eq(dedent`
Expand All @@ -50,6 +51,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: true,
projectRoot: __dirname,
})

expect(stub.firstCall.args[1].trim()).to.eq(dedent`
Expand All @@ -74,6 +76,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})

expect(stub.firstCall.args[1].trim()).to.eq(dedent`
Expand All @@ -91,13 +94,60 @@ describe('addToCypressConfig', () => {
expect(result.result).to.eq('ADDED')
})

it('will exclude defineConfig if cypress can\'t be imported from the projectRoot', async () => {
const result = await addTestingTypeToCypressConfig({
filePath: path.join(__dirname, '../__fixtures__/empty.config.js'),
info: {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: '/foo',
})

expect(stub.firstCall.args[1].trim()).to.eq(dedent`
module.exports = {
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
};
`)

expect(result.result).to.eq('ADDED')
})

it('will exclude defineConfig if cypress can\'t be imported from the projectRoot for an ECMA Script project', async () => {
const result = await addTestingTypeToCypressConfig({
filePath: path.join(__dirname, '../__fixtures__/empty.config.js'),
info: {
testingType: 'e2e',
},
isProjectUsingESModules: true,
projectRoot: '/foo',
})

expect(stub.firstCall.args[1].trim()).to.eq(dedent`
export default {
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
};
`)

expect(result.result).to.eq('ADDED')
})

it('will error if we are unable to add to the config', async () => {
const result = await addTestingTypeToCypressConfig({
filePath: path.join(__dirname, '../__fixtures__/invalid.config.ts'),
info: {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})

expect(result.result).to.eq('NEEDS_MERGE')
Expand All @@ -111,6 +161,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})

expect(result.result).to.eq('NEEDS_MERGE')
Expand Down
1 change: 1 addition & 0 deletions packages/data-context/src/actions/DevActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class DevActions {
if (!this._chokidar) {
this._chokidar = chokidar.watch(DevActions.CY_STATE_PATH, {
ignoreInitial: true,
ignorePermissionErrors: true,
})

this._chokidar.on('change', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/data-context/src/actions/WizardActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export class WizardActions {
isProjectUsingESModules: this.ctx.lifecycleManager.metaState.isProjectUsingESModules,
filePath: configFilePath,
info: testingTypeInfo,
projectRoot: this.projectRoot,
})

const description = (testingType === 'e2e')
Expand Down
1 change: 1 addition & 0 deletions packages/data-context/src/data/ProjectConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export class ProjectConfigManager {
const w = chokidar.watch(file, {
ignoreInitial: true,
cwd: this.options.projectRoot,
ignorePermissionErrors: true,
})

this._watchers.add(w)
Expand Down
1 change: 1 addition & 0 deletions packages/data-context/src/sources/ProjectDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export class ProjectDataSource {
// ignored by config
this._specWatcher = chokidar.watch('.', {
ignoreInitial: true,
ignorePermissionErrors: true,
cwd: projectRoot,
ignored: ['**/node_modules/**', ...excludeSpecPattern, ...additionalIgnorePattern],
})
Expand Down
21 changes: 2 additions & 19 deletions packages/data-context/src/sources/migration/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { LegacyCypressConfigJson, legacyIntegrationFolder } from '..'
import { parse } from '@babel/parser'
import generate from '@babel/generator'
import _ from 'lodash'
import { getBreakingKeys } from '@packages/config'
import { defineConfigAvailable, getBreakingKeys } from '@packages/config'

const debug = Debug('cypress:data-context:sources:migration:codegen')

Expand Down Expand Up @@ -81,6 +81,7 @@ export async function initComponentTestingMigration (
const watcher = chokidar.watch(
watchPaths, {
cwd: projectRoot,
ignorePermissionErrors: true,
},
)

Expand Down Expand Up @@ -147,24 +148,6 @@ async function getPluginRelativePath (cfg: LegacyCypressConfigJson, projectRoot:
return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot)
}

// If they are running an old version of Cypress
// or running Cypress that isn't installed in their
// project's node_modules, we don't want to include
// defineConfig(/***/) in their cypress.config.js,
// since it won't exist.
export function defineConfigAvailable (projectRoot: string) {
try {
const cypress = require.resolve('cypress', {
paths: [projectRoot],
})
const api = require(cypress)

return 'defineConfig' in api
} catch (e) {
return false
}
}

function createCypressConfig (config: ConfigOptions, pluginPath: string | undefined, options: CreateConfigOptions): string {
const globalString = Object.keys(config.global).length > 0 ? `${formatObjectForConfig(config.global)},` : ''
const componentString = options.hasComponentTesting ? createComponentTemplate(config.component) : ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ describe('startSpecWatcher', () => {
ignoreInitial: true,
cwd: projectRoot,
ignored: ['**/node_modules/**', '**/ignore.spec.ts', 'additional.ignore.cy.js'],
ignorePermissionErrors: true,
})

expect(onStub).to.have.been.calledWith('all', handleFsChange)
Expand Down
2 changes: 2 additions & 0 deletions packages/launchpad/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineConfig } from 'cypress'
import getenv from 'getenv'
import { snapshotCypressDirectory } from './cypress/tasks/snapshotsScaffold'
import { uninstallDependenciesInScaffoldedProject } from './cypress/tasks/uninstallDependenciesInScaffoldedProject'

const CYPRESS_INTERNAL_CLOUD_ENV = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV || 'development')

Expand Down Expand Up @@ -41,6 +42,7 @@ export default defineConfig({

on('task', {
snapshotCypressDirectory,
uninstallDependenciesInScaffoldedProject,
})

return await e2ePluginSetup(on, config)
Expand Down
13 changes: 13 additions & 0 deletions packages/launchpad/cypress/e2e/scaffold-project.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,17 @@ describe('scaffolding new projects', { defaultCommandTimeout: 7000 }, () => {
scaffoldAndOpenCTProject({ name: 'pristine', framework: 'Create React App', removeFixturesFolder: false })
assertScaffoldedFilesAreCorrect({ language, testingType: 'component', ctFramework: 'Create React App (v5)', customDirectory: 'without-fixtures' })
})

it('generates valid config file for pristine project without cypress installed', () => {
cy.scaffoldProject('pristine')
cy.openProject('pristine')
cy.withCtx((ctx) => ctx.currentProject).then((currentProject) => {
cy.task('uninstallDependenciesInScaffoldedProject', { currentProject })
})

cy.visitLaunchpad()
cy.contains('button', cy.i18n.testingType.e2e.name).click()
cy.contains('button', cy.i18n.setupPage.step.continue).click()
cy.contains('h1', cy.i18n.setupPage.testingCard.chooseABrowser).should('be.visible')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import fs from 'fs'
import path from 'path'

export async function uninstallDependenciesInScaffoldedProject ({ currentProject }) {
// @ts-ignore
fs.rmdirSync(path.resolve(currentProject, '../node_modules'), { recursive: true, force: true })

return null
}
14 changes: 12 additions & 2 deletions packages/server/lib/plugins/child/run_require_async_child.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ function run (ipc, file, projectRoot) {
// 3a. Yes: Use bundleRequire
// 3b. No: Continue through to `await import(configFile)`
// 4. Use node's dynamic import to import the configFile
let originalError

try {
return require(file)
} catch (err) {
originalError = err
if (!err.stack.includes('[ERR_REQUIRE_ESM]') && !err.stack.includes('SyntaxError: Cannot use import statement outside a module')) {
throw err
}
Expand All @@ -122,8 +124,16 @@ function run (ipc, file, projectRoot) {
debug(`User doesn't have esbuild. Going to use native node imports.`)

// We cannot replace the initial `require` with `await import` because
// Certain modules cannot be dynamically imported
return await import(file)
// Certain modules cannot be dynamically imported. If this throws, however, we want
// to show the original error that was thrown, because that's ultimately the source of the problem
try {
return await import(file)
} catch (e) {
// If we aren't able to import the file at all, throw the original error, since that has more accurate information
// of what failed to begin with
debug('esbuild fallback for loading config failed, throwing original error. node import error: %o', e)
throw originalError
}
}

throw err
Expand Down
3 changes: 3 additions & 0 deletions system-tests/__snapshots__/config_modules_spec.ts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports['cypress config with esm and cjs / does not support modules and ts without esbuild in config-cjs-and-esm/config-with-ts-module'] = `
STDOUT_ERROR_VALIDATED
`
7 changes: 7 additions & 0 deletions system-tests/test/config_modules_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ describe('cypress config with esm and cjs', function () {
spec: 'app.cy.js',
browser: 'chrome',
expectedExitCode: 1,
snapshot: true,
onStdout (stdout) {
expect(stdout).to.include('nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules')

// Need to make this stable b/c of filepaths, and snapshot: true is needed to invoke onStdout
return 'STDOUT_ERROR_VALIDATED'
},
})
})

Expand Down

0 comments on commit 3509e87

Please sign in to comment.