@atlaspack/fs 2.12.1-canary.3354

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,439 @@
1
+ // @flow
2
+
3
+ import type {Readable, Writable} from 'stream';
4
+ import type {
5
+ FilePath,
6
+ Encoding,
7
+ FileOptions,
8
+ FileSystem,
9
+ ReaddirOptions,
10
+ FileStats,
11
+ } from '@atlaspack/types-internal';
12
+ import type {
13
+ Event,
14
+ Options as WatcherOptions,
15
+ AsyncSubscription,
16
+ } from '@parcel/watcher';
17
+
18
+ import {registerSerializableClass} from '@atlaspack/core';
19
+ import WorkerFarm from '@atlaspack/workers';
20
+ import packageJSON from '../package.json';
21
+ import {findAncestorFile, findNodeModule, findFirstFile} from './find';
22
+ import {MemoryFS} from './MemoryFS';
23
+
24
+ import nullthrows from 'nullthrows';
25
+ import path from 'path';
26
+
27
+ export class OverlayFS implements FileSystem {
28
+ deleted: Set<FilePath> = new Set();
29
+ writable: FileSystem;
30
+ readable: FileSystem;
31
+ _cwd: FilePath;
32
+
33
+ constructor(workerFarmOrFS: WorkerFarm | FileSystem, readable: FileSystem) {
34
+ if (workerFarmOrFS instanceof WorkerFarm) {
35
+ this.writable = new MemoryFS(workerFarmOrFS);
36
+ } else {
37
+ this.writable = workerFarmOrFS;
38
+ }
39
+ this.readable = readable;
40
+ this._cwd = readable.cwd();
41
+ }
42
+
43
+ static deserialize(opts: any): OverlayFS {
44
+ let fs = new OverlayFS(opts.writable, opts.readable);
45
+ if (opts.deleted != null) fs.deleted = opts.deleted;
46
+ return fs;
47
+ }
48
+
49
+ serialize(): {|
50
+ $$raw: boolean,
51
+ readable: FileSystem,
52
+ writable: FileSystem,
53
+ deleted: Set<FilePath>,
54
+ |} {
55
+ return {
56
+ $$raw: false,
57
+ writable: this.writable,
58
+ readable: this.readable,
59
+ deleted: this.deleted,
60
+ };
61
+ }
62
+
63
+ _deletedThrows(filePath: FilePath): FilePath {
64
+ filePath = this._normalizePath(filePath);
65
+ if (this.deleted.has(filePath)) {
66
+ throw new FSError('ENOENT', filePath, 'does not exist');
67
+ }
68
+ return filePath;
69
+ }
70
+
71
+ _checkExists(filePath: FilePath): FilePath {
72
+ filePath = this._deletedThrows(filePath);
73
+ if (!this.existsSync(filePath)) {
74
+ throw new FSError('ENOENT', filePath, 'does not exist');
75
+ }
76
+ return filePath;
77
+ }
78
+
79
+ _isSymlink(filePath: FilePath): boolean {
80
+ filePath = this._normalizePath(filePath);
81
+ // Check the parts of the path to see if any are symlinks.
82
+ let {root, dir, base} = path.parse(filePath);
83
+ let segments = dir.slice(root.length).split(path.sep).concat(base);
84
+ while (segments.length) {
85
+ filePath = path.join(root, ...segments);
86
+ let name = segments.pop();
87
+ if (this.deleted.has(filePath)) {
88
+ return false;
89
+ } else if (
90
+ this.writable instanceof MemoryFS &&
91
+ this.writable.symlinks.has(filePath)
92
+ ) {
93
+ return true;
94
+ } else {
95
+ // HACK: Atlaspack fs does not provide `lstatSync`,
96
+ // so we use `readdirSync` to check if the path is a symlink.
97
+ let parent = path.resolve(filePath, '..');
98
+ if (parent === filePath) {
99
+ return false;
100
+ }
101
+ try {
102
+ for (let dirent of this.readdirSync(parent, {withFileTypes: true})) {
103
+ if (typeof dirent === 'string') {
104
+ break; // {withFileTypes: true} not supported
105
+ } else if (dirent.name === name) {
106
+ if (dirent.isSymbolicLink()) {
107
+ return true;
108
+ }
109
+ }
110
+ }
111
+ } catch (e) {
112
+ if (e.code === 'ENOENT') {
113
+ return false;
114
+ }
115
+ throw e;
116
+ }
117
+ }
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ async _copyPathForWrite(filePath: FilePath): Promise<FilePath> {
124
+ filePath = await this._normalizePath(filePath);
125
+ let dirPath = path.dirname(filePath);
126
+ if (this.existsSync(dirPath) && !this.writable.existsSync(dirPath)) {
127
+ await this.writable.mkdirp(dirPath);
128
+ }
129
+ return filePath;
130
+ }
131
+
132
+ _normalizePath(filePath: FilePath): FilePath {
133
+ return path.resolve(this.cwd(), filePath);
134
+ }
135
+
136
+ // eslint-disable-next-line require-await
137
+ async readFile(filePath: FilePath, encoding?: Encoding): Promise<any> {
138
+ return this.readFileSync(filePath, encoding);
139
+ }
140
+
141
+ async writeFile(
142
+ filePath: FilePath,
143
+ contents: string | Buffer,
144
+ options: ?FileOptions,
145
+ ): Promise<void> {
146
+ filePath = await this._copyPathForWrite(filePath);
147
+ await this.writable.writeFile(filePath, contents, options);
148
+ this.deleted.delete(filePath);
149
+ }
150
+
151
+ async copyFile(source: FilePath, destination: FilePath): Promise<void> {
152
+ source = this._normalizePath(source);
153
+ destination = await this._copyPathForWrite(destination);
154
+
155
+ if (await this.writable.exists(source)) {
156
+ await this.writable.writeFile(
157
+ destination,
158
+ await this.writable.readFile(source),
159
+ );
160
+ } else {
161
+ await this.writable.writeFile(
162
+ destination,
163
+ await this.readable.readFile(source),
164
+ );
165
+ }
166
+
167
+ this.deleted.delete(destination);
168
+ }
169
+
170
+ // eslint-disable-next-line require-await
171
+ async stat(filePath: FilePath): Promise<FileStats> {
172
+ return this.statSync(filePath);
173
+ }
174
+
175
+ async symlink(target: FilePath, filePath: FilePath): Promise<void> {
176
+ target = this._normalizePath(target);
177
+ filePath = this._normalizePath(filePath);
178
+ await this.writable.symlink(target, filePath);
179
+ this.deleted.delete(filePath);
180
+ }
181
+
182
+ async unlink(filePath: FilePath): Promise<void> {
183
+ filePath = this._normalizePath(filePath);
184
+
185
+ let toDelete = [filePath];
186
+
187
+ if (this.writable instanceof MemoryFS && this._isSymlink(filePath)) {
188
+ this.writable.symlinks.delete(filePath);
189
+ } else if (this.statSync(filePath).isDirectory()) {
190
+ let stack = [filePath];
191
+
192
+ // Recursively add every descendant path to deleted.
193
+ while (stack.length) {
194
+ let root = nullthrows(stack.pop());
195
+ for (let ent of this.readdirSync(root, {withFileTypes: true})) {
196
+ if (typeof ent === 'string') {
197
+ let childPath = path.join(root, ent);
198
+ toDelete.push(childPath);
199
+ if (this.statSync(childPath).isDirectory()) {
200
+ stack.push(childPath);
201
+ }
202
+ } else {
203
+ let childPath = path.join(root, ent.name);
204
+ toDelete.push(childPath);
205
+ if (ent.isDirectory()) {
206
+ stack.push(childPath);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ try {
214
+ await this.writable.unlink(filePath);
215
+ } catch (e) {
216
+ if (e.code === 'ENOENT' && !this.readable.existsSync(filePath)) {
217
+ throw e;
218
+ }
219
+ }
220
+
221
+ for (let pathToDelete of toDelete) {
222
+ this.deleted.add(pathToDelete);
223
+ }
224
+ }
225
+
226
+ async mkdirp(dir: FilePath): Promise<void> {
227
+ dir = this._normalizePath(dir);
228
+ await this.writable.mkdirp(dir);
229
+
230
+ if (this.deleted != null) {
231
+ let root = path.parse(dir).root;
232
+ while (dir !== root) {
233
+ this.deleted.delete(dir);
234
+ dir = path.dirname(dir);
235
+ }
236
+ }
237
+ }
238
+
239
+ async rimraf(filePath: FilePath): Promise<void> {
240
+ try {
241
+ await this.unlink(filePath);
242
+ } catch (e) {
243
+ // noop
244
+ }
245
+ }
246
+
247
+ // eslint-disable-next-line require-await
248
+ async ncp(source: FilePath, destination: FilePath): Promise<void> {
249
+ // TODO: Implement this correctly.
250
+ return this.writable.ncp(source, destination);
251
+ }
252
+
253
+ createReadStream(filePath: FilePath, opts?: ?FileOptions): Readable {
254
+ filePath = this._deletedThrows(filePath);
255
+ if (this.writable.existsSync(filePath)) {
256
+ return this.writable.createReadStream(filePath, opts);
257
+ }
258
+
259
+ return this.readable.createReadStream(filePath, opts);
260
+ }
261
+
262
+ createWriteStream(path: FilePath, opts?: ?FileOptions): Writable {
263
+ path = this._normalizePath(path);
264
+ this.deleted.delete(path);
265
+ return this.writable.createWriteStream(path, opts);
266
+ }
267
+
268
+ cwd(): FilePath {
269
+ return this._cwd;
270
+ }
271
+
272
+ chdir(path: FilePath): void {
273
+ this._cwd = this._checkExists(path);
274
+ }
275
+
276
+ // eslint-disable-next-line require-await
277
+ async realpath(filePath: FilePath): Promise<FilePath> {
278
+ return this.realpathSync(filePath);
279
+ }
280
+
281
+ readFileSync(filePath: FilePath, encoding?: Encoding): any {
282
+ filePath = this.realpathSync(filePath);
283
+ try {
284
+ // $FlowFixMe[incompatible-call]
285
+ return this.writable.readFileSync(filePath, encoding);
286
+ } catch (err) {
287
+ // $FlowFixMe[incompatible-call]
288
+ return this.readable.readFileSync(filePath, encoding);
289
+ }
290
+ }
291
+
292
+ statSync(filePath: FilePath): FileStats {
293
+ filePath = this._normalizePath(filePath);
294
+ try {
295
+ return this.writable.statSync(filePath);
296
+ } catch (e) {
297
+ if (e.code === 'ENOENT' && this.existsSync(filePath)) {
298
+ return this.readable.statSync(filePath);
299
+ }
300
+ throw e;
301
+ }
302
+ }
303
+
304
+ realpathSync(filePath: FilePath): FilePath {
305
+ filePath = this._deletedThrows(filePath);
306
+ filePath = this._deletedThrows(this.writable.realpathSync(filePath));
307
+ if (!this.writable.existsSync(filePath)) {
308
+ return this.readable.realpathSync(filePath);
309
+ }
310
+ return filePath;
311
+ }
312
+
313
+ // eslint-disable-next-line require-await
314
+ async exists(filePath: FilePath): Promise<boolean> {
315
+ return this.existsSync(filePath);
316
+ }
317
+
318
+ existsSync(filePath: FilePath): boolean {
319
+ filePath = this._normalizePath(filePath);
320
+ if (this.deleted.has(filePath)) return false;
321
+
322
+ try {
323
+ filePath = this.realpathSync(filePath);
324
+ } catch (err) {
325
+ if (err.code !== 'ENOENT') throw err;
326
+ }
327
+
328
+ if (this.deleted.has(filePath)) return false;
329
+
330
+ return (
331
+ this.writable.existsSync(filePath) || this.readable.existsSync(filePath)
332
+ );
333
+ }
334
+
335
+ // eslint-disable-next-line require-await
336
+ async readdir(path: FilePath, opts?: ReaddirOptions): Promise<any> {
337
+ return this.readdirSync(path, opts);
338
+ }
339
+
340
+ readdirSync(dir: FilePath, opts?: ReaddirOptions): any {
341
+ dir = this.realpathSync(dir);
342
+ // Read from both filesystems and merge the results
343
+ let entries = new Map();
344
+
345
+ try {
346
+ for (let entry: any of this.writable.readdirSync(dir, opts)) {
347
+ let filePath = path.join(dir, entry.name ?? entry);
348
+ if (this.deleted.has(filePath)) continue;
349
+ entries.set(filePath, entry);
350
+ }
351
+ } catch {
352
+ // noop
353
+ }
354
+
355
+ try {
356
+ for (let entry: any of this.readable.readdirSync(dir, opts)) {
357
+ let filePath = path.join(dir, entry.name ?? entry);
358
+ if (this.deleted.has(filePath)) continue;
359
+ if (entries.has(filePath)) continue;
360
+ entries.set(filePath, entry);
361
+ }
362
+ } catch {
363
+ // noop
364
+ }
365
+
366
+ return Array.from(entries.values());
367
+ }
368
+
369
+ async watch(
370
+ dir: FilePath,
371
+ fn: (err: ?Error, events: Array<Event>) => mixed,
372
+ opts: WatcherOptions,
373
+ ): Promise<AsyncSubscription> {
374
+ let writableSubscription = await this.writable.watch(dir, fn, opts);
375
+ let readableSubscription = await this.readable.watch(dir, fn, opts);
376
+ return {
377
+ unsubscribe: async () => {
378
+ await writableSubscription.unsubscribe();
379
+ await readableSubscription.unsubscribe();
380
+ },
381
+ };
382
+ }
383
+
384
+ async getEventsSince(
385
+ dir: FilePath,
386
+ snapshot: FilePath,
387
+ opts: WatcherOptions,
388
+ ): Promise<Array<Event>> {
389
+ let writableEvents = await this.writable.getEventsSince(
390
+ dir,
391
+ snapshot,
392
+ opts,
393
+ );
394
+ let readableEvents = await this.readable.getEventsSince(
395
+ dir,
396
+ snapshot,
397
+ opts,
398
+ );
399
+ return [...writableEvents, ...readableEvents];
400
+ }
401
+
402
+ async writeSnapshot(
403
+ dir: FilePath,
404
+ snapshot: FilePath,
405
+ opts: WatcherOptions,
406
+ ): Promise<void> {
407
+ await this.writable.writeSnapshot(dir, snapshot, opts);
408
+ }
409
+
410
+ findAncestorFile(
411
+ fileNames: Array<string>,
412
+ fromDir: FilePath,
413
+ root: FilePath,
414
+ ): ?FilePath {
415
+ return findAncestorFile(this, fileNames, fromDir, root);
416
+ }
417
+
418
+ findNodeModule(moduleName: string, fromDir: FilePath): ?FilePath {
419
+ return findNodeModule(this, moduleName, fromDir);
420
+ }
421
+
422
+ findFirstFile(filePaths: Array<FilePath>): ?FilePath {
423
+ return findFirstFile(this, filePaths);
424
+ }
425
+ }
426
+
427
+ class FSError extends Error {
428
+ code: string;
429
+ path: FilePath;
430
+ constructor(code: string, path: FilePath, message: string) {
431
+ super(`${code}: ${path} ${message}`);
432
+ this.name = 'FSError';
433
+ this.code = code;
434
+ this.path = path;
435
+ Error.captureStackTrace?.(this, this.constructor);
436
+ }
437
+ }
438
+
439
+ registerSerializableClass(`${packageJSON.version}:OverlayFS`, OverlayFS);
package/src/find.js ADDED
@@ -0,0 +1,81 @@
1
+ // @flow
2
+ import type {FilePath, FileSystem} from '@atlaspack/types-internal';
3
+ import path from 'path';
4
+
5
+ export function findNodeModule(
6
+ fs: FileSystem,
7
+ moduleName: string,
8
+ dir: FilePath,
9
+ ): ?FilePath {
10
+ let {root} = path.parse(dir);
11
+ while (dir !== root) {
12
+ // Skip node_modules directories
13
+ if (path.basename(dir) === 'node_modules') {
14
+ dir = path.dirname(dir);
15
+ }
16
+
17
+ try {
18
+ let moduleDir = path.join(dir, 'node_modules', moduleName);
19
+ let stats = fs.statSync(moduleDir);
20
+ if (stats.isDirectory()) {
21
+ return moduleDir;
22
+ }
23
+ } catch (err) {
24
+ // ignore
25
+ }
26
+
27
+ // Move up a directory
28
+ dir = path.dirname(dir);
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ export function findAncestorFile(
35
+ fs: FileSystem,
36
+ fileNames: Array<string>,
37
+ dir: FilePath,
38
+ root: FilePath,
39
+ ): ?FilePath {
40
+ let {root: pathRoot} = path.parse(dir);
41
+ // eslint-disable-next-line no-constant-condition
42
+ while (true) {
43
+ if (path.basename(dir) === 'node_modules') {
44
+ return null;
45
+ }
46
+
47
+ for (const fileName of fileNames) {
48
+ let filePath = path.join(dir, fileName);
49
+ try {
50
+ if (fs.statSync(filePath).isFile()) {
51
+ return filePath;
52
+ }
53
+ } catch (err) {
54
+ // ignore
55
+ }
56
+ }
57
+
58
+ if (dir === root || dir === pathRoot) {
59
+ break;
60
+ }
61
+
62
+ dir = path.dirname(dir);
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ export function findFirstFile(
69
+ fs: FileSystem,
70
+ filePaths: Array<FilePath>,
71
+ ): ?FilePath {
72
+ for (let filePath of filePaths) {
73
+ try {
74
+ if (fs.statSync(filePath).isFile()) {
75
+ return filePath;
76
+ }
77
+ } catch (err) {
78
+ // ignore
79
+ }
80
+ }
81
+ }
package/src/index.js ADDED
@@ -0,0 +1,45 @@
1
+ // @flow strict-local
2
+ import type {
3
+ FilePath,
4
+ FileSystem,
5
+ FileOptions,
6
+ } from '@atlaspack/types-internal';
7
+ import type {Readable, Writable} from 'stream';
8
+
9
+ import path from 'path';
10
+ import stream from 'stream';
11
+ import {promisify} from 'util';
12
+
13
+ export * from './NodeFS';
14
+ export * from './MemoryFS';
15
+ export * from './OverlayFS';
16
+
17
+ export type {FileSystem, FileOptions};
18
+
19
+ const pipeline: (Readable, Writable) => Promise<void> = promisify(
20
+ stream.pipeline,
21
+ );
22
+
23
+ // Recursively copies a directory from the sourceFS to the destinationFS
24
+ export async function ncp(
25
+ sourceFS: FileSystem,
26
+ source: FilePath,
27
+ destinationFS: FileSystem,
28
+ destination: FilePath,
29
+ ) {
30
+ await destinationFS.mkdirp(destination);
31
+ let files = await sourceFS.readdir(source);
32
+ for (let file of files) {
33
+ let sourcePath = path.join(source, file);
34
+ let destPath = path.join(destination, file);
35
+ let stats = await sourceFS.stat(sourcePath);
36
+ if (stats.isFile()) {
37
+ await pipeline(
38
+ sourceFS.createReadStream(sourcePath),
39
+ destinationFS.createWriteStream(destPath),
40
+ );
41
+ } else if (stats.isDirectory()) {
42
+ await ncp(sourceFS, sourcePath, destinationFS, destPath);
43
+ }
44
+ }
45
+ }