mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			504 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			504 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| "use strict";
 | |
| 
 | |
| const util = require("util");
 | |
| const { Volume, createFsFromVolume } = require("memfs");
 | |
| const FileSystemInfo = require("../lib/FileSystemInfo");
 | |
| const { buffersSerializer } = require("../lib/util/serialization");
 | |
| 
 | |
| describe("FileSystemInfo", () => {
 | |
| 	afterEach(() => {
 | |
| 		// restore the spy created with spyOn
 | |
| 		jest.restoreAllMocks();
 | |
| 	});
 | |
| 
 | |
| 	const files = [
 | |
| 		"/path/file.txt",
 | |
| 		"/path/nested/deep/file.txt",
 | |
| 		"/path/nested/deep/ignored.txt",
 | |
| 		"/path/context+files/file.txt",
 | |
| 		"/path/context+files/sub/file.txt",
 | |
| 		"/path/context+files/sub/ignored.txt",
 | |
| 		"/path/node_modules/package/file.txt",
 | |
| 		"/path/cache/package-1234/file.txt",
 | |
| 		"/path/circular/circular/file2.txt",
 | |
| 		"/path/nested/deep/symlink/file.txt",
 | |
| 		"/path/context+files/sub/symlink/file.txt",
 | |
| 		"/path/context/sub/symlink/file.txt",
 | |
| 		"/path/missing.txt",
 | |
| 		"/path/node_modules/@foo/package1/index.js",
 | |
| 		"/path/node_modules/@foo/package2/index.js",
 | |
| 		"/path/node_modules/bar-package3/index.js"
 | |
| 	];
 | |
| 	const directories = [
 | |
| 		"/path/context+files",
 | |
| 		"/path/context",
 | |
| 		"/path/missing",
 | |
| 		"/path/node_modules/package",
 | |
| 		"/path/node_modules/missing",
 | |
| 		"/path/node_modules/@foo",
 | |
| 		"/path/node_modules/@foo/package1",
 | |
| 		"/path/node_modules/@foo/package2",
 | |
| 		"/path/node_modules/bar-package3",
 | |
| 		"/path/cache/package-1234",
 | |
| 		"/path/cache/package-missing"
 | |
| 	];
 | |
| 	const missing = [
 | |
| 		"/path/package.json",
 | |
| 		"/path/file2.txt",
 | |
| 		"/path/context+files/file2.txt",
 | |
| 		"/path/node_modules/package.txt",
 | |
| 		"/path/node_modules/package/missing.txt",
 | |
| 		"/path/cache/package-2345",
 | |
| 		"/path/cache/package-1234/missing.txt",
 | |
| 		"/path/ignored.txt"
 | |
| 	];
 | |
| 	const ignored = [
 | |
| 		"/path/nested/deep/ignored.txt",
 | |
| 		"/path/context+files/sub/ignored.txt",
 | |
| 		"/path/context/sub/ignored.txt",
 | |
| 		"/path/ignored.txt",
 | |
| 		"/path/node_modules/package/ignored.txt",
 | |
| 		"/path/cache/package-1234/ignored.txt"
 | |
| 	];
 | |
| 	const unmanagedPaths = [
 | |
| 		"/path/node_modules/@foo/package1",
 | |
| 		"/path/node_modules/@foo/package2",
 | |
| 		"/path/node_modules/bar-package3"
 | |
| 	];
 | |
| 	const managedPaths = ["/path/node_modules"];
 | |
| 	const immutablePaths = ["/path/cache"];
 | |
| 	const createFs = () => {
 | |
| 		const fs = createFsFromVolume(new Volume());
 | |
| 		fs.mkdirSync("/path/context+files/sub", { recursive: true });
 | |
| 		fs.mkdirSync("/path/context/sub", { recursive: true });
 | |
| 		fs.mkdirSync("/path/nested/deep", { recursive: true });
 | |
| 		fs.mkdirSync("/path/node_modules/package", { recursive: true });
 | |
| 		fs.mkdirSync("/path/node_modules/@foo", { recursive: true });
 | |
| 		fs.mkdirSync("/path/node_modules/@foo/package1", { recursive: true });
 | |
| 		fs.mkdirSync("/path/node_modules/@foo/package2", { recursive: true });
 | |
| 		fs.mkdirSync("/path/node_modules/bar-package3", { recursive: true });
 | |
| 		fs.mkdirSync("/path/cache/package-1234", { recursive: true });
 | |
| 		fs.mkdirSync("/path/folder/context", { recursive: true });
 | |
| 		fs.mkdirSync("/path/folder/context+files", { recursive: true });
 | |
| 		fs.mkdirSync("/path/folder/nested", { recursive: true });
 | |
| 		fs.writeFileSync("/path/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/file2.txt", "Hello World2");
 | |
| 		fs.writeFileSync("/path/nested/deep/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/nested/deep/ignored.txt", "Ignored");
 | |
| 		fs.writeFileSync("/path/context+files/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/context+files/file2.txt", "Hello World2");
 | |
| 		fs.writeFileSync("/path/context+files/sub/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/context+files/sub/file2.txt", "Hello World2");
 | |
| 		fs.writeFileSync("/path/context+files/sub/file3.txt", "Hello World3");
 | |
| 		fs.writeFileSync("/path/context+files/sub/ignored.txt", "Ignored");
 | |
| 		fs.writeFileSync("/path/context/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/context/file2.txt", "Hello World2");
 | |
| 		fs.writeFileSync("/path/context/sub/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/context/sub/file2.txt", "Hello World2");
 | |
| 		fs.writeFileSync("/path/context/sub/file3.txt", "Hello World3");
 | |
| 		fs.writeFileSync("/path/context/sub/ignored.txt", "Ignored");
 | |
| 		fs.writeFileSync(
 | |
| 			"/path/node_modules/package/package.json",
 | |
| 			JSON.stringify({ name: "package", version: "1.0.0" })
 | |
| 		);
 | |
| 		fs.writeFileSync("/path/node_modules/package/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/node_modules/package/ignored.txt", "Ignored");
 | |
| 		fs.writeFileSync(
 | |
| 			"/path/cache/package-1234/package.json",
 | |
| 			JSON.stringify({ name: "package", version: "1.0.0" })
 | |
| 		);
 | |
| 		fs.writeFileSync("/path/cache/package-1234/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/cache/package-1234/ignored.txt", "Ignored");
 | |
| 		fs.symlinkSync("/path", "/path/circular", "dir");
 | |
| 		fs.writeFileSync("/path/folder/context/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/folder/context+files/file.txt", "Hello World");
 | |
| 		fs.writeFileSync("/path/folder/nested/file.txt", "Hello World");
 | |
| 		fs.writeFileSync(
 | |
| 			"/path/node_modules/@foo/package1/index.js",
 | |
| 			"Hello World"
 | |
| 		);
 | |
| 		fs.writeFileSync(
 | |
| 			"/path/node_modules/@foo/package2/index.js",
 | |
| 			"Hello World"
 | |
| 		);
 | |
| 		fs.writeFileSync("/path/node_modules/bar-package3/index.js", "Hello World");
 | |
| 		fs.symlinkSync("/path/folder/context", "/path/context/sub/symlink", "dir");
 | |
| 		fs.symlinkSync(
 | |
| 			"/path/folder/context+files",
 | |
| 			"/path/context+files/sub/symlink",
 | |
| 			"dir"
 | |
| 		);
 | |
| 		fs.symlinkSync("/path/folder/nested", "/path/nested/deep/symlink", "dir");
 | |
| 		return fs;
 | |
| 	};
 | |
| 
 | |
| 	const createFsInfo = (fs) => {
 | |
| 		const logger = {
 | |
| 			error: (...args) => {
 | |
| 				throw new Error(util.format(...args));
 | |
| 			}
 | |
| 		};
 | |
| 		const fsInfo = new FileSystemInfo(fs, {
 | |
| 			logger,
 | |
| 			unmanagedPaths,
 | |
| 			managedPaths,
 | |
| 			immutablePaths,
 | |
| 			hashFunction: "sha256"
 | |
| 		});
 | |
| 		for (const method of ["warn", "info", "log", "debug"]) {
 | |
| 			fsInfo.logs = [];
 | |
| 			fsInfo[method] = [];
 | |
| 			logger[method] = (...args) => {
 | |
| 				const msg = util.format(...args);
 | |
| 				fsInfo[method].push(msg);
 | |
| 				fsInfo.logs.push(`[${method}] ${msg}`);
 | |
| 			};
 | |
| 		}
 | |
| 		fsInfo.addFileTimestamps(new Map(ignored.map((i) => [i, "ignore"])));
 | |
| 		return fsInfo;
 | |
| 	};
 | |
| 
 | |
| 	const createSnapshot = (fs, options, callback) => {
 | |
| 		const fsInfo = createFsInfo(fs);
 | |
| 		fsInfo.createSnapshot(
 | |
| 			Date.now() + 10000,
 | |
| 			files,
 | |
| 			directories,
 | |
| 			missing,
 | |
| 			options,
 | |
| 			(err, snapshot) => {
 | |
| 				if (err) return callback(err);
 | |
| 				snapshot.name = "initial snapshot";
 | |
| 				// create another one to test the caching
 | |
| 				fsInfo.createSnapshot(
 | |
| 					Date.now() + 10000,
 | |
| 					files,
 | |
| 					directories,
 | |
| 					missing,
 | |
| 					options,
 | |
| 					(err, snapshot2) => {
 | |
| 						if (err) return callback(err);
 | |
| 						snapshot2.name = "cached snapshot";
 | |
| 						callback(null, snapshot, snapshot2);
 | |
| 					}
 | |
| 				);
 | |
| 			}
 | |
| 		);
 | |
| 	};
 | |
| 
 | |
| 	const clone = (object) => {
 | |
| 		const serialized = buffersSerializer.serialize(object, {});
 | |
| 		return buffersSerializer.deserialize(serialized, {});
 | |
| 	};
 | |
| 
 | |
| 	const expectSnapshotsState = (
 | |
| 		fs,
 | |
| 		snapshot,
 | |
| 		snapshot2,
 | |
| 		expected,
 | |
| 		callback
 | |
| 	) => {
 | |
| 		expectSnapshotState(fs, snapshot, expected, (err) => {
 | |
| 			if (err) return callback(err);
 | |
| 			if (!snapshot2) return callback();
 | |
| 			expectSnapshotState(fs, snapshot2, expected, callback);
 | |
| 		});
 | |
| 	};
 | |
| 
 | |
| 	const expectSnapshotState = (fs, snapshot, expected, callback) => {
 | |
| 		const fsInfo = createFsInfo(fs);
 | |
| 		const details = (snapshot) => `${fsInfo.logs.join("\n")}
 | |
| ${util.inspect(snapshot, false, Infinity, true)}`;
 | |
| 		fsInfo.checkSnapshotValid(snapshot, (err, valid) => {
 | |
| 			if (err) return callback(err);
 | |
| 			if (valid !== expected) {
 | |
| 				return callback(
 | |
| 					new Error(`Expected snapshot to be ${
 | |
| 						expected ? "valid" : "invalid"
 | |
| 					} but it is ${valid ? "valid" : "invalid"}:
 | |
| ${details(snapshot)}`)
 | |
| 				);
 | |
| 			}
 | |
| 			// Another try to check if direct caching works
 | |
| 			fsInfo.checkSnapshotValid(snapshot, (err, valid) => {
 | |
| 				if (err) return callback(err);
 | |
| 				if (valid !== expected) {
 | |
| 					return callback(
 | |
| 						new Error(`Expected snapshot lead to the same result when directly cached:
 | |
| ${details(snapshot)}`)
 | |
| 					);
 | |
| 				}
 | |
| 				// Another try to check if indirect caching works
 | |
| 				fsInfo.checkSnapshotValid(clone(snapshot), (err, valid) => {
 | |
| 					if (err) return callback(err);
 | |
| 					if (valid !== expected) {
 | |
| 						return callback(
 | |
| 							new Error(`Expected snapshot lead to the same result when indirectly cached:
 | |
| ${details(snapshot)}`)
 | |
| 						);
 | |
| 					}
 | |
| 					callback();
 | |
| 				});
 | |
| 			});
 | |
| 		});
 | |
| 	};
 | |
| 
 | |
| 	const updateFile = (fs, filename) => {
 | |
| 		const oldContent = fs.readFileSync(filename, "utf8");
 | |
| 		if (filename.endsWith(".json")) {
 | |
| 			const data = JSON.parse(oldContent);
 | |
| 			fs.writeFileSync(
 | |
| 				filename,
 | |
| 				JSON.stringify({
 | |
| 					...data,
 | |
| 					version: `${data.version}.1`
 | |
| 				})
 | |
| 			);
 | |
| 		} else {
 | |
| 			fs.writeFileSync(filename, `${oldContent}!`);
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	for (const [name, options] of [
 | |
| 		["timestamp", { timestamp: true }],
 | |
| 		["hash", { hash: true }],
 | |
| 		["tsh", { timestamp: true, hash: true }]
 | |
| 	]) {
 | |
| 		describe(`${name} mode`, () => {
 | |
| 			it("should always accept an empty snapshot", (done) => {
 | |
| 				const fs = createFs();
 | |
| 				const fsInfo = createFsInfo(fs);
 | |
| 				fsInfo.createSnapshot(
 | |
| 					Date.now() + 10000,
 | |
| 					[],
 | |
| 					[],
 | |
| 					[],
 | |
| 					options,
 | |
| 					(err, snapshot) => {
 | |
| 						if (err) return done(err);
 | |
| 						const fs = createFs();
 | |
| 						expectSnapshotState(fs, snapshot, true, done);
 | |
| 					}
 | |
| 				);
 | |
| 			});
 | |
| 
 | |
| 			it("should accept a snapshot when fs is unchanged", (done) => {
 | |
| 				const fs = createFs();
 | |
| 				createSnapshot(fs, options, (err, snapshot, snapshot2) => {
 | |
| 					if (err) return done(err);
 | |
| 					expectSnapshotsState(fs, snapshot, snapshot2, true, done);
 | |
| 				});
 | |
| 			});
 | |
| 
 | |
| 			const ignoredFileChanges = [
 | |
| 				"/path/nested/deep/ignored.txt",
 | |
| 				"/path/context+files/sub/ignored.txt"
 | |
| 			];
 | |
| 
 | |
| 			for (const fileChange of [
 | |
| 				"/path/file.txt",
 | |
| 				"/path/file2.txt",
 | |
| 				"/path/nested/deep/file.txt",
 | |
| 				"/path/context+files/file.txt",
 | |
| 				"/path/context+files/file2.txt",
 | |
| 				"/path/context+files/sub/file.txt",
 | |
| 				"/path/context+files/sub/file2.txt",
 | |
| 				"/path/context+files/sub/file3.txt",
 | |
| 				"/path/context/file.txt",
 | |
| 				"/path/context/file2.txt",
 | |
| 				"/path/context/sub/file.txt",
 | |
| 				"/path/context/sub/file2.txt",
 | |
| 				"/path/context/sub/file3.txt",
 | |
| 				"/path/node_modules/package/package.json",
 | |
| 				"/path/folder/context/file.txt",
 | |
| 				"/path/folder/context+files/file.txt",
 | |
| 				"/path/folder/nested/file.txt",
 | |
| 				"/path/node_modules/@foo/package1/index.js",
 | |
| 				"/path/node_modules/@foo/package2/index.js",
 | |
| 				"/path/node_modules/bar-package3/index.js",
 | |
| 				...(name !== "timestamp" ? ignoredFileChanges : []),
 | |
| 				...(name === "hash" ? ["/path/context/sub/ignored.txt"] : [])
 | |
| 			]) {
 | |
| 				it(`should invalidate the snapshot when ${fileChange} is changed`, (done) => {
 | |
| 					const fs = createFs();
 | |
| 					createSnapshot(fs, options, (err, snapshot, snapshot2) => {
 | |
| 						if (err) return done(err);
 | |
| 						updateFile(fs, fileChange);
 | |
| 						expectSnapshotsState(fs, snapshot, snapshot2, false, done);
 | |
| 					});
 | |
| 				});
 | |
| 			}
 | |
| 
 | |
| 			for (const fileChange of [
 | |
| 				"/path/node_modules/package/file.txt",
 | |
| 				"/path/node_modules/package/ignored.txt",
 | |
| 				"/path/cache/package-1234/package.json",
 | |
| 				"/path/cache/package-1234/file.txt",
 | |
| 				"/path/cache/package-1234/ignored.txt",
 | |
| 				...(name === "timestamp" ? ignoredFileChanges : []),
 | |
| 				...(name !== "hash" ? ["/path/context/sub/ignored.txt"] : [])
 | |
| 			]) {
 | |
| 				it(`should not invalidate the snapshot when ${fileChange} is changed`, (done) => {
 | |
| 					const fs = createFs();
 | |
| 					createSnapshot(fs, options, (err, snapshot, snapshot2) => {
 | |
| 						if (err) return done(err);
 | |
| 						updateFile(fs, fileChange);
 | |
| 						expectSnapshotsState(fs, snapshot, snapshot2, true, done);
 | |
| 					});
 | |
| 				});
 | |
| 			}
 | |
| 
 | |
| 			for (const newFile of [
 | |
| 				"/path/package.json",
 | |
| 				"/path/file2.txt",
 | |
| 				"/path/context+files/file2.txt",
 | |
| 				"/path/node_modules/package.txt"
 | |
| 			]) {
 | |
| 				it(`should invalidate the snapshot when ${newFile} is created`, (done) => {
 | |
| 					const fs = createFs();
 | |
| 					createSnapshot(fs, options, (err, snapshot, snapshot2) => {
 | |
| 						if (err) return done(err);
 | |
| 						fs.writeFileSync(newFile, "New file");
 | |
| 						expectSnapshotsState(fs, snapshot, snapshot2, false, done);
 | |
| 					});
 | |
| 				});
 | |
| 			}
 | |
| 
 | |
| 			for (const newFile of [
 | |
| 				"/path/node_modules/package/missing.txt",
 | |
| 				"/path/cache/package-1234/missing.txt",
 | |
| 				"/path/cache/package-2345",
 | |
| 				"/path/ignored.txt"
 | |
| 			]) {
 | |
| 				it(`should not invalidate the snapshot when ${newFile} is created`, (done) => {
 | |
| 					const fs = createFs();
 | |
| 					createSnapshot(fs, options, (err, snapshot, snapshot2) => {
 | |
| 						if (err) return done(err);
 | |
| 						fs.writeFileSync(newFile, "New file");
 | |
| 						expectSnapshotsState(fs, snapshot, snapshot2, true, done);
 | |
| 					});
 | |
| 				});
 | |
| 			}
 | |
| 
 | |
| 			if (name !== "timestamp") {
 | |
| 				it("should not invalidate snapshot when only timestamps have changed", (done) => {
 | |
| 					const fs = createFs();
 | |
| 					createSnapshot(fs, options, (err, snapshot, snapshot2) => {
 | |
| 						if (err) return done(err);
 | |
| 						const fs = createFs();
 | |
| 						expectSnapshotsState(fs, snapshot, snapshot2, true, done);
 | |
| 					});
 | |
| 				});
 | |
| 			}
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	describe("stable iterables identity", () => {
 | |
| 		const options = { timestamp: true };
 | |
| 
 | |
| 		/**
 | |
| 		 * @param {(err?: WebpackError | null, snapshot?: Snapshot | null) => void} callback callback function
 | |
| 		 */
 | |
| 		function getSnapshot(callback) {
 | |
| 			const fs = createFs();
 | |
| 			const fsInfo = createFsInfo(fs);
 | |
| 			fsInfo.createSnapshot(
 | |
| 				Date.now() + 10000,
 | |
| 				files,
 | |
| 				directories,
 | |
| 				missing,
 | |
| 				options,
 | |
| 				callback
 | |
| 			);
 | |
| 		}
 | |
| 
 | |
| 		it("should return same iterable for getFileIterable()", (done) => {
 | |
| 			getSnapshot((err, snapshot) => {
 | |
| 				if (err) done(err);
 | |
| 				expect(snapshot.getFileIterable()).toEqual(snapshot.getFileIterable());
 | |
| 				done();
 | |
| 			});
 | |
| 		});
 | |
| 
 | |
| 		it("should return same iterable for getContextIterable()", (done) => {
 | |
| 			getSnapshot((err, snapshot) => {
 | |
| 				if (err) done(err);
 | |
| 				expect(snapshot.getContextIterable()).toEqual(
 | |
| 					snapshot.getContextIterable()
 | |
| 				);
 | |
| 				done();
 | |
| 			});
 | |
| 		});
 | |
| 
 | |
| 		it("should return same iterable for getMissingIterable()", (done) => {
 | |
| 			getSnapshot((err, snapshot) => {
 | |
| 				if (err) done(err);
 | |
| 				expect(snapshot.getFileIterable()).toEqual(snapshot.getFileIterable());
 | |
| 				done();
 | |
| 			});
 | |
| 		});
 | |
| 	});
 | |
| 
 | |
| 	describe("symlinks", () => {
 | |
| 		it("should work with symlinks with errors", (done) => {
 | |
| 			const fs = createFs();
 | |
| 
 | |
| 			fs.symlinkSync(
 | |
| 				"/path/folder/context",
 | |
| 				"/path/context/sub/symlink-error",
 | |
| 				"dir"
 | |
| 			);
 | |
| 
 | |
| 			const originalReadlink = fs.readlink;
 | |
| 
 | |
| 			let i = 0;
 | |
| 
 | |
| 			jest.spyOn(fs, "readlink").mockImplementation((path, callback) => {
 | |
| 				if (path === "/path/context/sub/symlink-error" && i < 2) {
 | |
| 					i += 1;
 | |
| 					callback(new Error("test"));
 | |
| 					return;
 | |
| 				}
 | |
| 
 | |
| 				originalReadlink(path, callback);
 | |
| 			});
 | |
| 
 | |
| 			createSnapshot(
 | |
| 				fs,
 | |
| 				["timestamp", { timestamp: true }],
 | |
| 				(err, snapshot, snapshot2) => {
 | |
| 					if (err) return done(err);
 | |
| 					expectSnapshotsState(fs, snapshot, snapshot2, true, done);
 | |
| 				}
 | |
| 			);
 | |
| 		});
 | |
| 
 | |
| 		it("should work with symlinks with errors #1", (done) => {
 | |
| 			const fs = createFs();
 | |
| 
 | |
| 			fs.symlinkSync(
 | |
| 				"/path/folder/context",
 | |
| 				"/path/context/sub/symlink-error",
 | |
| 				"dir"
 | |
| 			);
 | |
| 
 | |
| 			jest.spyOn(fs, "readlink").mockImplementation((path, callback) => {
 | |
| 				callback(new Error("test"));
 | |
| 			});
 | |
| 
 | |
| 			const fsInfo = createFsInfo(fs);
 | |
| 			fsInfo.createSnapshot(
 | |
| 				Date.now() + 10000,
 | |
| 				files,
 | |
| 				directories,
 | |
| 				missing,
 | |
| 				["timestamp", { timestamp: true }],
 | |
| 				(err, snapshot) => {
 | |
| 					expect(snapshot).toBeNull();
 | |
| 					done();
 | |
| 				}
 | |
| 			);
 | |
| 		});
 | |
| 	});
 | |
| });
 |