UNPKG

6.24 kBJavaScriptView Raw
1'use strict';
2
3const fs = require('fs');
4const path = require('path');
5const increment = require('add-filename-increment');
6
7/**
8 * Asynchronously writes data to a file, replacing the file if it already
9 * exists and creating any intermediate directories if they don't already
10 * exist. Data can be a string or a buffer. Returns a promise if a callback
11 * function is not passed.
12 *
13 * ```js
14 * const write = require('write');
15 *
16 * // async/await
17 * (async () => {
18 * await write('foo.txt', 'This is content...');
19 * })();
20 *
21 * // promise
22 * write('foo.txt', 'This is content...')
23 * .then(() => {
24 * // do stuff
25 * });
26 *
27 * // callback
28 * write('foo.txt', 'This is content...', err => {
29 * // do stuff with err
30 * });
31 * ```
32 * @name write
33 * @param {String} `filepath` file path.
34 * @param {String|Buffer|Uint8Array} `data` Data to write.
35 * @param {Object} `options` Options to pass to [fs.writeFile][writefile]
36 * @param {Function} `callback` (optional) If no callback is provided, a promise is returned.
37 * @returns {Object} Returns an object with the `path` and `contents` of the file that was written to the file system. This is useful for debugging when `options.increment` is used and the path might have been modified.
38 * @api public
39 */
40
41const write = (filepath, data, options, callback) => {
42 if (typeof options === 'function') {
43 callback = options;
44 options = {};
45 }
46
47 const opts = { encoding: 'utf8', ...options };
48 const destpath = opts.increment ? incrementName(filepath, options) : filepath;
49 const result = { path: destpath, data };
50
51 if (opts.overwrite === false && exists(filepath, destpath)) {
52 throw new Error('File already exists: ' + destpath);
53 }
54
55 const promise = mkdir(path.dirname(destpath), { recursive: true, ...options })
56 .then(() => {
57 return new Promise((resolve, reject) => {
58 fs.createWriteStream(destpath, opts)
59 .on('error', err => reject(err))
60 .on('close', resolve)
61 .end(ensureNewline(data, opts));
62 });
63 });
64
65 if (typeof callback === 'function') {
66 promise.then(() => callback(null, result)).catch(callback);
67 return;
68 }
69
70 return promise.then(() => result);
71};
72
73/**
74 * The synchronous version of [write](#write). Returns undefined.
75 *
76 * ```js
77 * const write = require('write');
78 * write.sync('foo.txt', 'This is content...');
79 * ```
80 * @name .sync
81 * @param {String} `filepath` file path.
82 * @param {String|Buffer|Uint8Array} `data` Data to write.
83 * @param {Object} `options` Options to pass to [fs.writeFileSync][writefilesync]
84 * @returns {Object} Returns an object with the `path` and `contents` of the file that was written to the file system. This is useful for debugging when `options.increment` is used and the path might have been modified.
85 * @api public
86 */
87
88write.sync = (filepath, data, options) => {
89 if (typeof filepath !== 'string') {
90 throw new TypeError('expected filepath to be a string');
91 }
92
93 const opts = { encoding: 'utf8', ...options };
94 const destpath = opts.increment ? incrementName(filepath, options) : filepath;
95
96 if (opts.overwrite === false && exists(filepath, destpath)) {
97 throw new Error('File already exists: ' + destpath);
98 }
99
100 mkdirSync(path.dirname(destpath), { recursive: true, ...options });
101 fs.writeFileSync(destpath, ensureNewline(data, opts), opts);
102 return { path: destpath, data };
103};
104
105/**
106 * Returns a new [WriteStream][writestream] object. Uses `fs.createWriteStream`
107 * to write data to a file, replacing the file if it already exists and creating
108 * any intermediate directories if they don't already exist. Data can be a string
109 * or a buffer.
110 *
111 * ```js
112 * const fs = require('fs');
113 * const write = require('write');
114 * fs.createReadStream('README.md')
115 * .pipe(write.stream('a/b/c/other-file.md'))
116 * .on('close', () => {
117 * // do stuff
118 * });
119 * ```
120 * @name .stream
121 * @param {String} `filepath` file path.
122 * @param {Object} `options` Options to pass to [fs.createWriteStream][wsoptions]
123 * @return {Stream} Returns a new [WriteStream][writestream] object. (See [Writable Stream][writable]).
124 * @api public
125 */
126
127write.stream = (filepath, options) => {
128 if (typeof filepath !== 'string') {
129 throw new TypeError('expected filepath to be a string');
130 }
131
132 const opts = { encoding: 'utf8', ...options };
133 const destpath = opts.increment ? incrementName(filepath, options) : filepath;
134
135 if (opts.overwrite === false && exists(filepath, destpath)) {
136 throw new Error('File already exists: ' + filepath);
137 }
138
139 mkdirSync(path.dirname(destpath), { recursive: true, ...options });
140 return fs.createWriteStream(destpath, opts);
141};
142
143/**
144 * Increment the filename if the file already exists and enabled by the user
145 */
146
147const incrementName = (destpath, options = {}) => {
148 if (options.increment === true) options.increment = void 0;
149 return increment(destpath, options);
150};
151
152/**
153 * Ensure newline at EOF if defined on options
154 */
155
156const ensureNewline = (data, options) => {
157 if (!options || options.newline !== true) return data;
158 if (typeof data !== 'string' && !isBuffer(data)) {
159 return data;
160 }
161
162 // Only call `.toString()` on the last character. This way,
163 // if data is a buffer, we don't need to stringify the entire
164 // buffer just to append a newline.
165 if (String(data.slice(-1)) !== '\n') {
166 if (typeof data === 'string') {
167 return data + '\n';
168 }
169 return data.concat(Buffer.from('\n'));
170 }
171
172 return data;
173};
174
175// if filepath !== destpath, that means the user has enabled
176// "increment", which has already checked the file system and
177// renamed the file to avoid conflicts, so we don't need to
178// check again.
179const exists = (filepath, destpath) => {
180 return filepath === destpath && fs.existsSync(filepath);
181};
182
183const mkdir = (dirname, options) => {
184 return new Promise(resolve => fs.mkdir(dirname, options, () => resolve()));
185};
186
187const mkdirSync = (dirname, options) => {
188 try {
189 fs.mkdirSync(dirname, options);
190 } catch (err) { /* do nothing */ }
191};
192
193const isBuffer = data => {
194 if (data.constructor && typeof data.constructor.isBuffer === 'function') {
195 return data.constructor.isBuffer(data);
196 }
197 return false;
198};
199
200/**
201 * Expose `write`
202 */
203
204module.exports = write;