From 64fe0634178179e7a5e222162e04d93f754068df Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 27 Mar 2025 02:04:17 -0500 Subject: [PATCH 1/4] chore: fix `integration-tests` --- knip.ts | 2 - package.json | 3 - packages/integration-tests/jest.config.js | 14 ++- packages/integration-tests/package.json | 2 - .../tools/integration-test-base.ts | 116 +++++++++--------- .../integration-tests/tools/pack-packages.ts | 112 ++++++++++++----- packages/integration-tests/tsconfig.spec.json | 1 + yarn.lock | 23 ---- 8 files changed, 149 insertions(+), 124 deletions(-) diff --git a/knip.ts b/knip.ts index 594dc6e49d92..20e2709707c4 100644 --- a/knip.ts +++ b/knip.ts @@ -29,8 +29,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 6aeb68662580..9563725dea9b 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", @@ -116,13 +115,11 @@ "lint-staged": "^15.2.2", "make-dir": "^4.0.0", "markdownlint-cli": "^0.44.0", - "ncp": "^2.0.0", "nx": "20.4.5", "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..60525fef13de 100644 --- a/packages/integration-tests/jest.config.js +++ b/packages/integration-tests/jest.config.js @@ -2,20 +2,22 @@ // 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'); +const os = require('node:os'); // @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, -}; + maxWorkers: os.platform() === 'win32' ? 1 : os.cpus().length - 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..5706d095c1e5 100644 --- a/packages/integration-tests/tools/integration-test-base.ts +++ b/packages/integration-tests/tools/integration-test-base.ts @@ -1,43 +1,25 @@ -import type { DirOptions } from 'tmp'; - -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; -} +import * as childProcess from 'node:child_process'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; -const rootPackageJson: PackageJSON = require('../../../package.json'); +import type { PackageJSON } from './pack-packages.js'; -tmp.setGracefulCleanup(); +import rootPackageJson from '../../../package.json'; +import { execFile, homeOrTmpDir } from './pack-packages'; -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 { tseslintPackages } = global; const BASE_DEPENDENCIES: PackageJSON['devDependencies'] = { - ...global.tseslintPackages, + ...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'; // make sure that jest doesn't timeout the test -jest.setTimeout(60000); +jest.setTimeout(60_000); function integrationTest( testName: string, @@ -50,41 +32,51 @@ function integrationTest( describe(testName, () => { it('should work successfully', async () => { - const testFolder = await tmpDir({ - keep: KEEP_INTEGRATION_TEST_DIR, - }); - if (KEEP_INTEGRATION_TEST_DIR) { - console.error(testFolder); - } + const testFolder = path.join( + homeOrTmpDir, + 'typescript-eslint-integration-tests', + fixture, + ); + + await fs.mkdir(testFolder, { recursive: true }); // copy the fixture files to the temp folder - await copyDir(fixtureDir, testFolder); + await fs.cp(fixtureDir, testFolder, { recursive: true }); // build and write the package.json for the test - const fixturePackageJson: PackageJSON = await import( - path.join(fixtureDir, 'package.json') - ); - await writeFile( + const fixturePackageJson: PackageJSON = ( + await import(path.join(fixtureDir, 'package.json'), { + with: { type: 'json' }, + }) + ).default; + + await fs.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, + JSON.stringify( + { + private: true, + ...fixturePackageJson, + devDependencies: { + ...BASE_DEPENDENCIES, + ...fixturePackageJson.devDependencies, + }, + // ensure everything uses the locally packed versions instead of the NPM versions + resolutions: { + ...tseslintPackages, + }, }, - }), + null, + 2, + ), + { encoding: 'utf-8' }, ); // console.log('package.json written.'); // Ensure yarn uses the node-modules linker and not PnP - await writeFile( + await fs.writeFile( path.join(testFolder, '.yarnrc.yml'), - `nodeLinker: node-modules`, + `nodeLinker: node-modules\n`, + { encoding: 'utf-8' }, ); await new Promise((resolve, reject) => { @@ -97,6 +89,7 @@ function integrationTest( ['install', '--no-immutable'], { cwd: testFolder, + shell: true, }, (err, stdout, stderr) => { if (err) { @@ -119,8 +112,6 @@ function integrationTest( await executeTest(testFolder); }); - - afterAll(() => {}); }); }); } @@ -131,7 +122,9 @@ 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'); + + await fs.writeFile(outFile, '', { encoding: 'utf-8' }); let stderr = ''; try { await execFile( @@ -147,6 +140,7 @@ export function eslintIntegrationTest( ], { cwd: testFolder, + shell: true, }, ); } catch (ex) { @@ -161,11 +155,20 @@ 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, + path.posix.join('/usr', 'linked', 'tsconfig.json'), ); try { const lintOutput = JSON.parse(lintOutputRAW); @@ -186,8 +189,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..a23f1e7d1864 100644 --- a/packages/integration-tests/tools/pack-packages.ts +++ b/packages/integration-tests/tools/pack-packages.ts @@ -7,48 +7,96 @@ * 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'; -interface PackageJSON { +export const execFile = promisify(child_process.execFile); + +export interface PackageJSON { devDependencies: Record; name: string; private?: boolean; } 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; - } +export const homeOrTmpDir = os.tmpdir() || os.homedir(); - const result = spawnSync('npm', ['pack', packageDir], { - cwd: tarFolder, +const tarFolder = path.join( + homeOrTmpDir, + 'typescript-eslint-integration-tests', + 'tarballs', +); + +export const setup = async (): Promise => { + const PACKAGES = await fs.readdir(PACKAGES_DIR, { encoding: 'utf-8', + withFileTypes: true, }); - const stdoutLines = result.stdout.trim().split('\n'); - const tarball = stdoutLines[stdoutLines.length - 1]; - tseslintPackages[packageJson.name] = `file:${path.join(tarFolder, tarball)}`; -} + await fs.mkdir(tarFolder, { 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.stat(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: tarFolder, + 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(tarFolder, tarball)}`, + ] as const; + }), + ) + ).filter(e => e != null), + ); + + console.log('Finished packing local packages.'); + + return tseslintPackages; +}; + +const teardown = async (): Promise => { + if (process.env.KEEP_INTEGRATION_TEST_DIR !== 'true') { + await fs.rm(path.dirname(tarFolder), { 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 965495e5a1ad..21f30946627f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5794,15 +5794,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" @@ -6113,8 +6104,6 @@ __metadata: dependencies: "@jest/types": 29.6.3 jest: 29.7.0 - ncp: "*" - tmp: "*" tsx: "*" languageName: unknown linkType: soft @@ -6263,7 +6252,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 @@ -6301,13 +6289,11 @@ __metadata: lint-staged: ^15.2.2 make-dir: ^4.0.0 markdownlint-cli: ^0.44.0 - ncp: ^2.0.0 nx: 20.4.5 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:^" @@ -15915,15 +15901,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" From fb37fc529426375a33d953d9eb46ccb02d65ee15 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 3 Apr 2025 01:56:59 -0500 Subject: [PATCH 2/4] chore: fix `integration-tests` on Windows --- .../tools/integration-test-base.ts | 38 ++----------------- .../integration-tests/tools/pack-packages.ts | 2 +- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/packages/integration-tests/tools/integration-test-base.ts b/packages/integration-tests/tools/integration-test-base.ts index 5706d095c1e5..973c3ceaf39b 100644 --- a/packages/integration-tests/tools/integration-test-base.ts +++ b/packages/integration-tests/tools/integration-test-base.ts @@ -1,4 +1,3 @@ -import * as childProcess from 'node:child_process'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; @@ -79,36 +78,10 @@ function integrationTest( { encoding: 'utf-8' }, ); - 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, - shell: true, - }, - (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(); - } - }, - ); + await execFile('yarn', ['install', '--no-immutable'], { + cwd: testFolder, + shell: true, }); - // console.log('Install complete.'); await executeTest(testFolder); }); @@ -166,10 +139,7 @@ export function eslintIntegrationTest( (_, testFile: string) => `"filePath": "/${path.relative(testFolder, testFile)}"`, ) - .replaceAll( - /C:\\\\usr\\\\linked\\\\tsconfig.json/g, - path.posix.join('/usr', 'linked', 'tsconfig.json'), - ); + .replaceAll(/C:\\\\(usr)\\\\(linked)\\\\(tsconfig.json)/g, '/$1/$2/$3'); try { const lintOutput = JSON.parse(lintOutputRAW); expect(lintOutput).toMatchSnapshot(); diff --git a/packages/integration-tests/tools/pack-packages.ts b/packages/integration-tests/tools/pack-packages.ts index a23f1e7d1864..4036e05fcf86 100644 --- a/packages/integration-tests/tools/pack-packages.ts +++ b/packages/integration-tests/tools/pack-packages.ts @@ -48,7 +48,7 @@ export const setup = async (): Promise => { const packagePath = path.join(packageDir, 'package.json'); try { - if (!(await fs.stat(packagePath)).isFile()) { + if (!(await fs.lstat(packagePath)).isFile()) { return; } } catch { From fbe5951c0a0e24fb84bdd7e22471bffa2c8a8d1d Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 6 Apr 2025 04:18:23 -0500 Subject: [PATCH 3/4] chore: fix `integration-tests` on Windows --- packages/integration-tests/jest.config.js | 6 - .../tools/integration-test-base.ts | 74 +--------- .../integration-tests/tools/pack-packages.ts | 139 ++++++++++++++++-- 3 files changed, 133 insertions(+), 86 deletions(-) diff --git a/packages/integration-tests/jest.config.js b/packages/integration-tests/jest.config.js index 60525fef13de..ce0a347c52e1 100644 --- a/packages/integration-tests/jest.config.js +++ b/packages/integration-tests/jest.config.js @@ -3,7 +3,6 @@ // pack the packages ahead of time and create a mapping for use in the tests require('tsx/cjs'); const { setup } = require('./tools/pack-packages'); -const os = require('node:os'); // @ts-check /** @type {() => Promise} */ @@ -15,9 +14,4 @@ module.exports = async () => ({ 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: os.platform() === 'win32' ? 1 : os.cpus().length - 1, }); diff --git a/packages/integration-tests/tools/integration-test-base.ts b/packages/integration-tests/tools/integration-test-base.ts index 973c3ceaf39b..3b6d49936407 100644 --- a/packages/integration-tests/tools/integration-test-base.ts +++ b/packages/integration-tests/tools/integration-test-base.ts @@ -1,21 +1,7 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; -import type { PackageJSON } from './pack-packages.js'; - -import rootPackageJson from '../../../package.json'; -import { execFile, homeOrTmpDir } from './pack-packages'; - -const { tseslintPackages } = global; - -const BASE_DEPENDENCIES: PackageJSON['devDependencies'] = { - ...tseslintPackages, - eslint: rootPackageJson.devDependencies.eslint, - jest: rootPackageJson.devDependencies.jest, - typescript: rootPackageJson.devDependencies.typescript, -}; - -const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures'); +import { execFile, FIXTURES_DESTINATION_DIR } from './pack-packages'; // make sure that jest doesn't timeout the test jest.setTimeout(60_000); @@ -26,63 +12,12 @@ 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 = path.join( - homeOrTmpDir, - 'typescript-eslint-integration-tests', - fixture, - ); - - await fs.mkdir(testFolder, { recursive: true }); - - // copy the fixture files to the temp folder - await fs.cp(fixtureDir, testFolder, { recursive: true }); - - // build and write the package.json for the test - const fixturePackageJson: PackageJSON = ( - await import(path.join(fixtureDir, 'package.json'), { - with: { type: 'json' }, - }) - ).default; - - await fs.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: { - ...tseslintPackages, - }, - }, - null, - 2, - ), - { encoding: 'utf-8' }, - ); - // console.log('package.json written.'); - - // Ensure yarn uses the node-modules linker and not PnP - await fs.writeFile( - path.join(testFolder, '.yarnrc.yml'), - `nodeLinker: node-modules\n`, - { encoding: 'utf-8' }, - ); - - await execFile('yarn', ['install', '--no-immutable'], { - cwd: testFolder, - shell: true, - }); - await executeTest(testFolder); }); }); @@ -97,7 +32,6 @@ export function eslintIntegrationTest( // lint, outputting to a JSON file const outFile = path.join(testFolder, 'eslint.json'); - await fs.writeFile(outFile, '', { encoding: 'utf-8' }); let stderr = ''; try { await execFile( diff --git a/packages/integration-tests/tools/pack-packages.ts b/packages/integration-tests/tools/pack-packages.ts index 4036e05fcf86..8b49d5df9733 100644 --- a/packages/integration-tests/tools/pack-packages.ts +++ b/packages/integration-tests/tools/pack-packages.ts @@ -14,9 +14,11 @@ 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); -export interface PackageJSON { +interface PackageJSON { devDependencies: Record; name: string; private?: boolean; @@ -24,21 +26,37 @@ export interface PackageJSON { const PACKAGES_DIR = path.resolve(__dirname, '..', '..'); -export const homeOrTmpDir = os.tmpdir() || os.homedir(); - -const tarFolder = path.join( - homeOrTmpDir, +const INTEGRATION_TEST_DIR = path.join( + os.tmpdir() || os.homedir(), 'typescript-eslint-integration-tests', - 'tarballs', ); +const FIXTURES_DIR_BASENAME = 'fixtures'; + +export const FIXTURES_DESTINATION_DIR = path.join( + INTEGRATION_TEST_DIR, + FIXTURES_DIR_BASENAME, +); + +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 PACKAGES = await fs.readdir(PACKAGES_DIR, { encoding: 'utf-8', withFileTypes: true, }); - await fs.mkdir(tarFolder, { recursive: true }); + await fs.mkdir(FIXTURES_DESTINATION_DIR, { recursive: true }); + + await fs.mkdir(TAR_FOLDER, { recursive: true }); const tseslintPackages = Object.fromEntries( ( @@ -66,7 +84,7 @@ export const setup = async (): Promise => { } const result = await execFile('npm', ['pack', packageDir], { - cwd: tarFolder, + cwd: TAR_FOLDER, encoding: 'utf-8', shell: true, }); @@ -80,13 +98,114 @@ export const setup = async (): Promise => { return [ packageJson.name, - `file:${path.join(tarFolder, tarball)}`, + `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' }, + ); + + 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; @@ -94,7 +213,7 @@ export const setup = async (): Promise => { const teardown = async (): Promise => { if (process.env.KEEP_INTEGRATION_TEST_DIR !== 'true') { - await fs.rm(path.dirname(tarFolder), { recursive: true }); + await fs.rm(INTEGRATION_TEST_DIR, { recursive: true }); } }; From 85eb947bac5179db8bd2049c642714b1ef648da3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 7 Apr 2025 10:29:00 -0500 Subject: [PATCH 4/4] chore: fix `integration-tests` on Windows --- packages/integration-tests/tools/pack-packages.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/integration-tests/tools/pack-packages.ts b/packages/integration-tests/tools/pack-packages.ts index 8b49d5df9733..cf775c564929 100644 --- a/packages/integration-tests/tools/pack-packages.ts +++ b/packages/integration-tests/tools/pack-packages.ts @@ -135,6 +135,12 @@ export const setup = async (): Promise => { { 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,