diff --git a/knip.ts b/knip.ts index e3bd837e2e7e..9361ff92f9de 100644 --- a/knip.ts +++ b/knip.ts @@ -34,8 +34,6 @@ export default { 'glob', 'jest-specific-snapshot', 'make-dir', - 'ncp', - 'tmp', // imported for type purposes only 'website', ], diff --git a/package.json b/package.json index 6e602d50ba81..b6a10af66d84 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,6 @@ "@types/jest": "29.5.13", "@types/jest-specific-snapshot": "^0.5.9", "@types/natural-compare": "^1.4.3", - "@types/ncp": "^2.0.8", "@types/node": "^20.12.5", "@types/semver": "^7.5.8", "@types/tmp": "^0.2.6", @@ -117,13 +116,11 @@ "lint-staged": "^15.2.2", "make-dir": "^4.0.0", "markdownlint-cli": "^0.44.0", - "ncp": "^2.0.0", "nx": "20.7.2", "prettier": "3.5.0", "pretty-format": "^29.7.0", "rimraf": "^5.0.5", "semver": "7.7.0", - "tmp": "^0.2.1", "tsx": "*", "typescript": ">=4.8.4 <5.9.0", "typescript-eslint": "workspace:^", diff --git a/packages/integration-tests/jest.config.js b/packages/integration-tests/jest.config.js index 81c7288cdf99..ce0a347c52e1 100644 --- a/packages/integration-tests/jest.config.js +++ b/packages/integration-tests/jest.config.js @@ -2,20 +2,16 @@ // pack the packages ahead of time and create a mapping for use in the tests require('tsx/cjs'); -const { tseslintPackages } = require('./tools/pack-packages'); +const { setup } = require('./tools/pack-packages'); // @ts-check -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { +/** @type {() => Promise} */ +module.exports = async () => ({ ...require('../../jest.config.base.js'), globals: { - tseslintPackages, + tseslintPackages: await setup(), }, + globalTeardown: './tools/pack-packages.ts', testRegex: ['/tests/[^/]+.test.ts$'], rootDir: __dirname, - - // TODO(Brad Zacher) - for some reason if we run more than 1 test at a time - // yarn will error saying the tarballs are corrupt on just - // the first test. - maxWorkers: 1, -}; +}); diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 6788989e816c..a58c13b47ef7 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -20,8 +20,6 @@ "devDependencies": { "@jest/types": "29.6.3", "jest": "29.7.0", - "ncp": "*", - "tmp": "*", "tsx": "*" } } diff --git a/packages/integration-tests/tools/integration-test-base.ts b/packages/integration-tests/tools/integration-test-base.ts index b3d4426d60b9..3b6d49936407 100644 --- a/packages/integration-tests/tools/integration-test-base.ts +++ b/packages/integration-tests/tools/integration-test-base.ts @@ -1,43 +1,10 @@ -import type { DirOptions } from 'tmp'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; -import ncp from 'ncp'; -import childProcess from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; -import { promisify } from 'node:util'; -import tmp from 'tmp'; - -interface PackageJSON { - devDependencies: Record; - name: string; - private?: boolean; -} - -const rootPackageJson: PackageJSON = require('../../../package.json'); - -tmp.setGracefulCleanup(); - -const copyDir = promisify(ncp.ncp); -const execFile = promisify(childProcess.execFile); -const readFile = promisify(fs.readFile); -const tmpDir = promisify(tmp.dir) as (opts?: DirOptions) => Promise; -const tmpFile = promisify(tmp.file); -const writeFile = promisify(fs.writeFile); - -const BASE_DEPENDENCIES: PackageJSON['devDependencies'] = { - ...global.tseslintPackages, - eslint: rootPackageJson.devDependencies.eslint, - jest: rootPackageJson.devDependencies.jest, - typescript: rootPackageJson.devDependencies.typescript, -}; - -const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures'); -// an env var to persist the temp folder so that it can be inspected for debugging purposes -const KEEP_INTEGRATION_TEST_DIR = - process.env.KEEP_INTEGRATION_TEST_DIR === 'true'; +import { execFile, FIXTURES_DESTINATION_DIR } from './pack-packages'; // make sure that jest doesn't timeout the test -jest.setTimeout(60000); +jest.setTimeout(60_000); function integrationTest( testName: string, @@ -45,82 +12,14 @@ function integrationTest( executeTest: (testFolder: string) => Promise, ): void { const fixture = path.parse(testFilename).name.replace('.test', ''); - describe(fixture, () => { - const fixtureDir = path.join(FIXTURES_DIR, fixture); + const testFolder = path.join(FIXTURES_DESTINATION_DIR, fixture); + + describe(fixture, () => { describe(testName, () => { it('should work successfully', async () => { - const testFolder = await tmpDir({ - keep: KEEP_INTEGRATION_TEST_DIR, - }); - if (KEEP_INTEGRATION_TEST_DIR) { - console.error(testFolder); - } - - // copy the fixture files to the temp folder - await copyDir(fixtureDir, testFolder); - - // build and write the package.json for the test - const fixturePackageJson: PackageJSON = await import( - path.join(fixtureDir, 'package.json') - ); - await writeFile( - path.join(testFolder, 'package.json'), - JSON.stringify({ - private: true, - ...fixturePackageJson, - devDependencies: { - ...BASE_DEPENDENCIES, - ...fixturePackageJson.devDependencies, - }, - // ensure everything uses the locally packed versions instead of the NPM versions - resolutions: { - ...global.tseslintPackages, - }, - }), - ); - // console.log('package.json written.'); - - // Ensure yarn uses the node-modules linker and not PnP - await writeFile( - path.join(testFolder, '.yarnrc.yml'), - `nodeLinker: node-modules`, - ); - - await new Promise((resolve, reject) => { - // we use the non-promise version so we can log everything on error - childProcess.execFile( - // we use yarn instead of npm as it will cache the remote packages and - // make installing things faster - 'yarn', - // We call explicitly with --no-immutable to prevent errors related to missing lock files in CI - ['install', '--no-immutable'], - { - cwd: testFolder, - }, - (err, stdout, stderr) => { - if (err) { - if (stdout.length > 0) { - console.warn(stdout); - } - if (stderr.length > 0) { - console.error(stderr); - } - // childProcess.ExecFileException is an extension of Error - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(err); - } else { - resolve(); - } - }, - ); - }); - // console.log('Install complete.'); - await executeTest(testFolder); }); - - afterAll(() => {}); }); }); } @@ -131,7 +30,8 @@ export function eslintIntegrationTest( ): void { integrationTest('eslint', testFilename, async testFolder => { // lint, outputting to a JSON file - const outFile = await tmpFile(); + const outFile = path.join(testFolder, 'eslint.json'); + let stderr = ''; try { await execFile( @@ -147,6 +47,7 @@ export function eslintIntegrationTest( ], { cwd: testFolder, + shell: true, }, ); } catch (ex) { @@ -161,12 +62,18 @@ export function eslintIntegrationTest( expect(stderr).toHaveLength(0); // assert the linting state is consistent - const lintOutputRAW = (await readFile(outFile, 'utf8')) + const lintOutputRAW = (await fs.readFile(outFile, { encoding: 'utf-8' })) // clean the output to remove any changing facets so tests are stable .replaceAll( new RegExp(`"filePath": ?"(/private)?${testFolder}`, 'g'), '"filePath": "', - ); + ) + .replaceAll( + /"filePath":"([^"]*)"/g, + (_, testFile: string) => + `"filePath": "/${path.relative(testFolder, testFile)}"`, + ) + .replaceAll(/C:\\\\(usr)\\\\(linked)\\\\(tsconfig.json)/g, '/$1/$2/$3'); try { const lintOutput = JSON.parse(lintOutputRAW); expect(lintOutput).toMatchSnapshot(); @@ -186,8 +93,9 @@ export function typescriptIntegrationTest( ): void { integrationTest(testName, testFilename, async testFolder => { const [result] = await Promise.allSettled([ - execFile('yarn', ['tsc', '--noEmit', ...tscArgs], { + execFile('yarn', ['tsc', '--noEmit', '--skipLibCheck', ...tscArgs], { cwd: testFolder, + shell: true, }), ]); diff --git a/packages/integration-tests/tools/pack-packages.ts b/packages/integration-tests/tools/pack-packages.ts index 129db0558ac3..cf775c564929 100644 --- a/packages/integration-tests/tools/pack-packages.ts +++ b/packages/integration-tests/tools/pack-packages.ts @@ -7,10 +7,16 @@ * against the exact same version of the package. */ -import { spawnSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; -import tmp from 'tmp'; +import * as child_process from 'node:child_process'; +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { promisify } from 'node:util'; + +import rootPackageJson from '../../../package.json'; + +export const execFile = promisify(child_process.execFile); interface PackageJSON { devDependencies: Record; @@ -19,36 +25,203 @@ interface PackageJSON { } const PACKAGES_DIR = path.resolve(__dirname, '..', '..'); -const PACKAGES = fs.readdirSync(PACKAGES_DIR); - -const tarFolder = tmp.dirSync({ - // because of how jest executes things, we need to ensure - // the temp files hang around - keep: true, -}).name; - -export const tseslintPackages: PackageJSON['devDependencies'] = {}; -for (const pkg of PACKAGES) { - const packageDir = path.join(PACKAGES_DIR, pkg); - const packagePath = path.join(packageDir, 'package.json'); - if (!fs.existsSync(packagePath)) { - continue; - } - // eslint-disable-next-line @typescript-eslint/no-require-imports -- this file needs to be sync and CJS for jest - const packageJson = require(packagePath) as PackageJSON; - if (packageJson.private === true) { - continue; - } +const INTEGRATION_TEST_DIR = path.join( + os.tmpdir() || os.homedir(), + 'typescript-eslint-integration-tests', +); + +const FIXTURES_DIR_BASENAME = 'fixtures'; + +export const FIXTURES_DESTINATION_DIR = path.join( + INTEGRATION_TEST_DIR, + FIXTURES_DIR_BASENAME, +); - const result = spawnSync('npm', ['pack', packageDir], { - cwd: tarFolder, +const YARN_RC_CONTENT = 'nodeLinker: node-modules\n\nenableGlobalCache: true\n'; + +const FIXTURES_DIR = path.join(__dirname, '..', FIXTURES_DIR_BASENAME); + +const TAR_FOLDER = path.join(INTEGRATION_TEST_DIR, 'tarballs'); + +export const setup = async (): Promise => { + const testFileBaseNames = await fs.readdir(FIXTURES_DIR, { encoding: 'utf-8', }); - const stdoutLines = result.stdout.trim().split('\n'); - const tarball = stdoutLines[stdoutLines.length - 1]; - tseslintPackages[packageJson.name] = `file:${path.join(tarFolder, tarball)}`; -} + const PACKAGES = await fs.readdir(PACKAGES_DIR, { + encoding: 'utf-8', + withFileTypes: true, + }); + + await fs.mkdir(FIXTURES_DESTINATION_DIR, { recursive: true }); + + await fs.mkdir(TAR_FOLDER, { recursive: true }); + + const tseslintPackages = Object.fromEntries( + ( + await Promise.all( + PACKAGES.map(async ({ name: pkg }) => { + const packageDir = path.join(PACKAGES_DIR, pkg); + const packagePath = path.join(packageDir, 'package.json'); + + try { + if (!(await fs.lstat(packagePath)).isFile()) { + return; + } + } catch { + return; + } + + const packageJson: PackageJSON = ( + await import(pathToFileURL(packagePath).href, { + with: { type: 'json' }, + }) + ).default; + + if ('private' in packageJson && packageJson.private === true) { + return; + } + + const result = await execFile('npm', ['pack', packageDir], { + cwd: TAR_FOLDER, + encoding: 'utf-8', + shell: true, + }); + + if (typeof result.stdout !== 'string') { + return; + } + + const stdoutLines = result.stdout.trim().split('\n'); + const tarball = stdoutLines[stdoutLines.length - 1]; + + return [ + packageJson.name, + `file:${path.join(TAR_FOLDER, tarball)}`, + ] as const; + }), + ) + ).filter(e => e != null), + ); + + const BASE_DEPENDENCIES: PackageJSON['devDependencies'] = { + ...tseslintPackages, + eslint: rootPackageJson.devDependencies.eslint, + typescript: rootPackageJson.devDependencies.typescript, + vitest: rootPackageJson.devDependencies.vitest, + }; + + const temp = await fs.mkdtemp(path.join(INTEGRATION_TEST_DIR, 'temp'), { + encoding: 'utf-8', + }); + + await fs.writeFile(path.join(temp, '.yarnrc.yml'), YARN_RC_CONTENT, { + encoding: 'utf-8', + }); + + await fs.writeFile( + path.join(temp, 'package.json'), + JSON.stringify( + { + devDependencies: BASE_DEPENDENCIES, + packageManager: rootPackageJson.packageManager, + private: true, + resolutions: tseslintPackages, + }, + null, + 2, + ), + { encoding: 'utf-8' }, + ); + + // We install the tarballs here once so that yarn can cache them globally. + // This solves 2 problems: + // 1. Tests can be run concurrently because they won't be trying to install + // the same tarballs at the same time. + // 2. Installing the tarballs for each test becomes much faster as Yarn can + // grab them from the global cache folder. + await execFile('yarn', ['install', '--no-immutable'], { + cwd: temp, + shell: true, + }); + + await Promise.all( + testFileBaseNames.map(async fixture => { + const testFolder = path.join(FIXTURES_DESTINATION_DIR, fixture); + + const fixtureDir = path.join(FIXTURES_DIR, fixture); + + const fixturePackageJson: PackageJSON = ( + await import( + pathToFileURL(path.join(fixtureDir, 'package.json')).href, + { with: { type: 'json' } } + ) + ).default; + + await fs.cp(fixtureDir, testFolder, { recursive: true }); + + await fs.writeFile( + path.join(testFolder, 'package.json'), + JSON.stringify( + { + private: true, + ...fixturePackageJson, + devDependencies: { + ...BASE_DEPENDENCIES, + ...fixturePackageJson.devDependencies, + }, + + packageManager: rootPackageJson.packageManager, + + // ensure everything uses the locally packed versions instead of the NPM versions + resolutions: { + ...tseslintPackages, + }, + }, + null, + 2, + ), + { encoding: 'utf-8' }, + ); + + await fs.writeFile( + path.join(testFolder, '.yarnrc.yml'), + YARN_RC_CONTENT, + { encoding: 'utf-8' }, + ); + + const { stderr, stdout } = await execFile( + 'yarn', + ['install', '--no-immutable'], + { + cwd: testFolder, + shell: true, + }, + ); + + if (stderr) { + console.error(stderr); + + if (stdout) { + console.log(stdout); + } + } + }), + ); + + await fs.rm(temp, { recursive: true }); + + console.log('Finished packing local packages.'); + + return tseslintPackages; +}; + +const teardown = async (): Promise => { + if (process.env.KEEP_INTEGRATION_TEST_DIR !== 'true') { + await fs.rm(INTEGRATION_TEST_DIR, { recursive: true }); + } +}; -console.log('Finished packing local packages.'); +// eslint-disable-next-line import/no-default-export +export default teardown; diff --git a/packages/integration-tests/tsconfig.spec.json b/packages/integration-tests/tsconfig.spec.json index f148576f18c4..9bd369767292 100644 --- a/packages/integration-tests/tsconfig.spec.json +++ b/packages/integration-tests/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../dist/out-tsc/packages/integration-tests", "module": "NodeNext", + "resolveJsonModule": true, "types": ["jest", "node"] }, "include": [ diff --git a/yarn.lock b/yarn.lock index 0fa7bf88a16c..1dfa02a057b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5756,15 +5756,6 @@ __metadata: languageName: node linkType: hard -"@types/ncp@npm:^2.0.8": - version: 2.0.8 - resolution: "@types/ncp@npm:2.0.8" - dependencies: - "@types/node": "*" - checksum: 33ab7fb6f6777b9b77114acb159acb565c24fed7d78ab19553b9621f1da38c8bf5976b8cb8d0c66864b5c1d08db6ca155978ba07b93bbc2e661fc3e535e2677f - languageName: node - linkType: hard - "@types/node-forge@npm:^1.3.0": version: 1.3.11 resolution: "@types/node-forge@npm:1.3.11" @@ -6075,8 +6066,6 @@ __metadata: dependencies: "@jest/types": 29.6.3 jest: 29.7.0 - ncp: "*" - tmp: "*" tsx: "*" languageName: unknown linkType: soft @@ -6225,7 +6214,6 @@ __metadata: "@types/jest": 29.5.13 "@types/jest-specific-snapshot": ^0.5.9 "@types/natural-compare": ^1.4.3 - "@types/ncp": ^2.0.8 "@types/node": ^20.12.5 "@types/semver": ^7.5.8 "@types/tmp": ^0.2.6 @@ -6264,13 +6252,11 @@ __metadata: lint-staged: ^15.2.2 make-dir: ^4.0.0 markdownlint-cli: ^0.44.0 - ncp: ^2.0.0 nx: 20.7.2 prettier: 3.5.0 pretty-format: ^29.7.0 rimraf: ^5.0.5 semver: 7.7.0 - tmp: ^0.2.1 tsx: "*" typescript: ">=4.8.4 <5.9.0" typescript-eslint: "workspace:^" @@ -15860,15 +15846,6 @@ __metadata: languageName: node linkType: hard -"ncp@npm:*, ncp@npm:^2.0.0": - version: 2.0.0 - resolution: "ncp@npm:2.0.0" - bin: - ncp: ./bin/ncp - checksum: ea9b19221da1d1c5529bdb9f8e85c9d191d156bcaae408cce5e415b7fbfd8744c288e792bd7faf1fe3b70fd44c74e22f0d43c39b209bc7ac1fb8016f70793a16 - languageName: node - linkType: hard - "negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3"