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:
Zach Bloomquist
2022-06-16 00:35:31 -04:00
committed by GitHub
parent e87c492479
commit d01932bf75
44 changed files with 161 additions and 192 deletions

View File

@@ -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 }],

View File

@@ -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)) })))

View File

@@ -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')),
)

View File

@@ -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[]) => {

View File

@@ -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())

View File

@@ -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
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-console */
import path from 'path'
import fs from 'fs'

View File

@@ -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.*",

View File

@@ -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...')

View File

@@ -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'

View File

@@ -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')

View File

@@ -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')

View File

@@ -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}'`,

View File

@@ -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 })
}

View File

@@ -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"]')

View File

@@ -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),
)

View File

@@ -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),
)

View File

@@ -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'

View File

@@ -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')
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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(/^.\//)

View File

@@ -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,

View File

@@ -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 },
)

View File

@@ -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"
},

View File

@@ -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)

View File

@@ -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"]')

View File

@@ -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({

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -188,10 +188,4 @@ module.exports = {
// for testing purposes
__get: fileUtil.get.bind(fileUtil),
__removeSync () {
fileUtil._cache = {}
return fs.removeSync(this.path)
},
}

View File

@@ -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)
},
}

View File

@@ -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 () {

View File

@@ -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

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax */
const fs = require('fs')
const debug = require('debug')('net-profiler')

View 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)
}

View File

@@ -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')

View File

@@ -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
})
})
})
})

View File

@@ -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')
}

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-restricted-properties */
/* eslint-disable no-restricted-syntax */
const fs = require('fs')
const { expect } = require('chai')