Skip to content

spike: cjs to esm strategy #32052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/data-context/src/data/ProjectConfigIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const pkg = require('@packages/root')
const debug = debugLib(`cypress:lifecycle:ProjectConfigIpc`)
const debugVerbose = debugLib(`cypress-verbose:lifecycle:ProjectConfigIpc`)

const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child')
const CHILD_PROCESS_FILE_PATH = process.env.CYPRESS_INTERNAL_ENV === 'production' ? require.resolve('@packages/server/lib/plugins/child/require_async_child.js') : require.resolve('@packages/server/lib/plugins/child/require_async_child.ts')

// NOTE: need the file:// prefix to avoid https://nodejs.org/api/errors.html#err_unsupported_esm_url_scheme on windows
const tsx = os.platform() === 'win32' ? `file://${toPosix(require.resolve('tsx'))}` : toPosix(require.resolve('tsx'))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
process.title = 'Cypress: Config Manager'

const { telemetry, OTLPTraceExporterIpc, decodeTelemetryContext } = require('@packages/telemetry')
import { telemetry, OTLPTraceExporterIpc, decodeTelemetryContext } from '@packages/telemetry'
import minimist from 'minimist'
import { suppress } from '../../util/suppress_warnings'
import gracefulFs from 'graceful-fs'
import fs from 'fs'
import * as util from '../util'
import run from './run_require_async_child'

const { file, projectRoot, telemetryCtx } = require('minimist')(process.argv.slice(2))
const { file, projectRoot, telemetryCtx } = minimist(process.argv.slice(2))

const { context, version } = decodeTelemetryContext(telemetryCtx)

