Skip to content
This repository was archived by the owner on May 1, 2020. It is now read-only.

Commit f5529b5

Browse files
committed
fix(source-maps): fix race condition between copying and purging source maps
1 parent 47e6a95 commit f5529b5

File tree

2 files changed

+116
-42
lines changed

2 files changed

+116
-42
lines changed

src/util/source-maps.spec.ts

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,87 @@ import * as helpers from './helpers';
55

66
describe('source maps', () => {
77
describe('purgeSourceMapsIfNeeded', () => {
8-
it('should return a promise call unlink on all files with a .map extensin', () => {
9-
// arrange
10-
let env: any = {};
11-
env[Constants.ENV_VAR_GENERATE_SOURCE_MAP] = null;
12-
process.env = env;
13-
const buildDir = '/some/fake/build/dir';
14-
const context = { buildDir: buildDir };
15-
16-
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(['test.js', 'test.js.map', 'test2.js', 'test2.js.map']));
17-
const unlinkSpy = spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
18-
19-
// act
20-
const resultPromise = sourceMaps.purgeSourceMapsIfNeeded(context);
21-
22-
// assert
23-
return resultPromise.then(() => {
24-
expect(unlinkSpy.calls.argsFor(0)[0]).toEqual(join(buildDir, 'test.js.map'));
25-
expect(unlinkSpy.calls.argsFor(1)[0]).toEqual(join(buildDir, 'test2.js.map'));
8+
it('should copy files first, then purge the files', async () => {
9+
spyOn(helpers, helpers.getBooleanPropertyValue.name).and.callFake((argument: string) => {
10+
if (argument === Constants.ENV_VAR_MOVE_SOURCE_MAPS) {
11+
return true;
12+
}
2613
});
14+
15+
spyOn(helpers, helpers.mkDirpAsync.name).and.returnValue(Promise.resolve());
16+
17+
const knownFileNames = ['0.js', '0.js.map', '1.js', '1.js.map', 'main.js', 'main.js.map', 'vendor.js', 'vendor.js.map', 'main.css', 'polyfills.js', 'sw-toolbox.js', 'main.css', 'main.css.map'];
18+
19+
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(knownFileNames));
20+
21+
const context = {
22+
sourcemapDir: join(process.cwd(), 'sourceMapDir'),
23+
buildDir: join(process.cwd(), 'www', 'build')
24+
};
25+
26+
const copyFileSpy = spyOn(helpers, helpers.copyFileAsync.name).and.returnValue(Promise.resolve());
27+
const unlinkFileSpy = spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
28+
29+
const result = await sourceMaps.copySourcemaps(context, true);
30+
expect(helpers.mkDirpAsync).toHaveBeenCalledTimes(1);
31+
expect(helpers.mkDirpAsync).toHaveBeenCalledWith(context.sourcemapDir);
32+
33+
expect(helpers.readDirAsync).toHaveBeenCalledTimes(1);
34+
expect(helpers.readDirAsync).toHaveBeenLastCalledWith(context.buildDir);
35+
36+
expect(helpers.copyFileAsync).toHaveBeenCalledTimes(3);
37+
expect(copyFileSpy.calls.all()[0].args[0]).toEqual(join(context.buildDir, '0.js.map'));
38+
expect(copyFileSpy.calls.all()[0].args[1]).toEqual(join(context.sourcemapDir, '0.js.map'));
39+
expect(copyFileSpy.calls.all()[1].args[0]).toEqual(join(context.buildDir, '1.js.map'));
40+
expect(copyFileSpy.calls.all()[1].args[1]).toEqual(join(context.sourcemapDir, '1.js.map'));
41+
expect(copyFileSpy.calls.all()[2].args[0]).toEqual(join(context.buildDir, 'main.js.map'));
42+
expect(copyFileSpy.calls.all()[2].args[1]).toEqual(join(context.sourcemapDir, 'main.js.map'));
43+
44+
expect(helpers.unlinkAsync).toHaveBeenCalledTimes(5);
45+
expect(unlinkFileSpy.calls.all()[0].args[0]).toEqual(join(context.buildDir, '0.js.map'));
46+
expect(unlinkFileSpy.calls.all()[1].args[0]).toEqual(join(context.buildDir, '1.js.map'));
47+
expect(unlinkFileSpy.calls.all()[2].args[0]).toEqual(join(context.buildDir, 'main.js.map'));
48+
expect(unlinkFileSpy.calls.all()[3].args[0]).toEqual(join(context.buildDir, 'vendor.js.map'));
49+
expect(unlinkFileSpy.calls.all()[4].args[0]).toEqual(join(context.buildDir, 'main.css.map'));
50+
});
51+
52+
it('should copy the files but not purge them after', async () => {
53+
spyOn(helpers, helpers.getBooleanPropertyValue.name).and.callFake((argument: string) => {
54+
if (argument === Constants.ENV_VAR_MOVE_SOURCE_MAPS) {
55+
return true;
56+
}
57+
});
58+
59+
spyOn(helpers, helpers.mkDirpAsync.name).and.returnValue(Promise.resolve());
60+
61+
const knownFileNames = ['0.js', '0.js.map', '1.js', '1.js.map', 'main.js', 'main.js.map', 'vendor.js', 'vendor.js.map', 'main.css', 'polyfills.js', 'sw-toolbox.js', 'main.css', 'main.css.map'];
62+
63+
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(knownFileNames));
64+
65+
const context = {
66+
sourcemapDir: join(process.cwd(), 'sourceMapDir'),
67+
buildDir: join(process.cwd(), 'www', 'build')
68+
};
69+
70+
const copyFileSpy = spyOn(helpers, helpers.copyFileAsync.name).and.returnValue(Promise.resolve());
71+
const unlinkFileSpy = spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
72+
73+
const result = await sourceMaps.copySourcemaps(context, false);
74+
expect(helpers.mkDirpAsync).toHaveBeenCalledTimes(1);
75+
expect(helpers.mkDirpAsync).toHaveBeenCalledWith(context.sourcemapDir);
76+
77+
expect(helpers.readDirAsync).toHaveBeenCalledTimes(1);
78+
expect(helpers.readDirAsync).toHaveBeenLastCalledWith(context.buildDir);
79+
80+
expect(helpers.copyFileAsync).toHaveBeenCalledTimes(3);
81+
expect(copyFileSpy.calls.all()[0].args[0]).toEqual(join(context.buildDir, '0.js.map'));
82+
expect(copyFileSpy.calls.all()[0].args[1]).toEqual(join(context.sourcemapDir, '0.js.map'));
83+
expect(copyFileSpy.calls.all()[1].args[0]).toEqual(join(context.buildDir, '1.js.map'));
84+
expect(copyFileSpy.calls.all()[1].args[1]).toEqual(join(context.sourcemapDir, '1.js.map'));
85+
expect(copyFileSpy.calls.all()[2].args[0]).toEqual(join(context.buildDir, 'main.js.map'));
86+
expect(copyFileSpy.calls.all()[2].args[1]).toEqual(join(context.sourcemapDir, 'main.js.map'));
87+
88+
expect(helpers.unlinkAsync).toHaveBeenCalledTimes(0);
2789
});
2890
});
2991
});

src/util/source-maps.ts

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,50 @@
11
import { join, relative, basename } from 'path';
2-
import { mkdirpSync } from 'fs-extra';
2+
import { ensureDir, mkdirpSync } from 'fs-extra';
33
import * as Constants from './constants';
4-
import { copyFileAsync, getBooleanPropertyValue, readDirAsync, unlinkAsync } from './helpers';
4+
import { copyFileAsync, getBooleanPropertyValue, mkDirpAsync, readDirAsync, unlinkAsync } from './helpers';
55
import { BuildContext } from './interfaces';
66

7-
function copySourcemaps(context: BuildContext, shouldPurge: boolean) {
8-
return readDirAsync(context.buildDir).then((fileNames) => {
9-
const sourceMaps = fileNames.filter(fileName => fileName.endsWith('.map'));
10-
const fullPaths = sourceMaps.map(sourceMap => join(context.buildDir, sourceMap));
11-
const promises: Promise<void>[] = [];
12-
const copyBeforePurge = getBooleanPropertyValue(Constants.ENV_VAR_MOVE_SOURCE_MAPS);
13-
for (const fullPath of fullPaths) {
14-
if (copyBeforePurge) {
15-
mkdirpSync(context.sourcemapDir);
16-
const relativeTo = relative(fullPath, context.sourcemapDir);
17-
const fileName = basename(fullPath);
18-
if (fileName.indexOf('vendor.js') < 0) {
19-
promises.push(copyFileAsync(fullPath, join(context.sourcemapDir, fileName)));
20-
}
21-
}
22-
23-
if (shouldPurge) {
24-
promises.push(unlinkAsync(fullPath));
25-
}
7+
export async function copySourcemaps(context: BuildContext, shouldPurge: boolean) {
8+
const copyBeforePurge = getBooleanPropertyValue(Constants.ENV_VAR_MOVE_SOURCE_MAPS);
9+
if (copyBeforePurge) {
10+
await mkDirpAsync(context.sourcemapDir);
11+
}
12+
13+
const fileNames = await readDirAsync(context.buildDir);
14+
15+
// only include js source maps
16+
const sourceMaps = fileNames.filter(fileName => fileName.endsWith('.map'));
17+
18+
const toCopy = sourceMaps.filter(fileName => fileName.indexOf('vendor.js') < 0 && fileName.endsWith('.js.map'));
19+
const toCopyFullPaths = toCopy.map(fileName => join(context.buildDir, fileName));
20+
21+
const toPurge = sourceMaps.map(sourceMap => join(context.buildDir, sourceMap));
22+
23+
const copyFilePromises: Promise<any>[] = [];
24+
if (copyBeforePurge) {
25+
for (const fullPath of toCopyFullPaths) {
26+
const fileName = basename(fullPath);
27+
copyFilePromises.push(copyFileAsync(fullPath, join(context.sourcemapDir, fileName)));
2628
}
27-
return Promise.all(promises);
28-
});
29+
}
30+
31+
await Promise.all(copyFilePromises);
32+
33+
// okay cool, all of the files have been copied over, so go ahead and blow them all away
34+
const purgeFilePromises: Promise<any>[] = [];
35+
if (shouldPurge) {
36+
for (const fullPath of toPurge) {
37+
purgeFilePromises.push(unlinkAsync(fullPath));
38+
}
39+
}
40+
41+
return await Promise.all(purgeFilePromises);
2942
}
3043

3144
export function purgeSourceMapsIfNeeded(context: BuildContext): Promise<any> {
3245
if (getBooleanPropertyValue(Constants.ENV_VAR_GENERATE_SOURCE_MAP)) {
3346
// keep the source maps and just return
3447
return copySourcemaps(context, false);
3548
}
36-
3749
return copySourcemaps(context, true);
3850
}

0 commit comments

Comments
 (0)