import fs from 'fs'; import { check } from '../../dist/actions'; import { TsModule } from '../../dist/module'; import { defaultInstallLibraries } from '../../dist/config'; import { getTsPackage, TsPackage } from '../../dist/ts-package'; import { PackageManager } from '../src/config'; import { prepareTestProject } from '../src/project'; import path from 'path'; import { InstallerOptions } from '../../dist'; import { LogLevel } from '../../dist/system'; import { execSync } from 'child_process'; import ts from 'typescript'; /* ****************************************************************************************************************** */ // region: Config /* ****************************************************************************************************************** */ const verboseMode = !!process.env.VERBOSE; // const verboseMode = true; /* Options to use with install/uninstall */ const testingPackageManagers = [ 'npm', 'yarn', 'pnpm', // 'yarn3' ] satisfies PackageManager[]; const tspOptions: Partial = { logLevel: verboseMode ? LogLevel.verbose : LogLevel.system, silent: !verboseMode }; // endregion /* ****************************************************************************************************************** */ // region: Helpers /* ****************************************************************************************************************** */ function getModulesSources(tsPackage: TsPackage, moduleNames?: string[]) { moduleNames ??= defaultInstallLibraries; return new Map(moduleNames.map(name => { const modulePath = tsPackage.getModulePath(name); const dtsPath = modulePath.replace(/\.js$/, '.d.ts'); const moduleContentPath = TsModule.getContentFilePathForModulePath(modulePath); const js = fs.readFileSync(moduleContentPath, 'utf-8'); const dts = fs.existsSync(dtsPath) ? fs.readFileSync(dtsPath, 'utf-8') : undefined; return [ name, { js, dts } ]; })); } function updatePackageJson(pkgPath: string, cb: (pkgJson: any) => void) { let pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); cb(pkgJson); fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2)); } function resetRequireCache(dir: string) { dir = path.dirname(require.resolve(dir)); for (const key in require.cache) { if (key.startsWith(dir)) { delete require.cache[key]; } } } function runAction(tspDir: string, kind: 'api' | 'cli', cmd: string) { switch (kind) { case 'api': const scriptCode = ` require('ts-patch').${cmd} `; fs.writeFileSync(path.join(tspDir, 'run-cmd.js'), scriptCode, 'utf-8'); execSync(`node run-cmd.js`, { cwd: tspDir }); break; case 'cli': const flags = verboseMode ? `--verbose` : '--silent'; execSync(`ts-patch ${cmd} ${flags}`, { cwd: tspDir }); } resetRequireCache(tspDir); const { getTsPackage } = require(path.join(tspDir, 'ts-package.js')); const { getTsModule } = require(path.join(tspDir, 'module')); const tsPackage = getTsPackage(); const modules = defaultInstallLibraries.map((m: any) => getTsModule(tsPackage, m)); return { modules, tsPackage }; } function runInstall(tspDir: string, kind: 'api' | 'cli') { let cmd: string; if (kind === 'api') { cmd = `install(${JSON.stringify(tspOptions)})`; } else { cmd = `install ${verboseMode ? '--verbose' : '--silent'}`; } return runAction(tspDir, kind, cmd); } function runUninstall(tspDir: string, kind: 'api' | 'cli') { let cmd: string; if (kind === 'api') { const tspOptions: Partial = { logLevel: LogLevel.verbose, }; cmd = `uninstall(${JSON.stringify(tspOptions)})`; } else { cmd = `uninstall --verbose`; } return runAction(tspDir, kind, cmd); } // endregion /* ******************************************************************************************************************** * Tests * ********************************************************************************************************************/ describe(`TSP Actions`, () => { // TODO - Parallelize describe.each(testingPackageManagers)(`%s`, (packageManager) => { /* Install */ describe(`Install`, () => { let projectPath: string; let tmpProjectPath: string; let tspDir: string; let tsDir: string; let cachePath: string; let originalModulesSrc: Map; beforeAll(() => { const prepRes = prepareTestProject({ projectName: 'main', packageManager }); projectPath = prepRes.projectPath; tmpProjectPath = prepRes.tmpProjectPath; const tsPackage = getTsPackage(tsDir); originalModulesSrc = getModulesSources(tsPackage); tspDir = path.resolve(tmpProjectPath, 'node_modules', 'ts-patch'); tsDir = path.resolve(tmpProjectPath, 'node_modules', 'typescript'); cachePath = path.resolve(tspDir, '../.cache/ts-patch'); }); describe.each([ [ '', '0.0.0' ], [ ' (overwrite w/ higher version)', '1.1.1' ] ])(`Install %s`, (caption, installedTspVersion) => { let tsPackage: TsPackage; let modules: TsModule[]; beforeAll(() => { /* Set version */ updatePackageJson(path.join(tspDir, 'package.json'), (pkgData) => pkgData.version = installedTspVersion); tsPackage = getTsPackage(tsDir); /* Install */ const runRes = runInstall(tspDir, 'api'); modules = runRes.modules; }); test(`Original modules backed up`, () => { for (const m of modules) { const origSrcEntry = originalModulesSrc.get(m.moduleName)!; if (m.dtsPath) { const backupSrc = fs.readFileSync(m.backupCachePaths.dts!, 'utf-8'); expect(backupSrc).toBe(origSrcEntry.dts); } const backupSrc = fs.readFileSync(m.backupCachePaths.js!, 'utf-8'); expect(backupSrc).toBe(origSrcEntry.js); } }); test(`All modules patched`, () => { modules.forEach(m => { expect(m.isPatched).toBe(true); expect(m.moduleFile.patchDetail?.tspVersion).toBe(installedTspVersion); }) }); test(`check() is accurate`, () => { const checkResult = check(undefined, tspOptions); const unpatchedModuleNames = tsPackage.moduleNames.filter(m => !defaultInstallLibraries.includes(m)); unpatchedModuleNames.forEach(m => expect(checkResult[m]).toBeUndefined()); defaultInstallLibraries.forEach(m => expect(checkResult[m]?.tspVersion).toBe(installedTspVersion)); }); test(`No semantic errors in typescript.d.ts`, () => { const dtsFilePath = path.join(tsDir, 'typescript.d.ts'); const compilerOptions = Object.assign(ts.getDefaultCompilerOptions(), { target: ts.ScriptTarget.ES2018, lib: [ 'es2018' ], skipDefaultLibCheck: true }); const program = ts.createProgram([ dtsFilePath ], compilerOptions); const diagnostics = program.getSemanticDiagnostics(); // Using toHaveLength causes indefinite hang expect(diagnostics.length).toBe(0); }); }); }); /* Uninstall */ describe(`Uninstall`, () => { let projectPath: string; let tmpProjectPath: string; let tspDir: string; let tsDir: string; let cachePath: string; let modules: TsModule[]; let originalModulesSrc: Map; let tsPackage: TsPackage; beforeAll(() => { const prepRes = prepareTestProject({ projectName: 'main', packageManager }); projectPath = prepRes.projectPath; tmpProjectPath = prepRes.tmpProjectPath; tsPackage = getTsPackage(tsDir); originalModulesSrc = getModulesSources(tsPackage); tspDir = path.resolve(tmpProjectPath, 'node_modules', 'ts-patch'); tsDir = path.resolve(tmpProjectPath, 'node_modules', 'typescript'); cachePath = path.resolve(tspDir, '../.cache/ts-patch'); /* Install */ let runRes = runInstall(tspDir, 'api'); modules = runRes.modules; modules.forEach(m => { expect(m.isPatched).toBe(true); }); /* Uninstall */ runRes = runUninstall(tspDir, 'api'); modules = runRes.modules; }); test(`All modules unpatched`, () => { modules.forEach(m => { expect(m.isPatched).toBe(false); expect(m.moduleFile.patchDetail).toBeUndefined(); }); }); test(`All files match originals`, () => { for (const m of modules) { const origSrcEntry = originalModulesSrc.get(m.moduleName)!; if (m.dtsPath) { const src = fs.readFileSync(m.dtsPath, 'utf-8'); expect(src).toBe(origSrcEntry.dts); } const contentFilePath = TsModule.getContentFilePathForModulePath(m.modulePath); const src = fs.readFileSync(contentFilePath, 'utf-8'); expect(src).toBe(origSrcEntry.js); } }); test(`check() is accurate`, () => { const checkResult = check(undefined, tspOptions); tsPackage.moduleNames.forEach(m => expect(checkResult[m]).toBeUndefined()); }); }); }); });