UNPKG

6.51 kBPlain TextView Raw
1import path from 'path';
2
3import type { execaCommand } from 'execa';
4import { npmRunPathEnv } from 'npm-run-path';
5import { StrykerOptions } from '@stryker-mutator/api/core';
6import { normalizeWhitespaces, I } from '@stryker-mutator/util';
7import { Logger } from '@stryker-mutator/api/logging';
8import { tokens, commonTokens, Disposable } from '@stryker-mutator/api/plugin';
9
10import { TemporaryDirectory } from '../utils/temporary-directory.js';
11import { fileUtils } from '../utils/file-utils.js';
12import { coreTokens } from '../di/index.js';
13import { UnexpectedExitHandler } from '../unexpected-exit-handler.js';
14import { ProjectFile } from '../fs/index.js';
15import { Project } from '../fs/project.js';
16import { objectUtils } from '../utils/index.js';
17
18export class Sandbox implements Disposable {
19 private readonly fileMap = new Map<string, string>();
20
21 /**
22 * The working directory for this sandbox
23 * Either an actual sandbox directory, or the cwd when running in --inPlace mode
24 */
25 public readonly workingDirectory: string;
26 /**
27 * The backup directory when running in --inPlace mode
28 */
29 private readonly backupDirectory: string = '';
30 /**
31 * The sandbox dir or the backup dir when running in `--inPlace` mode
32 */
33 private readonly tempDirectory: string;
34
35 public static readonly inject = tokens(
36 commonTokens.options,
37 commonTokens.logger,
38 coreTokens.temporaryDirectory,
39 coreTokens.project,
40 coreTokens.execa,
41 coreTokens.unexpectedExitRegistry
42 );
43
44 constructor(
45 private readonly options: StrykerOptions,
46 private readonly log: Logger,
47 private readonly temporaryDirectory: I<TemporaryDirectory>,
48 private readonly project: Project,
49 private readonly execCommand: typeof execaCommand,
50 unexpectedExitHandler: I<UnexpectedExitHandler>
51 ) {
52 if (options.inPlace) {
53 this.workingDirectory = process.cwd();
54 this.backupDirectory = temporaryDirectory.getRandomDirectory('backup');
55 this.tempDirectory = this.backupDirectory;
56 this.log.info(
57 'In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: %s',
58 path.relative(process.cwd(), this.backupDirectory)
59 );
60 unexpectedExitHandler.registerHandler(this.dispose.bind(this, true));
61 } else {
62 this.workingDirectory = temporaryDirectory.getRandomDirectory('sandbox');
63 this.tempDirectory = this.workingDirectory;
64 this.log.debug('Creating a sandbox for files in %s', this.workingDirectory);
65 }
66 }
67
68 public async init(): Promise<void> {
69 await this.temporaryDirectory.createDirectory(this.tempDirectory);
70 await this.fillSandbox();
71 await this.runBuildCommand();
72 await this.symlinkNodeModulesIfNeeded();
73 }
74
75 public sandboxFileFor(fileName: string): string {
76 const sandboxFileName = this.fileMap.get(fileName);
77 if (sandboxFileName === undefined) {
78 throw new Error(`Cannot find sandbox file for ${fileName}`);
79 }
80 return sandboxFileName;
81 }
82
83 public originalFileFor(sandboxFileName: string): string {
84 return path.resolve(sandboxFileName).replace(this.workingDirectory, process.cwd());
85 }
86
87 private async fillSandbox(): Promise<void> {
88 await Promise.all(objectUtils.map(this.project.files, (file, name) => this.sandboxFile(name, file)));
89 }
90
91 private async runBuildCommand() {
92 if (this.options.buildCommand) {
93 const env = npmRunPathEnv();
94 this.log.info('Running build command "%s" in "%s".', this.options.buildCommand, this.workingDirectory);
95 this.log.debug('(using PATH: %s)', env.PATH);
96 await this.execCommand(this.options.buildCommand, { cwd: this.workingDirectory, env });
97 }
98 }
99
100 private async symlinkNodeModulesIfNeeded(): Promise<void> {
101 this.log.debug('Start symlink node_modules');
102 if (this.options.symlinkNodeModules && !this.options.inPlace) {
103 // TODO: Change with this.options.basePath when we have it
104 const basePath = process.cwd();
105 const nodeModulesList = await fileUtils.findNodeModulesList(basePath, this.options.tempDirName);
106
107 if (nodeModulesList.length > 0) {
108 for (const nodeModules of nodeModulesList) {
109 this.log.debug(`Create symlink from ${path.resolve(nodeModules)} to ${path.join(this.workingDirectory, nodeModules)}`);
110 await fileUtils
111 .symlinkJunction(path.resolve(nodeModules), path.join(this.workingDirectory, nodeModules))
112 .catch((error: NodeJS.ErrnoException) => {
113 if (error.code === 'EEXIST') {
114 this.log.warn(
115 normalizeWhitespaces(`Could not symlink "${nodeModules}" in sandbox directory,
116 it is already created in the sandbox. Please remove the node_modules from your sandbox files.
117 Alternatively, set \`symlinkNodeModules\` to \`false\` to disable this warning.`)
118 );
119 } else {
120 this.log.warn(`Unexpected error while trying to symlink "${nodeModules}" in sandbox directory.`, error);
121 }
122 });
123 }
124 } else {
125 this.log.debug(`Could not find a node_modules folder to symlink into the sandbox directory. Search "${basePath}" and its parent directories`);
126 }
127 }
128 }
129
130 /**
131 * Sandboxes a file (writes it to the sandbox). Either in-place, or an actual sandbox directory.
132 * @param name The name of the file
133 * @param file The file reference
134 */
135 private async sandboxFile(name: string, file: ProjectFile): Promise<void> {
136 if (this.options.inPlace) {
137 if (file.hasChanges) {
138 // File is changed (either mutated or by a preprocessor), make a backup and replace in-place
139 const backupFileName = await file.backupTo(this.backupDirectory);
140 this.log.debug('Stored backup file at %s', backupFileName);
141 await file.writeInPlace();
142 }
143 this.fileMap.set(name, name);
144 } else {
145 const targetFileName = await file.writeToSandbox(this.workingDirectory);
146 this.fileMap.set(name, targetFileName);
147 }
148 }
149
150 public dispose(unexpected = false): void {
151 if (this.backupDirectory) {
152 if (unexpected) {
153 console.error(`Detecting unexpected exit, recovering original files from ${path.relative(process.cwd(), this.backupDirectory)}`);
154 } else {
155 this.log.info(`Resetting your original files from ${path.relative(process.cwd(), this.backupDirectory)}.`);
156 }
157 fileUtils.moveDirectoryRecursiveSync(this.backupDirectory, this.workingDirectory);
158 }
159 }
160}