1 | import path from 'path';
|
2 |
|
3 | import type { execaCommand } from 'execa';
|
4 | import { npmRunPathEnv } from 'npm-run-path';
|
5 | import { StrykerOptions } from '@stryker-mutator/api/core';
|
6 | import { normalizeWhitespaces, I } from '@stryker-mutator/util';
|
7 | import { Logger } from '@stryker-mutator/api/logging';
|
8 | import { tokens, commonTokens, Disposable } from '@stryker-mutator/api/plugin';
|
9 |
|
10 | import { TemporaryDirectory } from '../utils/temporary-directory.js';
|
11 | import { fileUtils } from '../utils/file-utils.js';
|
12 | import { coreTokens } from '../di/index.js';
|
13 | import { UnexpectedExitHandler } from '../unexpected-exit-handler.js';
|
14 | import { ProjectFile } from '../fs/index.js';
|
15 | import { Project } from '../fs/project.js';
|
16 | import { objectUtils } from '../utils/index.js';
|
17 |
|
18 | export class Sandbox implements Disposable {
|
19 | private readonly fileMap = new Map<string, string>();
|
20 |
|
21 | |
22 |
|
23 |
|
24 |
|
25 | public readonly workingDirectory: string;
|
26 | |
27 |
|
28 |
|
29 | private readonly backupDirectory: string = '';
|
30 | |
31 |
|
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 |
|
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 |
|
132 |
|
133 |
|
134 |
|
135 | private async sandboxFile(name: string, file: ProjectFile): Promise<void> {
|
136 | if (this.options.inPlace) {
|
137 | if (file.hasChanges) {
|
138 |
|
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 | }
|