Expand All @@ -14,16 +20,14 @@ if (version && context) {

const span = telemetry.startSpan({ name: 'child:process', active: true })

require('../../util/suppress_warnings').suppress()
suppress()

process.on('disconnect', () => {
process.exit()
})

require('graceful-fs').gracefulify(require('fs'))
const util = require('../util')
gracefulFs.gracefulify(fs)
const ipc = util.wrapIpc(process)
const run = require('./run_require_async_child')

exporter.attachIPC(ipc)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
require('graceful-fs').gracefulify(require('fs'))
const debugLib = require('debug')
const { pathToFileURL } = require('url')
const util = require('../util')
const { RunPlugins } = require('./run_plugins')
import gracefulFs from 'graceful-fs'
import fs from 'fs'
import debugLib from 'debug'
import { pathToFileURL } from 'url'
import * as util from '../util'
import { RunPlugins } from './run_plugins'

gracefulFs.gracefulify(fs)

const debug = debugLib(`cypress:lifecycle:child:run_require_async_child:${process.pid}`)

interface IPC {
send: (event: string, data?: any) => void
on: (event: string, callback: (...args: any[]) => void) => void
}

/**
* Executes and returns the passed `file` (usually `configFile`) file in the ipc `loadConfig` event
* @param {*} ipc Inter Process Communication protocol
* @param {*} file the file we are trying to load
* @param {*} projectRoot the root of the typescript project (useful mainly for tsnode)
* @returns
*/
function run (ipc, file, projectRoot) {
function run (ipc: IPC, file: string, projectRoot: string) {
debug('configFile:', file)
debug('projectRoot:', projectRoot)
if (!projectRoot) {
Expand All @@ -27,7 +35,7 @@ function run (ipc, file, projectRoot) {
return false
})

process.on('unhandledRejection', (event) => {
process.on('unhandledRejection', (event: any) => {
let err = event

debug('unhandled rejection:', event)
Expand All @@ -43,10 +51,12 @@ function run (ipc, file, projectRoot) {
return false
})

const isValidSetupNodeEvents = (config, testingType) => {
const isValidSetupNodeEvents = async (config: any, testingType: string) => {
if (config[testingType] && config[testingType].setupNodeEvents && typeof config[testingType].setupNodeEvents !== 'function') {
const errors = await import('@packages/errors')

ipc.send('setupTestingType:error', util.serializeError(
require('@packages/errors').getError('SETUP_NODE_EVENTS_IS_NOT_FUNCTION', file, testingType, config[testingType].setupNodeEvents),
errors.getError('SETUP_NODE_EVENTS_IS_NOT_FUNCTION', file, testingType, config[testingType].setupNodeEvents),
))

return false
Expand All @@ -55,7 +65,7 @@ function run (ipc, file, projectRoot) {
return true
}

const getValidDevServer = (config) => {
const getValidDevServer = async (config: any) => {
const { devServer } = config

if (devServer && typeof devServer === 'function') {
Expand All @@ -64,30 +74,36 @@ function run (ipc, file, projectRoot) {

if (devServer && typeof devServer === 'object') {
if (devServer.bundler === 'webpack') {
return { devServer: require('@cypress/webpack-dev-server').devServer, objApi: true }
const { devServer: webpackDevServer } = await import('@cypress/webpack-dev-server')

return { devServer: webpackDevServer, objApi: true }
}

if (devServer.bundler === 'vite') {
// tsx magic allows this import to work even though its a ESM module and we are in a CJS context
return { devServer: require('@cypress/vite-dev-server').devServer, objApi: true }
const { devServer: viteDevServer } = await import('@cypress/vite-dev-server')

return { devServer: viteDevServer, objApi: true }
}
}

const errors = await import('@packages/errors')

ipc.send('setupTestingType:error', util.serializeError(
require('@packages/errors').getError('CONFIG_FILE_DEV_SERVER_IS_NOT_VALID', file, config),
errors.getError('CONFIG_FILE_DEV_SERVER_IS_NOT_VALID', file, config),
))

return false
}

// Config file loading of modules is tested within
// system-tests/projects/config-cjs-and-esm/*
const loadFile = async (file) => {
const loadFile = async (file: string) => {
try {
debug('Loading file %s', file)

return require(file)
} catch (err) {
} catch (err: any) {
if (!err.stack.includes('[ERR_REQUIRE_ESM]') && !err.stack.includes('SyntaxError: Cannot use import statement outside a module')) {
throw err
}
Expand All @@ -105,7 +121,7 @@ function run (ipc, file, projectRoot) {

return await import(fileURL)
} catch (err) {
debug('error loading file via native Node.js module loader %s', err.message)
debug('error loading file via native Node.js module loader %s', (err as Error).message)
throw err
}
}
Expand All @@ -118,15 +134,15 @@ function run (ipc, file, projectRoot) {
debug('loaded config file', file)
const result = configFileExport.default || configFileExport

const replacer = (_key, val) => {
const replacer = (_key: string, val: any) => {
return typeof val === 'function' ? `[Function ${val.name}]` : val
}

ipc.send('loadConfig:reply', { initialConfig: JSON.stringify(result, replacer), requires: util.nonNodeRequires() })

let hasSetup = false

ipc.on('setupTestingType', (testingType, options) => {
ipc.on('setupTestingType', async (testingType: string, options: any) => {
if (hasSetup) {
throw new Error('Already Setup')
}
Expand All @@ -137,29 +153,31 @@ function run (ipc, file, projectRoot) {

const runPlugins = new RunPlugins(ipc, projectRoot, file)

if (!isValidSetupNodeEvents(result, testingType)) {
if (!(await isValidSetupNodeEvents(result, testingType))) {
return
}

if (testingType === 'component') {
const devServerInfo = getValidDevServer(result.component || {})
const devServerInfo = await getValidDevServer(result.component || {})

if (!devServerInfo) {
return
}

const { devServer, objApi } = devServerInfo

runPlugins.runSetupNodeEvents(options, (on, config) => {
const setupNodeEvents = result.component && result.component.setupNodeEvents || ((on, config) => {})
runPlugins.runSetupNodeEvents(options, (on: any, config: any) => {
const setupNodeEvents = result.component && result.component.setupNodeEvents || ((on: any, config: any) => {})

const onConfigNotFound = async (devServer: any, root: string, searchedFor: any) => {
const errors = await import('@packages/errors')

const onConfigNotFound = (devServer, root, searchedFor) => {
ipc.send('setupTestingType:error', util.serializeError(
require('@packages/errors').getError('DEV_SERVER_CONFIG_FILE_NOT_FOUND', devServer, root, searchedFor),
errors.getError('DEV_SERVER_CONFIG_FILE_NOT_FOUND', devServer, root, searchedFor),
))
}

on('dev-server:start', (devServerOpts) => {
on('dev-server:start', (devServerOpts: any) => {
if (objApi) {
const { specs, devServerEvents } = devServerOpts

Expand All @@ -180,7 +198,7 @@ function run (ipc, file, projectRoot) {
return setupNodeEvents(on, config)
})
} else if (testingType === 'e2e') {
const setupNodeEvents = result.e2e && result.e2e.setupNodeEvents || ((on, config) => {})
const setupNodeEvents = result.e2e && result.e2e.setupNodeEvents || ((on: any, config: any) => {})

runPlugins.runSetupNodeEvents(options, setupNodeEvents)
} else {
Expand All @@ -192,7 +210,7 @@ function run (ipc, file, projectRoot) {
})

debug('loaded config from %s %o', file, result)
} catch (err) {
} catch (err: any) {
// With tsx, errors now come in as TransformErrors instead of TSErrors (as they also include JavaScript errors).
if (err.name === 'TransformError' || err.stack.includes('TransformError')) {
const { compilerErrorLocation, originalMessage, message } = util.buildErrorLocationFromTransformError(err, projectRoot)
Expand All @@ -203,20 +221,22 @@ function run (ipc, file, projectRoot) {
} else if (Array.isArray(err.errors)) {
// The stack trace of the esbuild error, do not give to much information related with the user error,
// we have the errors array which includes the users file and information related with the error
const firstError = err.errors.filter((e) => Boolean(e.location))[0]
const firstError = err.errors.filter((e: any) => Boolean(e.location))[0]

if (firstError && firstError.location.file) {
err.compilerErrorLocation = { filePath: firstError.location.file, line: Number(firstError.location.line), column: Number(firstError.location.column) }
}
}

const errors = await import('@packages/errors')

ipc.send('loadConfig:error', util.serializeError(
require('@packages/errors').getError('CONFIG_FILE_REQUIRE_ERROR', file, err),
errors.getError('CONFIG_FILE_REQUIRE_ERROR', file, err),
))
}
})

ipc.send('ready')
}

module.exports = run
export default run
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
const childProcess = require('child_process')
const path = require('path')
import childProcess from 'child_process'
import path from 'path'

const PROJECT_ROOT = path.join(path.dirname(require.resolve('@tooling/system-tests/package.json')), 'projects/kill-child-process')

describe('require_async_child', () => {
it('disconnects if the parent ipc is closed', (done) => {
const child = childProcess.fork(path.join(__dirname, 'run_child_fixture'))
const child = childProcess.fork(path.join(__dirname, 'run_child_fixture'), {
execArgv: ['--import', 'tsx'],
})

let childPid

child.on('message', (msg) => {
child.on('message', (msg: any) => {
if (msg.childPid) {
childPid = msg.childPid
child.send({ msg: 'toChild', data: { event: 'loadConfig', args: [] } })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
const childProcess = require('child_process')
const path = require('path')
import childProcess from 'child_process'
import type { ChildProcess } from 'child_process'
import path from 'path'

const REQUIRE_ASYNC_CHILD_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child')
const REQUIRE_ASYNC_CHILD_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child.ts')

let proc
let proc: ChildProcess

process.on('message', (msg) => {
process.on('message', (msg: any) => {
if (msg.msg === 'spawn') {
proc = childProcess.fork(REQUIRE_ASYNC_CHILD_PATH, ['--projectRoot', msg.data.projectRoot, '--file', path.join(msg.data.projectRoot, 'cypress.config.js')])
proc.on('message', (msg) => {
process.send({ childMessage: msg })
proc.on('message', (msg: any) => {
process.send!({ childMessage: msg })
})

process.send({ childPid: proc.pid })
process.send!({ childPid: proc.pid })
}

if (msg.msg === 'toChild') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
require('../../../spec_helper')
/* eslint-disable @typescript-eslint/no-this-alias, @typescript-eslint/unbound-method, mocha/no-synchronous-tests */
import '../../../spec_helper'
import runRequireAsyncChild from '../../../../lib/plugins/child/run_require_async_child'

const runRequireAsyncChild = require('../../../../lib/plugins/child/run_require_async_child')
// Global declarations for test environment
declare const describe: any
declare const beforeEach: any
declare const afterEach: any
declare const it: any
declare const expect: any
declare const sinon: any
declare const mockery: any
declare const process: any

describe('lib/plugins/child/run_require_async_child', () => {
beforeEach(function () {
Expand Down
Loading