mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-28 18:08:47 -06:00
fix: retry on EMFILE always, lint sync FS calls (#22175)
* fix: use graceful-fs always, warn in development on sync calls * skip prop linting in some dirs * eslint rules * use AST-based lint rule instead * comment * ignore existsSync * run without nextTick * remove dev warning code * fix order * register TS first * fix tests * fix test * cover new call site * fix new test
This commit is contained in:
29
.eslintrc.js
29
.eslintrc.js
@@ -5,6 +5,7 @@ const { specifiedRules } = require('graphql')
|
||||
const graphqlOpts = {
|
||||
env: 'literal',
|
||||
tagName: 'gql',
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
schemaString: fs.readFileSync(
|
||||
path.join(__dirname, 'packages/graphql/schemas/schema.graphql'),
|
||||
'utf8',
|
||||
@@ -33,6 +34,24 @@ module.exports = {
|
||||
'plugin:@cypress/dev/tests',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
// ignore in tests and scripts
|
||||
'**/scripts/**',
|
||||
'**/test/**',
|
||||
'**/system-tests/**',
|
||||
'packages/{app,driver,frontend-shared,launchpad}/cypress/**',
|
||||
'*.test.ts',
|
||||
// ignore in packages that don't run in the Cypress process
|
||||
'npm/create-cypress-tests/**',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-properties': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
'no-duplicate-imports': 'off',
|
||||
'import/no-duplicates': 'off',
|
||||
@@ -55,6 +74,16 @@ module.exports = {
|
||||
message: 'os.userInfo() will throw when there is not an `/etc/passwd` entry for the current user (like when running with --user 12345 in Docker). Do not use it unless you catch any potential errors.',
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
// esquery tool: https://estools.github.io/esquery/
|
||||
'error',
|
||||
{
|
||||
// match sync FS methods except for `existsSync`
|
||||
// examples: fse.readFileSync, fs.readFileSync, this.ctx.fs.readFileSync...
|
||||
selector: `MemberExpression[object.name='fs'][property.name=/^[A-z]+Sync$/]:not(MemberExpression[property.name='existsSync']), MemberExpression[property.name=/^[A-z]+Sync$/]:not(MemberExpression[property.name='existsSync']):has(MemberExpression[property.name='fs'])`,
|
||||
message: 'Synchronous fs calls should not be used in Cypress. Use an async API instead.',
|
||||
},
|
||||
],
|
||||
'graphql/capitalized-type-name': ['warn', graphqlOpts],
|
||||
'graphql/no-deprecated-fields': ['error', graphqlOpts],
|
||||
'graphql/template-strings': ['error', { ...graphqlOpts, validators }],
|
||||
|
||||
@@ -2,6 +2,7 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
module.exports =
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.assign({}, ...fs.readdirSync(__dirname)
|
||||
.filter((filename) => filename.endsWith('.js') && filename !== 'index.js')
|
||||
.map((filename) => ({ [filename.replace(/\.js$/u, '')]: require(path.resolve(__dirname, filename)) })))
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('Config options', () => {
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.withCtx(async (ctx, { specWithWhitespace }) => {
|
||||
ctx.actions.file.writeFileInProject(
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
ctx.path.join('src', specWithWhitespace),
|
||||
await ctx.file.readFileInProject(ctx.path.join('src', 'App.cy.jsx')),
|
||||
)
|
||||
|
||||
@@ -38,6 +38,8 @@ export const Cypress = (
|
||||
const indexHtmlFile = options.cypressConfig.indexHtmlFile
|
||||
|
||||
let specsPathsSet = getSpecsPathsSet(specs)
|
||||
// TODO: use async fs methods here
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
let loader = fs.readFileSync(INIT_FILEPATH, 'utf8')
|
||||
|
||||
devServerEvents.on('dev-server:specs:changed', (specs: Spec[]) => {
|
||||
|
||||
@@ -99,13 +99,15 @@ export class CypressCTWebpackPlugin {
|
||||
* has been "updated on disk", causing a recompliation (and pulling the new specs in as
|
||||
* dependencies).
|
||||
*/
|
||||
private onSpecsChange = (specs: Cypress.Cypress['spec'][]) => {
|
||||
private onSpecsChange = async (specs: Cypress.Cypress['spec'][]) => {
|
||||
if (!this.compilation || _.isEqual(specs, this.files)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.files = specs
|
||||
const inputFileSystem = this.compilation.inputFileSystem
|
||||
// TODO: don't use a sync fs method here
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const utimesSync: UtimesSync = inputFileSystem.fileSystem.utimesSync ?? fs.utimesSync
|
||||
|
||||
utimesSync(path.resolve(__dirname, 'browser.js'), new Date(), new Date())
|
||||
|
||||
@@ -69,7 +69,7 @@ async function loadWebpackConfig (devServerConfig: WebpackDevServerConfig): Prom
|
||||
buildId: `@cypress/react-${Math.random().toString()}`,
|
||||
config: nextConfig,
|
||||
dev: true,
|
||||
pagesDir: findPagesDir(devServerConfig.cypressConfig.projectRoot),
|
||||
pagesDir: await findPagesDir(devServerConfig.cypressConfig.projectRoot),
|
||||
entrypoints: {},
|
||||
rewrites: { fallback: [], afterFiles: [], beforeFiles: [] },
|
||||
...runWebpackSpan,
|
||||
@@ -106,9 +106,9 @@ function checkSWC (
|
||||
return false
|
||||
}
|
||||
|
||||
const existsSync = (file: string) => {
|
||||
const exists = async (file: string) => {
|
||||
try {
|
||||
fs.accessSync(file, fs.constants.F_OK)
|
||||
await fs.promises.access(file, fs.constants.F_OK)
|
||||
|
||||
return true
|
||||
} catch (_) {
|
||||
@@ -121,16 +121,16 @@ const existsSync = (file: string) => {
|
||||
* `${projectRoot}/pages` or `${projectRoot}/src/pages`.
|
||||
* If neither is found, return projectRoot
|
||||
*/
|
||||
function findPagesDir (projectRoot: string) {
|
||||
async function findPagesDir (projectRoot: string) {
|
||||
// prioritize ./pages over ./src/pages
|
||||
let pagesDir = path.join(projectRoot, 'pages')
|
||||
|
||||
if (existsSync(pagesDir)) {
|
||||
if (await exists(pagesDir)) {
|
||||
return pagesDir
|
||||
}
|
||||
|
||||
pagesDir = path.join(projectRoot, 'src', 'pages')
|
||||
if (existsSync(pagesDir)) {
|
||||
if (await exists(pagesDir)) {
|
||||
return pagesDir
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-console */
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"secure": "nsp check",
|
||||
"semantic-release": "semantic-release",
|
||||
"size": "npm pack --dry",
|
||||
"test": "node ./test-webpack-5.js",
|
||||
"test": "node ./scripts/test-webpack-5.js",
|
||||
"test-debug": "node --inspect --debug-brk ./node_modules/.bin/_mocha",
|
||||
"test-e2e": "mocha test/e2e/*.spec.*",
|
||||
"test-unit": "mocha test/unit/*.spec.*",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
const execa = require('execa')
|
||||
const pkg = require('./package.json')
|
||||
const path = require('path')
|
||||
const pkg = require('../package.json')
|
||||
const fs = require('fs')
|
||||
|
||||
const pkgJsonPath = path.join(__dirname, '..', 'package.json')
|
||||
|
||||
/**
|
||||
* This file installs Webpack 5 and runs the unit and e2e tests for the preprocessor.
|
||||
* We read package.json, update the webpack version, then re-run yarn install.
|
||||
@@ -17,7 +20,7 @@ const main = async () => {
|
||||
const install = () => execa('yarn', ['install', '--ignore-scripts'], { stdio: 'inherit' })
|
||||
|
||||
const resetPkg = async () => {
|
||||
fs.writeFileSync('package.json', originalPkg, 'utf8')
|
||||
fs.writeFileSync(pkgJsonPath, originalPkg, 'utf8')
|
||||
await install()
|
||||
}
|
||||
|
||||
@@ -38,7 +41,7 @@ const main = async () => {
|
||||
delete pkg.devDependencies['webpack']
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[@cypress/webpack-preprocessor]: updating package.json...')
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2))
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2))
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[@cypress/webpack-preprocessor]: install dependencies...')
|
||||
@@ -208,8 +208,8 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout:
|
||||
}).invoke('connected').should('be.true')
|
||||
})
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
ctx.actions.file.writeFileInProject(o.path, `
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject(o.path, `
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
|
||||
@@ -259,8 +259,8 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout:
|
||||
}).invoke('connected').should('be.true')
|
||||
})
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
ctx.actions.file.writeFileInProject(o.path, `
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject(o.path, `
|
||||
describe('Dom Content', () => {
|
||||
it('renders the new test content', () => {
|
||||
cy.visit('cypress/e2e/dom-content.html')
|
||||
|
||||
@@ -228,13 +228,13 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
|
||||
startAtSpecsPage('e2e')
|
||||
cy.get('[data-cy="spec-item"]')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
ctx.coreData.app.browserStatus = 'open'
|
||||
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`e2e: {`, `e2e: {\n chromeWebSecurity: false,\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
|
||||
o.sinon.stub(ctx.actions.browser, 'closeBrowser')
|
||||
o.sinon.stub(ctx.actions.browser, 'relaunchBrowser')
|
||||
@@ -252,24 +252,24 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
|
||||
it('restarts browser if there is a before:browser:launch task and there is a change on the config', () => {
|
||||
startAtSpecsPage('e2e')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
ctx.coreData.app.browserStatus = 'open'
|
||||
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`e2e: {`, `e2e: {\n setupNodeEvents(on) {\n on('before:browser:launch', () => {})\n},\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
})
|
||||
|
||||
cy.get('[data-cy="spec-item"]')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
ctx.coreData.app.browserStatus = 'open'
|
||||
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`e2e: {`, `e2e: {\n viewportHeight: 600,\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
|
||||
o.sinon.stub(ctx.actions.browser, 'closeBrowser')
|
||||
o.sinon.stub(ctx.actions.browser, 'relaunchBrowser')
|
||||
@@ -288,14 +288,14 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
|
||||
startAtSpecsPage('e2e')
|
||||
cy.get('[data-cy="spec-item"]')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
ctx.coreData.app.browserStatus = 'open'
|
||||
o.sinon.stub(ctx.actions.project, 'initializeActiveProject')
|
||||
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`{`, `{\n watchForFileChanges: false,\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
})
|
||||
|
||||
cy.get('[data-cy="loading-spinner"]').should('be.visible')
|
||||
@@ -310,14 +310,14 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
|
||||
startAtSpecsPage('e2e')
|
||||
cy.get('[data-cy="spec-item"]')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
ctx.coreData.app.browserStatus = 'open'
|
||||
o.sinon.stub(ctx.actions.project, 'initializeActiveProject')
|
||||
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(` e2e: {`, ` e2e: {\n baseUrl: 'https://example.cypress.io',\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
})
|
||||
|
||||
cy.get('[data-cy="loading-spinner"]').should('be.visible')
|
||||
|
||||
@@ -423,7 +423,7 @@ describe('App: Specs', () => {
|
||||
|
||||
it('generates spec with file name that does not contain a known spec extension', () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(
|
||||
`specPattern: 'src/**/*.{cy,spec}.{js,jsx}'`,
|
||||
@@ -700,7 +700,7 @@ describe('App: Specs', () => {
|
||||
|
||||
it('generates spec with file name that does not contain a known spec extension', () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(
|
||||
`specPattern: 'src/specs-folder/*.cy.{js,jsx}'`,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
function updateProjectIdInCypressConfig (value: string) {
|
||||
return cy.withCtx((ctx, o) => {
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
return cy.withCtx(async (ctx, o) => {
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`projectId: 'abc123'`, `projectId: '${o.value}'`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
}, { value })
|
||||
}
|
||||
|
||||
function updateViewportHeightInCypressConfig (value: number) {
|
||||
return cy.withCtx((ctx, o) => {
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
return cy.withCtx(async (ctx, o) => {
|
||||
let config = await ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`e2e: {`, `e2e: {\n viewportHeight: ${o.value},\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
}, { value })
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ describe('specChange subscription', () => {
|
||||
.should('contain', 'dom-content.spec.js')
|
||||
.should('contain', 'dom-list.spec.js')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
ctx.actions.file.writeFileInProject(o.path, '')
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject(o.path, '')
|
||||
}, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') })
|
||||
|
||||
cy.get('[data-cy="spec-item-link"]')
|
||||
|
||||
@@ -8,29 +8,29 @@ import assert from 'assert'
|
||||
export class FileActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
readFileInProject (relativePath: string): string {
|
||||
async readFileInProject (relativePath: string): Promise<string> {
|
||||
if (!this.ctx.currentProject) {
|
||||
throw new Error(`Cannot write file in project without active project`)
|
||||
}
|
||||
|
||||
const filePath = path.join(this.ctx.currentProject, relativePath)
|
||||
|
||||
this.ctx.fs.ensureDirSync(path.dirname(filePath))
|
||||
await this.ctx.fs.ensureDir(path.dirname(filePath))
|
||||
|
||||
return this.ctx.fs.readFileSync(filePath, 'utf-8')
|
||||
return this.ctx.fs.readFile(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
writeFileInProject (relativePath: string, data: any) {
|
||||
async writeFileInProject (relativePath: string, data: any) {
|
||||
if (!this.ctx.currentProject) {
|
||||
throw new Error(`Cannot write file in project without active project`)
|
||||
}
|
||||
|
||||
const filePath = path.join(this.ctx.currentProject, relativePath)
|
||||
|
||||
this.ctx.fs.ensureDirSync(path.dirname(filePath))
|
||||
await this.ctx.fs.ensureDir(path.dirname(filePath))
|
||||
|
||||
// Typically used in e2e tests, simpler than forcing async
|
||||
this.ctx.fs.writeFileSync(
|
||||
await this.ctx.fs.writeFile(
|
||||
filePath,
|
||||
data,
|
||||
)
|
||||
@@ -42,7 +42,7 @@ export class FileActions {
|
||||
}
|
||||
|
||||
// Typically used in e2e tests, simpler than forcing async
|
||||
this.ctx.fs.removeSync(path.join(this.ctx.currentProject, relativePath))
|
||||
await this.ctx.fs.remove(path.join(this.ctx.currentProject, relativePath))
|
||||
}
|
||||
|
||||
async moveFileInProject (relativePath: string, toRelativePath: string) {
|
||||
@@ -51,7 +51,7 @@ export class FileActions {
|
||||
}
|
||||
|
||||
// Typically used in e2e tests, simpler than forcing async
|
||||
this.ctx.fs.moveSync(
|
||||
await this.ctx.fs.move(
|
||||
path.join(this.ctx.currentProject, relativePath),
|
||||
path.join(this.ctx.currentProject, toRelativePath),
|
||||
)
|
||||
|
||||
@@ -166,7 +166,7 @@ export class MigrationActions {
|
||||
}
|
||||
|
||||
if (this.ctx.isGlobalMode) {
|
||||
const version = this.locallyInstalledCypressVersion(this.ctx.currentProject)
|
||||
const version = await this.locallyInstalledCypressVersion(this.ctx.currentProject)
|
||||
|
||||
if (!version) {
|
||||
// Could not resolve Cypress. Unlikely, but they are using a
|
||||
@@ -197,12 +197,12 @@ export class MigrationActions {
|
||||
})
|
||||
}
|
||||
|
||||
locallyInstalledCypressVersion (currentProject: string) {
|
||||
async locallyInstalledCypressVersion (currentProject: string) {
|
||||
try {
|
||||
const localCypressPkgJsonPath = require.resolve(path.join('cypress', 'package.json'), {
|
||||
paths: [currentProject],
|
||||
})
|
||||
const localCypressPkgJson = fs.readJsonSync(path.join(localCypressPkgJsonPath)) as { version: string }
|
||||
const localCypressPkgJson = await fs.readJson(path.join(localCypressPkgJsonPath)) as { version: string }
|
||||
|
||||
return localCypressPkgJson?.version ?? undefined
|
||||
} catch (e) {
|
||||
@@ -365,7 +365,7 @@ export class MigrationActions {
|
||||
throw new NonStandardMigrationError('support')
|
||||
}
|
||||
|
||||
this.ctx.fs.renameSync(
|
||||
await this.ctx.fs.rename(
|
||||
path.join(this.ctx.currentProject, beforeRelative),
|
||||
path.join(this.ctx.currentProject, afterRelative),
|
||||
)
|
||||
|
||||
@@ -246,6 +246,8 @@ export class ProjectConfigIpc extends EventEmitter {
|
||||
let isProjectUsingESModules = false
|
||||
|
||||
try {
|
||||
// TODO: convert this to async FS methods
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const pkgJson = fs.readJsonSync(path.join(this.projectRoot, 'package.json'))
|
||||
|
||||
isProjectUsingESModules = pkgJson.type === 'module'
|
||||
|
||||
@@ -469,7 +469,7 @@ export class ProjectLifecycleManager {
|
||||
// we run the legacy plugins/index.js in a child process
|
||||
// and mutate the config based on the return value for migration
|
||||
// only used in open mode (cannot migrate via terminal)
|
||||
const legacyConfig = this.ctx.fs.readJsonSync(legacyConfigPath) as LegacyCypressConfigJson
|
||||
const legacyConfig = await this.ctx.fs.readJson(legacyConfigPath) as LegacyCypressConfigJson
|
||||
|
||||
// should never throw, unless there existing pluginsFile errors out,
|
||||
// in which case they are attempting to migrate an already broken project.
|
||||
@@ -619,6 +619,8 @@ export class ProjectLifecycleManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: convert to async FS method
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const pkgJson = this.ctx.fs.readJsonSync(this._pathToFile('package.json'))
|
||||
|
||||
if (pkgJson.type === 'module') {
|
||||
@@ -701,6 +703,8 @@ export class ProjectLifecycleManager {
|
||||
|
||||
private verifyProjectRoot (root: string) {
|
||||
try {
|
||||
// TODO: convert to async fs call
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (!fs.statSync(root).isDirectory()) {
|
||||
throw new Error('NOT DIRECTORY')
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export class GitDataSource {
|
||||
|
||||
let toSet: GitInfo | null = null
|
||||
|
||||
const stat = fs.statSync(file)
|
||||
const stat = await fs.promises.stat(file)
|
||||
const ctime = dayjs(stat.ctime)
|
||||
const birthtime = dayjs(stat.birthtime)
|
||||
|
||||
|
||||
@@ -363,6 +363,8 @@ export class ProjectDataSource {
|
||||
// chokidar is extremely inconsistent in whether or not it has the stats arg internally
|
||||
if (!stats) {
|
||||
try {
|
||||
// TODO: find a way to avoid this sync call - might require patching chokidar
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
stats = fs.statSync(file)
|
||||
} catch {
|
||||
// If the file/folder is removed do not ignore it, in case it is added
|
||||
|
||||
@@ -148,11 +148,11 @@ async function getPluginRelativePath (cfg: LegacyCypressConfigJson, projectRoot:
|
||||
return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot)
|
||||
}
|
||||
|
||||
function createCypressConfig (config: ConfigOptions, pluginPath: string | undefined, options: CreateConfigOptions): string {
|
||||
async function createCypressConfig (config: ConfigOptions, pluginPath: string | undefined, options: CreateConfigOptions): Promise<string> {
|
||||
const globalString = Object.keys(config.global).length > 0 ? `${formatObjectForConfig(config.global)},` : ''
|
||||
const componentString = options.hasComponentTesting ? createComponentTemplate(config.component) : ''
|
||||
const e2eString = options.hasE2ESpec
|
||||
? createE2ETemplate(pluginPath, options, config.e2e)
|
||||
? await createE2ETemplate(pluginPath, options, config.e2e)
|
||||
: ''
|
||||
|
||||
if (defineConfigAvailable(options.projectRoot)) {
|
||||
@@ -188,7 +188,7 @@ function formatObjectForConfig (obj: Record<string, unknown>) {
|
||||
return JSON.stringify(obj, null, 2).replace(/^[{]|[}]$/g, '') // remove opening and closing {}
|
||||
}
|
||||
|
||||
function createE2ETemplate (pluginPath: string | undefined, createConfigOptions: CreateConfigOptions, options: Record<string, unknown>) {
|
||||
async function createE2ETemplate (pluginPath: string | undefined, createConfigOptions: CreateConfigOptions, options: Record<string, unknown>) {
|
||||
if (createConfigOptions.shouldAddCustomE2ESpecPattern && !options.specPattern) {
|
||||
options.specPattern = 'cypress/e2e/**/*.{js,jsx,ts,tsx}'
|
||||
}
|
||||
@@ -201,7 +201,7 @@ function createE2ETemplate (pluginPath: string | undefined, createConfigOptions:
|
||||
`
|
||||
}
|
||||
|
||||
const pluginFile = fs.readFileSync(path.join(createConfigOptions.projectRoot, pluginPath), 'utf8')
|
||||
const pluginFile = await fs.readFile(path.join(createConfigOptions.projectRoot, pluginPath), 'utf8')
|
||||
let relPluginsPath
|
||||
|
||||
const startsWithDotSlash = new RegExp(/^.\//)
|
||||
|
||||
@@ -477,11 +477,13 @@ describe('_makeSpecWatcher', () => {
|
||||
const SPEC_FILE_ABC = path.join('cypress', 'e2e', 'some', 'new', 'folder', 'abc.ts')
|
||||
|
||||
const writeFiles = () => {
|
||||
ctx.actions.file.writeFileInProject(SUPPORT_FILE, '// foo')
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE1, '// foo')
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE2, '// foo')
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE3, '// foo')
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE_ABC, '// foo')
|
||||
return Promise.all([
|
||||
ctx.actions.file.writeFileInProject(SUPPORT_FILE, '// foo'),
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE1, '// foo'),
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE2, '// foo'),
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE3, '// foo'),
|
||||
ctx.actions.file.writeFileInProject(SPEC_FILE_ABC, '// foo'),
|
||||
])
|
||||
}
|
||||
|
||||
it('watch for changes on files based on the specPattern', async function () {
|
||||
@@ -499,7 +501,7 @@ describe('_makeSpecWatcher', () => {
|
||||
specWatcher.on('add', (filePath) => allFiles.add(filePath))
|
||||
specWatcher.on('change', (filePath) => allFiles.add(filePath))
|
||||
|
||||
writeFiles()
|
||||
await writeFiles()
|
||||
|
||||
let attempt = 0
|
||||
|
||||
@@ -531,7 +533,7 @@ describe('_makeSpecWatcher', () => {
|
||||
specWatcher.on('add', (filePath) => allFiles.add(filePath))
|
||||
specWatcher.on('change', (filePath) => allFiles.add(filePath))
|
||||
|
||||
writeFiles()
|
||||
await writeFiles()
|
||||
|
||||
let attempt = 0
|
||||
|
||||
@@ -563,16 +565,11 @@ describe('_makeSpecWatcher', () => {
|
||||
|
||||
specWatcher.on('add', (filePath) => allFiles.add(filePath))
|
||||
specWatcher.on('change', (filePath) => allFiles.add(filePath))
|
||||
specWatcher.on('unlink', (filePath) => allFiles.delete(filePath))
|
||||
|
||||
writeFiles()
|
||||
|
||||
await writeFiles()
|
||||
await ctx.actions.file.removeFileInProject(SPEC_FILE1)
|
||||
|
||||
let attempt = 0
|
||||
|
||||
while (allFiles.size < 3 && attempt++ <= 100) {
|
||||
await delay(10)
|
||||
}
|
||||
await delay(1000)
|
||||
|
||||
expect(Array.from(allFiles).sort()).to.eql([
|
||||
SPEC_FILE_ABC,
|
||||
|
||||
@@ -21,6 +21,8 @@ extend type Mutation {
|
||||
|
||||
// Get the Remote schema we've sync'ed locally
|
||||
export const remoteSchema = buildSchema(
|
||||
// ignoring since this happens on the first tick
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
fs.readFileSync(path.join(__dirname, '../../schemas', 'cloud.graphql'), 'utf-8') + LOCAL_SCHEMA_EXTENSIONS,
|
||||
{ assumeValid: true },
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build-prod": "yarn build",
|
||||
"build": "ts-node ./scripts/build.ts && ts-node ./src/ico.ts",
|
||||
"build": "ts-node ./scripts/build.ts && ts-node ./scripts/ico.ts",
|
||||
"test-unit": "NODE_ENV=test mocha -r @packages/ts/register test/*.ts",
|
||||
"test": "yarn test-unit"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ async function build () {
|
||||
const distPath = path.join(__dirname, '../dist')
|
||||
const assetsPath = path.join(__dirname, '../assets')
|
||||
const iconsPath = path.join(distPath, 'icons')
|
||||
|
||||
const iconsetPath = path.join(distPath, 'cypress.iconset')
|
||||
|
||||
await fs.remove(distPath)
|
||||
|
||||
@@ -23,8 +23,8 @@ describe('baseUrl', () => {
|
||||
cy.get('[data-cy-testingtype="e2e"]').click()
|
||||
cy.get('[data-cy="alert"]').contains('Warning: Cannot Connect Base Url Warning')
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', `
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `
|
||||
module.exports = {
|
||||
e2e: {
|
||||
supportFile: false,
|
||||
@@ -47,8 +47,8 @@ describe('baseUrl', () => {
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
cy.get('[data-cy="alert"]').should('not.exist')
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', `
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `
|
||||
module.exports = {
|
||||
pageLoadTimeout: 10000,
|
||||
e2e: {
|
||||
@@ -86,8 +86,8 @@ describe('experimentalStudio', () => {
|
||||
cy.get('[data-cy="warning-alert"]').contains('Warning: Experimental Studio Removed')
|
||||
cy.findAllByLabelText(cy.i18n.components.modal.dismiss).first().click()
|
||||
cy.get('[data-cy="warning-alert"]').should('not.exist')
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', ctx.actions.file.readFileInProject('cypress.config.js'))
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', await ctx.actions.file.readFileInProject('cypress.config.js'))
|
||||
})
|
||||
|
||||
cy.get('[data-cy="loading-spinner"]')
|
||||
|
||||
@@ -79,8 +79,8 @@ describe('Error handling', () => {
|
||||
|
||||
cy.contains('Choose a Browser').should('not.exist')
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.actions.file.writeFileInProject('cypress.config.ts', `
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.ts', `
|
||||
import { defineConfig } from 'cypress'
|
||||
import { defineConfig as viteConfig } from 'vite'
|
||||
export default defineConfig({
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('global mode', () => {
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx.actions.migration, 'locallyInstalledCypressVersion').returns('10.0.0')
|
||||
o.sinon.stub(ctx.actions.migration, 'locallyInstalledCypressVersion').resolves('10.0.0')
|
||||
})
|
||||
|
||||
cy.contains('migration-e2e-export-default').click()
|
||||
|
||||
@@ -347,12 +347,16 @@ export function loadClientCertificateConfig (config) {
|
||||
function loadBinaryFromFile (filepath: string): Buffer {
|
||||
debug(`loadCertificateFile: ${filepath}`)
|
||||
|
||||
// TODO: update to async
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return fs.readFileSync(filepath)
|
||||
}
|
||||
|
||||
function loadTextFromFile (filepath: string): string {
|
||||
debug(`loadPassphraseFile: ${filepath}`)
|
||||
|
||||
// TODO: update to async
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return fs.readFileSync(filepath, 'utf8').toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ export function inPkgJson (dependency: WizardDependency, projectPath: string): D
|
||||
const loc = require.resolve(path.join(dependency.package, 'package.json'), {
|
||||
paths: [projectPath],
|
||||
})
|
||||
// TODO: convert to async FS method
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const pkg = fs.readJsonSync(loc) as PkgJson
|
||||
const pkgVersion = semver.coerce(pkg.version)
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
//
|
||||
// if running in production mode (CYPRESS_INTERNAL_ENV)
|
||||
// all transpile should have been done already
|
||||
// and these calls should do nothing
|
||||
require('@packages/ts/register')
|
||||
|
||||
const { patchFs } = require('./lib/util/patch-fs')
|
||||
const fs = require('fs')
|
||||
|
||||
// prevent EMFILE errors
|
||||
patchFs(fs)
|
||||
|
||||
// override tty if we're being forced to
|
||||
require('./lib/util/tty').override()
|
||||
|
||||
@@ -17,12 +27,6 @@ require('./lib/unhandled_exceptions')
|
||||
|
||||
process.env.UV_THREADPOOL_SIZE = 128
|
||||
|
||||
require('graceful-fs').gracefulify(require('fs'))
|
||||
// if running in production mode (CYPRESS_INTERNAL_ENV)
|
||||
// all transpile should have been done already
|
||||
// and these calls should do nothing
|
||||
require('@packages/ts/register')
|
||||
|
||||
if (isRunningElectron) {
|
||||
require('./lib/util/process_profiler').start()
|
||||
}
|
||||
|
||||
@@ -188,10 +188,4 @@ module.exports = {
|
||||
// for testing purposes
|
||||
|
||||
__get: fileUtil.get.bind(fileUtil),
|
||||
|
||||
__removeSync () {
|
||||
fileUtil._cache = {}
|
||||
|
||||
return fs.removeSync(this.path)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const debug = require('debug')('cypress:server:controllers:client')
|
||||
const socketIo = require('@packages/socket')
|
||||
|
||||
// hold onto the client source + version in memory
|
||||
const clientSource = socketIo.getClientSource()
|
||||
const clientSourcePath = socketIo.getPathToClientSource()
|
||||
const clientVersion = socketIo.getClientVersion()
|
||||
|
||||
module.exports = {
|
||||
@@ -19,6 +19,7 @@ module.exports = {
|
||||
.type('application/javascript')
|
||||
.set('ETag', clientVersion)
|
||||
.status(200)
|
||||
.send(clientSource)
|
||||
// TODO: replace this entire file and sendFile call with `express.static`.
|
||||
.sendFile(clientSourcePath)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { SocketCt } from './socket-ct'
|
||||
import { SocketE2E } from './socket-e2e'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
|
||||
import { fs } from './util/fs'
|
||||
import system from './util/system'
|
||||
import type { FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, TestingType } from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
@@ -521,10 +520,6 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
return cfg
|
||||
}
|
||||
|
||||
writeConfigFile ({ code, configFilename }: { code: string, configFilename: string }) {
|
||||
fs.writeFileSync(path.resolve(this.projectRoot, configFilename), code)
|
||||
}
|
||||
|
||||
// These methods are not related to start server/sockets/runners
|
||||
|
||||
async getProjectId () {
|
||||
|
||||
@@ -13,38 +13,4 @@ interface PromisifiedFsExtra {
|
||||
pathExistsAsync: Promisified<typeof fsExtra.pathExistsSync>
|
||||
}
|
||||
|
||||
// warn users if somehow synchronous file methods are invoked
|
||||
// these methods due to "too many files" errors are a huge pain
|
||||
const warnOnSyncFileSystem = () => {
|
||||
console.error('WARNING: fs sync methods can fail due to EMFILE errors')
|
||||
console.error('Cypress only works reliably when ALL fs calls are async')
|
||||
|
||||
return console.error('You should modify these sync calls to be async')
|
||||
}
|
||||
|
||||
const topLines = (from, n, text) => {
|
||||
return text.split('\n').slice(from, n).join('\n')
|
||||
}
|
||||
|
||||
// just hide this function itself
|
||||
// stripping top few lines of the stack
|
||||
const getStack = () => {
|
||||
const err = new Error()
|
||||
|
||||
return topLines(3, 10, err.stack)
|
||||
}
|
||||
|
||||
const addSyncFileSystemWarnings = (fs) => {
|
||||
const oldExistsSync = fs.existsSync
|
||||
|
||||
fs.existsSync = (filename) => {
|
||||
warnOnSyncFileSystem()
|
||||
console.error(getStack())
|
||||
|
||||
return oldExistsSync(filename)
|
||||
}
|
||||
}
|
||||
|
||||
addSyncFileSystemWarnings(fsExtra)
|
||||
|
||||
export const fs = Bluebird.promisifyAll(fsExtra) as PromisifiedFsExtra & typeof fsExtra
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const fs = require('fs')
|
||||
const debug = require('debug')('net-profiler')
|
||||
|
||||
|
||||
7
packages/server/lib/util/patch-fs.ts
Normal file
7
packages/server/lib/util/patch-fs.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import gracefulFs from 'graceful-fs'
|
||||
import type fs from 'fs'
|
||||
|
||||
export function patchFs (_fs: typeof fs) {
|
||||
// Add gracefulFs for EMFILE queuing.
|
||||
gracefulFs.gracefulify(_fs)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-restricted-properties */
|
||||
require('../spec_helper')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
@@ -105,12 +104,12 @@ let ctx
|
||||
describe('lib/cypress', () => {
|
||||
require('mocha-banner').register()
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(async function () {
|
||||
ctx = getCtx()
|
||||
process.chdir(previousCwd)
|
||||
this.timeout(8000)
|
||||
|
||||
cache.__removeSync()
|
||||
await cache.remove()
|
||||
|
||||
Fixtures.scaffold()
|
||||
this.todosPath = Fixtures.projectPath('todos')
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
require('../spec_helper')
|
||||
|
||||
const { fs } = require(`../../lib/util/fs`)
|
||||
|
||||
describe('lib/util/fs', () => {
|
||||
beforeEach(() => {
|
||||
return sinon.spy(console, 'error')
|
||||
})
|
||||
|
||||
it('warns when trying to use fs.existsSync', () => {
|
||||
fs.existsSync(__filename)
|
||||
const warning = 'WARNING: fs sync methods can fail due to EMFILE errors'
|
||||
|
||||
expect(console.error).to.be.calledWith(warning)
|
||||
})
|
||||
|
||||
context('fs.pathExists', () => {
|
||||
it('finds this file', () => {
|
||||
return fs.pathExists(__filename)
|
||||
.then((found) => {
|
||||
expect(found).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
it('does not find non-existent file', () => {
|
||||
return fs.pathExists('does-not-exist')
|
||||
.then((found) => {
|
||||
expect(found).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from 'fs'
|
||||
import buffer from 'buffer'
|
||||
import type http from 'http'
|
||||
import server, { Server as SocketIOBaseServer, ServerOptions, Socket, Namespace } from 'socket.io'
|
||||
@@ -40,7 +39,3 @@ export const getPathToClientSource = () => {
|
||||
export const getClientVersion = () => {
|
||||
return version
|
||||
}
|
||||
|
||||
export const getClientSource = () => {
|
||||
return fs.readFileSync(getPathToClientSource(), 'utf8')
|
||||
}
|
||||
|
||||
@@ -39,19 +39,6 @@ describe('Socket', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('.getClientSource', function () {
|
||||
it('returns client source as a string', function (done) {
|
||||
const clientPath = path.join(resolvePkg('socket.io-client'), 'dist', 'socket.io.js')
|
||||
|
||||
fs.readFile(clientPath, 'utf8', function (err, str) {
|
||||
if (err) done(err)
|
||||
|
||||
expect(lib.getClientSource()).to.eq(str)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('blob encoding + decoding', () => {
|
||||
it('correctly encodes and decodes binary blob data', (done) => {
|
||||
const encoder = new parser.Encoder()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-restricted-properties */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const fs = require('fs')
|
||||
const { expect } = require('chai')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user