diff --git a/packages/core/fs/src/NodeFS.js b/packages/core/fs/src/NodeFS.js index b8f7eef1e8d..23b352fbfed 100644 --- a/packages/core/fs/src/NodeFS.js +++ b/packages/core/fs/src/NodeFS.js @@ -34,6 +34,7 @@ export class NodeFS implements FileSystem { copyFile: any = promisify(fs.copyFile); stat: any = promisify(fs.stat); readdir: any = promisify(fs.readdir); + symlink: any = promisify(fs.symlink); unlink: any = promisify(fs.unlink); utimes: any = promisify(fs.utimes); ncp: any = promisify(ncp); diff --git a/packages/core/fs/src/OverlayFS.js b/packages/core/fs/src/OverlayFS.js index 1dd40800a18..47b8d06aea3 100644 --- a/packages/core/fs/src/OverlayFS.js +++ b/packages/core/fs/src/OverlayFS.js @@ -1,7 +1,13 @@ // @flow -import type {Stats} from 'fs'; -import type {FileSystem, ReaddirOptions} from './types'; +import type {Readable, Writable} from 'stream'; +import type { + Encoding, + FileOptions, + FileSystem, + ReaddirOptions, + Stats, +} from './types'; import type {FilePath} from '@parcel/types'; import type { Event, @@ -10,69 +16,142 @@ import type { } from '@parcel/watcher'; import {registerSerializableClass} from '@parcel/core'; +import WorkerFarm from '@parcel/workers'; import packageJSON from '../package.json'; import {findAncestorFile, findNodeModule, findFirstFile} from './find'; +import {MemoryFS} from './MemoryFS'; -function read(method) { - return async function (...args: Array) { - try { - return await this.writable[method](...args); - } catch (err) { - return this.readable[method](...args); - } - }; -} - -function readSync(method) { - return function (...args: Array) { - try { - return this.writable[method](...args); - } catch (err) { - return this.readable[method](...args); - } - }; -} - -function write(method) { - return function (...args: Array) { - return this.writable[method](...args); - }; -} - -function checkExists(method) { - return function (filePath: FilePath, ...args: Array) { - if (this.writable.existsSync(filePath)) { - return this.writable[method](filePath, ...args); - } - - return this.readable[method](filePath, ...args); - }; -} +import nullthrows from 'nullthrows'; +import path from 'path'; export class OverlayFS implements FileSystem { + deleted: Set = new Set(); writable: FileSystem; readable: FileSystem; - constructor(writable: FileSystem, readable: FileSystem) { - this.writable = writable; + _cwd: FilePath; + + constructor(workerFarmOrFS: WorkerFarm | FileSystem, readable: FileSystem) { + if (workerFarmOrFS instanceof WorkerFarm) { + this.writable = new MemoryFS(workerFarmOrFS); + } else { + this.writable = workerFarmOrFS; + } this.readable = readable; + this._cwd = readable.cwd(); } static deserialize(opts: any): OverlayFS { - return new OverlayFS(opts.writable, opts.readable); + let fs = new OverlayFS(opts.writable, opts.readable); + if (opts.deleted != null) fs.deleted = opts.deleted; + return fs; } - serialize(): {|$$raw: boolean, readable: FileSystem, writable: FileSystem|} { + serialize(): {| + $$raw: boolean, + readable: FileSystem, + writable: FileSystem, + deleted: Set, + |} { return { $$raw: false, writable: this.writable, readable: this.readable, + deleted: this.deleted, }; } - readFile: (...args: Array) => Promise> = - read('readFile'); - writeFile: (...args: Array) => any = write('writeFile'); - async copyFile(source: FilePath, destination: FilePath) { + _deletedThrows(filePath: FilePath): FilePath { + filePath = this._normalizePath(filePath); + if (this.deleted.has(filePath)) { + throw new FSError('ENOENT', filePath, 'does not exist'); + } + return filePath; + } + + _checkExists(filePath: FilePath): FilePath { + filePath = this._deletedThrows(filePath); + if (!this.existsSync(filePath)) { + throw new FSError('ENOENT', filePath, 'does not exist'); + } + return filePath; + } + + _isSymlink(filePath: FilePath): boolean { + filePath = this._normalizePath(filePath); + // Check the parts of the path to see if any are symlinks. + let {root, dir, base} = path.parse(filePath); + let segments = dir.slice(root.length).split(path.sep).concat(base); + while (segments.length) { + filePath = path.join(root, ...segments); + let name = segments.pop(); + if (this.deleted.has(filePath)) { + return false; + } else if ( + this.writable instanceof MemoryFS && + this.writable.symlinks.has(filePath) + ) { + return true; + } else { + // HACK: Parcel fs does not provide `lstatSync`, + // so we use `readdirSync` to check if the path is a symlink. + let parent = path.resolve(filePath, '..'); + if (parent === filePath) { + return false; + } + try { + for (let dirent of this.readdirSync(parent, {withFileTypes: true})) { + if (typeof dirent === 'string') { + break; // {withFileTypes: true} not supported + } else if (dirent.name === name) { + if (dirent.isSymbolicLink()) { + return true; + } + } + } + } catch (e) { + if (e.code === 'ENOENT') { + return false; + } + throw e; + } + } + } + + return false; + } + + async _copyPathForWrite(filePath: FilePath): Promise { + filePath = await this._normalizePath(filePath); + let dirPath = path.dirname(filePath); + if (this.existsSync(dirPath) && !this.writable.existsSync(dirPath)) { + await this.writable.mkdirp(dirPath); + } + return filePath; + } + + _normalizePath(filePath: FilePath): FilePath { + return path.resolve(this.cwd(), filePath); + } + + // eslint-disable-next-line require-await + async readFile(filePath: FilePath, encoding?: Encoding): Promise { + return this.readFileSync(filePath, encoding); + } + + async writeFile( + filePath: FilePath, + contents: string | Buffer, + options: ?FileOptions, + ): Promise { + filePath = await this._copyPathForWrite(filePath); + await this.writable.writeFile(filePath, contents, options); + this.deleted.delete(filePath); + } + + async copyFile(source: FilePath, destination: FilePath): Promise { + source = this._normalizePath(source); + destination = await this._copyPathForWrite(destination); + if (await this.writable.exists(source)) { await this.writable.writeFile( destination, @@ -84,69 +163,207 @@ export class OverlayFS implements FileSystem { await this.readable.readFile(source), ); } + + this.deleted.delete(destination); } - stat: (...args: Array) => Promise> = - read('stat'); - unlink: (...args: Array) => any = write('unlink'); - mkdirp: (...args: Array) => any = write('mkdirp'); - rimraf: (...args: Array) => any = write('rimraf'); - ncp: (...args: Array) => any = write('ncp'); - createReadStream: (filePath: FilePath, ...args: Array) => any = - checkExists('createReadStream'); - createWriteStream: (...args: Array) => any = write('createWriteStream'); - cwd: (...args: Array) => any = readSync('cwd'); - chdir: (...args: Array) => any = readSync('chdir'); - realpath: (filePath: FilePath, ...args: Array) => any = - checkExists('realpath'); - - readFileSync: (...args: Array) => any = readSync('readFileSync'); - statSync: (...args: Array) => any = readSync('statSync'); - existsSync: (...args: Array) => any = readSync('existsSync'); - realpathSync: (filePath: FilePath, ...args: Array) => any = - checkExists('realpathSync'); - async exists(filePath: FilePath): Promise { - return ( - (await this.writable.exists(filePath)) || this.readable.exists(filePath) - ); + // eslint-disable-next-line require-await + async stat(filePath: FilePath): Promise { + return this.statSync(filePath); } - async readdir(path: FilePath, opts?: ReaddirOptions): Promise { - // Read from both filesystems and merge the results - let writable = []; - let readable = []; + async symlink(target: FilePath, filePath: FilePath): Promise { + target = this._normalizePath(target); + filePath = this._normalizePath(filePath); + await this.writable.symlink(target, filePath); + this.deleted.delete(filePath); + } + + async unlink(filePath: FilePath): Promise { + filePath = this._normalizePath(filePath); + + let toDelete = [filePath]; + + if (this.writable instanceof MemoryFS && this._isSymlink(filePath)) { + this.writable.symlinks.delete(filePath); + } else if (this.statSync(filePath).isDirectory()) { + let stack = [filePath]; + + // Recursively add every descendant path to deleted. + while (stack.length) { + let root = nullthrows(stack.pop()); + for (let ent of this.readdirSync(root, {withFileTypes: true})) { + if (typeof ent === 'string') { + let childPath = path.join(root, ent); + toDelete.push(childPath); + if (this.statSync(childPath).isDirectory()) { + stack.push(childPath); + } + } else { + let childPath = path.join(root, ent.name); + toDelete.push(childPath); + if (ent.isDirectory()) { + stack.push(childPath); + } + } + } + } + } + try { - writable = await this.writable.readdir(path, opts); + await this.writable.unlink(filePath); + } catch (e) { + if (e.code === 'ENOENT' && !this.readable.existsSync(filePath)) { + throw e; + } + } + + for (let pathToDelete of toDelete) { + this.deleted.add(pathToDelete); + } + } + + async mkdirp(dir: FilePath): Promise { + dir = this._normalizePath(dir); + await this.writable.mkdirp(dir); + + if (this.deleted != null) { + let root = path.parse(dir).root; + while (dir !== root) { + this.deleted.delete(dir); + dir = path.dirname(dir); + } + } + } + + async rimraf(filePath: FilePath): Promise { + try { + await this.unlink(filePath); + } catch (e) { + // noop + } + } + + // eslint-disable-next-line require-await + async ncp(source: FilePath, destination: FilePath): Promise { + // TODO: Implement this correctly. + return this.writable.ncp(source, destination); + } + + createReadStream(filePath: FilePath, opts?: ?FileOptions): Readable { + filePath = this._deletedThrows(filePath); + if (this.writable.existsSync(filePath)) { + return this.writable.createReadStream(filePath, opts); + } + + return this.readable.createReadStream(filePath, opts); + } + + createWriteStream(path: FilePath, opts?: ?FileOptions): Writable { + path = this._normalizePath(path); + this.deleted.delete(path); + return this.writable.createWriteStream(path, opts); + } + + cwd(): FilePath { + return this._cwd; + } + + chdir(path: FilePath): void { + this._cwd = this._checkExists(path); + } + + // eslint-disable-next-line require-await + async realpath(filePath: FilePath): Promise { + return this.realpathSync(filePath); + } + + readFileSync(filePath: FilePath, encoding?: Encoding): any { + filePath = this.realpathSync(filePath); + try { + // $FlowFixMe[incompatible-call] + return this.writable.readFileSync(filePath, encoding); } catch (err) { - // do nothing + // $FlowFixMe[incompatible-call] + return this.readable.readFileSync(filePath, encoding); } + } + + statSync(filePath: FilePath): Stats { + filePath = this._normalizePath(filePath); + try { + return this.writable.statSync(filePath); + } catch (e) { + if (e.code === 'ENOENT' && this.existsSync(filePath)) { + return this.readable.statSync(filePath); + } + throw e; + } + } + + realpathSync(filePath: FilePath): FilePath { + filePath = this._deletedThrows(filePath); + filePath = this._deletedThrows(this.writable.realpathSync(filePath)); + if (!this.writable.existsSync(filePath)) { + return this.readable.realpathSync(filePath); + } + return filePath; + } + + // eslint-disable-next-line require-await + async exists(filePath: FilePath): Promise { + return this.existsSync(filePath); + } + + existsSync(filePath: FilePath): boolean { + filePath = this._normalizePath(filePath); + if (this.deleted.has(filePath)) return false; try { - readable = await this.readable.readdir(path, opts); + filePath = this.realpathSync(filePath); } catch (err) { - // do nothing + if (err.code !== 'ENOENT') throw err; } - return Array.from(new Set([...writable, ...readable])); + if (this.deleted.has(filePath)) return false; + + return ( + this.writable.existsSync(filePath) || this.readable.existsSync(filePath) + ); + } + + // eslint-disable-next-line require-await + async readdir(path: FilePath, opts?: ReaddirOptions): Promise { + return this.readdirSync(path, opts); } - readdirSync(path: FilePath, opts?: ReaddirOptions): any { + readdirSync(dir: FilePath, opts?: ReaddirOptions): any { + dir = this.realpathSync(dir); // Read from both filesystems and merge the results - let writable = []; - let readable = []; + let entries = new Map(); + try { - writable = this.writable.readdirSync(path, opts); - } catch (err) { - // do nothing + for (let entry: any of this.writable.readdirSync(dir, opts)) { + let filePath = path.join(dir, entry.name ?? entry); + if (this.deleted.has(filePath)) continue; + entries.set(filePath, entry); + } + } catch { + // noop } try { - readable = this.readable.readdirSync(path, opts); - } catch (err) { - // do nothing + for (let entry: any of this.readable.readdirSync(dir, opts)) { + let filePath = path.join(dir, entry.name ?? entry); + if (this.deleted.has(filePath)) continue; + if (entries.has(filePath)) continue; + entries.set(filePath, entry); + } + } catch { + // noop } - return Array.from(new Set([...writable, ...readable])); + return Array.from(entries.values()); } async watch( @@ -207,4 +424,16 @@ export class OverlayFS implements FileSystem { } } +class FSError extends Error { + code: string; + path: FilePath; + constructor(code: string, path: FilePath, message: string) { + super(`${code}: ${path} ${message}`); + this.name = 'FSError'; + this.code = code; + this.path = path; + Error.captureStackTrace?.(this, this.constructor); + } +} + registerSerializableClass(`${packageJSON.version}:OverlayFS`, OverlayFS); diff --git a/packages/core/fs/src/types.js b/packages/core/fs/src/types.js index 6463e31b3d0..8e46182903f 100644 --- a/packages/core/fs/src/types.js +++ b/packages/core/fs/src/types.js @@ -77,6 +77,7 @@ export interface FileSystem { readdir(path: FilePath, opts: {withFileTypes: true, ...}): Promise; readdirSync(path: FilePath, opts?: {withFileTypes?: false, ...}): FilePath[]; readdirSync(path: FilePath, opts: {withFileTypes: true, ...}): Dirent[]; + symlink(target: FilePath, path: FilePath): Promise; unlink(path: FilePath): Promise; realpath(path: FilePath): Promise; realpathSync(path: FilePath): FilePath; diff --git a/packages/core/fs/test/OverlayFS.test.js b/packages/core/fs/test/OverlayFS.test.js new file mode 100644 index 00000000000..786c848d21f --- /dev/null +++ b/packages/core/fs/test/OverlayFS.test.js @@ -0,0 +1,232 @@ +// @flow + +import {OverlayFS} from '../src/OverlayFS'; +import {fsFixture} from '@parcel/test-utils/src/fsFixture'; +import {MemoryFS} from '../src/MemoryFS'; +import WorkerFarm from '@parcel/workers'; + +import assert from 'assert'; +import path from 'path'; + +describe('OverlayFS', () => { + let underlayFS; + let fs; + let workerFarm; + + beforeEach(() => { + workerFarm = new WorkerFarm({ + workerPath: require.resolve('@parcel/core/src/worker.js'), + }); + underlayFS = new MemoryFS(workerFarm); + fs = new OverlayFS(workerFarm, underlayFS); + }); + + afterEach(async () => { + await workerFarm.end(); + }); + + it('copies on write', async () => { + await fsFixture(underlayFS)` + foo: foo + `; + + assert.equal(fs.readFileSync('foo', 'utf8'), 'foo'); + + await fs.writeFile('foo', 'bar'); + + assert.equal(fs.readFileSync('foo', 'utf8'), 'bar'); + assert.equal(underlayFS.readFileSync('foo', 'utf8'), 'foo'); + }); + + it('copies on write with dir', async () => { + await fsFixture(underlayFS)` + foo/foo: foo + `; + + assert.equal(fs.readFileSync('foo/foo', 'utf8'), 'foo'); + + await fs.writeFile('foo/bar', 'bar'); + + assert.equal(fs.readFileSync('foo/bar', 'utf8'), 'bar'); + assert(!underlayFS.existsSync('foo/bar')); + }); + + it('copies on write when copying', async () => { + await fsFixture(underlayFS)` + foo: foo + `; + + assert.equal(fs.readFileSync('foo', 'utf8'), 'foo'); + + await fs.copyFile('foo', 'bar'); + assert.equal(fs.readFileSync('bar', 'utf8'), 'foo'); + assert(!underlayFS.existsSync('bar')); + }); + + it('copies on write when copying with dir', async () => { + await fsFixture(underlayFS)` + foo/foo: foo + bar + `; + + assert.equal(fs.readFileSync('foo/foo', 'utf8'), 'foo'); + + await fs.copyFile('foo/foo', 'bar/bar'); + assert.equal(fs.readFileSync('bar/bar', 'utf8'), 'foo'); + assert(!underlayFS.existsSync('bar/bar')); + }); + + it('writes to memory', async () => { + await fs.writeFile('foo', 'foo'); + + assert.equal(fs.readFileSync('foo', 'utf8'), 'foo'); + assert(!underlayFS.existsSync('foo')); + }); + + it('symlinks in memory', async () => { + await fsFixture(underlayFS)` + foo: foo + `; + + assert(fs.existsSync('foo')); + + await fs.symlink('foo', 'bar'); + + assert.equal(fs.readFileSync('bar', 'utf8'), 'foo'); + assert.equal(underlayFS.readFileSync('foo', 'utf8'), 'foo'); + assert.equal(fs.realpathSync('bar'), path.resolve('/foo')); + assert(!underlayFS.existsSync('bar')); + }); + + it('tracks deletes', async () => { + await fsFixture(underlayFS)` + foo: bar + baz -> foo`; + + assert(fs.existsSync('foo')); + assert.equal(fs.realpathSync('baz'), path.resolve('/foo')); + assert(fs._isSymlink('baz')); + + await fs.rimraf('foo'); + + assert(!fs.existsSync('foo')); + assert(fs._isSymlink('baz')); + assert(!fs.existsSync('baz')); + assert(underlayFS.existsSync('foo')); + assert(underlayFS.existsSync('baz')); + }); + + it('tracks unlinks', async () => { + await fsFixture(underlayFS)` + foo: bar + baz -> foo`; + + assert(fs.existsSync('baz')); + assert.equal(fs.realpathSync('baz'), path.resolve('/foo')); + assert(fs._isSymlink('baz')); + + await fs.unlink('baz'); + + assert(!fs._isSymlink('baz')); + assert(!fs.existsSync('baz')); + assert(fs.existsSync('foo')); + assert(underlayFS.existsSync('foo')); + assert(underlayFS.existsSync('baz')); + assert.equal(underlayFS.realpathSync('baz'), path.resolve('/foo')); + }); + + it('tracks nested deletes', async () => { + await fsFixture(underlayFS)` + foo/bar: baz + foo/bat/baz: qux + `; + + assert(fs.existsSync('foo/bar')); + assert(fs.existsSync('foo/bat/baz')); + + await fs.rimraf('foo'); + + assert(!fs.existsSync('foo/bar')); + assert(!fs.existsSync('foo/bat/baz')); + assert(underlayFS.existsSync('foo/bar')); + assert(underlayFS.existsSync('foo/bat/baz')); + + await fs.mkdirp('foo'); + + assert(fs.existsSync('foo')); + assert(!fs.existsSync('foo/bar')); + assert(!fs.existsSync('foo/baz/bat')); + + await fs.mkdirp('foo/baz'); + assert(fs.existsSync('foo/baz')); + assert(!fs.existsSync('foo/baz/bat')); + }); + + it('supports changing to a dir that is only on the readable fs', async () => { + await fsFixture(underlayFS)` + foo/bar: baz + `; + + assert.equal(fs.cwd(), path.resolve('/')); + fs.chdir('/foo'); + assert.equal(fs.cwd(), path.resolve('/foo')); + }); + + it('supports changing to a dir that is only on the writable fs', async () => { + await fsFixture(underlayFS)` + foo/bar: bar + `; + + await fs.mkdirp('/bar'); + assert(!underlayFS.existsSync('/bar')); + + assert.equal(fs.cwd(), path.resolve('/')); + fs.chdir('/bar'); + assert.equal(fs.cwd(), path.resolve('/bar')); + }); + + it('supports changing dir relative to cwd', async () => { + await fsFixture(underlayFS)` + foo/bar: bar + `; + + assert.equal(fs.cwd(), path.resolve('/')); + fs.chdir('foo'); + assert.equal(fs.cwd(), path.resolve('/foo')); + }); + + it('changes dir without changing underlying fs dir', async () => { + await fsFixture(underlayFS)` + foo/bar: baz + foo/bat/baz: qux + `; + + assert.equal(fs.cwd(), path.resolve('/')); + assert.equal(underlayFS.cwd(), path.resolve('/')); + + fs.chdir('foo'); + + assert.equal(fs.cwd(), path.resolve('/foo')); + assert.equal(underlayFS.cwd(), path.resolve('/')); + }); + + it('errors when changing to a dir that does not exist on either fs', async () => { + await fsFixture(underlayFS)` + foo/bar: bar + `; + + assert.throws(() => fs.chdir('/bar'), /ENOENT/); + }); + + it('errors when changing to a deleted dir', async () => { + await fsFixture(underlayFS)` + foo/bar: bar + `; + + await fs.rimraf('foo'); + assert(!fs.existsSync('foo')); + assert(underlayFS.existsSync('foo')); + + assert.throws(() => fs.chdir('/foo'), /ENOENT/); + }); +}); diff --git a/packages/core/integration-tests/test/cache.js b/packages/core/integration-tests/test/cache.js index 2f402c78bb8..626402cbe0e 100644 --- a/packages/core/integration-tests/test/cache.js +++ b/packages/core/integration-tests/test/cache.js @@ -3439,8 +3439,10 @@ describe('cache', function () { }, async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.a {')); @@ -3460,7 +3462,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(css.includes('.a {')); @@ -3482,8 +3486,10 @@ describe('cache', function () { }, async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.a {')); @@ -3502,7 +3508,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(!css.includes('.a {')); @@ -3515,8 +3523,10 @@ describe('cache', function () { entries: ['index.js'], async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.index')); @@ -3545,7 +3555,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(css.includes('.index')); @@ -3580,8 +3592,10 @@ describe('cache', function () { }, async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.index')); @@ -3610,7 +3624,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(css.includes('.index')); @@ -3637,8 +3653,10 @@ describe('cache', function () { }, async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.a')); @@ -3663,7 +3681,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(!css.includes('.a')); @@ -3715,7 +3735,9 @@ describe('cache', function () { let b = await runBundle('index.js'); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(css.includes('.d')); @@ -3738,8 +3760,10 @@ describe('cache', function () { }, async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.included')); @@ -3756,7 +3780,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(!css.includes('.included')); @@ -3782,8 +3808,10 @@ describe('cache', function () { }, async update(b) { let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css') - ?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css') + ?.filePath, + ), 'utf8', ); assert(css.includes('.included')); @@ -3799,7 +3827,9 @@ describe('cache', function () { ); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(!css.includes('.included')); @@ -3841,7 +3871,9 @@ describe('cache', function () { let b = await runBundle('index.sass'); let css = await overlayFS.readFile( - b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + nullthrows( + b.bundleGraph.getBundles().find(b => b.type === 'css')?.filePath, + ), 'utf8', ); assert(css.includes('.d')); @@ -6067,6 +6099,7 @@ describe('cache', function () { it('should invalidate when deleting a dist file', async function () { let b = await testCache({ + outputFS: overlayFS, async update(b) { assert(await overlayFS.exists(path.join(distDir, 'index.js'))); let res = await run(b.bundleGraph); @@ -6083,6 +6116,7 @@ describe('cache', function () { it('should invalidate when deleting a source map', async function () { await testCache({ + outputFS: overlayFS, async update() { assert(await overlayFS.exists(path.join(distDir, 'index.js.map'))); @@ -6095,6 +6129,7 @@ describe('cache', function () { it('should invalidate when the dist directory', async function () { await testCache({ + outputFS: overlayFS, async update() { assert(await overlayFS.exists(path.join(distDir, 'index.js'))); assert(await overlayFS.exists(path.join(distDir, 'index.js.map')));