diff --git a/__tests__/server/utils/watchLocalModules.spec.js b/__tests__/server/utils/watchLocalModules.spec.js index f911a6a7a..b77ebbfe8 100644 --- a/__tests__/server/utils/watchLocalModules.spec.js +++ b/__tests__/server/utils/watchLocalModules.spec.js @@ -18,15 +18,15 @@ * permissions and limitations under the License. */ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import { fromJS } from 'immutable'; import loadModule from 'holocron/loadModule.node'; import { getModules, resetModuleRegistry } from 'holocron/moduleRegistry'; import watchLocalModules from '../../../src/server/utils/watchLocalModules'; jest.mock('fs', () => { - const fs = jest.requireActual('fs'); + const fsActual = jest.requireActual('fs'); const setImmediateNative = global.setImmediate; let mockedFilesystem; @@ -54,18 +54,19 @@ jest.mock('fs', () => { } */ function getEntry(parts) { - let current = mockedFilesystem; - for (const entryName of parts) { - if (!current || !current.has('entries')) { + return parts.reduce((parentEntry, entryName) => { + if (!parentEntry || !parentEntry.has('entries')) { return null; } - current = current.get('entries').get(entryName); - } - - return current; + return parentEntry.get('entries').get(entryName); + }, mockedFilesystem); } let inodeCount = 0; + function getNewInode() { + inodeCount += 1; + return inodeCount; + } const mock = { clear() { const createdMillis = Date.now() + Math.floor(Math.random() * 1e4) / 1e4; @@ -81,7 +82,7 @@ jest.mock('fs', () => { gid: 512, rdev: 0, blksize: 4096, - ino: ++inodeCount, + ino: getNewInode(), size: 128, blocks: 0, atimeMs: Date.now() + Math.floor(Math.random() * 1e4) / 1e4, @@ -107,7 +108,7 @@ jest.mock('fs', () => { let parent = mockedFilesystem; const parts = fsPath.split('/').filter(Boolean); - for (let i = 0; i < parts.length; i++) { + for (let i = 0; i < parts.length; i += 1) { const nextEntry = parts[i]; if ( parent.get('entries').has(nextEntry) @@ -133,7 +134,7 @@ jest.mock('fs', () => { gid: 512, rdev: 0, blksize: 4096, - ino: ++inodeCount, + ino: getNewInode(), size: 128, blocks: 0, atimeMs: Date.now() + 0.3254, @@ -181,7 +182,7 @@ jest.mock('fs', () => { gid: 512, rdev: 0, blksize: 4096, - ino: ++inodeCount, + ino: getNewInode(), size: contents.length, blocks: contents.length / 512, atimeMs: Date.now() + Math.floor(Math.random() * 1e4) / 1e4, @@ -200,7 +201,7 @@ jest.mock('fs', () => { print() { function traverser(parentPath, entry) { let printout = ''; - for (const [childName, childNode] of entry.get('entries').entries()) { + [...entry.get('entries').entries()].forEach(([childName, childNode]) => { const childPath = `${parentPath}/${childName}`; if (!childNode) { throw new Error(`no child for ${childName}??`); @@ -210,7 +211,7 @@ jest.mock('fs', () => { if (indicator === 'd') { printout += traverser(childPath, childNode); } - } + }); return printout; } @@ -222,7 +223,26 @@ jest.mock('fs', () => { mock.clear(); - jest.spyOn(fs, 'readdir').mockImplementation((dirPath, callback) => { + // https://github.com/isaacs/path-scurry/blob/main/src/index.ts#L88-L100 + // https://github.com/isaacs/path-scurry/blob/v1.10.2/src/index.ts#L268-L270 + function indicatorToDirentType(indicator) { + switch (indicator) { + case 'd': + return fsActual.constants.UV_DIRENT_DIR; + case 'f': + return fsActual.constants.UV_DIRENT_FILE; + default: + return fsActual.constants.UV_DIRENT_UNKNOWN; + } + } + + jest.spyOn(fsActual, 'readdir').mockImplementation((dirPath, options, callback) => { + if (!callback) { + /* eslint-disable no-param-reassign -- fs.readdir sets `options` as an optional argument */ + callback = options; + options = {}; + /* eslint-enable no-param-reassign */ + } const parts = dirPath.split('/').filter(Boolean); const dir = getEntry(parts); if (!dir) { @@ -234,57 +254,112 @@ jest.mock('fs', () => { return; } - setImmediateNative(callback, null, [...dir.get('entries').keys()]); + let directoryEntries = dir.get('entries'); + if (options.withFileTypes) { + directoryEntries = [...directoryEntries.entries()].map(([name, attributes]) => new fsActual.Dirent(name, indicatorToDirentType(attributes.get('indicator')), [dirPath, name].join('/'))); + } else { + directoryEntries = [...directoryEntries.keys()]; + } + setImmediateNative(callback, null, directoryEntries); + }); + + jest.spyOn(fsActual.promises, 'readdir').mockImplementation((dirPath, options = {}) => new Promise((resolve, reject) => { + const parts = dirPath.split('/').filter(Boolean); + const dir = getEntry(parts); + if (!dir) { + reject(new Error(`not in mock fs ${dirPath} (readdir)`)); + return; + } + if (dir.get('indicator') !== 'd') { + reject(new Error(`not a mocked directory ${dirPath} (readdir)`)); + return; + } + + let directoryEntries = dir.get('entries'); + if (options.withFileTypes) { + directoryEntries = [...directoryEntries.entries()].map(([name, attributes]) => new fsActual.Dirent(name, indicatorToDirentType(attributes.get('indicator')), [dirPath, name].join('/'))); + } else { + directoryEntries = [...directoryEntries.keys()]; + } + resolve(directoryEntries); + })); + + jest.spyOn(fsActual, 'readdirSync').mockImplementation((dirPath, options = {}) => { + const parts = dirPath.split('/').filter(Boolean); + const dir = getEntry(parts); + if (!dir) { + throw new Error(`not in mock fs ${dirPath} (readdir)`); + } + if (dir.get('indicator') !== 'd') { + throw new Error(`not a mocked directory ${dirPath} (readdir)`); + } + + let directoryEntries = dir.get('entries'); + if (options.withFileTypes) { + directoryEntries = [...directoryEntries.entries()].map(([name, attributes]) => new fsActual.Dirent(name, indicatorToDirentType(attributes.get('indicator')), [dirPath, name].join('/'))); + } else { + directoryEntries = [...directoryEntries.keys()]; + } + return directoryEntries; }); - jest.spyOn(fs.promises, 'stat').mockImplementation( + function statKeyArgsToPositional({ + dev, + mode, + nlink, + uid, + gid, + rdev, + blksize, + ino, + size, + blocks, + atimeMs, + mtimeMs, + ctimeMs, + birthtimeMs, + }) { + return [ + dev, + mode, + nlink, + uid, + gid, + rdev, + blksize, + ino, + size, + blocks, + atimeMs, + mtimeMs, + ctimeMs, + birthtimeMs, + ]; + } + + jest.spyOn(fsActual.promises, 'stat').mockImplementation( (fsPath) => new Promise((resolve, reject) => { const entry = getEntry(fsPath.split('/').filter(Boolean)); if (!entry) { - return reject(new Error(`no entry for ${fsPath} (stat)`)); + reject(new Error(`no entry for ${fsPath} (stat)`)); + } else { + resolve(new fsActual.Stats(...statKeyArgsToPositional(entry.get('stat')))); } - - const statArgs = entry.get('stat'); - const { - dev, - mode, - nlink, - uid, - gid, - rdev, - blksize, - ino, - size, - blocks, - atimeMs, - mtimeMs, - ctimeMs, - birthtimeMs, - } = statArgs; - return resolve( - new fs.Stats( - dev, - mode, - nlink, - uid, - gid, - rdev, - blksize, - ino, - size, - blocks, - atimeMs, - mtimeMs, - ctimeMs, - birthtimeMs - ) - ); }) ); - fs.mock = mock; + jest.spyOn(fsActual.promises, 'lstat').mockImplementation((fsPath) => new Promise((resolve, reject) => { + const entry = getEntry(fsPath.split('/').filter(Boolean)); + if (!entry) { + reject(new Error(`no entry for ${fsPath} (stat)`)); + } else { + resolve(new fsActual.Stats(...statKeyArgsToPositional(entry.get('stat')))); + } + })); + + fsActual.mock = mock; - return fs; + return fsActual; }); jest.mock('holocron/moduleRegistry', () => { @@ -314,7 +389,6 @@ jest.spyOn(console, 'log').mockImplementation(() => {}); describe('watchLocalModules', () => { let origOneAppDevCDNPort; - const setTimeoutOriginal = global.setTimeout; beforeAll(() => { origOneAppDevCDNPort = process.env.HTTP_ONE_APP_DEV_CDN_PORT; }); @@ -421,7 +495,7 @@ describe('watchLocalModules', () => { expect(getModules().get(moduleName)).toBe(updatedModule); expect(console.log).toHaveBeenCalledTimes(2); expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ + [ "the Node.js bundle for some-module finished saving, attempting to load", ] `); @@ -450,7 +524,6 @@ describe('watchLocalModules', () => { }, }, }; - const modulePath = `${moduleName}/${moduleVersion}/${moduleName}.node.js`; const originalModule = () => null; const updatedModule = () => null; const modules = fromJS({ [moduleName]: originalModule }); @@ -491,7 +564,7 @@ describe('watchLocalModules', () => { // run the third change poll, which should see the filesystem changes await setTimeout.mock.calls[1][0](); - + expect(setImmediate).toHaveBeenCalledTimes(1); // first the changeWatcher is queued to run again // then the writesFinishWatcher is queued @@ -500,10 +573,10 @@ describe('watchLocalModules', () => { // run the writesFinishWatcher poll await setTimeout.mock.calls[3][0](); - + expect(setImmediate).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenCalledTimes(4); - + expect(console.log).not.toHaveBeenCalled(); // run the change handler @@ -516,7 +589,7 @@ describe('watchLocalModules', () => { expect(getModules().get(moduleName)).toBe(updatedModule); expect(console.log).toHaveBeenCalledTimes(2); expect(console.log.mock.calls[1]).toMatchInlineSnapshot(` - Array [ + [ "finished reloading some-module", ] `); @@ -545,7 +618,6 @@ describe('watchLocalModules', () => { }, }, }; - const modulePath = `${moduleName}/${moduleVersion}/${moduleName}.node.js`; const originalModule = () => null; const updatedModule = () => null; const modules = fromJS({ [moduleName]: originalModule }); @@ -610,17 +682,17 @@ describe('watchLocalModules', () => { expect(loadModule).toHaveBeenCalledTimes(1); expect(loadModule.mock.calls[0][0]).toBe(moduleName); expect(loadModule.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { + { "baseUrl": "http://localhost:3001/static/modules/some-module/1.0.1/", - "browser": Object { + "browser": { "integrity": "234", "url": "http://localhost:3001/static/modules/some-module/1.0.1/some-module.browser.js", }, - "legacyBrowser": Object { + "legacyBrowser": { "integrity": "134633", "url": "http://localhost:3001/static/modules/some-module/1.0.1/some-module.legacy.browser.js", }, - "node": Object { + "node": { "integrity": "133", "url": "http://localhost:3001/static/modules/some-module/1.0.1/some-module.node.js", }, @@ -655,7 +727,6 @@ describe('watchLocalModules', () => { }, }, }; - const modulePath = `${moduleName}/${moduleVersion}/${moduleName}.node.js`; const originalModule = () => null; const modules = fromJS({ [moduleName]: originalModule }); const moduleMap = fromJS(moduleMapSample); @@ -664,7 +735,6 @@ describe('watchLocalModules', () => { .mkdir(path.resolve('static/modules', moduleName, moduleVersion), { parents: true }) .writeFile(path.resolve('static/modules', moduleName, moduleVersion, `${moduleName}.node.js`), 'console.log("hello");'); - // initiate watching watchLocalModules(); @@ -814,7 +884,7 @@ describe('watchLocalModules', () => { expect(setImmediate).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenCalledTimes(5); - + expect(loadModule).toHaveBeenCalled(); }); @@ -905,7 +975,7 @@ describe('watchLocalModules', () => { expect(getModules().get(moduleName)).toBe(updatedModule); expect(console.log).toHaveBeenCalledTimes(2); expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ + [ "the Node.js bundle for some-module finished saving, attempting to load", ] `); @@ -994,8 +1064,8 @@ describe('watchLocalModules', () => { expect(loadModule).not.toHaveBeenCalled(); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "module \\"other-module\\" not in the module map, make sure to serve-module first", + [ + "module "other-module" not in the module map, make sure to serve-module first", ] `); @@ -1032,7 +1102,8 @@ describe('watchLocalModules', () => { }); // instance when the CHANGE_WATCHER_INTERVAL and WRITING_FINISH_WATCHER_TIMEOUT lined up - // we need to avoid scheduling the write watcher like a tree (many branches, eventually eating all CPU and memory) + // we need to avoid scheduling the write watcher like a tree (many branches, eventually eating + // all CPU and memory) it('should schedule watching for writes only once when both watchers have run', async () => { expect.assertions(13); const moduleName = 'some-module'; @@ -1057,7 +1128,6 @@ describe('watchLocalModules', () => { }, }; const originalModule = () => null; - const updatedModule = () => null; const modules = fromJS({ [moduleName]: originalModule }); const moduleMap = fromJS(moduleMapSample); resetModuleRegistry(modules, moduleMap); @@ -1102,7 +1172,8 @@ describe('watchLocalModules', () => { // then the writesFinishWatcher is queued expect(setTimeout).toHaveBeenCalledTimes(4); - // there may be times that their intervals coincide, we don't want writesFinishWatcher to be scheduled twice + // there may be times that their intervals coincide + // we don't want writesFinishWatcher to be scheduled twice await Promise.all([ setTimeout.mock.calls[2][0](), setTimeout.mock.calls[3][0](), @@ -1172,15 +1243,15 @@ describe('watchLocalModules', () => { .writeFile(path.resolve('static/modules', moduleName, moduleVersion, `${moduleName}.browser.js`), 'console.log("hello again Browser");') .writeFile(path.resolve('static/modules', moduleName, moduleVersion, `${moduleName}.legacy.browser.js`), 'console.log("hello again previous spec Browser");'); loadModule.mockImplementation(() => Promise.reject(new Error('sample-module startup error'))); - + // run the third change poll, which should see but ignore the filesystem changes await setTimeout.mock.calls[1][0](); - + expect(setImmediate).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenCalledTimes(3); expect(loadModule).not.toHaveBeenCalled(); - + fs.mock.writeFile(path.resolve('static/modules', moduleName, moduleVersion, `${moduleName}.node.js`), 'console.log("hello Node.js");'); loadModule.mockClear().mockReturnValue(Promise.resolve(updatedModule)); @@ -1233,7 +1304,6 @@ describe('watchLocalModules', () => { }, }, }; - const modulePath = `${moduleName}/${moduleVersion}/${moduleName}.node.js`; const originalModule = () => null; const updatedModule = () => null; const modules = fromJS({ [moduleName]: originalModule }); @@ -1244,7 +1314,6 @@ describe('watchLocalModules', () => { .writeFile(path.resolve('static/modules', moduleName, moduleVersion, `${moduleName}.node.js`), 'console.log("hello");') .writeFile(path.resolve('static/modules', moduleName, moduleVersion, 'vendors.node.js'), 'console.log("hi");'); - // initiate watching watchLocalModules(); @@ -1331,7 +1400,6 @@ describe('watchLocalModules', () => { }, }, }; - const modulePath = `${moduleName}/${moduleVersion}/${moduleName}.node.js`; const originalModule = () => null; const updatedModule = () => null; const modules = fromJS({ [moduleName]: originalModule }); @@ -1343,8 +1411,6 @@ describe('watchLocalModules', () => { .mkdir(path.resolve('static/modules', moduleName, moduleVersion, 'assets'), { parents: true }) .writeFile(path.resolve('static/modules', moduleName, moduleVersion, 'assets', 'image.png'), 'binary stuff'); - - // initiate watching watchLocalModules(); diff --git a/package-lock.json b/package-lock.json index c692c9b0f..ca696b61f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,6 +108,7 @@ "eslint-plugin-jest": "^27.9.0", "eslint-plugin-jest-dom": "^4.0.3", "expect": "^29.7.0", + "glob": "^10.3.12", "husky": "^9.0.11", "jest": "^29.7.0", "jest-circus": "^29.7.0", @@ -460,6 +461,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", @@ -2909,49 +2930,6 @@ "glob": "^10.3.4" } }, - "node_modules/@fastify/static/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@fastify/static/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@fastify/static/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -3513,6 +3491,26 @@ } } }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/reporters/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3784,6 +3782,25 @@ "node": ">=10" } }, + "node_modules/@npmcli/move-file/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -5935,6 +5952,26 @@ "node": ">= 6" } }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/archiver/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7064,6 +7101,25 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/cacache/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -9623,6 +9679,26 @@ "integrity": "sha512-5fGyt9xmMqUl2VI7+rnUkKCiAQIpLns8sfQtTENy5L70ktbNw0Z3TFJ1JoFNYdx/jffz4YXU45VF75wKZD7sZQ==", "dev": true }, + "node_modules/devtools/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/devtools/node_modules/https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -11714,6 +11790,26 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -12252,19 +12348,21 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -12282,6 +12380,28 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -14200,6 +14320,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-config/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -14898,6 +15038,26 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-runtime/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -17658,11 +17818,11 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -18405,6 +18565,26 @@ "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", "dev": true }, + "node_modules/puppeteer-core/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/puppeteer-core/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -19421,6 +19601,25 @@ "rimraf": "bin.js" } }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", @@ -21303,6 +21502,26 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -22954,6 +23173,25 @@ "node": ">=6" } }, + "node_modules/webpack/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/webpack/node_modules/is-descriptor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", @@ -23634,6 +23872,26 @@ "node": ">= 10" } }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/zip-stream/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index f45b7fbda..a1dfec25b 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "eslint-plugin-jest": "^27.9.0", "eslint-plugin-jest-dom": "^4.0.3", "expect": "^29.7.0", + "glob": "^10.3.12", "husky": "^9.0.11", "jest": "^29.7.0", "jest-circus": "^29.7.0", diff --git a/src/server/utils/watchLocalModules.js b/src/server/utils/watchLocalModules.js index b7d65fb29..04a9b7c71 100644 --- a/src/server/utils/watchLocalModules.js +++ b/src/server/utils/watchLocalModules.js @@ -14,13 +14,9 @@ * permissions and limitations under the License. */ -// This file is only used in development so importing devDeps is not an issue -/* eslint "import/no-extraneous-dependencies": ["error", {"devDependencies": true}] */ - import path from 'node:path'; import fs from 'node:fs/promises'; -import { promisify } from 'node:util'; -import globSync from 'glob'; +import { glob } from 'glob'; import loadModule from 'holocron/loadModule.node'; import { getModules, @@ -30,8 +26,6 @@ import { } from 'holocron/moduleRegistry'; import onModuleLoad from './onModuleLoad'; -const glob = promisify(globSync); - const CHANGE_WATCHER_INTERVAL = 1000; const WRITING_FINISH_WATCHER_TIMEOUT = 400; @@ -89,7 +83,7 @@ export default function watchLocalModules() { // this may be an over-optimization in that it may be more overhead than it saves const stating = new Map(); - function stat(filePath) { + function dedupedStat(filePath) { if (!stating.has(filePath)) { stating.set( filePath, @@ -111,8 +105,11 @@ export default function watchLocalModules() { await Promise.allSettled( [...checkForNoWrites.entries()].map(async ([holocronEntrypoint, previousStat]) => { - const currentStat = await stat(path.join(moduleDirectory, holocronEntrypoint)); - if (currentStat.mtimeMs !== previousStat.mtimeMs || currentStat.size !== previousStat.size) { + const currentStat = await dedupedStat(path.join(moduleDirectory, holocronEntrypoint)); + if ( + currentStat.mtimeMs !== previousStat.mtimeMs + || currentStat.size !== previousStat.size + ) { // need to check again later checkForNoWrites.set(holocronEntrypoint, currentStat); return; @@ -123,7 +120,7 @@ export default function watchLocalModules() { }) ); - if (!nextWriteCheck && checkForNoWrites.size >= 1) { + if (!nextWriteCheck && checkForNoWrites.size > 0) { nextWriteCheck = setTimeout(writesFinishWatcher, WRITING_FINISH_WATCHER_TIMEOUT).unref(); } } @@ -141,7 +138,7 @@ export default function watchLocalModules() { const statsToWait = []; holocronEntrypoints.forEach((holocronEntrypoint) => { statsToWait.push( - stat(path.join(moduleDirectory, holocronEntrypoint)) + dedupedStat(path.join(moduleDirectory, holocronEntrypoint)) .then((stat) => currentStats.set(holocronEntrypoint, stat)) ); }); @@ -153,26 +150,23 @@ export default function watchLocalModules() { return; } - for (const [holocronEntrypoint, currentStat] of currentStats.entries()) { + [...currentStats.entries()].forEach(([holocronEntrypoint, currentStat]) => { if (!previousStats.has(holocronEntrypoint)) { checkForNoWrites.set(holocronEntrypoint, currentStat); - continue; + return; } const previousStat = previousStats.get(holocronEntrypoint); if (currentStat.mtimeMs !== previousStat.mtimeMs || currentStat.size !== previousStat.size) { checkForNoWrites.set(holocronEntrypoint, currentStat); - continue; } - - continue; - } + }); previousStats = currentStats; setTimeout(changeWatcher, CHANGE_WATCHER_INTERVAL).unref(); // wait for writes to the file to stop - if (!nextWriteCheck && checkForNoWrites.size >= 1) { + if (!nextWriteCheck && checkForNoWrites.size > 0) { nextWriteCheck = setTimeout(writesFinishWatcher, WRITING_FINISH_WATCHER_TIMEOUT).unref(); } }