UNPKG

5.87 kBJavaScriptView Raw
1'use strict';
2
3const Binding = require('./binding');
4const {FSError} = require('./error');
5const FileSystem = require('./filesystem');
6const realBinding = process.binding('fs');
7const path = require('path');
8const loader = require('./loader');
9const bypass = require('./bypass');
10const {
11 getReadFileContextPrototype,
12 patchReadFileContext
13} = require('./readfilecontext');
14const fs = require('fs');
15
16const toNamespacedPath = FileSystem.toNamespacedPath;
17
18const realProcessProps = {
19 cwd: process.cwd,
20 chdir: process.chdir
21};
22const realCreateWriteStream = fs.createWriteStream;
23const realStats = realBinding.Stats;
24const realStatWatcher = realBinding.StatWatcher;
25
26/**
27 * Pre-patch fs binding.
28 * This allows mock-fs to work properly under nodejs v10+ readFile
29 * As ReadFileContext nodejs v10+ implementation traps original binding methods:
30 * const { FSReqWrap, close, read } = process.binding('fs');
31 * Note this patch only solves issue for readFile, as the require of
32 * ReadFileContext is delayed by readFile implementation.
33 * if (!ReadFileContext) ReadFileContext = require('internal/fs/read_file_context')
34 *
35 * @param {string} key Property name.
36 */
37function patch(key) {
38 const existingMethod = realBinding[key];
39 realBinding[key] = function() {
40 if (this._mockedBinding) {
41 return this._mockedBinding[key].apply(this._mockedBinding, arguments);
42 } else {
43 return existingMethod.apply(this, arguments);
44 }
45 }.bind(realBinding);
46}
47
48for (const key in Binding.prototype) {
49 if (typeof realBinding[key] === 'function') {
50 // Stats and StatWatcher are constructors
51 if (key !== 'Stats' && key !== 'StatWatcher') {
52 patch(key);
53 }
54 }
55}
56
57const readFileContextPrototype = getReadFileContextPrototype();
58
59patchReadFileContext(readFileContextPrototype);
60
61function overrideBinding(binding) {
62 realBinding._mockedBinding = binding;
63}
64
65function overrideProcess(cwd, chdir) {
66 process.cwd = cwd;
67 process.chdir = chdir;
68}
69
70/**
71 * Have to disable write stream _writev on nodejs v10+.
72 *
73 * nodejs v8 lib/fs.js
74 * note binding.writeBuffers will use mock-fs patched writeBuffers.
75 *
76 * const binding = process.binding('fs');
77 * function writev(fd, chunks, position, callback) {
78 * // ...
79 * binding.writeBuffers(fd, chunks, position, req);
80 * }
81 *
82 * nodejs v10+ lib/internal/fs/streams.js
83 * note it uses original writeBuffers, bypassed mock-fs patched writeBuffers.
84 *
85 * const {writeBuffers} = internalBinding('fs');
86 * function writev(fd, chunks, position, callback) {
87 * // ...
88 * writeBuffers(fd, chunks, position, req);
89 * }
90 *
91 * Luckily _writev is an optional method on Writeable stream implementation.
92 * When _writev is missing, it will fall back to make multiple _write calls.
93 */
94function overrideCreateWriteStream() {
95 fs.createWriteStream = function(path, options) {
96 const output = realCreateWriteStream(path, options);
97 // disable _writev, this will over shadow WriteStream.prototype._writev
98 if (realBinding._mockedBinding) {
99 output._writev = undefined;
100 }
101 return output;
102 };
103}
104
105function overrideReadFileContext(binding) {
106 readFileContextPrototype._mockedBinding = binding;
107}
108
109function restoreBinding() {
110 delete realBinding._mockedBinding;
111 realBinding.Stats = realStats;
112 realBinding.StatWatcher = realStatWatcher;
113}
114
115function restoreProcess() {
116 for (const key in realProcessProps) {
117 process[key] = realProcessProps[key];
118 }
119}
120
121function restoreCreateWriteStream() {
122 fs.createWriteStream = realCreateWriteStream;
123}
124
125function restoreReadFileContext(binding) {
126 delete readFileContextPrototype._mockedBinding;
127}
128
129/**
130 * Swap out the fs bindings for a mock file system.
131 * @param {Object} config Mock file system configuration.
132 * @param {Object} options Any filesystem options.
133 * @param {boolean} options.createCwd Create a directory for `process.cwd()`
134 * (defaults to `true`).
135 * @param {boolean} options.createTmp Create a directory for `os.tmpdir()`
136 * (defaults to `true`).
137 */
138exports = module.exports = function mock(config, options) {
139 const system = FileSystem.create(config, options);
140 const binding = new Binding(system);
141
142 overrideBinding(binding);
143
144 overrideReadFileContext(binding);
145
146 let currentPath = process.cwd();
147 overrideProcess(
148 function cwd() {
149 if (realBinding._mockedBinding) {
150 return currentPath;
151 }
152 return realProcessProps.cwd();
153 },
154 function chdir(directory) {
155 if (realBinding._mockedBinding) {
156 if (!fs.statSync(toNamespacedPath(directory)).isDirectory()) {
157 throw new FSError('ENOTDIR');
158 }
159 currentPath = path.resolve(currentPath, directory);
160 } else {
161 return realProcessProps.chdir(directory);
162 }
163 }
164 );
165
166 overrideCreateWriteStream();
167};
168
169/**
170 * Get hold of the mocked filesystem's 'root'
171 * If fs hasn't currently been replaced, this will return an empty object
172 */
173exports.getMockRoot = function() {
174 if (realBinding._mockedBinding) {
175 return realBinding._mockedBinding.getSystem().getRoot();
176 } else {
177 return {};
178 }
179};
180
181/**
182 * Restore the fs bindings for the real file system.
183 */
184exports.restore = function() {
185 restoreBinding();
186 restoreProcess();
187 restoreCreateWriteStream();
188 restoreReadFileContext();
189};
190
191/**
192 * Create a file factory.
193 */
194exports.file = FileSystem.file;
195
196/**
197 * Create a directory factory.
198 */
199exports.directory = FileSystem.directory;
200
201/**
202 * Create a symbolic link factory.
203 */
204exports.symlink = FileSystem.symlink;
205
206/**
207 * Automatically maps specified paths (for use with `mock()`)
208 */
209exports.load = loader.load;
210
211/**
212 * Perform action, bypassing mock FS
213 * @example
214 * // This file exists on the real FS, not on the mocked FS
215 * const filePath = '/path/file.json';
216 * const data = mock.bypass(() => fs.readFileSync(filePath, 'utf-8'));
217 */
218exports.bypass = bypass;