From 4da97f0d0a4a54f5dd56e3b85412a9f4fc68d367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 23 Dec 2024 13:55:02 +0100 Subject: [PATCH 01/71] Remount NODEFS and other mounts after hotswapPhpRuntime is called --- .../php-wasm/node/src/lib/node-fs-mount.ts | 2 +- .../node/src/test/rotate-php-runtime.spec.ts | 35 ++++++++++++++++ packages/php-wasm/universal/src/lib/php.ts | 42 ++++++++++++++++++- .../universal/src/lib/rotate-php-runtime.ts | 2 +- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/php-wasm/node/src/lib/node-fs-mount.ts b/packages/php-wasm/node/src/lib/node-fs-mount.ts index 778c928714..4460393ea0 100644 --- a/packages/php-wasm/node/src/lib/node-fs-mount.ts +++ b/packages/php-wasm/node/src/lib/node-fs-mount.ts @@ -4,7 +4,7 @@ export function createNodeFsMountHandler(localPath: string): MountHandler { return async function (php, FS, vfsMountPoint) { FS.mount(FS.filesystems['NODEFS'], { root: localPath }, vfsMountPoint); return () => { - FS!.unmount(localPath); + FS!.unmount(vfsMountPoint); }; }; } diff --git a/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts b/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts index 1688fec8e4..1b883e521b 100644 --- a/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts +++ b/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts @@ -14,6 +14,41 @@ const recreateRuntime = async (version: any = LatestSupportedPHPVersion) => await loadNodeRuntime(version); describe('rotatePHPRuntime()', () => { + it('Preserves NODEFS mounts through PHP runtime recreation', async () => { + // Rotate the PHP runtime + const recreateRuntimeSpy = vitest.fn(recreateRuntime); + + const php = new PHP(await recreateRuntime()); + rotatePHPRuntime({ + php, + cwd: '/test-root', + recreateRuntime: recreateRuntimeSpy, + maxRequests: 10, + }); + + // Create a temporary directory and a file in it + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-')); + const tempFile = path.join(tempDir, 'file'); + fs.writeFileSync(tempFile, 'playground'); + + // Mount the temporary directory + php.mkdir('/test-root'); + await php.mount('/test-root', createNodeFsMountHandler(tempDir)); + + // Confirm the file is still there + expect(php.readFileAsText('/test-root/file')).toBe('playground'); + + // Rotate the PHP runtime + for (let i = 0; i < 15; i++) { + await php.run({ code: `` }); + } + + expect(recreateRuntimeSpy).toHaveBeenCalledTimes(1); + + // Confirm the local NODEFS mount is lost + expect(php.readFileAsText('/test-root/file')).toBe('playground'); + }); + it('Free up the available PHP memory', async () => { const freeMemory = (php: PHP) => php[__private__dont__use].HEAPU32.reduce( diff --git a/packages/php-wasm/universal/src/lib/php.ts b/packages/php-wasm/universal/src/lib/php.ts index 16ef240570..5bda8f2dd3 100644 --- a/packages/php-wasm/universal/src/lib/php.ts +++ b/packages/php-wasm/universal/src/lib/php.ts @@ -47,6 +47,12 @@ export type MountHandler = ( export const PHP_INI_PATH = '/internal/shared/php.ini'; const AUTO_PREPEND_SCRIPT = '/internal/shared/auto_prepend_file.php'; +type MountObject = { + path: string; + mountHandler: MountHandler; + unmount: () => Promise; +}; + /** * An environment-agnostic wrapper around the Emscripten PHP runtime * that universals the super low-level API and provides a more convenient @@ -62,6 +68,7 @@ export class PHP implements Disposable { #wasmErrorsTarget: UnhandledRejectionsTarget | null = null; #eventListeners: Map> = new Map(); #messageListeners: MessageListener[] = []; + #mounts: MountObject[] = []; requestHandler?: PHPRequestHandler; /** @@ -1000,7 +1007,7 @@ export class PHP implements Disposable { * is fully decoupled from the request handler and * accepts a constructor-level cwd argument. */ - hotSwapPHPRuntime(runtime: number, cwd?: string) { + async hotSwapPHPRuntime(runtime: number, cwd?: string) { // Once we secure the lock and have the new runtime ready, // the rest of the swap handler is synchronous to make sure // no other operations acts on the old runtime or FS. @@ -1008,8 +1015,16 @@ export class PHP implements Disposable { // asynchronous changes to either the filesystem or the // old PHP runtime without propagating them to the new // runtime. + const oldFS = this[__private__dont__use].FS; + // Unmount all the mount handlers + const mountHandlers: Record = {}; + for (const mount of this.#mounts) { + mountHandlers[mount.path] = mount.mountHandler; + await mount.unmount(); + } + // Kill the current runtime try { this.exit(); @@ -1028,6 +1043,14 @@ export class PHP implements Disposable { if (cwd) { copyFS(oldFS, this[__private__dont__use].FS, cwd); } + + // Re-mount all the mount handlers + for (const [virtualFSPath, mountHandler] of Object.entries( + mountHandlers + )) { + this.mkdir(virtualFSPath); + await this.mount(virtualFSPath, mountHandler); + } } /** @@ -1041,11 +1064,26 @@ export class PHP implements Disposable { virtualFSPath: string, mountHandler: MountHandler ): Promise { - return await mountHandler( + const unmountCallback = await mountHandler( this, this[__private__dont__use].FS, virtualFSPath ); + const mountObject: MountObject = { + mountHandler, + path: virtualFSPath, + unmount: async () => { + await unmountCallback(); + const index = this.#mounts.indexOf(mountObject); + if (index !== -1) { + this.#mounts.splice(index, 1); + } + }, + }; + this.#mounts.push(mountObject); + return () => { + mountObject.unmount(); + }; } /** diff --git a/packages/php-wasm/universal/src/lib/rotate-php-runtime.ts b/packages/php-wasm/universal/src/lib/rotate-php-runtime.ts index 8f159cbc80..5d706cbb05 100644 --- a/packages/php-wasm/universal/src/lib/rotate-php-runtime.ts +++ b/packages/php-wasm/universal/src/lib/rotate-php-runtime.ts @@ -42,7 +42,7 @@ export function rotatePHPRuntime({ async function rotateRuntime() { const release = await php.semaphore.acquire(); try { - php.hotSwapPHPRuntime(await recreateRuntime(), cwd); + await php.hotSwapPHPRuntime(await recreateRuntime(), cwd); // A new runtime has handled zero requests. runtimeRequestCount = 0; From 9456b228770d3d3e7c0c817444f30707d2300f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 23 Dec 2024 17:14:57 +0100 Subject: [PATCH 02/71] Preserve the /internal directory when hotSwapPhpRuntime is called --- .../node/src/test/rotate-php-runtime.spec.ts | 36 +++++++++++++++++++ packages/php-wasm/universal/src/lib/php.ts | 3 ++ 2 files changed, 39 insertions(+) diff --git a/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts b/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts index 1b883e521b..2bc00e18c6 100644 --- a/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts +++ b/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts @@ -14,6 +14,42 @@ const recreateRuntime = async (version: any = LatestSupportedPHPVersion) => await loadNodeRuntime(version); describe('rotatePHPRuntime()', () => { + it('Preserves the /internal directory through PHP runtime recreation', async () => { + // Rotate the PHP runtime + const recreateRuntimeSpy = vitest.fn(recreateRuntime); + + const php = new PHP(await recreateRuntime()); + rotatePHPRuntime({ + php, + cwd: '/test-root', + recreateRuntime: recreateRuntimeSpy, + maxRequests: 10, + }); + + // Create a temporary directory and a file in it + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-')); + const tempFile = path.join(tempDir, 'file'); + fs.writeFileSync(tempFile, 'playground'); + + // Mount the temporary directory + php.mkdir('/internal/shared'); + php.writeFile('/internal/shared/test', 'playground'); + + // Confirm the file is there + expect(php.fileExists('/internal/shared/test')).toBe(true); + + // Rotate the PHP runtime + for (let i = 0; i < 15; i++) { + await php.run({ code: `` }); + } + + expect(recreateRuntimeSpy).toHaveBeenCalledTimes(1); + + // Confirm the file is still there + expect(php.fileExists('/internal/shared/test')).toBe(true); + expect(php.readFileAsText('/internal/shared/test')).toBe('playground'); + }); + it('Preserves NODEFS mounts through PHP runtime recreation', async () => { // Rotate the PHP runtime const recreateRuntimeSpy = vitest.fn(recreateRuntime); diff --git a/packages/php-wasm/universal/src/lib/php.ts b/packages/php-wasm/universal/src/lib/php.ts index 5bda8f2dd3..754209cdc7 100644 --- a/packages/php-wasm/universal/src/lib/php.ts +++ b/packages/php-wasm/universal/src/lib/php.ts @@ -1039,6 +1039,9 @@ export class PHP implements Disposable { this.setSapiName(this.#sapiName); } + // Copy the old /internal directory to the new filesystem + copyFS(oldFS, this[__private__dont__use].FS, '/internal'); + // Copy the MEMFS directory structure from the old FS to the new one if (cwd) { copyFS(oldFS, this[__private__dont__use].FS, cwd); From fc74f35a0396875c4ee1b39a2395587182388392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 23 Dec 2024 17:42:44 +0100 Subject: [PATCH 03/71] Test with 4 mounts --- .../node/src/test/rotate-php-runtime.spec.ts | 107 +++++++++++++++++- packages/php-wasm/universal/src/lib/php.ts | 34 +++--- 2 files changed, 122 insertions(+), 19 deletions(-) diff --git a/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts b/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts index 2bc00e18c6..a0615fcb3e 100644 --- a/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts +++ b/packages/php-wasm/node/src/test/rotate-php-runtime.spec.ts @@ -50,7 +50,7 @@ describe('rotatePHPRuntime()', () => { expect(php.readFileAsText('/internal/shared/test')).toBe('playground'); }); - it('Preserves NODEFS mounts through PHP runtime recreation', async () => { + it('Preserves a single NODEFS mount through PHP runtime recreation', async () => { // Rotate the PHP runtime const recreateRuntimeSpy = vitest.fn(recreateRuntime); @@ -85,6 +85,111 @@ describe('rotatePHPRuntime()', () => { expect(php.readFileAsText('/test-root/file')).toBe('playground'); }); + it('Preserves 4 WordPress plugin mounts through PHP runtime recreation', async () => { + // Rotate the PHP runtime + const recreateRuntimeSpy = vitest.fn(recreateRuntime); + + const php = new PHP(await recreateRuntime()); + rotatePHPRuntime({ + php, + cwd: '/wordpress', + recreateRuntime: recreateRuntimeSpy, + maxRequests: 10, + }); + + // Create temporary directories and files for plugins and uploads + const tempDirs = [ + fs.mkdtempSync(path.join(os.tmpdir(), 'data-liberation-')), + fs.mkdtempSync(path.join(os.tmpdir(), 'data-liberation-markdown-')), + fs.mkdtempSync( + path.join(os.tmpdir(), 'data-liberation-static-files-editor-') + ), + fs.mkdtempSync(path.join(os.tmpdir(), 'static-pages-')), + ]; + + // Add test files to each directory + tempDirs.forEach((dir, i) => { + fs.writeFileSync(path.join(dir, 'test.php'), `plugin-${i}`); + }); + + // Create WordPress directory structure + php.mkdir('/wordpress/wp-content/plugins/data-liberation'); + php.mkdir('/wordpress/wp-content/plugins/z-data-liberation-markdown'); + php.mkdir( + '/wordpress/wp-content/plugins/z-data-liberation-static-files-editor' + ); + php.mkdir('/wordpress/wp-content/uploads/static-pages'); + + // Mount the directories using WordPress paths + await php.mount( + '/wordpress/wp-content/plugins/data-liberation', + createNodeFsMountHandler(tempDirs[0]) + ); + await php.mount( + '/wordpress/wp-content/plugins/z-data-liberation-markdown', + createNodeFsMountHandler(tempDirs[1]) + ); + await php.mount( + '/wordpress/wp-content/plugins/z-data-liberation-static-files-editor', + createNodeFsMountHandler(tempDirs[2]) + ); + await php.mount( + '/wordpress/wp-content/uploads/static-pages', + createNodeFsMountHandler(tempDirs[3]) + ); + + // Verify files exist + expect( + php.readFileAsText( + '/wordpress/wp-content/plugins/data-liberation/test.php' + ) + ).toBe('plugin-0'); + expect( + php.readFileAsText( + '/wordpress/wp-content/plugins/z-data-liberation-markdown/test.php' + ) + ).toBe('plugin-1'); + expect( + php.readFileAsText( + '/wordpress/wp-content/plugins/z-data-liberation-static-files-editor/test.php' + ) + ).toBe('plugin-2'); + expect( + php.readFileAsText( + '/wordpress/wp-content/uploads/static-pages/test.php' + ) + ).toBe('plugin-3'); + + // Rotate the PHP runtime + for (let i = 0; i < 15; i++) { + await php.run({ code: `` }); + } + + expect(recreateRuntimeSpy).toHaveBeenCalledTimes(1); + + // Verify files still exist after rotation + expect( + php.readFileAsText( + '/wordpress/wp-content/plugins/data-liberation/test.php' + ) + ).toBe('plugin-0'); + expect( + php.readFileAsText( + '/wordpress/wp-content/plugins/z-data-liberation-markdown/test.php' + ) + ).toBe('plugin-1'); + expect( + php.readFileAsText( + '/wordpress/wp-content/plugins/z-data-liberation-static-files-editor/test.php' + ) + ).toBe('plugin-2'); + expect( + php.readFileAsText( + '/wordpress/wp-content/uploads/static-pages/test.php' + ) + ).toBe('plugin-3'); + }); + it('Free up the available PHP memory', async () => { const freeMemory = (php: PHP) => php[__private__dont__use].HEAPU32.reduce( diff --git a/packages/php-wasm/universal/src/lib/php.ts b/packages/php-wasm/universal/src/lib/php.ts index 754209cdc7..81065a4b41 100644 --- a/packages/php-wasm/universal/src/lib/php.ts +++ b/packages/php-wasm/universal/src/lib/php.ts @@ -48,7 +48,6 @@ export const PHP_INI_PATH = '/internal/shared/php.ini'; const AUTO_PREPEND_SCRIPT = '/internal/shared/auto_prepend_file.php'; type MountObject = { - path: string; mountHandler: MountHandler; unmount: () => Promise; }; @@ -68,7 +67,7 @@ export class PHP implements Disposable { #wasmErrorsTarget: UnhandledRejectionsTarget | null = null; #eventListeners: Map> = new Map(); #messageListeners: MessageListener[] = []; - #mounts: MountObject[] = []; + #mounts: Record = {}; requestHandler?: PHPRequestHandler; /** @@ -1019,10 +1018,15 @@ export class PHP implements Disposable { const oldFS = this[__private__dont__use].FS; // Unmount all the mount handlers - const mountHandlers: Record = {}; - for (const mount of this.#mounts) { - mountHandlers[mount.path] = mount.mountHandler; - await mount.unmount(); + const mountHandlers: { mountHandler: MountHandler; vfsPath: string }[] = + []; + for (const [vfsPath, mount] of Object.entries(this.#mounts)) { + mountHandlers.push({ mountHandler: mount.mountHandler, vfsPath }); + try { + await mount.unmount(); + } catch (e) { + console.error(e); + } } // Kill the current runtime @@ -1048,11 +1052,9 @@ export class PHP implements Disposable { } // Re-mount all the mount handlers - for (const [virtualFSPath, mountHandler] of Object.entries( - mountHandlers - )) { - this.mkdir(virtualFSPath); - await this.mount(virtualFSPath, mountHandler); + for (const { mountHandler, vfsPath } of mountHandlers) { + this.mkdir(vfsPath); + await this.mount(vfsPath, mountHandler); } } @@ -1072,18 +1074,14 @@ export class PHP implements Disposable { this[__private__dont__use].FS, virtualFSPath ); - const mountObject: MountObject = { + const mountObject = { mountHandler, - path: virtualFSPath, unmount: async () => { await unmountCallback(); - const index = this.#mounts.indexOf(mountObject); - if (index !== -1) { - this.#mounts.splice(index, 1); - } + delete this.#mounts[virtualFSPath]; }, }; - this.#mounts.push(mountObject); + this.#mounts[virtualFSPath] = mountObject; return () => { mountObject.unmount(); }; From 199bc5ed30bbca974f4d0b77db738cabdb9a6511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 23 Dec 2024 20:41:12 +0100 Subject: [PATCH 04/71] Local files browser in the post editor --- package-lock.json | 9729 +++++++++++++---- package.json | 3 +- packages/playground/cli/src/cli.ts | 22 +- packages/playground/cli/src/server.ts | 77 +- .../src/FilePickerControl/index.tsx | 2 +- .../components/src/FilePickerTree/index.tsx | 4 +- packages/playground/components/src/index.ts | 3 + .../blueprint.json | 4 + .../plugin.php | 251 +- .../run.sh | 2 +- packages/playground/wordpress/src/boot.ts | 2 +- 11 files changed, 7601 insertions(+), 2498 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07baa9ce12..e55aa5ff43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@types/react-transition-group": "4.4.11", "@types/wicg-file-system-access": "2023.10.5", "@wordpress/dataviews": "4.5.0", + "@wordpress/scripts": "30.7.0", "ajv": "8.12.0", "async-lock": "1.4.1", "axios": "1.6.1", @@ -73,7 +74,7 @@ "@nx/web": "16.9.0", "@nx/webpack": "16.9.0", "@nx/workspace": "16.9.0", - "@playwright/test": "1.47.1", + "@playwright/test": "1.48.1", "@rollup/plugin-url": "^8.0.1", "@swc-node/register": "~1.6.7", "@swc/core": "~1.3.85", @@ -164,7 +165,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -322,7 +322,6 @@ }, "node_modules/@ampproject/remapping": { "version": "2.2.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -374,93 +373,44 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", - "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", - "dev": true, + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -478,66 +428,53 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", - "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", - "dev": true, + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -549,25 +486,22 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", "semver": "^6.3.1" }, "engines": { @@ -581,19 +515,18 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "dev": true, + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -607,16 +540,15 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", - "dev": true, + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", @@ -628,74 +560,41 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -705,35 +604,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -743,14 +642,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -759,25 +658,14 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -796,141 +684,112 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "license": "MIT", "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", - "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", - "dev": true, + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.4", - "@babel/types": "^7.23.4" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.3" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=4" + "node": ">=6.0.0" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" + "node": ">=6.9.0" }, - "engines": { - "node": ">=6.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -940,14 +799,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -957,13 +816,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", - "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1010,7 +869,6 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, "engines": { "node": ">=6.9.0" }, @@ -1020,7 +878,6 @@ }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1031,7 +888,6 @@ }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1042,7 +898,6 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -1055,7 +910,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1083,7 +938,6 @@ }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1096,7 +950,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -1105,12 +959,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", - "dev": true, + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1120,12 +974,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", - "dev": true, + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1136,7 +990,6 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1147,7 +1000,6 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1157,12 +1009,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1173,7 +1025,6 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1184,7 +1035,6 @@ }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1195,7 +1045,6 @@ }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1206,7 +1055,6 @@ }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1217,7 +1065,6 @@ }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1228,7 +1075,6 @@ }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1241,7 +1087,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1254,7 +1100,6 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -1267,12 +1112,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1285,7 +1130,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1298,12 +1142,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1313,15 +1157,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", - "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1331,14 +1174,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1348,12 +1191,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", + "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1363,12 +1206,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1378,13 +1221,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1394,14 +1237,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", - "dev": true, + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1411,19 +1253,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", "globals": "^11.1.0" }, "engines": { @@ -1434,13 +1273,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1450,12 +1289,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1465,13 +1304,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1481,12 +1320,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1495,14 +1334,29 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1512,13 +1366,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", - "dev": true, + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1528,13 +1381,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1544,12 +1396,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1559,14 +1412,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1576,13 +1429,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1592,12 +1444,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1607,13 +1459,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1623,12 +1474,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1638,13 +1489,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1654,14 +1505,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", - "dev": true, + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1671,15 +1521,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "license": "MIT", "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1689,13 +1539,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1705,13 +1555,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1721,12 +1571,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1736,13 +1586,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", + "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1752,13 +1601,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1768,16 +1616,14 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1787,13 +1633,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1803,13 +1649,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1819,14 +1664,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1836,12 +1680,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1851,13 +1695,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1867,15 +1711,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1885,12 +1728,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1901,7 +1744,6 @@ }, "node_modules/@babel/plugin-transform-react-constant-elements": { "version": "7.21.3", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2" @@ -1915,7 +1757,6 @@ }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" @@ -1928,15 +1769,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.21.5", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz", + "integrity": "sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.21.4", - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/plugin-syntax-jsx": "^7.21.4", - "@babel/types": "^7.21.5" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-jsx": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1947,7 +1789,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-development": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-transform-react-jsx": "^7.18.6" @@ -1991,7 +1832,6 @@ }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", @@ -2005,12 +1845,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.25.9", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2021,12 +1861,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2036,16 +1876,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.4.tgz", - "integrity": "sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.7.tgz", + "integrity": "sha512-Y9p487tyTzB0yDYQOtWnC+9HGOuogtP3/wNpun1xJXEEvI6vip59BSBTsHnekZLqxmPcgsrAKt46HAAb//xGhg==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -2059,18 +1899,17 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2080,13 +1919,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2096,12 +1935,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2111,12 +1950,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2126,12 +1965,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2141,15 +1980,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.4.tgz", - "integrity": "sha512-39hCCOl+YUAyMOu6B9SmUTiHUU0t/CxJNUmY3qRdJujbqi+lrQcL11ysYUsAvFWPBdhihrv1z0oRG84Yr3dODQ==", - "dev": true, + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz", + "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.23.3" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2159,12 +1999,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2174,13 +2014,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2190,13 +2030,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2206,13 +2046,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2222,26 +2062,28 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", - "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.7.tgz", + "integrity": "sha512-Gibz4OUdyNqqLj+7OAvBZxOD7CklCtMA5/j0JgUEwOnaRULsPDXmic2iKxL2DX2vQduPR5wH2hjZas/Vr/Oc0g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@babel/plugin-syntax-import-attributes": "^7.25.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -2253,59 +2095,60 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.3", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.3", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.3", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.3", - "@babel/plugin-transform-for-of": "^7.23.3", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.3", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", - "@babel/plugin-transform-numeric-separator": "^7.23.3", - "@babel/plugin-transform-object-rest-spread": "^7.23.3", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.3", - "@babel/plugin-transform-optional-chaining": "^7.23.3", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.3", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.25.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.7", + "@babel/plugin-transform-async-to-generator": "^7.25.7", + "@babel/plugin-transform-block-scoped-functions": "^7.25.7", + "@babel/plugin-transform-block-scoping": "^7.25.7", + "@babel/plugin-transform-class-properties": "^7.25.7", + "@babel/plugin-transform-class-static-block": "^7.25.7", + "@babel/plugin-transform-classes": "^7.25.7", + "@babel/plugin-transform-computed-properties": "^7.25.7", + "@babel/plugin-transform-destructuring": "^7.25.7", + "@babel/plugin-transform-dotall-regex": "^7.25.7", + "@babel/plugin-transform-duplicate-keys": "^7.25.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-dynamic-import": "^7.25.7", + "@babel/plugin-transform-exponentiation-operator": "^7.25.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.7", + "@babel/plugin-transform-for-of": "^7.25.7", + "@babel/plugin-transform-function-name": "^7.25.7", + "@babel/plugin-transform-json-strings": "^7.25.7", + "@babel/plugin-transform-literals": "^7.25.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.7", + "@babel/plugin-transform-member-expression-literals": "^7.25.7", + "@babel/plugin-transform-modules-amd": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-modules-systemjs": "^7.25.7", + "@babel/plugin-transform-modules-umd": "^7.25.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-new-target": "^7.25.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.7", + "@babel/plugin-transform-numeric-separator": "^7.25.7", + "@babel/plugin-transform-object-rest-spread": "^7.25.7", + "@babel/plugin-transform-object-super": "^7.25.7", + "@babel/plugin-transform-optional-catch-binding": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7", + "@babel/plugin-transform-private-methods": "^7.25.7", + "@babel/plugin-transform-private-property-in-object": "^7.25.7", + "@babel/plugin-transform-property-literals": "^7.25.7", + "@babel/plugin-transform-regenerator": "^7.25.7", + "@babel/plugin-transform-reserved-words": "^7.25.7", + "@babel/plugin-transform-shorthand-properties": "^7.25.7", + "@babel/plugin-transform-spread": "^7.25.7", + "@babel/plugin-transform-sticky-regex": "^7.25.7", + "@babel/plugin-transform-template-literals": "^7.25.7", + "@babel/plugin-transform-typeof-symbol": "^7.25.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.7", + "@babel/plugin-transform-unicode-property-regex": "^7.25.7", + "@babel/plugin-transform-unicode-regex": "^7.25.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", "semver": "^6.3.1" }, "engines": { @@ -2319,7 +2162,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -2328,7 +2170,6 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", @@ -2340,7 +2181,6 @@ }, "node_modules/@babel/preset-react": { "version": "7.18.6", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -2358,16 +2198,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", - "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", - "dev": true, + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz", + "integrity": "sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-syntax-jsx": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-typescript": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2376,16 +2216,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2411,34 +2246,31 @@ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", - "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", - "dev": true, + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.4", - "@babel/generator": "^7.23.4", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.4", - "@babel/types": "^7.23.4", - "debug": "^4.1.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2446,13 +2278,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", - "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2460,7 +2292,6 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", - "dev": true, "license": "MIT" }, "node_modules/@colors/colors": { @@ -2474,7 +2305,7 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2485,13 +2316,77 @@ }, "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", + "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1" + } + }, "node_modules/@cypress/request": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", @@ -2571,7 +2466,6 @@ }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3943,6 +3837,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", @@ -4131,6 +4035,20 @@ "react-waypoint": ">=9.0.2" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.19.7", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", @@ -4487,7 +4405,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -4502,7 +4419,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -4511,7 +4427,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -4534,7 +4449,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4549,14 +4463,12 @@ "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -4571,7 +4483,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -4582,14 +4493,12 @@ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4601,7 +4510,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -4613,7 +4521,6 @@ "version": "8.54.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -4656,6 +4563,57 @@ "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==", "license": "MIT" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz", + "integrity": "sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.5", + "@formatjs/intl-localematcher": "0.5.9", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz", + "integrity": "sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.7.tgz", + "integrity": "sha512-cuEHyRM5VqLQobANOjtjlgU7+qmk9Q3fDQuBiRRJ3+Wp3ZoZhpUPtUfuimZXsir6SaI2TaAJ+SLo9vLnV5QcbA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "@formatjs/icu-skeleton-parser": "1.8.11", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.11.tgz", + "integrity": "sha512-8LlHHE/yL/zVJZHAX3pbKaCjZKmBIO6aJY1mkVh4RMSEu/2WRZ4Ysvv3kKXJ9M8RJLBHdnk1/dUQFdod1Dt7Dw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz", + "integrity": "sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -4664,12 +4622,10 @@ }, "node_modules/@hapi/hoek": { "version": "9.3.0", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -4679,7 +4635,6 @@ "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", @@ -4693,7 +4648,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -4705,8 +4659,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, "node_modules/@hutson/parse-repository-url": { "version": "3.0.2", @@ -4815,7 +4768,6 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", - "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -4830,7 +4782,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4838,22 +4789,22 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -4862,7 +4813,6 @@ }, "node_modules/@jest/console/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4876,7 +4826,6 @@ }, "node_modules/@jest/console/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4891,7 +4840,6 @@ }, "node_modules/@jest/console/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4902,40 +4850,40 @@ }, "node_modules/@jest/console/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/@jest/core": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "license": "MIT", "dependencies": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -4953,7 +4901,8 @@ }, "node_modules/@jest/core/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4968,7 +4917,8 @@ }, "node_modules/@jest/core/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4982,7 +4932,8 @@ }, "node_modules/@jest/core/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4993,15 +4944,17 @@ }, "node_modules/@jest/core/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/@jest/core/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -5010,88 +4963,95 @@ } }, "node_modules/@jest/core/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/@jest/environment": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "license": "MIT", "dependencies": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "license": "MIT", "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "license": "MIT", "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -5099,13 +5059,13 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -5125,7 +5085,6 @@ }, "node_modules/@jest/reporters/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5139,7 +5098,6 @@ }, "node_modules/@jest/reporters/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5154,7 +5112,6 @@ }, "node_modules/@jest/reporters/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5165,12 +5122,10 @@ }, "node_modules/@jest/reporters/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/@jest/reporters/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5187,9 +5142,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/reporters/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5202,7 +5172,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -5211,11 +5180,12 @@ } }, "node_modules/@jest/source-map": { - "version": "29.4.3", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, @@ -5224,12 +5194,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "license": "MIT", "dependencies": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -5238,13 +5209,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "license": "MIT", "dependencies": { - "@jest/test-result": "^29.5.0", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -5252,21 +5224,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -5278,7 +5251,6 @@ }, "node_modules/@jest/transform/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5292,7 +5264,6 @@ }, "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5307,7 +5278,6 @@ }, "node_modules/@jest/transform/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5318,20 +5288,19 @@ }, "node_modules/@jest/transform/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/@jest/transform/node_modules/convert-source-map": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/@jest/types": { - "version": "29.5.0", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -5344,7 +5313,6 @@ }, "node_modules/@jest/types/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5358,7 +5326,6 @@ }, "node_modules/@jest/types/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5373,7 +5340,6 @@ }, "node_modules/@jest/types/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5384,17 +5350,17 @@ }, "node_modules/@jest/types/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "devOptional": true, + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -5402,15 +5368,15 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "devOptional": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -5418,7 +5384,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.3", - "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -5427,26 +5392,20 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", - "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "devOptional": true, + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "devOptional": true, - "license": "MIT" - }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", - "dev": true, "license": "MIT" }, "node_modules/@lerna/child-process": { @@ -6486,6 +6445,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -13744,6 +13712,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@paulirish/trace_engine": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.39.tgz", + "integrity": "sha512-2Y/ejHX5DDi5bjfWY/0c/BLVSfQ61Jw1Hy60Hnh0hfEO632D3FVctkzT4Q/lVAdvIPR0bUaok9JDTr1pu/OziA==", + "license": "BSD-3-Clause", + "dependencies": { + "third-party-web": "latest" + } + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "dev": true, @@ -13962,14 +13939,25 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@playwright/test": { - "version": "1.47.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz", - "integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==", - "dev": true, + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz", + "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.47.1" + "playwright": "1.48.1" }, "bin": { "playwright": "cli.js" @@ -13978,9 +13966,65 @@ "node": ">=18" } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", + "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", + "license": "MIT", + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", - "dev": true, "license": "MIT" }, "node_modules/@preact/signals-core": { @@ -14008,6 +14052,65 @@ "react": "^16.14.0 || 17.x || 18.x" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -14737,9 +14840,89 @@ "string-argv": "~0.3.1" } }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.2.tgz", + "integrity": "sha512-eo2F8cP6X+vr54Mp6vu+NoQEDz0M5O24Tz8jPY0T1CpiWdwCmHb7Sln+oLXeQ3/LlWdVQihBfKDBZfBdUfsBTg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.2", + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.2.tgz", + "integrity": "sha512-eurLBFQJC7WWWYoEna25Z9I/GJjqAmH339tv52XP8sqXV7B5hRcHDcfrsT/UGHpU316M24p3lWhj0eimtCZ0SQ==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.2.tgz", + "integrity": "sha512-bMvL2fD3TGLM5YAUoQ2Qz6bYeVU8f7YRFNSjKNxK4EbvFgAU9j1FD6EKg0V0RNOJYnJjGIZYMmcWTXBbVTJL6w==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.2", + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.2.tgz", + "integrity": "sha512-ZnW9gpIGaoU+vYZyVZca9dObfmWYiXEWIMUM/JXaFb8AhP1OXvYweNiU0Pe/gNrz4oGAogU8scJc70ar7Vj0ww==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.2", + "@sentry/core": "7.120.2", + "@sentry/integrations": "7.120.2", + "@sentry/types": "7.120.2", + "@sentry/utils": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.2.tgz", + "integrity": "sha512-FWVoiblHQJ892GaOqdXx/5/n5XDLF28z81vJ0lCY49PMh8waz8LJ0b9RSmt9tasSDl0OQ7eUlPl1xu1jTrv1NA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.2.tgz", + "integrity": "sha512-jgnQlw11mRfQrQRAXbq4zEd+tbYwHel5eqeS/oU6EImXRjmHNtS79nB8MHvJeQu1FMCpFs1Ymrrs5FICwS6VeQ==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sideway/address": { - "version": "4.1.4", - "dev": true, + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" @@ -14747,12 +14930,10 @@ }, "node_modules/@sideway/formula": { "version": "3.0.1", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sigstore/bundle": { @@ -14897,8 +15078,7 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sindresorhus/is": { "version": "0.14.0", @@ -14909,16 +15089,18 @@ } }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.1.0", - "dev": true, + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -14937,6 +15119,28 @@ "node": ">=14" } }, + "node_modules/@stylistic/stylelint-plugin": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.1.tgz", + "integrity": "sha512-XagAHHIa528EvyGybv8EEYGK5zrVW74cHpsjhtovVATbhDRuJYfE+X4HCaAieW9lCkwbX6L+X0I4CiUG3w/hFw==", + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1", + "@csstools/media-query-list-parser": "^3.0.1", + "is-plain-object": "^5.0.0", + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0", + "style-search": "^0.1.0", + "stylelint": "^16.8.2" + }, + "engines": { + "node": "^18.12 || >=20.9" + }, + "peerDependencies": { + "stylelint": "^16.8.0" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "6.5.1", "dev": true, @@ -14954,7 +15158,6 @@ }, "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { "version": "8.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -14969,7 +15172,6 @@ }, "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "8.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -15572,15 +15774,19 @@ }, "node_modules/@tootallnate/once": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@trysound/sax": { "version": "0.2.0", - "dev": true, "license": "ISC", "engines": { "node": ">=10.13.0" @@ -15593,22 +15799,22 @@ }, "node_modules/@tsconfig/node10": { "version": "1.0.9", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tufjs/canonical-json": { @@ -15687,7 +15893,6 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -15698,7 +15903,6 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.4", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -15706,7 +15910,6 @@ }, "node_modules/@types/babel__template": { "version": "7.4.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -15715,7 +15918,6 @@ }, "node_modules/@types/babel__traverse": { "version": "7.18.5", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.3.0" @@ -15723,7 +15925,6 @@ }, "node_modules/@types/body-parser": { "version": "1.19.2", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -15732,7 +15933,6 @@ }, "node_modules/@types/bonjour": { "version": "3.5.10", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -15758,7 +15958,6 @@ }, "node_modules/@types/connect": { "version": "3.4.35", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -15766,7 +15965,6 @@ }, "node_modules/@types/connect-history-api-fallback": { "version": "1.5.0", - "dev": true, "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", @@ -15783,7 +15981,6 @@ }, "node_modules/@types/eslint": { "version": "8.37.0", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*", @@ -15791,8 +15988,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "dev": true, + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", "dependencies": { "@types/eslint": "*", @@ -15801,12 +15999,10 @@ }, "node_modules/@types/estree": { "version": "0.0.39", - "dev": true, "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.17", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -15817,7 +16013,6 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.17.35", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -15841,7 +16036,6 @@ }, "node_modules/@types/glob": { "version": "7.2.0", - "dev": true, "license": "MIT", "dependencies": { "@types/minimatch": "*", @@ -15850,7 +16044,6 @@ }, "node_modules/@types/graceful-fs": { "version": "4.1.6", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -15897,7 +16090,6 @@ }, "node_modules/@types/http-proxy": { "version": "1.17.11", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -15910,12 +16102,10 @@ }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -15923,7 +16113,6 @@ }, "node_modules/@types/istanbul-reports": { "version": "3.0.1", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" @@ -15958,7 +16147,6 @@ }, "node_modules/@types/jsdom": { "version": "20.0.1", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -15968,7 +16156,6 @@ }, "node_modules/@types/jsdom/node_modules/parse5": { "version": "7.1.2", - "dev": true, "license": "MIT", "dependencies": { "entities": "^4.4.0" @@ -15981,12 +16168,10 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", - "dev": true, "license": "MIT" }, "node_modules/@types/jsonwebtoken": { @@ -16007,19 +16192,16 @@ }, "node_modules/@types/mime": { "version": "1.3.2", - "dev": true, "license": "MIT" }, "node_modules/@types/minimatch": { "version": "3.0.5", - "dev": true, "license": "MIT" }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" }, "node_modules/@types/mousetrap": { "version": "1.6.15", @@ -16039,8 +16221,7 @@ "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" }, "node_modules/@types/object-path": { "version": "0.11.1", @@ -16056,23 +16237,16 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "dev": true, - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.5", "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.7", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.4", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -16152,7 +16326,6 @@ }, "node_modules/@types/retry": { "version": "0.12.0", - "dev": true, "license": "MIT" }, "node_modules/@types/sax": { @@ -16165,12 +16338,10 @@ }, "node_modules/@types/semver": { "version": "7.5.0", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.1", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -16179,7 +16350,6 @@ }, "node_modules/@types/serve-index": { "version": "1.9.1", - "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" @@ -16187,7 +16357,6 @@ }, "node_modules/@types/serve-static": { "version": "1.15.1", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "*", @@ -16218,20 +16387,29 @@ }, "node_modules/@types/sockjs": { "version": "0.3.33", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/source-list-map": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.1", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/tapable": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", + "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", "license": "MIT" }, "node_modules/@types/tough-cookie": { "version": "4.0.2", - "dev": true, "license": "MIT" }, "node_modules/@types/ua-parser-js": { @@ -16239,6 +16417,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "license": "MIT", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/uglify-js/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@types/unist": { "version": "2.0.6", "dev": true, @@ -16254,6 +16450,49 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/webpack": { + "version": "4.41.40", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", + "integrity": "sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack-sources/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/webpack/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@types/wicg-file-system-access": { "version": "2023.10.5", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.5.tgz", @@ -17139,8 +17378,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.4", - "dev": true, + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -17148,7 +17388,6 @@ }, "node_modules/@types/yargs": { "version": "17.0.24", - "dev": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -17156,14 +17395,12 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.0", - "dev": true, "license": "MIT" }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, "optional": true, "dependencies": { "@types/node": "*" @@ -17173,7 +17410,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", - "dev": true, + "devOptional": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -17207,7 +17444,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "dev": true, + "devOptional": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -17234,7 +17471,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" @@ -17251,7 +17487,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "dev": true, + "devOptional": true, "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", @@ -17278,7 +17514,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -17291,7 +17526,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", @@ -17318,7 +17552,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", @@ -17344,7 +17577,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" @@ -17914,136 +18146,195 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "dev": true, + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, "node_modules/@wessberg/stringutil": { "version": "1.0.19", "dev": true, @@ -18097,6 +18388,39 @@ "npm": ">=8.19.2" } }, + "node_modules/@wordpress/babel-preset-default": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.14.0.tgz", + "integrity": "sha512-p7XtDRfuHtUjzz+k3YTqC/T2yzcgN+pFs7TFoXTZQQ/ZygmdXGzekZ10XcZc5aqJ5Kb5YvNv2jJlaBIlAYP7Qg==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/core": "7.25.7", + "@babel/plugin-transform-react-jsx": "7.25.7", + "@babel/plugin-transform-runtime": "7.25.7", + "@babel/preset-env": "7.25.7", + "@babel/preset-typescript": "7.25.7", + "@babel/runtime": "7.25.7", + "@wordpress/browserslist-config": "*", + "@wordpress/warning": "*", + "browserslist": "^4.21.10", + "core-js": "^3.31.0", + "react": "^18.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/base-styles": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-5.14.0.tgz", + "integrity": "sha512-VvWe/eq7g/CmICeHWjRPUkeRLEXo7TF9A7sh636KopCRkdzGtbBcNCDh7y+GusL6hF78Kfc+H6L8QUdYkMUb7A==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/blob": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@wordpress/blob/-/blob-4.4.0.tgz", @@ -18291,6 +18615,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@wordpress/browserslist-config": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.14.0.tgz", + "integrity": "sha512-a26hxY8R/A7FH/Z8oZsYS31ZC/Xy9QSBTi5w84MKSeYdWlck7t1QdCwUNF1u621wbuP7beiiu9FkYY4hI3Bk9A==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/commands": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@wordpress/commands/-/commands-1.4.0.tgz", @@ -18845,6 +19179,28 @@ "npm": ">=8.19.2" } }, + "node_modules/@wordpress/dependency-extraction-webpack-plugin": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.14.0.tgz", + "integrity": "sha512-gLiY07oJT5ejkQwca9kni+iamiWIKF+iadIrzfCTB+34jc8xXVT3ENmydLPcAY5toAejwJ+UmlCxCV8kXFatmA==", + "license": "GPL-2.0-or-later", + "dependencies": { + "json2php": "^0.0.7" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@wordpress/dependency-extraction-webpack-plugin/node_modules/json2php": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.7.tgz", + "integrity": "sha512-dnSoUiLAoVaMXxFsVi4CrPVYMKOuDBXTghXSmMINX44RZ8WM9cXlY7UqrQnlAcODCVO7FV3+8t/5nDKAjimLfg==", + "license": "BSD" + }, "node_modules/@wordpress/deprecated": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.9.0.tgz", @@ -18886,6 +19242,27 @@ "npm": ">=8.19.2" } }, + "node_modules/@wordpress/e2e-test-utils-playwright": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.14.0.tgz", + "integrity": "sha512-G9r3ZysgzAmUbR4bjGAEEP6P2RCIAG8uMU7yyzxOAHegINSbF3shEZKvVNBeKxNwHKAVa9koh/niGN3U4Kr6Rw==", + "license": "GPL-2.0-or-later", + "dependencies": { + "change-case": "^4.1.2", + "form-data": "^4.0.0", + "get-port": "^5.1.1", + "lighthouse": "^12.2.2", + "mime": "^3.0.0", + "web-vitals": "^4.2.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": ">=1" + } + }, "node_modules/@wordpress/element": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.4.0.tgz", @@ -19042,6 +19419,41 @@ "npm": ">=8.19.2" } }, + "node_modules/@wordpress/jest-console": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.14.0.tgz", + "integrity": "sha512-IFl9QJfGZegkwQ2gp26UMaQ0RL1yNj5BZsDBh3dGSkE9TTWm9ngrVms8ppHZ6EDA1v92z30VcKdB7rOmWXrk1w==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "7.25.7", + "jest-matcher-utils": "^29.6.2" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "jest": ">=29" + } + }, + "node_modules/@wordpress/jest-preset-default": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.14.0.tgz", + "integrity": "sha512-YYfJ+hafo9/wMRBWhHt95eeUU1T6iptdHwfhpn6ZLtxFkq5YtNolgOLT7e7hdjsg+rS49gKBclDI80i/r0DFeA==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/jest-console": "*", + "babel-jest": "29.7.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@babel/core": ">=7", + "jest": ">=29" + } + }, "node_modules/@wordpress/keyboard-shortcuts": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@wordpress/keyboard-shortcuts/-/keyboard-shortcuts-5.4.0.tgz", @@ -19144,6 +19556,36 @@ "react": "^18.0.0" } }, + "node_modules/@wordpress/npm-package-json-lint-config": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.14.0.tgz", + "integrity": "sha512-URZoTCrOfDEH1eo4yND0ASJAi73facDy2DKQmuHQXO07I98stoHAxXr7WCZJh3DatOaOR0QdlteiCAiMf8YBVA==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "npm-package-json-lint": ">=6.0.0" + } + }, + "node_modules/@wordpress/postcss-plugins-preset": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.14.0.tgz", + "integrity": "sha512-K9Ob4ILtvVyWKbXDZuFvAgyZbCoFVYxrv1HaEx+ir6Ct6Tdltuv9e9WzH0mOfhFi5IDR72gmOFllhjzo7FCmaw==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/base-styles": "*", + "autoprefixer": "^10.2.5" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/@wordpress/preferences": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@wordpress/preferences/-/preferences-4.4.0.tgz", @@ -19390,283 +19832,2109 @@ "npm": ">=8.19.2" } }, - "node_modules/@wordpress/shortcode": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.4.0.tgz", - "integrity": "sha512-iHxwn9nM7yIc3q2JbQdwcM6eXeg40P82gIQM/HOI+D3P5spfg3gJFzv+MWQpkUVF5BJY537plCQxHt3fxzNkZw==", - "dev": true, + "node_modules/@wordpress/scripts": { + "version": "30.7.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.7.0.tgz", + "integrity": "sha512-vwrf6Xo1GXV2ug4xdYMgZ2CVpNNfArOEJyX6w9CafIRmLOm8GkVGSza0VlEoOh1BTqQPv/awq6uiOKVMbVNB5Q==", "license": "GPL-2.0-or-later", "dependencies": { - "@babel/runtime": "^7.16.0", - "memize": "^2.0.1" + "@babel/core": "7.25.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@svgr/webpack": "^8.0.1", + "@wordpress/babel-preset-default": "*", + "@wordpress/browserslist-config": "*", + "@wordpress/dependency-extraction-webpack-plugin": "*", + "@wordpress/e2e-test-utils-playwright": "*", + "@wordpress/eslint-plugin": "*", + "@wordpress/jest-preset-default": "*", + "@wordpress/npm-package-json-lint-config": "*", + "@wordpress/postcss-plugins-preset": "*", + "@wordpress/prettier-config": "*", + "@wordpress/stylelint-config": "*", + "adm-zip": "^0.5.9", + "babel-jest": "29.7.0", + "babel-loader": "9.2.1", + "browserslist": "^4.21.10", + "chalk": "^4.0.0", + "check-node-version": "^4.1.0", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-spawn": "^7.0.6", + "css-loader": "^6.2.0", + "cssnano": "^6.0.1", + "cwd": "^0.10.0", + "dir-glob": "^3.0.1", + "eslint": "^8.3.0", + "expect-puppeteer": "^4.4.0", + "fast-glob": "^3.2.7", + "filenamify": "^4.2.0", + "jest": "^29.6.2", + "jest-dev-server": "^10.1.4", + "jest-environment-jsdom": "^29.6.2", + "jest-environment-node": "^29.6.2", + "json2php": "^0.0.9", + "markdownlint-cli": "^0.31.1", + "merge-deep": "^3.0.3", + "mini-css-extract-plugin": "^2.9.2", + "minimist": "^1.2.0", + "npm-package-json-lint": "^6.4.0", + "npm-packlist": "^3.0.0", + "postcss": "^8.4.5", + "postcss-loader": "^6.2.1", + "prettier": "npm:wp-prettier@3.0.3", + "puppeteer-core": "^23.10.1", + "react-refresh": "^0.14.0", + "read-pkg-up": "^7.0.1", + "resolve-bin": "^0.4.0", + "rtlcss-webpack-plugin": "^4.0.7", + "sass": "^1.50.1", + "sass-loader": "^16.0.3", + "schema-utils": "^4.2.0", + "source-map-loader": "^3.0.0", + "stylelint": "^16.8.2", + "terser-webpack-plugin": "^5.3.10", + "url-loader": "^4.1.1", + "webpack": "^5.97.0", + "webpack-bundle-analyzer": "^4.9.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + }, + "bin": { + "wp-scripts": "bin/wp-scripts.js" }, "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": "^1.48.1", + "react": "^18.0.0", + "react-dom": "^18.0.0" } }, - "node_modules/@wordpress/style-engine": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/style-engine/-/style-engine-2.4.0.tgz", - "integrity": "sha512-BY5WwK79qKAkMt+OvsTt2thM8B07Eup8+Gb9xn0ZoE/uuLabzH1AMDIYWQ06nWBOGMDT7WFfSIDp43xWpnc3/g==", - "dev": true, - "license": "GPL-2.0-or-later", + "node_modules/@wordpress/scripts/node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.16.0", - "change-case": "^4.1.2" + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" }, "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@wordpress/sync": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/sync/-/sync-1.4.0.tgz", - "integrity": "sha512-PvUmCeQOGz8tFWEZkHIXas7cmo9Gd1PrvGcWIL2a6X2HdePZNBa67A7ykcUAZHOZjJTaJCyeZHtRhG6Vms0zig==", - "dev": true, - "license": "GPL-2.0-or-later", - "dependencies": { - "@babel/runtime": "^7.16.0", - "@types/simple-peer": "^9.11.5", - "@wordpress/url": "^4.4.0", - "import-locals": "^2.0.0", - "lib0": "^0.2.42", - "simple-peer": "^9.11.0", - "y-indexeddb": "~9.0.11", - "y-protocols": "^1.0.5", - "y-webrtc": "~10.2.5", - "yjs": "~13.6.6" - }, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@wordpress/token-list": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/token-list/-/token-list-3.4.0.tgz", - "integrity": "sha512-I1KAyiIBx9Ro9ilNk5u6rwWhplc5OkuFtOy1Nw9eytpVIICXyd/A6O19hTkmrFrlvfDepZFYTij+M//ONGDIrg==", - "dev": true, - "license": "GPL-2.0-or-later", - "dependencies": { - "@babel/runtime": "^7.16.0" - }, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@wordpress/undo-manager": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.9.0.tgz", - "integrity": "sha512-JLrcmeCTqITbChkJy+PeXcE03+6ZgIfQ22cBg1+0mzLQxglx1gndTnhRcnCSebvsXnmOVmxvE4HmJ84lv7liCQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/is-shallow-equal": "^5.9.0" - }, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@wordpress/url": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.4.0.tgz", - "integrity": "sha512-t9yPr+c/T/8y04Yktdb/URJrQCQ4xPM7M9dq6h63IZHSaz3Ndr94Sn/1T10rXFlOoBJ6pr6AdetecFRfLLbh4g==", - "dev": true, - "license": "GPL-2.0-or-later", - "dependencies": { - "@babel/runtime": "^7.16.0", - "remove-accents": "^0.5.0" - }, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@wordpress/warning": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.9.0.tgz", - "integrity": "sha512-c+bEWwDjp3+Q7SAGb47CuZe56giBFNvutoyiAkn34pQZeO8pRjPElRABIkR7oyn4dEusjL1f6OQmU3dSYAMTpg==", - "license": "GPL-2.0-or-later", + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@wordpress/wordcount": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/wordcount/-/wordcount-4.4.0.tgz", - "integrity": "sha512-CKaz6YzmO7BeYtiyvlDGvnBRfmrn3qDBYY3r91KomAQvHl6m3M8N4SYfCoKjOldbTMgYCah6EPn4vqCxs6DEeg==", - "dev": true, - "license": "GPL-2.0-or-later", - "dependencies": { - "@babel/runtime": "^7.16.0" - }, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@wp-playground/blueprints": { - "resolved": "packages/playground/blueprints", - "link": true - }, - "node_modules/@wp-playground/cli": { - "resolved": "packages/playground/cli", - "link": true - }, - "node_modules/@wp-playground/client": { - "resolved": "packages/playground/client", - "link": true - }, - "node_modules/@wp-playground/common": { - "resolved": "packages/playground/common", - "link": true - }, - "node_modules/@wp-playground/components": { - "resolved": "packages/playground/components", - "link": true - }, - "node_modules/@wp-playground/data-liberation": { - "resolved": "packages/playground/data-liberation", - "link": true - }, - "node_modules/@wp-playground/nx-extensions": { - "resolved": "packages/nx-extensions", - "link": true - }, - "node_modules/@wp-playground/php-cors-proxy": { - "resolved": "packages/playground/php-cors-proxy", - "link": true - }, - "node_modules/@wp-playground/remote": { - "resolved": "packages/playground/remote", - "link": true - }, - "node_modules/@wp-playground/storage": { - "resolved": "packages/playground/storage", - "link": true - }, - "node_modules/@wp-playground/sync": { - "resolved": "packages/playground/sync", - "link": true - }, - "node_modules/@wp-playground/wordpress": { - "resolved": "packages/playground/wordpress", - "link": true - }, - "node_modules/@wp-playground/wordpress-builds": { - "resolved": "packages/playground/wordpress-builds", - "link": true - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true - }, - "node_modules/@yarnpkg/parsers": { - "version": "3.0.0-rc.46", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", - "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", - "dev": true, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", "dependencies": { - "js-yaml": "^3.10.0", - "tslib": "^2.4.0" + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "engines": { - "node": ">=14.15.0" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@zkochan/js-yaml": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", - "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", - "dev": true, + "node_modules/@wordpress/scripts/node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@zkochan/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/abab": { - "version": "2.0.6", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/accepts": { - "version": "1.3.8", + "node_modules/@wordpress/scripts/node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@babel/types": "^7.21.3", + "entities": "^4.4.0" }, "engines": { - "node": ">= 0.6" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "devOptional": true, - "bin": { - "acorn": "bin/acorn" + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" }, "engines": { - "node": ">=0.4.0" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-jsx/node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.0.0.tgz", + "integrity": "sha512-Hh1sO9UV0IYI7D+F6EQnhvs2HAv4H0iBVZikXZKcPmQudlwgV2OWdNprdSe8IoRmpMqmhQ+gkaj9Gwk6NReGHQ==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/eslint-parser": "7.25.7", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", + "@wordpress/babel-preset-default": "*", + "@wordpress/prettier-config": "*", + "cosmiconfig": "^7.0.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jest": "^27.4.3", + "eslint-plugin-jsdoc": "^46.4.6", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-playwright": "^0.15.3", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react-hooks": "^4.3.0", + "globals": "^13.12.0", + "requireindex": "^1.2.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@babel/core": ">=7", + "eslint": ">=8", + "prettier": ">=3", + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin/node_modules/@babel/eslint-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.7.tgz", + "integrity": "sha512-B+BO9x86VYsQHimucBAL1fxTJKF4wyKY6ZVzee9QgzdZOUfs3BaR6AQrgoGrRI+7IFS1wUz/VyQ+SoBcSpdPbw==", + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin/node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin/node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin/node_modules/eslint-plugin-jsdoc": { + "version": "46.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", + "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.41.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin/node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@wordpress/scripts/node_modules/@wordpress/prettier-config": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.14.0.tgz", + "integrity": "sha512-DZuASK64Jr8ycj9uaSlwsTURiaQ0sgQnu9ThgSK196jcDF1jxTll8JGoVIXgxhKgo3mFFjdtkNeBZ38DT5z6/g==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "prettier": ">=3" + } + }, + "node_modules/@wordpress/scripts/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@wordpress/scripts/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@wordpress/scripts/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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wordpress/scripts/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/@wordpress/scripts/node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/@wordpress/scripts/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/@wordpress/scripts/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "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/@wordpress/scripts/node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@wordpress/scripts/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@wordpress/scripts/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wordpress/scripts/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, + "node_modules/@wordpress/scripts/node_modules/ignore-walk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", + "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==", + "license": "ISC", + "dependencies": { + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@wordpress/scripts/node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@wordpress/scripts/node_modules/ignore-walk/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@wordpress/scripts/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@wordpress/scripts/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/@wordpress/scripts/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/@wordpress/scripts/node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wordpress/scripts/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/@wordpress/scripts/node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@wordpress/scripts/node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/@wordpress/scripts/node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "license": "ISC" + }, + "node_modules/@wordpress/scripts/node_modules/npm-packlist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz", + "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==", + "license": "ISC", + "dependencies": { + "glob": "^7.1.6", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/prettier": { + "name": "wp-prettier", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", + "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@wordpress/scripts/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wordpress/scripts/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wordpress/scripts/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/@wordpress/scripts/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/@wordpress/scripts/node_modules/sass-loader": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@wordpress/scripts/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/@wordpress/scripts/node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/@wordpress/scripts/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@wordpress/shortcode": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.4.0.tgz", + "integrity": "sha512-iHxwn9nM7yIc3q2JbQdwcM6eXeg40P82gIQM/HOI+D3P5spfg3gJFzv+MWQpkUVF5BJY537plCQxHt3fxzNkZw==", "dev": true, + "license": "GPL-2.0-or-later", "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" + "@babel/runtime": "^7.16.0", + "memize": "^2.0.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", + "node_modules/@wordpress/style-engine": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/style-engine/-/style-engine-2.4.0.tgz", + "integrity": "sha512-BY5WwK79qKAkMt+OvsTt2thM8B07Eup8+Gb9xn0ZoE/uuLabzH1AMDIYWQ06nWBOGMDT7WFfSIDp43xWpnc3/g==", "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "change-case": "^4.1.2" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/stylelint-config": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.6.0.tgz", + "integrity": "sha512-vi0Uz/CFt3FHWpXpVFTulSWdHRa3WaiCo1qX58cG3Z31bs1bgqUT3pOpW/4pfLQACdYZS8YpOKRidbFSmfc6og==", "license": "MIT", + "dependencies": { + "@stylistic/stylelint-plugin": "^3.0.1", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-recommended-scss": "^14.1.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, "peerDependencies": { - "acorn": "^8" + "stylelint": "^16.8.2" + } + }, + "node_modules/@wordpress/sync": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/sync/-/sync-1.4.0.tgz", + "integrity": "sha512-PvUmCeQOGz8tFWEZkHIXas7cmo9Gd1PrvGcWIL2a6X2HdePZNBa67A7ykcUAZHOZjJTaJCyeZHtRhG6Vms0zig==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@types/simple-peer": "^9.11.5", + "@wordpress/url": "^4.4.0", + "import-locals": "^2.0.0", + "lib0": "^0.2.42", + "simple-peer": "^9.11.0", + "y-indexeddb": "~9.0.11", + "y-protocols": "^1.0.5", + "y-webrtc": "~10.2.5", + "yjs": "~13.6.6" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/token-list": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/token-list/-/token-list-3.4.0.tgz", + "integrity": "sha512-I1KAyiIBx9Ro9ilNk5u6rwWhplc5OkuFtOy1Nw9eytpVIICXyd/A6O19hTkmrFrlvfDepZFYTij+M//ONGDIrg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/undo-manager": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.9.0.tgz", + "integrity": "sha512-JLrcmeCTqITbChkJy+PeXcE03+6ZgIfQ22cBg1+0mzLQxglx1gndTnhRcnCSebvsXnmOVmxvE4HmJ84lv7liCQ==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/is-shallow-equal": "^5.9.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/url": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.4.0.tgz", + "integrity": "sha512-t9yPr+c/T/8y04Yktdb/URJrQCQ4xPM7M9dq6h63IZHSaz3Ndr94Sn/1T10rXFlOoBJ6pr6AdetecFRfLLbh4g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "remove-accents": "^0.5.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/warning": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.9.0.tgz", + "integrity": "sha512-c+bEWwDjp3+Q7SAGb47CuZe56giBFNvutoyiAkn34pQZeO8pRjPElRABIkR7oyn4dEusjL1f6OQmU3dSYAMTpg==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wordpress/wordcount": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/wordcount/-/wordcount-4.4.0.tgz", + "integrity": "sha512-CKaz6YzmO7BeYtiyvlDGvnBRfmrn3qDBYY3r91KomAQvHl6m3M8N4SYfCoKjOldbTMgYCah6EPn4vqCxs6DEeg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, + "node_modules/@wp-playground/blueprints": { + "resolved": "packages/playground/blueprints", + "link": true + }, + "node_modules/@wp-playground/cli": { + "resolved": "packages/playground/cli", + "link": true + }, + "node_modules/@wp-playground/client": { + "resolved": "packages/playground/client", + "link": true + }, + "node_modules/@wp-playground/common": { + "resolved": "packages/playground/common", + "link": true + }, + "node_modules/@wp-playground/components": { + "resolved": "packages/playground/components", + "link": true + }, + "node_modules/@wp-playground/data-liberation": { + "resolved": "packages/playground/data-liberation", + "link": true + }, + "node_modules/@wp-playground/nx-extensions": { + "resolved": "packages/nx-extensions", + "link": true + }, + "node_modules/@wp-playground/php-cors-proxy": { + "resolved": "packages/playground/php-cors-proxy", + "link": true + }, + "node_modules/@wp-playground/remote": { + "resolved": "packages/playground/remote", + "link": true + }, + "node_modules/@wp-playground/storage": { + "resolved": "packages/playground/storage", + "link": true + }, + "node_modules/@wp-playground/sync": { + "resolved": "packages/playground/sync", + "link": true + }, + "node_modules/@wp-playground/wordpress": { + "resolved": "packages/playground/wordpress", + "link": true + }, + "node_modules/@wp-playground/wordpress-builds": { + "resolved": "packages/playground/wordpress-builds", + "link": true + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/@yarnpkg/parsers": { + "version": "3.0.0-rc.46", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", + "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", + "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@zkochan/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" } }, "node_modules/acorn-jsx": { "version": "5.3.2", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -19674,7 +21942,6 @@ }, "node_modules/acorn-walk": { "version": "8.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -19694,9 +21961,17 @@ "node": ">= 10.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", - "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -19743,9 +22018,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, "node_modules/ajv-formats": { "version": "2.1.1", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -19801,7 +22084,6 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -19809,7 +22091,6 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -19821,9 +22102,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", - "dev": true, "engines": [ "node >= 0.8.0" ], @@ -19846,7 +22138,6 @@ }, "node_modules/ansi-styles": { "version": "5.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -19857,7 +22148,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "devOptional": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -19893,6 +22183,15 @@ } ] }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/are-we-there-yet": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", @@ -19909,12 +22208,11 @@ }, "node_modules/arg": { "version": "4.1.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { "version": "1.0.10", - "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -19922,7 +22220,6 @@ }, "node_modules/argparse/node_modules/sprintf-js": { "version": "1.0.3", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/aria-hidden": { @@ -19940,15 +22237,22 @@ }, "node_modules/aria-query": { "version": "5.1.3", - "dev": true, "license": "Apache-2.0", "dependencies": { "deep-equal": "^2.0.5" } }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -19969,7 +22273,6 @@ }, "node_modules/array-flatten": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/array-ify": { @@ -19980,7 +22283,6 @@ }, "node_modules/array-includes": { "version": "3.1.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -19998,7 +22300,6 @@ }, "node_modules/array-union": { "version": "2.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -20006,7 +22307,6 @@ }, "node_modules/array-uniq": { "version": "1.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20014,7 +22314,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -20031,7 +22330,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -20048,7 +22346,6 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -20062,7 +22359,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -20100,16 +22396,26 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.7", - "dev": true, "license": "ISC" }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, "engines": { "node": ">=8" } @@ -20150,7 +22456,6 @@ }, "node_modules/autoprefixer": { "version": "10.4.14", - "dev": true, "funding": [ { "type": "opencollective", @@ -20187,7 +22492,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -20212,8 +22516,9 @@ "dev": true }, "node_modules/axe-core": { - "version": "4.7.1", - "dev": true, + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -20231,21 +22536,27 @@ }, "node_modules/axobject-query": { "version": "3.1.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "deep-equal": "^2.0.5" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, "node_modules/babel-jest": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "license": "MIT", "dependencies": { - "@jest/transform": "^29.5.0", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -20259,7 +22570,6 @@ }, "node_modules/babel-jest/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -20273,7 +22583,6 @@ }, "node_modules/babel-jest/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -20288,7 +22597,6 @@ }, "node_modules/babel-jest/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -20299,15 +22607,15 @@ }, "node_modules/babel-jest/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/babel-loader": { - "version": "9.1.2", - "dev": true, + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "license": "MIT", "dependencies": { - "find-cache-dir": "^3.3.2", + "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "engines": { @@ -20318,6 +22626,119 @@ "webpack": ">=5" } }, + "node_modules/babel-loader/node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/babel-loader/node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/babel-plugin-apply-mdx-type-prop": { "version": "1.6.22", "dev": true, @@ -20379,7 +22800,6 @@ }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -20393,8 +22813,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -20432,13 +22853,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", - "dev": true, + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -20449,31 +22870,31 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", - "dev": true, + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.33.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", - "dev": true, + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -20494,7 +22915,6 @@ }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -20515,11 +22935,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.5.0", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -20529,6 +22950,30 @@ "@babel/core": "^7.0.0" } }, + "node_modules/babel-runtime": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", + "integrity": "sha512-zeCYxDePWYAT/DfmQWIHsMSFW2vv45UIwIAMjGvQVsTd47RwsiRH0uK1yzyWZ7LDBKdhnGDPM6NYEO5CZyhPrg==", + "license": "MIT", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.10.0" + } + }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==", + "license": "MIT" + }, "node_modules/bail": { "version": "1.0.5", "dev": true, @@ -20542,6 +22987,52 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", + "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", + "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.1.tgz", + "integrity": "sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + } + }, "node_modules/base16": { "version": "1.0.0", "dev": true, @@ -20549,7 +23040,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, "funding": [ { "type": "github", @@ -20584,9 +23074,17 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", - "dev": true, "license": "MIT" }, "node_modules/bcrypt-pbkdf": { @@ -20612,7 +23110,6 @@ }, "node_modules/big.js": { "version": "5.2.2", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -20686,7 +23183,6 @@ }, "node_modules/binary-extensions": { "version": "2.2.0", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -20783,7 +23279,6 @@ }, "node_modules/bonjour-service": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "array-flatten": "^2.1.2", @@ -20794,7 +23289,6 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/bottleneck": { @@ -20956,20 +23450,21 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", - "dev": true, + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "funding": [ { "type": "opencollective", @@ -20984,11 +23479,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -21037,7 +23533,6 @@ }, "node_modules/bser": { "version": "2.1.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" @@ -21187,7 +23682,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "engines": { "node": "*" } @@ -21199,7 +23693,6 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "devOptional": true, "license": "MIT" }, "node_modules/buffer-indexof-polyfill": { @@ -21217,7 +23710,6 @@ }, "node_modules/builtin-modules": { "version": "3.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -21246,7 +23738,6 @@ }, "node_modules/bytes": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -21619,7 +24110,6 @@ }, "node_modules/camelcase": { "version": "6.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -21640,7 +24130,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", @@ -21657,14 +24146,12 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } }, "node_modules/caniuse-api": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.0.0", @@ -21674,10 +24161,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001564", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", - "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", - "dev": true, + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "funding": [ { "type": "opencollective", @@ -21691,7 +24177,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/capital-case": { "version": "1.0.4", @@ -21777,7 +24264,6 @@ }, "node_modules/char-regex": { "version": "1.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -21846,6 +24332,81 @@ "node": ">= 0.8.0" } }, + "node_modules/check-node-version": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-4.2.1.tgz", + "integrity": "sha512-YYmFYHV/X7kSJhuN/QYHUu998n/TRuDe8UenM3+m5NrkiH670lb9ILqHIvBencvJc4SDh+XcbXMR4b+TtubJiw==", + "license": "Unlicense", + "dependencies": { + "chalk": "^3.0.0", + "map-values": "^1.0.1", + "minimist": "^1.2.0", + "object-filter": "^1.0.2", + "run-parallel": "^1.1.4", + "semver": "^6.3.0" + }, + "bin": { + "check-node-version": "bin.js" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/check-node-version/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/check-node-version/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-node-version/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/check-node-version/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/check-node-version/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "dev": true, @@ -21895,7 +24456,6 @@ }, "node_modules/chokidar": { "version": "3.5.3", - "devOptional": true, "funding": [ { "type": "individual", @@ -21928,17 +24488,46 @@ "node": ">=10" } }, + "node_modules/chrome-launcher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.2.tgz", + "integrity": "sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/ci-info": { "version": "3.8.0", - "dev": true, "funding": [ { "type": "github", @@ -21951,8 +24540,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "dev": true, + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "license": "MIT" }, "node_modules/classnames": { @@ -21990,6 +24580,141 @@ "node": ">=6" } }, + "node_modules/clean-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "license": "MIT", + "dependencies": { + "@types/webpack": "^4.4.31", + "del": "^4.1.1" + }, + "engines": { + "node": ">=8.9.0" + }, + "peerDependencies": { + "webpack": "*" + } + }, + "node_modules/clean-webpack-plugin/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-webpack-plugin/node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "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/clean-webpack-plugin/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-webpack-plugin/node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/clean-webpack-plugin/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clean-webpack-plugin/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "dev": true, @@ -22093,7 +24818,6 @@ }, "node_modules/clone-deep": { "version": "4.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", @@ -22106,7 +24830,6 @@ }, "node_modules/clone-deep/node_modules/is-plain-object": { "version": "2.0.4", - "dev": true, "license": "MIT", "dependencies": { "isobject": "^3.0.1" @@ -22171,7 +24894,8 @@ }, "node_modules/co": { "version": "4.6.0", - "dev": true, + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -22189,7 +24913,6 @@ }, "node_modules/collect-v8-coverage": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/color": { @@ -22206,6 +24929,7 @@ }, "node_modules/color-convert": { "version": "1.9.3", + "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -22213,6 +24937,7 @@ }, "node_modules/color-name": { "version": "1.1.3", + "dev": true, "license": "MIT" }, "node_modules/color-string": { @@ -22255,7 +24980,6 @@ }, "node_modules/colorette": { "version": "2.0.20", - "dev": true, "license": "MIT" }, "node_modules/colors": { @@ -22315,17 +25039,31 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, "engines": { "node": ">= 10" } }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/common-ancestor-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "dev": true }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -22375,7 +25113,6 @@ }, "node_modules/compressible": { "version": "2.0.18", - "dev": true, "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" @@ -22386,7 +25123,6 @@ }, "node_modules/compression": { "version": "1.7.4", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.5", @@ -22403,7 +25139,6 @@ }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -22411,12 +25146,10 @@ }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/compression/node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, "license": "MIT" }, "node_modules/compute-scroll-into-view": { @@ -22489,7 +25222,6 @@ }, "node_modules/configstore": { "version": "5.0.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dot-prop": "^5.2.0", @@ -22505,7 +25237,6 @@ }, "node_modules/configstore/node_modules/write-file-atomic": { "version": "3.0.3", - "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -22521,7 +25252,6 @@ }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8" @@ -22789,7 +25519,6 @@ }, "node_modules/copy-webpack-plugin": { "version": "10.2.4", - "dev": true, "license": "MIT", "dependencies": { "fast-glob": "^3.2.7", @@ -22812,7 +25541,6 @@ }, "node_modules/copy-webpack-plugin/node_modules/array-union": { "version": "3.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -22823,7 +25551,6 @@ }, "node_modules/copy-webpack-plugin/node_modules/glob-parent": { "version": "6.0.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -22834,7 +25561,6 @@ }, "node_modules/copy-webpack-plugin/node_modules/globby": { "version": "12.2.0", - "dev": true, "license": "MIT", "dependencies": { "array-union": "^3.0.1", @@ -22853,7 +25579,6 @@ }, "node_modules/copy-webpack-plugin/node_modules/slash": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -22863,8 +25588,9 @@ } }, "node_modules/core-js": { - "version": "3.30.2", - "dev": true, + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -22873,12 +25599,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", - "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", - "dev": true, + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", + "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "license": "MIT", "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.24.2" }, "funding": { "type": "opencollective", @@ -22887,7 +25613,6 @@ }, "node_modules/core-js-pure": { "version": "3.30.2", - "dev": true, "hasInstallScript": true, "license": "MIT", "funding": { @@ -22934,9 +25659,79 @@ "node": ">=0.8" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/create-require": { "version": "1.1.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-fetch": { @@ -22986,8 +25781,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -23025,12 +25821,17 @@ }, "node_modules/crypto-random-string": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/csp_evaluator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.1.tgz", + "integrity": "sha512-N3ASg0C4kNPUaNxt1XAvzHIVuzdtr8KLgfk1O8WDyimp1GisPAHESupArO2ieHk9QWbrJ/WkQODyh21Ps/xhxw==", + "license": "Apache-2.0" + }, "node_modules/css": { "version": "3.0.0", "devOptional": true, @@ -23052,9 +25853,17 @@ "postcss": "^8.0.9" } }, + "node_modules/css-functions-list": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "license": "MIT", + "engines": { + "node": ">=12 || >=16" + } + }, "node_modules/css-loader": { "version": "6.7.3", - "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", @@ -23698,7 +26507,6 @@ }, "node_modules/css-select": { "version": "5.1.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -23733,7 +26541,6 @@ }, "node_modules/css-what": { "version": "6.1.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -23752,7 +26559,6 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -23867,8 +26673,7 @@ "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, "node_modules/cssstyle": { "version": "3.0.0", @@ -23886,6 +26691,19 @@ "version": "3.1.2", "license": "MIT" }, + "node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cypress": { "version": "13.6.0", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", @@ -24178,7 +26996,6 @@ }, "node_modules/damerau-levenshtein": { "version": "1.0.8", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/dargs": { @@ -24202,6 +27019,15 @@ "node": ">=0.10" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", @@ -24247,12 +27073,19 @@ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", "dev": true }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, "node_modules/debug": { - "version": "4.3.4", - "dev": true, + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -24263,9 +27096,14 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/decamelize": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -24275,7 +27113,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" @@ -24291,14 +27128,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/decimal.js": { "version": "10.4.3", - "dev": true, "license": "MIT" }, "node_modules/decode-named-character-reference": { @@ -24362,7 +27197,6 @@ }, "node_modules/deep-equal": { "version": "2.2.1", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", @@ -24390,7 +27224,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -24399,8 +27232,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -24411,7 +27243,6 @@ }, "node_modules/default-gateway": { "version": "6.0.3", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "execa": "^5.0.0" @@ -24438,7 +27269,6 @@ }, "node_modules/define-lazy-prop": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24446,7 +27276,6 @@ }, "node_modules/define-properties": { "version": "1.2.0", - "dev": true, "license": "MIT", "dependencies": { "has-property-descriptors": "^1.0.0", @@ -24459,6 +27288,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/del": { "version": "6.1.1", "dev": true, @@ -24602,7 +27445,8 @@ }, "node_modules/detect-newline": { "version": "3.1.0", - "dev": true, + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "license": "MIT", "engines": { "node": ">=8" @@ -24610,7 +27454,6 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "dev": true, "license": "MIT" }, "node_modules/detect-node-es": { @@ -24662,17 +27505,24 @@ "dev": true, "license": "MIT" }, + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "license": "BSD-3-Clause" + }, "node_modules/diff": { "version": "4.0.2", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -24686,7 +27536,6 @@ }, "node_modules/dir-glob": { "version": "3.0.1", - "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -24697,12 +27546,10 @@ }, "node_modules/dns-equal": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/dns-packet": { "version": "5.6.0", - "dev": true, "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" @@ -24713,7 +27560,6 @@ }, "node_modules/doctrine": { "version": "3.0.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -24781,7 +27627,6 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -24794,7 +27639,6 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "dev": true, "funding": [ { "type": "github", @@ -24805,7 +27649,6 @@ }, "node_modules/domexception": { "version": "4.0.0", - "dev": true, "license": "MIT", "dependencies": { "webidl-conversions": "^7.0.0" @@ -24816,7 +27659,6 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -24830,7 +27672,6 @@ }, "node_modules/domutils": { "version": "3.1.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -24853,7 +27694,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, "dependencies": { "is-obj": "^2.0.0" }, @@ -24916,7 +27756,6 @@ }, "node_modules/duplexer": { "version": "0.1.2", - "dev": true, "license": "MIT" }, "node_modules/duplexer2": { @@ -25001,10 +27840,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.594", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", - "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==", - "dev": true + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", + "license": "ISC" }, "node_modules/email-addresses": { "version": "5.0.0", @@ -25013,7 +27852,8 @@ }, "node_modules/emittery": { "version": "0.13.1", - "dev": true, + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "license": "MIT", "engines": { "node": ">=12" @@ -25024,12 +27864,10 @@ }, "node_modules/emoji-regex": { "version": "9.2.2", - "dev": true, "license": "MIT" }, "node_modules/emojis-list": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -25060,15 +27898,15 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { - "version": "5.14.0", - "dev": true, + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -25080,7 +27918,6 @@ }, "node_modules/enquirer": { "version": "2.3.6", - "dev": true, "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1" @@ -25091,7 +27928,6 @@ }, "node_modules/entities": { "version": "4.5.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -25104,7 +27940,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "engines": { "node": ">=6" } @@ -25113,7 +27948,6 @@ "version": "7.13.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", - "dev": true, "bin": { "envinfo": "dist/cli.js" }, @@ -25151,9 +27985,17 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.21.2", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", @@ -25200,7 +28042,6 @@ }, "node_modules/es-get-iterator": { "version": "1.1.3", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -25219,12 +28060,10 @@ }, "node_modules/es-module-lexer": { "version": "1.2.1", - "dev": true, "license": "MIT" }, "node_modules/es-set-tostringtag": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3", @@ -25237,7 +28076,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "has": "^1.0.3" @@ -25245,7 +28083,6 @@ }, "node_modules/es-to-primitive": { "version": "1.2.1", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.4", @@ -25436,7 +28273,9 @@ "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -25468,7 +28307,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -25489,7 +28327,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "optional": true, "engines": { "node": ">=0.10.0" @@ -25499,7 +28336,6 @@ "version": "8.46.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -25562,7 +28398,6 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.7", - "dev": true, "license": "MIT", "dependencies": { "debug": "^3.2.7", @@ -25572,7 +28407,6 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -25580,7 +28414,6 @@ }, "node_modules/eslint-module-utils": { "version": "2.8.0", - "dev": true, "license": "MIT", "dependencies": { "debug": "^3.2.7" @@ -25596,7 +28429,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -25787,7 +28619,6 @@ }, "node_modules/eslint-plugin-import": { "version": "2.27.5", - "dev": true, "license": "MIT", "dependencies": { "array-includes": "^3.1.6", @@ -25815,7 +28646,6 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -25823,7 +28653,6 @@ }, "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "2.1.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -25834,7 +28663,6 @@ }, "node_modules/eslint-plugin-import/node_modules/json5": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.0" @@ -25845,7 +28673,6 @@ }, "node_modules/eslint-plugin-import/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -25856,7 +28683,6 @@ }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -25864,7 +28690,6 @@ }, "node_modules/eslint-plugin-import/node_modules/strip-bom": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -25872,7 +28697,6 @@ }, "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { "version": "3.14.2", - "dev": true, "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", @@ -25881,9 +28705,33 @@ "strip-bom": "^3.0.0" } }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.7.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.7", @@ -25912,7 +28760,6 @@ }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -25923,7 +28770,6 @@ }, "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -25933,9 +28779,23 @@ "resolved": "packages/meta/src/eslint-plugin-playground-dev", "link": true }, + "node_modules/eslint-plugin-playwright": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.15.3.tgz", + "integrity": "sha512-LQMW5y0DLK5Fnpya7JR1oAYL2/7Y9wDiYw6VZqlKqcRGSgjbVKNqxraphk7ra1U3Bb5EK444xMgUlQPbMg2M1g==", + "license": "MIT", + "peerDependencies": { + "eslint": ">=7", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.32.2", - "dev": true, "license": "MIT", "dependencies": { "array-includes": "^3.1.6", @@ -25963,7 +28823,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "4.6.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -25974,7 +28833,6 @@ }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -25985,7 +28843,6 @@ }, "node_modules/eslint-plugin-react/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -25996,7 +28853,6 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.4", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.9.0", @@ -26012,7 +28868,6 @@ }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -26020,7 +28875,6 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -26032,7 +28886,6 @@ }, "node_modules/eslint-scope/node_modules/estraverse": { "version": "4.3.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -26042,7 +28895,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -26054,7 +28906,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26068,7 +28919,6 @@ }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -26082,12 +28932,10 @@ }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", - "dev": true, "license": "Python-2.0" }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -26102,7 +28950,6 @@ }, "node_modules/eslint/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -26113,14 +28960,12 @@ }, "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -26136,7 +28981,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -26150,7 +28994,6 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -26161,7 +29004,6 @@ }, "node_modules/eslint/node_modules/globals": { "version": "13.20.0", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.20.2" @@ -26175,7 +29017,6 @@ }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -26187,14 +29028,12 @@ "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -26207,7 +29046,6 @@ }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -26220,7 +29058,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -26233,7 +29070,6 @@ }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -26246,7 +29082,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -26261,7 +29096,6 @@ }, "node_modules/esprima": { "version": "4.0.1", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -26273,7 +29107,6 @@ }, "node_modules/esquery": { "version": "1.5.0", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -26284,7 +29117,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -26295,7 +29127,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -26308,7 +29139,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -26351,12 +29181,10 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", - "dev": true, "license": "MIT" }, "node_modules/events": { "version": "3.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -26364,7 +29192,6 @@ }, "node_modules/execa": { "version": "5.1.1", - "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -26411,7 +29238,6 @@ }, "node_modules/exit": { "version": "0.1.2", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -26424,21 +29250,40 @@ "node": ">=6" } }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-puppeteer": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz", + "integrity": "sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA==", + "license": "MIT" + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -26576,7 +29421,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -26596,7 +29440,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, "dependencies": { "pump": "^3.0.0" }, @@ -26620,9 +29463,20 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.2.7", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -26637,14 +29491,12 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-url-parser": { "version": "1.1.3", @@ -26659,6 +29511,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.15.0", "license": "ISC", @@ -26668,7 +29529,6 @@ }, "node_modules/faye-websocket": { "version": "0.11.4", - "dev": true, "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" @@ -26679,7 +29539,6 @@ }, "node_modules/fb-watchman": { "version": "2.0.2", - "dev": true, "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" @@ -26734,7 +29593,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, "dependencies": { "pend": "~1.2.0" } @@ -26780,7 +29638,6 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" @@ -26791,7 +29648,7 @@ }, "node_modules/file-loader": { "version": "6.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", @@ -26812,7 +29669,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26828,7 +29685,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, + "devOptional": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -26837,11 +29694,11 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "devOptional": true }, "node_modules/file-loader/node_modules/schema-utils": { "version": "3.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", @@ -26898,7 +29755,6 @@ }, "node_modules/filename-reserved-regex": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -26906,7 +29762,6 @@ }, "node_modules/filenamify": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "filename-reserved-regex": "^2.0.0", @@ -26929,7 +29784,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -26981,6 +29838,109 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-file-up": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", + "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-parent-dir": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", + "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", + "license": "MIT" + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", + "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", + "license": "MIT", + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-process": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz", + "integrity": "sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "commander": "^5.1.0", + "debug": "^4.1.1" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/find-process/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/find-process/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/find-process/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/find-process/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -26989,7 +29949,6 @@ }, "node_modules/find-up": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -27009,7 +29968,6 @@ }, "node_modules/flat-cache": { "version": "3.0.4", - "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.1.0", @@ -27021,7 +29979,6 @@ }, "node_modules/flat-cache/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -27040,7 +29997,6 @@ }, "node_modules/flat-cache/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -27051,7 +30007,6 @@ }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -27064,8 +30019,9 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "dev": true, + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "license": "ISC" }, "node_modules/flux": { @@ -27081,7 +30037,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -27100,12 +30058,32 @@ }, "node_modules/for-each": { "version": "0.3.3", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.3" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -27288,7 +30266,6 @@ }, "node_modules/fraction.js": { "version": "4.2.0", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -27335,6 +30312,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fs-extra": { "version": "11.1.1", "license": "MIT", @@ -27370,7 +30356,6 @@ }, "node_modules/fs-monkey": { "version": "1.0.3", - "dev": true, "license": "Unlicense" }, "node_modules/fs.realpath": { @@ -27455,7 +30440,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.5", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -27472,7 +30456,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -27516,7 +30499,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -27575,7 +30557,6 @@ }, "node_modules/get-package-type": { "version": "0.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -27706,7 +30687,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true, "engines": { "node": ">=8" }, @@ -27714,9 +30694,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -27727,7 +30718,6 @@ }, "node_modules/get-symbol-description": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -27740,6 +30730,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -28026,7 +31030,8 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "dev": true, + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { @@ -28075,7 +31080,6 @@ }, "node_modules/global-modules": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "global-prefix": "^3.0.0" @@ -28086,7 +31090,6 @@ }, "node_modules/global-prefix": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "ini": "^1.3.5", @@ -28100,12 +31103,10 @@ "node_modules/global-prefix/node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/global-prefix/node_modules/which": { "version": "1.3.1", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -28116,7 +31117,6 @@ }, "node_modules/globals": { "version": "11.12.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -28124,7 +31124,6 @@ }, "node_modules/globalthis": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.1.3" @@ -28138,7 +31137,6 @@ }, "node_modules/globby": { "version": "11.1.0", - "dev": true, "license": "MIT", "dependencies": { "array-union": "^2.1.0", @@ -28157,7 +31155,6 @@ }, "node_modules/globby/node_modules/fast-glob": { "version": "3.2.12", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -28170,6 +31167,12 @@ "node": ">=8.6.0" } }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "license": "MIT" + }, "node_modules/globrex": { "version": "0.1.2", "dev": true, @@ -28186,7 +31189,6 @@ }, "node_modules/gopd": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" @@ -28261,8 +31263,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/gray-matter": { "version": "4.0.3", @@ -28280,7 +31281,6 @@ }, "node_modules/gzip-size": { "version": "6.0.0", - "dev": true, "license": "MIT", "dependencies": { "duplexer": "^0.1.2" @@ -28294,7 +31294,6 @@ }, "node_modules/handle-thing": { "version": "2.0.1", - "dev": true, "license": "MIT" }, "node_modules/handlebars": { @@ -28331,7 +31330,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, "engines": { "node": ">=6" } @@ -28353,7 +31351,6 @@ }, "node_modules/has-bigints": { "version": "1.0.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -28361,7 +31358,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28369,7 +31365,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.1" @@ -28400,7 +31395,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -28591,6 +31585,18 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -28614,7 +31620,6 @@ }, "node_modules/hpack.js": { "version": "2.1.6", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -28625,12 +31630,10 @@ }, "node_modules/hpack.js/node_modules/isarray": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/hpack.js/node_modules/readable-stream": { "version": "2.3.8", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -28644,12 +31647,10 @@ }, "node_modules/hpack.js/node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, "license": "MIT" }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -28662,7 +31663,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^2.0.0" @@ -28673,12 +31673,10 @@ }, "node_modules/html-entities": { "version": "2.3.3", - "dev": true, "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", - "dev": true, "license": "MIT" }, "node_modules/html-minifier-terser": { @@ -28711,7 +31709,6 @@ }, "node_modules/html-tags": { "version": "3.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28776,7 +31773,6 @@ }, "node_modules/http-deceiver": { "version": "1.2.7", - "dev": true, "license": "MIT" }, "node_modules/http-errors": { @@ -28793,14 +31789,21 @@ "node": ">= 0.8" } }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/http-parser-js": { "version": "0.5.8", - "dev": true, "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", - "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", @@ -28813,7 +31816,6 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "@tootallnate/once": "2", @@ -28826,7 +31828,6 @@ }, "node_modules/http-proxy-middleware": { "version": "2.0.6", - "dev": true, "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", @@ -28849,7 +31850,6 @@ }, "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -28962,7 +31962,6 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -28974,7 +31973,6 @@ }, "node_modules/human-signals": { "version": "2.1.0", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -29020,7 +32018,6 @@ }, "node_modules/icss-utils": { "version": "5.1.0", - "dev": true, "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" @@ -29042,7 +32039,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -29110,6 +32106,18 @@ "node": ">=0.10.0" } }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "license": "MIT" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "9.0.21", "dev": true, @@ -29121,7 +32129,6 @@ }, "node_modules/immutable": { "version": "4.3.0", - "devOptional": true, "license": "MIT" }, "node_modules/import-cwd": { @@ -29178,7 +32185,6 @@ }, "node_modules/import-local": { "version": "3.1.0", - "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -29203,7 +32209,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -29516,7 +32521,6 @@ }, "node_modules/internal-slot": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.0", @@ -29535,6 +32539,18 @@ "node": ">= 0.10" } }, + "node_modules/intl-messageformat": { + "version": "10.7.10", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.10.tgz", + "integrity": "sha512-hp7iejCBiJdW3zmOe18FdlJu8U/JsADSDiBPQhfdSeI8B9POtvPRvPh3nMlvhYayGMKLv6maldhR7y3Pf1vkpw==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "@formatjs/fast-memoize": "2.2.5", + "@formatjs/icu-messageformat-parser": "2.9.7", + "tslib": "2" + } + }, "node_modules/invariant": { "version": "2.2.4", "dev": true, @@ -29547,7 +32563,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -29559,17 +32574,24 @@ "node_modules/ip-address/node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, "node_modules/ipaddr.js": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" } }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-alphabetical": { "version": "1.0.4", "dev": true, @@ -29594,7 +32616,6 @@ }, "node_modules/is-arguments": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -29609,7 +32630,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -29626,7 +32646,6 @@ }, "node_modules/is-bigint": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" @@ -29637,7 +32656,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "devOptional": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -29648,7 +32666,6 @@ }, "node_modules/is-boolean-object": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -29685,7 +32702,6 @@ }, "node_modules/is-builtin-module": { "version": "3.2.1", - "dev": true, "license": "MIT", "dependencies": { "builtin-modules": "^3.3.0" @@ -29699,7 +32715,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -29736,7 +32751,6 @@ }, "node_modules/is-date-object": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -29759,7 +32773,6 @@ }, "node_modules/is-docker": { "version": "2.2.1", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -29773,7 +32786,6 @@ }, "node_modules/is-extendable": { "version": "0.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29795,7 +32807,8 @@ }, "node_modules/is-generator-fn": { "version": "2.1.0", - "dev": true, + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "license": "MIT", "engines": { "node": ">=6" @@ -29854,7 +32867,6 @@ }, "node_modules/is-map": { "version": "2.0.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -29867,7 +32879,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -29889,6 +32900,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -29896,7 +32909,6 @@ }, "node_modules/is-number-object": { "version": "1.0.7", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -29910,7 +32922,6 @@ }, "node_modules/is-obj": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -29918,15 +32929,37 @@ }, "node_modules/is-path-cwd": { "version": "2.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "license": "MIT", + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd/node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "license": "MIT", + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -29936,7 +32969,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -29950,7 +32982,6 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/is-promise": { @@ -29969,7 +33000,6 @@ }, "node_modules/is-regex": { "version": "1.1.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -30000,7 +33030,6 @@ }, "node_modules/is-set": { "version": "2.0.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -30008,7 +33037,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2" @@ -30028,7 +33056,6 @@ }, "node_modules/is-stream": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -30039,7 +33066,6 @@ }, "node_modules/is-string": { "version": "1.0.7", - "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -30053,7 +33079,6 @@ }, "node_modules/is-symbol": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -30079,7 +33104,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.10", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", @@ -30097,7 +33121,6 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/is-unicode-supported": { @@ -30113,7 +33136,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.1", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -30121,7 +33143,6 @@ }, "node_modules/is-weakref": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2" @@ -30132,7 +33153,6 @@ }, "node_modules/is-weakset": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -30156,6 +33176,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-word-character": { "version": "1.0.4", "dev": true, @@ -30167,7 +33196,6 @@ }, "node_modules/is-wsl": { "version": "2.2.0", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -30183,7 +33211,6 @@ }, "node_modules/isarray": { "version": "2.0.5", - "dev": true, "license": "MIT" }, "node_modules/isbot": { @@ -30196,12 +33223,10 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -30226,7 +33251,6 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -30234,7 +33258,6 @@ }, "node_modules/istanbul-lib-instrument": { "version": "5.2.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -30249,7 +33272,6 @@ }, "node_modules/istanbul-lib-instrument/node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -30257,7 +33279,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.0", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -30270,7 +33291,6 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -30283,7 +33303,6 @@ }, "node_modules/istanbul-lib-source-maps/node_modules/source-map": { "version": "0.6.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -30291,7 +33310,6 @@ }, "node_modules/istanbul-reports": { "version": "3.1.5", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -30393,14 +33411,15 @@ } }, "node_modules/jest": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "license": "MIT", "dependencies": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.5.0" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" @@ -30418,11 +33437,13 @@ } }, "node_modules/jest-changed-files": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "license": "MIT", "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { @@ -30430,27 +33451,28 @@ } }, "node_modules/jest-circus": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "license": "MIT", "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -30459,9 +33481,27 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-circus/node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/jest-circus/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -30476,7 +33516,8 @@ }, "node_modules/jest-circus/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -30490,7 +33531,8 @@ }, "node_modules/jest-circus/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -30501,15 +33543,31 @@ }, "node_modules/jest-circus/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/jest-circus/node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/jest-circus/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -30518,26 +33576,27 @@ } }, "node_modules/jest-circus/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-cli": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "license": "MIT", "dependencies": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { @@ -30557,7 +33616,8 @@ }, "node_modules/jest-cli/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -30571,7 +33631,8 @@ }, "node_modules/jest-cli/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -30586,7 +33647,8 @@ }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -30597,34 +33659,36 @@ }, "node_modules/jest-cli/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-config": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -30646,7 +33710,6 @@ }, "node_modules/jest-config/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -30661,7 +33724,6 @@ }, "node_modules/jest-config/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -30675,7 +33737,6 @@ }, "node_modules/jest-config/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -30686,12 +33747,10 @@ }, "node_modules/jest-config/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -30710,7 +33769,6 @@ }, "node_modules/jest-config/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -30720,11 +33778,12 @@ } }, "node_modules/jest-config/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -30733,19 +33792,118 @@ } }, "node_modules/jest-config/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/jest-dev-server": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-10.1.4.tgz", + "integrity": "sha512-bGQ6sedNGtT6AFHhCVqGTXMPz7UyJi/ZrhNBgyqsP0XU9N8acCEIfqZEA22rOaZ+NdEVsaltk6tL7UT6aXfI7w==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.7", + "prompts": "^2.4.2", + "spawnd": "^10.1.4", + "tree-kill": "^1.2.2", + "wait-on": "^8.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jest-dev-server/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-dev-server/node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/jest-dev-server/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-dev-server/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-dev-server/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/jest-dev-server/node_modules/wait-on": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz", + "integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.7", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jest-diff": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -30753,7 +33911,6 @@ }, "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -30768,7 +33925,6 @@ }, "node_modules/jest-diff/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -30782,7 +33938,6 @@ }, "node_modules/jest-diff/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -30793,15 +33948,15 @@ }, "node_modules/jest-diff/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -30810,13 +33965,15 @@ } }, "node_modules/jest-diff/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-docblock": { - "version": "29.4.3", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -30826,15 +33983,16 @@ } }, "node_modules/jest-each": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -30842,7 +34000,8 @@ }, "node_modules/jest-each/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -30857,7 +34016,8 @@ }, "node_modules/jest-each/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -30871,7 +34031,8 @@ }, "node_modules/jest-each/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -30882,15 +34043,17 @@ }, "node_modules/jest-each/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-each/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -30899,22 +34062,24 @@ } }, "node_modules/jest-each/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-environment-jsdom": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "license": "MIT", "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/jsdom": "^20.0.0", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", "jsdom": "^20.0.0" }, "engines": { @@ -30933,7 +34098,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, "dependencies": { "cssom": "~0.3.6" }, @@ -30944,14 +34108,12 @@ "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" }, "node_modules/jest-environment-jsdom/node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "dev": true, "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", @@ -30965,7 +34127,6 @@ "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "dev": true, "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", @@ -31010,7 +34171,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, "dependencies": { "entities": "^4.4.0" }, @@ -31022,7 +34182,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, "dependencies": { "punycode": "^2.1.1" }, @@ -31034,7 +34193,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" @@ -31044,43 +34202,46 @@ } }, "node_modules/jest-environment-node": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "license": "MIT", "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -31092,23 +34253,25 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "license": "MIT", "dependencies": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -31117,19 +34280,21 @@ } }, "node_modules/jest-leak-detector/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -31137,7 +34302,6 @@ }, "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31152,7 +34316,6 @@ }, "node_modules/jest-matcher-utils/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31166,7 +34329,6 @@ }, "node_modules/jest-matcher-utils/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31177,15 +34339,15 @@ }, "node_modules/jest-matcher-utils/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -31194,22 +34356,24 @@ } }, "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-message-util": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -31219,7 +34383,6 @@ }, "node_modules/jest-message-util/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31234,7 +34397,6 @@ }, "node_modules/jest-message-util/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31248,7 +34410,6 @@ }, "node_modules/jest-message-util/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31259,15 +34420,15 @@ }, "node_modules/jest-message-util/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -31276,18 +34437,20 @@ } }, "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-mock": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.5.0" + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -31295,7 +34458,6 @@ }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -31310,24 +34472,26 @@ } }, "node_modules/jest-regex-util": { - "version": "29.4.3", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -31337,12 +34501,13 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "license": "MIT", "dependencies": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -31350,7 +34515,6 @@ }, "node_modules/jest-resolve/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31364,7 +34528,6 @@ }, "node_modules/jest-resolve/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31379,7 +34542,6 @@ }, "node_modules/jest-resolve/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31390,41 +34552,40 @@ }, "node_modules/jest-resolve/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/jest-resolve/node_modules/resolve.exports": { "version": "2.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/jest-runner": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "license": "MIT", "dependencies": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -31434,7 +34595,8 @@ }, "node_modules/jest-runner/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31448,7 +34610,8 @@ }, "node_modules/jest-runner/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31463,7 +34626,8 @@ }, "node_modules/jest-runner/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31474,12 +34638,14 @@ }, "node_modules/jest-runner/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-runner/node_modules/source-map": { "version": "0.6.1", - "dev": true, + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -31487,7 +34653,8 @@ }, "node_modules/jest-runner/node_modules/source-map-support": { "version": "0.5.13", - "dev": true, + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -31495,30 +34662,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "license": "MIT", "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -31528,7 +34696,8 @@ }, "node_modules/jest-runtime/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31542,7 +34711,8 @@ }, "node_modules/jest-runtime/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31557,7 +34727,8 @@ }, "node_modules/jest-runtime/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31568,12 +34739,15 @@ }, "node_modules/jest-runtime/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-runtime/node_modules/glob": { "version": "7.2.3", - "dev": true, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -31592,7 +34766,8 @@ }, "node_modules/jest-runtime/node_modules/minimatch": { "version": "3.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -31602,33 +34777,31 @@ } }, "node_modules/jest-snapshot": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.5.0", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -31636,7 +34809,8 @@ }, "node_modules/jest-snapshot/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31651,7 +34825,8 @@ }, "node_modules/jest-snapshot/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31665,7 +34840,8 @@ }, "node_modules/jest-snapshot/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31676,15 +34852,17 @@ }, "node_modules/jest-snapshot/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -31693,16 +34871,18 @@ } }, "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-util": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -31715,7 +34895,6 @@ }, "node_modules/jest-util/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31729,7 +34908,6 @@ }, "node_modules/jest-util/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31744,7 +34922,6 @@ }, "node_modules/jest-util/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31755,20 +34932,20 @@ }, "node_modules/jest-util/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/jest-validate": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "license": "MIT", "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.5.0" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -31776,7 +34953,8 @@ }, "node_modules/jest-validate/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31791,7 +34969,8 @@ }, "node_modules/jest-validate/node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31805,7 +34984,8 @@ }, "node_modules/jest-validate/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31816,15 +34996,17 @@ }, "node_modules/jest-validate/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-validate/node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -31833,22 +35015,24 @@ } }, "node_modules/jest-validate/node_modules/react-is": { - "version": "18.2.0", - "dev": true, + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/jest-watcher": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "license": "MIT", "dependencies": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { @@ -31857,7 +35041,8 @@ }, "node_modules/jest-watcher/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -31871,7 +35056,8 @@ }, "node_modules/jest-watcher/node_modules/chalk": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -31886,7 +35072,8 @@ }, "node_modules/jest-watcher/node_modules/color-convert": { "version": "2.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -31897,16 +35084,18 @@ }, "node_modules/jest-watcher/node_modules/color-name": { "version": "1.1.4", - "dev": true, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/jest-worker": { - "version": "29.5.0", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "license": "MIT", "dependencies": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -31916,7 +35105,6 @@ }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -31943,24 +35131,39 @@ "dev": true }, "node_modules/joi": { - "version": "17.9.2", - "dev": true, + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -31976,6 +35179,15 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsdom": { "version": "22.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", @@ -32030,14 +35242,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -32073,7 +35286,6 @@ }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/json-stringify-nice": { @@ -32091,9 +35303,14 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, + "node_modules/json2php": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.9.tgz", + "integrity": "sha512-fQMYwvPsQt8hxRnCGyg1r2JVi6yL8Um0DIIawiKiMK9yhAAkcRNj5UsBWoaFvFzPpcWbgw9L6wzj+UMYA702Mw==", + "license": "BSD" + }, "node_modules/json5": { "version": "2.2.3", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -32121,7 +35338,6 @@ }, "node_modules/jsonc-parser": { "version": "3.2.0", - "dev": true, "license": "MIT" }, "node_modules/jsonfile": { @@ -32197,7 +35413,6 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.3", - "dev": true, "license": "MIT", "dependencies": { "array-includes": "^3.1.5", @@ -32248,7 +35463,6 @@ }, "node_modules/kind-of": { "version": "6.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -32256,7 +35470,6 @@ }, "node_modules/kleur": { "version": "3.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -32264,12 +35477,17 @@ }, "node_modules/klona": { "version": "2.0.6", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "license": "MIT" + }, "node_modules/kolorist": { "version": "1.8.0", "dev": true, @@ -32277,12 +35495,10 @@ }, "node_modules/language-subtag-registry": { "version": "0.3.22", - "dev": true, "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "language-subtag-registry": "~0.3.2" @@ -32301,7 +35517,6 @@ }, "node_modules/launch-editor": { "version": "2.6.0", - "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -32317,6 +35532,15 @@ "node": "> 0.8" } }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lerna": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/lerna/-/lerna-6.6.2.tgz", @@ -32728,7 +35952,6 @@ }, "node_modules/leven": { "version": "3.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -32738,7 +35961,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -32969,6 +36191,99 @@ } } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lighthouse": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.3.0.tgz", + "integrity": "sha512-OaLE8DasnwQkn2CBo2lKtD+IQv42mNP3T+Vaw29I++rAh0Zpgc6SM15usdIYyzhRMR5EWFxze5Fyb+HENJSh2A==", + "license": "Apache-2.0", + "dependencies": { + "@paulirish/trace_engine": "0.0.39", + "@sentry/node": "^7.0.0", + "axe-core": "^4.10.2", + "chrome-launcher": "^1.1.2", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.1", + "devtools-protocol": "0.0.1312386", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^23.10.4", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.26.1", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.16" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", + "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "license": "Apache-2.0" + }, + "node_modules/lighthouse/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "dev": true, @@ -32997,6 +36312,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/listenercount": { "version": "1.0.1", "license": "ISC" @@ -33054,7 +36378,6 @@ }, "node_modules/loader-runner": { "version": "4.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" @@ -33062,7 +36385,6 @@ }, "node_modules/loader-utils": { "version": "2.0.4", - "dev": true, "license": "MIT", "dependencies": { "big.js": "^5.2.2", @@ -33084,9 +36406,17 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -33097,7 +36427,12 @@ }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -33114,7 +36449,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "license": "MIT" }, "node_modules/lodash.flow": { "version": "3.5.0", @@ -33171,12 +36506,10 @@ }, "node_modules/lodash.memoize": { "version": "4.1.2", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "dev": true, "license": "MIT" }, "node_modules/lodash.once": { @@ -33184,9 +36517,14 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "license": "MIT" + }, "node_modules/lodash.uniq": { "version": "4.5.0", - "dev": true, "license": "MIT" }, "node_modules/log-symbols": { @@ -33286,6 +36624,12 @@ "node": ">=8" } }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -33324,7 +36668,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -33352,7 +36695,6 @@ }, "node_modules/make-dir": { "version": "3.1.0", - "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -33366,7 +36708,6 @@ }, "node_modules/make-dir/node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -33374,7 +36715,7 @@ }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/make-fetch-happen": { @@ -33611,7 +36952,6 @@ }, "node_modules/makeerror": { "version": "1.0.12", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" @@ -33621,7 +36961,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, "engines": { "node": ">=8" }, @@ -33629,6 +36968,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/map-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", + "integrity": "sha512-BbShUnr5OartXJe1GeccAWtfro11hhgNJg6G9/UtWKjVGvV5U4C09cg5nk8JUevhXODaXY+hQ3xxMUKSs62ONQ==", + "license": "Public Domain" + }, "node_modules/markdown-escapes": { "version": "1.0.4", "dev": true, @@ -33638,6 +36983,145 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdownlint": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", + "integrity": "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==", + "license": "MIT", + "dependencies": { + "markdown-it": "12.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.31.1.tgz", + "integrity": "sha512-keIOMwQn+Ch7MoBwA+TdkyVMuxAeZFEGmIIlvwgV0Z1TGS5MxPnRr29XCLhkNzCHU+uNKGjU+VEjLX+Z9kli6g==", + "license": "MIT", + "dependencies": { + "commander": "~9.0.0", + "get-stdin": "~9.0.0", + "glob": "~7.2.0", + "ignore": "~5.2.0", + "js-yaml": "^4.1.0", + "jsonc-parser": "~3.0.0", + "markdownlint": "~0.25.1", + "markdownlint-rule-helpers": "~0.16.0", + "minimatch": "~3.0.5", + "run-con": "~1.2.10" + }, + "bin": { + "markdownlint": "markdownlint.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/markdownlint-cli/node_modules/commander": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz", + "integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/markdownlint-cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "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/markdownlint-cli/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/markdownlint-cli/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/markdownlint-cli/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "license": "MIT" + }, + "node_modules/markdownlint-rule-helpers": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz", + "integrity": "sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w==", + "license": "MIT" + }, "node_modules/marked": { "version": "4.3.0", "dev": true, @@ -33649,6 +37133,22 @@ "node": ">= 12" } }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "license": "Apache-2.0" + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdast-squeeze-paragraphs": { "version": "4.0.0", "dev": true, @@ -33755,7 +37255,6 @@ }, "node_modules/mdurl": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/media-typer": { @@ -33768,7 +37267,6 @@ }, "node_modules/memfs": { "version": "3.5.1", - "dev": true, "license": "Unlicense", "dependencies": { "fs-monkey": "^1.0.3" @@ -33951,13 +37449,108 @@ "node": ">=10" } }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/merge-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -33967,6 +37560,12 @@ "node": ">= 8" } }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "license": "MIT", @@ -34397,10 +37996,12 @@ "license": "MIT" }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -34409,7 +38010,6 @@ }, "node_modules/mime": { "version": "3.0.0", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -34437,7 +38037,6 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -34457,7 +38056,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, "engines": { "node": ">=4" } @@ -34490,12 +38088,10 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", - "dev": true, "license": "ISC" }, "node_modules/minimatch": { "version": "3.0.5", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -34515,7 +38111,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", @@ -34761,6 +38356,28 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "devOptional": true, @@ -34849,7 +38466,6 @@ }, "node_modules/mrmime": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -34867,7 +38483,6 @@ }, "node_modules/multicast-dns": { "version": "7.2.5", - "dev": true, "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", @@ -34936,12 +38551,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "dev": true, "license": "MIT" }, "node_modules/natural-compare-lite": { "version": "1.4.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/needle": { @@ -34977,9 +38591,17 @@ }, "node_modules/neo-async": { "version": "2.6.2", - "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "license": "MIT", @@ -35063,7 +38685,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -35176,7 +38797,6 @@ }, "node_modules/node-int64": { "version": "0.4.0", - "dev": true, "license": "MIT" }, "node_modules/node-machine-id": { @@ -35186,10 +38806,10 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" }, "node_modules/nopt": { "version": "7.2.1", @@ -35244,7 +38864,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -35252,7 +38871,6 @@ }, "node_modules/normalize-range": { "version": "0.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -35368,6 +38986,374 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/npm-package-json-lint": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-6.4.0.tgz", + "integrity": "sha512-cuXAJJB1Rdqz0UO6w524matlBqDBjcNt7Ru+RDIu4y6RI1gVqiWBnylrK8sPRk81gGBA0X8hJbDXolVOoTc+sA==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "chalk": "^4.1.2", + "cosmiconfig": "^8.0.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "ignore": "^5.2.0", + "is-plain-obj": "^3.0.0", + "jsonc-parser": "^3.2.0", + "log-symbols": "^4.1.0", + "meow": "^9.0.0", + "plur": "^4.0.0", + "semver": "^7.3.8", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1", + "type-fest": "^3.2.0", + "validate-npm-package-name": "^5.0.0" + }, + "bin": { + "npmPkgJsonLint": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/npm-package-json-lint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/npm-package-json-lint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-package-json-lint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/npm-package-json-lint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm-package-json-lint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm-package-json-lint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/npm-package-json-lint/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/npm-package-json-lint/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-package-json-lint/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/npm-package-json-lint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/npm-package-json-lint/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-package-json-lint/node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/npm-package-json-lint/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/npm-packlist": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz", @@ -35631,7 +39617,6 @@ }, "node_modules/npm-run-path": { "version": "4.0.1", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -35663,7 +39648,6 @@ }, "node_modules/nth-check": { "version": "2.1.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -35674,7 +39658,6 @@ }, "node_modules/nwsapi": { "version": "2.2.4", - "dev": true, "license": "MIT" }, "node_modules/nx": { @@ -35873,6 +39856,12 @@ "node": ">=0.10.0" } }, + "node_modules/object-filter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", + "integrity": "sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==", + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.12.3", "license": "MIT", @@ -35882,7 +39871,6 @@ }, "node_modules/object-is": { "version": "1.1.5", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35897,7 +39885,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35913,7 +39900,6 @@ }, "node_modules/object.assign": { "version": "4.1.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35930,7 +39916,6 @@ }, "node_modules/object.entries": { "version": "1.1.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35943,7 +39928,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35959,7 +39943,6 @@ }, "node_modules/object.hasown": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.1.4", @@ -35971,7 +39954,6 @@ }, "node_modules/object.values": { "version": "1.1.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35987,7 +39969,6 @@ }, "node_modules/obuf": { "version": "1.1.2", - "dev": true, "license": "MIT" }, "node_modules/octokit": { @@ -36205,7 +40186,6 @@ }, "node_modules/on-headers": { "version": "1.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -36220,7 +40200,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -36234,7 +40213,6 @@ }, "node_modules/open": { "version": "8.4.2", - "dev": true, "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", @@ -36250,7 +40228,6 @@ }, "node_modules/opener": { "version": "1.5.2", - "dev": true, "license": "(WTFPL OR MIT)", "bin": { "opener": "bin/opener-bin.js" @@ -36260,7 +40237,6 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -36351,7 +40327,6 @@ }, "node_modules/os-homedir": { "version": "1.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -36390,7 +40365,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -36404,7 +40378,6 @@ }, "node_modules/p-locate": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -36415,7 +40388,6 @@ }, "node_modules/p-locate/node_modules/p-limit": { "version": "2.3.0", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -36487,7 +40459,6 @@ }, "node_modules/p-retry": { "version": "4.6.2", - "dev": true, "license": "MIT", "dependencies": { "@types/retry": "0.12.0", @@ -36499,7 +40470,6 @@ }, "node_modules/p-retry/node_modules/retry": { "version": "0.13.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -36518,7 +40488,6 @@ }, "node_modules/p-try": { "version": "2.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -36539,6 +40508,87 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json": { "version": "6.5.0", "dev": true, @@ -36835,6 +40885,11 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parse-conflict-json": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", @@ -36903,6 +40958,15 @@ "dev": true, "license": "ISC" }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-path": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", @@ -36980,7 +41044,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -36995,12 +41058,10 @@ }, "node_modules/path-is-inside": { "version": "1.0.2", - "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -37075,8 +41136,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/performance-now": { "version": "2.1.0", @@ -37085,9 +41145,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -37113,7 +41173,6 @@ }, "node_modules/pinkie": { "version": "2.0.4", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -37121,7 +41180,6 @@ }, "node_modules/pinkie-promise": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "pinkie": "^2.0.0" @@ -37132,7 +41190,6 @@ }, "node_modules/pirates": { "version": "4.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -37140,7 +41197,6 @@ }, "node_modules/pkg-dir": { "version": "4.2.0", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -37228,13 +41284,12 @@ } }, "node_modules/playwright": { - "version": "1.47.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz", - "integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==", - "dev": true, + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", + "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.47.1" + "playwright-core": "1.48.1" }, "bin": { "playwright": "cli.js" @@ -37247,10 +41302,9 @@ } }, "node_modules/playwright-core": { - "version": "1.47.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz", - "integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==", - "dev": true, + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", + "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -37259,6 +41313,21 @@ "node": ">=18" } }, + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "license": "MIT", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -37304,9 +41373,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -37324,8 +41393,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -37479,7 +41548,6 @@ }, "node_modules/postcss-loader": { "version": "6.2.1", - "dev": true, "license": "MIT", "dependencies": { "cosmiconfig": "^7.0.0", @@ -37498,6 +41566,12 @@ "webpack": "^5.0.0" } }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "license": "MIT" + }, "node_modules/postcss-merge-idents": { "version": "5.1.1", "dev": true, @@ -37625,7 +41699,6 @@ }, "node_modules/postcss-modules-extract-imports": { "version": "3.0.0", - "dev": true, "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" @@ -37636,7 +41709,6 @@ }, "node_modules/postcss-modules-local-by-default": { "version": "4.0.0", - "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", @@ -37652,7 +41724,6 @@ }, "node_modules/postcss-modules-scope": { "version": "3.0.0", - "dev": true, "license": "ISC", "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -37666,7 +41737,6 @@ }, "node_modules/postcss-modules-values": { "version": "4.0.0", - "dev": true, "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" @@ -37871,9 +41941,68 @@ "postcss": "^8.2.15" } }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "dev": true, + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -37941,7 +42070,6 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "dev": true, "license": "MIT" }, "node_modules/postcss-zindex": { @@ -37993,7 +42121,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -38020,6 +42147,18 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -38100,6 +42239,15 @@ "version": "2.0.1", "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise": { "version": "7.3.1", "dev": true, @@ -38155,7 +42303,6 @@ }, "node_modules/prompts": { "version": "2.4.2", - "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -38229,6 +42376,83 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "license": "MIT" @@ -38240,12 +42464,10 @@ }, "node_modules/psl": { "version": "1.9.0", - "dev": true, "license": "MIT" }, "node_modules/pump": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -38270,14 +42492,38 @@ "node": ">=8" } }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause" + }, "node_modules/pure-color": { "version": "1.3.0", "dev": true, "license": "MIT" }, "node_modules/pure-rand": { - "version": "6.0.2", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "funding": [ { "type": "individual", @@ -38318,7 +42564,6 @@ }, "node_modules/querystringify": { "version": "2.2.0", - "dev": true, "license": "MIT" }, "node_modules/queue": { @@ -38347,18 +42592,22 @@ ], "license": "MIT" }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "license": "MIT" + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, "engines": { "node": ">=8" } }, "node_modules/randombytes": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -39255,7 +43504,6 @@ }, "node_modules/react-refresh": { "version": "0.14.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -39786,7 +44034,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "devOptional": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -39825,7 +44072,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -39843,14 +44089,13 @@ }, "node_modules/regenerate": { "version": "1.4.2", - "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dev": true, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -39867,14 +44112,13 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -39898,15 +44142,15 @@ } }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "license": "MIT", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -39914,6 +44158,12 @@ "node": ">=4" } }, + "node_modules/regexpu-core/node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, "node_modules/registry-auth-token": { "version": "4.2.2", "dev": true, @@ -39942,24 +44192,27 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/relateurl": { @@ -40488,9 +44741,17 @@ "dev": true, "license": "ISC" }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/reselect": { @@ -40513,9 +44774,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-bin": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/resolve-bin/-/resolve-bin-0.4.3.tgz", + "integrity": "sha512-9u8TMpc+SEHXxQXblXHz5yRvRZERkCZimFN9oz85QI3uhkh7nqfjm6OGTLg+8vucpXGcY4jLK6WkylPmt7GSvw==", + "license": "MIT", + "dependencies": { + "find-parent-dir": "~0.3.0" + } + }, "node_modules/resolve-cwd": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -40524,9 +44793,67 @@ "node": ">=8" } }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", + "license": "MIT", + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-dir/node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", + "license": "MIT", + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-dir/node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-dir/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/resolve-dir/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/resolve-from": { "version": "5.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -40605,6 +44932,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/rollup": { "version": "3.22.0", "license": "MIT", @@ -41072,7 +45408,6 @@ }, "node_modules/rtlcss": { "version": "3.5.0", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^5.0.0", @@ -41084,9 +45419,18 @@ "rtlcss": "bin/rtlcss.js" } }, + "node_modules/rtlcss-webpack-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/rtlcss-webpack-plugin/-/rtlcss-webpack-plugin-4.0.7.tgz", + "integrity": "sha512-ouSbJtgcLBBQIsMgarxsDnfgRqm/AS4BKls/mz/Xb6HSl+PdEzefTR+Wz5uWQx4odoX0g261Z7yb3QBz0MTm0g==", + "license": "MIT", + "dependencies": { + "babel-runtime": "~6.25.0", + "rtlcss": "^3.5.0" + } + }, "node_modules/rtlcss/node_modules/find-up": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -41101,7 +45445,6 @@ }, "node_modules/rtlcss/node_modules/locate-path": { "version": "6.0.0", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -41115,7 +45458,6 @@ }, "node_modules/rtlcss/node_modules/p-locate": { "version": "5.0.0", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -41136,6 +45478,30 @@ "node": ">=0.12.0" } }, + "node_modules/run-con": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.12.tgz", + "integrity": "sha512-5257ILMYIF4RztL9uoZ7V9Q97zHtNHn5bN3NobeAnzB1P3ASLgg8qocM2u+R18ttp+VEM78N2LK8XcNVtnSRrg==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~3.0.0", + "minimist": "^1.2.8", + "strip-json-comments": "~3.1.1" + }, + "bin": { + "run-con": "cli.js" + } + }, + "node_modules/run-con/node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "funding": [ @@ -41165,7 +45531,6 @@ }, "node_modules/rxjs": { "version": "7.8.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -41207,7 +45572,6 @@ }, "node_modules/safe-regex-test": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -41232,7 +45596,6 @@ }, "node_modules/sass": { "version": "1.62.1", - "devOptional": true, "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -41290,7 +45653,6 @@ }, "node_modules/saxes": { "version": "6.0.0", - "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -41308,8 +45670,9 @@ } }, "node_modules/schema-utils": { - "version": "4.0.1", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -41318,7 +45681,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -41327,7 +45690,6 @@ }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -41362,12 +45724,10 @@ }, "node_modules/select-hose": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/selfsigned": { "version": "2.1.1", - "dev": true, "license": "MIT", "dependencies": { "node-forge": "^1" @@ -41480,8 +45840,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "dev": true, + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" @@ -41555,7 +45916,6 @@ }, "node_modules/serve-index": { "version": "1.9.1", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", @@ -41572,7 +45932,6 @@ }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -41580,7 +45939,6 @@ }, "node_modules/serve-index/node_modules/depd": { "version": "1.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -41588,7 +45946,6 @@ }, "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", - "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", @@ -41602,22 +45959,18 @@ }, "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", - "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", - "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -41677,7 +46030,6 @@ }, "node_modules/shallow-clone": { "version": "3.0.1", - "dev": true, "license": "MIT", "dependencies": { "kind-of": "^6.0.2" @@ -41720,7 +46072,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -41731,7 +46082,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -41739,7 +46089,6 @@ }, "node_modules/shell-quote": { "version": "1.8.1", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -42007,7 +46356,6 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "dev": true, "license": "ISC" }, "node_modules/sigstore": { @@ -42218,7 +46566,6 @@ }, "node_modules/sirv": { "version": "2.0.3", - "dev": true, "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.20", @@ -42231,7 +46578,6 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "dev": true, "license": "MIT" }, "node_modules/sitemap": { @@ -42264,7 +46610,6 @@ }, "node_modules/slash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -42326,7 +46671,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -42342,7 +46686,6 @@ }, "node_modules/sockjs": { "version": "0.3.24", - "dev": true, "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", @@ -42354,7 +46697,6 @@ "version": "2.8.3", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "dev": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -42406,9 +46748,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -42416,7 +46758,6 @@ }, "node_modules/source-map-loader": { "version": "3.0.2", - "dev": true, "license": "MIT", "dependencies": { "abab": "^2.0.5", @@ -42474,11 +46815,35 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spawnd": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-10.1.4.tgz", + "integrity": "sha512-drqHc0mKJmtMsiGMOCwzlc5eZ0RPtRvT7tQAluW2A0qUc0G7TQ8KLcn3E6K5qzkLkH2UkS3nYQiVGULvvsD9dw==", + "license": "MIT", + "dependencies": { + "signal-exit": "^4.1.0", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/spawnd/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -42487,14 +46852,12 @@ "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -42503,12 +46866,10 @@ "node_modules/spdx-license-ids": { "version": "3.0.18", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", - "dev": true + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==" }, "node_modules/spdy": { "version": "4.0.2", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -42523,7 +46884,6 @@ }, "node_modules/spdy-transport": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", @@ -42534,6 +46894,20 @@ "wbuf": "^1.7.3" } }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -42622,7 +46996,6 @@ }, "node_modules/stack-utils": { "version": "2.0.6", - "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -42633,7 +47006,6 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -42644,6 +47016,12 @@ "dev": true, "license": "MIT" }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, "node_modules/state-toggle": { "version": "1.0.3", "dev": true, @@ -42681,7 +47059,6 @@ }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "internal-slot": "^1.0.4" @@ -42690,6 +47067,20 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", @@ -42713,7 +47104,6 @@ }, "node_modules/string-length": { "version": "4.0.2", - "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -42762,7 +47152,6 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.8", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -42780,7 +47169,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.7", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -42796,7 +47184,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -42809,7 +47196,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -42866,7 +47252,6 @@ }, "node_modules/strip-bom": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -42882,7 +47267,6 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -42892,7 +47276,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, "dependencies": { "min-indent": "^1.0.0" }, @@ -42902,7 +47285,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -42924,7 +47306,6 @@ }, "node_modules/strip-outer": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" @@ -42935,7 +47316,6 @@ }, "node_modules/strip-outer/node_modules/escape-string-regexp": { "version": "1.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -42977,6 +47357,12 @@ "webpack": "^5.0.0" } }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "license": "ISC" + }, "node_modules/style-to-object": { "version": "0.3.0", "dev": true, @@ -43000,6 +47386,402 @@ "postcss": "^8.2.15" } }, + "node_modules/stylelint": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz", + "integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/media-query-list-parser": "^4.0.2", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.1.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.0.1", + "debug": "^4.3.7", + "fast-glob": "^3.3.2", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^9.1.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^6.0.2", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.35.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.1.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz", + "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.1.0.tgz", + "integrity": "sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==", + "license": "MIT", + "dependencies": { + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^14.0.1", + "stylelint-scss": "^6.4.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.6.1" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-scss": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.10.0.tgz", + "integrity": "sha512-y03if6Qw9xBMoVaf7tzp5BbnYhYvudIKzURkhSHzcHG0bW0fAYvQpTUVJOe7DyhHaxeThBil4ObEMvGbV7+M+w==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.1", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.35.0", + "mdn-data": "^2.12.2", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.0.2" + } + }, + "node_modules/stylelint-scss/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/stylelint-scss/node_modules/css-tree/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "node_modules/stylelint-scss/node_modules/mdn-data": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.14.0.tgz", + "integrity": "sha512-QjcSiIvUHjmXp5wNLClRjQeU0Zp+I2Dag+AhtQto0nyKYZ3IF/pUzCuHe7Bv77EC92XE5t3EXeEiEv/to2Bwig==", + "license": "CC0-1.0" + }, + "node_modules/stylelint-scss/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", + "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/stylelint/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/stylelint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "license": "MIT" + }, + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/stylelint/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/stylelint/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", + "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", + "license": "MIT", + "dependencies": { + "flat-cache": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", + "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", + "license": "MIT", + "dependencies": { + "flatted": "^3.3.1", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/stylelint/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/stylelint/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/stylelint/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, + "node_modules/stylelint/node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -43124,7 +47906,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -43133,6 +47914,22 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz", + "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "license": "MIT", @@ -43145,9 +47942,13 @@ }, "node_modules/svg-parser": { "version": "2.0.4", - "dev": true, "license": "MIT" }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" + }, "node_modules/svgo": { "version": "2.8.0", "dev": true, @@ -43233,9 +48034,90 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "dev": true, "license": "MIT" }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/tannin": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", @@ -43247,7 +48129,6 @@ }, "node_modules/tapable": { "version": "2.2.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -43414,12 +48295,13 @@ } }, "node_modules/terser": { - "version": "5.17.4", - "devOptional": true, + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "license": "BSD-2-Clause", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -43431,15 +48313,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.8", - "dev": true, + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -43463,34 +48346,8 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -43501,32 +48358,8 @@ "node": ">= 10.13.0" } }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -43540,12 +48373,10 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "devOptional": true, "license": "MIT" }, "node_modules/terser/node_modules/source-map": { "version": "0.6.1", - "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -43553,7 +48384,6 @@ }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", - "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -43562,7 +48392,6 @@ }, "node_modules/test-exclude": { "version": "6.0.0", - "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -43575,7 +48404,6 @@ }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -43594,7 +48422,6 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -43603,6 +48430,15 @@ "node": "*" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -43614,7 +48450,12 @@ }, "node_modules/text-table": { "version": "0.2.0", - "dev": true, + "license": "MIT" + }, + "node_modules/third-party-web": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.2.tgz", + "integrity": "sha512-taJ0Us0lKoYBqcbccMuDElSUPOxmBfwlHe1OkHQ3KFf+RwovvBHdXhbFk9XJVQE2vHzxbTwvwg5GFsT9hbDokQ==", "license": "MIT" }, "node_modules/throttleit": { @@ -43628,7 +48469,6 @@ }, "node_modules/through": { "version": "2.3.8", - "dev": true, "license": "MIT" }, "node_modules/through2": { @@ -43642,7 +48482,6 @@ }, "node_modules/thunky": { "version": "1.1.0", - "dev": true, "license": "MIT" }, "node_modules/tiny-emitter": { @@ -43683,6 +48522,21 @@ "node": ">=14.0.0" } }, + "node_modules/tldts-core": { + "version": "6.1.69", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.69.tgz", + "integrity": "sha512-nygxy9n2PBUFQUtAXAc122gGo+04/j5qr5TGQFZTHafTKYvmARVXt2cA5rgero2/dnXUfkdPtiJoKmrd3T+wdA==", + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.69", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.69.tgz", + "integrity": "sha512-H3c/jgvuv8ghdMNJ0rMVECknl1B7qz+BZsFBd4n7l6J2CRnJx8foZ2Qrd32ZY2dqcMgER7KIQ52u1zTejSISAg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.69" + } + }, "node_modules/tmp": { "version": "0.2.1", "dev": true, @@ -43740,16 +48594,8 @@ }, "node_modules/tmpl": { "version": "1.0.5", - "dev": true, "license": "BSD-3-Clause" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-readable-stream": { "version": "1.0.0", "dev": true, @@ -43760,6 +48606,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -43777,7 +48625,6 @@ }, "node_modules/totalist": { "version": "3.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -43787,7 +48634,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -43800,7 +48646,6 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -43818,6 +48663,15 @@ "node": ">=14" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/treeverse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", @@ -43844,14 +48698,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, "engines": { "node": ">=8" } }, "node_modules/trim-repeated": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" @@ -43862,7 +48714,6 @@ }, "node_modules/trim-repeated/node_modules/escape-string-regexp": { "version": "1.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -43890,7 +48741,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -44089,7 +48939,7 @@ }, "node_modules/ts-node": { "version": "10.9.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -44237,7 +49087,6 @@ "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, "dependencies": { "tslib": "^1.8.1" }, @@ -44251,8 +49100,7 @@ "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tuf-js": { "version": "1.1.7", @@ -44380,7 +49228,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -44390,7 +49237,6 @@ }, "node_modules/type-detect": { "version": "4.0.8", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -44398,7 +49244,6 @@ }, "node_modules/type-fest": { "version": "0.21.3", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -44421,7 +49266,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -44437,6 +49281,12 @@ "dev": true, "license": "MIT" }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -44445,7 +49295,6 @@ }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", - "dev": true, "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" @@ -44497,7 +49346,6 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -44524,6 +49372,12 @@ "node": "*" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, "node_modules/ufo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", @@ -44545,7 +49399,6 @@ }, "node_modules/unbox-primitive": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -44557,6 +49410,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -44589,7 +49476,6 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -44597,7 +49483,6 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -44609,7 +49494,6 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -44617,7 +49501,6 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -44686,7 +49569,6 @@ }, "node_modules/unique-string": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "crypto-random-string": "^2.0.0" @@ -44886,10 +49768,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -44904,9 +49785,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -45078,7 +49960,6 @@ }, "node_modules/url-loader": { "version": "4.1.1", - "dev": true, "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", @@ -45106,7 +49987,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -45122,7 +50002,6 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -45130,12 +50009,10 @@ "node_modules/url-loader/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/url-loader/node_modules/schema-utils": { "version": "3.1.2", - "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", @@ -45152,7 +50029,6 @@ }, "node_modules/url-parse": { "version": "1.5.10", - "dev": true, "license": "MIT", "dependencies": { "querystringify": "^2.1.1", @@ -45308,7 +50184,6 @@ }, "node_modules/uuid": { "version": "8.3.2", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -45354,12 +50229,11 @@ }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { "version": "9.1.0", - "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -45374,7 +50248,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -45384,7 +50257,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", - "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -46264,7 +51136,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", - "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^4.0.0" @@ -46307,7 +51178,6 @@ }, "node_modules/walker": { "version": "1.0.8", - "dev": true, "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" @@ -46327,8 +51197,9 @@ "license": "Apache-2.0" }, "node_modules/watchpack": { - "version": "2.4.0", - "dev": true, + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -46340,7 +51211,6 @@ }, "node_modules/wbuf": { "version": "1.7.3", - "dev": true, "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" @@ -46363,42 +51233,47 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/webpack": { - "version": "5.83.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -46418,19 +51293,22 @@ } }, "node_modules/webpack-bundle-analyzer": { - "version": "4.8.0", - "dev": true, + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", - "lodash": "^4.17.20", + "html-escaper": "^2.0.2", "opener": "^1.5.2", - "sirv": "^1.0.7", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { @@ -46440,75 +51318,85 @@ "node": ">= 10.13.0" } }, - "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=8" + "node": ">=14.15.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "webpack": "5.x.x" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } } }, - "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=14" } }, - "node_modules/webpack-bundle-analyzer/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-bundle-analyzer/node_modules/sirv": { - "version": "1.0.19", - "dev": true, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" - }, "engines": { - "node": ">= 10" + "node": ">=10.13.0" } }, - "node_modules/webpack-bundle-analyzer/node_modules/totalist": { - "version": "1.1.0", - "dev": true, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, "engines": { - "node": ">=6" + "node": ">= 10.13.0" } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "dev": true, + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "license": "MIT", "dependencies": { "colorette": "^2.0.10", @@ -46529,8 +51417,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.0", - "dev": true, + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.9", @@ -46539,7 +51428,7 @@ "@types/serve-index": "^1.9.1", "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/ws": "^8.5.5", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", @@ -46561,7 +51450,7 @@ "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", + "webpack-dev-middleware": "^5.3.4", "ws": "^8.13.0" }, "bin": { @@ -46588,7 +51477,6 @@ }, "node_modules/webpack-dev-server/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -46607,7 +51495,6 @@ }, "node_modules/webpack-dev-server/node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -46618,7 +51505,6 @@ }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -46632,7 +51518,6 @@ }, "node_modules/webpack-merge": { "version": "5.8.0", - "dev": true, "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", @@ -46652,7 +51537,6 @@ }, "node_modules/webpack-sources": { "version": "3.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" @@ -46679,15 +51563,16 @@ } }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.1", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -46703,25 +51588,25 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/webpack/node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "dev": true, "license": "MIT" }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.2", - "dev": true, + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", @@ -46804,7 +51689,6 @@ }, "node_modules/websocket-driver": { "version": "0.7.4", - "dev": true, "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", @@ -46817,7 +51701,6 @@ }, "node_modules/websocket-extensions": { "version": "0.1.4", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=0.8.0" @@ -46825,7 +51708,6 @@ }, "node_modules/whatwg-encoding": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -46838,7 +51720,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, "engines": { "node": ">=12" } @@ -46858,7 +51739,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -46872,7 +51752,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", @@ -46887,7 +51766,6 @@ }, "node_modules/which-collection": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-map": "^2.0.1", @@ -46906,7 +51784,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.9", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", @@ -47004,7 +51881,6 @@ }, "node_modules/wildcard": { "version": "2.0.1", - "dev": true, "license": "MIT" }, "node_modules/wordwrap": { @@ -47126,7 +52002,6 @@ }, "node_modules/write-file-atomic": { "version": "4.0.2", - "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -47222,7 +52097,6 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -47242,7 +52116,6 @@ }, "node_modules/xdg-basedir": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -47261,7 +52134,6 @@ }, "node_modules/xml-name-validator": { "version": "4.0.0", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12" @@ -47269,7 +52141,6 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "dev": true, "license": "MIT" }, "node_modules/xtend": { @@ -47373,8 +52244,7 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { "version": "1.10.2", @@ -47422,7 +52292,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -47448,7 +52317,7 @@ }, "node_modules/yn": { "version": "3.1.1", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -47456,7 +52325,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -47495,6 +52363,15 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "1.0.5", "dev": true, diff --git a/package.json b/package.json index 8ec1227d40..158ddb1c0a 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@types/react-transition-group": "4.4.11", "@types/wicg-file-system-access": "2023.10.5", "@wordpress/dataviews": "4.5.0", + "@wordpress/scripts": "30.7.0", "ajv": "8.12.0", "async-lock": "1.4.1", "axios": "1.6.1", @@ -126,7 +127,7 @@ "@nx/web": "16.9.0", "@nx/webpack": "16.9.0", "@nx/workspace": "16.9.0", - "@playwright/test": "1.47.1", + "@playwright/test": "1.48.1", "@rollup/plugin-url": "^8.0.1", "@swc-node/register": "~1.6.7", "@swc/core": "~1.3.85", diff --git a/packages/playground/cli/src/cli.ts b/packages/playground/cli/src/cli.ts index 18160ccd2b..b175785b9d 100644 --- a/packages/playground/cli/src/cli.ts +++ b/packages/playground/cli/src/cli.ts @@ -229,11 +229,13 @@ async function run() { } lastCaption = e.detail.caption || lastCaption || 'Running the Blueprint'; - logger.log( + process.stdout.clearLine(0); + process.stdout.cursorTo(0); + process.stdout.write( '\r\x1b[K' + `${lastCaption.trim()} – ${e.detail.progress}%` ); if (progress100) { - logger.log('\n'); + process.stdout.write('\n'); } }); return compileBlueprint(blueprint as Blueprint, { @@ -274,8 +276,10 @@ async function run() { Math.min(100, (100 * e.detail.loaded) / e.detail.total) ); if (!args.quiet) { + process.stdout.clearLine(0); + process.stdout.cursorTo(0); process.stdout.write( - `\rDownloading WordPress ${percentProgress}%... ` + `Downloading WordPress ${percentProgress}%...` ); } }) as any); @@ -309,6 +313,7 @@ async function run() { WP_DEBUG_DISPLAY: false, }; + logger.log(`Booting WordPress...`); requestHandler = await bootWordPress({ siteUrl: absoluteUrl, createPhpRuntime: async () => @@ -334,14 +339,23 @@ async function run() { }, }, }); + logger.log(`Booted!`); const php = await requestHandler.getPrimaryPhp(); try { - if (wpDetails && !args.mountBeforeInstall) { + if ( + wpDetails && + !args.mountBeforeInstall && + !fs.existsSync(preinstalledWpContentPath) + ) { + logger.log( + `Caching preinstalled WordPress for the next boot...` + ); fs.writeFileSync( preinstalledWpContentPath, await zipDirectory(php, '/wordpress') ); + logger.log(`Cached!`); } if (args.mount) { diff --git a/packages/playground/cli/src/server.ts b/packages/playground/cli/src/server.ts index 55e3b99376..b394cbd32d 100644 --- a/packages/playground/cli/src/server.ts +++ b/packages/playground/cli/src/server.ts @@ -10,59 +10,46 @@ export interface ServerOptions { } export async function startServer(options: ServerOptions) { - const app = express(); - - const server = await new Promise< - Server - >((resolve, reject) => { - const server = app.listen(options.port, () => { - const address = server.address(); - if (address === null || typeof address === 'string') { - reject(new Error('Server address is not available')); - } else { - resolve(server); - } - }); + Bun.serve({ + port: options.port, + async fetch(req: Request, res: Response) { + const phpResponse = await options.handleRequest({ + url: req.url, + headers: parseHeaders(req), + method: req.method as any, + body: await bufferRequestBody(req), + }); + + return new Response(phpResponse.bytes, { + status: phpResponse.httpStatusCode, + headers: phpResponse.headers, + }); + }, }); - - app.use('/', async (req, res) => { - const phpResponse = await options.handleRequest({ - url: req.url, - headers: parseHeaders(req), - method: req.method as any, - body: await bufferRequestBody(req), - }); - - res.statusCode = phpResponse.httpStatusCode; - for (const key in phpResponse.headers) { - res.setHeader(key, phpResponse.headers[key]); - } - res.end(phpResponse.bytes); + await new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, 1000); }); - - const address = server.address(); - const port = (address! as AddressInfo).port; - await options.onBind(port); + await options.onBind(options.port); } const bufferRequestBody = async (req: Request): Promise => - await new Promise((resolve) => { - const body: Uint8Array[] = []; - req.on('data', (chunk) => { - body.push(chunk); - }); - req.on('end', () => { - resolve(Buffer.concat(body)); - }); - }); + new Uint8Array(await req.arrayBuffer()); +// await new Promise((resolve) => { +// const body: Uint8Array[] = []; +// req.on('data', (chunk) => { +// body.push(chunk); +// }); +// req.on('end', () => { +// resolve(Buffer.concat(body)); +// }); +// }); const parseHeaders = (req: Request): Record => { const requestHeaders: Record = {}; - if (req.rawHeaders && req.rawHeaders.length) { - for (let i = 0; i < req.rawHeaders.length; i += 2) { - requestHeaders[req.rawHeaders[i].toLowerCase()] = - req.rawHeaders[i + 1]; - } + for (const [key, value] of req.headers.entries()) { + requestHeaders[key.toLowerCase()] = value; } return requestHeaders; }; diff --git a/packages/playground/components/src/FilePickerControl/index.tsx b/packages/playground/components/src/FilePickerControl/index.tsx index 707752a6b3..59247e2b52 100644 --- a/packages/playground/components/src/FilePickerControl/index.tsx +++ b/packages/playground/components/src/FilePickerControl/index.tsx @@ -3,7 +3,7 @@ import { Button, Modal } from '@wordpress/components'; import { PathPreview } from './PathPreview'; import css from './style.module.css'; import type { FileNode } from '../FilePickerTree'; -import FilePickerTree from '../FilePickerTree'; +import { FilePickerTree } from '../FilePickerTree'; export function FilePickerControl({ value = '', diff --git a/packages/playground/components/src/FilePickerTree/index.tsx b/packages/playground/components/src/FilePickerTree/index.tsx index eabfff1de4..29ae8146f1 100644 --- a/packages/playground/components/src/FilePickerTree/index.tsx +++ b/packages/playground/components/src/FilePickerTree/index.tsx @@ -28,7 +28,7 @@ export type FilePickerControlProps = { type ExpandedNodePaths = Record; -const FilePickerTree: React.FC = ({ +export const FilePickerTree: React.FC = ({ isLoading = false, error = undefined, files, @@ -324,5 +324,3 @@ const FileName: React.FC<{ ); }; - -export default FilePickerTree; diff --git a/packages/playground/components/src/index.ts b/packages/playground/components/src/index.ts index 838008a0b2..5a8a431e57 100644 --- a/packages/playground/components/src/index.ts +++ b/packages/playground/components/src/index.ts @@ -1 +1,4 @@ +export { FilePickerControl } from './FilePickerControl'; +export { FilePickerTree } from './FilePickerTree'; + export * from './icons'; diff --git a/packages/playground/data-liberation-static-files-editor/blueprint.json b/packages/playground/data-liberation-static-files-editor/blueprint.json index 0ec3011920..098ae0160f 100644 --- a/packages/playground/data-liberation-static-files-editor/blueprint.json +++ b/packages/playground/data-liberation-static-files-editor/blueprint.json @@ -19,6 +19,10 @@ { "step": "activatePlugin", "pluginPath": "z-data-liberation-static-files-editor/plugin.php" + }, + { + "step": "runPHP", + "code": " 'My Notes', 'post_status' => 'publish', 'post_type' => 'page'));" } ] } diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index a6f9826c10..6ac3c1c842 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -39,10 +39,19 @@ class WP_Static_Files_Editor_Plugin { - static public function register_hooks() { - $fs = new WP_Filesystem( WP_STATIC_CONTENT_DIR ); - $static_sync = new WP_Static_File_Sync( $fs ); - $static_sync->initialize_sync(); + static private $fs; + + static private function get_fs() { + if(!self::$fs) { + self::$fs = new WP_Filesystem( WP_STATIC_CONTENT_DIR ); + } + return self::$fs; + } + + static public function initialize() { + // Register hooks + // $static_sync = new WP_Static_File_Sync( self::get_fs() ); + // $static_sync->initialize_sync(); register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); @@ -50,21 +59,144 @@ static public function register_hooks() { self::register_post_type(); }); - add_filter('manage_local_file_posts_columns', function($columns) { - $columns['local_file_path'] = 'Local File Path'; - return $columns; + // Register the admin page + add_action('admin_menu', function() { + // Get first post or create new one + $posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'ASC' + )); + + if (empty($posts)) { + // Create a new draft post if none exists + $post_id = wp_insert_post(array( + 'post_title' => 'My first note', + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_status' => 'draft' + )); + } else { + $post_id = $posts[0]->ID; + } + + $edit_url = admin_url('post.php?post=' . $post_id . '&action=edit'); + + add_menu_page( + 'Edit Local Files', + 'Edit Local Files', + 'manage_options', + $edit_url, // Direct link to edit page + '', // No callback needed + 'dashicons-media-text', + 30 + ); }); - add_action('manage_local_file_posts_custom_column', function($column_name, $post_id) use ($fs) { - if ($column_name === 'local_file_path') { - $local_file_path = get_post_meta($post_id, 'local_file_path', true); - echo esc_html($local_file_path); - if(!$fs->is_file($local_file_path)) { - echo ' (missing)'; - } + add_action('admin_enqueue_scripts', function($hook) { + $screen = get_current_screen(); + $enqueue_script = $screen && $screen->base === 'post' && $screen->post_type === WP_LOCAL_FILE_POST_TYPE; + if (!$enqueue_script) { + return; } - }, 10, 2); + wp_register_script( + 'static-files-editor', + plugins_url('ui/build/index.tsx.js', __FILE__), + array('wp-element', 'wp-components', 'wp-block-editor', 'wp-edit-post', 'wp-plugins', 'wp-editor', 'wp-api-fetch'), + '1.0.0', + true + ); + + wp_add_inline_script( + 'static-files-editor', + 'window.WP_LOCAL_FILE_POST_TYPE = ' . json_encode(WP_LOCAL_FILE_POST_TYPE) . ';', + 'before' + ); + + wp_register_style( + 'static-files-editor', + plugins_url('ui/build/style-index.tsx.css', __FILE__), + array('wp-components', 'wp-block-editor', 'wp-edit-post'), + '1.0.0' + ); + + wp_enqueue_script('static-files-editor'); + wp_enqueue_style('static-files-editor'); + + // Preload the initial files tree + wp_add_inline_script('wp-api-fetch', 'wp.apiFetch.use(wp.apiFetch.createPreloadingMiddleware({ + "/static-files-editor/v1/get-files-tree": { + body: '.json_encode(WP_Static_Files_Editor_Plugin::get_files_tree_endpoint()).' + } + }));', 'after'); + }); + + + add_action('rest_api_init', function() { + register_rest_route('static-files-editor/v1', '/get-or-create-post-for-file', array( + 'methods' => 'POST', + 'callback' => array(self::class, 'get_or_create_post_for_file'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + )); + + register_rest_route('static-files-editor/v1', '/get-files-tree', array( + 'methods' => 'GET', + 'callback' => array(self::class, 'get_files_tree_endpoint'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + )); + + register_rest_route('static-files-editor/v1', '/create-directory', array( + 'methods' => 'POST', + 'callback' => array(self::class, 'create_directory_endpoint'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + )); + }); + } + + static public function get_local_files_tree($subdirectory = '') { + $tree = []; + $fs = self::get_fs(); + + $base_dir = $subdirectory ? $subdirectory : '/'; + self::build_local_file_tree_recursive($fs, $base_dir, $tree); + + return $tree; + } + + static private function build_local_file_tree_recursive($fs, $dir, &$tree) { + $items = $fs->ls($dir); + if ($items === false) { + return; + } + + foreach ($items as $item) { + $path = $dir === '/' ? "/$item" : "$dir/$item"; + + if ($fs->is_dir($path)) { + $node = array( + 'type' => 'folder', + 'name' => $item, + 'children' => [] + ); + $tree[] = $node; + + // Recursively build children + $last_index = count($tree) - 1; + self::build_local_file_tree_recursive($fs, $path, $tree[$last_index]['children']); + } else { + $tree[] = array( + 'type' => 'file', + 'name' => $item, + ); + } + } } /** @@ -136,6 +268,14 @@ static private function register_post_type() { 'has_archive' => false, 'show_in_rest' => true, )); + + // Register the meta field for file paths + register_post_meta(WP_LOCAL_FILE_POST_TYPE, 'local_file_path', array( + 'type' => 'string', + 'description' => 'Path to the local file', + 'single' => true, + 'show_in_rest' => true, + )); } /** @@ -157,6 +297,85 @@ static private function reset_db_data() { $GLOBALS['@pdo']->query("UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='wp_commentmeta'"); } + static public function get_or_create_post_for_file($request) { + $file_path = $request->get_param('path'); + $file_path = '/' . ltrim($file_path, '/'); + $create_file = $request->get_param('create_file'); + + if (!$file_path) { + return new WP_Error('missing_path', 'File path is required'); + } + + // Create the file if requested and it doesn't exist + if ($create_file) { + $fs = self::get_fs(); + if (!$fs->put_contents($file_path, '')) { + return new WP_Error('file_creation_failed', 'Failed to create file'); + } + } + + // Check if a post already exists for this file path + $existing_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $file_path, + 'posts_per_page' => 1 + )); + + $post_content = ''; + if (file_exists($file_path)) { + $post_content = file_get_contents($file_path); + } + + if (!empty($existing_posts)) { + // Update existing post + $post_data = array( + 'ID' => $existing_posts[0]->ID, + 'post_content' => $post_content + ); + $post_id = wp_update_post($post_data); + } else { + // Create new post + $post_data = array( + 'post_title' => basename($file_path), + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_status' => 'publish', + 'post_content' => $post_content, + 'meta_input' => array( + 'local_file_path' => $file_path + ) + ); + $post_id = wp_insert_post($post_data); + } + + if (is_wp_error($post_id)) { + return $post_id; + } + + return array( + 'post_id' => $post_id + ); + } + + static public function get_files_tree_endpoint() { + return self::get_local_files_tree(); + } + + static public function create_directory_endpoint($request) { + $path = $request->get_param('path'); + if (!$path) { + return new WP_Error('missing_path', 'Directory path is required'); + } + $path = '/' . ltrim($path, '/'); + + $fs = self::get_fs(); + if (!$fs->mkdir($path)) { + return new WP_Error('mkdir_failed', 'Failed to create directory'); + } + + return array('success' => true); + } + } -WP_Static_Files_Editor_Plugin::register_hooks(); +WP_Static_Files_Editor_Plugin::initialize(); diff --git a/packages/playground/data-liberation-static-files-editor/run.sh b/packages/playground/data-liberation-static-files-editor/run.sh index a68be80ce3..d78b543d41 100644 --- a/packages/playground/data-liberation-static-files-editor/run.sh +++ b/packages/playground/data-liberation-static-files-editor/run.sh @@ -3,7 +3,7 @@ rm -rf ./my-notes/workdir/* cp -r ./my-notes/safe-copy/* ./my-notes/workdir/ -bun ../cli/src/cli.ts \ +bun --inspect ../cli/src/cli.ts \ server \ --mount=../data-liberation-static-files-editor:/wordpress/wp-content/plugins/z-data-liberation-static-files-editor \ --mount=../data-liberation-markdown:/wordpress/wp-content/plugins/z-data-liberation-markdown \ diff --git a/packages/playground/wordpress/src/boot.ts b/packages/playground/wordpress/src/boot.ts index 025fab4844..510753aa3f 100644 --- a/packages/playground/wordpress/src/boot.ts +++ b/packages/playground/wordpress/src/boot.ts @@ -166,7 +166,7 @@ export async function bootWordPress(options: BootOptions) { php, cwd: requestHandler.documentRoot, recreateRuntime: options.createPhpRuntime, - maxRequests: 400, + maxRequests: 40, }); return php; From e46e7e974c9df120821a08845adf0abb7270e61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 24 Dec 2024 12:25:47 +0100 Subject: [PATCH 05/71] Add Editor UI and hooks to synchronize WordPress pages to local files --- .../src/WP_Blocks_To_Markdown.php | 19 +- .../src/WP_Markdown_To_Blocks.php | 11 +- .../.gitignore | 1 + .../plugin.php | 262 +++++++++++--- .../project.json | 23 ++ .../ui/src/add-local-files-tab.tsx | 68 ++++ .../FilePickerControl/PathPreview.tsx | 29 ++ .../components/FilePickerControl/index.tsx | 68 ++++ .../FilePickerControl/style.module.css | 91 +++++ .../src/components/FilePickerTree/index.tsx | 324 ++++++++++++++++++ .../FilePickerTree/style.module.css | 95 +++++ .../ui/src/components/icons.tsx | 164 +++++++++ .../ui/src/editor.css | 3 + .../ui/src/index.tsx | 155 +++++++++ .../ui/src/tsconfig.json | 21 ++ .../ui/webpack.config.js | 34 ++ 16 files changed, 1317 insertions(+), 51 deletions(-) create mode 100644 packages/playground/data-liberation-static-files-editor/.gitignore create mode 100644 packages/playground/data-liberation-static-files-editor/project.json create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/PathPreview.tsx create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/index.tsx create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/style.module.css create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/icons.tsx create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/editor.css create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/index.tsx create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/tsconfig.json create mode 100644 packages/playground/data-liberation-static-files-editor/ui/webpack.config.js diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 05ae218810..cbcd59a802 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -7,25 +7,40 @@ class WP_Blocks_To_Markdown { private $block_markup; private $state; private $parents = []; + private $metadata; - public function __construct($block_markup) { + public function __construct($block_markup, $metadata = []) { $this->block_markup = $block_markup; $this->state = array( 'indent' => array(), 'listStyle' => array() ); + $this->metadata = $metadata; } private $markdown; public function convert() { - $this->markdown = $this->blocks_to_markdown(parse_blocks($this->block_markup)); + $this->markdown = ''; + $this->markdown .= $this->frontmatter(); + $this->markdown .= $this->blocks_to_markdown(parse_blocks($this->block_markup)); } public function get_result() { return $this->markdown; } + private function frontmatter() { + if(empty($this->metadata)){ + return ''; + } + $frontmatter = ''; + foreach ($this->metadata as $key => $value) { + $frontmatter .= "$key: ".json_encode($value)."\n"; + } + return "---\n$frontmatter---\n\n"; + } + private function blocks_to_markdown($blocks) { $output = ''; foreach ($blocks as $block) { diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index 94da3e484c..38856f6bf2 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -42,7 +42,6 @@ public function convert() { return false; } $this->convert_markdown_to_blocks(); - // $this->block_markup = WP_Import_Utils::convert_blocks_to_markup( $this->parsed_blocks ); return true; } @@ -122,7 +121,9 @@ private function convert_markdown_to_blocks() { 'list', $attrs ); - $this->append_content( '
    ' ); + + $tag = $attrs['ordered'] ? 'ol' : 'ul'; + $this->append_content( '<' . $tag . ' class="wp-block-list">' ); break; case ExtensionBlock\ListItem::class: @@ -255,7 +256,11 @@ private function convert_markdown_to_blocks() { $this->pop_block(); break; case ExtensionBlock\ListBlock::class: - $this->append_content( '
' ); + if($node->getListData()->type === 'unordered') { + $this->append_content( '' ); + } else { + $this->append_content( '' ); + } $this->pop_block(); break; case ExtensionBlock\ListItem::class: diff --git a/packages/playground/data-liberation-static-files-editor/.gitignore b/packages/playground/data-liberation-static-files-editor/.gitignore new file mode 100644 index 0000000000..c415a9f59d --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/.gitignore @@ -0,0 +1 @@ +ui/build diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 6ac3c1c842..0b680e053e 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -53,7 +53,7 @@ static public function initialize() { // $static_sync = new WP_Static_File_Sync( self::get_fs() ); // $static_sync->initialize_sync(); - register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); + // register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); add_action('init', function() { self::register_post_type(); @@ -132,7 +132,6 @@ static public function initialize() { }));', 'after'); }); - add_action('rest_api_init', function() { register_rest_route('static-files-editor/v1', '/get-or-create-post-for-file', array( 'methods' => 'POST', @@ -158,6 +157,168 @@ static public function initialize() { }, )); }); + + // @TODO: the_content and rest_prepare_local_file filters run twice for REST API requests. + // find a way of only running them once. + + // Add the filter for 'the_content' + add_filter('the_content', function($content, $post = null) { + // If no post is provided, try to get it from the global scope + if (!$post) { + global $post; + } + + // Check if this post is of type "local_file" + if ($post && $post->post_type === 'local_file') { + // Get the latest content from the database first + $content = $post->post_content; + + // Then refresh from file if needed + $new_content = self::refresh_post_from_local_file($post); + if(!is_wp_error($new_content)) { + $content = $new_content; + } + return $content; + } + + // Return original content for all other post types + return $content; + }, 10, 2); + + // Add filter for REST API responses + add_filter('rest_prepare_local_file', function($response, $post, $request) { + $new_content = self::refresh_post_from_local_file($post); + if(!is_wp_error($new_content)) { + $response->data['content']['raw'] = $new_content; + $response->data['content']['rendered'] = ''; + } + return $response; + }, 10, 3); + + // Update the file after post is saved + add_action('save_post_' . WP_LOCAL_FILE_POST_TYPE, function($post_id, $post, $update) { + self::save_post_data_to_local_file($post); + }, 10, 3); + } + + static private $synchronizing = false; + static private function acquire_synchronization_lock() { + // Ignore auto-saves or revisions + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + // return false; + } + + // Skip if in maintenance mode + if (wp_is_maintenance_mode()) { + return false; + } + + if (defined('WP_IMPORTING') && WP_IMPORTING) { + return false; + } + + // @TODO: Synchronize between threads + if(self::$synchronizing) { + return false; + } + self::$synchronizing = true; + return true; + } + + static private function release_synchronization_lock() { + self::$synchronizing = false; + } + + static private function refresh_post_from_local_file($post) { + try { + if(!self::acquire_synchronization_lock()) { + return; + } + + $post_id = $post->ID; + $fs = self::get_fs(); + $path = get_post_meta($post_id, 'local_file_path', true); + if(!$fs->is_file($path)) { + _doing_it_wrong(__METHOD__, 'File not found', '1.0.0'); + return; + } + $content = $fs->read_file($path); + $extension = pathinfo($path, PATHINFO_EXTENSION); + switch($extension) { + case 'md': + $converter = new WP_Markdown_To_Blocks( $content ); + break; + case 'xhtml': + $converter = new WP_HTML_To_Blocks( WP_XML_Processor::create_from_string( $content ) ); + break; + case 'html': + default: + $converter = new WP_HTML_To_Blocks( WP_HTML_Processor::create_fragment( $content ) ); + break; + } + $converter->convert(); + + $metadata = []; + foreach($converter->get_all_metadata() as $key => $value) { + $metadata[$key] = $value[0]; + } + $new_content = $converter->get_block_markup(); + + $updated = wp_update_post(array( + 'ID' => $post_id, + 'post_content' => $new_content, + // 'meta_input' => $metadata, + )); + if(is_wp_error($updated)) { + return $updated; + } + + return $new_content; + } finally { + self::release_synchronization_lock(); + } + } + + static private function save_post_data_to_local_file($post) { + try { + if(!self::acquire_synchronization_lock()) { + return; + } + + $post_id = $post->ID; + if ( + empty($post->ID) || + $post->post_status !== 'publish' || + $post->post_type !== WP_LOCAL_FILE_POST_TYPE + ) { + return; + } + + $fs = self::get_fs(); + $path = get_post_meta($post_id, 'local_file_path', true); + $extension = pathinfo($path, PATHINFO_EXTENSION); + $metadata = get_post_meta($post_id); + + // @TODO: Include specific post fields in the stored metadata + // foreach(WP_Imported_Entity::POST_FIELDS as $field) { + // $metadata[$field] = get_post_field($field, $post_id); + // } + $content = get_post_field('post_content', $post_id); + switch($extension) { + // @TODO: Add support for HTML and XHTML + case 'html': + case 'xhtml': + case 'md': + default: + $converter = new WP_Blocks_To_Markdown( $content, $metadata ); + break; + } + $converter->convert(); + $fs->put_contents($path, $converter->get_result()); + } finally { + self::release_synchronization_lock(); + } + } static public function get_local_files_tree($subdirectory = '') { @@ -298,58 +459,67 @@ static private function reset_db_data() { } static public function get_or_create_post_for_file($request) { - $file_path = $request->get_param('path'); - $file_path = '/' . ltrim($file_path, '/'); - $create_file = $request->get_param('create_file'); - - if (!$file_path) { - return new WP_Error('missing_path', 'File path is required'); - } + try { + if(!self::acquire_synchronization_lock()) { + return; + } - // Create the file if requested and it doesn't exist - if ($create_file) { + $file_path = $request->get_param('path'); + $file_path = '/' . ltrim($file_path, '/'); + $create_file = $request->get_param('create_file'); + + if (!$file_path) { + return new WP_Error('missing_path', 'File path is required'); + } + + // Create the file if requested and it doesn't exist $fs = self::get_fs(); - if (!$fs->put_contents($file_path, '')) { - return new WP_Error('file_creation_failed', 'Failed to create file'); + if ($create_file) { + if (!$fs->put_contents($file_path, '')) { + return new WP_Error('file_creation_failed', 'Failed to create file'); + } } - } - // Check if a post already exists for this file path - $existing_posts = get_posts(array( - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'meta_key' => 'local_file_path', - 'meta_value' => $file_path, - 'posts_per_page' => 1 - )); + // Check if a post already exists for this file path + $existing_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $file_path, + 'posts_per_page' => 1 + )); - $post_content = ''; - if (file_exists($file_path)) { - $post_content = file_get_contents($file_path); - } + if (!empty($existing_posts)) { + // Update existing post + $post_data = array( + 'ID' => $existing_posts[0]->ID, + 'post_content' => '' + ); + $post_id = wp_update_post($post_data); + } else { + // Create new post + $post_data = array( + 'post_title' => basename($file_path), + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_status' => 'publish', + 'post_content' => '', + 'meta_input' => array( + 'local_file_path' => $file_path + ) + ); + $post_id = wp_insert_post($post_data); + } - if (!empty($existing_posts)) { - // Update existing post - $post_data = array( - 'ID' => $existing_posts[0]->ID, - 'post_content' => $post_content - ); - $post_id = wp_update_post($post_data); - } else { - // Create new post - $post_data = array( - 'post_title' => basename($file_path), - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'post_status' => 'publish', - 'post_content' => $post_content, - 'meta_input' => array( - 'local_file_path' => $file_path - ) - ); - $post_id = wp_insert_post($post_data); + if (is_wp_error($post_id)) { + return $post_id; + } + + } finally { + self::release_synchronization_lock(); } - if (is_wp_error($post_id)) { - return $post_id; + $refreshed_post = self::refresh_post_from_local_file(get_post($post_id)); + if (is_wp_error($refreshed_post)) { + return $refreshed_post; } return array( diff --git a/packages/playground/data-liberation-static-files-editor/project.json b/packages/playground/data-liberation-static-files-editor/project.json new file mode 100644 index 0000000000..db75f075ca --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/project.json @@ -0,0 +1,23 @@ +{ + "name": "playground-data-liberation-static-files-editor", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/playground/data-liberation-static-files-editor/ui/src", + "projectType": "library", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "command": "wp-scripts build --config=webpack.config.js src/index.tsx", + "cwd": "packages/playground/data-liberation-static-files-editor/ui" + } + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "command": "wp-scripts start --config=webpack.config.js src/index.tsx", + "cwd": "packages/playground/data-liberation-static-files-editor/ui" + } + } + }, + "tags": [] +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx new file mode 100644 index 0000000000..5ba672ed65 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx @@ -0,0 +1,68 @@ +declare global { + interface Window { + wp: { + element: any; + blockEditor: any; + dataLiberationCreateElementDecorator: (fn: Function) => Function; + }; + React: { + createElement: Function & { patched?: boolean }; + }; + ReactJSXRuntime: { + jsx: Function; + jsxs: Function; + }; + } +} + +/** + * Adds a new "Local files" tab to the list view sidebar. + * + * Don't do this at home! This function monkey-patches + * React.createElement to ensure the "local files" tab + * is passed as one of the 's props. + * + * This makes assumptions about the internals of the + * post editor and will likely break in future versions. + * + * A more future-proof solution would involve either: + * + * * Contributing a new extension point to the post editor. + * * Creating a dedicated editor for local files. This would + * require heavy maintenance, though, and reconciling changes + * and patches from the post editor so it's not ideal. + * + * @param tab - The tab to add to the list view sidebar. + */ +export function addLocalFilesTab(tab: { + name: string; + title: string; + panel: React.ReactElement; +}) { + function patchArguments(args: any[]) { + let [type, props, ...children] = args; + const newProps = { ...props }; + if ('tabs' in newProps) { + const hasLocalFilesTab = newProps.tabs.find( + (tab) => tab.name === 'local-files' + ); + if (!hasLocalFilesTab) { + newProps.tabs.unshift(tab); + } + newProps.defaultTabId = 'local-files'; + } + return [type, newProps, ...children]; + } + + // Monkey-patch window.React.createElement + const originalCreateElement = window.React.createElement as any; + (window.React as any).createElement = function (...args: any[]) { + return originalCreateElement(...patchArguments(args)); + }; + + // Monkey-patch window.ReactJSXRuntime.jsx + const originalJSX = window.ReactJSXRuntime.jsx; + window.ReactJSXRuntime.jsx = (...args: any[]) => { + return originalJSX(...patchArguments(args)); + }; +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/PathPreview.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/PathPreview.tsx new file mode 100644 index 0000000000..2e1f516465 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/PathPreview.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import css from './style.module.css'; + +export function PathPreview({ path }: { path: string }) { + if (!path) { + return ( +
+ Select a path +
+ ); + } + + const segments = path.split('/'); + let pathPreviewEnd = (segments.length > 2 ? '/' : '') + segments.pop(); + if (pathPreviewEnd.length > 10) { + pathPreviewEnd = pathPreviewEnd.substring(pathPreviewEnd.length - 10); + } + const pathPreviewStart = path.substring( + 0, + path.length - pathPreviewEnd.length + ); + return ( +
+ ); +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/index.tsx new file mode 100644 index 0000000000..f3806b52a1 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/index.tsx @@ -0,0 +1,68 @@ +import React, { useState } from '@wordpress/element'; +import { Button, Modal } from '@wordpress/components'; +import { PathPreview } from './PathPreview'; +import css from './style.module.css'; +import type { FileNode } from '../FilePickerTree'; +import { FilePickerTree } from '../FilePickerTree'; + +export function FilePickerControl({ + value = '', + onChange, + files = [], + isLoading = false, + error = undefined, +}: { + value?: string; + onChange: (selectedPath: string) => void; + files?: FileNode[]; + isLoading?: boolean; + error?: string; +}) { + const [isOpen, setOpen] = useState(false); + const openModal = () => setOpen(true); + const closeModal = () => setOpen(false); + + const [lastSelectedPath, setLastSelectedPath] = useState( + value || null + ); + function handleSubmit(event?: React.FormEvent) { + event?.preventDefault(); + onChange(lastSelectedPath || ''); + closeModal(); + } + + return ( + <> + + {isOpen && ( + +
+ +
+ +
+ +
+ )} + + ); +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/style.module.css b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/style.module.css new file mode 100644 index 0000000000..210ebe6b06 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerControl/style.module.css @@ -0,0 +1,91 @@ +.control { + border: 1px solid #ddd; + border-radius: 4px; + padding: 2px; + display: flex; + align-items: center; + width: 230px; + gap: 8px; + cursor: pointer; + + .browse-label { + display: inline-flex; + text-decoration: none; + font-family: inherit; + font-weight: normal; + font-size: 13px; + margin: 0; + border: 0; + cursor: pointer; + -webkit-appearance: none; + background: none; + transition: box-shadow 0.1s linear; + align-items: center; + box-sizing: border-box; + padding: 6px 12px; + border-radius: 2px; + border: 1px solid #ddd; + } + + .selected-path { + width: 250px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .path-preview { + overflow: hidden; + color: #000 !important; + } +} + +.modal :global(.components-modal__content) { + margin-bottom: 80px; + padding: 0; +} + +.modal-footer { + position: absolute; + left: 0; + bottom: 0; + flex-direction: column; + flex-wrap: nowrap; + min-height: 80px; + padding: 16px 24px; + gap: 8px; + box-sizing: border-box; + border-top: 1px solid #ddd; + background: #fff; + display: flex; + align-items: flex-end; + justify-content: center; + width: 100%; +} + +.path-mapping-button { + height: 30px; +} + +.path-preview { + display: flex; + align-items: center; + justify-content: center; +} + +.path-preview::before, +.path-preview::after { + overflow: hidden; + white-space: pre; +} + +.path-preview::before { + content: attr(data-content-start); + text-overflow: ellipsis; + flex-grow: 1; +} + +.path-preview::after { + content: attr(data-content-end); + flex-shrink: 0; +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx new file mode 100644 index 0000000000..547c3909f3 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -0,0 +1,324 @@ +import React, { useEffect, useRef, useState } from '@wordpress/element'; +import { + __experimentalTreeGrid as TreeGrid, + __experimentalTreeGridRow as TreeGridRow, + __experimentalTreeGridCell as TreeGridCell, + Button, + Spinner, +} from '@wordpress/components'; +import { Icon, chevronRight, chevronDown } from '@wordpress/icons'; +import '@wordpress/components/build-style/style.css'; +import css from './style.module.css'; +import classNames from 'classnames'; +import { folder, file } from '../icons'; + +export type FileNode = { + name: string; + type: 'file' | 'folder'; + children?: FileNode[]; +}; + +export type FilePickerControlProps = { + files: FileNode[]; + initialPath?: string; + onSelect?: (path: string, node: FileNode) => void; + isLoading?: boolean; + error?: string; +}; + +type ExpandedNodePaths = Record; + +export const FilePickerTree: React.FC = ({ + isLoading = false, + error = undefined, + files, + initialPath, + onSelect = () => {}, +}) => { + initialPath = initialPath ? initialPath.replace(/^\/+/, '') : '/'; + const [expanded, setExpanded] = useState(() => { + if (!initialPath) { + return {}; + } + const expanded: ExpandedNodePaths = {}; + const pathParts = initialPath.split('/'); + for (let i = 0; i < pathParts.length; i++) { + const pathSoFar = pathParts.slice(0, i + 1).join('/'); + expanded[pathSoFar] = true; + } + return expanded; + }); + const [selectedPath, setSelectedPath] = useState(() => + initialPath ? initialPath : null + ); + + const expandNode = (path: string, isOpen: boolean) => { + setExpanded((prevState) => ({ + ...prevState, + [path]: isOpen, + })); + }; + + const selectPath = (path: string, node: FileNode) => { + setSelectedPath(path); + onSelect(path, node); + }; + + const generatePath = (node: FileNode, parentPath = ''): string => { + return parentPath + ? `${parentPath}/${node.name}`.replaceAll(/\/+/g, '/') + : node.name; + }; + + const [searchBuffer, setSearchBuffer] = useState(''); + const searchBufferTimeoutRef = useRef(null); + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key.length === 1 && event.key.match(/\S/)) { + const newSearchBuffer = searchBuffer + event.key.toLowerCase(); + setSearchBuffer(newSearchBuffer); + // Clear the buffer after 1 second + if (searchBufferTimeoutRef.current) { + clearTimeout(searchBufferTimeoutRef.current); + } + searchBufferTimeoutRef.current = setTimeout(() => { + setSearchBuffer(''); + }, 1000); + + if (thisContainerRef.current) { + const buttons = Array.from( + thisContainerRef.current.querySelectorAll( + '.file-node-button' + ) + ); + const activeElement = document.activeElement; + let startIndex = 0; + if ( + activeElement && + buttons.includes(activeElement as HTMLButtonElement) + ) { + startIndex = buttons.indexOf( + activeElement as HTMLButtonElement + ); + } + for (let i = 0; i < buttons.length; i++) { + const index = (startIndex + i) % buttons.length; + const button = buttons[index]; + if ( + button.textContent + ?.toLowerCase() + .trim() + .startsWith(newSearchBuffer) + ) { + (button as HTMLButtonElement).focus(); + break; + } + } + } + } else { + // Clear the buffer for any non-letter key press + setSearchBuffer(''); + if (searchBufferTimeoutRef.current) { + clearTimeout(searchBufferTimeoutRef.current); + } + } + } + + const thisContainerRef = useRef(null); + + useEffect(() => { + // automatically focus the first button when the files are loaded + if (thisContainerRef.current) { + const firstButton = initialPath + ? thisContainerRef.current.querySelector( + `[data-path="${initialPath}"]` + ) + : thisContainerRef.current.querySelector('.file-node-button'); + if (firstButton) { + (firstButton as HTMLButtonElement).focus(); + } + } + }, [files.length > 0]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+

Error loading files

+

{error}

+
+ ); + } + + return ( +
+ + {files.map((file, index) => ( + + ))} + +
+ ); +}; + +const NodeRow: React.FC<{ + node: FileNode; + level: number; + position: number; + setSize: number; + expandedNodePaths: ExpandedNodePaths; + expandNode: (path: string, isOpen: boolean) => void; + selectPath: (path: string) => void; + selectedNode: string | null; + generatePath: (node: FileNode, parentPath?: string) => string; + parentPath?: string; + parentMapping?: string; +}> = ({ + node, + level, + position, + setSize, + expandedNodePaths, + expandNode, + selectPath, + generatePath, + parentPath = '', + selectedNode, +}) => { + const path = generatePath(node, parentPath); + const isExpanded = expandedNodePaths[path]; + + const toggleOpen = () => expandNode(path, !isExpanded); + + const handleKeyDown = (event: any) => { + if (event.key === 'ArrowLeft') { + if (isExpanded) { + toggleOpen(); + } else { + ( + document.querySelector( + `[data-path="${parentPath}"]` + ) as HTMLButtonElement + )?.focus(); + } + event.preventDefault(); + event.stopPropagation(); + } else if (event.key === 'ArrowRight') { + if (isExpanded) { + if (node.children?.length) { + const firstChildPath = generatePath(node.children[0], path); + ( + document.querySelector( + `[data-path="${firstChildPath}"]` + ) as HTMLButtonElement + )?.focus(); + } + } else { + toggleOpen(); + } + event.preventDefault(); + event.stopPropagation(); + } else if (event.key === 'Space') { + expandNode(path, !isExpanded); + } else if (event.key === 'Enter') { + const form = event.currentTarget?.closest('form'); + if (form) { + setTimeout(() => { + form.dispatchEvent(new Event('submit', { bubbles: true })); + }); + } + } + }; + return ( + <> + + + {() => ( + + )} + + + {isExpanded && + node.children && + node.children.map((child, index) => ( + + ))} + + ); +}; + +const FileName: React.FC<{ + node: FileNode; + level: number; + isOpen?: boolean; +}> = ({ node, level, isOpen }) => { + const indent: string[] = []; + for (let i = 0; i < level; i++) { + indent.push('    '); + } + return ( + <> + + {node.type === 'folder' ? ( + + ) : ( +
 
+ )} + + {node.name} + + ); +}; diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css new file mode 100644 index 0000000000..78b71827dc --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css @@ -0,0 +1,95 @@ +.file-picker-tree { + width: 100%; + + tr:nth-child(even) { + background-color: #f7f7f7; + } + + &:active { + color: var(--wp-admin-theme-color); + } + + /* &:focus { + outline: 2px solid var(--wp-admin-theme-color); + } */ +} + +.file-node-button { + display: inline-flex; + text-decoration: none; + font-family: inherit; + font-weight: normal; + font-size: 13px; + margin: 0; + border: 0; + cursor: pointer; + -webkit-appearance: none; + background: none; + transition: box-shadow 0.1s linear; + height: 30px; + align-items: center; + box-sizing: border-box; + padding: 6px 12px; + border-radius: 2px; + + width: 100%; + white-space: nowrap; + color: var(--wp-components-color-foreground, #1e1e1e); + background: transparent; + padding: 6px; + + font-family: -apple-system, 'system-ui', 'Segoe UI', Roboto, Oxygen-Sans, + Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; + font-size: 13px; + font-weight: 400; + + /* &:focus, */ + &.selected { + background-color: var(--wp-admin-theme-color); + color: #ffffff; + outline: 0 !important; + box-shadow: none !important; + } + + &.selected:active, + &.selected:hover { + color: #ffffff !important; + } +} + +.file-name { + margin-left: 10px; +} + +.path-picker-control { + border: 1px solid #ddd; + border-radius: 4px; + padding: 2px; + display: flex; + align-items: center; + width: 230px; + gap: 8px; + cursor: pointer; +} + +.selected-path { + width: 250px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.error-container, +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px; +} + +.loading-container :global(.components-spinner) { + width: 40px; + height: 40px; +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/icons.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/icons.tsx new file mode 100644 index 0000000000..b176fd6f50 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/icons.tsx @@ -0,0 +1,164 @@ +import React from '@wordpress/element'; + +export const playgroundLogo = (props?: React.SVGProps) => { + return ( + + + + + + ); +}; + +export const temporaryStorage = (props?: React.SVGProps) => { + return ( + + + + ); +}; + +export const WordPressIcon = (props?: React.SVGProps) => { + return ( + + + + ); +}; + +export const folder = ( + + + +); + +export const file = ( + + + +); + +export const ClockIcon = (props?: React.SVGProps) => ( + + + +); + +export const layout = ( + + + +); + +export function getLogoDataURL(logo: { mime: string; data: string }): string { + return `data:${logo.mime};base64,${logo.data}`; +} + +export function SiteManagerIcon() { + return ( + + + + ); +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/editor.css b/packages/playground/data-liberation-static-files-editor/ui/src/editor.css new file mode 100644 index 0000000000..f48803b52b --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/editor.css @@ -0,0 +1,3 @@ +:global(.editor-visual-editor__post-title-wrapper) { + display: none; +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx new file mode 100644 index 0000000000..99813bc1cb --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState, useCallback } from '@wordpress/element'; +import { FileNode, FilePickerTree } from './components/FilePickerTree'; +import { store as editorStore } from '@wordpress/editor'; +import { store as preferencesStore } from '@wordpress/preferences'; +import { dispatch, useSelect } from '@wordpress/data'; +import apiFetch from '@wordpress/api-fetch'; +import { addLocalFilesTab } from './add-local-files-tab'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { Spinner, Button } from '@wordpress/components'; +import { useEntityProp } from '@wordpress/core-data'; +import './editor.css'; + +// Pre-populated by plugin.php +const WP_LOCAL_FILE_POST_TYPE = window.WP_LOCAL_FILE_POST_TYPE; + +const fileTreePromise = apiFetch({ + path: '/static-files-editor/v1/get-files-tree', +}); + +function ConnectedFilePickerTree() { + const [fileTree, setFileTree] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Get the current post's file path from meta + const [meta] = useEntityProp('postType', WP_LOCAL_FILE_POST_TYPE, 'meta'); + const [selectedPath, setSelectedPath] = useState( + meta?.local_file_path || '/' + ); + + const refreshFileTree = useCallback(async () => { + const tree = await apiFetch({ + path: '/static-files-editor/v1/get-files-tree', + }); + setFileTree(tree); + }, []); + + useEffect(() => { + fileTreePromise + .then((tree) => { + setFileTree(tree); + }) + .catch((error) => { + console.error('Failed to load file tree:', error); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + const onNavigateToEntityRecord = useSelect( + (select) => + select(blockEditorStore).getSettings().onNavigateToEntityRecord, + [] + ); + + const handleFileClick = async (filePath: string, node: FileNode) => { + if (node.type === 'folder') { + setSelectedPath(filePath); + return; + } + + // 1. Create/get post for this file path + const response = (await apiFetch({ + path: '/static-files-editor/v1/get-or-create-post-for-file', + method: 'POST', + data: { path: filePath }, + })) as { post_id: string }; + + // 2. Switch to the new post in the editor + onNavigateToEntityRecord({ + postId: response.post_id, + postType: WP_LOCAL_FILE_POST_TYPE, + }); + }; + + const handleCreateFile = async () => { + const newFilePath = `${selectedPath}/untitled.md`.replace(/\/+/g, '/'); + try { + const response = (await apiFetch({ + path: '/static-files-editor/v1/get-or-create-post-for-file', + method: 'POST', + data: { + path: newFilePath, + create_file: true, + }, + })) as { post_id: string }; + + await refreshFileTree(); + + onNavigateToEntityRecord({ + postId: response.post_id, + postType: WP_LOCAL_FILE_POST_TYPE, + }); + } catch (error) { + console.error('Failed to create file:', error); + } + }; + + const handleCreateDirectory = async () => { + try { + await apiFetch({ + path: '/static-files-editor/v1/create-directory', + method: 'POST', + data: { + path: `${selectedPath}/empty`.replace(/\/+/g, '/'), + }, + }); + await refreshFileTree(); + } catch (error) { + console.error('Failed to create directory:', error); + } + }; + + if (isLoading) { + return ; + } + + if (!fileTree) { + return
No files found
; + } + + return ( +
+
+ + +
+ +
+ ); +} + +addLocalFilesTab({ + name: 'local-files', + title: 'Local Files', + panel: ( +
+ +
+ ), +}); + +dispatch(preferencesStore).set('welcomeGuide', false); +dispatch(preferencesStore).set('enableChoosePatternModal', false); +dispatch(editorStore).setIsListViewOpened(true); diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/tsconfig.json b/packages/playground/data-liberation-static-files-editor/ui/src/tsconfig.json new file mode 100644 index 0000000000..4e88ab9228 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": ["ES2022", "esnext.disposable", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": "." + } +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/webpack.config.js b/packages/playground/data-liberation-static-files-editor/ui/webpack.config.js new file mode 100644 index 0000000000..6e9282e408 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/webpack.config.js @@ -0,0 +1,34 @@ +const defaultConfig = require('@wordpress/scripts/config/webpack.config'); + +/** + * Override the css-loader to use camelCase for class names in CSS modules. + */ +module.exports = { + ...defaultConfig, + module: { + ...defaultConfig.module, + rules: defaultConfig.module.rules.map((rule) => { + if (rule.test?.toString().includes('.css')) { + return { + ...rule, + use: rule.use.map((loader) => { + if (loader.loader?.includes('/css-loader/')) { + return { + ...loader, + options: { + ...loader.options, + modules: { + ...loader.options?.modules, + exportLocalsConvention: 'camelCase', + }, + }, + }; + } + return loader; + }), + }; + } + return rule; + }), + }, +}; From 7b5b125b63809b21f4afa86be6ff133e7210b3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 25 Dec 2024 19:02:52 +0100 Subject: [PATCH 06/71] Implement Git sparse checkout in PHP --- .../data-liberation/src/WP_Git_Filesystem.php | 581 ++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 packages/playground/data-liberation/src/WP_Git_Filesystem.php diff --git a/packages/playground/data-liberation/src/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/WP_Git_Filesystem.php new file mode 100644 index 0000000000..24fdb45cef --- /dev/null +++ b/packages/playground/data-liberation/src/WP_Git_Filesystem.php @@ -0,0 +1,581 @@ + 'commit', + self::OBJECT_TYPE_TREE => 'tree', + self::OBJECT_TYPE_BLOB => 'blob', + self::OBJECT_TYPE_TAG => 'tag', + self::OBJECT_TYPE_RESERVED => 'reserved', + self::OBJECT_TYPE_OFS_DELTA => 'ofs_delta', + self::OBJECT_TYPE_REF_DELTA => 'ref_delta', + ]; + + const FILE_MODE_DIRECTORY = '040000'; + const FILE_MODE_REGULAR_NON_EXECUTABLE = '100644'; + const FILE_MODE_REGULAR_EXECUTABLE = '100755'; + const FILE_MODE_SYMBOLIC_LINK = '120000'; + const FILE_MODE_COMMIT = '160000'; + + const FILE_MODE_NAMES = [ + self::FILE_MODE_DIRECTORY => 'directory', + self::FILE_MODE_REGULAR_NON_EXECUTABLE => 'regular_non_executable', + self::FILE_MODE_REGULAR_EXECUTABLE => 'regular_executable', + self::FILE_MODE_SYMBOLIC_LINK => 'symbolic_link', + self::FILE_MODE_COMMIT => 'commit', + ]; + + private $repoUrl; + + public function __construct($repoUrl) { + $this->repoUrl = rtrim($repoUrl, '/'); + } + + public function fetchFiles($directory = '') { + $headRef = $this->fetchRefHash('HEAD'); + if (!$headRef) { + return false; + } + + $index = $this->list_objects($headRef); + if(false === $index) { + return false; + } + + $commit_tree_sha = $index['objects'][$index['by_oid'][$headRef]]['tree']; + $commit_tree = $index['objects'][$index['by_oid'][$commit_tree_sha]]['content']; + $subdir = $index['objects'][$index['by_oid'][$commit_tree['wp-content']['sha1']]]['content']; + $subdir = $index['objects'][$index['by_oid'][$subdir['html-pages']['sha1']]]['content']; + $subdir = $index['objects'][$index['by_oid'][$subdir['0_wordpress-playground']['sha1']]]['content']; + + $refs = []; + foreach($subdir as $file) { + $refs[] = $file['sha1']; + } + + $blobs = $this->fetch_blobs($refs); + var_dump($blobs); + var_dump($refs); + die(); + + return $index; + } + + private function fetchRefHash($ref_name) { + // Fetch HEAD ref + $url = $this->repoUrl . '/git-upload-pack'; + $response = $this->http_request( + $url, + $this->encode_packet_line("command=ls-refs\n") . + $this->encode_packet_line("agent=git/2.37.3\n") . + $this->encode_packet_line("object-format=sha1\n") . + "0001" . + $this->encode_packet_line("peel\n") . + $this->encode_packet_line("ref-prefix $ref_name\n") . + "0000", + [ + 'Accept: application/x-git-upload-pack-advertisement', + 'Content-Type: application/x-git-upload-pack-request', + 'Git-Protocol: version=2' + ] + ); + + if (!$response) { + return false; + } + + $ref_hash = null; + foreach ($this->parse_git_protocol_v2_packets($response) as $frame) { + if (strpos($frame, $ref_name) !== false) { + $ref_hash = trim(explode(' ', $frame)[0]); + break; + } + } + + if (!$ref_hash) { + return false; + } + + return $ref_hash; + } + + private function parse_git_protocol_v2_packets($bytes) { + $offset = 0; + while ($offset < strlen($bytes)) { + $length = hexdec(substr($bytes, $offset, 4)); + $offset += 4; + $frame = substr($bytes, $offset, $length); + yield $frame; + $offset += $length; + } + } + + private function list_objects($ref_hash) { + $body = + $this->encode_packet_line("want {$ref_hash} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3 filter\n") . + $this->encode_packet_line("filter blob:none\n") . + $this->encode_packet_line("shallow {$ref_hash}\n") . + $this->encode_packet_line("deepen 1\n") . + "0000" . + $this->encode_packet_line("done\n") . + $this->encode_packet_line("done\n"); + + $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ + 'Accept: application/x-git-upload-pack-advertisement', + 'Content-Type: application/x-git-upload-pack-request', + ]); + + $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); + return $this->compute_pack_index($pack_data); + } + + private function fetch_blobs($refs) { + $body = ''; + foreach($refs as $ref) { + $body .= $this->encode_packet_line("want {$ref} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3\n"); + } + $body .= "0000"; + $body .= $this->encode_packet_line("done\n"); + + $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ + 'Accept: application/x-git-upload-pack-advertisement', + 'Content-Type: application/x-git-upload-pack-request', + ]); + $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); + return $this->compute_pack_index($pack_data); + } + + private function http_request($url, $postData = null, $headers = []) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + if ($postData) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + $response = curl_exec($ch); + curl_close($ch); + + return $response; + } + + private function encode_packet_line($data) { + $length = strlen($data) + 4; + return str_pad(dechex($length), 4, '0', STR_PAD_LEFT) . $data; + } + + private function accumulate_pack_data_from_multiplexed_chunks($raw_response) { + $parsed_pack_data = []; + $parsed_chunks = $this->parse_multiplexed_pack_data($raw_response); + foreach($parsed_chunks as $chunk) { + if($chunk['type'] !== 'side-band') { + continue; + } + $parsed_pack_data[] = $chunk['data']; + } + return implode('', $parsed_pack_data); + } + + private function parse_multiplexed_pack_data($bytes) { + $offset = 0; + while ($offset < strlen($bytes)) { + $lengthHex = substr($bytes, $offset, 4); + $offset += 4; + + if ($lengthHex === "0000") { + continue; // End of packet + } + + $length = hexdec($lengthHex); + if ($length === 0) { + break; // No more data + } + + // This is one raw packet line + $content = substr($bytes, $offset, $length - 4); + $offset += $length - 4; + + // Parse possible multiple side-band chunks inside this single packet line + $subOffset = 0; + while ($subOffset < strlen($content)) { + $channel = $content[$subOffset]; + $subOffset++; + if ($subOffset >= strlen($content)) { + break; // No data left after channel byte + } + + // We'll assume the rest of this line is the sub-chunk’s data + // (Git typically sends only one sub-chunk per packet line, but + // if there were multiple, you’d keep parsing until subOffset hits the end) + $chunkData = substr($content, $subOffset); + $subOffset = strlen($content); + + if ($channel === "\x01") { + yield ['type' => 'side-band', 'data' => $chunkData]; + } elseif ($channel === "\x02") { + yield ['type' => 'progress', 'data' => $chunkData]; + } elseif ($channel === "\x03") { + yield ['type' => 'fatal', 'data' => $chunkData]; + } else { + yield ['type' => 'unknown', 'data' => $channel . $chunkData]; + } + } + } + } + + private function parse_pack_data($packData) { + $offset = 0; + + // Basic sanity checks + if (strlen($packData) < 12) { + return false; + } + + $header = substr($packData, $offset, 4); + $offset += 4; + if ($header !== "PACK") { + return false; + } + + $version = unpack('N', substr($packData, $offset, 4))[1]; + $offset += 4; + + $objectCount = unpack('N', substr($packData, $offset, 4))[1]; + $offset += 4; + + $objects = []; + + for ($i = 0; $i < $objectCount; $i++) { + if ($offset >= strlen($packData)) { + break; + } + + $header_offset = $offset; + $object = $this->parse_pack_header($packData, $offset); + $object['type_name'] = self::OBJECT_NAMES[$object['type']]; + $object['content_offset'] = $offset; + $object['header_offset'] = $header_offset; + $object['content'] = $this->inflate_object($packData, $offset, $object['length']); + $object['compressed_length'] = $offset - $object['content_offset']; + $objects[] = $object; + } + return [ + 'objects' => $objects, + 'total_objects' => $objectCount, + 'pack_version' => $version + ]; + } + + private function compute_pack_index($pack_data) { + $parsed_pack = $this->parse_pack_data($pack_data); + $objects = $parsed_pack['objects']; + + $by_oid = []; + $by_offset = []; + $resolved_objects = 0; + // Index entities and resolve deltas + // Run until all objects are resolved + while($resolved_objects < count($objects)) { + $resolved_in_this_iteration = 0; + for($i = 0; $i < count($objects); $i++) { + // Skip already processed objects + if( + isset($by_offset[$objects[$i]['header_offset']]) && + isset($by_oid[$objects[$i]['oid']]) + ) { + continue; + } + + if($objects[$i]['type'] === self::OBJECT_TYPE_OFS_DELTA) { + $target_offset = $objects[$i]['header_offset'] - $objects[$i]['ofs']; + if(!isset($by_offset[$target_offset])) { + continue; + } + // TODO: Make sure the base object will never be another delta. + $base = $objects[$by_offset[$target_offset]]; + $objects[$i]['content'] = $this->applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['type'] = $base['type']; + $objects[$i]['type_name'] = $base['type_name']; + } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { + if(!isset($by_oid[$objects[$i]['reference']])) { + continue; + } + $base = $objects[$by_oid[$objects[$i]['reference']]]; + $objects[$i]['content'] = $this->applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['type'] = $base['type']; + $objects[$i]['type_name'] = $base['type_name']; + } + $oid = sha1($this->wrap_git_object($objects[$i]['type_name'], $objects[$i]['content'])); + $objects[$i]['oid'] = $oid; + $by_oid[$oid] = $i; + $by_offset[$objects[$i]['header_offset']] = $i; + ++$resolved_in_this_iteration; + ++$resolved_objects; + } + if($resolved_in_this_iteration === 0) { + throw new Exception('Could not resolve objects'); + } + } + + // Resolve trees + foreach($objects as $k => $object) { + if( $object['type'] === self::OBJECT_TYPE_TREE ) { + $objects[$k]['content'] = $this->parse_tree_bytes($object['content']); + } else if($object['type'] === self::OBJECT_TYPE_COMMIT) { + $objects[$k]['tree'] = substr($object['content'], 5, 40); + } + } + + return [ + 'objects' => $objects, + 'by_oid' => $by_oid, + 'by_offset' => $by_offset, + ]; + } + + private function wrap_git_object($type, $object) { + $length = strlen($object); + return "$type $length\x00" . $object; + } + + private function parse_pack_header(string $packData, int &$offset) { + // Object type is encoded in bits 654 + $byte = ord($packData[$offset++]); + $type = ($byte >> 4) & 0b111; + // The length encoding get complicated. + // Last four bits of length is encoded in bits 3210 + $length = $byte & 0b1111; + // Whether the next byte is part of the variable-length encoded number + // is encoded in bit 7 + if ($byte & 0b10000000) { + $shift = 4; + $byte = ord($packData[$offset++]); + while ($byte & 0b10000000) { + $length |= ($byte & 0b01111111) << $shift; + $shift += 7; + $byte = ord($packData[$offset++]); + } + $length |= ($byte & 0b01111111) << $shift; + } + // Handle deltified objects + $ofs = null; + $reference = null; + if ($type === self::OBJECT_TYPE_OFS_DELTA) { + // Git uses a specific formula: ofs = ((ofs + 1) << 7) + (c & 0x7f) + // for each continuation byte. The first byte doesn't do the "ofs+1" part. + // This code matches Git’s logic. + $ofs = 0; + // Read the first byte + $c = ord($packData[$offset++]); + $ofs = ($c & 0x7F); + + // If bit 7 (0x80) is set, we keep reading + while ($c & 0x80) { + $c = ord($packData[$offset++]); + $ofs = (($ofs + 1) << 7) + ($c & 0x7F); + } + } else if ($type === self::OBJECT_TYPE_REF_DELTA) { + $reference = substr($packData, $offset, 20); + $offset += 20; + } + return [ + 'ofs' => $ofs, + 'type' => $type, + 'length' => $length, + 'reference' => $reference + ]; + } + + /** + * Incrementally inflate the next object’s compressed data until it yields + * $uncompressedSize bytes, or we hit the end of the compressed stream. + * Adjusts $offset so that after returning, $offset points to the next object header. + */ + private function inflate_object(string $packData, int &$offset, int $uncompressedSize): ?string { + $inflateContext = inflate_init(ZLIB_ENCODING_DEFLATE); + if (!$inflateContext) { + return null; + } + + $inflated = ''; + $packLen = strlen($packData); + + $bytes_read = 0; + while ($offset < $packLen) { + // Feed chunks into inflate. We don’t know how big each chunk is, + // so let's just pick something arbitrary: + $chunk = substr($packData, $offset, 256); + + $res = inflate_add($inflateContext, $chunk); + switch(inflate_get_status($inflateContext)) { + case ZLIB_BUF_ERROR: + case ZLIB_DATA_ERROR: + case ZLIB_VERSION_ERROR: + case ZLIB_MEM_ERROR: + throw new Exception('Inflate error'); + } + if ($res === false) { + throw new Exception('Inflate error'); + } + $bytes_read_for_this_chunk = inflate_get_read_len($inflateContext) - $bytes_read; + $offset += $bytes_read_for_this_chunk; + + $bytes_read = inflate_get_read_len($inflateContext); + $inflated .= $res; + + if(inflate_get_status($inflateContext) === ZLIB_STREAM_END) { + break; + } + } + return $inflated; + } + + private function parse_tree_bytes($treeContent) { + $offset = 0; + $files = []; + + while ($offset < strlen($treeContent)) { + if ($offset >= strlen($treeContent)) { + var_dump('uninitialized string offset'); + break; // Prevent uninitialized string offset + } + + // Read file mode + $modeEnd = strpos($treeContent, ' ', $offset); + if ($modeEnd === false || $modeEnd >= strlen($treeContent)) { + var_dump('invalid mode'); + break; // Invalid mode + } + $mode = substr($treeContent, $offset, $modeEnd - $offset); + $offset = $modeEnd + 1; + + if(preg_match('/^0?4.*/', $mode)) { + $mode = self::FILE_MODE_DIRECTORY; + } else if(preg_match('/^1006.*/', $mode)) { + $mode = self::FILE_MODE_REGULAR_NON_EXECUTABLE; + } else if(preg_match('/^1007.*/', $mode)) { + $mode = self::FILE_MODE_REGULAR_EXECUTABLE; + } else if(preg_match('/^120.*/', $mode)) { + $mode = self::FILE_MODE_SYMBOLIC_LINK; + } else if(preg_match('/^160.*/', $mode)) { + $mode = self::FILE_MODE_COMMIT; + } + + // Read file name + $nameEnd = strpos($treeContent, "\0", $offset); + if ($nameEnd === false || $nameEnd >= strlen($treeContent)) { + var_dump('invalid name'); + break; // Invalid name + } + $name = substr($treeContent, $offset, $nameEnd - $offset); + $offset = $nameEnd + 1; + + // Read SHA1 + if ($offset + 20 > strlen($treeContent)) { + var_dump('invalid sha1'); + break; // Prevent out-of-bounds access + } + $sha1 = bin2hex(substr($treeContent, $offset, 20)); + $offset += 20; + + $files[$name] = [ + 'mode' => $mode, + 'name' => $name, + 'sha1' => $sha1, + ]; + } + + return $files; + } + + private function applyDelta($base_bytes, $delta_bytes) { + $offset = 0; + + $base_size = $this->readVariableLength($delta_bytes, $offset); + if($base_size !== strlen($base_bytes)) { + // @TODO: Do not throw exceptions...? Or do? + throw new Exception('Base size mismatch'); + } + $result_size = $this->readVariableLength($delta_bytes, $offset); + + $result = ''; + while ($offset < strlen($delta_bytes)) { + $byte = ord($delta_bytes[$offset++]); + if ($byte & 0x80) { + $copyOffset = 0; + $copySize = 0; + if ($byte & 0x01) $copyOffset |= ord($delta_bytes[$offset++]); + if ($byte & 0x02) $copyOffset |= ord($delta_bytes[$offset++]) << 8; + if ($byte & 0x04) $copyOffset |= ord($delta_bytes[$offset++]) << 16; + if ($byte & 0x08) $copyOffset |= ord($delta_bytes[$offset++]) << 24; + if ($byte & 0x10) $copySize |= ord($delta_bytes[$offset++]); + if ($byte & 0x20) $copySize |= ord($delta_bytes[$offset++]) << 8; + if ($byte & 0x40) $copySize |= ord($delta_bytes[$offset++]) << 16; + if ($copySize === 0) $copySize = 0x10000; + $result .= substr($base_bytes, $copyOffset, $copySize); + } else { + $result .= substr($delta_bytes, $offset, $byte); + $offset += $byte; + } + } + + if(strlen($result) !== $result_size) { + // @TODO: Do not throw exceptions...? Or do? + throw new Exception('Result size mismatch'); + } + + return $result; + } + + private function readVariableLength($data, &$offset) { + $result = 0; + $shift = 0; + do { + $byte = ord($data[$offset++]); + $result |= ($byte & 0x7F) << $shift; + $shift += 7; + } while ($byte & 0x80); + return $result; + } +} + + +// $client = new WP_Git_Index('https://github.com/WordPress/gutenberg.git'); +$client = new WP_Git_Index('https://github.com/adamziel/playground-docs-workflow.git'); +$files = $client->fetchFiles(); + +if ($files === false) { + echo "Failed to fetch repository files.\n"; + exit; +} + +// Filter for the `/docs` directory +$docsDirectory = []; +foreach ($files as $path => $content) { + if (strpos($path, 'docs/') === 0) { + $docsDirectory[$path] = $content; + } +} + +// Display the files in the `/docs` directory +if (!empty($docsDirectory)) { + foreach ($docsDirectory as $filePath => $fileContent) { + echo "File: $filePath\n"; + echo "Content:\n$fileContent\n\n"; + } +} else { + echo "No files found in the /docs directory.\n"; +} \ No newline at end of file From e8210590af65c23eded990fc8f3f02a8c3c724bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 26 Dec 2024 01:47:02 +0100 Subject: [PATCH 07/71] Plug in WP_Git_Filesystem to directly browse and edit the static files from a git repo --- .../plugin.php | 9 +- .../src/components/FilePickerTree/index.tsx | 201 +++++- .../playground/data-liberation/bootstrap.php | 5 + .../data-liberation/src/git/WP_Git_Client.php | 190 ++++++ .../src/git/WP_Git_Filesystem.php | 105 +++ .../WP_Git_Pack_Index.php} | 622 +++++++----------- 6 files changed, 736 insertions(+), 396 deletions(-) create mode 100644 packages/playground/data-liberation/src/git/WP_Git_Client.php create mode 100644 packages/playground/data-liberation/src/git/WP_Git_Filesystem.php rename packages/playground/data-liberation/src/{WP_Git_Filesystem.php => git/WP_Git_Pack_Index.php} (56%) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 0b680e053e..6222d29954 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -43,7 +43,11 @@ class WP_Static_Files_Editor_Plugin { static private function get_fs() { if(!self::$fs) { - self::$fs = new WP_Filesystem( WP_STATIC_CONTENT_DIR ); + // self::$fs = new WP_Filesystem( WP_STATIC_CONTENT_DIR ); + self::$fs = new WP_Git_Filesystem( + new WP_Git_Client('https://github.com/WordPress/gutenberg'), + '/docs/how-to-guides/data-basics' + ); } return self::$fs; } @@ -239,7 +243,7 @@ static private function refresh_post_from_local_file($post) { $fs = self::get_fs(); $path = get_post_meta($post_id, 'local_file_path', true); if(!$fs->is_file($path)) { - _doing_it_wrong(__METHOD__, 'File not found', '1.0.0'); + _doing_it_wrong(__METHOD__, 'File not found: ' . $path, '1.0.0'); return; } $content = $fs->read_file($path); @@ -280,6 +284,7 @@ static private function refresh_post_from_local_file($post) { } static private function save_post_data_to_local_file($post) { + return; try { if(!self::acquire_synchronization_lock()) { return; diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 547c3909f3..684c4e21a7 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -5,6 +5,7 @@ import { __experimentalTreeGridCell as TreeGridCell, Button, Spinner, + ButtonGroup, } from '@wordpress/components'; import { Icon, chevronRight, chevronDown } from '@wordpress/icons'; import '@wordpress/components/build-style/style.css'; @@ -22,18 +23,31 @@ export type FilePickerControlProps = { files: FileNode[]; initialPath?: string; onSelect?: (path: string, node: FileNode) => void; + onNodeCreated?: ( + type: 'file' | 'folder', + path: string, + name: string + ) => void; isLoading?: boolean; error?: string; }; type ExpandedNodePaths = Record; +type TempNode = { + type: 'file' | 'folder'; + parentPath: string; +}; + export const FilePickerTree: React.FC = ({ isLoading = false, error = undefined, files, initialPath, onSelect = () => {}, + onNodeCreated = (...args) => { + console.log('onNodeCreated', args); + }, }) => { initialPath = initialPath ? initialPath.replace(/^\/+/, '') : '/'; const [expanded, setExpanded] = useState(() => { @@ -52,6 +66,8 @@ export const FilePickerTree: React.FC = ({ initialPath ? initialPath : null ); + const [tempNode, setTempNode] = useState(null); + const expandNode = (path: string, isOpen: boolean) => { setExpanded((prevState) => ({ ...prevState, @@ -70,6 +86,49 @@ export const FilePickerTree: React.FC = ({ : node.name; }; + const handleCreateNode = (type: 'file' | 'folder') => { + if (!selectedPath) return; + + // Find selected node + const pathParts = selectedPath.split('/'); + let currentNode: FileNode | undefined = undefined; + let currentNodes = files; + + for (const part of pathParts) { + currentNode = currentNodes.find((n) => n.name === part); + if (!currentNode) break; + currentNodes = currentNode.children || []; + } + + if (!currentNode) return; + + // If selected node is a file, use its parent path + const parentPath = + currentNode.type === 'file' + ? pathParts.slice(0, -1).join('/') + : selectedPath; + + console.log({ + parentPath, + currentNode, + selectedPath, + pathParts, + }); + + expandNode(parentPath, true); + setTempNode({ type, parentPath }); + }; + + const handleTempNodeComplete = (name: string) => { + if (!tempNode) return; + onNodeCreated(tempNode.type, tempNode.parentPath, name); + setTempNode(null); + }; + + const handleTempNodeCancel = () => { + setTempNode(null); + }; + const [searchBuffer, setSearchBuffer] = useState(''); const searchBufferTimeoutRef = useRef(null); function handleKeyDown(event: React.KeyboardEvent) { @@ -158,6 +217,23 @@ export const FilePickerTree: React.FC = ({ return (
+ + + + + {files.map((file, index) => ( = ({ selectedNode={selectedPath} selectPath={selectPath} generatePath={generatePath} + tempNode={tempNode} + onTempNodeComplete={handleTempNodeComplete} + onTempNodeCancel={handleTempNodeCancel} /> ))} @@ -190,6 +269,9 @@ const NodeRow: React.FC<{ generatePath: (node: FileNode, parentPath?: string) => string; parentPath?: string; parentMapping?: string; + tempNode?: TempNode | null; + onTempNodeComplete?: (name: string) => void; + onTempNodeCancel?: () => void; }> = ({ node, level, @@ -201,6 +283,9 @@ const NodeRow: React.FC<{ generatePath, parentPath = '', selectedNode, + tempNode, + onTempNodeComplete, + onTempNodeCancel, }) => { const path = generatePath(node, parentPath); const isExpanded = expandedNodePaths[path]; @@ -246,6 +331,7 @@ const NodeRow: React.FC<{ } } }; + return ( <> - {isExpanded && - node.children && - node.children.map((child, index) => ( - - ))} + {isExpanded && ( + <> + {tempNode && tempNode.parentPath === path && ( + + + {() => ( + + )} + + + )} + {node.children && + node.children.map((child, index) => ( + + ))} + + )} ); }; +const TempNodeInput: React.FC<{ + type: 'file' | 'folder'; + onComplete: (name: string) => void; + onCancel: () => void; + level: number; +}> = ({ type, onComplete, onCancel, level }) => { + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + const handleBlur = () => { + const value = inputRef.current?.value.trim() || ''; + if (value) { + onComplete(value); + } else { + onCancel(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + const value = inputRef.current?.value.trim() || ''; + if (value) { + onComplete(value); + } else { + onCancel(); + } + } else if (e.key === 'Escape') { + onCancel(); + } + }; + + const indent: string[] = []; + for (let i = 0; i < level; i++) { + indent.push('    '); + } + + return ( +
+ + + +
+ ); +}; + const FileName: React.FC<{ node: FileNode; level: number; diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 802b07337f..9e1326edba 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -81,6 +81,11 @@ require_once __DIR__ . '/src/import/WP_Retry_Frontloading_Iterator.php'; require_once __DIR__ . '/src/import/WP_Import_HTML_Processor.php'; require_once __DIR__ . '/src/import/WP_Import_Utils.php'; + +require_once __DIR__ . '/src/git/WP_Git_Client.php'; +require_once __DIR__ . '/src/git/WP_Git_Pack_Index.php'; +require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; + require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; require_once __DIR__ . '/src/utf8_decoder.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php new file mode 100644 index 0000000000..c76064cbdd --- /dev/null +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -0,0 +1,190 @@ +repoUrl = rtrim($repoUrl, '/'); + } + + public function fetchRefs($prefix) { + $url = $this->repoUrl . '/git-upload-pack'; + $response = $this->http_request( + $url, + $this->encode_packet_line("command=ls-refs\n") . + $this->encode_packet_line("agent=git/2.37.3\n") . + $this->encode_packet_line("object-format=sha1\n") . + "0001" . + $this->encode_packet_line("peel\n") . + $this->encode_packet_line("ref-prefix $prefix\n") . + "0000", + [ + 'Accept: application/x-git-upload-pack-advertisement', + 'Content-Type: application/x-git-upload-pack-request', + 'Git-Protocol: version=2' + ] + ); + + if (!$response) { + return false; + } + + $refs = []; + foreach ($this->parse_git_protocol_v2_packets($response) as $frame) { + $space_pos = strpos($frame, ' '); + if($space_pos === false) { + continue; + } + $hash = substr($frame, 0, $space_pos); + $newline_pos = strpos($frame, "\n"); + $name = substr($frame, $space_pos + 1, $newline_pos - $space_pos - 1); + $refs[$name] = $hash; + } + return $refs; + } + + private function parse_git_protocol_v2_packets($bytes) { + $offset = 0; + while ($offset < strlen($bytes)) { + $length = hexdec(substr($bytes, $offset, 4)); + $offset += 4; + $frame = substr($bytes, $offset, $length); + yield $frame; + $offset += $length; + } + } + + public function list_objects($ref_hash) { + $body = + $this->encode_packet_line("want {$ref_hash} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3 filter\n") . + $this->encode_packet_line("filter blob:none\n") . + $this->encode_packet_line("shallow {$ref_hash}\n") . + $this->encode_packet_line("deepen 1\n") . + "0000" . + $this->encode_packet_line("done\n") . + $this->encode_packet_line("done\n"); + + $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ + 'Accept: application/x-git-upload-pack-advertisement', + 'Content-Type: application/x-git-upload-pack-request', + ]); + + $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); + return WP_Git_Pack_Index::from_pack_data($pack_data); + } + + public function fetchObjects($refs) { + $body = ''; + foreach($refs as $ref) { + $body .= $this->encode_packet_line("want {$ref} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3\n"); + } + $body .= "0000"; + $body .= $this->encode_packet_line("done\n"); + + $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ + 'Accept: application/x-git-upload-pack-advertisement', + 'Content-Type: application/x-git-upload-pack-request', + ]); + $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); + return WP_Git_Pack_Index::from_pack_data($pack_data); + } + + public function backfillBlobs($index, $root = '/') { + $sub_root = $index->get_by_path($root); + + $blobs_shas = []; + foreach($index->get_descendants($sub_root['oid']) as $blob) { + $blobs_shas[] = $blob['sha1']; + } + $blobs_index = $this->fetchObjects($blobs_shas); + $index->set_external_get_by_oid(function($oid) use ($blobs_index) { + return $blobs_index->get_by_oid($oid); + }); + return $index; + } + + private function http_request($url, $postData = null, $headers = []) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + if ($postData) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + + $response = curl_exec($ch); + curl_close($ch); + + return $response; + } + + private function encode_packet_line($data) { + $length = strlen($data) + 4; + return str_pad(dechex($length), 4, '0', STR_PAD_LEFT) . $data; + } + + private function accumulate_pack_data_from_multiplexed_chunks($raw_response) { + $parsed_pack_data = []; + $parsed_chunks = $this->parse_multiplexed_pack_data($raw_response); + foreach($parsed_chunks as $chunk) { + if($chunk['type'] !== 'side-band') { + continue; + } + $parsed_pack_data[] = $chunk['data']; + } + return implode('', $parsed_pack_data); + } + + private function parse_multiplexed_pack_data($bytes) { + $offset = 0; + while ($offset < strlen($bytes)) { + $lengthHex = substr($bytes, $offset, 4); + $offset += 4; + + if ($lengthHex === "0000") { + continue; // End of packet + } + + $length = hexdec($lengthHex); + if ($length === 0) { + break; // No more data + } + + // This is one raw packet line + $content = substr($bytes, $offset, $length - 4); + $offset += $length - 4; + + // Parse possible multiple side-band chunks inside this single packet line + $subOffset = 0; + while ($subOffset < strlen($content)) { + $channel = $content[$subOffset]; + $subOffset++; + if ($subOffset >= strlen($content)) { + break; // No data left after channel byte + } + + // We'll assume the rest of this line is the sub-chunk’s data + // (Git typically sends only one sub-chunk per packet line, but + // if there were multiple, you’d keep parsing until subOffset hits the end) + $chunkData = substr($content, $subOffset); + $subOffset = strlen($content); + + if ($channel === "\x01") { + yield ['type' => 'side-band', 'data' => $chunkData]; + } elseif ($channel === "\x02") { + yield ['type' => 'progress', 'data' => $chunkData]; + } elseif ($channel === "\x03") { + yield ['type' => 'fatal', 'data' => $chunkData]; + } else { + yield ['type' => 'unknown', 'data' => $channel . $chunkData]; + } + } + } + } + +} diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php new file mode 100644 index 0000000000..241cd5df9c --- /dev/null +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -0,0 +1,105 @@ +client = $client; + $this->root = $root; + } + + public function ls($parent = '/') { + $parent = $this->resolve_path($parent); + $tree = $this->get_files_list()->get_by_path($parent); + if(!$tree) { + return []; + } + return array_keys($tree['content']); + } + + public function is_dir($path) { + $path = $this->resolve_path($path); + $tree = $this->get_files_list()->get_by_path($path); + return isset($tree['type']) && $tree['type'] === WP_Git_Pack_Index::OBJECT_TYPE_TREE; + } + + public function is_file($path) { + $path = $this->resolve_path($path); + // We may not have the blob object yet, but we surely have the parent + // tree object. Instead of resolving the blob by its path, let's check + // if the requested file is in the parent tree. + $object = $this->get_files_list()->get_by_path(dirname($path)); + return ( + $object && + isset($object['type']) && + $object['type'] === WP_Git_Pack_Index::OBJECT_TYPE_TREE && + isset($object['content'][basename($path)]) + ); + } + + public function start_streaming_file($path) { + throw new Exception('Not implemented'); + } + + public function next_file_chunk() { + throw new Exception('Not implemented'); + } + + public function get_file_chunk() { + throw new Exception('Not implemented'); + } + + public function get_error_message() { + throw new Exception('Not implemented'); + } + + public function close_file_reader() { + throw new Exception('Not implemented'); + } + + public function read_file($path) { + $this->ensure_files_data(); + $path = $this->resolve_path($path); + $object = $this->get_files_list()->get_by_path($path); + if(!$object) { + return false; + } + return $object['content']; + } + + private function resolve_path($path) { + return wp_join_paths($this->root, $path); + } + + private function ensure_files_data() { + if(!$this->blobs_backfilled) { + $this->client->backfillBlobs( + $this->get_files_list(), + $this->root + ); + $this->blobs_backfilled = true; + } + } + + private function get_files_list() { + if(!$this->headRef) { + $refs = $this->client->fetchRefs('HEAD'); + if(!isset($refs['HEAD'])) { + throw new Exception('HEAD ref not found'); + } + $this->headRef = $refs['HEAD']; + } + if(!$this->files_list) { + $this->files_list = $this->client->list_objects($this->headRef); + } + return $this->files_list; + } + +} diff --git a/packages/playground/data-liberation/src/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php similarity index 56% rename from packages/playground/data-liberation/src/WP_Git_Filesystem.php rename to packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php index 24fdb45cef..251b0dab80 100644 --- a/packages/playground/data-liberation/src/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php @@ -1,6 +1,6 @@ 'commit', ]; - private $repoUrl; + private $objects = []; + private $by_oid = []; + private $external_get_by_oid = null; - public function __construct($repoUrl) { - $this->repoUrl = rtrim($repoUrl, '/'); + private function __construct( + $objects = [], + $by_oid = [] + ) { + $this->objects = $objects; + $this->by_oid = $by_oid; } - - public function fetchFiles($directory = '') { - $headRef = $this->fetchRefHash('HEAD'); - if (!$headRef) { - return false; - } - - $index = $this->list_objects($headRef); - if(false === $index) { - return false; - } - $commit_tree_sha = $index['objects'][$index['by_oid'][$headRef]]['tree']; - $commit_tree = $index['objects'][$index['by_oid'][$commit_tree_sha]]['content']; - $subdir = $index['objects'][$index['by_oid'][$commit_tree['wp-content']['sha1']]]['content']; - $subdir = $index['objects'][$index['by_oid'][$subdir['html-pages']['sha1']]]['content']; - $subdir = $index['objects'][$index['by_oid'][$subdir['0_wordpress-playground']['sha1']]]['content']; - - $refs = []; - foreach($subdir as $file) { - $refs[] = $file['sha1']; - } - - $blobs = $this->fetch_blobs($refs); - var_dump($blobs); - var_dump($refs); - die(); - - return $index; + public function set_external_get_by_oid($external_get_by_oid) { + $this->external_get_by_oid = $external_get_by_oid; } - private function fetchRefHash($ref_name) { - // Fetch HEAD ref - $url = $this->repoUrl . '/git-upload-pack'; - $response = $this->http_request( - $url, - $this->encode_packet_line("command=ls-refs\n") . - $this->encode_packet_line("agent=git/2.37.3\n") . - $this->encode_packet_line("object-format=sha1\n") . - "0001" . - $this->encode_packet_line("peel\n") . - $this->encode_packet_line("ref-prefix $ref_name\n") . - "0000", - [ - 'Accept: application/x-git-upload-pack-advertisement', - 'Content-Type: application/x-git-upload-pack-request', - 'Git-Protocol: version=2' - ] - ); - - if (!$response) { - return false; + public function get_by_oid($oid) { + if(isset($this->by_oid[$oid])) { + return $this->objects[$this->by_oid[$oid]]; + } + if($this->external_get_by_oid) { + $factory = $this->external_get_by_oid; + return $factory($oid); } + return null; + } - $ref_hash = null; - foreach ($this->parse_git_protocol_v2_packets($response) as $frame) { - if (strpos($frame, $ref_name) !== false) { - $ref_hash = trim(explode(' ', $frame)[0]); - break; + public function get_by_path($path, $root_tree_oid=null) { + if($root_tree_oid === null) { + foreach($this->objects as $object) { + if($object['type'] === self::OBJECT_TYPE_COMMIT) { + $root_tree_oid = $object['tree']; + break; + } } } - - if (!$ref_hash) { - return false; + $current_tree = $this->get_by_oid($root_tree_oid); + if (!$current_tree) { + return null; } - return $ref_hash; - } - - private function parse_git_protocol_v2_packets($bytes) { - $offset = 0; - while ($offset < strlen($bytes)) { - $length = hexdec(substr($bytes, $offset, 4)); - $offset += 4; - $frame = substr($bytes, $offset, $length); - yield $frame; - $offset += $length; + if($current_tree['type'] === self::OBJECT_TYPE_COMMIT) { + $current_tree = $this->get_by_oid($current_tree['tree']); } - } - - private function list_objects($ref_hash) { - $body = - $this->encode_packet_line("want {$ref_hash} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3 filter\n") . - $this->encode_packet_line("filter blob:none\n") . - $this->encode_packet_line("shallow {$ref_hash}\n") . - $this->encode_packet_line("deepen 1\n") . - "0000" . - $this->encode_packet_line("done\n") . - $this->encode_packet_line("done\n"); - - $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ - 'Accept: application/x-git-upload-pack-advertisement', - 'Content-Type: application/x-git-upload-pack-request', - ]); - - $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); - return $this->compute_pack_index($pack_data); - } - private function fetch_blobs($refs) { - $body = ''; - foreach($refs as $ref) { - $body .= $this->encode_packet_line("want {$ref} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3\n"); + $path = trim($path, '/'); + if (empty($path)) { + return $current_tree; } - $body .= "0000"; - $body .= $this->encode_packet_line("done\n"); - - $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ - 'Accept: application/x-git-upload-pack-advertisement', - 'Content-Type: application/x-git-upload-pack-request', - ]); - $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); - return $this->compute_pack_index($pack_data); - } - - private function http_request($url, $postData = null, $headers = []) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - if ($postData) { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $path_segments = explode('/', $path); + foreach ($path_segments as $segment) { + if (!isset($current_tree['content'][$segment])) { + return null; + } + $next_oid = $current_tree['content'][$segment]['sha1']; + $current_tree = $this->get_by_oid($next_oid); + if (!$current_tree) { + return null; + } } - $response = curl_exec($ch); - curl_close($ch); - - return $response; + return $current_tree; } - private function encode_packet_line($data) { - $length = strlen($data) + 4; - return str_pad(dechex($length), 4, '0', STR_PAD_LEFT) . $data; - } - - private function accumulate_pack_data_from_multiplexed_chunks($raw_response) { - $parsed_pack_data = []; - $parsed_chunks = $this->parse_multiplexed_pack_data($raw_response); - foreach($parsed_chunks as $chunk) { - if($chunk['type'] !== 'side-band') { - continue; + public function get_descendants($tree_oid) { + $tree = $this->get_by_oid($tree_oid); + if (!$tree || !isset($tree['content'])) { + return []; + } + foreach ($tree['content'] as $name => $object) { + if ($object['mode'] === self::FILE_MODE_DIRECTORY) { + yield from $this->get_descendants($object['sha1']); + } else { + yield $object; } - $parsed_pack_data[] = $chunk['data']; } - return implode('', $parsed_pack_data); } - private function parse_multiplexed_pack_data($bytes) { - $offset = 0; - while ($offset < strlen($bytes)) { - $lengthHex = substr($bytes, $offset, 4); - $offset += 4; - - if ($lengthHex === "0000") { - continue; // End of packet - } - - $length = hexdec($lengthHex); - if ($length === 0) { - break; // No more data - } - - // This is one raw packet line - $content = substr($bytes, $offset, $length - 4); - $offset += $length - 4; - - // Parse possible multiple side-band chunks inside this single packet line - $subOffset = 0; - while ($subOffset < strlen($content)) { - $channel = $content[$subOffset]; - $subOffset++; - if ($subOffset >= strlen($content)) { - break; // No data left after channel byte - } - - // We'll assume the rest of this line is the sub-chunk’s data - // (Git typically sends only one sub-chunk per packet line, but - // if there were multiple, you’d keep parsing until subOffset hits the end) - $chunkData = substr($content, $subOffset); - $subOffset = strlen($content); - - if ($channel === "\x01") { - yield ['type' => 'side-band', 'data' => $chunkData]; - } elseif ($channel === "\x02") { - yield ['type' => 'progress', 'data' => $chunkData]; - } elseif ($channel === "\x03") { - yield ['type' => 'fatal', 'data' => $chunkData]; - } else { - yield ['type' => 'unknown', 'data' => $channel . $chunkData]; - } - } + public function get_descendants_tree($tree_oid) { + $tree = $this->get_by_oid($tree_oid); + if (!$tree || !isset($tree['content'])) { + return []; } - } - private function parse_pack_data($packData) { - $offset = 0; - - // Basic sanity checks - if (strlen($packData) < 12) { - return false; - } - - $header = substr($packData, $offset, 4); - $offset += 4; - if ($header !== "PACK") { - return false; - } - - $version = unpack('N', substr($packData, $offset, 4))[1]; - $offset += 4; - - $objectCount = unpack('N', substr($packData, $offset, 4))[1]; - $offset += 4; - - $objects = []; - - for ($i = 0; $i < $objectCount; $i++) { - if ($offset >= strlen($packData)) { - break; + $descendants = []; + foreach ($tree['content'] as $name => $object) { + if ($object['mode'] === self::FILE_MODE_DIRECTORY) { + $descendants[$name] = $this->get_descendants_tree($object['sha1']); + } else { + $blob = $this->get_by_oid($object['sha1']); + $descendants[$name] = isset($blob['content']) ? $blob['content'] : null; } - - $header_offset = $offset; - $object = $this->parse_pack_header($packData, $offset); - $object['type_name'] = self::OBJECT_NAMES[$object['type']]; - $object['content_offset'] = $offset; - $object['header_offset'] = $header_offset; - $object['content'] = $this->inflate_object($packData, $offset, $object['length']); - $object['compressed_length'] = $offset - $object['content_offset']; - $objects[] = $object; } - return [ - 'objects' => $objects, - 'total_objects' => $objectCount, - 'pack_version' => $version - ]; + + return $descendants; } - private function compute_pack_index($pack_data) { - $parsed_pack = $this->parse_pack_data($pack_data); + static public function from_pack_data($pack_data) { + $parsed_pack = self::parse_pack_data($pack_data); $objects = $parsed_pack['objects']; $by_oid = []; @@ -306,7 +159,7 @@ private function compute_pack_index($pack_data) { } // TODO: Make sure the base object will never be another delta. $base = $objects[$by_offset[$target_offset]]; - $objects[$i]['content'] = $this->applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); $objects[$i]['type'] = $base['type']; $objects[$i]['type_name'] = $base['type_name']; } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { @@ -314,11 +167,11 @@ private function compute_pack_index($pack_data) { continue; } $base = $objects[$by_oid[$objects[$i]['reference']]]; - $objects[$i]['content'] = $this->applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); $objects[$i]['type'] = $base['type']; $objects[$i]['type_name'] = $base['type_name']; } - $oid = sha1($this->wrap_git_object($objects[$i]['type_name'], $objects[$i]['content'])); + $oid = sha1(self::wrap_git_object($objects[$i]['type_name'], $objects[$i]['content'])); $objects[$i]['oid'] = $oid; $by_oid[$oid] = $i; $by_offset[$objects[$i]['header_offset']] = $i; @@ -333,117 +186,63 @@ private function compute_pack_index($pack_data) { // Resolve trees foreach($objects as $k => $object) { if( $object['type'] === self::OBJECT_TYPE_TREE ) { - $objects[$k]['content'] = $this->parse_tree_bytes($object['content']); + $objects[$k]['content'] = self::parse_tree_bytes($object['content']); } else if($object['type'] === self::OBJECT_TYPE_COMMIT) { $objects[$k]['tree'] = substr($object['content'], 5, 40); } } - return [ - 'objects' => $objects, - 'by_oid' => $by_oid, - 'by_offset' => $by_offset, - ]; + return new WP_Git_Pack_Index( + $objects, + $by_oid + ); } - private function wrap_git_object($type, $object) { + static private function wrap_git_object($type, $object) { $length = strlen($object); return "$type $length\x00" . $object; } - private function parse_pack_header(string $packData, int &$offset) { - // Object type is encoded in bits 654 - $byte = ord($packData[$offset++]); - $type = ($byte >> 4) & 0b111; - // The length encoding get complicated. - // Last four bits of length is encoded in bits 3210 - $length = $byte & 0b1111; - // Whether the next byte is part of the variable-length encoded number - // is encoded in bit 7 - if ($byte & 0b10000000) { - $shift = 4; - $byte = ord($packData[$offset++]); - while ($byte & 0b10000000) { - $length |= ($byte & 0b01111111) << $shift; - $shift += 7; - $byte = ord($packData[$offset++]); - } - $length |= ($byte & 0b01111111) << $shift; + static private function applyDelta($base_bytes, $delta_bytes) { + $offset = 0; + + $base_size = self::readVariableLength($delta_bytes, $offset); + if($base_size !== strlen($base_bytes)) { + // @TODO: Do not throw exceptions...? Or do? + throw new Exception('Base size mismatch'); } - // Handle deltified objects - $ofs = null; - $reference = null; - if ($type === self::OBJECT_TYPE_OFS_DELTA) { - // Git uses a specific formula: ofs = ((ofs + 1) << 7) + (c & 0x7f) - // for each continuation byte. The first byte doesn't do the "ofs+1" part. - // This code matches Git’s logic. - $ofs = 0; - // Read the first byte - $c = ord($packData[$offset++]); - $ofs = ($c & 0x7F); + $result_size = self::readVariableLength($delta_bytes, $offset); - // If bit 7 (0x80) is set, we keep reading - while ($c & 0x80) { - $c = ord($packData[$offset++]); - $ofs = (($ofs + 1) << 7) + ($c & 0x7F); + $result = ''; + while ($offset < strlen($delta_bytes)) { + $byte = ord($delta_bytes[$offset++]); + if ($byte & 0x80) { + $copyOffset = 0; + $copySize = 0; + if ($byte & 0x01) $copyOffset |= ord($delta_bytes[$offset++]); + if ($byte & 0x02) $copyOffset |= ord($delta_bytes[$offset++]) << 8; + if ($byte & 0x04) $copyOffset |= ord($delta_bytes[$offset++]) << 16; + if ($byte & 0x08) $copyOffset |= ord($delta_bytes[$offset++]) << 24; + if ($byte & 0x10) $copySize |= ord($delta_bytes[$offset++]); + if ($byte & 0x20) $copySize |= ord($delta_bytes[$offset++]) << 8; + if ($byte & 0x40) $copySize |= ord($delta_bytes[$offset++]) << 16; + if ($copySize === 0) $copySize = 0x10000; + $result .= substr($base_bytes, $copyOffset, $copySize); + } else { + $result .= substr($delta_bytes, $offset, $byte); + $offset += $byte; } - } else if ($type === self::OBJECT_TYPE_REF_DELTA) { - $reference = substr($packData, $offset, 20); - $offset += 20; } - return [ - 'ofs' => $ofs, - 'type' => $type, - 'length' => $length, - 'reference' => $reference - ]; - } - /** - * Incrementally inflate the next object’s compressed data until it yields - * $uncompressedSize bytes, or we hit the end of the compressed stream. - * Adjusts $offset so that after returning, $offset points to the next object header. - */ - private function inflate_object(string $packData, int &$offset, int $uncompressedSize): ?string { - $inflateContext = inflate_init(ZLIB_ENCODING_DEFLATE); - if (!$inflateContext) { - return null; + if(strlen($result) !== $result_size) { + // @TODO: Do not throw exceptions...? Or do? + throw new Exception('Result size mismatch'); } - - $inflated = ''; - $packLen = strlen($packData); - - $bytes_read = 0; - while ($offset < $packLen) { - // Feed chunks into inflate. We don’t know how big each chunk is, - // so let's just pick something arbitrary: - $chunk = substr($packData, $offset, 256); - - $res = inflate_add($inflateContext, $chunk); - switch(inflate_get_status($inflateContext)) { - case ZLIB_BUF_ERROR: - case ZLIB_DATA_ERROR: - case ZLIB_VERSION_ERROR: - case ZLIB_MEM_ERROR: - throw new Exception('Inflate error'); - } - if ($res === false) { - throw new Exception('Inflate error'); - } - $bytes_read_for_this_chunk = inflate_get_read_len($inflateContext) - $bytes_read; - $offset += $bytes_read_for_this_chunk; - $bytes_read = inflate_get_read_len($inflateContext); - $inflated .= $res; - - if(inflate_get_status($inflateContext) === ZLIB_STREAM_END) { - break; - } - } - return $inflated; - } - - private function parse_tree_bytes($treeContent) { + return $result; + } + + static private function parse_tree_bytes($treeContent) { $offset = 0; $files = []; @@ -501,46 +300,7 @@ private function parse_tree_bytes($treeContent) { return $files; } - private function applyDelta($base_bytes, $delta_bytes) { - $offset = 0; - - $base_size = $this->readVariableLength($delta_bytes, $offset); - if($base_size !== strlen($base_bytes)) { - // @TODO: Do not throw exceptions...? Or do? - throw new Exception('Base size mismatch'); - } - $result_size = $this->readVariableLength($delta_bytes, $offset); - - $result = ''; - while ($offset < strlen($delta_bytes)) { - $byte = ord($delta_bytes[$offset++]); - if ($byte & 0x80) { - $copyOffset = 0; - $copySize = 0; - if ($byte & 0x01) $copyOffset |= ord($delta_bytes[$offset++]); - if ($byte & 0x02) $copyOffset |= ord($delta_bytes[$offset++]) << 8; - if ($byte & 0x04) $copyOffset |= ord($delta_bytes[$offset++]) << 16; - if ($byte & 0x08) $copyOffset |= ord($delta_bytes[$offset++]) << 24; - if ($byte & 0x10) $copySize |= ord($delta_bytes[$offset++]); - if ($byte & 0x20) $copySize |= ord($delta_bytes[$offset++]) << 8; - if ($byte & 0x40) $copySize |= ord($delta_bytes[$offset++]) << 16; - if ($copySize === 0) $copySize = 0x10000; - $result .= substr($base_bytes, $copyOffset, $copySize); - } else { - $result .= substr($delta_bytes, $offset, $byte); - $offset += $byte; - } - } - - if(strlen($result) !== $result_size) { - // @TODO: Do not throw exceptions...? Or do? - throw new Exception('Result size mismatch'); - } - - return $result; - } - - private function readVariableLength($data, &$offset) { + static private function readVariableLength($data, &$offset) { $result = 0; $shift = 0; do { @@ -550,32 +310,140 @@ private function readVariableLength($data, &$offset) { } while ($byte & 0x80); return $result; } -} + static private function parse_pack_data($packData) { + $offset = 0; + + // Basic sanity checks + if (strlen($packData) < 12) { + return false; + } + + $header = substr($packData, $offset, 4); + $offset += 4; + if ($header !== "PACK") { + return false; + } + + $version = unpack('N', substr($packData, $offset, 4))[1]; + $offset += 4; + + $objectCount = unpack('N', substr($packData, $offset, 4))[1]; + $offset += 4; + + $objects = []; + + for ($i = 0; $i < $objectCount; $i++) { + if ($offset >= strlen($packData)) { + break; + } -// $client = new WP_Git_Index('https://github.com/WordPress/gutenberg.git'); -$client = new WP_Git_Index('https://github.com/adamziel/playground-docs-workflow.git'); -$files = $client->fetchFiles(); + $header_offset = $offset; + $object = self::parse_pack_header($packData, $offset); + $object['type_name'] = self::OBJECT_NAMES[$object['type']]; + $object['content_offset'] = $offset; + $object['header_offset'] = $header_offset; + $object['content'] = self::inflate_object($packData, $offset, $object['length']); + $object['compressed_length'] = $offset - $object['content_offset']; + $objects[] = $object; + } + return [ + 'objects' => $objects, + 'total_objects' => $objectCount, + 'pack_version' => $version + ]; + } -if ($files === false) { - echo "Failed to fetch repository files.\n"; - exit; -} + /** + * Incrementally inflate the next object’s compressed data until it yields + * $uncompressedSize bytes, or we hit the end of the compressed stream. + * Adjusts $offset so that after returning, $offset points to the next object header. + */ + static private function inflate_object(string $packData, int &$offset, int $uncompressedSize): ?string { + $inflateContext = inflate_init(ZLIB_ENCODING_DEFLATE); + if (!$inflateContext) { + return null; + } + + $inflated = ''; + $packLen = strlen($packData); + + $bytes_read = 0; + while ($offset < $packLen) { + // Feed chunks into inflate. We don’t know how big each chunk is, + // so let's just pick something arbitrary: + $chunk = substr($packData, $offset, 256); + + $res = inflate_add($inflateContext, $chunk); + switch(inflate_get_status($inflateContext)) { + case ZLIB_BUF_ERROR: + case ZLIB_DATA_ERROR: + case ZLIB_VERSION_ERROR: + case ZLIB_MEM_ERROR: + throw new Exception('Inflate error'); + } + if ($res === false) { + throw new Exception('Inflate error'); + } + $bytes_read_for_this_chunk = inflate_get_read_len($inflateContext) - $bytes_read; + $offset += $bytes_read_for_this_chunk; -// Filter for the `/docs` directory -$docsDirectory = []; -foreach ($files as $path => $content) { - if (strpos($path, 'docs/') === 0) { - $docsDirectory[$path] = $content; + $bytes_read = inflate_get_read_len($inflateContext); + $inflated .= $res; + + if(inflate_get_status($inflateContext) === ZLIB_STREAM_END) { + break; + } + } + return $inflated; } -} -// Display the files in the `/docs` directory -if (!empty($docsDirectory)) { - foreach ($docsDirectory as $filePath => $fileContent) { - echo "File: $filePath\n"; - echo "Content:\n$fileContent\n\n"; + static private function parse_pack_header(string $packData, int &$offset) { + // Object type is encoded in bits 654 + $byte = ord($packData[$offset++]); + $type = ($byte >> 4) & 0b111; + // The length encoding get complicated. + // Last four bits of length is encoded in bits 3210 + $length = $byte & 0b1111; + // Whether the next byte is part of the variable-length encoded number + // is encoded in bit 7 + if ($byte & 0b10000000) { + $shift = 4; + $byte = ord($packData[$offset++]); + while ($byte & 0b10000000) { + $length |= ($byte & 0b01111111) << $shift; + $shift += 7; + $byte = ord($packData[$offset++]); + } + $length |= ($byte & 0b01111111) << $shift; + } + // Handle deltified objects + $ofs = null; + $reference = null; + if ($type === self::OBJECT_TYPE_OFS_DELTA) { + // Git uses a specific formula: ofs = ((ofs + 1) << 7) + (c & 0x7f) + // for each continuation byte. The first byte doesn't do the "ofs+1" part. + // This code matches Git’s logic. + $ofs = 0; + // Read the first byte + $c = ord($packData[$offset++]); + $ofs = ($c & 0x7F); + + // If bit 7 (0x80) is set, we keep reading + while ($c & 0x80) { + $c = ord($packData[$offset++]); + $ofs = (($ofs + 1) << 7) + ($c & 0x7F); + } + } else if ($type === self::OBJECT_TYPE_REF_DELTA) { + $reference = substr($packData, $offset, 20); + $offset += 20; + } + return [ + 'ofs' => $ofs, + 'type' => $type, + 'length' => $length, + 'reference' => $reference + ]; } -} else { - echo "No files found in the /docs directory.\n"; -} \ No newline at end of file + +} \ No newline at end of file From 4d10e75e11529bce27cff2ce71d4c5359f2eca0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 12:36:56 +0100 Subject: [PATCH 08/71] Prototype git push --- .../push-simple.php | 528 ++++++++++++++++++ .../data-liberation/src/git/WP_Git_Client.php | 2 +- .../src/git/WP_Git_Pack_Index.php | 30 +- 3 files changed, 545 insertions(+), 15 deletions(-) create mode 100644 packages/playground/data-liberation-static-files-editor/push-simple.php diff --git a/packages/playground/data-liberation-static-files-editor/push-simple.php b/packages/playground/data-liberation-static-files-editor/push-simple.php new file mode 100644 index 0000000000..9e5a9e07b4 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/push-simple.php @@ -0,0 +1,528 @@ + 9]); + return deflate_add($context, $content, ZLIB_FINISH); +} + +class WP_Git_Pack_Encoder { + + // Helper: Build a barebones pack with the given objects (no compression). + // Objects must be in an order that satisfies dependencies if you skip deltas. + static public function build_pack(array $objects): string { + // PACK header: 4-byte signature, 4-byte version=2, 4-byte object count + $pack = "PACK"; + $pack .= pack("N", 2); // version + $pack .= pack("N", count($objects)); // number of objects + + foreach ($objects as $obj) { + if( + $obj['type'] === WP_Git_Pack_Index::OBJECT_TYPE_TREE && + is_array($obj['content']) + ) { + $obj['content'] = WP_Git_Pack_Encoder::encode_tree_bytes($obj['content']); + } + $pack .= self::object_header($obj['type'], strlen($obj['content'])); + $pack .= deflate_raw($obj['content']); + } + + // Then append a 20-byte trailing pack checksum (SHA1 of all preceding bytes). + $packSha = sha1($pack, true); + return $pack . $packSha; + } + + static public function object_header(int $type, int $size): string { + // First byte: type in bits 4-6, size bits 0-3 + $firstByte = $size & 0b1111; + $firstByte |= ($type & 0b111) << 4; + // Continuation bit 7 if needed + if($size > 15 ) { + $firstByte |= 0b10000000; + } + + // Get remaining size bits after first 4 bits + $remainingSize = $size >> 4; + + // Build result starting with first byte + $result = chr($firstByte); + // Add continuation bytes if needed + while ($remainingSize > 0) { + // Set continuation bit if we have more bytes + $byte = $remainingSize & 0b01111111; + $remainingSize >>= 7; + if($remainingSize > 0) { + $byte |= 0b10000000; + } + + $result .= chr($byte); + } + + return $result; + } + + /** + * @param $tree array{ + * array { + * $mode: string, + * $name: string, + * $sha1: string, + * } + * } + */ + static public function encode_tree_bytes($tree) { + $tree_bytes = ''; + foreach ($tree as $value) { + $tree_bytes .= $value['mode'] . " " . $value['name'] . "\0" . hex2bin($value['sha1']); + } + return $tree_bytes; + } + + static public function create_object($basic_data) { + $object = [ + 'ofs' => 0, + 'type' => $basic_data['type'], + 'reference' => null, + 'header_offset' => 0, + ]; + switch($basic_data['type']) { + case WP_Git_Pack_Index::OBJECT_TYPE_BLOB: + $object['content'] = $basic_data['content']; + $object['oid'] = sha1(self::wrap_object($basic_data['type'], $basic_data['content'])); + break; + case WP_Git_Pack_Index::OBJECT_TYPE_TREE: + $object['content'] = []; + foreach($basic_data['content'] as $value) { + $object['content'][$value['name']] = $value; + } + ksort($object['content']); + $encoded_bytes = self::encode_tree_bytes($object['content']); + $object['oid'] = sha1(self::wrap_object($basic_data['type'], $encoded_bytes)); + break; + case WP_Git_Pack_Index::OBJECT_TYPE_COMMIT: + $object['content'] = $basic_data['content']; + $object['tree'] = $basic_data['tree']; + $object['oid'] = sha1(self::wrap_object($basic_data['type'], $basic_data['content'])); + break; + } + return $object; + } + + static public function wrap_object($type, $object) { + $length = strlen($object); + $type_name = WP_Git_Pack_Index::OBJECT_NAMES[$type]; + return "$type_name $length\x00" . $object; + } + + // Helper: encode a single pkt-line + static public function encode_packet_line(string $payload): string { + $length = strlen($payload) + 4; + return sprintf("%04x", $length) . $payload; + } + + // Helper: produce a flush packet (0000) + static public function encode_flush(): string { + return "0000"; + } + +} + +// Very naive GET +function httpGet(string $url): ?string { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + $result = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($err) { + return null; + } + return $result; +} + +// Very naive POST +function httpPost(string $url, string $data, array $headers = []): ?string { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $hdr = []; + foreach ($headers as $h) { + $hdr[] = $h; + } + // We also need "Git-Protocol: version=2" for some servers, or not, depending on the server + // $hdr[] = 'Git-Protocol: version=2'; + + curl_setopt($ch, CURLOPT_HTTPHEADER, $hdr); + + $result = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($err) { + return null; + } + return $result; +} + + +/** + * Represents a set of changes to be applied to a data store. + */ +class WP_Changeset { + /** + * Created files. + * @var array + */ + public $create; + + /** + * Updated files. + * @var array + */ + public $update; + + /** + * Deleted files. + * @var array + */ + public $delete; + + public function __construct($create = [], $update = [], $delete = []) { + $this->create = $create; + $this->update = $update; + $this->delete = $delete; + } +}; + +class WP_Git_Utils { + + private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; + + /** + * Computes Git objects needed to commit a changeset. + * + * @param WP_Git_Pack_Index $oldIndex The index containing existing objects + * @param WP_Changeset $changeset The changes to commit + * @return string The Git objects with type, content and SHA + */ + public function compute_push_objects( + WP_Git_Pack_Index $index, + WP_Changeset $changeset, + $branchName, + $parent_hash = null + ) { + $new_index = []; + + $new_tree = new stdClass(); + foreach (array_merge($changeset->create, $changeset->update) as $path => $content) { + $new_blob = WP_Git_Pack_Encoder::create_object([ + 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, + 'content' => $content, + ]); + $new_index[] = $new_blob; + $this->set_oid($new_tree, $path, $new_blob['oid']); + } + + foreach ($changeset->delete as $path) { + $this->set_oid($new_tree, $path, self::DELETE_PLACEHOLDER); + } + + if(!$parent_hash) { + $parent_hash = "0000000000000000000000000000000000000000"; + } + $parent = ''; + if($parent_hash) { + $parent = "parent $parent_hash\n"; + } + $root_tree = $this->backfill_trees($index, $new_index, $new_tree, '/'); + $commit = WP_Git_Pack_Encoder::create_object([ + 'type' => WP_Git_Pack_Index::OBJECT_TYPE_COMMIT, + 'content' => sprintf( + "tree %s\n{$parent}author %s\ncommitter %s\n\n%s\n", + $root_tree['oid'], + "John Doe " . time() . " +0000", + "John Doe " . time() . " +0000", + "Hello!" + ), + 'tree' => $root_tree['oid'], + ]); + $commitSha = $commit['oid']; + $new_index[] = $commit; + // Make $new_index unique by 'oid' column + $seen_oids = []; + $new_index = array_filter($new_index, function($obj) use (&$seen_oids) { + if (isset($seen_oids[$obj['oid']])) { + return false; + } + $seen_oids[$obj['oid']] = true; + return true; + }); + // $new_index = array_reverse($new_index); + + var_dump($new_index); + + $pktPush = WP_Git_Pack_Encoder::encode_packet_line("$parent_hash $commitSha refs/heads/$branchName\0report-status force-update\n"); + $pktPush .= WP_Git_Pack_Encoder::encode_flush(); // "0000" + $pktPush .= WP_Git_Pack_Encoder::build_pack($new_index); + $pktPush .= WP_Git_Pack_Encoder::encode_flush(); // "0000" + + return $pktPush; + } + + private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $subtree_delta, $subtree_path = '/') { + $subtree_path = ltrim($subtree_path, '/'); + $new_tree_content = []; + + $indexed_tree = $current_index->get_by_path($subtree_path); + if($indexed_tree) { + foreach($indexed_tree['content'] as $object) { + // Backfill the unchanged objects from the currently indexed subtree. + $name = $object['name']; + if(!isset($subtree_delta->children[$name])) { + $new_tree_content[$name] = $object; + } + } + } + + // Index changed and new objects in the current subtree. + foreach($subtree_delta->children as $name => $subtree_child) { + // Ignore any deleted objects. + if(isset($subtree_child->oid) && $subtree_child->oid === self::DELETE_PLACEHOLDER) { + continue; + } + + // Index blobs + switch($subtree_child->type) { + case WP_Git_Pack_Index::OBJECT_TYPE_BLOB: + $new_tree_content[$name] = [ + 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'name' => $name, + 'sha1' => $subtree_child->oid, + ]; + break; + case WP_Git_Pack_Index::OBJECT_TYPE_TREE: + $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); + $new_tree_content[$name] = [ + 'mode' => WP_Git_Pack_Index::FILE_MODE_DIRECTORY, + 'name' => $name, + 'sha1' => $subtree_object['oid'], + ]; + break; + } + } + + $new_tree_object = WP_Git_Pack_Encoder::create_object([ + 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, + 'content' => $new_tree_content, + ]); + + $new_index[] = $new_tree_object; + return $new_tree_object; + } + + private function set_oid($root_tree, $path, $oid) { + $blob = new stdClass(); + $blob->type = WP_Git_Pack_Index::OBJECT_TYPE_BLOB; + $blob->oid = $oid; + + $subtree_path = dirname($path); + if($subtree_path === '.') { + $subtree = $root_tree; + } else { + $subtree = $this->get_subtree($root_tree, $subtree_path); + } + $filename = basename($path); + $subtree->children[$filename] = $blob; + } + + private function get_subtree($root_tree, $path) { + $path = trim($path, '/'); + $segments = explode('/', $path); + $subtree = $root_tree; + foreach ($segments as $segment) { + if (!isset($subtree->children[$segment])) { + $new_subtree = new stdClass(); + $new_subtree->type = WP_Git_Pack_Index::OBJECT_TYPE_TREE; + $new_subtree->children = []; + $subtree->children[$segment] = $new_subtree; + } + $subtree = $subtree->children[$segment]; + } + return $subtree; + } + +} + +$client = new WP_Git_Client("https://github.com/adamziel/pantheon-playground-demo.git"); +$head_hash = $client->fetchRefs('HEAD')['HEAD']; +$index = $client->list_objects($head_hash); + +$utils = new WP_Git_Utils(); +$push_bytes = $utils->compute_push_objects( + $index, + new WP_Changeset( + [ + // Weird! This only works if these paths are sorted alphabetically. Do I have to sort pack data in a particular order? + // Perhaps topological sort matters? + // Also, the content of each file must be differnet. I suspect Github rejects duplicate blobs? + 'wp-content/plugin/light-mode-another-two.css' => '/* Light uu mode */', + // 'wp-content/dark-mode.css' => '/* Dark mode */', + // 'wp-admin/tzeme/dark-mode3.css' => '/* Dark mode xx */', + // 'wp-content/azeme/dark-mode4.css' => '/* Dark mode xxx */', + // 'wp-content/ztra/dark-mode.css' => '/* differddent */', + ], + [], + [ + 'wp-content/dark-mode.css', + '0', + '1' + ] + ), + 'main', + $head_hash +); + +// $pack_data = substr($push_bytes, strpos($push_bytes, "PACK")); +// $new_index = WP_Git_Pack_Index::from_pack_data($pack_data); +// var_dump($new_index); + +// die('......'); +$repoUrl = "https://" . GITHUB_TOKEN . "@github.com/adamziel/pantheon-playground-demo.git"; + +// 6) POST this to "/git-receive-pack" +$url = rtrim($repoUrl, '.git').'.git/git-receive-pack'; +$response = httpPost($url, $push_bytes, [ + 'Content-Type: application/x-git-receive-pack-request', + 'Accept: application/x-git-receive-pack-result', +]); + +// 7) Check the response. If it contains "unpack ok" and "ok refs/heads/my-playground", +// we succeeded. Otherwise, there’s an error message in side-band or progress +// channels. We’ll just echo it here for debugging. +echo "Push response:\n$response\n"; +die(); + +// ----------------------------------------------------------------------------- +// Example usage +try { + // Use authorization header instead of basic auth in URL for better security + pushSingleFileOverHttp( + "https://" . GITHUB_TOKEN . "@github.com/adamziel/pantheon-playground-demo.git", + "my-playground", + // Use 40 zeros to create a new branch + // "0000000000000000000000000000000000000000" + "b7a8ab2605e6d7fbec140cba3d28e94d31d2a6aa" + // "main", + // "a9abffe7d324a4b9343eaad3f5e4b35c584448b6" + ); +} catch (Exception $e) { + echo "Error: ", $e->getMessage(), "\n"; +} + + + +//-------- + + +// $readme_md = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, +// 'content' => '## This is a Readme file', +// ]); +// $index_html = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, +// 'content' => '

Hello, world!

', +// ]); +// $style_css = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, +// 'content' => 'body { background-color: red; }', +// ]); + +// $html_pages = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'content' => [ +// [ +// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'name' => 'readme.md', +// 'sha1' => $readme_md['oid'], +// ], +// [ +// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'name' => 'index.html', +// 'sha1' => $index_html['oid'], +// ], +// ], +// ]); + +// $theme_tree = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'content' => [ +// [ +// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'name' => 'style.css', +// 'sha1' => $style_css['oid'], +// ], +// ], +// ]); + +// $wp_content_tree = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'content' => [ +// [ +// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'name' => 'html-pages', +// 'sha1' => $html_pages['oid'], +// ], +// [ +// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'name' => 'theme', +// 'sha1' => $theme_tree['oid'], +// ], +// ], +// ]); + +// $root_tree = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'content' => [ +// [ +// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'name' => 'wp-content', +// 'sha1' => $wp_content_tree['oid'], +// ], +// ], +// ]); + +// $commit = create_object([ +// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_COMMIT, +// 'content' => 'Hello, world!', +// 'tree' => $root_tree['oid'], +// ]); + + +// $push_bytes = $utils->compute_push_objects( +// new WP_Git_Pack_Index([ +// $commit, +// $root_tree, +// $wp_content_tree, +// $html_pages, +// $theme_tree, +// $style_css, +// $index_html, +// $readme_md, +// ]), +// new WP_Changeset( +// [ +// 'wp-content/theme/light-mode.css' => '/* Light mode */', +// 'wp-content/theme/dark-mode.css' => '/* Dark mode */', +// ] +// ), +// 'my-new-branch' +// ); + diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index c76064cbdd..1b8a9bca09 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -140,7 +140,7 @@ private function accumulate_pack_data_from_multiplexed_chunks($raw_response) { return implode('', $parsed_pack_data); } - private function parse_multiplexed_pack_data($bytes) { + static public function parse_multiplexed_pack_data($bytes) { $offset = 0; while ($offset < strlen($bytes)) { $lengthHex = substr($bytes, $offset, 4); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php index 251b0dab80..753aa18312 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php @@ -38,11 +38,17 @@ class WP_Git_Pack_Index { private $by_oid = []; private $external_get_by_oid = null; - private function __construct( + public function __construct( $objects = [], $by_oid = [] ) { $this->objects = $objects; + if($by_oid === []) { + $by_oid = []; + foreach($objects as $k => $object) { + $by_oid[$object['oid']] = $k; + } + } $this->by_oid = $by_oid; } @@ -161,7 +167,6 @@ static public function from_pack_data($pack_data) { $base = $objects[$by_offset[$target_offset]]; $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); $objects[$i]['type'] = $base['type']; - $objects[$i]['type_name'] = $base['type_name']; } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { if(!isset($by_oid[$objects[$i]['reference']])) { continue; @@ -169,9 +174,8 @@ static public function from_pack_data($pack_data) { $base = $objects[$by_oid[$objects[$i]['reference']]]; $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); $objects[$i]['type'] = $base['type']; - $objects[$i]['type_name'] = $base['type_name']; } - $oid = sha1(self::wrap_git_object($objects[$i]['type_name'], $objects[$i]['content'])); + $oid = sha1(self::wrap_git_object($objects[$i]['type'], $objects[$i]['content'])); $objects[$i]['oid'] = $oid; $by_oid[$oid] = $i; $by_offset[$objects[$i]['header_offset']] = $i; @@ -198,9 +202,10 @@ static public function from_pack_data($pack_data) { ); } - static private function wrap_git_object($type, $object) { + static public function wrap_git_object($type, $object) { $length = strlen($object); - return "$type $length\x00" . $object; + $type_name = self::OBJECT_NAMES[$type]; + return "$type_name $length\x00" . $object; } static private function applyDelta($base_bytes, $delta_bytes) { @@ -340,11 +345,8 @@ static private function parse_pack_data($packData) { $header_offset = $offset; $object = self::parse_pack_header($packData, $offset); - $object['type_name'] = self::OBJECT_NAMES[$object['type']]; - $object['content_offset'] = $offset; $object['header_offset'] = $header_offset; - $object['content'] = self::inflate_object($packData, $offset, $object['length']); - $object['compressed_length'] = $offset - $object['content_offset']; + $object['content'] = self::inflate_object($packData, $offset, $object['uncompressed_length']); $objects[] = $object; } return [ @@ -404,18 +406,18 @@ static private function parse_pack_header(string $packData, int &$offset) { $type = ($byte >> 4) & 0b111; // The length encoding get complicated. // Last four bits of length is encoded in bits 3210 - $length = $byte & 0b1111; + $uncompressed_length = $byte & 0b1111; // Whether the next byte is part of the variable-length encoded number // is encoded in bit 7 if ($byte & 0b10000000) { $shift = 4; $byte = ord($packData[$offset++]); while ($byte & 0b10000000) { - $length |= ($byte & 0b01111111) << $shift; + $uncompressed_length |= ($byte & 0b01111111) << $shift; $shift += 7; $byte = ord($packData[$offset++]); } - $length |= ($byte & 0b01111111) << $shift; + $uncompressed_length |= ($byte & 0b01111111) << $shift; } // Handle deltified objects $ofs = null; @@ -441,7 +443,7 @@ static private function parse_pack_header(string $packData, int &$offset) { return [ 'ofs' => $ofs, 'type' => $type, - 'length' => $length, + 'uncompressed_length' => $uncompressed_length, 'reference' => $reference ]; } From 45d7a88c0ca65997544eac7a4a7a5e8da5b0d2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 19:51:55 +0100 Subject: [PATCH 09/71] Put all pack-related methods in WP_Git_Pack_Processor --- .../push-simple.php | 455 +++++++++++++++--- 1 file changed, 397 insertions(+), 58 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/push-simple.php b/packages/playground/data-liberation-static-files-editor/push-simple.php index 9e5a9e07b4..940786efb8 100644 --- a/packages/playground/data-liberation-static-files-editor/push-simple.php +++ b/packages/playground/data-liberation-static-files-editor/push-simple.php @@ -4,19 +4,48 @@ * containing "WordPress Playground is cool", over Git’s HTTP smart protocol. */ +use WordPress\AsyncHttp\Client; + require_once __DIR__ . '/../data-liberation/bootstrap.php'; require_once __DIR__ . '/secrets.php'; -function deflate_raw(string $content): string { - $context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 9]); - return deflate_add($context, $content, ZLIB_FINISH); -} - -class WP_Git_Pack_Encoder { +class WP_Git_Pack_Processor { + + const OBJECT_TYPE_COMMIT = 1; + const OBJECT_TYPE_TREE = 2; + const OBJECT_TYPE_BLOB = 3; + const OBJECT_TYPE_TAG = 4; + const OBJECT_TYPE_RESERVED = 5; + const OBJECT_TYPE_OFS_DELTA = 6; + const OBJECT_TYPE_REF_DELTA = 7; + + const OBJECT_NAMES = [ + self::OBJECT_TYPE_COMMIT => 'commit', + self::OBJECT_TYPE_TREE => 'tree', + self::OBJECT_TYPE_BLOB => 'blob', + self::OBJECT_TYPE_TAG => 'tag', + self::OBJECT_TYPE_RESERVED => 'reserved', + self::OBJECT_TYPE_OFS_DELTA => 'ofs_delta', + self::OBJECT_TYPE_REF_DELTA => 'ref_delta', + ]; + + const FILE_MODE_DIRECTORY = '040000'; + const FILE_MODE_REGULAR_NON_EXECUTABLE = '100644'; + const FILE_MODE_REGULAR_EXECUTABLE = '100755'; + const FILE_MODE_SYMBOLIC_LINK = '120000'; + const FILE_MODE_COMMIT = '160000'; + + const FILE_MODE_NAMES = [ + self::FILE_MODE_DIRECTORY => 'directory', + self::FILE_MODE_REGULAR_NON_EXECUTABLE => 'regular_non_executable', + self::FILE_MODE_REGULAR_EXECUTABLE => 'regular_executable', + self::FILE_MODE_SYMBOLIC_LINK => 'symbolic_link', + self::FILE_MODE_COMMIT => 'commit', + ]; // Helper: Build a barebones pack with the given objects (no compression). // Objects must be in an order that satisfies dependencies if you skip deltas. - static public function build_pack(array $objects): string { + static public function encode(array $objects): string { // PACK header: 4-byte signature, 4-byte version=2, 4-byte object count $pack = "PACK"; $pack .= pack("N", 2); // version @@ -24,13 +53,13 @@ static public function build_pack(array $objects): string { foreach ($objects as $obj) { if( - $obj['type'] === WP_Git_Pack_Index::OBJECT_TYPE_TREE && + $obj['type'] === WP_Git_Pack_Processor::OBJECT_TYPE_TREE && is_array($obj['content']) ) { - $obj['content'] = WP_Git_Pack_Encoder::encode_tree_bytes($obj['content']); + $obj['content'] = WP_Git_Pack_Processor::encode_tree_bytes($obj['content']); } $pack .= self::object_header($obj['type'], strlen($obj['content'])); - $pack .= deflate_raw($obj['content']); + $pack .= self::deflate($obj['content']); } // Then append a 20-byte trailing pack checksum (SHA1 of all preceding bytes). @@ -38,7 +67,12 @@ static public function build_pack(array $objects): string { return $pack . $packSha; } - static public function object_header(int $type, int $size): string { + static private function deflate(string $content): string { + $context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 9]); + return deflate_add($context, $content, ZLIB_FINISH); + } + + static private function object_header(int $type, int $size): string { // First byte: type in bits 4-6, size bits 0-3 $firstByte = $size & 0b1111; $firstByte |= ($type & 0b111) << 4; @@ -76,7 +110,7 @@ static public function object_header(int $type, int $size): string { * } * } */ - static public function encode_tree_bytes($tree) { + static private function encode_tree_bytes($tree) { $tree_bytes = ''; foreach ($tree as $value) { $tree_bytes .= $value['mode'] . " " . $value['name'] . "\0" . hex2bin($value['sha1']); @@ -92,11 +126,11 @@ static public function create_object($basic_data) { 'header_offset' => 0, ]; switch($basic_data['type']) { - case WP_Git_Pack_Index::OBJECT_TYPE_BLOB: + case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: $object['content'] = $basic_data['content']; $object['oid'] = sha1(self::wrap_object($basic_data['type'], $basic_data['content'])); break; - case WP_Git_Pack_Index::OBJECT_TYPE_TREE: + case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: $object['content'] = []; foreach($basic_data['content'] as $value) { $object['content'][$value['name']] = $value; @@ -105,7 +139,7 @@ static public function create_object($basic_data) { $encoded_bytes = self::encode_tree_bytes($object['content']); $object['oid'] = sha1(self::wrap_object($basic_data['type'], $encoded_bytes)); break; - case WP_Git_Pack_Index::OBJECT_TYPE_COMMIT: + case WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT: $object['content'] = $basic_data['content']; $object['tree'] = $basic_data['tree']; $object['oid'] = sha1(self::wrap_object($basic_data['type'], $basic_data['content'])); @@ -114,22 +148,331 @@ static public function create_object($basic_data) { return $object; } - static public function wrap_object($type, $object) { + static private function wrap_object($type, $object) { $length = strlen($object); - $type_name = WP_Git_Pack_Index::OBJECT_NAMES[$type]; + $type_name = WP_Git_Pack_Processor::OBJECT_NAMES[$type]; return "$type_name $length\x00" . $object; } - // Helper: encode a single pkt-line static public function encode_packet_line(string $payload): string { $length = strlen($payload) + 4; return sprintf("%04x", $length) . $payload; } - // Helper: produce a flush packet (0000) - static public function encode_flush(): string { + static private function encode_flush(): string { return "0000"; } + + + static public function decode($pack_bytes) { + $parsed_pack = self::parse_pack_data($pack_bytes); + $objects = $parsed_pack['objects']; + + $by_oid = []; + $by_offset = []; + $resolved_objects = 0; + // Index entities and resolve deltas + // Run until all objects are resolved + while($resolved_objects < count($objects)) { + $resolved_in_this_iteration = 0; + for($i = 0; $i < count($objects); $i++) { + // Skip already processed objects + if( + isset($by_offset[$objects[$i]['header_offset']]) && + isset($by_oid[$objects[$i]['oid']]) + ) { + continue; + } + + if($objects[$i]['type'] === self::OBJECT_TYPE_OFS_DELTA) { + $target_offset = $objects[$i]['header_offset'] - $objects[$i]['ofs']; + if(!isset($by_offset[$target_offset])) { + continue; + } + // TODO: Make sure the base object will never be another delta. + $base = $objects[$by_offset[$target_offset]]; + $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['type'] = $base['type']; + } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { + if(!isset($by_oid[$objects[$i]['reference']])) { + continue; + } + $base = $objects[$by_oid[$objects[$i]['reference']]]; + $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['type'] = $base['type']; + } + $oid = sha1(self::wrap_git_object($objects[$i]['type'], $objects[$i]['content'])); + $objects[$i]['oid'] = $oid; + $by_oid[$oid] = $i; + $by_offset[$objects[$i]['header_offset']] = $i; + ++$resolved_in_this_iteration; + ++$resolved_objects; + } + if($resolved_in_this_iteration === 0) { + throw new Exception('Could not resolve objects'); + } + } + + // Resolve trees + foreach($objects as $k => $object) { + if( $object['type'] === self::OBJECT_TYPE_TREE ) { + $objects[$k]['content'] = self::parse_tree_bytes($object['content']); + } else if($object['type'] === self::OBJECT_TYPE_COMMIT) { + $objects[$k]['tree'] = substr($object['content'], 5, 40); + } + } + + return new WP_Git_Pack_Index( + $objects, + $by_oid + ); + } + + static private function wrap_git_object($type, $object) { + $length = strlen($object); + $type_name = self::OBJECT_NAMES[$type]; + return "$type_name $length\x00" . $object; + } + + static private function applyDelta($base_bytes, $delta_bytes) { + $offset = 0; + + $base_size = self::readVariableLength($delta_bytes, $offset); + if($base_size !== strlen($base_bytes)) { + // @TODO: Do not throw exceptions...? Or do? + throw new Exception('Base size mismatch'); + } + $result_size = self::readVariableLength($delta_bytes, $offset); + + $result = ''; + while ($offset < strlen($delta_bytes)) { + $byte = ord($delta_bytes[$offset++]); + if ($byte & 0x80) { + $copyOffset = 0; + $copySize = 0; + if ($byte & 0x01) $copyOffset |= ord($delta_bytes[$offset++]); + if ($byte & 0x02) $copyOffset |= ord($delta_bytes[$offset++]) << 8; + if ($byte & 0x04) $copyOffset |= ord($delta_bytes[$offset++]) << 16; + if ($byte & 0x08) $copyOffset |= ord($delta_bytes[$offset++]) << 24; + if ($byte & 0x10) $copySize |= ord($delta_bytes[$offset++]); + if ($byte & 0x20) $copySize |= ord($delta_bytes[$offset++]) << 8; + if ($byte & 0x40) $copySize |= ord($delta_bytes[$offset++]) << 16; + if ($copySize === 0) $copySize = 0x10000; + $result .= substr($base_bytes, $copyOffset, $copySize); + } else { + $result .= substr($delta_bytes, $offset, $byte); + $offset += $byte; + } + } + + if(strlen($result) !== $result_size) { + // @TODO: Do not throw exceptions...? Or do? + throw new Exception('Result size mismatch'); + } + + return $result; + } + + static private function parse_tree_bytes($treeContent) { + $offset = 0; + $files = []; + + while ($offset < strlen($treeContent)) { + if ($offset >= strlen($treeContent)) { + var_dump('uninitialized string offset'); + break; // Prevent uninitialized string offset + } + + // Read file mode + $modeEnd = strpos($treeContent, ' ', $offset); + if ($modeEnd === false || $modeEnd >= strlen($treeContent)) { + var_dump('invalid mode'); + break; // Invalid mode + } + $mode = substr($treeContent, $offset, $modeEnd - $offset); + $offset = $modeEnd + 1; + + if(preg_match('/^0?4.*/', $mode)) { + $mode = self::FILE_MODE_DIRECTORY; + } else if(preg_match('/^1006.*/', $mode)) { + $mode = self::FILE_MODE_REGULAR_NON_EXECUTABLE; + } else if(preg_match('/^1007.*/', $mode)) { + $mode = self::FILE_MODE_REGULAR_EXECUTABLE; + } else if(preg_match('/^120.*/', $mode)) { + $mode = self::FILE_MODE_SYMBOLIC_LINK; + } else if(preg_match('/^160.*/', $mode)) { + $mode = self::FILE_MODE_COMMIT; + } + + // Read file name + $nameEnd = strpos($treeContent, "\0", $offset); + if ($nameEnd === false || $nameEnd >= strlen($treeContent)) { + var_dump('invalid name'); + break; // Invalid name + } + $name = substr($treeContent, $offset, $nameEnd - $offset); + $offset = $nameEnd + 1; + + // Read SHA1 + if ($offset + 20 > strlen($treeContent)) { + var_dump('invalid sha1'); + break; // Prevent out-of-bounds access + } + $sha1 = bin2hex(substr($treeContent, $offset, 20)); + $offset += 20; + + $files[$name] = [ + 'mode' => $mode, + 'name' => $name, + 'sha1' => $sha1, + ]; + } + + return $files; + } + + static private function readVariableLength($data, &$offset) { + $result = 0; + $shift = 0; + do { + $byte = ord($data[$offset++]); + $result |= ($byte & 0x7F) << $shift; + $shift += 7; + } while ($byte & 0x80); + return $result; + } + + static private function parse_pack_data($packData) { + $offset = 0; + + // Basic sanity checks + if (strlen($packData) < 12) { + return false; + } + + $header = substr($packData, $offset, 4); + $offset += 4; + if ($header !== "PACK") { + return false; + } + + $version = unpack('N', substr($packData, $offset, 4))[1]; + $offset += 4; + + $objectCount = unpack('N', substr($packData, $offset, 4))[1]; + $offset += 4; + + $objects = []; + + for ($i = 0; $i < $objectCount; $i++) { + if ($offset >= strlen($packData)) { + break; + } + + $header_offset = $offset; + $object = self::parse_pack_header($packData, $offset); + $object['header_offset'] = $header_offset; + $object['content'] = self::inflate_object($packData, $offset, $object['uncompressed_length']); + $objects[] = $object; + } + return [ + 'objects' => $objects, + 'total_objects' => $objectCount, + 'pack_version' => $version + ]; + } + + /** + * Incrementally inflate the next object’s compressed data until it yields + * $uncompressedSize bytes, or we hit the end of the compressed stream. + * Adjusts $offset so that after returning, $offset points to the next object header. + */ + static private function inflate_object(string $packData, int &$offset, int $uncompressedSize): ?string { + $inflateContext = inflate_init(ZLIB_ENCODING_DEFLATE); + if (!$inflateContext) { + return null; + } + + $inflated = ''; + $packLen = strlen($packData); + + $bytes_read = 0; + while ($offset < $packLen) { + // Feed chunks into inflate. We don’t know how big each chunk is, + // so let's just pick something arbitrary: + $chunk = substr($packData, $offset, 256); + + $res = inflate_add($inflateContext, $chunk); + switch(inflate_get_status($inflateContext)) { + case ZLIB_BUF_ERROR: + case ZLIB_DATA_ERROR: + case ZLIB_VERSION_ERROR: + case ZLIB_MEM_ERROR: + throw new Exception('Inflate error'); + } + if ($res === false) { + throw new Exception('Inflate error'); + } + $bytes_read_for_this_chunk = inflate_get_read_len($inflateContext) - $bytes_read; + $offset += $bytes_read_for_this_chunk; + + $bytes_read = inflate_get_read_len($inflateContext); + $inflated .= $res; + + if(inflate_get_status($inflateContext) === ZLIB_STREAM_END) { + break; + } + } + return $inflated; + } + + static private function parse_pack_header(string $packData, int &$offset) { + // Object type is encoded in bits 654 + $byte = ord($packData[$offset++]); + $type = ($byte >> 4) & 0b111; + // The length encoding get complicated. + // Last four bits of length is encoded in bits 3210 + $uncompressed_length = $byte & 0b1111; + // Whether the next byte is part of the variable-length encoded number + // is encoded in bit 7 + if ($byte & 0b10000000) { + $shift = 4; + $byte = ord($packData[$offset++]); + while ($byte & 0b10000000) { + $uncompressed_length |= ($byte & 0b01111111) << $shift; + $shift += 7; + $byte = ord($packData[$offset++]); + } + $uncompressed_length |= ($byte & 0b01111111) << $shift; + } + // Handle deltified objects + $ofs = null; + $reference = null; + if ($type === self::OBJECT_TYPE_OFS_DELTA) { + // Git uses a specific formula: ofs = ((ofs + 1) << 7) + (c & 0x7f) + // for each continuation byte. The first byte doesn't do the "ofs+1" part. + // This code matches Git’s logic. + $ofs = 0; + // Read the first byte + $c = ord($packData[$offset++]); + $ofs = ($c & 0x7F); + + // If bit 7 (0x80) is set, we keep reading + while ($c & 0x80) { + $c = ord($packData[$offset++]); + $ofs = (($ofs + 1) << 7) + ($c & 0x7F); + } + } else if ($type === self::OBJECT_TYPE_REF_DELTA) { + $reference = substr($packData, $offset, 20); + $offset += 20; + } + return [ + 'ofs' => $ofs, + 'type' => $type, + 'uncompressed_length' => $uncompressed_length, + 'reference' => $reference + ]; + } } @@ -209,7 +552,7 @@ class WP_Git_Utils { /** * Computes Git objects needed to commit a changeset. * - * @param WP_Git_Pack_Index $oldIndex The index containing existing objects + * @param WP_Git_Pack_Processor $oldIndex The index containing existing objects * @param WP_Changeset $changeset The changes to commit * @return string The Git objects with type, content and SHA */ @@ -223,8 +566,8 @@ public function compute_push_objects( $new_tree = new stdClass(); foreach (array_merge($changeset->create, $changeset->update) as $path => $content) { - $new_blob = WP_Git_Pack_Encoder::create_object([ - 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, + $new_blob = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, 'content' => $content, ]); $new_index[] = $new_blob; @@ -243,8 +586,8 @@ public function compute_push_objects( $parent = "parent $parent_hash\n"; } $root_tree = $this->backfill_trees($index, $new_index, $new_tree, '/'); - $commit = WP_Git_Pack_Encoder::create_object([ - 'type' => WP_Git_Pack_Index::OBJECT_TYPE_COMMIT, + $commit = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, 'content' => sprintf( "tree %s\n{$parent}author %s\ncommitter %s\n\n%s\n", $root_tree['oid'], @@ -269,10 +612,10 @@ public function compute_push_objects( var_dump($new_index); - $pktPush = WP_Git_Pack_Encoder::encode_packet_line("$parent_hash $commitSha refs/heads/$branchName\0report-status force-update\n"); - $pktPush .= WP_Git_Pack_Encoder::encode_flush(); // "0000" - $pktPush .= WP_Git_Pack_Encoder::build_pack($new_index); - $pktPush .= WP_Git_Pack_Encoder::encode_flush(); // "0000" + $pktPush = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $commitSha refs/heads/$branchName\0report-status force-update\n"); + $pktPush .= "0000"; + $pktPush .= WP_Git_Pack_Processor::encode($new_index); + $pktPush .= "0000"; return $pktPush; } @@ -301,17 +644,17 @@ private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $ // Index blobs switch($subtree_child->type) { - case WP_Git_Pack_Index::OBJECT_TYPE_BLOB: + case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, 'name' => $name, 'sha1' => $subtree_child->oid, ]; break; - case WP_Git_Pack_Index::OBJECT_TYPE_TREE: + case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Index::FILE_MODE_DIRECTORY, + 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, 'name' => $name, 'sha1' => $subtree_object['oid'], ]; @@ -319,8 +662,8 @@ private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $ } } - $new_tree_object = WP_Git_Pack_Encoder::create_object([ - 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, + $new_tree_object = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, 'content' => $new_tree_content, ]); @@ -330,7 +673,7 @@ private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $ private function set_oid($root_tree, $path, $oid) { $blob = new stdClass(); - $blob->type = WP_Git_Pack_Index::OBJECT_TYPE_BLOB; + $blob->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; $blob->oid = $oid; $subtree_path = dirname($path); @@ -350,7 +693,7 @@ private function get_subtree($root_tree, $path) { foreach ($segments as $segment) { if (!isset($subtree->children[$segment])) { $new_subtree = new stdClass(); - $new_subtree->type = WP_Git_Pack_Index::OBJECT_TYPE_TREE; + $new_subtree->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; $new_subtree->children = []; $subtree->children[$segment] = $new_subtree; } @@ -380,18 +723,14 @@ private function get_subtree($root_tree, $path) { // 'wp-content/ztra/dark-mode.css' => '/* differddent */', ], [], - [ - 'wp-content/dark-mode.css', - '0', - '1' - ] + [] ), 'main', $head_hash ); // $pack_data = substr($push_bytes, strpos($push_bytes, "PACK")); -// $new_index = WP_Git_Pack_Index::from_pack_data($pack_data); +// $new_index = WP_Git_Pack_Processor::from_pack_data($pack_data); // var_dump($new_index); // die('......'); @@ -433,28 +772,28 @@ private function get_subtree($root_tree, $path) { // $readme_md = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, // 'content' => '## This is a Readme file', // ]); // $index_html = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, // 'content' => '

Hello, world!

', // ]); // $style_css = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_BLOB, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, // 'content' => 'body { background-color: red; }', // ]); // $html_pages = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, // 'content' => [ // [ -// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, // 'name' => 'readme.md', // 'sha1' => $readme_md['oid'], // ], // [ -// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, // 'name' => 'index.html', // 'sha1' => $index_html['oid'], // ], @@ -462,10 +801,10 @@ private function get_subtree($root_tree, $path) { // ]); // $theme_tree = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, // 'content' => [ // [ -// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, // 'name' => 'style.css', // 'sha1' => $style_css['oid'], // ], @@ -473,15 +812,15 @@ private function get_subtree($root_tree, $path) { // ]); // $wp_content_tree = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, // 'content' => [ // [ -// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, // 'name' => 'html-pages', // 'sha1' => $html_pages['oid'], // ], // [ -// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, // 'name' => 'theme', // 'sha1' => $theme_tree['oid'], // ], @@ -489,10 +828,10 @@ private function get_subtree($root_tree, $path) { // ]); // $root_tree = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_TREE, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, // 'content' => [ // [ -// 'mode' => WP_Git_Pack_Index::FILE_MODE_REGULAR_NON_EXECUTABLE, +// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, // 'name' => 'wp-content', // 'sha1' => $wp_content_tree['oid'], // ], @@ -500,14 +839,14 @@ private function get_subtree($root_tree, $path) { // ]); // $commit = create_object([ -// 'type' => WP_Git_Pack_Index::OBJECT_TYPE_COMMIT, +// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, // 'content' => 'Hello, world!', // 'tree' => $root_tree['oid'], // ]); // $push_bytes = $utils->compute_push_objects( -// new WP_Git_Pack_Index([ +// new WP_Git_Pack_Processor([ // $commit, // $root_tree, // $wp_content_tree, From 0d8f88ffd7c74260c629545c62b1c2d67546077c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 21:04:07 +0100 Subject: [PATCH 10/71] WP_Git_Filesystem that can read and write files from a Git repo --- .gitignore | 1 + .../src/WP_Markdown_Importer.php | 4 +- .../WP_Static_File_Sync.php | 509 ------------------ .../plugin.php | 26 +- .../tests/WPStaticFileSyncTests.php | 4 +- .../playground/data-liberation/bootstrap.php | 3 +- .../data-liberation/src/git/WP_Git_Client.php | 214 +++++++- .../src/git/WP_Git_Filesystem.php | 162 +++++- .../src/git/WP_Git_Pack_Index.php | 447 +++++---------- .../src/git/WP_Git_Pack_Processor.php} | 399 -------------- .../WPDirectoryTreeEntityReaderTests.php | 4 +- .../WPFilesystemToPostHierarchyTests.php | 2 +- 12 files changed, 500 insertions(+), 1275 deletions(-) delete mode 100644 packages/playground/data-liberation-static-files-editor/WP_Static_File_Sync.php rename packages/playground/{data-liberation-static-files-editor/push-simple.php => data-liberation/src/git/WP_Git_Pack_Processor.php} (57%) diff --git a/.gitignore b/.gitignore index 2c123c0b78..f9c50abea2 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ testem.log *.timestamp-1678999213403.mjs /.env packages/playground/website/cypress/downloads +packages/playground/data-liberation-static-files-editor/secrets.php vite.config.ts.timestamp-*.mjs # System Files diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_Importer.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_Importer.php index 9041b562d1..b4820f7b9f 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_Importer.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_Importer.php @@ -1,6 +1,6 @@ $markdown_directory, 'first_post_id' => 1, diff --git a/packages/playground/data-liberation-static-files-editor/WP_Static_File_Sync.php b/packages/playground/data-liberation-static-files-editor/WP_Static_File_Sync.php deleted file mode 100644 index 89b80b9c48..0000000000 --- a/packages/playground/data-liberation-static-files-editor/WP_Static_File_Sync.php +++ /dev/null @@ -1,509 +0,0 @@ -filesystem = $filesystem; - $this->post_type = $options['post_type'] ?? WP_LOCAL_FILE_POST_TYPE; - $this->index_file_pattern = $options['index_file_pattern'] ?? '/^index\.\w+$/'; - $this->default_index_filename = $options['default_index_filename'] ?? 'index.md'; - } - - public function initialize_sync() { - add_action('pre_post_update', [$this, 'cache_previous_post']); - add_action('save_post', [$this, 'on_save_post'], 10, 3); - add_action('delete_post', [$this, 'on_delete_post']); - } - - public function deinitialize_sync() { - // @TODO: Confirm we don't have to preserve the original $callback - // array for remove_action() to work. - remove_action('pre_post_update', [$this, 'cache_previous_post']); - remove_action('save_post', [$this, 'on_save_post'], 10, 3); - remove_action('delete_post', [$this, 'on_delete_post']); - } - - /** - * Cache the post data before it gets updated - */ - public function cache_previous_post($post_id) { - $this->previous_post = get_post($post_id, ARRAY_A); - } - - /** - * Handle saving of a post or page. - */ - public function on_save_post(int $post_id, WP_Post $post, bool $update): void - { - if (!$this->wordpress_ready_for_sync()) { - return; - } - - if ( - empty($post->ID) || - $post->post_status !== 'publish' || - $post->post_type !== $this->post_type - ) { - return; - } - - try { - // Ensure the parent directory exists. - if($post->post_parent) { - $parent_file_path_before = get_post_meta($post->post_parent, 'local_file_path', true); - $parent_file_path_after = $this->ensure_is_directory_index($parent_file_path_before); - if(false === $parent_file_path_after) { - $this->bail( - 'failed_to_ensure_parent_directory_index', - 'Failed to ensure parent directory index for post ' . $post->post_parent - ); - return; - } - if($parent_file_path_after !== $parent_file_path_before) { - update_post_meta($post->post_parent, 'local_file_path', $parent_file_path_after); - } - $parent_dir = dirname($parent_file_path_after); - } else { - $parent_dir = '/'; - } - - // @TODO: Handle creation of a new post - - // Figure out the new local file path of the updated page. - $has_children = !!get_posts([ - 'post_type' => $this->post_type, - 'post_parent' => $post->ID, - 'numberposts' => 1, - 'fields' => 'ids' - ]); - $parent_changed = $post->post_parent !== $this->previous_post['post_parent']; - $local_path_before = get_post_meta($post_id, 'local_file_path', true) ?? ''; - if($has_children && $parent_changed) { - // Move the entire directory subtree to the new parent. - $local_path_to_move_from = dirname($local_path_before); - $local_path_to_move_to = $this->append_unique_suffix( - wp_join_paths($parent_dir, basename($local_path_to_move_from)) - ); - if(false === $this->filesystem->rename($local_path_to_move_from, $local_path_to_move_to)) { - $this->bail('failed_to_rename_file', 'Failed to rename file: ' . $local_path_to_move_from); - return; - } - update_post_meta($post_id, 'local_file_path', $local_path_to_move_to); - $local_path_changed = true; - $local_path_after = wp_join_paths($local_path_to_move_to, basename($local_path_before)); - } else { - $filename_after = $has_children ? 'index' : sanitize_title($post->post_name); - - $extension = pathinfo($local_path_before, PATHINFO_EXTENSION) ?: 'md'; - if($extension) { - $filename_after .= '.' . $extension; - } - - $local_path_after = wp_join_paths($parent_dir, $filename_after); - $local_path_changed = !$local_path_before || $local_path_before !== $local_path_after; - if($local_path_changed) { - $local_path_after = $this->append_unique_suffix($local_path_after); - } - - $success = $this->filesystem->put_contents( - $local_path_after, - $this->convert_content($post_id) - ); - if(false === $success) { - $this->bail('failed_to_create_file', 'Failed to create file: ' . $local_path_after); - return; - } - } - if($local_path_changed) { - if($local_path_before) { - $this->filesystem->rm($local_path_before); - } - update_post_meta($post_id, 'local_file_path', $local_path_after); - } - - // If we're moving the page under a new parent, flatten the old parent's - // directory if it now contains only the index file. - if($parent_changed && $this->previous_post['post_parent']) { - $old_parent_prev_path = get_post_meta($this->previous_post['post_parent'], 'local_file_path', true); - $old_parent_new_path = $this->flatten_parent_if_needed($old_parent_prev_path); - if($old_parent_new_path) { - update_post_meta($this->previous_post['post_parent'], 'local_file_path', $old_parent_new_path); - } - } - - // If the page we just updated was a parent node itself, update, the local_file_path - // meta of its entire subtree. - if($this->previous_post['post_parent']) { - $this->update_indexed_local_file_paths_for_children($post->ID); - } - } catch(Exception $e) { - // @TODO: Handle failures gracefully. - var_dump($e->getMessage()); - var_dump($e->getTraceAsString()); - throw $e; - } - } - - private function update_indexed_local_file_paths_for_children($parent_id) { - $children = get_posts([ - 'post_type' => $this->post_type, - 'post_parent' => $parent_id, - ]); - if(empty($children)) { - return; - } - $parent_new_file_path = get_post_meta($parent_id, 'local_file_path', true); - foreach($children as $child) { - $child_local_path_before = get_post_meta($child->ID, 'local_file_path', true); - $child_local_path_after = dirname($parent_new_file_path) . '/' . basename($child_local_path_before); - update_post_meta($child->ID, 'local_file_path', $child_local_path_after); - $this->update_indexed_local_file_paths_for_children($child->ID); - } - } - - /** - * Handle deletion of a post or page. - */ - public function on_delete_post(int $post_id): void - { - if (!$this->wordpress_ready_for_sync()) { - return; - } - if (!$post_id) { - return; - } - $post = get_post($post_id); - if ( - $post->post_status !== 'publish' || - $post->post_type !== $this->post_type - ) { - return; - } - - $local_file_path = get_post_meta($post_id, 'local_file_path', true); - if (! $local_file_path ) { - return; - } - - if (! $this->filesystem->exists($local_file_path)) { - return; - } - - $has_children = !!get_posts([ - 'post_type' => $this->post_type, - 'post_parent' => $post_id, - 'numberposts' => 1, - 'fields' => 'ids' - ]); - - if($has_children) { - $path_to_delete = dirname($local_file_path); - $success = $this->filesystem->rmdir($path_to_delete, ['recursive' => true]); - } else { - $path_to_delete = $local_file_path; - $success = $this->filesystem->rm($path_to_delete); - } - - if(!$success) { - $this->bail('failed_to_delete_directory', 'Failed to delete local file: ' . $path_to_delete); - return; - } - - $this->flatten_parent_if_needed(dirname($path_to_delete)); - } - - private function wordpress_ready_for_sync(): bool { - // Ignore auto-saves or revisions - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - return false; - } - - // Skip if in maintenance mode - if (wp_is_maintenance_mode()) { - return false; - } - - if (defined('WP_IMPORTING') && WP_IMPORTING) { - return false; - } - - return true; - } - - private function convert_content( $page_id ) { - $page = get_post($page_id); - if(!$page) { - return ''; - } - - $content_converter = get_post_meta($page_id, 'content_converter', true) ?: 'md'; - - /** - * @TODO: Decide – should we only do one of the following - * instead of both? - * - * 1. Include the title as the first H1 block - * 2. Include the title as a metadata field - */ - $title_block = ( - WP_Import_Utils::block_opener('heading', array('level' => 1)) . - '

' . esc_html(get_the_title($page_id)) . '

' . - WP_Import_Utils::block_closer('heading') - ); - $block_markup = $title_block . $page->post_content; - $metadata = array( - 'title' => get_the_title($page_id), - ); - - switch($content_converter) { - // case 'blocks': - // $converter = new WP_Blocks_To_Blocks( - // $block_markup, - // $metadata - // ); - // break; - // case 'html': - // case 'xhtml': - // $converter = new WP_Blocks_To_HTML( - // $block_markup, - // $metadata - // ); - // break; - case 'md': - default: - $converter = new WP_Blocks_To_Markdown( - $block_markup, - $metadata - ); - break; - } - if(false === $converter->convert()) { - // @TODO: error handling. - return; - } - return $converter->get_result(); - } - - public function get_last_error() - { - return $this->last_error; - } - - /** - * Ensure the given path is a directory index (e.g., becomes `/index.{extension}`). - */ - public function ensure_is_directory_index(string $path) - { - // If we're given a directory, ensure it has an index file. - if ($this->filesystem->is_dir($path)) { - $index_file = $this->find_index_file($path); - if (!$index_file) { - // Default to Markdown. @TODO: Make this configurable. - $index_file = wp_join_paths($path, $this->default_index_filename); - $this->filesystem->put_contents($index_file, ''); // Create an empty index file - } - return $index_file; - } - - // If we're given a file, create a parent directory with the - // same name (without the extension) and move the file inside - // as its index file. - if ($this->filesystem->is_file($path)) { - // If the file is already an index file, we're done. - if($this->is_index_file($path)) { - return $path; - } - - // @TODO: Handle a file with no extension. - $extension = pathinfo($path, PATHINFO_EXTENSION); - - $swap_path = $path; - if ( $extension ) { - $new_dir = $this->remove_extension($path); - } else { - $new_dir = $path; - /** - * When the file has no extension, $new_dir is the same as $path. - * We need to rename the file to a unique name to avoid collisions. - */ - $swap_path = $this->append_unique_suffix($path); - if(!$this->filesystem->rename($path, $swap_path)) { - $this->bail('failed_to_rename_file', 'Failed to rename file: ' . $path); - return false; - } - } - - if(!$this->filesystem->mkdir($new_dir)) { - $this->bail('failed_to_create_directory', 'Failed to create directory: ' . $new_dir); - return false; - } - - $new_filename = $this->remove_extension($this->default_index_filename); - if ($extension) { - $new_filename .= ".{$extension}"; - } - - $index_file = wp_join_paths($new_dir, $new_filename); - if(!$this->filesystem->rename($swap_path, $index_file)) { - $this->bail('failed_to_rename_file', 'Failed to rename file: ' . $path); - return false; - } - return $index_file; - } - - $this->bail('path_not_found', 'Path does not exist: ' . $path); - return false; - } - - /** - * Flatten a parent directory if it only contains an `index` file. - */ - public function flatten_parent_if_needed(string $directory_index_path): bool - { - if ($this->filesystem->is_file($directory_index_path)) { - $parent_dir = dirname($directory_index_path); - } else if ($this->filesystem->is_dir($directory_index_path)) { - $parent_dir = $directory_index_path; - } else { - return $directory_index_path; - } - - if(!$parent_dir || $parent_dir === '/') { - return $directory_index_path; - } - - $files = $this->filesystem->ls($parent_dir); - if(count($files) === 0) { - $this->filesystem->rmdir($parent_dir); - return $directory_index_path; - } - - // Can't flatten if there are more than one file in the parent directory. - if (count($files) !== 1) { - return $directory_index_path; - } - - if ($this->filesystem->is_dir($directory_index_path)) { - $directory_index_path = $directory_index_path . '/' . $files[0]; - } - - if($this->is_index_file($files[0])) { - // If the directory index is an index file, rename it from "index" - // to the parent directory name - $extension = pathinfo($directory_index_path, PATHINFO_EXTENSION); - $new_filename = basename($parent_dir); - if ($extension) { - $new_filename .= ".{$extension}"; - } - } else { - // If the directory index is not an index file, keep its name - $new_filename = $files[0]; - } - - $new_path = $this->append_unique_suffix( - wp_join_paths(dirname($parent_dir), $new_filename) - ); - if (!$this->filesystem->rename($directory_index_path, $new_path)) { - $this->bail('failed_to_rename_file', 'Failed to rename file: ' . $directory_index_path . ' to ' . $new_path); - return false; - } - - if (!$this->filesystem->rmdir($parent_dir)) { - $this->bail('failed_to_delete_directory', 'Failed to delete directory: ' . $parent_dir); - return false; - } - - return $new_path; - } - - /** - * Append a unique suffix to a file path to avoid collisions. - */ - private function append_unique_suffix(string $path): string - { - $dir = dirname($path); - $filename = basename($path); - $extension = pathinfo($filename, PATHINFO_EXTENSION); - $name = pathinfo($filename, PATHINFO_FILENAME); - - $new_path = $path; - $counter = 1; - while ($this->filesystem->exists($new_path)) { - $new_filename = $name . "-{$counter}"; - if ($extension) { - $new_filename .= "." . $extension; - } - $new_path = wp_join_paths($dir, $new_filename); - $counter++; - } - return $new_path; - } - - /** - * Find the index file in a directory. - * - * @TODO: Make configurable. - */ - private function find_index_file(string $directory): ?string - { - $files = $this->filesystem->ls($directory); - foreach ($files as $file) { - if ($this->is_index_file($file)) { - return $file; - } - } - return null; - } - - private function is_index_file(string $path): bool - { - return preg_match($this->index_file_pattern, basename($path)); - } - - private function remove_extension(string $path): string - { - $extension = pathinfo($path, PATHINFO_EXTENSION); - return substr($path, 0, -strlen(".{$extension}")); - } - - private function bail($code, $message) { - throw new Exception("$code: $message"); - // $this->last_error = new WP_Error($code, $message); - // return false; - } - -} diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 6222d29954..8382dc46db 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -19,7 +19,7 @@ * @TODO: Maybe use Playground's FilePickerTree React component? Or re-implement it with interactivity API? */ -use WordPress\Filesystem\WP_Filesystem; +use WordPress\Filesystem\WP_Local_Filesystem; if ( ! defined( 'WP_STATIC_CONTENT_DIR' ) ) { define( 'WP_STATIC_CONTENT_DIR', WP_CONTENT_DIR . '/uploads/static-pages' ); @@ -36,6 +36,7 @@ } require_once __DIR__ . '/WP_Static_File_Sync.php'; +require_once __DIR__ . '/secrets.php'; class WP_Static_Files_Editor_Plugin { @@ -43,10 +44,13 @@ class WP_Static_Files_Editor_Plugin { static private function get_fs() { if(!self::$fs) { - // self::$fs = new WP_Filesystem( WP_STATIC_CONTENT_DIR ); self::$fs = new WP_Git_Filesystem( - new WP_Git_Client('https://github.com/WordPress/gutenberg'), - '/docs/how-to-guides/data-basics' + new WP_Git_Client(GIT_REPO_URL, [ + 'author' => GIT_AUTHOR, + 'committer' => GIT_COMMITTER, + ]), + GIT_BRANCH, + GIT_DIRECTORY_ROOT ); } return self::$fs; @@ -54,9 +58,6 @@ static private function get_fs() { static public function initialize() { // Register hooks - // $static_sync = new WP_Static_File_Sync( self::get_fs() ); - // $static_sync->initialize_sync(); - // register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); add_action('init', function() { @@ -173,7 +174,7 @@ static public function initialize() { } // Check if this post is of type "local_file" - if ($post && $post->post_type === 'local_file') { + if ($post && $post->post_type === WP_LOCAL_FILE_POST_TYPE) { // Get the latest content from the database first $content = $post->post_content; @@ -190,7 +191,7 @@ static public function initialize() { }, 10, 2); // Add filter for REST API responses - add_filter('rest_prepare_local_file', function($response, $post, $request) { + add_filter('rest_prepare_' . WP_LOCAL_FILE_POST_TYPE, function($response, $post, $request) { $new_content = self::refresh_post_from_local_file($post); if(!is_wp_error($new_content)) { $response->data['content']['raw'] = $new_content; @@ -247,6 +248,10 @@ static private function refresh_post_from_local_file($post) { return; } $content = $fs->read_file($path); + if(!is_string($content)) { + _doing_it_wrong(__METHOD__, 'File not found: ' . $path, '1.0.0'); + return; + } $extension = pathinfo($path, PATHINFO_EXTENSION); switch($extension) { case 'md': @@ -284,7 +289,6 @@ static private function refresh_post_from_local_file($post) { } static private function save_post_data_to_local_file($post) { - return; try { if(!self::acquire_synchronization_lock()) { return; @@ -386,7 +390,7 @@ static public function import_static_pages() { $importer = WP_Stream_Importer::create( function () { return new WP_Filesystem_Entity_Reader( - new WP_Filesystem(WP_STATIC_CONTENT_DIR), + new WP_Local_Filesystem(WP_STATIC_CONTENT_DIR), array( 'post_type' => WP_LOCAL_FILE_POST_TYPE, ) diff --git a/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php b/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php index 6b312b812f..85456c838f 100644 --- a/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php +++ b/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php @@ -1,14 +1,14 @@ filesystem = new WP_Filesystem(__DIR__ . '/static-files-tests/'); + $this->filesystem = new WP_Local_Filesystem(__DIR__ . '/static-files-tests/'); } public function tearDown(): void { diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 9e1326edba..5c5c0ffacf 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -12,7 +12,7 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/Client.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Abstract_Filesystem.php'; -require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Local_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_File_Visitor_Event.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Visitor.php'; @@ -83,6 +83,7 @@ require_once __DIR__ . '/src/import/WP_Import_Utils.php'; require_once __DIR__ . '/src/git/WP_Git_Client.php'; +require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; require_once __DIR__ . '/src/git/WP_Git_Pack_Index.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 1b8a9bca09..72d68b3e3b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -4,9 +4,13 @@ class WP_Git_Client { private $repoUrl; + private $author; + private $committer; - public function __construct($repoUrl) { + public function __construct($repoUrl, $options = []) { $this->repoUrl = rtrim($repoUrl, '/'); + $this->author = $options['author'] ?? "John Doe "; + $this->committer = $options['committer'] ?? "John Doe "; } public function fetchRefs($prefix) { @@ -56,6 +60,59 @@ private function parse_git_protocol_v2_packets($bytes) { } } + public function push($git_objects, $options = []) { + $empty_hash = "0000000000000000000000000000000000000000"; + $parent_hash = $options['parent_hash'] ?? $empty_hash; + $tree_hash = $options['tree_hash'] ?? $empty_hash; + $branchName = $options['branch_name']; + $author = ($options['author'] ?? $this->author) . " " . time() . " +0000"; + $committer = ($options['committer'] ?? $this->committer) . " " . time() . " +0000"; + $message = $options['message'] ?? "Hello!"; + + $parent = ''; + if($parent_hash !== $empty_hash) { + $parent = "parent $parent_hash\n"; + } + $commit_object = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, + 'content' => sprintf( + "tree %s\n%sauthor %s\ncommitter %s\n\n%s\n", + $tree_hash, + $parent, + $author, + $committer, + $message + ), + 'tree' => $tree_hash, + ]); + $commit_sha = $commit_object['oid']; + + $git_objects[] = $commit_object; + + $push_packet = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $commit_sha refs/heads/$branchName\0report-status force-update\n"); + $push_packet .= "0000"; + $push_packet .= WP_Git_Pack_Processor::encode($git_objects); + $push_packet .= "0000"; + + $url = rtrim($this->repoUrl, '.git').'.git/git-receive-pack'; + $response = $this->http_request($url, $push_packet, [ + 'Content-Type: application/x-git-receive-pack-request', + 'Accept: application/x-git-receive-pack-result', + ]); + + $response_chunks = iterator_to_array($this->parse_multiplexed_pack_data($response)); + if( + trim($response_chunks[0]['data']) !== 'unpack ok' || + trim($response_chunks[1]['data']) !== 'ok refs/heads/' . $branchName + ) { + throw new Exception('Push failed:' . $response); + } + return [ + 'new_head_hash' => $commit_sha, + 'new_tree_hash' => $tree_hash, + ]; + } + public function list_objects($ref_hash) { $body = $this->encode_packet_line("want {$ref_hash} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3 filter\n") . @@ -187,4 +244,159 @@ static public function parse_multiplexed_pack_data($bytes) { } } + private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; + + /** + * Computes Git objects needed to commit a changeset. + * + * @param WP_Git_Pack_Processor $oldIndex The index containing existing objects + * @param WP_Changeset $changeset The changes to commit + * @return string The Git objects with type, content and SHA + */ + public function compute_push_objects( + WP_Git_Pack_Index $index, + WP_Changeset $changeset, + $branchName, + $parent_hash = null + ) { + $new_index = []; + + $new_tree = new stdClass(); + foreach (array_merge($changeset->create, $changeset->update) as $path => $content) { + $new_blob = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, + 'content' => $content, + ]); + $new_index[] = $new_blob; + $this->set_oid($new_tree, $path, $new_blob['oid']); + } + + foreach ($changeset->delete as $path) { + $this->set_oid($new_tree, $path, self::DELETE_PLACEHOLDER); + } + + if(!$parent_hash) { + $parent_hash = "0000000000000000000000000000000000000000"; + } + $parent = ''; + if($parent_hash) { + $parent = "parent $parent_hash\n"; + } + $root_tree = $this->backfill_trees($index, $new_index, $new_tree, '/'); + $commit = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, + 'content' => sprintf( + "tree %s\n{$parent}author %s\ncommitter %s\n\n%s\n", + $root_tree['oid'], + "John Doe " . time() . " +0000", + "John Doe " . time() . " +0000", + "Hello!" + ), + 'tree' => $root_tree['oid'], + ]); + $commit_sha = $commit['oid']; + $new_index[] = $commit; + // Make $new_index unique by 'oid' column + $seen_oids = []; + $new_index = array_filter($new_index, function($obj) use (&$seen_oids) { + if (isset($seen_oids[$obj['oid']])) { + return false; + } + $seen_oids[$obj['oid']] = true; + return true; + }); + // $new_index = array_reverse($new_index); + + var_dump($new_index); + + $pktPush = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $commit_sha refs/heads/$branchName\0report-status force-update\n"); + $pktPush .= "0000"; + $pktPush .= WP_Git_Pack_Processor::encode($new_index); + $pktPush .= "0000"; + + return $pktPush; + } + + private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $subtree_delta, $subtree_path = '/') { + $subtree_path = ltrim($subtree_path, '/'); + $new_tree_content = []; + + $indexed_tree = $current_index->get_by_path($subtree_path); + if($indexed_tree) { + foreach($indexed_tree['content'] as $object) { + // Backfill the unchanged objects from the currently indexed subtree. + $name = $object['name']; + if(!isset($subtree_delta->children[$name])) { + $new_tree_content[$name] = $object; + } + } + } + + // Index changed and new objects in the current subtree. + foreach($subtree_delta->children as $name => $subtree_child) { + // Ignore any deleted objects. + if(isset($subtree_child->oid) && $subtree_child->oid === self::DELETE_PLACEHOLDER) { + continue; + } + + // Index blobs + switch($subtree_child->type) { + case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: + $new_tree_content[$name] = [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'name' => $name, + 'sha1' => $subtree_child->oid, + ]; + break; + case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: + $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); + $new_tree_content[$name] = [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, + 'name' => $name, + 'sha1' => $subtree_object['oid'], + ]; + break; + } + } + + $new_tree_object = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, + 'content' => $new_tree_content, + ]); + + $new_index[] = $new_tree_object; + return $new_tree_object; + } + + private function set_oid($root_tree, $path, $oid) { + $blob = new stdClass(); + $blob->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; + $blob->oid = $oid; + + $subtree_path = dirname($path); + if($subtree_path === '.') { + $subtree = $root_tree; + } else { + $subtree = $this->get_subtree($root_tree, $subtree_path); + } + $filename = basename($path); + $subtree->children[$filename] = $blob; + } + + private function get_subtree($root_tree, $path) { + $path = trim($path, '/'); + $segments = explode('/', $path); + $subtree = $root_tree; + foreach ($segments as $segment) { + if (!isset($subtree->children[$segment])) { + $new_subtree = new stdClass(); + $new_subtree->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; + $new_subtree->children = []; + $subtree->children[$segment] = $new_subtree; + } + $subtree = $subtree->children[$segment]; + } + return $subtree; + } + } diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index 241cd5df9c..aa58ee8504 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -6,18 +6,24 @@ class WP_Git_Filesystem extends WP_Abstract_Filesystem { private $client; private $root; - private $headRef; - private $files_list; + private $branch_name; + private $head_hash; + private $index; private $blobs_backfilled = false; - public function __construct(WP_Git_Client $client, $root = '/') { + public function __construct( + WP_Git_Client $client, + $branch_name = 'main', + $root = '/' + ) { $this->client = $client; $this->root = $root; + $this->branch_name = $branch_name; } public function ls($parent = '/') { $parent = $this->resolve_path($parent); - $tree = $this->get_files_list()->get_by_path($parent); + $tree = $this->get_index()->get_by_path($parent); if(!$tree) { return []; } @@ -26,8 +32,22 @@ public function ls($parent = '/') { public function is_dir($path) { $path = $this->resolve_path($path); - $tree = $this->get_files_list()->get_by_path($path); - return isset($tree['type']) && $tree['type'] === WP_Git_Pack_Index::OBJECT_TYPE_TREE; + // We may not have the blob object yet, but we surely have the parent + // tree object. Instead of resolving the blob by its path, let's check + // if the requested file is in the parent tree. + $object = $this->get_index()->get_by_path(dirname($path)); + if(!$object || !isset($object['type']) || $object['type'] !== WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { + return false; + } + if(!isset($object['content'][basename($path)])) { + return false; + } + $blob = $object['content'][basename($path)]; + + return ( + isset($blob['mode']) && + $blob['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY + ); } public function is_file($path) { @@ -35,12 +55,18 @@ public function is_file($path) { // We may not have the blob object yet, but we surely have the parent // tree object. Instead of resolving the blob by its path, let's check // if the requested file is in the parent tree. - $object = $this->get_files_list()->get_by_path(dirname($path)); + $object = $this->get_index()->get_by_path(dirname($path)); + if(!$object || !isset($object['type']) || $object['type'] !== WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { + return false; + } + if(!isset($object['content'][basename($path)])) { + return false; + } + $blob = $object['content'][basename($path)]; + return ( - $object && - isset($object['type']) && - $object['type'] === WP_Git_Pack_Index::OBJECT_TYPE_TREE && - isset($object['content'][basename($path)]) + isset($blob['mode']) && + $blob['mode'] === WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE ); } @@ -65,9 +91,12 @@ public function close_file_reader() { } public function read_file($path) { + if(!$this->is_file($path)) { + return false; + } $this->ensure_files_data(); $path = $this->resolve_path($path); - $object = $this->get_files_list()->get_by_path($path); + $object = $this->get_index()->get_by_path($path); if(!$object) { return false; } @@ -81,25 +110,114 @@ private function resolve_path($path) { private function ensure_files_data() { if(!$this->blobs_backfilled) { $this->client->backfillBlobs( - $this->get_files_list(), + $this->get_index(), $this->root ); $this->blobs_backfilled = true; } } - private function get_files_list() { - if(!$this->headRef) { - $refs = $this->client->fetchRefs('HEAD'); - if(!isset($refs['HEAD'])) { - throw new Exception('HEAD ref not found'); + private function get_index() { + if(!$this->head_hash) { + $key = 'refs/heads/' . $this->branch_name; + $refs = $this->client->fetchRefs($key); + if(!isset($refs[$key])) { + throw new Exception($key . ' ref not found'); } - $this->headRef = $refs['HEAD']; + $this->head_hash = $refs[$key]; + } + if(!$this->index) { + $this->index = $this->client->list_objects($this->head_hash); + } + return $this->index; + } + + // These methods are not a part of the interface, but they are useful + // for dealing with a local filesystem. + + public function rename($old_path, $new_path) { + if(!$this->is_file($old_path)) { + return false; } - if(!$this->files_list) { - $this->files_list = $this->client->list_objects($this->headRef); + return $this->commit_and_push( + [ + $this->get_full_path($new_path) => $this->read_file($old_path), + ], + [ + $this->get_full_path($old_path), + ] + ); + } + + public function mkdir($path) { + // Git doesn't support empty directories, let's not do anything. + return true; + } + + public function rm($path) { + if($this->is_dir($path)) { + return false; + } + return $this->commit_and_push( + [], + [ + $this->get_full_path($path) + ] + ); + } + + public function rmdir($path, $options = []) { + if(!$this->is_dir($path)) { + return false; + } + // There are no empty directories in Git. We're assuming + // there are always files in the directory. + if(!$options['recursive']) { + return false; } - return $this->files_list; + + return $this->commit_and_push( + [], + [ + $this->get_full_path($path) + ] + ); + } + + public function put_contents($path, $data) { + return $this->commit_and_push( + [ + $this->get_full_path($path) => $data, + ] + ); + } + + private function get_full_path($relative_path) { + return ltrim(wp_join_paths($this->root, $relative_path), '/'); + } + + private function commit_and_push($updates=[], $deletes=[]) { + $commit_data = $this->get_index()->derive_commit_pack_data( + $updates, + $deletes, + ); + + $result = $this->client->push($commit_data['objects'], [ + 'branch_name' => $this->branch_name, + 'parent_hash' => $this->head_hash, + 'tree_hash' => $commit_data['root_tree_oid'], + ]); + + if(!$result) { + // @TODO: Error handling + return false; + } + + $this->head_hash = $result['new_head_hash']; + // Reset the index so it's refetched the next time we need it. + $this->index = null; + + return true; } } diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php index 753aa18312..94099ff684 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php @@ -2,42 +2,14 @@ class WP_Git_Pack_Index { - const OBJECT_TYPE_COMMIT = 1; - const OBJECT_TYPE_TREE = 2; - const OBJECT_TYPE_BLOB = 3; - const OBJECT_TYPE_TAG = 4; - const OBJECT_TYPE_RESERVED = 5; - const OBJECT_TYPE_OFS_DELTA = 6; - const OBJECT_TYPE_REF_DELTA = 7; - - const OBJECT_NAMES = [ - self::OBJECT_TYPE_COMMIT => 'commit', - self::OBJECT_TYPE_TREE => 'tree', - self::OBJECT_TYPE_BLOB => 'blob', - self::OBJECT_TYPE_TAG => 'tag', - self::OBJECT_TYPE_RESERVED => 'reserved', - self::OBJECT_TYPE_OFS_DELTA => 'ofs_delta', - self::OBJECT_TYPE_REF_DELTA => 'ref_delta', - ]; - - const FILE_MODE_DIRECTORY = '040000'; - const FILE_MODE_REGULAR_NON_EXECUTABLE = '100644'; - const FILE_MODE_REGULAR_EXECUTABLE = '100755'; - const FILE_MODE_SYMBOLIC_LINK = '120000'; - const FILE_MODE_COMMIT = '160000'; - - const FILE_MODE_NAMES = [ - self::FILE_MODE_DIRECTORY => 'directory', - self::FILE_MODE_REGULAR_NON_EXECUTABLE => 'regular_non_executable', - self::FILE_MODE_REGULAR_EXECUTABLE => 'regular_executable', - self::FILE_MODE_SYMBOLIC_LINK => 'symbolic_link', - self::FILE_MODE_COMMIT => 'commit', - ]; - private $objects = []; private $by_oid = []; private $external_get_by_oid = null; + static public function from_pack_data($pack_data) { + return WP_Git_Pack_Processor::decode($pack_data); + } + public function __construct( $objects = [], $by_oid = [] @@ -70,7 +42,7 @@ public function get_by_oid($oid) { public function get_by_path($path, $root_tree_oid=null) { if($root_tree_oid === null) { foreach($this->objects as $object) { - if($object['type'] === self::OBJECT_TYPE_COMMIT) { + if($object['type'] === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { $root_tree_oid = $object['tree']; break; } @@ -81,7 +53,7 @@ public function get_by_path($path, $root_tree_oid=null) { return null; } - if($current_tree['type'] === self::OBJECT_TYPE_COMMIT) { + if($current_tree['type'] === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { $current_tree = $this->get_by_oid($current_tree['tree']); } @@ -111,7 +83,7 @@ public function get_descendants($tree_oid) { return []; } foreach ($tree['content'] as $name => $object) { - if ($object['mode'] === self::FILE_MODE_DIRECTORY) { + if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { yield from $this->get_descendants($object['sha1']); } else { yield $object; @@ -127,7 +99,7 @@ public function get_descendants_tree($tree_oid) { $descendants = []; foreach ($tree['content'] as $name => $object) { - if ($object['mode'] === self::FILE_MODE_DIRECTORY) { + if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { $descendants[$name] = $this->get_descendants_tree($object['sha1']); } else { $blob = $this->get_by_oid($object['sha1']); @@ -138,314 +110,139 @@ public function get_descendants_tree($tree_oid) { return $descendants; } - static public function from_pack_data($pack_data) { - $parsed_pack = self::parse_pack_data($pack_data); - $objects = $parsed_pack['objects']; - - $by_oid = []; - $by_offset = []; - $resolved_objects = 0; - // Index entities and resolve deltas - // Run until all objects are resolved - while($resolved_objects < count($objects)) { - $resolved_in_this_iteration = 0; - for($i = 0; $i < count($objects); $i++) { - // Skip already processed objects - if( - isset($by_offset[$objects[$i]['header_offset']]) && - isset($by_oid[$objects[$i]['oid']]) - ) { - continue; - } - - if($objects[$i]['type'] === self::OBJECT_TYPE_OFS_DELTA) { - $target_offset = $objects[$i]['header_offset'] - $objects[$i]['ofs']; - if(!isset($by_offset[$target_offset])) { - continue; - } - // TODO: Make sure the base object will never be another delta. - $base = $objects[$by_offset[$target_offset]]; - $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); - $objects[$i]['type'] = $base['type']; - } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { - if(!isset($by_oid[$objects[$i]['reference']])) { - continue; - } - $base = $objects[$by_oid[$objects[$i]['reference']]]; - $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); - $objects[$i]['type'] = $base['type']; - } - $oid = sha1(self::wrap_git_object($objects[$i]['type'], $objects[$i]['content'])); - $objects[$i]['oid'] = $oid; - $by_oid[$oid] = $i; - $by_offset[$objects[$i]['header_offset']] = $i; - ++$resolved_in_this_iteration; - ++$resolved_objects; - } - if($resolved_in_this_iteration === 0) { - throw new Exception('Could not resolve objects'); - } - } - - // Resolve trees - foreach($objects as $k => $object) { - if( $object['type'] === self::OBJECT_TYPE_TREE ) { - $objects[$k]['content'] = self::parse_tree_bytes($object['content']); - } else if($object['type'] === self::OBJECT_TYPE_COMMIT) { - $objects[$k]['tree'] = substr($object['content'], 5, 40); - } - } + private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; - return new WP_Git_Pack_Index( - $objects, - $by_oid - ); - } + /** + * Computes Git objects needed to commit a changeset. + * + * Important! A remote git repo will only accept the objects + * produced by this method if: + * + * * The paths in each tree are sorted alphabetically. + * * There may be no duplicate blobs. + * + * @param WP_Git_Pack_Processor $oldIndex The index containing existing objects + * @param WP_Changeset $changeset The changes to commit + * @return string The Git objects with type, content and SHA + */ + public function derive_commit_pack_data( + $updates = [], + $deletes = [] + ) { + $new_index = []; + + $new_tree = new stdClass(); + foreach ($updates as $path => $content) { + $new_blob = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, + 'content' => $content, + ]); + $new_index[] = $new_blob; + $this->set_oid($new_tree, $path, $new_blob['oid']); + } + + foreach ($deletes as $path) { + $this->set_oid($new_tree, $path, self::DELETE_PLACEHOLDER); + } + + $root_tree = $this->backfill_trees($this, $new_index, $new_tree, '/'); + + // Make $new_index unique by 'oid' column + $seen_oids = []; + $new_index = array_filter($new_index, function($obj) use (&$seen_oids) { + if (isset($seen_oids[$obj['oid']])) { + return false; + } + $seen_oids[$obj['oid']] = true; + return true; + }); - static public function wrap_git_object($type, $object) { - $length = strlen($object); - $type_name = self::OBJECT_NAMES[$type]; - return "$type_name $length\x00" . $object; + return [ + 'objects' => $new_index, + 'root_tree_oid' => $root_tree['oid'], + ]; } - static private function applyDelta($base_bytes, $delta_bytes) { - $offset = 0; - - $base_size = self::readVariableLength($delta_bytes, $offset); - if($base_size !== strlen($base_bytes)) { - // @TODO: Do not throw exceptions...? Or do? - throw new Exception('Base size mismatch'); - } - $result_size = self::readVariableLength($delta_bytes, $offset); - - $result = ''; - while ($offset < strlen($delta_bytes)) { - $byte = ord($delta_bytes[$offset++]); - if ($byte & 0x80) { - $copyOffset = 0; - $copySize = 0; - if ($byte & 0x01) $copyOffset |= ord($delta_bytes[$offset++]); - if ($byte & 0x02) $copyOffset |= ord($delta_bytes[$offset++]) << 8; - if ($byte & 0x04) $copyOffset |= ord($delta_bytes[$offset++]) << 16; - if ($byte & 0x08) $copyOffset |= ord($delta_bytes[$offset++]) << 24; - if ($byte & 0x10) $copySize |= ord($delta_bytes[$offset++]); - if ($byte & 0x20) $copySize |= ord($delta_bytes[$offset++]) << 8; - if ($byte & 0x40) $copySize |= ord($delta_bytes[$offset++]) << 16; - if ($copySize === 0) $copySize = 0x10000; - $result .= substr($base_bytes, $copyOffset, $copySize); - } else { - $result .= substr($delta_bytes, $offset, $byte); - $offset += $byte; + private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $subtree_delta, $subtree_path = '/') { + $subtree_path = ltrim($subtree_path, '/'); + $new_tree_content = []; + + $indexed_tree = $current_index->get_by_path($subtree_path); + if($indexed_tree) { + foreach($indexed_tree['content'] as $object) { + // Backfill the unchanged objects from the currently indexed subtree. + $name = $object['name']; + if(!isset($subtree_delta->children[$name])) { + $new_tree_content[$name] = $object; + } } } - if(strlen($result) !== $result_size) { - // @TODO: Do not throw exceptions...? Or do? - throw new Exception('Result size mismatch'); - } - - return $result; - } - - static private function parse_tree_bytes($treeContent) { - $offset = 0; - $files = []; - - while ($offset < strlen($treeContent)) { - if ($offset >= strlen($treeContent)) { - var_dump('uninitialized string offset'); - break; // Prevent uninitialized string offset + // Index changed and new objects in the current subtree. + foreach($subtree_delta->children as $name => $subtree_child) { + // Ignore any deleted objects. + if(isset($subtree_child->oid) && $subtree_child->oid === self::DELETE_PLACEHOLDER) { + continue; } - // Read file mode - $modeEnd = strpos($treeContent, ' ', $offset); - if ($modeEnd === false || $modeEnd >= strlen($treeContent)) { - var_dump('invalid mode'); - break; // Invalid mode - } - $mode = substr($treeContent, $offset, $modeEnd - $offset); - $offset = $modeEnd + 1; - - if(preg_match('/^0?4.*/', $mode)) { - $mode = self::FILE_MODE_DIRECTORY; - } else if(preg_match('/^1006.*/', $mode)) { - $mode = self::FILE_MODE_REGULAR_NON_EXECUTABLE; - } else if(preg_match('/^1007.*/', $mode)) { - $mode = self::FILE_MODE_REGULAR_EXECUTABLE; - } else if(preg_match('/^120.*/', $mode)) { - $mode = self::FILE_MODE_SYMBOLIC_LINK; - } else if(preg_match('/^160.*/', $mode)) { - $mode = self::FILE_MODE_COMMIT; - } - - // Read file name - $nameEnd = strpos($treeContent, "\0", $offset); - if ($nameEnd === false || $nameEnd >= strlen($treeContent)) { - var_dump('invalid name'); - break; // Invalid name - } - $name = substr($treeContent, $offset, $nameEnd - $offset); - $offset = $nameEnd + 1; - - // Read SHA1 - if ($offset + 20 > strlen($treeContent)) { - var_dump('invalid sha1'); - break; // Prevent out-of-bounds access + // Index blobs + switch($subtree_child->type) { + case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: + $new_tree_content[$name] = [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'name' => $name, + 'sha1' => $subtree_child->oid, + ]; + break; + case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: + $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); + $new_tree_content[$name] = [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, + 'name' => $name, + 'sha1' => $subtree_object['oid'], + ]; + break; } - $sha1 = bin2hex(substr($treeContent, $offset, 20)); - $offset += 20; - - $files[$name] = [ - 'mode' => $mode, - 'name' => $name, - 'sha1' => $sha1, - ]; } - return $files; - } + $new_tree_object = WP_Git_Pack_Processor::create_object([ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, + 'content' => $new_tree_content, + ]); - static private function readVariableLength($data, &$offset) { - $result = 0; - $shift = 0; - do { - $byte = ord($data[$offset++]); - $result |= ($byte & 0x7F) << $shift; - $shift += 7; - } while ($byte & 0x80); - return $result; + $new_index[] = $new_tree_object; + return $new_tree_object; } - static private function parse_pack_data($packData) { - $offset = 0; - - // Basic sanity checks - if (strlen($packData) < 12) { - return false; - } - - $header = substr($packData, $offset, 4); - $offset += 4; - if ($header !== "PACK") { - return false; - } - - $version = unpack('N', substr($packData, $offset, 4))[1]; - $offset += 4; - - $objectCount = unpack('N', substr($packData, $offset, 4))[1]; - $offset += 4; - - $objects = []; - - for ($i = 0; $i < $objectCount; $i++) { - if ($offset >= strlen($packData)) { - break; - } + private function set_oid($root_tree, $path, $oid) { + $blob = new stdClass(); + $blob->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; + $blob->oid = $oid; - $header_offset = $offset; - $object = self::parse_pack_header($packData, $offset); - $object['header_offset'] = $header_offset; - $object['content'] = self::inflate_object($packData, $offset, $object['uncompressed_length']); - $objects[] = $object; + $subtree_path = dirname($path); + if($subtree_path === '.') { + $subtree = $root_tree; + } else { + $subtree = $this->get_subtree($root_tree, $subtree_path); } - return [ - 'objects' => $objects, - 'total_objects' => $objectCount, - 'pack_version' => $version - ]; + $filename = basename($path); + $subtree->children[$filename] = $blob; } - /** - * Incrementally inflate the next object’s compressed data until it yields - * $uncompressedSize bytes, or we hit the end of the compressed stream. - * Adjusts $offset so that after returning, $offset points to the next object header. - */ - static private function inflate_object(string $packData, int &$offset, int $uncompressedSize): ?string { - $inflateContext = inflate_init(ZLIB_ENCODING_DEFLATE); - if (!$inflateContext) { - return null; - } - - $inflated = ''; - $packLen = strlen($packData); - - $bytes_read = 0; - while ($offset < $packLen) { - // Feed chunks into inflate. We don’t know how big each chunk is, - // so let's just pick something arbitrary: - $chunk = substr($packData, $offset, 256); - - $res = inflate_add($inflateContext, $chunk); - switch(inflate_get_status($inflateContext)) { - case ZLIB_BUF_ERROR: - case ZLIB_DATA_ERROR: - case ZLIB_VERSION_ERROR: - case ZLIB_MEM_ERROR: - throw new Exception('Inflate error'); - } - if ($res === false) { - throw new Exception('Inflate error'); - } - $bytes_read_for_this_chunk = inflate_get_read_len($inflateContext) - $bytes_read; - $offset += $bytes_read_for_this_chunk; - - $bytes_read = inflate_get_read_len($inflateContext); - $inflated .= $res; - - if(inflate_get_status($inflateContext) === ZLIB_STREAM_END) { - break; - } - } - return $inflated; - } - - static private function parse_pack_header(string $packData, int &$offset) { - // Object type is encoded in bits 654 - $byte = ord($packData[$offset++]); - $type = ($byte >> 4) & 0b111; - // The length encoding get complicated. - // Last four bits of length is encoded in bits 3210 - $uncompressed_length = $byte & 0b1111; - // Whether the next byte is part of the variable-length encoded number - // is encoded in bit 7 - if ($byte & 0b10000000) { - $shift = 4; - $byte = ord($packData[$offset++]); - while ($byte & 0b10000000) { - $uncompressed_length |= ($byte & 0b01111111) << $shift; - $shift += 7; - $byte = ord($packData[$offset++]); - } - $uncompressed_length |= ($byte & 0b01111111) << $shift; - } - // Handle deltified objects - $ofs = null; - $reference = null; - if ($type === self::OBJECT_TYPE_OFS_DELTA) { - // Git uses a specific formula: ofs = ((ofs + 1) << 7) + (c & 0x7f) - // for each continuation byte. The first byte doesn't do the "ofs+1" part. - // This code matches Git’s logic. - $ofs = 0; - // Read the first byte - $c = ord($packData[$offset++]); - $ofs = ($c & 0x7F); - - // If bit 7 (0x80) is set, we keep reading - while ($c & 0x80) { - $c = ord($packData[$offset++]); - $ofs = (($ofs + 1) << 7) + ($c & 0x7F); - } - } else if ($type === self::OBJECT_TYPE_REF_DELTA) { - $reference = substr($packData, $offset, 20); - $offset += 20; - } - return [ - 'ofs' => $ofs, - 'type' => $type, - 'uncompressed_length' => $uncompressed_length, - 'reference' => $reference - ]; + private function get_subtree($root_tree, $path) { + $path = trim($path, '/'); + $segments = explode('/', $path); + $subtree = $root_tree; + foreach ($segments as $segment) { + if (!isset($subtree->children[$segment])) { + $new_subtree = new stdClass(); + $new_subtree->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; + $new_subtree->children = []; + $subtree->children[$segment] = $new_subtree; + } + $subtree = $subtree->children[$segment]; + } + return $subtree; } } \ No newline at end of file diff --git a/packages/playground/data-liberation-static-files-editor/push-simple.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php similarity index 57% rename from packages/playground/data-liberation-static-files-editor/push-simple.php rename to packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 940786efb8..2bcfffddc4 100644 --- a/packages/playground/data-liberation-static-files-editor/push-simple.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -1,13 +1,4 @@ - */ - public $create; - - /** - * Updated files. - * @var array - */ - public $update; - - /** - * Deleted files. - * @var array - */ - public $delete; - - public function __construct($create = [], $update = [], $delete = []) { - $this->create = $create; - $this->update = $update; - $this->delete = $delete; - } -}; - -class WP_Git_Utils { - - private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; - - /** - * Computes Git objects needed to commit a changeset. - * - * @param WP_Git_Pack_Processor $oldIndex The index containing existing objects - * @param WP_Changeset $changeset The changes to commit - * @return string The Git objects with type, content and SHA - */ - public function compute_push_objects( - WP_Git_Pack_Index $index, - WP_Changeset $changeset, - $branchName, - $parent_hash = null - ) { - $new_index = []; - - $new_tree = new stdClass(); - foreach (array_merge($changeset->create, $changeset->update) as $path => $content) { - $new_blob = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, - 'content' => $content, - ]); - $new_index[] = $new_blob; - $this->set_oid($new_tree, $path, $new_blob['oid']); - } - - foreach ($changeset->delete as $path) { - $this->set_oid($new_tree, $path, self::DELETE_PLACEHOLDER); - } - - if(!$parent_hash) { - $parent_hash = "0000000000000000000000000000000000000000"; - } - $parent = ''; - if($parent_hash) { - $parent = "parent $parent_hash\n"; - } - $root_tree = $this->backfill_trees($index, $new_index, $new_tree, '/'); - $commit = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, - 'content' => sprintf( - "tree %s\n{$parent}author %s\ncommitter %s\n\n%s\n", - $root_tree['oid'], - "John Doe " . time() . " +0000", - "John Doe " . time() . " +0000", - "Hello!" - ), - 'tree' => $root_tree['oid'], - ]); - $commitSha = $commit['oid']; - $new_index[] = $commit; - // Make $new_index unique by 'oid' column - $seen_oids = []; - $new_index = array_filter($new_index, function($obj) use (&$seen_oids) { - if (isset($seen_oids[$obj['oid']])) { - return false; - } - $seen_oids[$obj['oid']] = true; - return true; - }); - // $new_index = array_reverse($new_index); - - var_dump($new_index); - - $pktPush = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $commitSha refs/heads/$branchName\0report-status force-update\n"); - $pktPush .= "0000"; - $pktPush .= WP_Git_Pack_Processor::encode($new_index); - $pktPush .= "0000"; - - return $pktPush; - } - - private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $subtree_delta, $subtree_path = '/') { - $subtree_path = ltrim($subtree_path, '/'); - $new_tree_content = []; - - $indexed_tree = $current_index->get_by_path($subtree_path); - if($indexed_tree) { - foreach($indexed_tree['content'] as $object) { - // Backfill the unchanged objects from the currently indexed subtree. - $name = $object['name']; - if(!isset($subtree_delta->children[$name])) { - $new_tree_content[$name] = $object; - } - } - } - - // Index changed and new objects in the current subtree. - foreach($subtree_delta->children as $name => $subtree_child) { - // Ignore any deleted objects. - if(isset($subtree_child->oid) && $subtree_child->oid === self::DELETE_PLACEHOLDER) { - continue; - } - - // Index blobs - switch($subtree_child->type) { - case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: - $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, - 'name' => $name, - 'sha1' => $subtree_child->oid, - ]; - break; - case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: - $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); - $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, - 'name' => $name, - 'sha1' => $subtree_object['oid'], - ]; - break; - } - } - - $new_tree_object = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, - 'content' => $new_tree_content, - ]); - - $new_index[] = $new_tree_object; - return $new_tree_object; - } - - private function set_oid($root_tree, $path, $oid) { - $blob = new stdClass(); - $blob->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; - $blob->oid = $oid; - - $subtree_path = dirname($path); - if($subtree_path === '.') { - $subtree = $root_tree; - } else { - $subtree = $this->get_subtree($root_tree, $subtree_path); - } - $filename = basename($path); - $subtree->children[$filename] = $blob; - } - - private function get_subtree($root_tree, $path) { - $path = trim($path, '/'); - $segments = explode('/', $path); - $subtree = $root_tree; - foreach ($segments as $segment) { - if (!isset($subtree->children[$segment])) { - $new_subtree = new stdClass(); - $new_subtree->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; - $new_subtree->children = []; - $subtree->children[$segment] = $new_subtree; - } - $subtree = $subtree->children[$segment]; - } - return $subtree; - } - -} - -$client = new WP_Git_Client("https://github.com/adamziel/pantheon-playground-demo.git"); -$head_hash = $client->fetchRefs('HEAD')['HEAD']; -$index = $client->list_objects($head_hash); - -$utils = new WP_Git_Utils(); -$push_bytes = $utils->compute_push_objects( - $index, - new WP_Changeset( - [ - // Weird! This only works if these paths are sorted alphabetically. Do I have to sort pack data in a particular order? - // Perhaps topological sort matters? - // Also, the content of each file must be differnet. I suspect Github rejects duplicate blobs? - 'wp-content/plugin/light-mode-another-two.css' => '/* Light uu mode */', - // 'wp-content/dark-mode.css' => '/* Dark mode */', - // 'wp-admin/tzeme/dark-mode3.css' => '/* Dark mode xx */', - // 'wp-content/azeme/dark-mode4.css' => '/* Dark mode xxx */', - // 'wp-content/ztra/dark-mode.css' => '/* differddent */', - ], - [], - [] - ), - 'main', - $head_hash -); - -// $pack_data = substr($push_bytes, strpos($push_bytes, "PACK")); -// $new_index = WP_Git_Pack_Processor::from_pack_data($pack_data); -// var_dump($new_index); - -// die('......'); -$repoUrl = "https://" . GITHUB_TOKEN . "@github.com/adamziel/pantheon-playground-demo.git"; - -// 6) POST this to "/git-receive-pack" -$url = rtrim($repoUrl, '.git').'.git/git-receive-pack'; -$response = httpPost($url, $push_bytes, [ - 'Content-Type: application/x-git-receive-pack-request', - 'Accept: application/x-git-receive-pack-result', -]); - -// 7) Check the response. If it contains "unpack ok" and "ok refs/heads/my-playground", -// we succeeded. Otherwise, there’s an error message in side-band or progress -// channels. We’ll just echo it here for debugging. -echo "Push response:\n$response\n"; -die(); - -// ----------------------------------------------------------------------------- -// Example usage -try { - // Use authorization header instead of basic auth in URL for better security - pushSingleFileOverHttp( - "https://" . GITHUB_TOKEN . "@github.com/adamziel/pantheon-playground-demo.git", - "my-playground", - // Use 40 zeros to create a new branch - // "0000000000000000000000000000000000000000" - "b7a8ab2605e6d7fbec140cba3d28e94d31d2a6aa" - // "main", - // "a9abffe7d324a4b9343eaad3f5e4b35c584448b6" - ); -} catch (Exception $e) { - echo "Error: ", $e->getMessage(), "\n"; -} - - - -//-------- - - -// $readme_md = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, -// 'content' => '## This is a Readme file', -// ]); -// $index_html = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, -// 'content' => '

Hello, world!

', -// ]); -// $style_css = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, -// 'content' => 'body { background-color: red; }', -// ]); - -// $html_pages = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, -// 'content' => [ -// [ -// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, -// 'name' => 'readme.md', -// 'sha1' => $readme_md['oid'], -// ], -// [ -// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, -// 'name' => 'index.html', -// 'sha1' => $index_html['oid'], -// ], -// ], -// ]); - -// $theme_tree = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, -// 'content' => [ -// [ -// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, -// 'name' => 'style.css', -// 'sha1' => $style_css['oid'], -// ], -// ], -// ]); - -// $wp_content_tree = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, -// 'content' => [ -// [ -// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, -// 'name' => 'html-pages', -// 'sha1' => $html_pages['oid'], -// ], -// [ -// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, -// 'name' => 'theme', -// 'sha1' => $theme_tree['oid'], -// ], -// ], -// ]); - -// $root_tree = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, -// 'content' => [ -// [ -// 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, -// 'name' => 'wp-content', -// 'sha1' => $wp_content_tree['oid'], -// ], -// ], -// ]); - -// $commit = create_object([ -// 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, -// 'content' => 'Hello, world!', -// 'tree' => $root_tree['oid'], -// ]); - - -// $push_bytes = $utils->compute_push_objects( -// new WP_Git_Pack_Processor([ -// $commit, -// $root_tree, -// $wp_content_tree, -// $html_pages, -// $theme_tree, -// $style_css, -// $index_html, -// $readme_md, -// ]), -// new WP_Changeset( -// [ -// 'wp-content/theme/light-mode.css' => '/* Light mode */', -// 'wp-content/theme/dark-mode.css' => '/* Dark mode */', -// ] -// ), -// 'my-new-branch' -// ); - diff --git a/packages/playground/data-liberation/tests/WPDirectoryTreeEntityReaderTests.php b/packages/playground/data-liberation/tests/WPDirectoryTreeEntityReaderTests.php index 1d9745679f..24533da0ce 100644 --- a/packages/playground/data-liberation/tests/WPDirectoryTreeEntityReaderTests.php +++ b/packages/playground/data-liberation/tests/WPDirectoryTreeEntityReaderTests.php @@ -6,7 +6,7 @@ class WPDirectoryTreeEntityReaderTests extends TestCase { public function test_with_create_index_pages_true() { $reader = WP_Directory_Tree_Entity_Reader::create( - new WordPress\Filesystem\WP_Filesystem(), + new WordPress\Filesystem\WP_Local_Filesystem(), [ 'root_dir' => __DIR__ . '/fixtures/directory-tree-entity-reader', 'first_post_id' => 2, @@ -40,7 +40,7 @@ public function test_with_create_index_pages_true() { public function test_with_create_index_pages_false() { $reader = WP_Directory_Tree_Entity_Reader::create( - new WordPress\Filesystem\WP_Filesystem(), + new WordPress\Filesystem\WP_Local_Filesystem(), [ 'root_dir' => __DIR__ . '/fixtures/directory-tree-entity-reader', 'first_post_id' => 2, diff --git a/packages/playground/data-liberation/tests/WPFilesystemToPostHierarchyTests.php b/packages/playground/data-liberation/tests/WPFilesystemToPostHierarchyTests.php index 87240aaa2c..c39c33f1ff 100644 --- a/packages/playground/data-liberation/tests/WPFilesystemToPostHierarchyTests.php +++ b/packages/playground/data-liberation/tests/WPFilesystemToPostHierarchyTests.php @@ -6,7 +6,7 @@ class WPFilesystemToPostHierarchyTests extends TestCase { public function test_with_create_index_pages_true() { $reader = WP_Filesystem_To_Post_Hierarchy::create( - new WordPress\Filesystem\WP_Filesystem(), + new WordPress\Filesystem\WP_Local_Filesystem(), [ 'root_dir' => __DIR__ . '/fixtures/directory-tree-entity-reader', 'first_post_id' => 2, From cefacc2850761b370f79983add0b787363d4a7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 22:10:33 +0100 Subject: [PATCH 11/71] Tweak new file creation and metadata loading --- .../src/WP_Markdown_To_Blocks.php | 1 - .../plugin.php | 31 +-- .../src/components/FilePickerTree/index.tsx | 217 ++++++++++-------- .../ui/src/index.tsx | 53 ++--- .../data-liberation/src/git/WP_Git_Client.php | 155 ------------- 5 files changed, 156 insertions(+), 301 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index 38856f6bf2..bf1bca49ce 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -71,7 +71,6 @@ private function convert_markdown_to_blocks() { ); $parser = new MarkdownParser( $environment ); - $document = $parser->parse( $this->markdown ); $this->frontmatter = array(); foreach ( $document->data->export() as $key => $value ) { diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 8382dc46db..215f9387a5 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -35,8 +35,9 @@ }); } -require_once __DIR__ . '/WP_Static_File_Sync.php'; -require_once __DIR__ . '/secrets.php'; +if(file_exists(__DIR__ . '/secrets.php')) { + require_once __DIR__ . '/secrets.php'; +} class WP_Static_Files_Editor_Plugin { @@ -58,7 +59,7 @@ static private function get_fs() { static public function initialize() { // Register hooks - // register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); + register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); add_action('init', function() { self::register_post_type(); @@ -132,7 +133,7 @@ static public function initialize() { // Preload the initial files tree wp_add_inline_script('wp-api-fetch', 'wp.apiFetch.use(wp.apiFetch.createPreloadingMiddleware({ "/static-files-editor/v1/get-files-tree": { - body: '.json_encode(WP_Static_Files_Editor_Plugin::get_files_tree_endpoint()).' + body: '.json_encode(WP_Static_Files_Editor_Plugin::get_files_tree_endpoint()).', } }));', 'after'); }); @@ -254,16 +255,16 @@ static private function refresh_post_from_local_file($post) { } $extension = pathinfo($path, PATHINFO_EXTENSION); switch($extension) { - case 'md': - $converter = new WP_Markdown_To_Blocks( $content ); - break; case 'xhtml': $converter = new WP_HTML_To_Blocks( WP_XML_Processor::create_from_string( $content ) ); break; case 'html': - default: $converter = new WP_HTML_To_Blocks( WP_HTML_Processor::create_fragment( $content ) ); break; + case 'md': + default: + $converter = new WP_Markdown_To_Blocks( $content ); + break; } $converter->convert(); @@ -276,6 +277,9 @@ static private function refresh_post_from_local_file($post) { $updated = wp_update_post(array( 'ID' => $post_id, 'post_content' => $new_content, + 'post_title' => $metadata['post_title'] ?? '', + 'post_date_gmt' => $metadata['post_date_gmt'] ?? '', + 'menu_order' => $metadata['menu_order'] ?? '', // 'meta_input' => $metadata, )); if(is_wp_error($updated)) { @@ -306,12 +310,13 @@ static private function save_post_data_to_local_file($post) { $fs = self::get_fs(); $path = get_post_meta($post_id, 'local_file_path', true); $extension = pathinfo($path, PATHINFO_EXTENSION); - $metadata = get_post_meta($post_id); + $metadata = []; + foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { + $metadata[$key] = get_post_field($key, $post_id); + } + // @TODO: Also include actual post_meta. Which ones? All? The + // ones explicitly set by the user in the editor? - // @TODO: Include specific post fields in the stored metadata - // foreach(WP_Imported_Entity::POST_FIELDS as $field) { - // $metadata[$field] = get_post_field($field, $post_id); - // } $content = get_post_field('post_content', $post_id); switch($extension) { // @TODO: Add support for HTML and XHTML diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 684c4e21a7..a7106839b9 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -19,15 +19,16 @@ export type FileNode = { children?: FileNode[]; }; +export type CreatedNode = { + type: 'file' | 'folder'; + path: string; +}; + export type FilePickerControlProps = { files: FileNode[]; initialPath?: string; onSelect?: (path: string, node: FileNode) => void; - onNodeCreated?: ( - type: 'file' | 'folder', - path: string, - name: string - ) => void; + onNodeCreated?: (node: CreatedNode) => void; isLoading?: boolean; error?: string; }; @@ -87,10 +88,14 @@ export const FilePickerTree: React.FC = ({ }; const handleCreateNode = (type: 'file' | 'folder') => { - if (!selectedPath) return; - - // Find selected node - const pathParts = selectedPath.split('/'); + if (!selectedPath) { + setTempNode({ + type, + parentPath: '', + }); + return; + } + const pathParts = selectedPath.split('/') || []; let currentNode: FileNode | undefined = undefined; let currentNodes = files; @@ -100,20 +105,11 @@ export const FilePickerTree: React.FC = ({ currentNodes = currentNode.children || []; } - if (!currentNode) return; - // If selected node is a file, use its parent path const parentPath = - currentNode.type === 'file' - ? pathParts.slice(0, -1).join('/') - : selectedPath; - - console.log({ - parentPath, - currentNode, - selectedPath, - pathParts, - }); + currentNode?.type === 'folder' && expanded[selectedPath] + ? selectedPath + : pathParts.slice(0, -1).join('/'); expandNode(parentPath, true); setTempNode({ type, parentPath }); @@ -121,7 +117,12 @@ export const FilePickerTree: React.FC = ({ const handleTempNodeComplete = (name: string) => { if (!tempNode) return; - onNodeCreated(tempNode.type, tempNode.parentPath, name); + // @TODO: Replace with joinPaths() + const fullPath = `${tempNode.parentPath}/${name}`.replace(/\/+/g, '/'); + onNodeCreated({ + type: tempNode.type, + path: fullPath, + }); setTempNode(null); }; @@ -132,6 +133,11 @@ export const FilePickerTree: React.FC = ({ const [searchBuffer, setSearchBuffer] = useState(''); const searchBufferTimeoutRef = useRef(null); function handleKeyDown(event: React.KeyboardEvent) { + // Don't filter if we're creating a new file or folder – + // this would only blur and hide the filename input. + if (tempNode) { + return; + } if (event.key.length === 1 && event.key.match(/\S/)) { const newSearchBuffer = searchBuffer + event.key.toLowerCase(); setSearchBuffer(newSearchBuffer); @@ -217,41 +223,48 @@ export const FilePickerTree: React.FC = ({ return (
- - - - +
+ + + + +
- {files.map((file, index) => ( - - ))} +
); @@ -262,6 +275,7 @@ const NodeRow: React.FC<{ level: number; position: number; setSize: number; + isRoot: boolean; expandedNodePaths: ExpandedNodePaths; expandNode: (path: string, isOpen: boolean) => void; selectPath: (path: string) => void; @@ -277,6 +291,7 @@ const NodeRow: React.FC<{ level, position, setSize, + isRoot, expandedNodePaths, expandNode, selectPath, @@ -288,7 +303,7 @@ const NodeRow: React.FC<{ onTempNodeCancel, }) => { const path = generatePath(node, parentPath); - const isExpanded = expandedNodePaths[path]; + const isExpanded = isRoot || expandedNodePaths[path]; const toggleOpen = () => expandNode(path, !isExpanded); @@ -334,54 +349,54 @@ const NodeRow: React.FC<{ return ( <> - - - {() => ( - + )} + + + )} + {tempNode && tempNode.parentPath === path && ( + + + {() => ( + - - )} - - + )} + + + )} {isExpanded && ( <> - {tempNode && tempNode.parentPath === path && ( - - - {() => ( - - )} - - - )} {node.children && node.children.map((child, index) => ( { - const tree = await apiFetch({ + fileTreePromise = apiFetch({ path: '/static-files-editor/v1/get-files-tree', }); - setFileTree(tree); + setFileTree(await fileTreePromise); }, []); useEffect(() => { @@ -73,8 +77,19 @@ function ConnectedFilePickerTree() { }); }; - const handleCreateFile = async () => { - const newFilePath = `${selectedPath}/untitled.md`.replace(/\/+/g, '/'); + const handleNodeCreated = async (node: CreatedNode) => { + if (node.type === 'file') { + await createEmptyFile(node.path); + } else if (node.type === 'folder') { + // Create an empty .gitkeep file in the new directory + // to make sure it will actually be created in the filesystem. + // @TODO: Rethink this approach. Ideally we could just display the + // directory in the tree, and let the user create files inside it. + await createEmptyFile(node.path + '/.gitkeep'); + } + }; + + const createEmptyFile = async (newFilePath: string) => { try { const response = (await apiFetch({ path: '/static-files-editor/v1/get-or-create-post-for-file', @@ -96,21 +111,6 @@ function ConnectedFilePickerTree() { } }; - const handleCreateDirectory = async () => { - try { - await apiFetch({ - path: '/static-files-editor/v1/create-directory', - method: 'POST', - data: { - path: `${selectedPath}/empty`.replace(/\/+/g, '/'), - }, - }); - await refreshFileTree(); - } catch (error) { - console.error('Failed to create directory:', error); - } - }; - if (isLoading) { return ; } @@ -121,20 +121,11 @@ function ConnectedFilePickerTree() { return (
-
- - -
); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 72d68b3e3b..1ef4b3dcff 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -243,160 +243,5 @@ static public function parse_multiplexed_pack_data($bytes) { } } } - - private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; - - /** - * Computes Git objects needed to commit a changeset. - * - * @param WP_Git_Pack_Processor $oldIndex The index containing existing objects - * @param WP_Changeset $changeset The changes to commit - * @return string The Git objects with type, content and SHA - */ - public function compute_push_objects( - WP_Git_Pack_Index $index, - WP_Changeset $changeset, - $branchName, - $parent_hash = null - ) { - $new_index = []; - - $new_tree = new stdClass(); - foreach (array_merge($changeset->create, $changeset->update) as $path => $content) { - $new_blob = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, - 'content' => $content, - ]); - $new_index[] = $new_blob; - $this->set_oid($new_tree, $path, $new_blob['oid']); - } - - foreach ($changeset->delete as $path) { - $this->set_oid($new_tree, $path, self::DELETE_PLACEHOLDER); - } - - if(!$parent_hash) { - $parent_hash = "0000000000000000000000000000000000000000"; - } - $parent = ''; - if($parent_hash) { - $parent = "parent $parent_hash\n"; - } - $root_tree = $this->backfill_trees($index, $new_index, $new_tree, '/'); - $commit = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, - 'content' => sprintf( - "tree %s\n{$parent}author %s\ncommitter %s\n\n%s\n", - $root_tree['oid'], - "John Doe " . time() . " +0000", - "John Doe " . time() . " +0000", - "Hello!" - ), - 'tree' => $root_tree['oid'], - ]); - $commit_sha = $commit['oid']; - $new_index[] = $commit; - // Make $new_index unique by 'oid' column - $seen_oids = []; - $new_index = array_filter($new_index, function($obj) use (&$seen_oids) { - if (isset($seen_oids[$obj['oid']])) { - return false; - } - $seen_oids[$obj['oid']] = true; - return true; - }); - // $new_index = array_reverse($new_index); - - var_dump($new_index); - - $pktPush = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $commit_sha refs/heads/$branchName\0report-status force-update\n"); - $pktPush .= "0000"; - $pktPush .= WP_Git_Pack_Processor::encode($new_index); - $pktPush .= "0000"; - - return $pktPush; - } - - private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $subtree_delta, $subtree_path = '/') { - $subtree_path = ltrim($subtree_path, '/'); - $new_tree_content = []; - - $indexed_tree = $current_index->get_by_path($subtree_path); - if($indexed_tree) { - foreach($indexed_tree['content'] as $object) { - // Backfill the unchanged objects from the currently indexed subtree. - $name = $object['name']; - if(!isset($subtree_delta->children[$name])) { - $new_tree_content[$name] = $object; - } - } - } - - // Index changed and new objects in the current subtree. - foreach($subtree_delta->children as $name => $subtree_child) { - // Ignore any deleted objects. - if(isset($subtree_child->oid) && $subtree_child->oid === self::DELETE_PLACEHOLDER) { - continue; - } - - // Index blobs - switch($subtree_child->type) { - case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: - $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, - 'name' => $name, - 'sha1' => $subtree_child->oid, - ]; - break; - case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: - $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); - $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, - 'name' => $name, - 'sha1' => $subtree_object['oid'], - ]; - break; - } - } - - $new_tree_object = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, - 'content' => $new_tree_content, - ]); - - $new_index[] = $new_tree_object; - return $new_tree_object; - } - - private function set_oid($root_tree, $path, $oid) { - $blob = new stdClass(); - $blob->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; - $blob->oid = $oid; - - $subtree_path = dirname($path); - if($subtree_path === '.') { - $subtree = $root_tree; - } else { - $subtree = $this->get_subtree($root_tree, $subtree_path); - } - $filename = basename($path); - $subtree->children[$filename] = $blob; - } - - private function get_subtree($root_tree, $path) { - $path = trim($path, '/'); - $segments = explode('/', $path); - $subtree = $root_tree; - foreach ($segments as $segment) { - if (!isset($subtree->children[$segment])) { - $new_subtree = new stdClass(); - $new_subtree->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; - $new_subtree->children = []; - $subtree->children[$segment] = $new_subtree; - } - $subtree = $subtree->children[$segment]; - } - return $subtree; - } } From 18e73b824b158268064fd53e3319e8ff7026a86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 22:15:11 +0100 Subject: [PATCH 12/71] Add specific exception for renaming directories --- .../playground/data-liberation/src/git/WP_Git_Filesystem.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index aa58ee8504..246fa9dec4 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -136,6 +136,9 @@ private function get_index() { // for dealing with a local filesystem. public function rename($old_path, $new_path) { + if($this->is_dir($old_path)) { + throw new Exception('Renaming directories is not supported yet'); + } if(!$this->is_file($old_path)) { return false; } From 0da644759091d4846700d8926711da03b94c455a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 22:52:35 +0100 Subject: [PATCH 13/71] Support renaming and deleting files --- .../components/src/FilePickerTree/index.tsx | 12 +- .../plugin.php | 52 ++++ .../src/components/FilePickerTree/index.tsx | 225 +++++++++++++----- .../ui/src/index.tsx | 46 +++- 4 files changed, 267 insertions(+), 68 deletions(-) diff --git a/packages/playground/components/src/FilePickerTree/index.tsx b/packages/playground/components/src/FilePickerTree/index.tsx index 29ae8146f1..de0a29b1c3 100644 --- a/packages/playground/components/src/FilePickerTree/index.tsx +++ b/packages/playground/components/src/FilePickerTree/index.tsx @@ -5,8 +5,9 @@ import { __experimentalTreeGridCell as TreeGridCell, Button, Spinner, + DropdownMenu, } from '@wordpress/components'; -import { Icon, chevronRight, chevronDown } from '@wordpress/icons'; +import { Icon, chevronRight, chevronDown, more } from '@wordpress/icons'; import '@wordpress/components/build-style/style.css'; import css from './style.module.css'; import classNames from 'classnames'; @@ -277,6 +278,15 @@ const NodeRow: React.FC<{ )} + + {() => ( + + )} + {isExpanded && node.children && diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 215f9387a5..c90d46466b 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -162,6 +162,14 @@ static public function initialize() { return current_user_can('edit_posts'); }, )); + + register_rest_route('static-files-editor/v1', '/rename-file', array( + 'methods' => 'POST', + 'callback' => array(self::class, 'rename_file_endpoint'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + )); }); // @TODO: the_content and rest_prepare_local_file filters run twice for REST API requests. @@ -201,6 +209,18 @@ static public function initialize() { return $response; }, 10, 3); + // Delete the associated file when a post is deleted + add_action('before_delete_post', function($post_id) { + $post = get_post($post_id); + if ($post && $post->post_type === WP_LOCAL_FILE_POST_TYPE) { + $fs = self::get_fs(); + $path = get_post_meta($post_id, 'local_file_path', true); + if ($path && $fs->is_file($path)) { + $fs->rm($path); + } + } + }); + // Update the file after post is saved add_action('save_post_' . WP_LOCAL_FILE_POST_TYPE, function($post_id, $post, $update) { self::save_post_data_to_local_file($post); @@ -560,6 +580,38 @@ static public function create_directory_endpoint($request) { return array('success' => true); } + static public function rename_file_endpoint($request) { + $original_path = $request->get_param('originalPath'); + $new_path = $request->get_param('newPath'); + + if (!$original_path || !$new_path) { + return new WP_Error('missing_path', 'Both original and new paths are required'); + } + + // Find and update associated post + $existing_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $original_path, + 'posts_per_page' => 1 + )); + + $fs = self::get_fs(); + if (!$fs->rename($original_path, $new_path)) { + return new WP_Error('rename_failed', 'Failed to rename file'); + } + + if (!empty($existing_posts)) { + update_post_meta($existing_posts[0]->ID, 'local_file_path', $new_path); + wp_update_post(array( + 'ID' => $existing_posts[0]->ID, + 'post_title' => basename($new_path) + )); + } + + return array('success' => true); + } + } WP_Static_Files_Editor_Plugin::initialize(); diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index a7106839b9..9162d043d9 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -6,8 +6,9 @@ import { Button, Spinner, ButtonGroup, + DropdownMenu, } from '@wordpress/components'; -import { Icon, chevronRight, chevronDown } from '@wordpress/icons'; +import { Icon, chevronRight, chevronDown, more } from '@wordpress/icons'; import '@wordpress/components/build-style/style.css'; import css from './style.module.css'; import classNames from 'classnames'; @@ -29,15 +30,19 @@ export type FilePickerControlProps = { initialPath?: string; onSelect?: (path: string, node: FileNode) => void; onNodeCreated?: (node: CreatedNode) => void; + onNodeRenamed?: (originalPath: string, newPath: string) => void; + onNodeDeleted?: (path: string) => void; isLoading?: boolean; error?: string; }; type ExpandedNodePaths = Record; -type TempNode = { - type: 'file' | 'folder'; - parentPath: string; +type EditedNode = { + reason: 'rename' | 'create'; + type?: 'file' | 'folder'; + parentPath?: string; + originalPath?: string; }; export const FilePickerTree: React.FC = ({ @@ -49,6 +54,12 @@ export const FilePickerTree: React.FC = ({ onNodeCreated = (...args) => { console.log('onNodeCreated', args); }, + onNodeDeleted = (path: string) => { + console.log('onNodeDeleted', path); + }, + onNodeRenamed = (args: { originalPath: string; newPath: string }) => { + console.log('onNodeRenamed', args); + }, }) => { initialPath = initialPath ? initialPath.replace(/^\/+/, '') : '/'; const [expanded, setExpanded] = useState(() => { @@ -67,7 +78,7 @@ export const FilePickerTree: React.FC = ({ initialPath ? initialPath : null ); - const [tempNode, setTempNode] = useState(null); + const [editedNode, setEditedNode] = useState(null); const expandNode = (path: string, isOpen: boolean) => { setExpanded((prevState) => ({ @@ -87,9 +98,17 @@ export const FilePickerTree: React.FC = ({ : node.name; }; + const handleRenameRequest = (path: string, newName: string) => { + setEditedNode({ + reason: 'rename', + originalPath: path, + }); + }; + const handleCreateNode = (type: 'file' | 'folder') => { if (!selectedPath) { - setTempNode({ + setEditedNode({ + reason: 'create', type, parentPath: '', }); @@ -112,22 +131,40 @@ export const FilePickerTree: React.FC = ({ : pathParts.slice(0, -1).join('/'); expandNode(parentPath, true); - setTempNode({ type, parentPath }); + setEditedNode({ + reason: 'create', + type, + parentPath, + }); }; - const handleTempNodeComplete = (name: string) => { - if (!tempNode) return; + const handleEditedNodeComplete = (name: string) => { + if (!editedNode) return; // @TODO: Replace with joinPaths() - const fullPath = `${tempNode.parentPath}/${name}`.replace(/\/+/g, '/'); - onNodeCreated({ - type: tempNode.type, - path: fullPath, - }); - setTempNode(null); + if (editedNode.reason === 'rename') { + const newPath = [ + ...editedNode.originalPath.split('/').slice(0, -1), + name, + ].join('/'); + onNodeRenamed({ + originalPath: editedNode.originalPath, + newPath, + }); + } else { + const fullPath = `${editedNode.parentPath}/${name}`.replace( + /\/+/g, + '/' + ); + onNodeCreated({ + type: editedNode.type, + path: fullPath, + }); + } + setEditedNode(null); }; - const handleTempNodeCancel = () => { - setTempNode(null); + const handleEditedNodeCancel = () => { + setEditedNode(null); }; const [searchBuffer, setSearchBuffer] = useState(''); @@ -135,7 +172,7 @@ export const FilePickerTree: React.FC = ({ function handleKeyDown(event: React.KeyboardEvent) { // Don't filter if we're creating a new file or folder – // this would only blur and hide the filename input. - if (tempNode) { + if (editedNode) { return; } if (event.key.length === 1 && event.key.match(/\S/)) { @@ -261,9 +298,11 @@ export const FilePickerTree: React.FC = ({ selectedNode={selectedPath} selectPath={selectPath} generatePath={generatePath} - tempNode={tempNode} - onTempNodeComplete={handleTempNodeComplete} - onTempNodeCancel={handleTempNodeCancel} + editedNode={editedNode} + onEditedNodeComplete={handleEditedNodeComplete} + onEditedNodeCancel={handleEditedNodeCancel} + onNodeDeleted={onNodeDeleted} + startRenaming={handleRenameRequest} />
@@ -283,9 +322,11 @@ const NodeRow: React.FC<{ generatePath: (node: FileNode, parentPath?: string) => string; parentPath?: string; parentMapping?: string; - tempNode?: TempNode | null; - onTempNodeComplete?: (name: string) => void; - onTempNodeCancel?: () => void; + editedNode?: EditedNode | null; + onEditedNodeComplete?: (name: string) => void; + onEditedNodeCancel?: () => void; + onNodeDeleted?: (path: string) => void; + startRenaming?: (path: string) => void; }> = ({ node, level, @@ -298,9 +339,11 @@ const NodeRow: React.FC<{ generatePath, parentPath = '', selectedNode, - tempNode, - onTempNodeComplete, - onTempNodeCancel, + editedNode, + onEditedNodeComplete, + onEditedNodeCancel, + onNodeDeleted, + startRenaming, }) => { const path = generatePath(node, parentPath); const isExpanded = isRoot || expandedNodePaths[path]; @@ -356,45 +399,90 @@ const NodeRow: React.FC<{ setSize={setSize} > - {() => ( - - )} + ) : ( + + ) + } - - )} - {tempNode && tempNode.parentPath === path && ( - {() => ( - +
+ { + startRenaming(path); + }, + }, + { + title: 'Delete', + onClick: () => { + onNodeDeleted(path); + }, + }, + ]} + /> +
)}
)} + {editedNode && + editedNode.reason === 'create' && + editedNode.parentPath === path && ( + + + {() => ( + + )} + + + )} {isExpanded && ( <> {node.children && @@ -411,9 +499,11 @@ const NodeRow: React.FC<{ selectPath={selectPath} generatePath={generatePath} parentPath={path} - tempNode={tempNode} - onTempNodeComplete={onTempNodeComplete} - onTempNodeCancel={onTempNodeCancel} + editedNode={editedNode} + onEditedNodeComplete={onEditedNodeComplete} + onEditedNodeCancel={onEditedNodeCancel} + onNodeDeleted={onNodeDeleted} + startRenaming={startRenaming} /> ))} @@ -422,17 +512,22 @@ const NodeRow: React.FC<{ ); }; -const TempNodeInput: React.FC<{ +const FilenameForm: React.FC<{ type: 'file' | 'folder'; onComplete: (name: string) => void; onCancel: () => void; level: number; -}> = ({ type, onComplete, onCancel, level }) => { + initialValue?: string; +}> = ({ type, onComplete, onCancel, level, initialValue = '' }) => { const inputRef = useRef(null); useEffect(() => { - inputRef.current?.focus(); - }, []); + if (inputRef.current) { + inputRef.current.value = initialValue; + inputRef.current.focus(); + inputRef.current.select(); + } + }, [initialValue]); const handleBlur = () => { const value = inputRef.current?.value.trim() || ''; @@ -462,7 +557,7 @@ const TempNodeInput: React.FC<{ } return ( -
+
); From 88b57b781bc9d748703eb975abed528e165e00b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 23:14:43 +0100 Subject: [PATCH 14/71] Tweak the user experience of the file picker. Enable drag&drop --- .../plugin.php | 25 +- .../src/components/FilePickerTree/index.tsx | 227 +++++++++++++++--- .../ui/src/index.tsx | 44 ++-- 3 files changed, 236 insertions(+), 60 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index c90d46466b..9a96faee1a 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -163,9 +163,10 @@ static public function initialize() { }, )); - register_rest_route('static-files-editor/v1', '/rename-file', array( + + register_rest_route('static-files-editor/v1', '/move-file', array( 'methods' => 'POST', - 'callback' => array(self::class, 'rename_file_endpoint'), + 'callback' => array(self::class, 'move_file_endpoint'), 'permission_callback' => function() { return current_user_can('edit_posts'); }, @@ -580,32 +581,32 @@ static public function create_directory_endpoint($request) { return array('success' => true); } - static public function rename_file_endpoint($request) { - $original_path = $request->get_param('originalPath'); - $new_path = $request->get_param('newPath'); + static public function move_file_endpoint($request) { + $from_path = $request->get_param('fromPath'); + $to_path = $request->get_param('toPath'); - if (!$original_path || !$new_path) { - return new WP_Error('missing_path', 'Both original and new paths are required'); + if (!$from_path || !$to_path) { + return new WP_Error('missing_path', 'Both source and target paths are required'); } // Find and update associated post $existing_posts = get_posts(array( 'post_type' => WP_LOCAL_FILE_POST_TYPE, 'meta_key' => 'local_file_path', - 'meta_value' => $original_path, + 'meta_value' => $from_path, 'posts_per_page' => 1 )); $fs = self::get_fs(); - if (!$fs->rename($original_path, $new_path)) { - return new WP_Error('rename_failed', 'Failed to rename file'); + if (!$fs->rename($from_path, $to_path)) { + return new WP_Error('move_failed', 'Failed to move file'); } if (!empty($existing_posts)) { - update_post_meta($existing_posts[0]->ID, 'local_file_path', $new_path); + update_post_meta($existing_posts[0]->ID, 'local_file_path', $to_path); wp_update_post(array( 'ID' => $existing_posts[0]->ID, - 'post_title' => basename($new_path) + 'post_title' => basename($to_path) )); } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 9162d043d9..7f75709d1b 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -30,8 +30,14 @@ export type FilePickerControlProps = { initialPath?: string; onSelect?: (path: string, node: FileNode) => void; onNodeCreated?: (node: CreatedNode) => void; - onNodeRenamed?: (originalPath: string, newPath: string) => void; onNodeDeleted?: (path: string) => void; + onNodeMoved?: ({ + fromPath, + toPath, + }: { + fromPath: string; + toPath: string; + }) => void; isLoading?: boolean; error?: string; }; @@ -45,6 +51,12 @@ type EditedNode = { originalPath?: string; }; +type DragState = { + path: string; + hoverPath: string | null; + hoverType: 'file' | 'folder' | null; +}; + export const FilePickerTree: React.FC = ({ isLoading = false, error = undefined, @@ -57,8 +69,14 @@ export const FilePickerTree: React.FC = ({ onNodeDeleted = (path: string) => { console.log('onNodeDeleted', path); }, - onNodeRenamed = (args: { originalPath: string; newPath: string }) => { - console.log('onNodeRenamed', args); + onNodeMoved = ({ + fromPath, + toPath, + }: { + fromPath: string; + toPath: string; + }) => { + console.log('onNodeMoved', fromPath, toPath); }, }) => { initialPath = initialPath ? initialPath.replace(/^\/+/, '') : '/'; @@ -79,6 +97,7 @@ export const FilePickerTree: React.FC = ({ ); const [editedNode, setEditedNode] = useState(null); + const [dragState, setDragState] = useState(null); const expandNode = (path: string, isOpen: boolean) => { setExpanded((prevState) => ({ @@ -146,9 +165,9 @@ export const FilePickerTree: React.FC = ({ ...editedNode.originalPath.split('/').slice(0, -1), name, ].join('/'); - onNodeRenamed({ - originalPath: editedNode.originalPath, - newPath, + onNodeMoved({ + fromPath: editedNode.originalPath, + toPath: newPath, }); } else { const fullPath = `${editedNode.parentPath}/${name}`.replace( @@ -167,6 +186,87 @@ export const FilePickerTree: React.FC = ({ setEditedNode(null); }; + const handleDragStart = (path: string, type: 'file' | 'folder') => { + setDragState({ + path, + hoverPath: null, + hoverType: null, + }); + }; + + const isDescendantPath = (parentPath: string, childPath: string) => { + return childPath.startsWith(parentPath + '/'); + }; + + const handleDragOver = ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => { + e.preventDefault(); + if (dragState && dragState.path !== path) { + // Prevent dropping a folder into its own descendant + if (dragState.path && isDescendantPath(dragState.path, path)) { + e.dataTransfer.dropEffect = 'none'; + return; + } + + e.dataTransfer.dropEffect = 'move'; + setDragState({ + ...dragState, + hoverPath: path, + hoverType: type, + }); + } + }; + + const handleDrop = ( + e: React.DragEvent, + targetPath: string, + targetType: 'file' | 'folder' + ) => { + e.preventDefault(); + if (dragState) { + // Prevent dropping a folder into its own descendant + if ( + dragState.path && + targetType === 'folder' && + isDescendantPath(dragState.path, targetPath) + ) { + return; + } + + const fromPath = dragState.path.replace(/^\/+/, ''); + + const targetParentPath = + targetType === 'file' + ? targetPath.split('/').slice(0, -1).join('/') + : targetPath; + + const toPath = [ + targetParentPath.split('/'), + dragState.path.split('/').pop(), + ] + .join('/') + .replace(/^\/+/, ''); + + setDragState(null); + + if (fromPath === toPath) { + return; + } + + onNodeMoved({ + fromPath, + toPath, + }); + } + }; + + const handleDragEnd = () => { + setDragState(null); + }; + const [searchBuffer, setSearchBuffer] = useState(''); const searchBufferTimeoutRef = useRef(null); function handleKeyDown(event: React.KeyboardEvent) { @@ -303,6 +403,11 @@ export const FilePickerTree: React.FC = ({ onEditedNodeCancel={handleEditedNodeCancel} onNodeDeleted={onNodeDeleted} startRenaming={handleRenameRequest} + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDrop={handleDrop} + onDragEnd={handleDragEnd} + dragState={dragState} />
@@ -327,6 +432,19 @@ const NodeRow: React.FC<{ onEditedNodeCancel?: () => void; onNodeDeleted?: (path: string) => void; startRenaming?: (path: string) => void; + onDragStart?: (path: string, type: 'file' | 'folder') => void; + onDragOver?: ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => void; + onDrop?: ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => void; + onDragEnd?: () => void; + dragState?: DragState | null; }> = ({ node, level, @@ -344,9 +462,16 @@ const NodeRow: React.FC<{ onEditedNodeCancel, onNodeDeleted, startRenaming, + onDragStart, + onDragOver, + onDrop, + onDragEnd, + dragState, }) => { const path = generatePath(node, parentPath); const isExpanded = isRoot || expandedNodePaths[path]; + const isBeingDragged = dragState?.path === path; + const isHovered = dragState?.hoverPath === path; const toggleOpen = () => expandNode(path, !isExpanded); @@ -410,30 +535,71 @@ const NodeRow: React.FC<{ initialValue={node.name} /> ) : ( - + onDragEnd={onDragEnd} + style={{ + opacity: isBeingDragged ? 0.5 : 1, + backgroundColor: + isHovered && + node.type === 'folder' + ? '#e5f1f8' + : undefined, + position: 'relative', + }} + > + + + ) } @@ -504,6 +670,11 @@ const NodeRow: React.FC<{ onEditedNodeCancel={onEditedNodeCancel} onNodeDeleted={onNodeDeleted} startRenaming={startRenaming} + onDragStart={onDragStart} + onDragOver={onDragOver} + onDrop={onDrop} + onDragEnd={onDragEnd} + dragState={dragState} /> ))} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 8de2d9578b..9a83992b4d 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -78,25 +78,6 @@ function ConnectedFilePickerTree() { await refreshFileTree(); }; - const handleNodeRenamed = async (args: { - originalPath: string; - newPath: string; - }) => { - try { - await apiFetch({ - path: '/static-files-editor/v1/rename-file', - method: 'POST', - data: { - originalPath: args.originalPath, - newPath: args.newPath, - }, - }); - await refreshFileTree(); - } catch (error) { - console.error('Failed to rename file:', error); - } - }; - const handleFileClick = async (filePath: string, node: FileNode) => { if (node.type === 'folder') { setSelectedPath(filePath); @@ -151,6 +132,29 @@ function ConnectedFilePickerTree() { } }; + const handleNodeMoved = async ({ + fromPath, + toPath, + }: { + fromPath: string; + toPath: string; + }) => { + try { + console.log('Moving file from', fromPath, 'to', toPath); + await apiFetch({ + path: '/static-files-editor/v1/move-file', + method: 'POST', + data: { + fromPath, + toPath, + }, + }); + await refreshFileTree(); + } catch (error) { + console.error('Failed to move file:', error); + } + }; + if (isLoading) { return ; } @@ -166,8 +170,8 @@ function ConnectedFilePickerTree() { onSelect={handleFileClick} initialPath={selectedPath} onNodeCreated={handleNodeCreated} - onNodeRenamed={handleNodeRenamed} onNodeDeleted={handleNodeDeleted} + onNodeMoved={handleNodeMoved} /> ); From 74e671a150c64d6c804d6fd5e83e75be31c3b1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 23:23:44 +0100 Subject: [PATCH 15/71] Replace recursive props with a context --- .../src/components/FilePickerTree/index.tsx | 253 +++++++++--------- 1 file changed, 130 insertions(+), 123 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 7f75709d1b..6763220bc5 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useRef, useState } from '@wordpress/element'; +import React, { + useEffect, + useRef, + useState, + createContext, + useContext, +} from '@wordpress/element'; import { __experimentalTreeGrid as TreeGrid, __experimentalTreeGridRow as TreeGridRow, @@ -57,6 +63,30 @@ type DragState = { hoverType: 'file' | 'folder' | null; }; +type FilePickerContextType = { + expandedNodePaths: ExpandedNodePaths; + expandNode: (path: string, isOpen: boolean) => void; + selectPath: (path: string, node: FileNode) => void; + selectedNode: string | null; + generatePath: (node: FileNode, parentPath?: string) => string; + editedNode: EditedNode | null; + onEditedNodeComplete: (name: string) => void; + onEditedNodeCancel: () => void; + onNodeDeleted: (path: string) => void; + startRenaming: (path: string) => void; + onDragStart: (path: string, type: 'file' | 'folder') => void; + onDragOver: ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => void; + onDrop: (e: React.DragEvent, path: string, type: 'file' | 'folder') => void; + onDragEnd: () => void; + dragState: DragState | null; +}; + +const FilePickerContext = createContext(null); + export const FilePickerTree: React.FC = ({ isLoading = false, error = undefined, @@ -159,15 +189,21 @@ export const FilePickerTree: React.FC = ({ const handleEditedNodeComplete = (name: string) => { if (!editedNode) return; - // @TODO: Replace with joinPaths() + setEditedNode(null); if (editedNode.reason === 'rename') { - const newPath = [ - ...editedNode.originalPath.split('/').slice(0, -1), - name, - ].join('/'); + // @TODO: Replace with joinPaths() + const fromPath = editedNode.originalPath.replace(/^\/+/, ''); + const toPath = [...fromPath.split('/').slice(0, -1), name].join( + '/' + ); + + if (fromPath === toPath) { + return; + } + onNodeMoved({ - fromPath: editedNode.originalPath, - toPath: newPath, + fromPath, + toPath, }); } else { const fullPath = `${editedNode.parentPath}/${name}`.replace( @@ -179,7 +215,6 @@ export const FilePickerTree: React.FC = ({ path: fullPath, }); } - setEditedNode(null); }; const handleEditedNodeCancel = () => { @@ -358,59 +393,69 @@ export const FilePickerTree: React.FC = ({ ); } - return ( -
-
- - - - -
+ const contextValue = { + expandedNodePaths: expanded, + expandNode, + selectPath, + selectedNode: selectedPath, + generatePath, + editedNode, + onEditedNodeComplete: handleEditedNodeComplete, + onEditedNodeCancel: handleEditedNodeCancel, + onNodeDeleted, + startRenaming: handleRenameRequest, + onDragStart: handleDragStart, + onDragOver: handleDragOver, + onDrop: handleDrop, + onDragEnd: handleDragEnd, + dragState, + }; - - +
+
- -
+ > + + + + +
+ + + + +
+ ); }; @@ -420,54 +465,30 @@ const NodeRow: React.FC<{ position: number; setSize: number; isRoot: boolean; - expandedNodePaths: ExpandedNodePaths; - expandNode: (path: string, isOpen: boolean) => void; - selectPath: (path: string) => void; - selectedNode: string | null; - generatePath: (node: FileNode, parentPath?: string) => string; - parentPath?: string; - parentMapping?: string; - editedNode?: EditedNode | null; - onEditedNodeComplete?: (name: string) => void; - onEditedNodeCancel?: () => void; - onNodeDeleted?: (path: string) => void; - startRenaming?: (path: string) => void; - onDragStart?: (path: string, type: 'file' | 'folder') => void; - onDragOver?: ( - e: React.DragEvent, - path: string, - type: 'file' | 'folder' - ) => void; - onDrop?: ( - e: React.DragEvent, - path: string, - type: 'file' | 'folder' - ) => void; - onDragEnd?: () => void; - dragState?: DragState | null; -}> = ({ - node, - level, - position, - setSize, - isRoot, - expandedNodePaths, - expandNode, - selectPath, - generatePath, - parentPath = '', - selectedNode, - editedNode, - onEditedNodeComplete, - onEditedNodeCancel, - onNodeDeleted, - startRenaming, - onDragStart, - onDragOver, - onDrop, - onDragEnd, - dragState, -}) => { + parentPath: string; +}> = ({ node, level, position, setSize, isRoot, parentPath }) => { + const context = useContext(FilePickerContext); + if (!context) + throw new Error('NodeRow must be used within FilePickerContext'); + + const { + expandedNodePaths, + expandNode, + selectPath, + generatePath, + selectedNode, + editedNode, + onEditedNodeComplete, + onEditedNodeCancel, + onNodeDeleted, + startRenaming, + onDragStart, + onDragOver, + onDrop, + onDragEnd, + dragState, + } = context; + const path = generatePath(node, parentPath); const isExpanded = isRoot || expandedNodePaths[path]; const isBeingDragged = dragState?.path === path; @@ -659,22 +680,8 @@ const NodeRow: React.FC<{ level={level + 1} position={index + 1} setSize={node.children!.length} - expandedNodePaths={expandedNodePaths} - expandNode={expandNode} - selectedNode={selectedNode} - selectPath={selectPath} - generatePath={generatePath} + isRoot={false} parentPath={path} - editedNode={editedNode} - onEditedNodeComplete={onEditedNodeComplete} - onEditedNodeCancel={onEditedNodeCancel} - onNodeDeleted={onNodeDeleted} - startRenaming={startRenaming} - onDragStart={onDragStart} - onDragOver={onDragOver} - onDrop={onDrop} - onDragEnd={onDragEnd} - dragState={dragState} /> ))} From 7a4a0dcf8585205aa3d907bfab499269dd121ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 27 Dec 2024 23:56:37 +0100 Subject: [PATCH 16/71] Enable file uploading by drag&drop --- .../plugin.php | 111 ++++++++++++++++ .../src/components/FilePickerTree/index.tsx | 118 +++++++++++++++--- .../ui/src/components/FilePickerTree/types.ts | 11 ++ .../ui/src/index.tsx | 61 +++++---- 4 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/types.ts diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 9a96faee1a..83114a6fb7 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -171,6 +171,14 @@ static public function initialize() { return current_user_can('edit_posts'); }, )); + + register_rest_route('static-files-editor/v1', '/create-files', array( + 'methods' => 'POST', + 'callback' => array(self::class, 'create_files_endpoint'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + )); }); // @TODO: the_content and rest_prepare_local_file filters run twice for REST API requests. @@ -613,6 +621,109 @@ static public function move_file_endpoint($request) { return array('success' => true); } + static public function create_files_endpoint($request) { + $path = $request->get_param('path'); + $nodes_json = $request->get_param('nodes'); + + if(!$path) { + $path = '/'; + } + + if (!$nodes_json) { + return new WP_Error('invalid_tree', 'Invalid file tree structure'); + } + + $nodes = json_decode($nodes_json, true); + if (!$nodes) { + return new WP_Error('invalid_json', 'Invalid JSON structure'); + } + + $created_files = []; + + try { + $fs = self::get_fs(); + foreach ($nodes as $node) { + $result = self::process_node($node, $path, $fs, $request); + if (is_wp_error($result)) { + return $result; + } + $created_files = array_merge($created_files, $result); + } + + return array( + 'created_files' => $created_files + ); + } catch (Exception $e) { + return new WP_Error('creation_failed', $e->getMessage()); + } + } + + static private function process_node($node, $parent_path, $fs, $request) { + if (!isset($node['name']) || !isset($node['type'])) { + return new WP_Error('invalid_node', 'Invalid node structure'); + } + + $path = rtrim($parent_path, '/') . '/' . ltrim($node['name'], '/'); + $created_files = []; + + if ($node['type'] === 'folder') { + if (!$fs->mkdir($path)) { + return new WP_Error('mkdir_failed', "Failed to create directory: $path"); + } + + if (!empty($node['children'])) { + foreach ($node['children'] as $child) { + $result = self::process_node($child, $path, $fs, $request); + if (is_wp_error($result)) { + return $result; + } + $created_files = array_merge($created_files, $result); + } + } + } else { + $content = ''; + if (isset($node['content']) && is_string($node['content']) && strpos($node['content'], '@file:') === 0) { + $file_key = substr($node['content'], 6); + $uploaded_file = $request->get_file_params()[$file_key] ?? null; + if ($uploaded_file && $uploaded_file['error'] === UPLOAD_ERR_OK) { + $content = file_get_contents($uploaded_file['tmp_name']); + } + } + + if (!$fs->put_contents($path, $content)) { + return new WP_Error('write_failed', "Failed to write file: $path"); + } + + /* + // @TODO: Should we create posts here? + // * We'll reindex the data later anyway and create those posts on demand. + // * ^ yes, but this means we don't have these posts in the database right after the upload. + // * But if we do create them, how do we know which files need a post, and which ones are + // images, videos, etc? + $post_data = array( + 'post_title' => basename($path), + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_status' => 'publish', + 'meta_input' => array( + 'local_file_path' => $path + ) + ); + $post_id = wp_insert_post($post_data); + + if (is_wp_error($post_id)) { + return $post_id; + } + */ + + $created_files[] = array( + 'path' => $path, + // 'post_id' => $post_id + ); + } + + return $created_files; + } + } WP_Static_Files_Editor_Plugin::initialize(); diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 6763220bc5..f321462bff 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -1,4 +1,5 @@ -import React, { +import React from 'react'; +import { useEffect, useRef, useState, @@ -19,23 +20,32 @@ import '@wordpress/components/build-style/style.css'; import css from './style.module.css'; import classNames from 'classnames'; import { folder, file } from '../icons'; +import { FileTree } from './types'; export type FileNode = { name: string; type: 'file' | 'folder'; children?: FileNode[]; + content?: File; }; -export type CreatedNode = { - type: 'file' | 'folder'; - path: string; -}; +export type CreatedNode = + | { + type: 'file' | 'folder'; + path: string; + content?: string | ArrayBuffer | File; + } + | { + type: 'tree'; + path: string; + content: FileNode[]; + }; export type FilePickerControlProps = { files: FileNode[]; initialPath?: string; onSelect?: (path: string, node: FileNode) => void; - onNodeCreated?: (node: CreatedNode) => void; + onNodesCreated?: (tree: FileTree) => void; onNodeDeleted?: (path: string) => void; onNodeMoved?: ({ fromPath, @@ -61,6 +71,7 @@ type DragState = { path: string; hoverPath: string | null; hoverType: 'file' | 'folder' | null; + isExternal?: boolean; }; type FilePickerContextType = { @@ -93,8 +104,8 @@ export const FilePickerTree: React.FC = ({ files, initialPath, onSelect = () => {}, - onNodeCreated = (...args) => { - console.log('onNodeCreated', args); + onNodesCreated = (tree: FileTree) => { + console.log('onNodesCreated', tree); }, onNodeDeleted = (path: string) => { console.log('onNodeDeleted', path); @@ -206,13 +217,15 @@ export const FilePickerTree: React.FC = ({ toPath, }); } else { - const fullPath = `${editedNode.parentPath}/${name}`.replace( - /\/+/g, - '/' - ); - onNodeCreated({ - type: editedNode.type, - path: fullPath, + onNodesCreated({ + path: editedNode.parentPath, + nodes: [ + { + name: name, + type: editedNode.type, + content: null, + }, + ], }); } }; @@ -226,6 +239,7 @@ export const FilePickerTree: React.FC = ({ path, hoverPath: null, hoverType: null, + isExternal: false, }); }; @@ -239,6 +253,19 @@ export const FilePickerTree: React.FC = ({ type: 'file' | 'folder' ) => { e.preventDefault(); + + // Handle external files being dragged in + if (e.dataTransfer.types.includes('Files')) { + e.dataTransfer.dropEffect = 'copy'; + setDragState({ + path: '', + hoverPath: path, + hoverType: type, + isExternal: true, + }); + return; + } + if (dragState && dragState.path !== path) { // Prevent dropping a folder into its own descendant if (dragState.path && isDescendantPath(dragState.path, path)) { @@ -255,12 +282,71 @@ export const FilePickerTree: React.FC = ({ } }; - const handleDrop = ( + const handleDrop = async ( e: React.DragEvent, targetPath: string, targetType: 'file' | 'folder' ) => { e.preventDefault(); + // Handle file/directory upload + if (e.dataTransfer.items.length > 0) { + const targetFolder = + targetType === 'folder' + ? targetPath + : targetPath.split('/').slice(0, -1).join('/'); + const items = Array.from(e.dataTransfer.items); + + const buildTree = async ( + entry: FileSystemEntry, + parentPath: string = '' + ): Promise => { + if (entry.isFile) { + const fileEntry = entry as FileSystemFileEntry; + const file = await new Promise((resolve) => + fileEntry.file(resolve) + ); + return { + name: entry.name, + type: 'file', + content: file, + }; + } else { + const dirEntry = entry as FileSystemDirectoryEntry; + const reader = dirEntry.createReader(); + const entries = await new Promise( + (resolve) => { + reader.readEntries((entries) => resolve(entries)); + } + ); + + const children = await Promise.all( + entries.map((entry) => buildTree(entry)) + ); + + return { + name: entry.name, + type: 'folder', + children, + }; + } + }; + + const rootNodes = await Promise.all( + items + .map((item) => item.webkitGetAsEntry()) + .filter((entry): entry is FileSystemEntry => entry !== null) + .map((entry) => buildTree(entry)) + ); + + onNodesCreated({ + path: targetFolder, + nodes: rootNodes, + }); + + setDragState(null); + return; + } + if (dragState) { // Prevent dropping a folder into its own descendant if ( diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/types.ts b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/types.ts new file mode 100644 index 0000000000..259a28b864 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/types.ts @@ -0,0 +1,11 @@ +export type FileNode = { + name: string; + type: 'file' | 'folder'; + children?: FileNode[]; + content?: File; +}; + +export type FileTree = { + path: string; + nodes: FileNode[]; +}; diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 9a83992b4d..fc4d7014c7 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -3,6 +3,7 @@ import { CreatedNode, FileNode, FilePickerTree, + FileTree, } from './components/FilePickerTree'; import { store as editorStore } from '@wordpress/editor'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -98,37 +99,47 @@ function ConnectedFilePickerTree() { }); }; - const handleNodeCreated = async (node: CreatedNode) => { - if (node.type === 'file') { - await createEmptyFile(node.path); - } else if (node.type === 'folder') { - // Create an empty .gitkeep file in the new directory - // to make sure it will actually be created in the filesystem. - // @TODO: Rethink this approach. Ideally we could just display the - // directory in the tree, and let the user create files inside it. - await createEmptyFile(node.path + '/.gitkeep'); - } - }; - - const createEmptyFile = async (newFilePath: string) => { + const handleNodesCreated = async (tree: FileTree) => { try { + const formData = new FormData(); + formData.append('path', tree.path); + + // Convert nodes to JSON, but extract files to separate form fields + const processNode = (node: FileNode, prefix: string): any => { + const nodeData = { ...node }; + if (node.content instanceof File) { + formData.append(`${prefix}_content`, node.content); + nodeData.content = `@file:${prefix}_content`; + } + if (node.children) { + nodeData.children = node.children.map((child, index) => + processNode(child, `${prefix}_${index}`) + ); + } + return nodeData; + }; + + const processedNodes = tree.nodes.map((node, index) => + processNode(node, `file_${index}`) + ); + formData.append('nodes', JSON.stringify(processedNodes)); + const response = (await apiFetch({ - path: '/static-files-editor/v1/get-or-create-post-for-file', + path: '/static-files-editor/v1/create-files', method: 'POST', - data: { - path: newFilePath, - create_file: true, - }, - })) as { post_id: string }; + body: formData, + })) as { created_files: Array<{ path: string; post_id: string }> }; await refreshFileTree(); - onNavigateToEntityRecord({ - postId: response.post_id, - postType: WP_LOCAL_FILE_POST_TYPE, - }); + if (response.created_files.length > 0) { + onNavigateToEntityRecord({ + postId: response.created_files[0].post_id, + postType: WP_LOCAL_FILE_POST_TYPE, + }); + } } catch (error) { - console.error('Failed to create file:', error); + console.error('Failed to create files:', error); } }; @@ -169,7 +180,7 @@ function ConnectedFilePickerTree() { files={fileTree} onSelect={handleFileClick} initialPath={selectedPath} - onNodeCreated={handleNodeCreated} + onNodesCreated={handleNodesCreated} onNodeDeleted={handleNodeDeleted} onNodeMoved={handleNodeMoved} /> From e55b94a717e25bd07c161a3167d864ed123c20ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 28 Dec 2024 00:00:34 +0100 Subject: [PATCH 17/71] Enable deleting file subtrees --- .../plugin.php | 63 ++++++++++++++++--- .../ui/src/index.tsx | 28 +++------ 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 83114a6fb7..4742d9ef99 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -179,6 +179,14 @@ static public function initialize() { return current_user_can('edit_posts'); }, )); + + register_rest_route('static-files-editor/v1', '/delete-path', array( + 'methods' => 'POST', + 'callback' => array(self::class, 'delete_path_endpoint'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + )); }); // @TODO: the_content and rest_prepare_local_file filters run twice for REST API requests. @@ -219,16 +227,18 @@ static public function initialize() { }, 10, 3); // Delete the associated file when a post is deleted - add_action('before_delete_post', function($post_id) { - $post = get_post($post_id); - if ($post && $post->post_type === WP_LOCAL_FILE_POST_TYPE) { - $fs = self::get_fs(); - $path = get_post_meta($post_id, 'local_file_path', true); - if ($path && $fs->is_file($path)) { - $fs->rm($path); - } - } - }); + // @TODO: Rethink this. We have a separate endpoint for deleting an entire path. + // Do we need a separate hook at all? + // add_action('before_delete_post', function($post_id) { + // $post = get_post($post_id); + // if ($post && $post->post_type === WP_LOCAL_FILE_POST_TYPE) { + // $fs = self::get_fs(); + // $path = get_post_meta($post_id, 'local_file_path', true); + // if ($path && $fs->is_file($path)) { + // $fs->rm($path); + // } + // } + // }); // Update the file after post is saved add_action('save_post_' . WP_LOCAL_FILE_POST_TYPE, function($post_id, $post, $update) { @@ -724,6 +734,39 @@ static private function process_node($node, $parent_path, $fs, $request) { return $created_files; } + static public function delete_path_endpoint($request) { + $path = $request->get_param('path'); + if (!$path) { + return new WP_Error('missing_path', 'File path is required'); + } + + // Find and delete associated post + $existing_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $path, + 'posts_per_page' => 1 + )); + + if (!empty($existing_posts)) { + wp_delete_post($existing_posts[0]->ID, true); + } + + // Delete the actual file + $fs = self::get_fs(); + if($fs->is_dir($path)) { + if (!$fs->rmdir($path, ['recursive' => true])) { + return new WP_Error('delete_failed', 'Failed to delete directory'); + } + } else { + if (!$fs->rm($path)) { + return new WP_Error('delete_failed', 'Failed to delete file'); + } + } + + return array('success' => true); + } + } WP_Static_Files_Editor_Plugin::initialize(); diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index fc4d7014c7..4750f0dc78 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -59,24 +59,16 @@ function ConnectedFilePickerTree() { ); const handleNodeDeleted = async (path: string) => { - const { post_id } = (await apiFetch({ - path: '/static-files-editor/v1/get-or-create-post-for-file', - method: 'POST', - data: { path }, - })) as { post_id: string }; - console.log({ - post_id, - deleteUrl: `/wp/v2/${WP_LOCAL_FILE_POST_TYPE}/${post_id}`, - }); - - await apiFetch({ - // ?force=true to skip the trash and delete the file immediately - path: `/wp/v2/${WP_LOCAL_FILE_POST_TYPE}/${post_id}?force=true`, - headers: { - 'X-HTTP-Method-Override': 'DELETE', - }, - }); - await refreshFileTree(); + try { + await apiFetch({ + path: '/static-files-editor/v1/delete-path', + method: 'POST', + data: { path }, + }); + await refreshFileTree(); + } catch (error) { + console.error('Failed to delete file:', error); + } }; const handleFileClick = async (filePath: string, node: FileNode) => { From 718bbe690de22fea61c00213147504a9bc0a0bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 28 Dec 2024 00:11:01 +0100 Subject: [PATCH 18/71] Dsplay directories before files, use a more useful default commit message --- .../components/src/FilePickerTree/index.tsx | 24 ++++++++++--- .../src/components/FilePickerTree/index.tsx | 36 ++++++++++--------- .../data-liberation/src/git/WP_Git_Client.php | 2 +- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/playground/components/src/FilePickerTree/index.tsx b/packages/playground/components/src/FilePickerTree/index.tsx index de0a29b1c3..8d77a72cde 100644 --- a/packages/playground/components/src/FilePickerTree/index.tsx +++ b/packages/playground/components/src/FilePickerTree/index.tsx @@ -156,10 +156,17 @@ export const FilePickerTree: React.FC = ({ ); } + const sortedFiles = [...files].sort((a, b) => { + if (a.type === b.type) { + return a.name.localeCompare(b.name); + } + return a.type === 'folder' ? -1 : 1; + }); + return (
- {files.map((file, index) => ( + {sortedFiles.map((file, index) => ( { + if (a.type === b.type) { + return a.name.localeCompare(b.name); + } + return a.type === 'folder' ? -1 : 1; + }) + : []; + return ( <> {isExpanded && - node.children && - node.children.map((child, index) => ( + sortedChildren.map((child, index) => ( { + if (a.type === b.type) { + return a.name.localeCompare(b.name); + } + return a.type === 'folder' ? -1 : 1; + }) + : []; return ( <> {!isRoot && ( @@ -756,22 +764,18 @@ const NodeRow: React.FC<{ )} - {isExpanded && ( - <> - {node.children && - node.children.map((child, index) => ( - - ))} - - )} + {isExpanded && + sortedChildren?.map((child, index) => ( + + ))} ); }; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 1ef4b3dcff..3b5a5fc698 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -67,7 +67,7 @@ public function push($git_objects, $options = []) { $branchName = $options['branch_name']; $author = ($options['author'] ?? $this->author) . " " . time() . " +0000"; $committer = ($options['committer'] ?? $this->committer) . " " . time() . " +0000"; - $message = $options['message'] ?? "Hello!"; + $message = $options['message'] ?? "Changes from WordPress."; $parent = ''; if($parent_hash !== $empty_hash) { From d9c0eadb3dfcff229300ff6d2b93c7e83dd37a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 28 Dec 2024 00:35:21 +0100 Subject: [PATCH 19/71] Display a visually consistent selection in the Local Files tab without a visible split between the file name and the ellipsis menu --- .../src/components/FilePickerTree/index.tsx | 44 +++++++++---------- .../FilePickerTree/style.module.css | 28 +++++++++++- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index fc5930a5ac..0664df4cdd 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -714,34 +714,30 @@ const NodeRow: React.FC<{ level={level} /> +
+ { + startRenaming(path); + }, + }, + { + title: 'Delete', + onClick: () => { + onNodeDeleted(path); + }, + }, + ]} + /> +
) } - - {() => ( -
- { - startRenaming(path); - }, - }, - { - title: 'Delete', - onClick: () => { - onNodeDeleted(path); - }, - }, - ]} - /> -
- )} -
)} {editedNode && diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css index 78b71827dc..acbb94a91d 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css @@ -1,6 +1,8 @@ .file-picker-tree { width: 100%; + border-collapse: collapse; + tr:nth-child(even) { background-color: #f7f7f7; } @@ -12,6 +14,29 @@ /* &:focus { outline: 2px solid var(--wp-admin-theme-color); } */ + + td { + position: relative; + } + .more-actions { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + opacity: 0; + max-height: 100%; + } + + tr:hover .more-actions { + opacity: 1; + } + + tr:has(.selected, :focus, :active) .more-actions { + opacity: 1; + button { + color: #ffffff !important; + } + } } .file-node-button { @@ -26,7 +51,8 @@ -webkit-appearance: none; background: none; transition: box-shadow 0.1s linear; - height: 30px; + height: 40px; + align-items: center; box-sizing: border-box; padding: 6px 12px; From f945ec339e0400880e4072feb59972fab805747c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 28 Dec 2024 00:37:42 +0100 Subject: [PATCH 20/71] Use the vertical three dots icon for "more" --- .../ui/src/components/FilePickerTree/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 0664df4cdd..9d57cde48a 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -15,7 +15,12 @@ import { ButtonGroup, DropdownMenu, } from '@wordpress/components'; -import { Icon, chevronRight, chevronDown, more } from '@wordpress/icons'; +import { + Icon, + chevronRight, + chevronDown, + moreVertical, +} from '@wordpress/icons'; import '@wordpress/components/build-style/style.css'; import css from './style.module.css'; import classNames from 'classnames'; @@ -716,7 +721,7 @@ const NodeRow: React.FC<{
Date: Sat, 28 Dec 2024 00:38:49 +0100 Subject: [PATCH 21/71] CSS tweaks --- .../ui/src/components/FilePickerTree/style.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css index acbb94a91d..d41790ff82 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css @@ -31,7 +31,7 @@ opacity: 1; } - tr:has(.selected, :focus, :active) .more-actions { + tr:has(.selected) .more-actions { opacity: 1; button { color: #ffffff !important; From 1fbfbfc2daef9bd5fd151fadc9cfc953f325a9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 28 Dec 2024 00:57:01 +0100 Subject: [PATCH 22/71] CSS tweaks --- .../src/components/FilePickerTree/index.tsx | 18 ++++++++++++++- .../FilePickerTree/style.module.css | 1 - .../ui/src/editor.css | 3 --- .../ui/src/index.tsx | 22 +++++++++---------- .../ui/src/style.module.css | 14 ++++++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) delete mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/editor.css create mode 100644 packages/playground/data-liberation-static-files-editor/ui/src/style.module.css diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 9d57cde48a..b896834996 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -49,6 +49,7 @@ export type CreatedNode = export type FilePickerControlProps = { files: FileNode[]; initialPath?: string; + className?: string; onSelect?: (path: string, node: FileNode) => void; onNodesCreated?: (tree: FileTree) => void; onNodeDeleted?: (path: string) => void; @@ -108,6 +109,7 @@ export const FilePickerTree: React.FC = ({ error = undefined, files, initialPath, + className = '', onSelect = () => {}, onNodesCreated = (tree: FileTree) => { console.log('onNodesCreated', tree); @@ -293,6 +295,12 @@ export const FilePickerTree: React.FC = ({ targetType: 'file' | 'folder' ) => { e.preventDefault(); + // Prevent a parent element event handler from handling the drop + // again. + if (e.isPropagationStopped()) { + return; + } + e.stopPropagation(); // Handle file/directory upload if (e.dataTransfer.items.length > 0) { const targetFolder = @@ -504,12 +512,20 @@ export const FilePickerTree: React.FC = ({ return ( -
+
{ + handleDrop?.(e, '/', 'folder'); + }} + >
diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css index d41790ff82..a815cd79a6 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/style.module.css @@ -1,6 +1,5 @@ .file-picker-tree { width: 100%; - border-collapse: collapse; tr:nth-child(even) { diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/editor.css b/packages/playground/data-liberation-static-files-editor/ui/src/editor.css deleted file mode 100644 index f48803b52b..0000000000 --- a/packages/playground/data-liberation-static-files-editor/ui/src/editor.css +++ /dev/null @@ -1,3 +0,0 @@ -:global(.editor-visual-editor__post-title-wrapper) { - display: none; -} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 4750f0dc78..49e921a4f4 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -13,7 +13,7 @@ import { addLocalFilesTab } from './add-local-files-tab'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { Spinner, Button } from '@wordpress/components'; import { useEntityProp } from '@wordpress/core-data'; -import './editor.css'; +import css from './style.module.css'; // Pre-populated by plugin.php const WP_LOCAL_FILE_POST_TYPE = window.WP_LOCAL_FILE_POST_TYPE; @@ -167,16 +167,14 @@ function ConnectedFilePickerTree() { } return ( -
- -
+ ); } @@ -184,7 +182,7 @@ addLocalFilesTab({ name: 'local-files', title: 'Local Files', panel: ( -
+
), diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/style.module.css b/packages/playground/data-liberation-static-files-editor/ui/src/style.module.css new file mode 100644 index 0000000000..bb60474ac7 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/ui/src/style.module.css @@ -0,0 +1,14 @@ +:global(.editor-visual-editor__post-title-wrapper) { + display: none; +} + +.file-picker-tree-container, +.file-picker-tree-container > div { + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.file-picker-tree-container > div > div[role='application'] { + flex-grow: 1; +} From 7e5e96a6483386bafec610a8fe4a37e39a6dade1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 28 Dec 2024 01:02:58 +0100 Subject: [PATCH 23/71] Restrict the local files tab to the list view sidebar --- .../ui/src/add-local-files-tab.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx index 5ba672ed65..3674e9c581 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx @@ -42,15 +42,22 @@ export function addLocalFilesTab(tab: { function patchArguments(args: any[]) { let [type, props, ...children] = args; const newProps = { ...props }; - if ('tabs' in newProps) { - const hasLocalFilesTab = newProps.tabs.find( - (tab) => tab.name === 'local-files' - ); - if (!hasLocalFilesTab) { - newProps.tabs.unshift(tab); - } - newProps.defaultTabId = 'local-files'; + if (!('tabs' in newProps)) { + return [type, newProps, ...children]; } + const hasListViewTab = newProps.tabs.find( + (tab) => tab.name === 'list-view' + ); + if (!hasListViewTab) { + return [type, newProps, ...children]; + } + const hasLocalFilesTab = newProps.tabs.find( + (tab) => tab.name === 'local-files' + ); + if (!hasLocalFilesTab) { + newProps.tabs.unshift(tab); + } + newProps.defaultTabId = 'local-files'; return [type, newProps, ...children]; } From 6951723d2c851773b38af3c5373ee0ee7c7b352f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 29 Dec 2024 22:48:00 +0100 Subject: [PATCH 24/71] WP_Git_Client: Use AsyncHttp\Client for HTTP requests --- .../data-liberation/blueprints-library | 2 +- .../playground/data-liberation/bootstrap.php | 1 + .../data-liberation/src/git/WP_Git_Client.php | 59 +++++++++++++------ 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/playground/data-liberation/blueprints-library b/packages/playground/data-liberation/blueprints-library index b1362cbe3c..6bf961fcc2 160000 --- a/packages/playground/data-liberation/blueprints-library +++ b/packages/playground/data-liberation/blueprints-library @@ -1 +1 @@ -Subproject commit b1362cbe3ca0956a36cc00dd769698bde0ba2ec7 +Subproject commit 6bf961fcc2188ef5d5c65c182ebc1336bc238bb2 diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 5c5c0ffacf..9df2f90f14 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -21,6 +21,7 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_GZ_File_Reader.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_Remote_File_Reader.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_Remote_File_Ranged_Reader.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_String_Reader.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Zip/ZipStreamReader.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Zip/WP_Zip_Filesystem.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 3b5a5fc698..9708d3d982 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -1,16 +1,23 @@ repoUrl = rtrim($repoUrl, '/'); $this->author = $options['author'] ?? "John Doe "; $this->committer = $options['committer'] ?? "John Doe "; + $this->http_client = $options['http_client'] ?? new Client(); } public function fetchRefs($prefix) { @@ -25,9 +32,9 @@ public function fetchRefs($prefix) { $this->encode_packet_line("ref-prefix $prefix\n") . "0000", [ - 'Accept: application/x-git-upload-pack-advertisement', - 'Content-Type: application/x-git-upload-pack-request', - 'Git-Protocol: version=2' + 'Accept' => 'application/x-git-upload-pack-advertisement', + 'Content-Type' => 'application/x-git-upload-pack-request', + 'Git-Protocol' => 'version=2' ] ); @@ -96,8 +103,8 @@ public function push($git_objects, $options = []) { $url = rtrim($this->repoUrl, '.git').'.git/git-receive-pack'; $response = $this->http_request($url, $push_packet, [ - 'Content-Type: application/x-git-receive-pack-request', - 'Accept: application/x-git-receive-pack-result', + 'Content-Type' => 'application/x-git-receive-pack-request', + 'Accept' => 'application/x-git-receive-pack-result', ]); $response_chunks = iterator_to_array($this->parse_multiplexed_pack_data($response)); @@ -162,22 +169,36 @@ public function backfillBlobs($index, $root = '/') { return $index; } - private function http_request($url, $postData = null, $headers = []) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - - if ($postData) { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + public function get_last_error() { + $last_request = $this->http_client->get_request(); + if(!$last_request) { + return null; } + return $last_request->error; + } - $response = curl_exec($ch); - curl_close($ch); + private function http_request($url, $postData = null, $headers = []) { + $request_info = []; + if($postData) { + $request_info['headers'] = $headers; + $request_info['method'] = 'POST'; + $request_info['body_stream'] = WP_String_Reader::create($postData); + } + $request = new Request($url, $request_info); + $this->http_client->enqueue($request); - return $response; + $buffered_response = ''; + while($this->http_client->await_next_event()) { + $event = $this->http_client->get_event(); + switch($event) { + case Client::EVENT_BODY_CHUNK_AVAILABLE: + $buffered_response .= $this->http_client->get_response_body_chunk(); + break; + case Client::EVENT_FAILED: + return false; + } + } + return $buffered_response; } private function encode_packet_line($data) { From 8b764c139bffef9f3f9c0e8e22f25c721b530718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Dec 2024 00:04:08 +0100 Subject: [PATCH 25/71] Fix disappearing editor, add post loading overlay --- .../ui/src/add-local-files-tab.tsx | 35 ++++ .../ui/src/index.tsx | 156 ++++++++++++++---- 2 files changed, 159 insertions(+), 32 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx index 3674e9c581..bc8843c29b 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx @@ -73,3 +73,38 @@ export function addLocalFilesTab(tab: { return originalJSX(...patchArguments(args)); }; } + +export function addLoadingOverlay(Overlay: React.ReactElement) { + function patchArguments(args: any[]) { + let [type, props, ...children] = args; + if (!props || typeof props.className !== 'string') { + return [type, props, ...children]; + } + const hasContentAreaClass = props.className.includes( + 'interface-interface-skeleton__content' + ); + if (!hasContentAreaClass) { + return [type, props, ...children]; + } + const newProps = { ...props }; + if (!Array.isArray(newProps.children)) { + newProps.children = [newProps.children]; + } + if (!newProps.children.includes(Overlay)) { + newProps.children.unshift(Overlay); + } + return [type, newProps, ...newProps.children]; + } + + // Monkey-patch window.React.createElement + const originalCreateElement = window.React.createElement as any; + (window.React as any).createElement = function (...args: any[]) { + return originalCreateElement(...patchArguments(args)); + }; + + // Monkey-patch window.ReactJSXRuntime.jsx + const originalJSX = window.ReactJSXRuntime.jsx; + window.ReactJSXRuntime.jsx = (...args: any[]) => { + return originalJSX(...patchArguments(args)); + }; +} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 49e921a4f4..3429bf8e33 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -1,19 +1,21 @@ import React, { useEffect, useState, useCallback } from '@wordpress/element'; -import { - CreatedNode, - FileNode, - FilePickerTree, - FileTree, -} from './components/FilePickerTree'; +import { FileNode, FilePickerTree } from './components/FilePickerTree'; import { store as editorStore } from '@wordpress/editor'; import { store as preferencesStore } from '@wordpress/preferences'; -import { dispatch, useSelect } from '@wordpress/data'; +import { + register, + createReduxStore, + dispatch, + useDispatch, + useSelect, +} from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; -import { addLocalFilesTab } from './add-local-files-tab'; +import { addLoadingOverlay, addLocalFilesTab } from './add-local-files-tab'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { Spinner, Button } from '@wordpress/components'; -import { useEntityProp } from '@wordpress/core-data'; +import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import css from './style.module.css'; +import { FileTree } from 'components/FilePickerTree/types'; // Pre-populated by plugin.php const WP_LOCAL_FILE_POST_TYPE = window.WP_LOCAL_FILE_POST_TYPE; @@ -22,6 +24,31 @@ let fileTreePromise = apiFetch({ path: '/static-files-editor/v1/get-files-tree', }); +// Create a custom store for transient UI state +const STORE_NAME = 'static-files-editor/ui'; +const uiStore = createReduxStore(STORE_NAME, { + reducer(state = { isPostLoading: false }, action) { + switch (action.type) { + case 'SET_POST_LOADING': + return { ...state, isPostLoading: action.isLoading }; + default: + return state; + } + }, + actions: { + setPostLoading(isLoading) { + return { type: 'SET_POST_LOADING', isLoading }; + }, + }, + selectors: { + isPostLoading(state) { + return state.isPostLoading; + }, + }, +}); + +register(uiStore); + function ConnectedFilePickerTree() { const [fileTree, setFileTree] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -31,6 +58,63 @@ function ConnectedFilePickerTree() { const [selectedPath, setSelectedPath] = useState( meta?.local_file_path || '/' ); + // interface-interface-skeleton__content + useEffect(() => { + async function refreshPostId() { + setPostLoading(true); + const { post_id } = (await apiFetch({ + path: '/static-files-editor/v1/get-or-create-post-for-file', + method: 'POST', + data: { path: selectedPath }, + })) as { post_id: string }; + setSelectedPostId(post_id); + } + refreshPostId(); + }, [selectedPath]); + + const initialPostId = useSelect( + (select) => select(editorStore).getCurrentPostId(), + [] + ); + const [selectedPostId, setSelectedPostId] = useState(initialPostId); + + const { post, hasLoadedPost, onNavigateToEntityRecord } = useSelect( + (select) => { + const { getEntityRecord, isResolving, hasFinishedResolution } = + select(coreStore); + return { + onNavigateToEntityRecord: + select(blockEditorStore).getSettings() + .onNavigateToEntityRecord, + post: getEntityRecord( + 'postType', + WP_LOCAL_FILE_POST_TYPE, + selectedPostId + ), + hasLoadedPost: hasFinishedResolution('getEntityRecord', [ + 'postType', + WP_LOCAL_FILE_POST_TYPE, + selectedPostId, + ]), + }; + }, + [selectedPostId] + ); + + const { setPostLoading } = useDispatch(STORE_NAME); + + useEffect(() => { + // Only navigate once the post has been loaded. Otherwise the editor + // will disappear for a second – the component renders its + // children conditionally on having the post available. + setPostLoading(!hasLoadedPost); + if (hasLoadedPost && post) { + onNavigateToEntityRecord({ + postId: selectedPostId, + postType: WP_LOCAL_FILE_POST_TYPE, + }); + } + }, [hasLoadedPost, post, setPostLoading]); const refreshFileTree = useCallback(async () => { fileTreePromise = apiFetch({ @@ -52,12 +136,6 @@ function ConnectedFilePickerTree() { }); }, []); - const onNavigateToEntityRecord = useSelect( - (select) => - select(blockEditorStore).getSettings().onNavigateToEntityRecord, - [] - ); - const handleNodeDeleted = async (path: string) => { try { await apiFetch({ @@ -72,23 +150,7 @@ function ConnectedFilePickerTree() { }; const handleFileClick = async (filePath: string, node: FileNode) => { - if (node.type === 'folder') { - setSelectedPath(filePath); - return; - } - - // 1. Create/get post for this file path - const { post_id } = (await apiFetch({ - path: '/static-files-editor/v1/get-or-create-post-for-file', - method: 'POST', - data: { path: filePath }, - })) as { post_id: string }; - - // 2. Switch to the new post in the editor - onNavigateToEntityRecord({ - postId: post_id, - postType: WP_LOCAL_FILE_POST_TYPE, - }); + setSelectedPath(filePath); }; const handleNodesCreated = async (tree: FileTree) => { @@ -188,6 +250,36 @@ addLocalFilesTab({ ), }); +function PostLoadingOverlay() { + const isLoading = useSelect( + (select) => select(STORE_NAME).isPostLoading(), + [] + ); + if (!isLoading) { + return null; + } + return ( +
+ +
+ ); +} + +addLoadingOverlay(); + dispatch(preferencesStore).set('welcomeGuide', false); dispatch(preferencesStore).set('enableChoosePatternModal', false); dispatch(editorStore).setIsListViewOpened(true); From fdf93936a277bd0d27c8f51a993c6cbcbbc0c7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Dec 2024 00:45:12 +0100 Subject: [PATCH 26/71] Add WP_Git_Cached_Index that stores objects in a local directory --- .../blueprint.json | 4 + .../run.sh | 1 + .../playground/data-liberation/bootstrap.php | 1 + .../src/git/WP_Git_Cached_Index.php | 81 +++++++++++++++++++ .../src/git/WP_Git_Pack_Processor.php | 9 ++- 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php diff --git a/packages/playground/data-liberation-static-files-editor/blueprint.json b/packages/playground/data-liberation-static-files-editor/blueprint.json index 098ae0160f..b05a2498f3 100644 --- a/packages/playground/data-liberation-static-files-editor/blueprint.json +++ b/packages/playground/data-liberation-static-files-editor/blueprint.json @@ -20,6 +20,10 @@ "step": "activatePlugin", "pluginPath": "z-data-liberation-static-files-editor/plugin.php" }, + { + "step": "activatePlugin", + "pluginPath": "gutenberg/gutenberg.php" + }, { "step": "runPHP", "code": " 'My Notes', 'post_status' => 'publish', 'post_type' => 'page'));" diff --git a/packages/playground/data-liberation-static-files-editor/run.sh b/packages/playground/data-liberation-static-files-editor/run.sh index d78b543d41..d90ec0835c 100644 --- a/packages/playground/data-liberation-static-files-editor/run.sh +++ b/packages/playground/data-liberation-static-files-editor/run.sh @@ -8,5 +8,6 @@ bun --inspect ../cli/src/cli.ts \ --mount=../data-liberation-static-files-editor:/wordpress/wp-content/plugins/z-data-liberation-static-files-editor \ --mount=../data-liberation-markdown:/wordpress/wp-content/plugins/z-data-liberation-markdown \ --mount=../data-liberation:/wordpress/wp-content/plugins/data-liberation \ + --mount=../../../../gutenberg:/wordpress/wp-content/plugins/gutenberg \ --mount=./my-notes/workdir:/wordpress/wp-content/uploads/static-pages \ --blueprint=./blueprint.json diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 9df2f90f14..327cc2fc6a 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -86,6 +86,7 @@ require_once __DIR__ . '/src/git/WP_Git_Client.php'; require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; require_once __DIR__ . '/src/git/WP_Git_Pack_Index.php'; +require_once __DIR__ . '/src/git/WP_Git_Cached_Index.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php new file mode 100644 index 0000000000..c44bf19591 --- /dev/null +++ b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php @@ -0,0 +1,81 @@ +fs = $fs; + if(!$this->fs->is_dir('objects')) { + $this->fs->mkdir('objects'); + } + if(!$this->fs->is_dir('refs')) { + $this->fs->mkdir('refs'); + } + if(!$this->fs->is_dir('refs/heads')) { + $this->fs->mkdir('refs/heads'); + } + } + + /** + * @TODO: Streaming read + */ + public function get_object($oid) { + $contents = $this->fs->read_file($this->get_object_path($oid)); + return WP_Git_Pack_Processor::inflate($contents); + } + + public function set_head($head, $oid) { + if($head !== 'HEAD' && !str_starts_with($head, 'refs/heads/')) { + _doing_it_wrong(__METHOD__, 'Invalid head: ' . $head); + return false; + } + return $this->fs->put_contents($head, $oid); + } + + public function get_head($head='HEAD') { + if($head === 'HEAD') { + $head_contents = $this->fs->read_file('HEAD'); + if(strpos($head_contents, 'ref: ') !== 0) { + return null; + } + $head = trim(substr($head_contents, 5)); + } else if(!str_starts_with($head, 'refs/heads/')) { + _doing_it_wrong(__METHOD__, 'Invalid head: ' . $head); + return false; + } + return trim($this->fs->read_file($head)); + } + + public function add_object($type, $content) { + $oid = sha1(self::wrap_git_object($type, $content)); + $oid_path = $this->get_object_path($oid); + $oid_dir = dirname($oid_path); + if(!$this->fs->is_dir($oid_dir)) { + $this->fs->mkdir($oid_dir, true); + } + $success = $this->fs->put_contents( + $this->get_object_path($oid), + WP_Git_Pack_Processor::deflate($content) + ); + if(!$success) { + return false; + } + return $oid; + } + + private function get_object_path($oid) { + return 'objects/' . $oid[0] . $oid[1] . '/' . substr($oid, 2); + } + + static private function wrap_git_object($type, $object) { + $length = strlen($object); + $type_name = WP_Git_Pack_Processor::OBJECT_NAMES[$type]; + return "$type_name $length\x00" . $object; + } + +} \ No newline at end of file diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 2bcfffddc4..480de64600 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -58,11 +58,16 @@ static public function encode(array $objects): string { return $pack . $packSha; } - static private function deflate(string $content): string { + static public function deflate(string $content): string { $context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 9]); return deflate_add($context, $content, ZLIB_FINISH); } + static public function inflate(string $content): string { + $context = inflate_init(ZLIB_ENCODING_DEFLATE); + return inflate_add($context, $content, ZLIB_FINISH); + } + static private function object_header(int $type, int $size): string { // First byte: type in bits 4-6, size bits 0-3 $firstByte = $size & 0b1111; @@ -101,7 +106,7 @@ static private function object_header(int $type, int $size): string { * } * } */ - static private function encode_tree_bytes($tree) { + static public function encode_tree_bytes($tree) { $tree_bytes = ''; foreach ($tree as $value) { $tree_bytes .= $value['mode'] . " " . $value['name'] . "\0" . hex2bin($value['sha1']); From 63ed39a653eae6cc2216c90b343ff39fb8916d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Dec 2024 18:27:00 +0100 Subject: [PATCH 27/71] Expand WP_Git_Cached_Index to support more operations and commits --- .../src/git/WP_Git_Cached_Index.php | 270 ++++++++++++++++-- .../src/git/WP_Git_Pack_Processor.php | 23 +- 2 files changed, 269 insertions(+), 24 deletions(-) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php index c44bf19591..c62169a01a 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php @@ -6,6 +6,16 @@ class WP_Git_Cached_Index { private $fs; + private $oid; + private $type; + private $length; + private $contents; + private $parsed_commit; + private $parsed_tree; + private $last_error; + + private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; + public function __construct( WP_Abstract_Filesystem $fs ) { @@ -22,37 +32,135 @@ public function __construct( } /** - * @TODO: Streaming read + * @TODO: Streaming read. Don't load everything into memory. */ - public function get_object($oid) { + public function read_object($oid) { + // Reset the object state + $this->oid = null; + $this->type = null; + $this->length = null; + $this->contents = null; + $this->parsed_commit = null; + $this->parsed_tree = null; + $contents = $this->fs->read_file($this->get_object_path($oid)); - return WP_Git_Pack_Processor::inflate($contents); + $contents = WP_Git_Pack_Processor::inflate($contents); + $type_length = strpos($contents, ' '); + $this->oid = $oid; + $this->type = substr($contents, 0, $type_length); + $this->length = substr($contents, $type_length + 1, strpos($contents, "\x00", $type_length) - $type_length - 1); + $this->contents = substr($contents, strpos($contents, "\x00", $type_length) + 1); + if($this->type === WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT]) { + $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_message($this->contents); + } else if($this->type === WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_TREE]) { + $this->parsed_tree = WP_Git_Pack_Processor::parse_tree_bytes($this->contents); + } + return true; + } + + public function oid_exists($oid) { + return $this->fs->is_file($this->get_object_path($oid)); } - public function set_head($head, $oid) { - if($head !== 'HEAD' && !str_starts_with($head, 'refs/heads/')) { - _doing_it_wrong(__METHOD__, 'Invalid head: ' . $head); + public function read_by_path($path, $root_tree_oid=null) { + if($root_tree_oid === null) { + $head_oid = $this->get_ref_head('HEAD'); + if(false === $this->read_object($head_oid)) { + return false; + } + $root_tree_oid = $this->get_commit_tree_oid(); + } + if(false === $this->read_object($root_tree_oid)) { return false; } - return $this->fs->put_contents($head, $oid); - } - public function get_head($head='HEAD') { - if($head === 'HEAD') { - $head_contents = $this->fs->read_file('HEAD'); - if(strpos($head_contents, 'ref: ') !== 0) { + $path = trim($path, '/'); + if (empty($path)) { + return true; + } + + $path_segments = explode('/', $path); + foreach ($path_segments as $segment) { + if (!isset($this->parsed_tree[$segment])) { return null; } - $head = trim(substr($head_contents, 5)); - } else if(!str_starts_with($head, 'refs/heads/')) { - _doing_it_wrong(__METHOD__, 'Invalid head: ' . $head); + $next_oid = $this->parsed_tree[$segment]['sha1']; + if(false === $this->read_object($next_oid)) { + return false; + } + } + + return true; + } + + public function get_descendants($tree_oid) { + if(false === $this->read_object($tree_oid)) { + return []; + } + foreach ($this->parsed_tree as $object) { + if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { + yield from $this->get_descendants($object['sha1']); + } else { + yield $object; + } + } + } + + public function get_type() { + return $this->type; + } + + public function get_length() { + return $this->length; + } + + public function get_contents() { + return $this->contents; + } + + public function get_parsed_commit() { + return $this->parsed_commit; + } + + public function get_commit_tree_oid() { + return $this->parsed_commit['tree']; + } + + public function get_parsed_tree() { + return $this->parsed_tree; + } + + public function set_ref_head($ref, $oid) { + if($ref !== 'HEAD' && !str_starts_with($ref, 'refs/heads/')) { + _doing_it_wrong(__METHOD__, 'Invalid head: ' . $ref, '1.0.0'); + return false; + } + return $this->fs->put_contents($ref, $oid); + } + + public function get_ref_head($ref='HEAD') { + if($ref === 'HEAD') { + $ref = $this->get_HEAD_ref(); + } else if(!str_starts_with($ref, 'refs/heads/')) { + _doing_it_wrong(__METHOD__, 'Invalid head: ' . $ref, '1.0.0'); return false; } - return trim($this->fs->read_file($head)); + return trim($this->fs->read_file($ref)); + } + + private function get_HEAD_ref() { + $ref_contents = $this->fs->read_file('HEAD'); + if(strpos($ref_contents, 'ref: ') !== 0) { + return null; + } + return trim(substr($ref_contents, 5)); } public function add_object($type, $content) { $oid = sha1(self::wrap_git_object($type, $content)); + if($this->oid_exists($oid)) { + return $oid; + } $oid_path = $this->get_object_path($oid); $oid_dir = dirname($oid_path); if(!$this->fs->is_dir($oid_dir)) { @@ -60,7 +168,9 @@ public function add_object($type, $content) { } $success = $this->fs->put_contents( $this->get_object_path($oid), - WP_Git_Pack_Processor::deflate($content) + WP_Git_Pack_Processor::deflate( + self::wrap_git_object($type, $content) + ) ); if(!$success) { return false; @@ -78,4 +188,128 @@ static private function wrap_git_object($type, $object) { return "$type_name $length\x00" . $object; } -} \ No newline at end of file + public function commit($changeset, $commit_meta=[]) { + $commit_meta['author'] = $commit_meta['author'] ?? 'John Doe '; + $commit_meta['committer'] = $commit_meta['committer'] ?? 'John Doe '; + $commit_meta['message'] = $commit_meta['message'] ?? 'Changes'; + + // First process all blob updates + $updates = $changeset['updates'] ?? []; + $deletes = $changeset['deletes'] ?? []; + $move_trees = $changeset['move_trees'] ?? []; + + // Track which trees need updating + $changed_trees = [ + '/' => ['entries' => []] + ]; + + // Process blob updates + foreach ($updates as $path => $content) { + $blob_oid = $this->add_object(WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, $content); + $this->mark_tree_path_changed($changed_trees, dirname($path)); + $changed_trees[dirname($path)]['entries'][basename($path)] = [ + 'name' => basename($path), + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'sha1' => $blob_oid + ]; + } + + // Process deletes + foreach ($deletes as $path) { + if (!$this->read_by_path(dirname($path))) { + _doing_it_wrong(__METHOD__, 'File not found in HEAD: ' . $path, '1.0.0'); + return false; + } + $this->mark_tree_path_changed($changed_trees, dirname($path)); + $changed_trees[dirname($path)]['entries'][basename($path)] = self::DELETE_PLACEHOLDER; + } + + // Process tree moves + foreach ($move_trees as $old_path => $new_path) { + if (!$this->read_by_path($old_path)) { + _doing_it_wrong(__METHOD__, 'Path not found in HEAD: ' . $old_path, '1.0.0'); + return false; + } + $this->mark_tree_path_changed($changed_trees, dirname($old_path)); + $this->mark_tree_path_changed($changed_trees, dirname($new_path)); + + $changed_trees[dirname($old_path)]['entries'][basename($old_path)] = self::DELETE_PLACEHOLDER; + $changed_trees[dirname($new_path)]['entries'][basename($new_path)] = [ + 'name' => basename($new_path), + 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, + 'sha1' => $this->oid + ]; + } + + // Process trees bottom-up recursively + $root_tree_oid = $this->commit_tree('/', $changed_trees); + + // Create commit object + $commit_message = []; + $commit_message[] = "tree " . $root_tree_oid; + if($this->get_ref_head('HEAD')) { + $commit_message[] = "parent " . $this->get_ref_head('HEAD'); + } + $commit_message[] = "author " . $commit_meta['author']; + $commit_message[] = "committer " . $commit_meta['committer']; + $commit_message[] = "\n" . $commit_meta['message']; + $commit_message = implode("\n", $commit_message); + $commit_oid = $this->add_object(WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, $commit_message); + + // Update HEAD + $head_ref = $this->get_HEAD_ref(); + if(false === $this->set_ref_head($head_ref, $commit_oid)) { + $this->last_error = 'Failed to set HEAD'; + return false; + } + return $commit_oid; + } + + private function mark_tree_path_changed(&$changed_trees, $path) { + while ($path !== '/') { + if (!isset($changed_trees[$path])) { + $changed_trees[$path] = ['entries' => []]; + } + $path = dirname($path); + } + } + + private function commit_tree($path, $changed_trees) { + $tree_objects = []; + + // Load existing tree if it exists + if ($this->read_by_path($path)) { + $tree_objects = $this->get_parsed_tree(); + } + + // Apply any changes to this tree + if (isset($changed_trees[$path]['entries'])) { + foreach ($changed_trees[$path]['entries'] as $name => $entry) { + if ($entry === self::DELETE_PLACEHOLDER) { + unset($tree_objects[$name]); + } else { + $tree_objects[$name] = $entry; + } + } + } + + // Recursively process child trees + foreach ($changed_trees as $child_path => $child_tree) { + if (dirname($child_path) === $path && $child_path !== '/') { + $child_oid = $this->commit_tree($child_path, $changed_trees); + $tree_objects[basename($child_path)] = [ + 'name' => basename($child_path), + 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, + 'sha1' => $child_oid + ]; + } + } + + // Create new tree object + return $this->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_TREE, + WP_Git_Pack_Processor::encode_tree_bytes($tree_objects) + ); + } + +} diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 480de64600..d2ab5ba0ac 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -155,11 +155,6 @@ static public function encode_packet_line(string $payload): string { return sprintf("%04x", $length) . $payload; } - static private function encode_flush(): string { - return "0000"; - } - - static public function decode($pack_bytes) { $parsed_pack = self::parse_pack_data($pack_bytes); $objects = $parsed_pack['objects']; @@ -269,7 +264,23 @@ static private function applyDelta($base_bytes, $delta_bytes) { return $result; } - static private function parse_tree_bytes($treeContent) { + static public function parse_commit_message($commit_message) { + $lines = explode("\n", $commit_message); + $parsed = []; + foreach($lines as $k => $line) { + if(!trim($line)) { + $parsed['message'] = array_slice($lines, $k + 1); + break; + } + $type_len = strpos($line, ' '); + $type = substr($line, 0, $type_len); + $value = substr($line, $type_len + 1); + $parsed[$type] = $value; + } + return $parsed; + } + + static public function parse_tree_bytes($treeContent) { $offset = 0; $files = []; From 59884eda7f6294a03f39a8898904ab79862e7729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Dec 2024 20:26:28 +0100 Subject: [PATCH 28/71] Streaming git read, ability to compute objects newly added in a commit --- .../src/git/WP_Git_Cached_Index.php | 287 ++++++++++++++---- .../src/git/WP_Git_Pack_Processor.php | 2 +- 2 files changed, 237 insertions(+), 52 deletions(-) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php index c62169a01a..515bf1db16 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php @@ -8,8 +8,10 @@ class WP_Git_Cached_Index { private $oid; private $type; - private $length; - private $contents; + private $content_inflate_handle; + private $object_content_chunk; + private $called_next_object_chunk; + private $buffered_object_content; private $parsed_commit; private $parsed_tree; private $last_error; @@ -31,33 +33,146 @@ public function __construct( } } - /** - * @TODO: Streaming read. Don't load everything into memory. - */ public function read_object($oid) { - // Reset the object state - $this->oid = null; - $this->type = null; - $this->length = null; - $this->contents = null; - $this->parsed_commit = null; - $this->parsed_tree = null; + $this->reset(); + + $object_path = $this->get_object_path($oid); + if(!$this->fs->is_file($object_path)) { + return false; + } - $contents = $this->fs->read_file($this->get_object_path($oid)); - $contents = WP_Git_Pack_Processor::inflate($contents); - $type_length = strpos($contents, ' '); $this->oid = $oid; - $this->type = substr($contents, 0, $type_length); - $this->length = substr($contents, $type_length + 1, strpos($contents, "\x00", $type_length) - $type_length - 1); - $this->contents = substr($contents, strpos($contents, "\x00", $type_length) + 1); - if($this->type === WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT]) { - $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_message($this->contents); - } else if($this->type === WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_TREE]) { - $this->parsed_tree = WP_Git_Pack_Processor::parse_tree_bytes($this->contents); + if(!$this->open_object_stream()) { + return false; + } + + // Read the object header and initialize the internal state + // for the specific get_* methods below. + $header = false; + $content = ''; + while($this->next_object_chunk()) { + $content .= $this->get_object_content_chunk(); + $null_byte_position = strpos($content, "\x00"); + if($null_byte_position === false) { + continue; + } + $header = substr($content, 0, $null_byte_position); + break; + } + + if(false === $header) { + $this->last_error = 'Failed to read the object header'; + return false; + } + + $this->object_content_chunk = substr($content, strlen($header) + 1); + + // Parse the header + $type_length = strpos($header, ' '); + $type = substr($header, 0, $type_length); + switch($type) { + case WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_BLOB]: + $this->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; + break; + case WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_TREE]: + $this->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; + break; + case WP_Git_Pack_Processor::OBJECT_NAMES[WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT]: + $this->type = WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT; + break; + default: + $this->last_error = 'Invalid object type: ' . $type; + return false; + } + return true; + } + + public function get_type() { + return $this->type; + } + + public function get_oid() { + return $this->oid; + } + + public function get_length() { + return $this->fs->get_streamed_file_length(); + } + + private function open_object_stream() { + $this->content_inflate_handle = inflate_init(ZLIB_ENCODING_DEFLATE); + if(!$this->content_inflate_handle) { + $this->last_error = 'Failed to initialize inflate handle'; + return false; + } + if(!$this->fs->open_file_stream($this->get_object_path($this->oid))) { + return false; + } + return true; + } + + public function next_object_chunk() { + if(false === $this->fs->next_file_chunk()) { + $this->last_error = $this->fs->get_error_message(); + return false; } + $this->called_next_object_chunk = true; + $chunk = $this->fs->get_file_chunk(); + $next_chunk = inflate_add($this->content_inflate_handle, $chunk); + if(false === $next_chunk) { + $this->last_error = 'Failed to inflate chunk'; + $this->close_object_stream(); + return false; + } + $this->object_content_chunk = $next_chunk; return true; } + public function get_object_content_chunk() { + return $this->object_content_chunk; + } + + private function close_object_stream() { + $this->fs->close_file_stream(); + $this->content_inflate_handle = null; + return true; + } + + public function get_parsed_commit() { + if(null === $this->parsed_commit) { + $commit_contents = $this->read_entire_object_contents(); + $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_message($commit_contents); + } + return $this->parsed_commit; + } + + public function get_parsed_tree() { + if(null === $this->parsed_tree) { + $tree_contents = $this->read_entire_object_contents(); + $this->parsed_tree = WP_Git_Pack_Processor::parse_tree_bytes($tree_contents); + } + return $this->parsed_tree; + } + + public function read_entire_object_contents() { + // If we've advanced the stream, we can't reuse it to read the entire + // object anymore. Let's re-initialize the stream. + if($this->called_next_object_chunk) { + $this->read_object($this->oid); + } + if(null !== $this->buffered_object_content) { + return $this->buffered_object_content; + } + // Load the entire object into memory and keep the result + // for later use. We'll likely need it again before we're + // done with the current object. + $this->buffered_object_content = $this->object_content_chunk; + while($this->next_object_chunk()) { + $this->buffered_object_content .= $this->get_object_content_chunk(); + } + return $this->buffered_object_content; + } + public function oid_exists($oid) { return $this->fs->is_file($this->get_object_path($oid)); } @@ -68,7 +183,10 @@ public function read_by_path($path, $root_tree_oid=null) { if(false === $this->read_object($head_oid)) { return false; } - $root_tree_oid = $this->get_commit_tree_oid(); + $root_tree_oid = $this->get_parsed_commit()['tree'] ?? null; + } + if($root_tree_oid === null) { + return false; } if(false === $this->read_object($root_tree_oid)) { return false; @@ -93,41 +211,90 @@ public function read_by_path($path, $root_tree_oid=null) { return true; } - public function get_descendants($tree_oid) { - if(false === $this->read_object($tree_oid)) { - return []; + public function get_last_error() { + return $this->last_error; + } + + public function find_objects_added_in($new_tree_oid, $old_tree_oid=null) { + if($new_tree_oid === $old_tree_oid) { + return false; } - foreach ($this->parsed_tree as $object) { - if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { - yield from $this->get_descendants($object['sha1']); - } else { - yield $object; - } + + // Resolve the actual tree oid if $new_tree_oid is a commit + if(false === $this->read_object($new_tree_oid)) { + $this->last_error = 'Failed to read object: ' . $new_tree_oid; + return false; + } + if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { + // yield the commit object itself + $parsed_commit = $this->get_parsed_commit(); + $new_tree_oid = $parsed_commit['tree']; + yield $this->oid; } - } - public function get_type() { - return $this->type; - } + // Resolve the actual tree oid if $old_tree_oid is a commit + if($old_tree_oid) { + if(false === $this->read_object($old_tree_oid)) { + $this->last_error = 'Failed to read object: ' . $old_tree_oid; + return false; + } + if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { + $old_tree_oid = $this->get_parsed_commit()['tree']; + } + } - public function get_length() { - return $this->length; - } + $stack = [[$new_tree_oid, $old_tree_oid]]; + + while(!empty($stack)) { + list($current_new_oid, $current_old_oid) = array_pop($stack); + + if(false === $this->read_object($current_new_oid)) { + $this->last_error = 'Failed to read object: ' . $current_new_oid; + return false; + } + $new_tree = $this->get_parsed_tree(); + + $old_tree = []; + if($current_old_oid) { + if(false === $this->read_object($current_old_oid)) { + $this->last_error = 'Failed to read object: ' . $current_old_oid; + return false; + } + $old_tree = $this->get_parsed_tree(); + } - public function get_contents() { - return $this->contents; - } + foreach($new_tree as $name => $object) { + // Object is new + if(!isset($old_tree[$name])) { + if(false === $this->read_object($object['sha1'])) { + $this->last_error = 'Failed to read object: ' . $object['sha1']; + return false; + } + yield $object['sha1']; + if($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { + $stack[] = [$object['sha1'], null]; + } + continue; + } - public function get_parsed_commit() { - return $this->parsed_commit; - } + // Object is unchanged + if($object['sha1'] === $old_tree[$name]['sha1']) { + continue; + } - public function get_commit_tree_oid() { - return $this->parsed_commit['tree']; - } + if(false === $this->read_object($object['sha1'])) { + $this->last_error = 'Failed to read object: ' . $object['sha1']; + return false; + } + + yield $object['sha1']; - public function get_parsed_tree() { - return $this->parsed_tree; + if($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { + // Object is a changed directory - add to stack for recursive processing + $stack[] = [$object['sha1'], $old_tree[$name]['sha1']]; + } + } + } } public function set_ref_head($ref, $oid) { @@ -205,6 +372,7 @@ public function commit($changeset, $commit_meta=[]) { // Process blob updates foreach ($updates as $path => $content) { + $path = '/' . ltrim($path, '/'); $blob_oid = $this->add_object(WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, $content); $this->mark_tree_path_changed($changed_trees, dirname($path)); $changed_trees[dirname($path)]['entries'][basename($path)] = [ @@ -216,6 +384,7 @@ public function commit($changeset, $commit_meta=[]) { // Process deletes foreach ($deletes as $path) { + $path = '/' . ltrim($path, '/'); if (!$this->read_by_path(dirname($path))) { _doing_it_wrong(__METHOD__, 'File not found in HEAD: ' . $path, '1.0.0'); return false; @@ -226,6 +395,8 @@ public function commit($changeset, $commit_meta=[]) { // Process tree moves foreach ($move_trees as $old_path => $new_path) { + $old_path = '/' . ltrim($old_path, '/'); + $new_path = '/' . ltrim($new_path, '/'); if (!$this->read_by_path($old_path)) { _doing_it_wrong(__METHOD__, 'Path not found in HEAD: ' . $old_path, '1.0.0'); return false; @@ -244,7 +415,7 @@ public function commit($changeset, $commit_meta=[]) { // Process trees bottom-up recursively $root_tree_oid = $this->commit_tree('/', $changed_trees); - // Create commit object + // Create a new commit object $commit_message = []; $commit_message[] = "tree " . $root_tree_oid; if($this->get_ref_head('HEAD')) { @@ -262,9 +433,22 @@ public function commit($changeset, $commit_meta=[]) { $this->last_error = 'Failed to set HEAD'; return false; } + $this->reset(); return $commit_oid; } + private function reset() { + $this->close_object_stream(); + $this->oid = null; + $this->type = null; + $this->parsed_commit = null; + $this->parsed_tree = null; + $this->called_next_object_chunk = false; + $this->buffered_object_content = null; + $this->object_content_chunk = null; + $this->last_error = null; + } + private function mark_tree_path_changed(&$changed_trees, $path) { while ($path !== '/') { if (!isset($changed_trees[$path])) { @@ -312,4 +496,5 @@ private function commit_tree($path, $changed_trees) { ); } + } diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index d2ab5ba0ac..5d61512f3b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -269,7 +269,7 @@ static public function parse_commit_message($commit_message) { $parsed = []; foreach($lines as $k => $line) { if(!trim($line)) { - $parsed['message'] = array_slice($lines, $k + 1); + $parsed['message'] = implode("\n", array_slice($lines, $k + 1)); break; } $type_len = strpos($line, ' '); From 6200a4a674ad03ba01772fb47e0d2da563a0f13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Dec 2024 23:10:35 +0100 Subject: [PATCH 29/71] Implement git pull and push --- .../playground/data-liberation/bootstrap.php | 2 +- .../src/git/WP_Git_Cached_Index.php | 237 +++++++++++------ .../data-liberation/src/git/WP_Git_Client.php | 143 ++++++---- .../src/git/WP_Git_Filesystem.php | 8 +- .../src/git/WP_Git_Pack_Index.php | 248 ------------------ .../src/git/WP_Git_Pack_Processor.php | 27 +- 6 files changed, 272 insertions(+), 393 deletions(-) delete mode 100644 packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 327cc2fc6a..2584b924cb 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -13,6 +13,7 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Abstract_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Local_Filesystem.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_In_Memory_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_File_Visitor_Event.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Visitor.php'; @@ -85,7 +86,6 @@ require_once __DIR__ . '/src/git/WP_Git_Client.php'; require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; -require_once __DIR__ . '/src/git/WP_Git_Pack_Index.php'; require_once __DIR__ . '/src/git/WP_Git_Cached_Index.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php index 515bf1db16..f7ed67289b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php @@ -10,7 +10,7 @@ class WP_Git_Cached_Index { private $type; private $content_inflate_handle; private $object_content_chunk; - private $called_next_object_chunk; + private $called_next_body_chunk; private $buffered_object_content; private $parsed_commit; private $parsed_tree; @@ -22,14 +22,20 @@ public function __construct( WP_Abstract_Filesystem $fs ) { $this->fs = $fs; - if(!$this->fs->is_dir('objects')) { - $this->fs->mkdir('objects'); - } - if(!$this->fs->is_dir('refs')) { - $this->fs->mkdir('refs'); - } - if(!$this->fs->is_dir('refs/heads')) { - $this->fs->mkdir('refs/heads'); + $this->initialize_filesystem(); + } + + private function initialize_filesystem() { + $paths = [ + 'objects', + 'refs', + 'refs/heads', + 'refs/remotes', + ]; + foreach($paths as $path) { + if(!$this->fs->is_dir($path)) { + $this->fs->mkdir($path); + } } } @@ -50,8 +56,8 @@ public function read_object($oid) { // for the specific get_* methods below. $header = false; $content = ''; - while($this->next_object_chunk()) { - $content .= $this->get_object_content_chunk(); + while($this->next_body_chunk()) { + $content .= $this->get_body_chunk(); $null_byte_position = strpos($content, "\x00"); if($null_byte_position === false) { continue; @@ -111,12 +117,12 @@ private function open_object_stream() { return true; } - public function next_object_chunk() { + public function next_body_chunk() { if(false === $this->fs->next_file_chunk()) { $this->last_error = $this->fs->get_error_message(); return false; } - $this->called_next_object_chunk = true; + $this->called_next_body_chunk = true; $chunk = $this->fs->get_file_chunk(); $next_chunk = inflate_add($this->content_inflate_handle, $chunk); if(false === $next_chunk) { @@ -128,7 +134,7 @@ public function next_object_chunk() { return true; } - public function get_object_content_chunk() { + public function get_body_chunk() { return $this->object_content_chunk; } @@ -139,15 +145,19 @@ private function close_object_stream() { } public function get_parsed_commit() { - if(null === $this->parsed_commit) { + if(null === $this->parsed_commit && $this->oid) { $commit_contents = $this->read_entire_object_contents(); $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_message($commit_contents); + if(!$this->parsed_commit) { + $this->last_error = 'Failed to parse commit'; + $this->parsed_commit = []; + } } return $this->parsed_commit; } public function get_parsed_tree() { - if(null === $this->parsed_tree) { + if(null === $this->parsed_tree && $this->oid) { $tree_contents = $this->read_entire_object_contents(); $this->parsed_tree = WP_Git_Pack_Processor::parse_tree_bytes($tree_contents); } @@ -157,7 +167,7 @@ public function get_parsed_tree() { public function read_entire_object_contents() { // If we've advanced the stream, we can't reuse it to read the entire // object anymore. Let's re-initialize the stream. - if($this->called_next_object_chunk) { + if($this->called_next_body_chunk) { $this->read_object($this->oid); } if(null !== $this->buffered_object_content) { @@ -167,8 +177,8 @@ public function read_entire_object_contents() { // for later use. We'll likely need it again before we're // done with the current object. $this->buffered_object_content = $this->object_content_chunk; - while($this->next_object_chunk()) { - $this->buffered_object_content .= $this->get_object_content_chunk(); + while($this->next_body_chunk()) { + $this->buffered_object_content .= $this->get_body_chunk(); } return $this->buffered_object_content; } @@ -199,11 +209,14 @@ public function read_by_path($path, $root_tree_oid=null) { $path_segments = explode('/', $path); foreach ($path_segments as $segment) { - if (!isset($this->parsed_tree[$segment])) { - return null; + $parsed_tree = $this->get_parsed_tree(); + if (!isset($parsed_tree[$segment])) { + $this->reset(); + return false; } - $next_oid = $this->parsed_tree[$segment]['sha1']; + $next_oid = $parsed_tree[$segment]['sha1']; if(false === $this->read_object($next_oid)) { + $this->reset(); return false; } } @@ -215,9 +228,33 @@ public function get_last_error() { return $this->last_error; } - public function find_objects_added_in($new_tree_oid, $old_tree_oid=null) { - if($new_tree_oid === $old_tree_oid) { - return false; + public function find_path_descendants($path) { + if(!$this->read_by_path($path)) { + return []; + } + $stack = [$this->oid]; + $oids = []; + while(!empty($stack)) { + $oid = array_pop($stack); + if(!$this->read_object($oid)) { + return false; + } + if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { + $tree = $this->get_parsed_tree(); + foreach($tree as $object) { + $oids[] = $object['sha1']; + } + } else if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_BLOB) { + $oids[] = $this->get_oid(); + } + } + return $oids; + } + + public function find_objects_added_in($new_tree_oid, $old_tree_oid=null, $options=[]) { + $old_tree_index = $options['old_tree_index'] ?? $this; + if($old_tree_index === null) { + $old_tree_index = $this; } // Resolve the actual tree oid if $new_tree_oid is a commit @@ -234,93 +271,127 @@ public function find_objects_added_in($new_tree_oid, $old_tree_oid=null) { // Resolve the actual tree oid if $old_tree_oid is a commit if($old_tree_oid) { - if(false === $this->read_object($old_tree_oid)) { + if(false === $old_tree_index->read_object($old_tree_oid)) { $this->last_error = 'Failed to read object: ' . $old_tree_oid; return false; } - if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { - $old_tree_oid = $this->get_parsed_commit()['tree']; + if($old_tree_index->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { + $old_tree_oid = $old_tree_index->get_parsed_commit()['tree']; } } + if($new_tree_oid === $old_tree_oid) { + return false; + } + $stack = [[$new_tree_oid, $old_tree_oid]]; while(!empty($stack)) { list($current_new_oid, $current_old_oid) = array_pop($stack); + + // Object is unchanged + if($current_new_oid === $current_old_oid) { + continue; + } if(false === $this->read_object($current_new_oid)) { $this->last_error = 'Failed to read object: ' . $current_new_oid; return false; } + if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_BLOB) { + yield $this->get_oid(); + continue; + } else if($this->get_type() !== WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { + _doing_it_wrong(__METHOD__, 'Invalid object type in find_objects_added_in: ' . $this->get_type(), '1.0.0'); + return false; + } + $new_tree = $this->get_parsed_tree(); + yield $this->get_oid(); $old_tree = []; if($current_old_oid) { - if(false === $this->read_object($current_old_oid)) { + if(false === $old_tree_index->read_object($current_old_oid)) { $this->last_error = 'Failed to read object: ' . $current_old_oid; return false; } - $old_tree = $this->get_parsed_tree(); + $old_tree = $old_tree_index->get_parsed_tree(); } foreach($new_tree as $name => $object) { - // Object is new - if(!isset($old_tree[$name])) { - if(false === $this->read_object($object['sha1'])) { - $this->last_error = 'Failed to read object: ' . $object['sha1']; - return false; - } - yield $object['sha1']; - if($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { - $stack[] = [$object['sha1'], null]; - } - continue; - } - - // Object is unchanged - if($object['sha1'] === $old_tree[$name]['sha1']) { - continue; - } - - if(false === $this->read_object($object['sha1'])) { - $this->last_error = 'Failed to read object: ' . $object['sha1']; - return false; - } - - yield $object['sha1']; - - if($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { - // Object is a changed directory - add to stack for recursive processing - $stack[] = [$object['sha1'], $old_tree[$name]['sha1']]; - } + $stack[] = [$object['sha1'], $old_tree[$name]['sha1'] ?? null]; } } } public function set_ref_head($ref, $oid) { - if($ref !== 'HEAD' && !str_starts_with($ref, 'refs/heads/')) { - _doing_it_wrong(__METHOD__, 'Invalid head: ' . $ref, '1.0.0'); + $path = $this->resolve_ref_file_path($ref); + if(!$path) { + return false; + } + return $this->fs->put_contents($path, $oid); + } + + public function get_ref_head($ref='HEAD', $options=[]) { + if($this->oid_exists($ref)) { + return $ref; + } + $path = $this->resolve_ref_file_path($ref); + if(!$path) { + $this->last_error = 'Failed to resolve ref file path: ' . $ref; return false; } - return $this->fs->put_contents($ref, $oid); + if(!$this->fs->is_file($path)) { + $this->last_error = 'Ref file not found: ' . $path; + return false; + } + $contents = trim($this->fs->read_file($path)); + if($options['resolve_ref'] ?? true) { + if(strpos($contents, 'ref: ') === 0) { + $branch = trim(substr($contents, 5)); + return $this->get_ref_head($branch, $options); + } + } + return $contents; } - public function get_ref_head($ref='HEAD') { - if($ref === 'HEAD') { - $ref = $this->get_HEAD_ref(); - } else if(!str_starts_with($ref, 'refs/heads/')) { - _doing_it_wrong(__METHOD__, 'Invalid head: ' . $ref, '1.0.0'); + private function resolve_ref_file_path($ref) { + $ref = trim($ref); + if(str_starts_with($ref, 'ref: ')) { + $ref = trim(substr($ref, 5)); + } + if( + str_contains($ref, '/') && + !str_starts_with($ref, 'refs/heads/') && + !str_starts_with($ref, 'refs/remotes/') + ) { + _doing_it_wrong(__METHOD__, 'Invalid ref name: ' . $ref, '1.0.0'); return false; } - return trim($this->fs->read_file($ref)); + if(str_contains($ref, '../')) { + _doing_it_wrong(__METHOD__, 'Invalid ref name: ' . $ref, '1.0.0'); + return false; + } + + // Make sure all the directories leading up to the ref exist + // @TODO: Support recursive mode in mkdir() + $segments = explode('/', dirname($ref)); + $path = ''; + foreach($segments as $segment) { + $path .= '/' . $segment; + if(!$this->fs->is_dir($path)) { + $this->fs->mkdir($path); + } + } + return $ref; } - private function get_HEAD_ref() { - $ref_contents = $this->fs->read_file('HEAD'); - if(strpos($ref_contents, 'ref: ') !== 0) { - return null; + public function branch_exists($ref) { + $path = $this->resolve_ref_file_path($ref); + if(!$path) { + return false; } - return trim(substr($ref_contents, 5)); + return $this->fs->is_file($path); } public function add_object($type, $content) { @@ -421,17 +492,19 @@ public function commit($changeset, $commit_meta=[]) { if($this->get_ref_head('HEAD')) { $commit_message[] = "parent " . $this->get_ref_head('HEAD'); } - $commit_message[] = "author " . $commit_meta['author']; - $commit_message[] = "committer " . $commit_meta['committer']; + $commit_message[] = "author " . $commit_meta['author'] . " " . time() . " +0000"; + $commit_message[] = "committer " . $commit_meta['committer'] . " " . time() . " +0000"; $commit_message[] = "\n" . $commit_meta['message']; $commit_message = implode("\n", $commit_message); $commit_oid = $this->add_object(WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, $commit_message); // Update HEAD - $head_ref = $this->get_HEAD_ref(); - if(false === $this->set_ref_head($head_ref, $commit_oid)) { - $this->last_error = 'Failed to set HEAD'; - return false; + $head_ref = $this->get_ref_head('HEAD', ['resolve_ref' => false]); + if($this->branch_exists($head_ref)) { + if(false === $this->set_ref_head($head_ref, $commit_oid)) { + $this->last_error = 'Failed to set HEAD'; + return false; + } } $this->reset(); return $commit_oid; @@ -443,7 +516,7 @@ private function reset() { $this->type = null; $this->parsed_commit = null; $this->parsed_tree = null; - $this->called_next_object_chunk = false; + $this->called_next_body_chunk = false; $this->buffered_object_content = null; $this->object_content_chunk = null; $this->last_error = null; @@ -489,6 +562,10 @@ private function commit_tree($path, $changed_trees) { } } + // Git seems to require alphabetical order for the tree objects. + // Or at least GitHub rejects the push if the tree objects are not sorted. + ksort($tree_objects); + // Create new tree object return $this->add_object( WP_Git_Pack_Processor::OBJECT_TYPE_TREE, diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 9708d3d982..ab956c5735 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -12,12 +12,18 @@ class WP_Git_Client { * @var Client */ private $http_client; + /** + * @var WP_Git_Cached_Index + */ + private $index; + private $remote_name = 'origin'; - public function __construct($repoUrl, $options = []) { + public function __construct(WP_Git_Cached_Index $index, $repoUrl, $options = []) { $this->repoUrl = rtrim($repoUrl, '/'); $this->author = $options['author'] ?? "John Doe "; $this->committer = $options['committer'] ?? "John Doe "; $this->http_client = $options['http_client'] ?? new Client(); + $this->index = $index; } public function fetchRefs($prefix) { @@ -52,6 +58,12 @@ public function fetchRefs($prefix) { $newline_pos = strpos($frame, "\n"); $name = substr($frame, $space_pos + 1, $newline_pos - $space_pos - 1); $refs[$name] = $hash; + + $localized_refname = $name; + if(str_starts_with($name, 'refs/heads/')) { + $localized_refname = substr($name, strlen('refs/heads/')); + } + $this->index->set_ref_head('refs/remotes/' . $this->remote_name . '/' . $localized_refname, $hash); } return $refs; } @@ -67,38 +79,36 @@ private function parse_git_protocol_v2_packets($bytes) { } } - public function push($git_objects, $options = []) { - $empty_hash = "0000000000000000000000000000000000000000"; - $parent_hash = $options['parent_hash'] ?? $empty_hash; - $tree_hash = $options['tree_hash'] ?? $empty_hash; - $branchName = $options['branch_name']; - $author = ($options['author'] ?? $this->author) . " " . time() . " +0000"; - $committer = ($options['committer'] ?? $this->committer) . " " . time() . " +0000"; - $message = $options['message'] ?? "Changes from WordPress."; + public function force_push_one_commit() { + $push_ref_name = $this->index->get_ref_head('HEAD', ['resolve_ref' => false]); + $push_ref_name = $this->localize_ref_name($push_ref_name); - $parent = ''; - if($parent_hash !== $empty_hash) { - $parent = "parent $parent_hash\n"; + $push_commit = $this->index->get_ref_head('refs/heads/' . $push_ref_name); + $this->index->read_object($push_commit); + $parent_hash = $this->index->get_parsed_commit()['parent'] ?? '0000000000000000000000000000000000000000'; + + $remote_commit = $this->index->get_ref_head('refs/remotes/' . $this->remote_name . '/' . $push_ref_name); + // @TODO: Do find_objects_added_since to enable pushing multiple commits at once. + // OR! perhaps supporting "have" and "want" would solve this. + $delta = $this->index->find_objects_added_in($push_commit, $remote_commit); + + // @TODO: Implement streaming push bytes instead of buffering everything like this. + $pack_objects = []; + foreach($delta as $oid) { + // @TODO: just stream the saved object instead of re-reading and re-encoding it. + $body = ''; + do { + $body .= $this->index->get_body_chunk(); + } while($this->index->next_body_chunk()); + $pack_objects[] = [ + 'type' => $this->index->get_type(), + 'content' => $body, + ]; } - $commit_object = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, - 'content' => sprintf( - "tree %s\n%sauthor %s\ncommitter %s\n\n%s\n", - $tree_hash, - $parent, - $author, - $committer, - $message - ), - 'tree' => $tree_hash, - ]); - $commit_sha = $commit_object['oid']; - $git_objects[] = $commit_object; - - $push_packet = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $commit_sha refs/heads/$branchName\0report-status force-update\n"); + $push_packet = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $push_commit refs/heads/$push_ref_name\0report-status force-update\n"); $push_packet .= "0000"; - $push_packet .= WP_Git_Pack_Processor::encode($git_objects); + $push_packet .= WP_Git_Pack_Processor::encode($pack_objects); $push_packet .= "0000"; $url = rtrim($this->repoUrl, '.git').'.git/git-receive-pack'; @@ -110,14 +120,22 @@ public function push($git_objects, $options = []) { $response_chunks = iterator_to_array($this->parse_multiplexed_pack_data($response)); if( trim($response_chunks[0]['data']) !== 'unpack ok' || - trim($response_chunks[1]['data']) !== 'ok refs/heads/' . $branchName + trim($response_chunks[1]['data']) !== 'ok refs/heads/' . $push_ref_name ) { throw new Exception('Push failed:' . $response); } - return [ - 'new_head_hash' => $commit_sha, - 'new_tree_hash' => $tree_hash, - ]; + $this->index->set_ref_head('refs/remotes/' . $this->remote_name . '/' . $push_ref_name, $push_commit); + return true; + } + + private function localize_ref_name($ref_name) { + if(str_starts_with($ref_name, 'ref: ')) { + $ref_name = trim(substr($ref_name, 5)); + } + if(str_starts_with($ref_name, 'refs/heads/')) { + return substr($ref_name, strlen('refs/heads/')); + } + return $ref_name; } public function list_objects($ref_hash) { @@ -136,9 +154,47 @@ public function list_objects($ref_hash) { ]); $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); - return WP_Git_Pack_Index::from_pack_data($pack_data); + return WP_Git_Pack_Processor::decode($pack_data); } + public function force_pull($branch_name, $path = '/') { + $path = '/' . ltrim($path, '/'); + $remote_refs = $this->fetchRefs('refs/heads/' . $branch_name); + $remote_head = $remote_refs['refs/heads/' . $branch_name]; + $remote_index = $this->list_objects($remote_head); + + $remote_branch_ref = 'refs/heads/' . $branch_name; + $remote_index->set_ref_head($remote_branch_ref, $remote_head); + $remote_index->set_ref_head('HEAD', 'ref: ' . $remote_branch_ref); + + $local_index = $this->index; + $local_ref = $local_index->get_ref_head('refs/heads/' . $branch_name); + + $all_path_related_oids = $remote_index->find_path_descendants($path); + $subpath = $path; + do { + $subpath = dirname($subpath); + $remote_index->read_by_path($subpath); + $all_path_related_oids[] = $remote_index->get_oid(); + } while($subpath !== '/'); + $all_path_related_oids[] = $remote_head; + $all_path_related_oids = array_flip($all_path_related_oids); + + // @TODO: Support "want" and "have" here + $new_oids = $remote_index->find_objects_added_in($remote_head, $local_ref, [ + 'old_tree_index' => $local_index, + ]); + $objects_to_fetch = []; + foreach($new_oids as $oid) { + if(!isset($all_path_related_oids[$oid])) { + continue; + } + $objects_to_fetch[] = $oid; + } + $this->fetchObjects($objects_to_fetch); + $this->index->set_ref_head('refs/heads/' . $branch_name, $remote_head); + } + public function fetchObjects($refs) { $body = ''; foreach($refs as $ref) { @@ -152,21 +208,8 @@ public function fetchObjects($refs) { 'Content-Type: application/x-git-upload-pack-request', ]); $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); - return WP_Git_Pack_Index::from_pack_data($pack_data); - } - - public function backfillBlobs($index, $root = '/') { - $sub_root = $index->get_by_path($root); - - $blobs_shas = []; - foreach($index->get_descendants($sub_root['oid']) as $blob) { - $blobs_shas[] = $blob['sha1']; - } - $blobs_index = $this->fetchObjects($blobs_shas); - $index->set_external_get_by_oid(function($oid) use ($blobs_index) { - return $blobs_index->get_by_oid($oid); - }); - return $index; + WP_Git_Pack_Processor::decode($pack_data, $this->index); + return true; } public function get_last_error() { diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index 246fa9dec4..264da853af 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -70,7 +70,7 @@ public function is_file($path) { ); } - public function start_streaming_file($path) { + public function open_file_stream($path) { throw new Exception('Not implemented'); } @@ -86,7 +86,11 @@ public function get_error_message() { throw new Exception('Not implemented'); } - public function close_file_reader() { + public function close_file_stream() { + throw new Exception('Not implemented'); + } + + public function get_streamed_file_length() { throw new Exception('Not implemented'); } diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php deleted file mode 100644 index 94099ff684..0000000000 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Index.php +++ /dev/null @@ -1,248 +0,0 @@ -objects = $objects; - if($by_oid === []) { - $by_oid = []; - foreach($objects as $k => $object) { - $by_oid[$object['oid']] = $k; - } - } - $this->by_oid = $by_oid; - } - - public function set_external_get_by_oid($external_get_by_oid) { - $this->external_get_by_oid = $external_get_by_oid; - } - - public function get_by_oid($oid) { - if(isset($this->by_oid[$oid])) { - return $this->objects[$this->by_oid[$oid]]; - } - if($this->external_get_by_oid) { - $factory = $this->external_get_by_oid; - return $factory($oid); - } - return null; - } - - public function get_by_path($path, $root_tree_oid=null) { - if($root_tree_oid === null) { - foreach($this->objects as $object) { - if($object['type'] === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { - $root_tree_oid = $object['tree']; - break; - } - } - } - $current_tree = $this->get_by_oid($root_tree_oid); - if (!$current_tree) { - return null; - } - - if($current_tree['type'] === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { - $current_tree = $this->get_by_oid($current_tree['tree']); - } - - $path = trim($path, '/'); - if (empty($path)) { - return $current_tree; - } - - $path_segments = explode('/', $path); - foreach ($path_segments as $segment) { - if (!isset($current_tree['content'][$segment])) { - return null; - } - $next_oid = $current_tree['content'][$segment]['sha1']; - $current_tree = $this->get_by_oid($next_oid); - if (!$current_tree) { - return null; - } - } - - return $current_tree; - } - - public function get_descendants($tree_oid) { - $tree = $this->get_by_oid($tree_oid); - if (!$tree || !isset($tree['content'])) { - return []; - } - foreach ($tree['content'] as $name => $object) { - if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { - yield from $this->get_descendants($object['sha1']); - } else { - yield $object; - } - } - } - - public function get_descendants_tree($tree_oid) { - $tree = $this->get_by_oid($tree_oid); - if (!$tree || !isset($tree['content'])) { - return []; - } - - $descendants = []; - foreach ($tree['content'] as $name => $object) { - if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { - $descendants[$name] = $this->get_descendants_tree($object['sha1']); - } else { - $blob = $this->get_by_oid($object['sha1']); - $descendants[$name] = isset($blob['content']) ? $blob['content'] : null; - } - } - - return $descendants; - } - - private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; - - /** - * Computes Git objects needed to commit a changeset. - * - * Important! A remote git repo will only accept the objects - * produced by this method if: - * - * * The paths in each tree are sorted alphabetically. - * * There may be no duplicate blobs. - * - * @param WP_Git_Pack_Processor $oldIndex The index containing existing objects - * @param WP_Changeset $changeset The changes to commit - * @return string The Git objects with type, content and SHA - */ - public function derive_commit_pack_data( - $updates = [], - $deletes = [] - ) { - $new_index = []; - - $new_tree = new stdClass(); - foreach ($updates as $path => $content) { - $new_blob = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, - 'content' => $content, - ]); - $new_index[] = $new_blob; - $this->set_oid($new_tree, $path, $new_blob['oid']); - } - - foreach ($deletes as $path) { - $this->set_oid($new_tree, $path, self::DELETE_PLACEHOLDER); - } - - $root_tree = $this->backfill_trees($this, $new_index, $new_tree, '/'); - - // Make $new_index unique by 'oid' column - $seen_oids = []; - $new_index = array_filter($new_index, function($obj) use (&$seen_oids) { - if (isset($seen_oids[$obj['oid']])) { - return false; - } - $seen_oids[$obj['oid']] = true; - return true; - }); - - return [ - 'objects' => $new_index, - 'root_tree_oid' => $root_tree['oid'], - ]; - } - - private function backfill_trees(WP_Git_Pack_Index $current_index, &$new_index, $subtree_delta, $subtree_path = '/') { - $subtree_path = ltrim($subtree_path, '/'); - $new_tree_content = []; - - $indexed_tree = $current_index->get_by_path($subtree_path); - if($indexed_tree) { - foreach($indexed_tree['content'] as $object) { - // Backfill the unchanged objects from the currently indexed subtree. - $name = $object['name']; - if(!isset($subtree_delta->children[$name])) { - $new_tree_content[$name] = $object; - } - } - } - - // Index changed and new objects in the current subtree. - foreach($subtree_delta->children as $name => $subtree_child) { - // Ignore any deleted objects. - if(isset($subtree_child->oid) && $subtree_child->oid === self::DELETE_PLACEHOLDER) { - continue; - } - - // Index blobs - switch($subtree_child->type) { - case WP_Git_Pack_Processor::OBJECT_TYPE_BLOB: - $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, - 'name' => $name, - 'sha1' => $subtree_child->oid, - ]; - break; - case WP_Git_Pack_Processor::OBJECT_TYPE_TREE: - $subtree_object = $this->backfill_trees($current_index, $new_index, $subtree_child, $subtree_path . '/' . $name); - $new_tree_content[$name] = [ - 'mode' => WP_Git_Pack_Processor::FILE_MODE_DIRECTORY, - 'name' => $name, - 'sha1' => $subtree_object['oid'], - ]; - break; - } - } - - $new_tree_object = WP_Git_Pack_Processor::create_object([ - 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_TREE, - 'content' => $new_tree_content, - ]); - - $new_index[] = $new_tree_object; - return $new_tree_object; - } - - private function set_oid($root_tree, $path, $oid) { - $blob = new stdClass(); - $blob->type = WP_Git_Pack_Processor::OBJECT_TYPE_BLOB; - $blob->oid = $oid; - - $subtree_path = dirname($path); - if($subtree_path === '.') { - $subtree = $root_tree; - } else { - $subtree = $this->get_subtree($root_tree, $subtree_path); - } - $filename = basename($path); - $subtree->children[$filename] = $blob; - } - - private function get_subtree($root_tree, $path) { - $path = trim($path, '/'); - $segments = explode('/', $path); - $subtree = $root_tree; - foreach ($segments as $segment) { - if (!isset($subtree->children[$segment])) { - $new_subtree = new stdClass(); - $new_subtree->type = WP_Git_Pack_Processor::OBJECT_TYPE_TREE; - $new_subtree->children = []; - $subtree->children[$segment] = $new_subtree; - } - $subtree = $subtree->children[$segment]; - } - return $subtree; - } - -} \ No newline at end of file diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 5d61512f3b..e38d81fab5 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -1,5 +1,7 @@ $object) { - if( $object['type'] === self::OBJECT_TYPE_TREE ) { - $objects[$k]['content'] = self::parse_tree_bytes($object['content']); - } else if($object['type'] === self::OBJECT_TYPE_COMMIT) { - $objects[$k]['tree'] = substr($object['content'], 5, 40); - } + foreach($objects as $object) { + $pack_index->add_object($object['type'], $object['content']); } - return new WP_Git_Pack_Index( - $objects, - $by_oid - ); + return $pack_index; } static private function wrap_git_object($type, $object) { From d56adb5fd87ecd15ff3aec9663ea2ee3fbfc1e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 30 Dec 2024 23:31:16 +0100 Subject: [PATCH 30/71] Rename Git_Cached_Index to Git_Repository ane enable remote management --- .../src/WP_Blocks_To_Markdown.php | 12 ++++- .../playground/data-liberation/bootstrap.php | 2 +- .../data-liberation/src/git/WP_Git_Client.php | 29 ++++++------ .../src/git/WP_Git_Pack_Processor.php | 2 +- ...Cached_Index.php => WP_Git_Repository.php} | 44 ++++++++++++++++++- 5 files changed, 70 insertions(+), 19 deletions(-) rename packages/playground/data-liberation/src/git/{WP_Git_Cached_Index.php => WP_Git_Repository.php} (93%) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index cbcd59a802..80457245d2 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -217,7 +217,17 @@ private function block_to_markdown($block) { return "\n---\n\n"; default: - return ''; + $markdown = []; + if($inner_html){ + $markdown[] = "```block"; + $markdown[] = ""; + $markdown[] = $inner_html; + $markdown[] = ""; + $markdown[] = "```"; + } else { + $markdown[] = ""; + } + return implode("\n", $markdown); } } diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 2584b924cb..9059423e52 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -86,7 +86,7 @@ require_once __DIR__ . '/src/git/WP_Git_Client.php'; require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; -require_once __DIR__ . '/src/git/WP_Git_Cached_Index.php'; +require_once __DIR__ . '/src/git/WP_Git_Repository.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index ab956c5735..9a25dcd9bd 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -5,31 +5,25 @@ use WordPress\ByteReader\WP_String_Reader; class WP_Git_Client { - private $repoUrl; - private $author; - private $committer; /** * @var Client */ private $http_client; /** - * @var WP_Git_Cached_Index + * @var WP_Git_Repository */ private $index; private $remote_name = 'origin'; - public function __construct(WP_Git_Cached_Index $index, $repoUrl, $options = []) { - $this->repoUrl = rtrim($repoUrl, '/'); - $this->author = $options['author'] ?? "John Doe "; - $this->committer = $options['committer'] ?? "John Doe "; + public function __construct(WP_Git_Repository $index, $options = []) { + $this->remote_name = $options['remote_name'] ?? 'origin'; $this->http_client = $options['http_client'] ?? new Client(); $this->index = $index; } public function fetchRefs($prefix) { - $url = $this->repoUrl . '/git-upload-pack'; $response = $this->http_request( - $url, + '/git-upload-pack', $this->encode_packet_line("command=ls-refs\n") . $this->encode_packet_line("agent=git/2.37.3\n") . $this->encode_packet_line("object-format=sha1\n") . @@ -111,8 +105,7 @@ public function force_push_one_commit() { $push_packet .= WP_Git_Pack_Processor::encode($pack_objects); $push_packet .= "0000"; - $url = rtrim($this->repoUrl, '.git').'.git/git-receive-pack'; - $response = $this->http_request($url, $push_packet, [ + $response = $this->http_request('/git-receive-pack', $push_packet, [ 'Content-Type' => 'application/x-git-receive-pack-request', 'Accept' => 'application/x-git-receive-pack-result', ]); @@ -148,7 +141,7 @@ public function list_objects($ref_hash) { $this->encode_packet_line("done\n") . $this->encode_packet_line("done\n"); - $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ + $response = $this->http_request('/git-upload-pack', $body, [ 'Accept: application/x-git-upload-pack-advertisement', 'Content-Type: application/x-git-upload-pack-request', ]); @@ -203,7 +196,7 @@ public function fetchObjects($refs) { $body .= "0000"; $body .= $this->encode_packet_line("done\n"); - $response = $this->http_request($this->repoUrl . '/git-upload-pack', $body, [ + $response = $this->http_request('/git-upload-pack', $body, [ 'Accept: application/x-git-upload-pack-advertisement', 'Content-Type: application/x-git-upload-pack-request', ]); @@ -220,7 +213,13 @@ public function get_last_error() { return $last_request->error; } - private function http_request($url, $postData = null, $headers = []) { + private function http_request($path, $postData = null, $headers = []) { + $remote = $this->index->get_remote($this->remote_name); + if(!$remote) { + $this->last_error = 'Remote "' . $this->remote_name . '" not found'; + return false; + } + $url = $remote['url'] . $path; $request_info = []; if($postData) { $request_info['headers'] = $headers; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index e38d81fab5..5c152ddc2f 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -163,7 +163,7 @@ static public function encode_packet_line(string $payload): string { */ static public function decode($pack_bytes, $pack_index=null) { if(null === $pack_index) { - $pack_index = new WP_Git_Cached_Index(new WP_In_Memory_Filesystem()); + $pack_index = new WP_Git_Repository(new WP_In_Memory_Filesystem()); } $parsed_pack = self::parse_pack_data($pack_bytes); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php similarity index 93% rename from packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php rename to packages/playground/data-liberation/src/git/WP_Git_Repository.php index f7ed67289b..e040f388f6 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Cached_Index.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -2,7 +2,7 @@ use WordPress\Filesystem\WP_Abstract_Filesystem; -class WP_Git_Cached_Index { +class WP_Git_Repository { private $fs; @@ -15,6 +15,7 @@ class WP_Git_Cached_Index { private $parsed_commit; private $parsed_tree; private $last_error; + private $parsed_config; private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; @@ -39,6 +40,47 @@ private function initialize_filesystem() { } } + public function add_remote($name, $url) { + $this->set_config_section('remote "' . $name . '"', [ + 'url' => $url, + 'fetch' => '+refs/heads/*:refs/remotes/' . $name . '/*', + ]); + } + + public function get_remote($name) { + $this->parse_config(); + $key = 'remote "' . $name . '"'; + return $this->parsed_config[$key] ?? null; + } + + private function set_config_section($section, $key_value_pairs) { + $this->parse_config(); + $this->parsed_config[$section] = $key_value_pairs; + $this->write_config(); + } + + private function parse_config() { + if(!$this->parsed_config) { + if(!$this->fs->is_file('config')) { + $this->parsed_config = []; + return; + } + $this->parsed_config = parse_ini_string($this->fs->read_file('config'), true, INI_SCANNER_RAW); + } + } + + private function write_config() { + $this->parse_config(); + $lines = []; + foreach($this->parsed_config as $section => $key_value_pairs) { + $lines[] = "[{$section}]"; + foreach($key_value_pairs as $key => $value) { + $lines[] = " {$key} = {$value}"; + } + } + $this->fs->put_contents('config', implode("\n", $lines)); + } + public function read_object($oid) { $this->reset(); From 724e17797377382f7170ab2fb5d9b1be61beb244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 00:05:13 +0100 Subject: [PATCH 31/71] Support user.name and user.email git settings and auto push mode in the filesystem --- .../plugin.php | 18 +- .../src/git/WP_Git_Filesystem.php | 186 +++++++----------- .../src/git/WP_Git_Repository.php | 45 ++++- 3 files changed, 117 insertions(+), 132 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 4742d9ef99..a1f85a242f 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -45,13 +45,19 @@ class WP_Static_Files_Editor_Plugin { static private function get_fs() { if(!self::$fs) { + $dot_git_path = WP_CONTENT_DIR . '/.static-pages.git'; + $local_fs = new WP_Local_Filesystem($dot_git_path); + $repo = new WP_Git_Repository($local_fs); + $repo->add_remote('origin', GIT_REPO_URL); + $repo->set_ref_head('HEAD', 'refs/heads/' . GIT_BRANCH); + $repo->set_config_value('user.name', GIT_USER_NAME); + $repo->set_config_value('user.email', GIT_USER_EMAIL); self::$fs = new WP_Git_Filesystem( - new WP_Git_Client(GIT_REPO_URL, [ - 'author' => GIT_AUTHOR, - 'committer' => GIT_COMMITTER, - ]), - GIT_BRANCH, - GIT_DIRECTORY_ROOT + $repo, + [ + 'root' => GIT_DIRECTORY_ROOT, + 'auto_push' => true, + ] ); } return self::$fs; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index 264da853af..9ebd4dfdf8 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -3,71 +3,53 @@ use WordPress\Filesystem\WP_Abstract_Filesystem; class WP_Git_Filesystem extends WP_Abstract_Filesystem { - private $client; + /** + * @var WP_Git_Repository + */ + private $repo; + /** + * @var string + */ private $root; - - private $branch_name; - private $head_hash; - private $index; - private $blobs_backfilled = false; + private $auto_push; + private $client; public function __construct( - WP_Git_Client $client, - $branch_name = 'main', - $root = '/' + WP_Git_Repository $repo, + $options = [] ) { - $this->client = $client; - $this->root = $root; - $this->branch_name = $branch_name; + $this->repo = $repo; + $this->root = $options['root'] ?? '/'; + $this->auto_push = $options['auto_push'] ?? false; + $this->client = $options['client'] ?? new WP_Git_Client($repo); } public function ls($parent = '/') { - $parent = $this->resolve_path($parent); - $tree = $this->get_index()->get_by_path($parent); + $path = $this->resolve_path($parent); + if(false === $this->repo->read_by_path($path)) { + return false; + } + $tree = $this->repo->get_parsed_tree(); if(!$tree) { - return []; + return false; } - return array_keys($tree['content']); + return array_keys($tree); } public function is_dir($path) { $path = $this->resolve_path($path); - // We may not have the blob object yet, but we surely have the parent - // tree object. Instead of resolving the blob by its path, let's check - // if the requested file is in the parent tree. - $object = $this->get_index()->get_by_path(dirname($path)); - if(!$object || !isset($object['type']) || $object['type'] !== WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { + if(false === $this->repo->read_by_path($path)) { return false; } - if(!isset($object['content'][basename($path)])) { - return false; - } - $blob = $object['content'][basename($path)]; - - return ( - isset($blob['mode']) && - $blob['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY - ); + return WP_Git_Pack_Processor::OBJECT_TYPE_TREE === $this->repo->get_type(); } public function is_file($path) { $path = $this->resolve_path($path); - // We may not have the blob object yet, but we surely have the parent - // tree object. Instead of resolving the blob by its path, let's check - // if the requested file is in the parent tree. - $object = $this->get_index()->get_by_path(dirname($path)); - if(!$object || !isset($object['type']) || $object['type'] !== WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { + if(false === $this->repo->read_by_path($path)) { return false; } - if(!isset($object['content'][basename($path)])) { - return false; - } - $blob = $object['content'][basename($path)]; - - return ( - isset($blob['mode']) && - $blob['mode'] === WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE - ); + return WP_Git_Pack_Processor::OBJECT_TYPE_BLOB === $this->repo->get_type(); } public function open_file_stream($path) { @@ -98,62 +80,42 @@ public function read_file($path) { if(!$this->is_file($path)) { return false; } - $this->ensure_files_data(); $path = $this->resolve_path($path); - $object = $this->get_index()->get_by_path($path); - if(!$object) { - return false; - } - return $object['content']; + $this->repo->read_by_path($path); + return $this->repo->read_entire_object_contents(); } private function resolve_path($path) { return wp_join_paths($this->root, $path); } - private function ensure_files_data() { - if(!$this->blobs_backfilled) { - $this->client->backfillBlobs( - $this->get_index(), - $this->root - ); - $this->blobs_backfilled = true; - } - } - - private function get_index() { - if(!$this->head_hash) { - $key = 'refs/heads/' . $this->branch_name; - $refs = $this->client->fetchRefs($key); - if(!isset($refs[$key])) { - throw new Exception($key . ' ref not found'); - } - $this->head_hash = $refs[$key]; - } - if(!$this->index) { - $this->index = $this->client->list_objects($this->head_hash); - } - return $this->index; - } - // These methods are not a part of the interface, but they are useful // for dealing with a local filesystem. public function rename($old_path, $new_path) { - if($this->is_dir($old_path)) { - throw new Exception('Renaming directories is not supported yet'); - } - if(!$this->is_file($old_path)) { + if($this->is_file($old_path)) { + return $this->commit( + [ + 'updates' => [ + $this->resolve_path($new_path) => $this->read_file($old_path), + ], + 'deletes' => [ + $this->resolve_path($old_path), + ], + ] + ); + } else if($this->is_dir($old_path)) { + return $this->commit( + [ + 'renames' => [ + $this->resolve_path($old_path) => $this->resolve_path($new_path), + ], + ] + ); + } else { + _doing_it_wrong(__METHOD__, 'Cannot rename a non-existent file or directory ' . $old_path, '1.0.0'); return false; } - return $this->commit_and_push( - [ - $this->get_full_path($new_path) => $this->read_file($old_path), - ], - [ - $this->get_full_path($old_path), - ] - ); } public function mkdir($path) { @@ -165,10 +127,11 @@ public function rm($path) { if($this->is_dir($path)) { return false; } - return $this->commit_and_push( - [], + return $this->commit( [ - $this->get_full_path($path) + 'deletes' => [ + $this->resolve_path($path) + ], ] ); } @@ -183,47 +146,34 @@ public function rmdir($path, $options = []) { return false; } - return $this->commit_and_push( - [], + return $this->commit( [ - $this->get_full_path($path) + 'deletes' => [ + $this->resolve_path($path) + ], ] ); } public function put_contents($path, $data) { - return $this->commit_and_push( + return $this->commit( [ - $this->get_full_path($path) => $data, + 'updates' => [ + $this->resolve_path($path) => $data, + ], ] ); } - private function get_full_path($relative_path) { - return ltrim(wp_join_paths($this->root, $relative_path), '/'); - } - - private function commit_and_push($updates=[], $deletes=[]) { - $commit_data = $this->get_index()->derive_commit_pack_data( - $updates, - $deletes, - ); - - $result = $this->client->push($commit_data['objects'], [ - 'branch_name' => $this->branch_name, - 'parent_hash' => $this->head_hash, - 'tree_hash' => $commit_data['root_tree_oid'], - ]); - - if(!$result) { - // @TODO: Error handling + private function commit($changeset, $commit_meta=[]) { + if(false === $this->repo->commit($changeset, $commit_meta)) { return false; } - - $this->head_hash = $result['new_head_hash']; - // Reset the index so it's refetched the next time we need it. - $this->index = null; - + if($this->auto_push) { + if(false === $this->client->force_push_one_commit()) { + return false; + } + } return true; } diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index e040f388f6..2856dc8dba 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -41,10 +41,9 @@ private function initialize_filesystem() { } public function add_remote($name, $url) { - $this->set_config_section('remote "' . $name . '"', [ - 'url' => $url, - 'fetch' => '+refs/heads/*:refs/remotes/' . $name . '/*', - ]); + $this->set_config_value(['remote', $name, 'url'], $url); + // @TODO: support fetch option + // $this->set_config_value(['remote', $name, 'fetch'], '+refs/heads/*:refs/remotes/' . $name . '/*'); } public function get_remote($name) { @@ -53,12 +52,38 @@ public function get_remote($name) { return $this->parsed_config[$key] ?? null; } - private function set_config_section($section, $key_value_pairs) { + public function set_config_value($key, $value) { $this->parse_config(); - $this->parsed_config[$section] = $key_value_pairs; + list($section, $key) = $this->parse_config_key($key); + + if(!isset($this->parsed_config[$section])) { + $this->parsed_config[$section] = []; + } + $this->parsed_config[$section][$key] = $value; $this->write_config(); } + public function get_config_value($key) { + $this->parse_config(); + list($section, $key) = $this->parse_config_key($key); + return $this->parsed_config[$section][$key] ?? null; + } + + private function parse_config_key($key) { + if(is_string($key)) { + $key = explode('.', $key); + } + $section_name = array_shift($key); + $trailing_key = array_pop($key); + $section_subkey = implode('.', $key); + + $section = $section_name; + if($section_subkey) { + $section .= ' "' . $section_subkey . '"'; + } + return [$section, $trailing_key]; + } + private function parse_config() { if(!$this->parsed_config) { if(!$this->fs->is_file('config')) { @@ -469,8 +494,12 @@ static private function wrap_git_object($type, $object) { } public function commit($changeset, $commit_meta=[]) { - $commit_meta['author'] = $commit_meta['author'] ?? 'John Doe '; - $commit_meta['committer'] = $commit_meta['committer'] ?? 'John Doe '; + if(!isset($commit_meta['author'])) { + $commit_meta['author'] = $this->get_config_value('user.name') . ' <' . $this->get_config_value('user.email') . '>'; + } + if(!isset($commit_meta['committer'])) { + $commit_meta['committer'] = $this->get_config_value('user.name') . ' <' . $this->get_config_value('user.email') . '>'; + } $commit_meta['message'] = $commit_meta['message'] ?? 'Changes'; // First process all blob updates From c07add9fa176640b09d608fbb926167f963267f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 00:35:46 +0100 Subject: [PATCH 32/71] Use serialize_blocks to store custom blocks in markdown --- .../src/WP_Blocks_To_Markdown.php | 16 +++++++--------- .../src/WP_Markdown_To_Blocks.php | 9 +++++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 80457245d2..72003c43c3 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -217,16 +217,14 @@ private function block_to_markdown($block) { return "\n---\n\n"; default: - $markdown = []; - if($inner_html){ - $markdown[] = "```block"; - $markdown[] = ""; - $markdown[] = $inner_html; - $markdown[] = ""; - $markdown[] = "```"; - } else { - $markdown[] = ""; + // Short-circuit empty entries produced by the block parser. + if(!$block_name) { + return ''; } + $markdown = []; + $markdown[] = "```block"; + $markdown[] = serialize_block($block); + $markdown[] = "```"; return implode("\n", $markdown); } } diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index bf1bca49ce..12076fe82a 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -165,8 +165,13 @@ private function convert_markdown_to_blocks() { if ( method_exists( $node, 'getInfo' ) && $node->getInfo() ) { $attrs['language'] = preg_replace( '/[ \t\r\n\f].*/', '', $node->getInfo() ); } - $this->push_block( 'code', $attrs ); - $this->append_content( '
' . trim( str_replace( "\n", '
', htmlspecialchars( $node->getLiteral() ) ) ) . '
' ); + if('block' === $attrs['language']) { + // This is a special case for preserving block literals that could not be expressed as markdown. + $this->append_content( "\n" . $node->getLiteral() . "\n" ); + } else { + $this->push_block( 'code', $attrs ); + $this->append_content( '
' . trim( str_replace( "\n", '
', htmlspecialchars( $node->getLiteral() ) ) ) . '
' ); + } break; case ExtensionBlock\HtmlBlock::class: From decd81e618ff7e8a5d15746773a06b03d19500f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 00:36:21 +0100 Subject: [PATCH 33/71] Resolve minor papercuts to enable using the plugin with git --- .../blueprint.json | 4 ---- .../plugin.php | 16 ++++++++++++++-- .../data-liberation-static-files-editor/run.sh | 1 - .../data-liberation/src/git/WP_Git_Client.php | 6 +++++- .../src/git/WP_Git_Repository.php | 5 +---- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/blueprint.json b/packages/playground/data-liberation-static-files-editor/blueprint.json index b05a2498f3..098ae0160f 100644 --- a/packages/playground/data-liberation-static-files-editor/blueprint.json +++ b/packages/playground/data-liberation-static-files-editor/blueprint.json @@ -20,10 +20,6 @@ "step": "activatePlugin", "pluginPath": "z-data-liberation-static-files-editor/plugin.php" }, - { - "step": "activatePlugin", - "pluginPath": "gutenberg/gutenberg.php" - }, { "step": "runPHP", "code": " 'My Notes', 'post_status' => 'publish', 'post_type' => 'page'));" diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index a1f85a242f..e1c45bec3f 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -46,17 +46,27 @@ class WP_Static_Files_Editor_Plugin { static private function get_fs() { if(!self::$fs) { $dot_git_path = WP_CONTENT_DIR . '/.static-pages.git'; + if(!is_dir($dot_git_path)) { + mkdir($dot_git_path, 0777, true); + } $local_fs = new WP_Local_Filesystem($dot_git_path); $repo = new WP_Git_Repository($local_fs); $repo->add_remote('origin', GIT_REPO_URL); $repo->set_ref_head('HEAD', 'refs/heads/' . GIT_BRANCH); $repo->set_config_value('user.name', GIT_USER_NAME); $repo->set_config_value('user.email', GIT_USER_EMAIL); + + $client = new WP_Git_Client($repo); + if(false === $client->force_pull()) { + _doing_it_wrong(__METHOD__, 'Failed to pull from remote repository', '1.0.0'); + } + self::$fs = new WP_Git_Filesystem( $repo, [ 'root' => GIT_DIRECTORY_ROOT, 'auto_push' => true, + 'client' => $client, ] ); } @@ -68,6 +78,7 @@ static public function initialize() { register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); add_action('init', function() { + self::get_fs(); self::register_post_type(); }); @@ -86,7 +97,7 @@ static public function initialize() { $post_id = wp_insert_post(array( 'post_title' => 'My first note', 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'post_status' => 'draft' + 'post_status' => 'publish' )); } else { $post_id = $posts[0]->ID; @@ -440,7 +451,8 @@ static public function import_static_pages() { $importer = WP_Stream_Importer::create( function () { return new WP_Filesystem_Entity_Reader( - new WP_Local_Filesystem(WP_STATIC_CONTENT_DIR), + self::get_fs(), + // new WP_Local_Filesystem(WP_STATIC_CONTENT_DIR) array( 'post_type' => WP_LOCAL_FILE_POST_TYPE, ) diff --git a/packages/playground/data-liberation-static-files-editor/run.sh b/packages/playground/data-liberation-static-files-editor/run.sh index d90ec0835c..d78b543d41 100644 --- a/packages/playground/data-liberation-static-files-editor/run.sh +++ b/packages/playground/data-liberation-static-files-editor/run.sh @@ -8,6 +8,5 @@ bun --inspect ../cli/src/cli.ts \ --mount=../data-liberation-static-files-editor:/wordpress/wp-content/plugins/z-data-liberation-static-files-editor \ --mount=../data-liberation-markdown:/wordpress/wp-content/plugins/z-data-liberation-markdown \ --mount=../data-liberation:/wordpress/wp-content/plugins/data-liberation \ - --mount=../../../../gutenberg:/wordpress/wp-content/plugins/gutenberg \ --mount=./my-notes/workdir:/wordpress/wp-content/uploads/static-pages \ --blueprint=./blueprint.json diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 9a25dcd9bd..a5a5c9c114 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -150,7 +150,11 @@ public function list_objects($ref_hash) { return WP_Git_Pack_Processor::decode($pack_data); } - public function force_pull($branch_name, $path = '/') { + public function force_pull($branch_name=null, $path = '/') { + if(!$branch_name) { + $branch_name = $this->index->get_ref_head('HEAD', ['resolve_ref' => false]); + $branch_name = $this->localize_ref_name($branch_name); + } $path = '/' . ltrim($path, '/'); $remote_refs = $this->fetchRefs('refs/heads/' . $branch_name); $remote_head = $remote_refs['refs/heads/' . $branch_name]; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 2856dc8dba..36247484b9 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -414,10 +414,7 @@ public function get_ref_head($ref='HEAD', $options=[]) { } $contents = trim($this->fs->read_file($path)); if($options['resolve_ref'] ?? true) { - if(strpos($contents, 'ref: ') === 0) { - $branch = trim(substr($contents, 5)); - return $this->get_ref_head($branch, $options); - } + return $this->get_ref_head($contents, $options); } return $contents; } From 96f52a39243a9971865f7e7f7eee0e8e7d669b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 01:35:54 +0100 Subject: [PATCH 34/71] Store auto-saves on the disk --- .../plugin.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index e1c45bec3f..b5c2cda68a 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -261,15 +261,23 @@ static public function initialize() { add_action('save_post_' . WP_LOCAL_FILE_POST_TYPE, function($post_id, $post, $update) { self::save_post_data_to_local_file($post); }, 10, 3); + + // Also update file when autosave occurs + add_action('wp_creating_autosave', function($autosave) { + $autosave = (object)$autosave; + if ($autosave->post_type !== 'revision') { + return; + } + $parent_post = get_post($autosave->post_parent); + if ($parent_post->post_type !== WP_LOCAL_FILE_POST_TYPE) { + return; + } + self::save_post_data_to_local_file($autosave); + }, 10, 1); } static private $synchronizing = false; static private function acquire_synchronization_lock() { - // Ignore auto-saves or revisions - if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { - // return false; - } - // Skip if in maintenance mode if (wp_is_maintenance_mode()) { return false; @@ -353,22 +361,23 @@ static private function save_post_data_to_local_file($post) { if(!self::acquire_synchronization_lock()) { return; } - $post_id = $post->ID; if ( empty($post->ID) || - $post->post_status !== 'publish' || - $post->post_type !== WP_LOCAL_FILE_POST_TYPE + ($post->post_status !== 'publish' && $post->post_status !== 'inherit') || + ($post->post_type !== WP_LOCAL_FILE_POST_TYPE && $post->post_type !== 'revision') ) { return; } + $root_post = $post->post_type === 'revision' ? get_post($post->post_parent) : $post; + $fs = self::get_fs(); - $path = get_post_meta($post_id, 'local_file_path', true); + $path = get_post_meta($root_post->ID, 'local_file_path', true); $extension = pathinfo($path, PATHINFO_EXTENSION); $metadata = []; foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { - $metadata[$key] = get_post_field($key, $post_id); + $metadata[$key] = get_post_field($key, $root_post->ID); } // @TODO: Also include actual post_meta. Which ones? All? The // ones explicitly set by the user in the editor? @@ -388,7 +397,6 @@ static private function save_post_data_to_local_file($post) { } finally { self::release_synchronization_lock(); } - } static public function get_local_files_tree($subdirectory = '') { From 270737dba578ce98c9b1b9e0ed842c4169516332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 01:58:44 +0100 Subject: [PATCH 35/71] Improve Blocks/Markdown conversion --- .../src/WP_Blocks_To_Markdown.php | 14 +++++++++++++- .../src/WP_Markdown_To_Blocks.php | 1 + .../data-liberation-static-files-editor/plugin.php | 8 ++++++++ .../data-liberation/src/git/WP_Git_Filesystem.php | 9 +++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 72003c43c3..9a73e4c86a 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -78,7 +78,19 @@ private function block_to_markdown($block) { return "![" . ($attributes['alt'] ?? '') . "](" . ($attributes['url'] ?? '') . ")\n\n"; case 'core/heading': - $level = $attributes['level'] ?? 1; + $level = $attributes['level'] ?? null; + if(null === $level) { + $processor = WP_Data_Liberation_HTML_Processor::create_fragment($inner_html); + if($processor->next_tag()) { + $tag = $processor->get_tag(); + if(strlen($tag) > 1 && is_numeric($tag[1])) { + $level = (int)$tag[1]; + } + } + if(null === $level) { + $level = 1; + } + } $content = $this->html_to_markdown($inner_html); return str_repeat('#', $level) . ' ' . $content . "\n\n"; diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index 12076fe82a..29efe4cca4 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -181,6 +181,7 @@ private function convert_markdown_to_blocks() { case ExtensionBlock\ThematicBreak::class: $this->push_block( 'separator' ); + $this->append_content( '
' ); break; case Block\Paragraph::class: diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index b5c2cda68a..d5c6b8e74c 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -393,6 +393,14 @@ static private function save_post_data_to_local_file($post) { break; } $converter->convert(); + + if(method_exists($fs, 'set_next_commit_message')) { + if($post->post_type === 'revision') { + $fs->set_next_commit_message('Autosave of ' . $root_post->post_title); + } else { + $fs->set_next_commit_message('User saved ' . $post->post_title); + } + } $fs->put_contents($path, $converter->get_result()); } finally { self::release_synchronization_lock(); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index 9ebd4dfdf8..a10b28ffb9 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -13,6 +13,7 @@ class WP_Git_Filesystem extends WP_Abstract_Filesystem { private $root; private $auto_push; private $client; + private $next_commit_message; public function __construct( WP_Git_Repository $repo, @@ -165,7 +166,15 @@ public function put_contents($path, $data) { ); } + public function set_next_commit_message($message) { + $this->next_commit_message = $message; + } + private function commit($changeset, $commit_meta=[]) { + if($this->next_commit_message) { + $commit_meta['message'] = $this->next_commit_message; + $this->next_commit_message = null; + } if(false === $this->repo->commit($changeset, $commit_meta)) { return false; } From 7bef2bc4f6d1204cdb1f331c38454a984b5875dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 16:11:36 +0100 Subject: [PATCH 36/71] Implement reparent, squash, amend --- .../src/git/WP_Git_Pack_Processor.php | 15 +- .../src/git/WP_Git_Repository.php | 158 +++++++++++++++--- 2 files changed, 149 insertions(+), 24 deletions(-) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 5c152ddc2f..404b300a84 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -267,7 +267,7 @@ static private function applyDelta($base_bytes, $delta_bytes) { return $result; } - static public function parse_commit_message($commit_message) { + static public function parse_commit_body($commit_message) { $lines = explode("\n", $commit_message); $parsed = []; foreach($lines as $k => $line) { @@ -278,7 +278,18 @@ static public function parse_commit_message($commit_message) { $type_len = strpos($line, ' '); $type = substr($line, 0, $type_len); $value = substr($line, $type_len + 1); - $parsed[$type] = $value; + + if($type === 'author') { + $author_date_starts = strpos($value, '>') + 1; + $parsed['author'] = substr($value, 0, $author_date_starts); + $parsed['author_date'] = substr($value, $author_date_starts + 1); + } else if($type === 'committer') { + $committer_date_starts = strpos($value, '>') + 1; + $parsed['committer'] = substr($value, 0, $committer_date_starts); + $parsed['committer_date'] = substr($value, $committer_date_starts + 1); + } else { + $parsed[$type] = $value; + } } return $parsed; } diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 36247484b9..ee1c29e857 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -18,6 +18,7 @@ class WP_Git_Repository { private $parsed_config; private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; + private const NULL_OID = '0000000000000000000000000000000000000000'; public function __construct( WP_Abstract_Filesystem $fs @@ -213,8 +214,8 @@ private function close_object_stream() { public function get_parsed_commit() { if(null === $this->parsed_commit && $this->oid) { - $commit_contents = $this->read_entire_object_contents(); - $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_message($commit_contents); + $commit_body = $this->read_entire_object_contents(); + $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_body($commit_body); if(!$this->parsed_commit) { $this->last_error = 'Failed to parse commit'; $this->parsed_commit = []; @@ -490,19 +491,11 @@ static private function wrap_git_object($type, $object) { return "$type_name $length\x00" . $object; } - public function commit($changeset, $commit_meta=[]) { - if(!isset($commit_meta['author'])) { - $commit_meta['author'] = $this->get_config_value('user.name') . ' <' . $this->get_config_value('user.email') . '>'; - } - if(!isset($commit_meta['committer'])) { - $commit_meta['committer'] = $this->get_config_value('user.name') . ' <' . $this->get_config_value('user.email') . '>'; - } - $commit_meta['message'] = $commit_meta['message'] ?? 'Changes'; - + public function commit($options=[]) { // First process all blob updates - $updates = $changeset['updates'] ?? []; - $deletes = $changeset['deletes'] ?? []; - $move_trees = $changeset['move_trees'] ?? []; + $updates = $options['updates'] ?? []; + $deletes = $options['deletes'] ?? []; + $move_trees = $options['move_trees'] ?? []; // Track which trees need updating $changed_trees = [ @@ -551,20 +544,24 @@ public function commit($changeset, $commit_meta=[]) { ]; } + $is_amend = isset($options['amend']) && $options['amend']; + // Process trees bottom-up recursively $root_tree_oid = $this->commit_tree('/', $changed_trees); // Create a new commit object - $commit_message = []; - $commit_message[] = "tree " . $root_tree_oid; + $options['tree'] = $root_tree_oid; if($this->get_ref_head('HEAD')) { - $commit_message[] = "parent " . $this->get_ref_head('HEAD'); + $options['parent'] = $this->get_ref_head('HEAD'); + if($is_amend && !$options['message']) { + $this->read_object($options['parent']); + $options['message'] = $this->get_parsed_commit()['message']; + } } - $commit_message[] = "author " . $commit_meta['author'] . " " . time() . " +0000"; - $commit_message[] = "committer " . $commit_meta['committer'] . " " . time() . " +0000"; - $commit_message[] = "\n" . $commit_meta['message']; - $commit_message = implode("\n", $commit_message); - $commit_oid = $this->add_object(WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, $commit_message); + $commit_oid = $this->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, + $this->create_commit_string($options) + ); // Update HEAD $head_ref = $this->get_ref_head('HEAD', ['resolve_ref' => false]); @@ -574,10 +571,127 @@ public function commit($changeset, $commit_meta=[]) { return false; } } + + if(isset($options['amend']) && $options['amend'] && isset($options['parent'])) { + $commit_oid = $this->squash($commit_oid, $options['parent']); + } + $this->reset(); return $commit_oid; } + public function squash($squash_into_commit_oid, $squash_until_ancestor_oid) { + // Find the parent of the squashed range + $this->read_object($squash_until_ancestor_oid); + $new_base_oid = $this->get_parsed_commit()['parent'] ?? self::NULL_OID; + + // Reparent the commits from HEAD until $squash_into_commit_oid onto the parent + // of the squashed range. + $new_head = $this->reparent_commit_range( + $this->get_ref_head('HEAD'), + $squash_into_commit_oid, + $new_base_oid + ); + + // Finally, set the HEAD of the current branch to the new squashed commit. + $current_branch = $this->get_ref_head('HEAD', ['resolve_ref' => false]); + $this->set_ref_head($current_branch, $new_head); + + return $new_head; + } + + /** + * This is not a rebase()! It won't replay the changes while resolving conflicts. + * It just changes the parent of the specified commit range to $new_base_oid. + */ + public function reparent_commit_range($head_oid, $last_ancestor_oid, $new_base_oid) { + // @TODO: Error handling. Exceptions would make it very convenient – maybe let's + // use them internally? + $commits_to_rebase = []; + $moving_head = $head_oid; + while($this->read_object($moving_head)) { + $commits_to_rebase[] = $this->oid; + if($this->oid === $last_ancestor_oid) { + break; + } + $parent = $this->get_parsed_commit()['parent'] ?? self::NULL_OID; + if(self::NULL_OID === $parent ) { + _doing_it_wrong( + __METHOD__, + '$last_ancestor_oid must be an ancestor of $head_oid for reparenting to work, but ' . $last_ancestor_oid . ' is not an ancestor of ' . $this->oid . '.', + '1.0.0' + ); + return false; + } + $moving_head = $parent; + } + + // Rebase $squash_into_commit_oid and its descenrants onto the parent + // of the squashed range. + $new_parent_oid = $new_base_oid; + for($i=count($commits_to_rebase)-1; $i>=0; $i--) { + $this->read_object($commits_to_rebase[$i]); + $parsed_old_commit = $this->get_parsed_commit(); + $new_parent_oid = $this->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, + $this->derive_commit_string($parsed_old_commit, [ + 'parent' => $new_parent_oid, + ]) + ); + } + $new_head_oid = $new_parent_oid; + + return $new_head_oid; + } + + private function derive_commit_string($parsed_commit, $updates) { + /** + * Keep the author and author_date as they are. + * + * The Git Book says: + * + * > You may be wondering what the difference is between author and committer. The + * > author is the person who originally wrote the patch, whereas the committer is + * > the person who last applied the patch. So, if you send in a patch to a project + * > and one of the core members applies the patch, both of you get credit — you as + * > the author and the core member as the committer + * + * See http://git-scm.com/book/ch2-3.html for more information. + */ + unset($updates['author']); + unset($updates['author_date']); + return $this->create_commit_string(array_merge($parsed_commit, $updates)); + } + + private function create_commit_string($options) { + if(!isset($options['tree'])) { + _doing_it_wrong(__METHOD__, '"tree" commit meta field is required', '1.0.0'); + return false; + } + if(!isset($options['author'])) { + $options['author'] = $this->get_config_value('user.name') . ' <' . $this->get_config_value('user.email') . '>'; + } + if(!isset($options['author_date'])) { + $options['author_date'] = time() . ' +0000'; + } + if(!isset($options['committer'])) { + $options['committer'] = $this->get_config_value('user.name') . ' <' . $this->get_config_value('user.email') . '>'; + } + if(!isset($options['committer_date'])) { + $options['committer_date'] = time() . ' +0000'; + } + $options['message'] = $options['message'] ?? 'Changes'; + $commit_message = []; + $commit_message[] = "tree " . $options['tree']; + if(isset($options['parent']) && $options['parent'] !== self::NULL_OID) { + $commit_message[] = "parent " . $options['parent']; + } + $commit_message[] = "author " . $options['author'] . " " . $options['author_date']; + $commit_message[] = "committer " . $options['committer'] . " " . $options['committer_date']; + $commit_message[] = "\n" . $options['message']; + return implode("\n", $commit_message); + } + private function reset() { $this->close_object_stream(); $this->oid = null; From e3684956ea0968b6a15390128bf8b15a7c22577d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 31 Dec 2024 16:37:23 +0100 Subject: [PATCH 37/71] Send autosaves as commit amends --- .../src/WP_Blocks_To_Markdown.php | 2 + .../blueprint.json | 4 + .../plugin.php | 133 ++++++++++-------- .../tests/WPStaticFileSyncTests.php | 4 +- .../WP_Directory_Tree_Entity_Reader.php | 4 +- .../entity-readers/WP_EPub_Entity_Reader.php | 2 +- .../WP_Filesystem_Entity_Reader.php | 2 +- .../data-liberation/src/git/WP_Git_Client.php | 3 +- .../src/git/WP_Git_Filesystem.php | 21 +-- .../src/git/WP_Git_Repository.php | 4 +- 10 files changed, 99 insertions(+), 80 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 9a73e4c86a..95206f36d7 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -234,9 +234,11 @@ private function block_to_markdown($block) { return ''; } $markdown = []; + $markdown[] = ""; $markdown[] = "```block"; $markdown[] = serialize_block($block); $markdown[] = "```"; + $markdown[] = ""; return implode("\n", $markdown); } } diff --git a/packages/playground/data-liberation-static-files-editor/blueprint.json b/packages/playground/data-liberation-static-files-editor/blueprint.json index 098ae0160f..8689c4331c 100644 --- a/packages/playground/data-liberation-static-files-editor/blueprint.json +++ b/packages/playground/data-liberation-static-files-editor/blueprint.json @@ -7,6 +7,10 @@ "WP_DEBUG_LOG": true, "WP_DEBUG_DISPLAY": true }, + "plugins": ["gutenberg"], + "siteOptions": { + "wp_page_for_privacy_policy": null + }, "steps": [ { "step": "activatePlugin", diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index d5c6b8e74c..6548db4dff 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -69,6 +69,9 @@ static private function get_fs() { 'client' => $client, ] ); + if(!self::$fs->is_dir('/.autosaves')) { + self::$fs->mkdir('/.autosaves'); + } } return self::$fs; } @@ -259,20 +262,62 @@ static public function initialize() { // Update the file after post is saved add_action('save_post_' . WP_LOCAL_FILE_POST_TYPE, function($post_id, $post, $update) { - self::save_post_data_to_local_file($post); + try { + if(!self::acquire_synchronization_lock()) { + return; + } + $post_id = $post->ID; + if ( + empty($post->ID) || + $post->post_status !== 'publish' || + $post->post_type !== WP_LOCAL_FILE_POST_TYPE + ) { + return; + } + + $path = get_post_meta($post_id, 'local_file_path', true); + $content = self::convert_post_to_string($path, $post); + + $fs = self::get_fs(); + $fs->put_contents($path, $content, [ + 'message' => 'User saved ' . $post->post_title, + ]); + } finally { + self::release_synchronization_lock(); + } }, 10, 3); // Also update file when autosave occurs add_action('wp_creating_autosave', function($autosave) { - $autosave = (object)$autosave; - if ($autosave->post_type !== 'revision') { - return; - } - $parent_post = get_post($autosave->post_parent); - if ($parent_post->post_type !== WP_LOCAL_FILE_POST_TYPE) { - return; + try { + if(!self::acquire_synchronization_lock()) { + return; + } + $autosave = (object)$autosave; + if ( + empty($autosave->ID) || + $autosave->post_status !== 'inherit' || + $autosave->post_type !== 'revision' + ) { + return; + } + $parent_post = get_post($autosave->post_parent); + if ($parent_post->post_type !== WP_LOCAL_FILE_POST_TYPE) { + return; + } + + $path = wp_join_paths( + '/.autosaves/', + get_post_meta($parent_post->ID, 'local_file_path', true) + ); + $fs = self::get_fs(); + $content = self::convert_post_to_string($path, $autosave); + $fs->put_contents($path, $content, [ + 'amend' => true, + ]); + } finally { + self::release_synchronization_lock(); } - self::save_post_data_to_local_file($autosave); }, 10, 1); } @@ -312,7 +357,7 @@ static private function refresh_post_from_local_file($post) { _doing_it_wrong(__METHOD__, 'File not found: ' . $path, '1.0.0'); return; } - $content = $fs->read_file($path); + $content = $fs->get_contents($path); if(!is_string($content)) { _doing_it_wrong(__METHOD__, 'File not found: ' . $path, '1.0.0'); return; @@ -356,55 +401,29 @@ static private function refresh_post_from_local_file($post) { } } - static private function save_post_data_to_local_file($post) { - try { - if(!self::acquire_synchronization_lock()) { - return; - } - $post_id = $post->ID; - if ( - empty($post->ID) || - ($post->post_status !== 'publish' && $post->post_status !== 'inherit') || - ($post->post_type !== WP_LOCAL_FILE_POST_TYPE && $post->post_type !== 'revision') - ) { - return; - } + static private function convert_post_to_string($path, $post) { + $post_id = $post->ID; - $root_post = $post->post_type === 'revision' ? get_post($post->post_parent) : $post; - - $fs = self::get_fs(); - $path = get_post_meta($root_post->ID, 'local_file_path', true); - $extension = pathinfo($path, PATHINFO_EXTENSION); - $metadata = []; - foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { - $metadata[$key] = get_post_field($key, $root_post->ID); - } - // @TODO: Also include actual post_meta. Which ones? All? The - // ones explicitly set by the user in the editor? - - $content = get_post_field('post_content', $post_id); - switch($extension) { - // @TODO: Add support for HTML and XHTML - case 'html': - case 'xhtml': - case 'md': - default: - $converter = new WP_Blocks_To_Markdown( $content, $metadata ); - break; - } - $converter->convert(); - - if(method_exists($fs, 'set_next_commit_message')) { - if($post->post_type === 'revision') { - $fs->set_next_commit_message('Autosave of ' . $root_post->post_title); - } else { - $fs->set_next_commit_message('User saved ' . $post->post_title); - } - } - $fs->put_contents($path, $converter->get_result()); - } finally { - self::release_synchronization_lock(); + $extension = pathinfo($path, PATHINFO_EXTENSION); + $metadata = []; + foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { + $metadata[$key] = get_post_field($key, $post->ID); + } + // @TODO: Also include actual post_meta. Which ones? All? The + // ones explicitly set by the user in the editor? + + $content = get_post_field('post_content', $post_id); + switch($extension) { + // @TODO: Add support for HTML and XHTML + case 'html': + case 'xhtml': + case 'md': + default: + $converter = new WP_Blocks_To_Markdown( $content, $metadata ); + break; } + $converter->convert(); + return $converter->get_result(); } static public function get_local_files_tree($subdirectory = '') { diff --git a/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php b/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php index 85456c838f..c323487892 100644 --- a/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php +++ b/packages/playground/data-liberation-static-files-editor/tests/WPStaticFileSyncTests.php @@ -76,7 +76,7 @@ public function test_ensure_is_directory_index_creates_an_index_file_if_needed() $fs = $this->filesystem; $this->assertFalse($fs->is_file('/pride-and-prejudice.md')); $this->assertTrue($fs->is_file('/pride-and-prejudice/index.md')); - $this->assertEquals('Pride and Prejudice', $fs->read_file('/pride-and-prejudice/index.md')); + $this->assertEquals('Pride and Prejudice', $fs->get_contents('/pride-and-prejudice/index.md')); } public function test_ensure_is_directory_index_returns_the_index_file_if_it_already_exists() { @@ -91,7 +91,7 @@ public function test_ensure_is_directory_index_returns_the_index_file_if_it_alre $fs = $this->filesystem; $this->assertTrue($fs->is_file('/pride-and-prejudice/index.md')); - $this->assertEquals('Pride and Prejudice', $fs->read_file('/pride-and-prejudice/index.md')); + $this->assertEquals('Pride and Prejudice', $fs->get_contents('/pride-and-prejudice/index.md')); } private function setup_directory_tree($structure, $path_so_far = '/') { diff --git a/packages/playground/data-liberation/src/entity-readers/WP_Directory_Tree_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_Directory_Tree_Entity_Reader.php index 6259e88ad8..c99fe0c5fd 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_Directory_Tree_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_Directory_Tree_Entity_Reader.php @@ -131,7 +131,7 @@ public function next_entity() { $file_path = $this->pending_directory_index; $this->parent_ids[ $depth ] = $this->emit_post_entity( array( - 'content' => $this->filesystem->read_file( $file_path ), + 'content' => $this->filesystem->get_contents( $file_path ), 'local_file_path' => $file_path, 'parent_id' => $parent_id, 'title_fallback' => WP_Import_Utils::slug_to_title( basename( $file_path ) ), @@ -148,7 +148,7 @@ public function next_entity() { $file_path = array_shift( $this->pending_files ); $this->emit_post_entity( array( - 'content' => $this->filesystem->read_file( $file_path ), + 'content' => $this->filesystem->get_contents( $file_path ), 'local_file_path' => $file_path, 'parent_id' => $parent_id, 'title_fallback' => WP_Import_Utils::slug_to_title( basename( $file_path ) ), diff --git a/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php index db7b8b9df3..ec6427afcd 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php @@ -94,7 +94,7 @@ public function next_entity() { } $html_file = array_shift( $this->remaining_html_files ); - $html = $this->zip->read_file( $html_file ); + $html = $this->zip->get_contents( $html_file ); $this->current_html_reader = new WP_HTML_Entity_Reader( WP_XML_Processor::create_from_string( $html ), $this->current_post_id diff --git a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php index 92f2521118..c978454135 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php @@ -50,7 +50,7 @@ public function next_entity(): bool { $source_content_converter = null; $post_tree_node = $this->post_tree->get_current_node(); if($post_tree_node['type'] === 'file') { - $content = $this->filesystem->read_file($post_tree_node['local_file_path']); + $content = $this->filesystem->get_contents($post_tree_node['local_file_path']); $extension = pathinfo($post_tree_node['local_file_path'], PATHINFO_EXTENSION); switch($extension) { case 'md': diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index a5a5c9c114..665a34d943 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -84,6 +84,7 @@ public function force_push_one_commit() { $remote_commit = $this->index->get_ref_head('refs/remotes/' . $this->remote_name . '/' . $push_ref_name); // @TODO: Do find_objects_added_since to enable pushing multiple commits at once. // OR! perhaps supporting "have" and "want" would solve this. + // $delta = $this->index->find_objects_added_in($push_commit, $parent_hash); $delta = $this->index->find_objects_added_in($push_commit, $remote_commit); // @TODO: Implement streaming push bytes instead of buffering everything like this. @@ -100,7 +101,7 @@ public function force_push_one_commit() { ]; } - $push_packet = WP_Git_Pack_Processor::encode_packet_line("$parent_hash $push_commit refs/heads/$push_ref_name\0report-status force-update\n"); + $push_packet = WP_Git_Pack_Processor::encode_packet_line("$remote_commit $push_commit refs/heads/$push_ref_name\0report-status force-update\n"); $push_packet .= "0000"; $push_packet .= WP_Git_Pack_Processor::encode($pack_objects); $push_packet .= "0000"; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index a10b28ffb9..be7c8a1e88 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -13,7 +13,6 @@ class WP_Git_Filesystem extends WP_Abstract_Filesystem { private $root; private $auto_push; private $client; - private $next_commit_message; public function __construct( WP_Git_Repository $repo, @@ -77,7 +76,7 @@ public function get_streamed_file_length() { throw new Exception('Not implemented'); } - public function read_file($path) { + public function get_contents($path) { if(!$this->is_file($path)) { return false; } @@ -98,7 +97,7 @@ public function rename($old_path, $new_path) { return $this->commit( [ 'updates' => [ - $this->resolve_path($new_path) => $this->read_file($old_path), + $this->resolve_path($new_path) => $this->get_contents($old_path), ], 'deletes' => [ $this->resolve_path($old_path), @@ -156,26 +155,20 @@ public function rmdir($path, $options = []) { ); } - public function put_contents($path, $data) { + public function put_contents($path, $data, $options=[]) { return $this->commit( [ 'updates' => [ $this->resolve_path($path) => $data, ], + 'amend' => isset($options['amend']) && $options['amend'], + 'message' => isset($options['message']) ? $options['message'] : null, ] ); } - public function set_next_commit_message($message) { - $this->next_commit_message = $message; - } - - private function commit($changeset, $commit_meta=[]) { - if($this->next_commit_message) { - $commit_meta['message'] = $this->next_commit_message; - $this->next_commit_message = null; - } - if(false === $this->repo->commit($changeset, $commit_meta)) { + private function commit($options) { + if(false === $this->repo->commit($options)) { return false; } if($this->auto_push) { diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index ee1c29e857..064db90eb5 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -91,7 +91,7 @@ private function parse_config() { $this->parsed_config = []; return; } - $this->parsed_config = parse_ini_string($this->fs->read_file('config'), true, INI_SCANNER_RAW); + $this->parsed_config = parse_ini_string($this->fs->get_contents('config'), true, INI_SCANNER_RAW); } } @@ -413,7 +413,7 @@ public function get_ref_head($ref='HEAD', $options=[]) { $this->last_error = 'Ref file not found: ' . $path; return false; } - $contents = trim($this->fs->read_file($path)); + $contents = trim($this->fs->get_contents($path)); if($options['resolve_ref'] ?? true) { return $this->get_ref_head($contents, $options); } From 1676afaecab92ce944770d5ff17599e51a7f3ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 1 Jan 2025 14:36:04 +0100 Subject: [PATCH 38/71] Drag and drop from the editor to desktop --- .../blueprint.json | 2 +- .../plugin.php | 55 ++++++++ .../src/components/FilePickerTree/index.tsx | 32 +++-- .../ui/src/index.tsx | 23 +++- .../data-liberation/blueprints-library | 2 +- .../data-liberation/src/git/WP_Git_Client.php | 3 + .../src/git/WP_Git_Filesystem.php | 14 +- .../src/git/WP_Git_Repository.php | 123 ++++++++++++++++-- 8 files changed, 228 insertions(+), 26 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/blueprint.json b/packages/playground/data-liberation-static-files-editor/blueprint.json index 8689c4331c..6b3bf8e6f4 100644 --- a/packages/playground/data-liberation-static-files-editor/blueprint.json +++ b/packages/playground/data-liberation-static-files-editor/blueprint.json @@ -9,7 +9,7 @@ }, "plugins": ["gutenberg"], "siteOptions": { - "wp_page_for_privacy_policy": null + "wp_page_for_privacy_policy": "" }, "steps": [ { diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 6548db4dff..af0284e5f3 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -207,6 +207,23 @@ static public function initialize() { return current_user_can('edit_posts'); }, )); + + register_rest_route('static-files-editor/v1', '/download-file', array( + 'methods' => 'GET', + 'callback' => array(self::class, 'download_file_endpoint'), + 'permission_callback' => function() { + return current_user_can('edit_posts'); + }, + 'args' => array( + 'path' => array( + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => function($param) { + return '/' . ltrim($param, '/'); + } + ) + ) + )); }); // @TODO: the_content and rest_prepare_local_file filters run twice for REST API requests. @@ -321,6 +338,44 @@ static public function initialize() { }, 10, 1); } + static public function download_file_endpoint($request) { + $path = $request->get_param('path'); + $fs = self::get_fs(); + + if($fs->is_dir($path)) { + return new WP_Error('file_error', 'Directory download is not supported yet.'); + } + + // Get file info + $filename = basename($path); + $filesize = $fs->get_streamed_file_length($path); + + // Set headers for file download + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . $filesize); + header('Cache-Control: no-cache'); + + // Stream file contents + if(!$fs->open_file_stream($path)) { + return new WP_Error('file_error', 'Could not read file'); + } + + echo $fs->get_file_chunk(); + + while($fs->next_file_chunk()) { + echo $fs->get_file_chunk(); + } + + $fs->close_file_stream(); + + if(!$fs->get_error_message()) { + exit; + } + + return new WP_Error('file_error', 'Could not read file'); + } + static private $synchronizing = false; static private function acquire_synchronization_lock() { // Skip if in maintenance mode diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index b896834996..6579de32e3 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -51,6 +51,11 @@ export type FilePickerControlProps = { initialPath?: string; className?: string; onSelect?: (path: string, node: FileNode) => void; + onDragStart?: ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => void; onNodesCreated?: (tree: FileTree) => void; onNodeDeleted?: (path: string) => void; onNodeMoved?: ({ @@ -91,7 +96,11 @@ type FilePickerContextType = { onEditedNodeCancel: () => void; onNodeDeleted: (path: string) => void; startRenaming: (path: string) => void; - onDragStart: (path: string, type: 'file' | 'folder') => void; + onDragStart: ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => void; onDragOver: ( e: React.DragEvent, path: string, @@ -111,6 +120,7 @@ export const FilePickerTree: React.FC = ({ initialPath, className = '', onSelect = () => {}, + onDragStart = () => {}, onNodesCreated = (tree: FileTree) => { console.log('onNodesCreated', tree); }, @@ -241,7 +251,12 @@ export const FilePickerTree: React.FC = ({ setEditedNode(null); }; - const handleDragStart = (path: string, type: 'file' | 'folder') => { + const handleDragStart = ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => { + onDragStart(e, path, type); setDragState({ path, hoverPath: null, @@ -307,7 +322,9 @@ export const FilePickerTree: React.FC = ({ targetType === 'folder' ? targetPath : targetPath.split('/').slice(0, -1).join('/'); - const items = Array.from(e.dataTransfer.items); + const items = Array.from(e.dataTransfer.items).filter( + (item) => item.kind !== 'DownloadURL' + ); const buildTree = async ( entry: FileSystemEntry, @@ -377,10 +394,7 @@ export const FilePickerTree: React.FC = ({ ? targetPath.split('/').slice(0, -1).join('/') : targetPath; - const toPath = [ - targetParentPath.split('/'), - dragState.path.split('/').pop(), - ] + const toPath = [targetParentPath, dragState.path.split('/').pop()] .join('/') .replace(/^\/+/, ''); @@ -706,8 +720,8 @@ const NodeRow: React.FC<{ )} data-path={path} draggable={true} - onDragStart={() => - onDragStart?.(path, node.type) + onDragStart={(e) => + onDragStart?.(e, path, node.type) } onDragOver={(e) => onDragOver?.(e, path, node.type) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 3429bf8e33..709742ec6f 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState, useCallback } from '@wordpress/element'; +import React from 'react'; +import { useEffect, useState, useCallback } from '@wordpress/element'; import { FileNode, FilePickerTree } from './components/FilePickerTree'; import { store as editorStore } from '@wordpress/editor'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -220,6 +221,25 @@ function ConnectedFilePickerTree() { } }; + /** + * Enable drag and drop of files from the file picker tree to desktop. + */ + const handleDragStart = ( + e: React.DragEvent, + path: string, + type: 'file' | 'folder' + ) => { + // Directory downloads are not supported yet. + if (type === 'file') { + const url = `${window.wpApiSettings.root}static-files-editor/v1/download-file?path=${path}&_wpnonce=${window.wpApiSettings.nonce}`; + const filename = path.split('/').pop(); + e.dataTransfer.setData( + 'DownloadURL', + `text/plain:${filename}:${url}` + ); + } + }; + if (isLoading) { return ; } @@ -236,6 +256,7 @@ function ConnectedFilePickerTree() { onNodesCreated={handleNodesCreated} onNodeDeleted={handleNodeDeleted} onNodeMoved={handleNodeMoved} + onDragStart={handleDragStart} /> ); } diff --git a/packages/playground/data-liberation/blueprints-library b/packages/playground/data-liberation/blueprints-library index 6bf961fcc2..e8b7a07cbf 160000 --- a/packages/playground/data-liberation/blueprints-library +++ b/packages/playground/data-liberation/blueprints-library @@ -1 +1 @@ -Subproject commit 6bf961fcc2188ef5d5c65c182ebc1336bc238bb2 +Subproject commit e8b7a07cbfb054402dc352b5bff6655504e509b1 diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 665a34d943..af527f580b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -194,6 +194,9 @@ public function force_pull($branch_name=null, $path = '/') { } public function fetchObjects($refs) { + if(empty($refs)) { + return true; + } $body = ''; foreach($refs as $ref) { $body .= $this->encode_packet_line("want {$ref} multi_ack_detailed no-done side-band-64k thin-pack ofs-delta agent=git/2.37.3\n"); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index be7c8a1e88..c8b468fc9b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -53,27 +53,29 @@ public function is_file($path) { } public function open_file_stream($path) { - throw new Exception('Not implemented'); + return $this->repo->read_by_path($path); } public function next_file_chunk() { - throw new Exception('Not implemented'); + return $this->repo->next_body_chunk(); } public function get_file_chunk() { - throw new Exception('Not implemented'); + return $this->repo->get_body_chunk(); } public function get_error_message() { - throw new Exception('Not implemented'); + // @TODO: Manage our own errors in addition to passing + // through the underlying repo's errors. + return $this->repo->get_last_error(); } public function close_file_stream() { - throw new Exception('Not implemented'); + // @TODO: Implement this } public function get_streamed_file_length() { - throw new Exception('Not implemented'); + return $this->repo->get_length(); } public function get_contents($path) { diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 064db90eb5..305e80194e 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -4,18 +4,118 @@ class WP_Git_Repository { + /** + * The filesystem root where the repository index files are stored. + * + * @var WP_Abstract_Filesystem + */ private $fs; + /** + * The SHA-1 ID of the current object. + * + * @var string + */ private $oid; + + /** + * The type of the current object. One of: + * + * - WP_Git_Pack_Processor::OBJECT_TYPE_BLOB + * - WP_Git_Pack_Processor::OBJECT_TYPE_TREE + * - WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT + * + * @var string + */ private $type; + + /** + * Structured data parsed from the currently processed + * commit object. + * + * @var array { + * @type string $tree + * @type string $parent + * @type string $author + * @type string $committer + * @type string $message + * } + */ + private $parsed_commit; + + /** + * Structured data parsed from the currently processed + * tree object. + * + * @var array { + * @type array { + * @type string $mode + * @type string $oid + * @type string $path + * } + * } + */ + private $parsed_tree; + + /** + * Structured data parsed from the repository `config` file. + * + * @var array + */ + private $parsed_config; + + /** + * PHP inflate context to decompress the currently streamed + * object content. + * + * @var InflateContext + */ private $content_inflate_handle; + + /** + * A decompressed chunk of the currently streamed + * object. + * + * @var string + */ private $object_content_chunk; - private $called_next_body_chunk; + + /** + * $consumer_called_next_chunk prevents the first object body + * chunk from being returned until the consumer has called + * next_body_chunk() at least once. + * + * The API consumer expects the following streaming interface: + * + * 1. read_object() + * 1. next_body_chunk() + * 1. get_body_chunk() + * + * However, internally we need to start streaming the object + * in read_object(). This immediately populates the first + * object body chunk even before the consumer calls next_body_chunk(). + * + * If the consumer just calls get_body_chunk() immediately after + * read_object(), they'll effectively skip the first chunk. + * + * This boolean flag prevents that from happening. get_body_chunk() + * will return an empty string until next_body_chunk() has been called + * at least once. + * + * @var bool + */ + private $consumer_called_next_chunk = false; + + /** + * Memoized body of the last object read by read_object(). + * + * It prevents read_entire_object_contents() from re-reading + * the object from the filesystem on every call. + * + * @var string|null + */ private $buffered_object_content; - private $parsed_commit; - private $parsed_tree; private $last_error; - private $parsed_config; private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; private const NULL_OID = '0000000000000000000000000000000000000000'; @@ -140,6 +240,7 @@ public function read_object($oid) { } $this->object_content_chunk = substr($content, strlen($header) + 1); + $this->consumer_called_next_chunk = false; // Parse the header $type_length = strpos($header, ' '); @@ -186,11 +287,14 @@ private function open_object_stream() { } public function next_body_chunk() { + if($this->consumer_called_next_chunk === false) { + $this->consumer_called_next_chunk = true; + return true; + } if(false === $this->fs->next_file_chunk()) { $this->last_error = $this->fs->get_error_message(); return false; } - $this->called_next_body_chunk = true; $chunk = $this->fs->get_file_chunk(); $next_chunk = inflate_add($this->content_inflate_handle, $chunk); if(false === $next_chunk) { @@ -203,6 +307,9 @@ public function next_body_chunk() { } public function get_body_chunk() { + if($this->consumer_called_next_chunk === false) { + return ''; + } return $this->object_content_chunk; } @@ -235,7 +342,7 @@ public function get_parsed_tree() { public function read_entire_object_contents() { // If we've advanced the stream, we can't reuse it to read the entire // object anymore. Let's re-initialize the stream. - if($this->called_next_body_chunk) { + if($this->consumer_called_next_chunk) { $this->read_object($this->oid); } if(null !== $this->buffered_object_content) { @@ -244,7 +351,7 @@ public function read_entire_object_contents() { // Load the entire object into memory and keep the result // for later use. We'll likely need it again before we're // done with the current object. - $this->buffered_object_content = $this->object_content_chunk; + $this->buffered_object_content = ''; while($this->next_body_chunk()) { $this->buffered_object_content .= $this->get_body_chunk(); } @@ -698,7 +805,7 @@ private function reset() { $this->type = null; $this->parsed_commit = null; $this->parsed_tree = null; - $this->called_next_body_chunk = false; + $this->consumer_called_next_chunk = false; $this->buffered_object_content = null; $this->object_content_chunk = null; $this->last_error = null; From d33b84ff764aa45970f1babb4b11a6c30f2ed4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 1 Jan 2025 15:15:11 +0100 Subject: [PATCH 39/71] Preview static files --- .../ui/src/add-local-files-tab.tsx | 6 +- .../ui/src/index.tsx | 103 ++++++++++++++---- 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx index bc8843c29b..c3c4b73fd9 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/add-local-files-tab.tsx @@ -74,7 +74,7 @@ export function addLocalFilesTab(tab: { }; } -export function addLoadingOverlay(Overlay: React.ReactElement) { +export function addComponentToEditorContentArea(Component: React.ReactElement) { function patchArguments(args: any[]) { let [type, props, ...children] = args; if (!props || typeof props.className !== 'string') { @@ -90,8 +90,8 @@ export function addLoadingOverlay(Overlay: React.ReactElement) { if (!Array.isArray(newProps.children)) { newProps.children = [newProps.children]; } - if (!newProps.children.includes(Overlay)) { - newProps.children.unshift(Overlay); + if (!newProps.children.includes(Component)) { + newProps.children.unshift(Component); } return [type, newProps, ...newProps.children]; } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 709742ec6f..b806156b88 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -11,7 +11,11 @@ import { useSelect, } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; -import { addLoadingOverlay, addLocalFilesTab } from './add-local-files-tab'; +import { + addComponentToEditorContentArea, + addLoadingOverlay, + addLocalFilesTab, +} from './add-local-files-tab'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { Spinner, Button } from '@wordpress/components'; import { useEntityProp, store as coreStore } from '@wordpress/core-data'; @@ -28,10 +32,12 @@ let fileTreePromise = apiFetch({ // Create a custom store for transient UI state const STORE_NAME = 'static-files-editor/ui'; const uiStore = createReduxStore(STORE_NAME, { - reducer(state = { isPostLoading: false }, action) { + reducer(state = { isPostLoading: false, previewPath: null }, action) { switch (action.type) { case 'SET_POST_LOADING': return { ...state, isPostLoading: action.isLoading }; + case 'SET_PREVIEW_PATH': + return { ...state, previewPath: action.path }; default: return state; } @@ -40,11 +46,17 @@ const uiStore = createReduxStore(STORE_NAME, { setPostLoading(isLoading) { return { type: 'SET_POST_LOADING', isLoading }; }, + setPreviewPath(path) { + return { type: 'SET_PREVIEW_PATH', path }; + }, }, selectors: { isPostLoading(state) { return state.isPostLoading; }, + getPreviewPath(state) { + return state.previewPath; + }, }, }); @@ -59,16 +71,23 @@ function ConnectedFilePickerTree() { const [selectedPath, setSelectedPath] = useState( meta?.local_file_path || '/' ); - // interface-interface-skeleton__content + useEffect(() => { async function refreshPostId() { - setPostLoading(true); - const { post_id } = (await apiFetch({ - path: '/static-files-editor/v1/get-or-create-post-for-file', - method: 'POST', - data: { path: selectedPath }, - })) as { post_id: string }; - setSelectedPostId(post_id); + const extension = selectedPath.split('.').pop()?.toLowerCase(); + if (extension === 'md' || extension === 'html') { + setPostLoading(true); + const { post_id } = (await apiFetch({ + path: '/static-files-editor/v1/get-or-create-post-for-file', + method: 'POST', + data: { path: selectedPath }, + })) as { post_id: string }; + setSelectedPostId(post_id); + setPreviewPath(null); + } else { + setSelectedPostId(null); + setPreviewPath(selectedPath); + } } refreshPostId(); }, [selectedPath]); @@ -102,20 +121,22 @@ function ConnectedFilePickerTree() { [selectedPostId] ); - const { setPostLoading } = useDispatch(STORE_NAME); + const { setPostLoading, setPreviewPath } = useDispatch(STORE_NAME); useEffect(() => { // Only navigate once the post has been loaded. Otherwise the editor // will disappear for a second – the component renders its // children conditionally on having the post available. - setPostLoading(!hasLoadedPost); - if (hasLoadedPost && post) { - onNavigateToEntityRecord({ - postId: selectedPostId, - postType: WP_LOCAL_FILE_POST_TYPE, - }); + if (selectedPostId) { + setPostLoading(!hasLoadedPost); + if (hasLoadedPost && post) { + onNavigateToEntityRecord({ + postId: selectedPostId, + postType: WP_LOCAL_FILE_POST_TYPE, + }); + } } - }, [hasLoadedPost, post, setPostLoading]); + }, [hasLoadedPost, post, setPostLoading, selectedPostId]); const refreshFileTree = useCallback(async () => { fileTreePromise = apiFetch({ @@ -271,6 +292,50 @@ addLocalFilesTab({ ), }); +function FilePreviewOverlay() { + const previewPath = useSelect( + (select) => select(STORE_NAME).getPreviewPath(), + [] + ); + + if (!previewPath) { + return null; + } + + const extension = previewPath.split('.').pop()?.toLowerCase(); + const isPreviewable = ['jpg', 'jpeg', 'png', 'gif', 'svg'].includes( + extension || '' + ); + + return ( +
+

{previewPath.split('/').pop()}

+ {isPreviewable ? ( + {previewPath} + ) : ( +
Preview not available for this file type
+ )} +
+ ); +} + +addComponentToEditorContentArea(); + function PostLoadingOverlay() { const isLoading = useSelect( (select) => select(STORE_NAME).isPostLoading(), @@ -299,7 +364,7 @@ function PostLoadingOverlay() { ); } -addLoadingOverlay(); +addComponentToEditorContentArea(); dispatch(preferencesStore).set('welcomeGuide', false); dispatch(preferencesStore).set('enableChoosePatternModal', false); From 620755d0cfba3b7ca4dce6f730de9ebfd64df54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 1 Jan 2025 15:18:38 +0100 Subject: [PATCH 40/71] Only force pull at most once every 10 minutes, exclude .autosaves from the files browser --- .../plugin.php | 25 +++++++++++++++---- .../data-liberation/blueprints-library | 2 +- .../playground/data-liberation/bootstrap.php | 1 + 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index af0284e5f3..6e646d5be5 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -29,6 +29,10 @@ define( 'WP_LOCAL_FILE_POST_TYPE', 'local_file' ); } +if( ! defined( 'WP_AUTOSAVES_DIRECTORY' )) { + define( 'WP_AUTOSAVES_DIRECTORY', '.autosaves' ); +} + if(isset($_GET['dump'])) { add_action('init', function() { WP_Static_Files_Editor_Plugin::import_static_pages(); @@ -57,8 +61,14 @@ static private function get_fs() { $repo->set_config_value('user.email', GIT_USER_EMAIL); $client = new WP_Git_Client($repo); - if(false === $client->force_pull()) { - _doing_it_wrong(__METHOD__, 'Failed to pull from remote repository', '1.0.0'); + + // Only force pull at most once every 10 minutes + $last_pull_time = get_transient('wp_git_last_pull_time'); + if (!$last_pull_time) { + if (false === $client->force_pull()) { + _doing_it_wrong(__METHOD__, 'Failed to pull from remote repository', '1.0.0'); + } + set_transient('wp_git_last_pull_time', time(), 10 * MINUTE_IN_SECONDS); } self::$fs = new WP_Git_Filesystem( @@ -69,8 +79,8 @@ static private function get_fs() { 'client' => $client, ] ); - if(!self::$fs->is_dir('/.autosaves')) { - self::$fs->mkdir('/.autosaves'); + if(!self::$fs->is_dir('/' . WP_AUTOSAVES_DIRECTORY)) { + self::$fs->mkdir('/' . WP_AUTOSAVES_DIRECTORY); } } return self::$fs; @@ -324,7 +334,7 @@ static public function initialize() { } $path = wp_join_paths( - '/.autosaves/', + '/' . WP_AUTOSAVES_DIRECTORY . '/', get_post_meta($parent_post->ID, 'local_file_path', true) ); $fs = self::get_fs(); @@ -498,6 +508,11 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree) { } foreach ($items as $item) { + // Exclude the autosaves directory from the files tree + if($dir === '/' && $item === WP_AUTOSAVES_DIRECTORY) { + continue; + } + $path = $dir === '/' ? "/$item" : "$dir/$item"; if ($fs->is_dir($path)) { diff --git a/packages/playground/data-liberation/blueprints-library b/packages/playground/data-liberation/blueprints-library index e8b7a07cbf..8bc9dc3b00 160000 --- a/packages/playground/data-liberation/blueprints-library +++ b/packages/playground/data-liberation/blueprints-library @@ -1 +1 @@ -Subproject commit e8b7a07cbfb054402dc352b5bff6655504e509b1 +Subproject commit 8bc9dc3b00a0018dcd7f453b7429b00179e422c0 diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 9059423e52..caff2097ce 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -14,6 +14,7 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Abstract_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Local_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_In_Memory_Filesystem.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_SQLite_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_File_Visitor_Event.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Visitor.php'; From 8594402948525b56062e3b5b89fe93286345804f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 1 Jan 2025 15:27:57 +0100 Subject: [PATCH 41/71] =?UTF-8?q?Reduce=20waiting=20in=20the=20UI=20?= =?UTF-8?q?=E2=80=93=20server=20provides=20each=20page's=20ID,=20the=20edi?= =?UTF-8?q?tor=20reuses=20that=20ID=20while=20it=20can?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin.php | 31 ++++++++++++++++--- .../ui/src/index.tsx | 30 ++++++++++++------ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 6e646d5be5..e131849e2f 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -490,18 +490,33 @@ static private function convert_post_to_string($path, $post) { $converter->convert(); return $converter->get_result(); } - static public function get_local_files_tree($subdirectory = '') { $tree = []; $fs = self::get_fs(); + // Get all file paths and post IDs in one query + $file_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'posts_per_page' => -1, + 'fields' => 'id=>meta' + )); + + $path_to_post_id = array(); + foreach($file_posts as $post) { + $file_path = get_post_meta($post->ID, 'local_file_path', true); + if ($file_path) { + $path_to_post_id[$file_path] = $post->ID; + } + } + $base_dir = $subdirectory ? $subdirectory : '/'; - self::build_local_file_tree_recursive($fs, $base_dir, $tree); + self::build_local_file_tree_recursive($fs, $base_dir, $tree, $path_to_post_id); return $tree; } - static private function build_local_file_tree_recursive($fs, $dir, &$tree) { + static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path_to_post_id) { $items = $fs->ls($dir); if ($items === false) { return; @@ -525,12 +540,18 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree) { // Recursively build children $last_index = count($tree) - 1; - self::build_local_file_tree_recursive($fs, $path, $tree[$last_index]['children']); + self::build_local_file_tree_recursive($fs, $path, $tree[$last_index]['children'], $path_to_post_id); } else { - $tree[] = array( + $node = array( 'type' => 'file', 'name' => $item, ); + + if (isset($path_to_post_id[$path])) { + $node['post_id'] = $path_to_post_id[$path]; + } + + $tree[] = $node; } } } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index b806156b88..7b7a392423 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -62,6 +62,11 @@ const uiStore = createReduxStore(STORE_NAME, { register(uiStore); +const isStaticPagePath = (path: string) => { + const extension = path.split('.').pop()?.toLowerCase(); + return ['md', 'html'].includes(extension); +}; + function ConnectedFilePickerTree() { const [fileTree, setFileTree] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -74,17 +79,19 @@ function ConnectedFilePickerTree() { useEffect(() => { async function refreshPostId() { - const extension = selectedPath.split('.').pop()?.toLowerCase(); - if (extension === 'md' || extension === 'html') { + if (isStaticPagePath(selectedPath)) { setPostLoading(true); - const { post_id } = (await apiFetch({ - path: '/static-files-editor/v1/get-or-create-post-for-file', - method: 'POST', - data: { path: selectedPath }, - })) as { post_id: string }; - setSelectedPostId(post_id); + if (!selectedPostId) { + const { post_id } = (await apiFetch({ + path: '/static-files-editor/v1/get-or-create-post-for-file', + method: 'POST', + data: { path: selectedPath }, + })) as { post_id: string }; + setSelectedPostId(post_id); + } setPreviewPath(null); } else { + setPostLoading(false); setSelectedPostId(null); setPreviewPath(selectedPath); } @@ -100,7 +107,7 @@ function ConnectedFilePickerTree() { const { post, hasLoadedPost, onNavigateToEntityRecord } = useSelect( (select) => { - const { getEntityRecord, isResolving, hasFinishedResolution } = + const { getEntityRecord, hasFinishedResolution } = select(coreStore); return { onNavigateToEntityRecord: @@ -173,6 +180,11 @@ function ConnectedFilePickerTree() { const handleFileClick = async (filePath: string, node: FileNode) => { setSelectedPath(filePath); + if (isStaticPagePath(filePath)) { + setSelectedPostId(node.post_id); + } else { + setSelectedPostId(null); + } }; const handleNodesCreated = async (tree: FileTree) => { From 6956b5850a608290ca15eacabfd442a5c115498d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 1 Jan 2025 21:55:13 +0100 Subject: [PATCH 42/71] Import attachments on boot and on upload --- .../plugin.php | 469 ++++++++++-------- .../src/components/FilePickerTree/index.tsx | 144 ++++-- .../ui/src/index.tsx | 40 +- .../data-liberation/blueprints-library | 2 +- .../playground/data-liberation/bootstrap.php | 2 + .../playground/data-liberation/plugin.php | 18 +- .../WP_Filesystem_Entity_Reader.php | 47 +- .../WP_Filesystem_To_Post_Tree.php | 46 +- .../data-liberation/src/functions.php | 91 ++-- .../src/git/WP_Git_Filesystem.php | 69 ++- .../src/git/WP_Git_Repository.php | 6 +- .../src/import/WP_Attachment_Downloader.php | 39 +- .../src/import/WP_Entity_Importer.php | 67 +-- .../src/import/WP_File_Visitor.php | 96 ---- .../src/import/WP_Stream_Importer.php | 129 +++-- 15 files changed, 687 insertions(+), 578 deletions(-) delete mode 100644 packages/playground/data-liberation/src/import/WP_File_Visitor.php diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index e131849e2f..dc64c63f1a 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -20,6 +20,8 @@ */ use WordPress\Filesystem\WP_Local_Filesystem; +use WordPress\Filesystem\WP_Filesystem_Visitor; +use WordPress\Filesystem\WP_Uploaded_Directory_Tree_Filesystem; if ( ! defined( 'WP_STATIC_CONTENT_DIR' ) ) { define( 'WP_STATIC_CONTENT_DIR', WP_CONTENT_DIR . '/uploads/static-pages' ); @@ -46,6 +48,7 @@ class WP_Static_Files_Editor_Plugin { static private $fs; + static private $client; static private function get_fs() { if(!self::$fs) { @@ -60,12 +63,12 @@ static private function get_fs() { $repo->set_config_value('user.name', GIT_USER_NAME); $repo->set_config_value('user.email', GIT_USER_EMAIL); - $client = new WP_Git_Client($repo); + self::$client = new WP_Git_Client($repo); // Only force pull at most once every 10 minutes $last_pull_time = get_transient('wp_git_last_pull_time'); if (!$last_pull_time) { - if (false === $client->force_pull()) { + if (false === self::$client->force_pull()) { _doing_it_wrong(__METHOD__, 'Failed to pull from remote repository', '1.0.0'); } set_transient('wp_git_last_pull_time', time(), 10 * MINUTE_IN_SECONDS); @@ -76,7 +79,7 @@ static private function get_fs() { [ 'root' => GIT_DIRECTORY_ROOT, 'auto_push' => true, - 'client' => $client, + 'client' => self::$client, ] ); if(!self::$fs->is_dir('/' . WP_AUTOSAVES_DIRECTORY)) { @@ -86,6 +89,53 @@ static private function get_fs() { return self::$fs; } + static public function menu_item_callback() { + // Get first post or create new one + $posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'posts_per_page' => 2, + 'orderby' => 'ID', + 'order' => 'ASC', + )); + + if (empty($posts)) { + try { + if(!self::acquire_synchronization_lock()) { + die('There are no local files yet and we could not acquire a synchronization lock to create one.'); + } + // Create a new draft post if none exists + $post_id = wp_insert_post(array( + 'post_title' => 'My first note', + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_status' => 'publish', + 'meta_input' => array( + 'local_file_path' => '/my-first-note.md', + ), + )); + } finally { + self::release_synchronization_lock(); + } + } else { + // Look for the first post that's not the default "my-first-note.md" + $post_id = null; + foreach ($posts as $post) { + $path = get_post_meta($post->ID, 'local_file_path', true); + if ($path !== '/my-first-note.md') { + $post_id = $post->ID; + break; + } + } + // Fallback to first post if no other found + if ($post_id === null) { + $post_id = $posts[0]->ID; + } + } + + $edit_url = admin_url('post.php?post=' . $post_id . '&action=edit'); + wp_redirect($edit_url); + exit; + } + static public function initialize() { // Register hooks register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); @@ -93,37 +143,25 @@ static public function initialize() { add_action('init', function() { self::get_fs(); self::register_post_type(); + + // Redirect menu page to custom route + global $pagenow; + if ($pagenow === 'admin.php' && isset($_GET['page']) && $_GET['page'] === 'static_files_editor') { + self::menu_item_callback(); + } }); // Register the admin page add_action('admin_menu', function() { - // Get first post or create new one - $posts = get_posts(array( - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'posts_per_page' => 1, - 'orderby' => 'ID', - 'order' => 'ASC' - )); - - if (empty($posts)) { - // Create a new draft post if none exists - $post_id = wp_insert_post(array( - 'post_title' => 'My first note', - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'post_status' => 'publish' - )); - } else { - $post_id = $posts[0]->ID; - } - - $edit_url = admin_url('post.php?post=' . $post_id . '&action=edit'); - add_menu_page( - 'Edit Local Files', - 'Edit Local Files', + 'Static Files Editor', + 'Static Files Editor', 'manage_options', - $edit_url, // Direct link to edit page - '', // No callback needed + 'static_files_editor', + function() { + // No callback needed, we're handling this in the init hook + // to redirect before any HTML is output. + }, 'dashicons-media-text', 30 ); @@ -185,15 +223,6 @@ static public function initialize() { }, )); - register_rest_route('static-files-editor/v1', '/create-directory', array( - 'methods' => 'POST', - 'callback' => array(self::class, 'create_directory_endpoint'), - 'permission_callback' => function() { - return current_user_can('edit_posts'); - }, - )); - - register_rest_route('static-files-editor/v1', '/move-file', array( 'methods' => 'POST', 'callback' => array(self::class, 'move_file_endpoint'), @@ -253,7 +282,7 @@ static public function initialize() { // Then refresh from file if needed $new_content = self::refresh_post_from_local_file($post); - if(!is_wp_error($new_content)) { + if(false !== $new_content && !is_wp_error($new_content)) { $content = $new_content; } return $content; @@ -367,7 +396,7 @@ static public function download_file_endpoint($request) { header('Cache-Control: no-cache'); // Stream file contents - if(!$fs->open_file_stream($path)) { + if(!$fs->open_read_stream($path)) { return new WP_Error('file_error', 'Could not read file'); } @@ -377,9 +406,9 @@ static public function download_file_endpoint($request) { echo $fs->get_file_chunk(); } - $fs->close_file_stream(); + $fs->close_read_stream(); - if(!$fs->get_error_message()) { + if(!$fs->get_last_error()) { exit; } @@ -412,20 +441,30 @@ static private function release_synchronization_lock() { static private function refresh_post_from_local_file($post) { try { if(!self::acquire_synchronization_lock()) { - return; + return false; } $post_id = $post->ID; $fs = self::get_fs(); $path = get_post_meta($post_id, 'local_file_path', true); if(!$fs->is_file($path)) { - _doing_it_wrong(__METHOD__, 'File not found: ' . $path, '1.0.0'); - return; + // @TODO: Log the error outside of this method. + // This happens naturally when the underlying file is deleted. + // It's annoying to keep seeing this error when developing + // the plugin so I'm commenting it out. + // + // Really, this may not even be an error. The caller must + // decide whether to log the error or handle the failure + // gracefully. + // + // This method only needs to bubble the error information up, + // e.g. by throwing, returning WP_Error, or setting self::$last_error. + return false; } $content = $fs->get_contents($path); if(!is_string($content)) { - _doing_it_wrong(__METHOD__, 'File not found: ' . $path, '1.0.0'); - return; + // @TODO: Ditto the previous comment. + return false; } $extension = pathinfo($path, PATHINFO_EXTENSION); switch($extension) { @@ -490,6 +529,7 @@ static private function convert_post_to_string($path, $post) { $converter->convert(); return $converter->get_result(); } + static public function get_local_files_tree($subdirectory = '') { $tree = []; $fs = self::get_fs(); @@ -527,6 +567,12 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path if($dir === '/' && $item === WP_AUTOSAVES_DIRECTORY) { continue; } + // Exclude the .gitkeep file from the files tree. + // WP_Git_Filesystem::mkdir() creates an empty .gitkeep file in each created + // directory since Git doesn't support empty directories. + if($item === '.gitkeep') { + continue; + } $path = $dir === '/' ? "/$item" : "$dir/$item"; @@ -558,6 +604,8 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path /** * Import static pages from a disk, if one exists. + * + * @TODO: Error handling */ static public function import_static_pages() { if ( ! is_dir( WP_STATIC_CONTENT_DIR ) ) { @@ -569,29 +617,32 @@ static public function import_static_pages() { } define('WP_IMPORTING', true); + // Make sure the post type is registered even if we're + // running before the init hook. self::register_post_type(); // Prevent ID conflicts self::reset_db_data(); + self::do_import_static_pages(); + } + + static private function do_import_static_pages($options = array()) { $importer = WP_Stream_Importer::create( - function () { + function () use ($options) { return new WP_Filesystem_Entity_Reader( self::get_fs(), - // new WP_Local_Filesystem(WP_STATIC_CONTENT_DIR) array( 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_tree_options' => $options['post_tree_options'] ?? array(), ) ); - }, - array(), - null + } ); $import_session = WP_Import_Session::create( - array( - 'data_source' => 'static_pages', - 'importer' => $importer, + array ( + 'data_source' => 'static_pages' ) ); @@ -658,7 +709,7 @@ static private function reset_db_data() { static public function get_or_create_post_for_file($request) { try { if(!self::acquire_synchronization_lock()) { - return; + return new WP_Error('synchronization_lock_failed', 'Failed to acquire synchronization lock'); } $file_path = $request->get_param('path'); @@ -728,154 +779,159 @@ static public function get_files_tree_endpoint() { return self::get_local_files_tree(); } - static public function create_directory_endpoint($request) { - $path = $request->get_param('path'); - if (!$path) { - return new WP_Error('missing_path', 'Directory path is required'); - } - $path = '/' . ltrim($path, '/'); - - $fs = self::get_fs(); - if (!$fs->mkdir($path)) { - return new WP_Error('mkdir_failed', 'Failed to create directory'); - } - - return array('success' => true); - } - static public function move_file_endpoint($request) { - $from_path = $request->get_param('fromPath'); - $to_path = $request->get_param('toPath'); - - if (!$from_path || !$to_path) { - return new WP_Error('missing_path', 'Both source and target paths are required'); - } - - // Find and update associated post - $existing_posts = get_posts(array( - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'meta_key' => 'local_file_path', - 'meta_value' => $from_path, - 'posts_per_page' => 1 - )); - - $fs = self::get_fs(); - if (!$fs->rename($from_path, $to_path)) { - return new WP_Error('move_failed', 'Failed to move file'); - } + try { + if(!self::acquire_synchronization_lock()) { + return; + } + $from_path = $request->get_param('fromPath'); + $to_path = $request->get_param('toPath'); + + if (!$from_path || !$to_path) { + return new WP_Error('missing_path', 'Both source and target paths are required'); + } - if (!empty($existing_posts)) { - update_post_meta($existing_posts[0]->ID, 'local_file_path', $to_path); - wp_update_post(array( - 'ID' => $existing_posts[0]->ID, - 'post_title' => basename($to_path) + // Find and update associated post + $existing_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $from_path, + 'posts_per_page' => 1 )); - } - return array('success' => true); - } + $fs = self::get_fs(); + if (!$fs->rename($from_path, $to_path)) { + return new WP_Error('move_failed', 'Failed to move file'); + } - static public function create_files_endpoint($request) { - $path = $request->get_param('path'); - $nodes_json = $request->get_param('nodes'); - - if(!$path) { - $path = '/'; - } + if (!empty($existing_posts)) { + update_post_meta($existing_posts[0]->ID, 'local_file_path', $to_path); + wp_update_post(array( + 'ID' => $existing_posts[0]->ID, + 'post_title' => basename($to_path) + )); + } - if (!$nodes_json) { - return new WP_Error('invalid_tree', 'Invalid file tree structure'); - } + // Pull new changes from the remote repository after + // performing a write operation. + self::$client->force_pull(); - $nodes = json_decode($nodes_json, true); - if (!$nodes) { - return new WP_Error('invalid_json', 'Invalid JSON structure'); + return array('success' => true); + } finally { + self::release_synchronization_lock(); } + } - $created_files = []; - + /** + * Imports files from the HTTP request into WordPress. + * + * This method: + * * Creates the uploaded files in the filesystem managed by this plugin. + * * Imports the uploaded files into WordPress as posts and attachments. + * + * @TODO: Rethink the attachments handling. Right now, we're creating two copies + * of each static asset. One in the managed filesystem (which could be a Git repo) + * and one in the WordPress uploads directory. Perhaps this is the way to go, + * but let's have a discussion about it. + */ + static public function create_files_endpoint($request) { try { - $fs = self::get_fs(); - foreach ($nodes as $node) { - $result = self::process_node($node, $path, $fs, $request); - if (is_wp_error($result)) { - return $result; - } - $created_files = array_merge($created_files, $result); + if(!self::acquire_synchronization_lock()) { + return; } + $uploaded_fs = WP_Uploaded_Directory_Tree_Filesystem::create($request, 'nodes'); - return array( - 'created_files' => $created_files - ); - } catch (Exception $e) { - return new WP_Error('creation_failed', $e->getMessage()); - } - } - - static private function process_node($node, $parent_path, $fs, $request) { - if (!isset($node['name']) || !isset($node['type'])) { - return new WP_Error('invalid_node', 'Invalid node structure'); - } + // Copy the uploaded files to the main filesystem + $main_fs = self::get_fs(); + $create_in_dir = wp_canonicalize_path($request->get_param('path')); + $uploaded_fs->copy('/', $create_in_dir, [ + 'recursive' => true, + 'to_fs' => $main_fs, + ]); - $path = rtrim($parent_path, '/') . '/' . ltrim($node['name'], '/'); - $created_files = []; - - if ($node['type'] === 'folder') { - if (!$fs->mkdir($path)) { - return new WP_Error('mkdir_failed', "Failed to create directory: $path"); - } - - if (!empty($node['children'])) { - foreach ($node['children'] as $child) { - $result = self::process_node($child, $path, $fs, $request); - if (is_wp_error($result)) { - return $result; - } - $created_files = array_merge($created_files, $result); - } - } - } else { - $content = ''; - if (isset($node['content']) && is_string($node['content']) && strpos($node['content'], '@file:') === 0) { - $file_key = substr($node['content'], 6); - $uploaded_file = $request->get_file_params()[$file_key] ?? null; - if ($uploaded_file && $uploaded_file['error'] === UPLOAD_ERR_OK) { - $content = file_get_contents($uploaded_file['tmp_name']); + // Import the uploaded files into WordPress + $parent_id = null; + if($create_in_dir) { + $parent_post = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $create_in_dir, + 'posts_per_page' => 1 + )); + if(!empty($parent_post)) { + $parent_id = $parent_post[0]->ID; } } - if (!$fs->put_contents($path, $content)) { - return new WP_Error('write_failed', "Failed to write file: $path"); - } - - /* - // @TODO: Should we create posts here? - // * We'll reindex the data later anyway and create those posts on demand. - // * ^ yes, but this means we don't have these posts in the database right after the upload. - // * But if we do create them, how do we know which files need a post, and which ones are - // images, videos, etc? - $post_data = array( - 'post_title' => basename($path), - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'post_status' => 'publish', - 'meta_input' => array( - 'local_file_path' => $path + $importer = WP_Stream_Importer::create( + function () use ($parent_id, $uploaded_fs) { + return new WP_Filesystem_Entity_Reader( + $uploaded_fs, + array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'post_tree_options' => array( + 'root_parent_id' => $parent_id, + 'create_index_pages' => false, + ), + ) + ); + }, + array( + 'attachment_downloader_options' => array( + 'source_from_filesystem' => $uploaded_fs, + ), + ) + ); + + $import_session = WP_Import_Session::create( + array ( + 'data_source' => 'static_pages' ) ); - $post_id = wp_insert_post($post_data); - if (is_wp_error($post_id)) { - return $post_id; + $result = data_liberation_import_step( $import_session, $importer ); + if(is_wp_error($result)) { + return $result; + } + + /** + * @TODO: A method such as $import_session->get_imported_entities() + * that iterates over the imported entities would be highly + * useful here. We don't have one, so we need the clunky + * inference below to get the imported posts. + */ + $created_files = []; + $visitor = new WP_Filesystem_Visitor($uploaded_fs); + while($visitor->next()) { + $event = $visitor->get_event(); + if(!$event->is_entering()) { + continue; + } + foreach($event->files as $file) { + $created_path = wp_join_paths($create_in_dir, $event->dir, $file); + $created_post = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $created_path, + 'posts_per_page' => 1 + )); + $created_files[] = array( + 'path' => $created_path, + 'post_id' => $created_post ? $created_post[0]->ID : null + ); + } } - */ - $created_files[] = array( - 'path' => $path, - // 'post_id' => $post_id + // Pull new changes from the remote repository after + // performing a write operation. + self::$client->force_pull(); + + return array( + 'created_files' => $created_files ); + } finally { + self::release_synchronization_lock(); } - - return $created_files; } static public function delete_path_endpoint($request) { @@ -884,31 +940,42 @@ static public function delete_path_endpoint($request) { return new WP_Error('missing_path', 'File path is required'); } - // Find and delete associated post - $existing_posts = get_posts(array( - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'meta_key' => 'local_file_path', - 'meta_value' => $path, - 'posts_per_page' => 1 - )); - - if (!empty($existing_posts)) { - wp_delete_post($existing_posts[0]->ID, true); - } + try { + if(!self::acquire_synchronization_lock()) { + return new WP_Error('synchronization_lock_failed', 'Failed to acquire synchronization lock'); + } + // Find and delete associated post + $existing_posts = get_posts(array( + 'post_type' => WP_LOCAL_FILE_POST_TYPE, + 'meta_key' => 'local_file_path', + 'meta_value' => $path, + 'posts_per_page' => 1 + )); - // Delete the actual file - $fs = self::get_fs(); - if($fs->is_dir($path)) { - if (!$fs->rmdir($path, ['recursive' => true])) { - return new WP_Error('delete_failed', 'Failed to delete directory'); + if (!empty($existing_posts)) { + wp_delete_post($existing_posts[0]->ID, true); } - } else { - if (!$fs->rm($path)) { - return new WP_Error('delete_failed', 'Failed to delete file'); + + // Delete the actual file + $fs = self::get_fs(); + if($fs->is_dir($path)) { + if (!$fs->rmdir($path, ['recursive' => true])) { + return new WP_Error('delete_failed', 'Failed to delete directory'); + } + } else { + if (!$fs->rm($path)) { + return new WP_Error('delete_failed', 'Failed to delete file'); + } } - } - return array('success' => true); + // Pull new changes from the remote repository after + // performing a write operation. + self::$client->force_pull(); + + return array('success' => true); + } finally { + self::release_synchronization_lock(); + } } } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 6579de32e3..dbde7e7f65 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -5,6 +5,7 @@ import { useState, createContext, useContext, + createRoot, } from '@wordpress/element'; import { __experimentalTreeGrid as TreeGrid, @@ -82,7 +83,7 @@ type DragState = { path: string; hoverPath: string | null; hoverType: 'file' | 'folder' | null; - isExternal?: boolean; + isExternal: boolean; }; type FilePickerContextType = { @@ -113,6 +114,34 @@ type FilePickerContextType = { const FilePickerContext = createContext(null); +function createDragImage(node: FileNode): HTMLElement { + const dragImage = ReactElementToHTML(); + dragImage.style.cssText = ` + position: fixed; + top: -1000px; + left: -1000px; + padding: 6px 12px; + background: white; + border-radius: 2px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + display: flex; + align-items: center; + justify-content: center; + font-family: -apple-system, system-ui, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-size: 13px; + white-space: nowrap; + `; + document.body.appendChild(dragImage); + return dragImage; +} + +function ReactElementToHTML(element: React.ReactElement): HTMLElement { + const container = document.createElement('div'); + const root = createRoot(container); + root.render(element); + return container; +} + export const FilePickerTree: React.FC = ({ isLoading = false, error = undefined, @@ -256,13 +285,25 @@ export const FilePickerTree: React.FC = ({ path: string, type: 'file' | 'folder' ) => { - onDragStart(e, path, type); + e.stopPropagation(); + const dragImage = createDragImage({ + name: path.split('/').pop() || '', + type: type, + }); + e.dataTransfer.setDragImage(dragImage, 10, 10); + + // Clean up the drag image element after a short delay + setTimeout(() => { + document.body.removeChild(dragImage); + }, 0); + setDragState({ path, hoverPath: null, hoverType: null, - isExternal: false, }); + + onDragStart?.(e, path, type); }; const isDescendantPath = (parentPath: string, childPath: string) => { @@ -281,9 +322,9 @@ export const FilePickerTree: React.FC = ({ e.dataTransfer.dropEffect = 'copy'; setDragState({ path: '', + isExternal: true, hoverPath: path, hoverType: type, - isExternal: true, }); return; } @@ -316,19 +357,51 @@ export const FilePickerTree: React.FC = ({ return; } e.stopPropagation(); - // Handle file/directory upload + + // Internal drag&drop within the FilePickerTree + if (dragState && !dragState.isExternal) { + // Prevent dropping a folder into its own descendant + if ( + dragState.path && + targetType === 'folder' && + isDescendantPath(dragState.path, targetPath) + ) { + return; + } + + const fromPath = dragState.path.replace(/^\/+/, ''); + + const targetParentPath = + targetType === 'file' + ? targetPath.split('/').slice(0, -1).join('/') + : targetPath; + + const toPath = [targetParentPath, dragState.path.split('/').pop()] + .join('/') + .replace(/^\/+/, ''); + + setDragState(null); + + if (fromPath === toPath) { + return; + } + + onNodeMoved({ + fromPath, + toPath, + }); + return; + } + + // Drag&Drop from desktop into the FilePickerTree if (e.dataTransfer.items.length > 0) { const targetFolder = targetType === 'folder' ? targetPath : targetPath.split('/').slice(0, -1).join('/'); - const items = Array.from(e.dataTransfer.items).filter( - (item) => item.kind !== 'DownloadURL' - ); - + const items = Array.from(e.dataTransfer.items); const buildTree = async ( - entry: FileSystemEntry, - parentPath: string = '' + entry: FileSystemEntry ): Promise => { if (entry.isFile) { const fileEntry = entry as FileSystemFileEntry; @@ -376,39 +449,6 @@ export const FilePickerTree: React.FC = ({ setDragState(null); return; } - - if (dragState) { - // Prevent dropping a folder into its own descendant - if ( - dragState.path && - targetType === 'folder' && - isDescendantPath(dragState.path, targetPath) - ) { - return; - } - - const fromPath = dragState.path.replace(/^\/+/, ''); - - const targetParentPath = - targetType === 'file' - ? targetPath.split('/').slice(0, -1).join('/') - : targetPath; - - const toPath = [targetParentPath, dragState.path.split('/').pop()] - .join('/') - .replace(/^\/+/, ''); - - setDragState(null); - - if (fromPath === toPath) { - return; - } - - onNodeMoved({ - fromPath, - toPath, - }); - } }; const handleDragEnd = () => { @@ -740,7 +780,7 @@ const NodeRow: React.FC<{ position: 'relative', }} > - = ({ node, level, isOpen }) => { +}> = ({ node, level, isOpen = false }) => { const indent: string[] = []; for (let i = 0; i < level; i++) { indent.push('    '); @@ -893,6 +933,16 @@ const FileName: React.FC<{ ) : (
 
)} + + + ); +}; + +const FileName: React.FC<{ + node: FileNode; +}> = ({ node }) => { + return ( + <> {node.name} diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 7b7a392423..8429f50d7d 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -62,9 +62,15 @@ const uiStore = createReduxStore(STORE_NAME, { register(uiStore); -const isStaticPagePath = (path: string) => { - const extension = path.split('.').pop()?.toLowerCase(); - return ['md', 'html'].includes(extension); +const isStaticAssetPath = (path: string) => { + let extension = undefined; + const lastDot = path.lastIndexOf('.'); + if (lastDot !== -1) { + extension = path.substring(lastDot + 1).toLowerCase(); + } + // We treat every extension except of the well-known ones + // as a static asset. + return extension && !['md', 'html', 'xhtml'].includes(extension); }; function ConnectedFilePickerTree() { @@ -79,7 +85,16 @@ function ConnectedFilePickerTree() { useEffect(() => { async function refreshPostId() { - if (isStaticPagePath(selectedPath)) { + console.log( + 'refreshPostId', + selectedPath, + isStaticAssetPath(selectedPath) + ); + if (isStaticAssetPath(selectedPath)) { + setPostLoading(false); + setSelectedPostId(null); + setPreviewPath(selectedPath); + } else { setPostLoading(true); if (!selectedPostId) { const { post_id } = (await apiFetch({ @@ -90,10 +105,6 @@ function ConnectedFilePickerTree() { setSelectedPostId(post_id); } setPreviewPath(null); - } else { - setPostLoading(false); - setSelectedPostId(null); - setPreviewPath(selectedPath); } } refreshPostId(); @@ -180,7 +191,7 @@ function ConnectedFilePickerTree() { const handleFileClick = async (filePath: string, node: FileNode) => { setSelectedPath(filePath); - if (isStaticPagePath(filePath)) { + if (node.post_id && !isStaticAssetPath(filePath)) { setSelectedPostId(node.post_id); } else { setSelectedPostId(null); @@ -220,7 +231,10 @@ function ConnectedFilePickerTree() { await refreshFileTree(); - if (response.created_files.length > 0) { + if ( + response.created_files.length === 1 && + response.created_files[0].post_type === WP_LOCAL_FILE_POST_TYPE + ) { onNavigateToEntityRecord({ postId: response.created_files[0].post_id, postType: WP_LOCAL_FILE_POST_TYPE, @@ -266,10 +280,16 @@ function ConnectedFilePickerTree() { if (type === 'file') { const url = `${window.wpApiSettings.root}static-files-editor/v1/download-file?path=${path}&_wpnonce=${window.wpApiSettings.nonce}`; const filename = path.split('/').pop(); + // For dragging & dropping to desktop e.dataTransfer.setData( 'DownloadURL', `text/plain:${filename}:${url}` ); + // For dragging & dropping into the editor canvas + e.dataTransfer.setData( + 'text/html', + `${filename}` + ); } }; diff --git a/packages/playground/data-liberation/blueprints-library b/packages/playground/data-liberation/blueprints-library index 8bc9dc3b00..5840909e4c 160000 --- a/packages/playground/data-liberation/blueprints-library +++ b/packages/playground/data-liberation/blueprints-library @@ -1 +1 @@ -Subproject commit 8bc9dc3b00a0018dcd7f453b7429b00179e422c0 +Subproject commit 5840909e4cc0a144914f59c2e406ac2e5d891f49 diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index caff2097ce..9aac3947c4 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -17,6 +17,8 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_SQLite_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_File_Visitor_Event.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Visitor.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Chroot.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Uploaded_Directory_Tree_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_Byte_Reader.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_File_Reader.php'; diff --git a/packages/playground/data-liberation/plugin.php b/packages/playground/data-liberation/plugin.php index 8c319005c3..36aecbdd20 100644 --- a/packages/playground/data-liberation/plugin.php +++ b/packages/playground/data-liberation/plugin.php @@ -352,7 +352,7 @@ function data_liberation_import_step( $session, $importer = null ) { $importer = data_liberation_create_importer( $metadata ); } if ( ! $importer ) { - return; + return new WP_Error('import_failed', 'Failed to create importer'); } /** @@ -372,10 +372,10 @@ function data_liberation_import_step( $session, $importer = null ) { // frontloading stage. if ( $importer->get_stage() === WP_Stream_Importer::STAGE_FRONTLOAD_ASSETS ) { if ( $fetched_files > 0 ) { - break; + return new WP_Error('import_failed', 'Frontloading failed'); } } else { - break; + return new WP_Error('import_failed', 'Frontloading failed'); } } if ( $time_taken >= $hard_time_limit_seconds ) { @@ -383,10 +383,11 @@ function data_liberation_import_step( $session, $importer = null ) { // @TODO: Make it easily configurable // @TODO: Bump the number of download attempts for the placeholders, // set the status to `error` in each interrupted download. - break; + return new WP_Error('import_failed', 'Time limit exceeded'); } - if ( true !== $importer->next_step() ) { + if ( ! $importer->next_step() ) { + // Time to advance to the next stage. $session->set_reentrancy_cursor( $importer->get_reentrancy_cursor() ); $should_advance_to_next_stage = null !== $importer->get_next_stage(); @@ -394,11 +395,12 @@ function data_liberation_import_step( $session, $importer = null ) { if ( WP_Stream_Importer::STAGE_FRONTLOAD_ASSETS === $importer->get_stage() ) { $resolved_all_failures = $session->count_unfinished_frontloading_placeholders() === 0; if ( ! $resolved_all_failures ) { - break; + return new WP_Error('import_failed', 'Downloads failed'); } } } if ( ! $importer->advance_to_next_stage() ) { + // We're done. break; } $session->set_stage( $importer->get_stage() ); @@ -409,7 +411,9 @@ function data_liberation_import_step( $session, $importer = null ) { switch ( $importer->get_stage() ) { case WP_Stream_Importer::STAGE_INDEX_ENTITIES: // Bump the total number of entities to import. - $session->create_frontloading_placeholders( $importer->get_indexed_assets_urls() ); + $session->create_frontloading_placeholders( + $importer->get_indexed_assets_urls() + ); $session->bump_total_number_of_entities( $importer->get_indexed_entities_counts() ); diff --git a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php index c978454135..4fe8860e16 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php @@ -14,12 +14,18 @@ public function __construct( $filesystem, $options = array() ) { $this->post_type = $options['post_type'] ?? 'page'; $this->post_tree = WP_Filesystem_To_Post_Tree::create( $this->filesystem, - array ( - 'first_post_id' => 2, - 'filter_pattern' => '#\.(?:md|html|xhtml)$#', - 'index_file_pattern' => '#^index\.[a-z]+$#', + array_merge( + array( + 'root_parent_id' => null, + 'filter_pattern' => '#\.(?:md|html|xhtml|png|jpg|jpeg|gif|svg|webp|mp4)$#', + 'index_file_pattern' => '#^index\.[a-z]+$#', + ), + $options['post_tree_options'] ?? array() ) ); + if(false === $this->post_tree) { + return false; + } } public function get_last_error(): ?string { @@ -50,22 +56,51 @@ public function next_entity(): bool { $source_content_converter = null; $post_tree_node = $this->post_tree->get_current_node(); if($post_tree_node['type'] === 'file') { - $content = $this->filesystem->get_contents($post_tree_node['local_file_path']); $extension = pathinfo($post_tree_node['local_file_path'], PATHINFO_EXTENSION); switch($extension) { case 'md': + $content = $this->filesystem->get_contents($post_tree_node['local_file_path']); $converter = new WP_Markdown_To_Blocks( $content ); $source_content_converter = 'md'; break; case 'xhtml': + $content = $this->filesystem->get_contents($post_tree_node['local_file_path']); $converter = new WP_HTML_To_Blocks( WP_XML_Processor::create_from_string( $content ) ); $source_content_converter = 'xhtml'; break; case 'html': - default: + $content = $this->filesystem->get_contents($post_tree_node['local_file_path']); $converter = new WP_HTML_To_Blocks( WP_HTML_Processor::create_fragment( $content ) ); $source_content_converter = 'html'; break; + default: + $filetype = 'application/octet-stream'; + if(function_exists('wp_check_filetype')) { + $filetype = wp_check_filetype(basename($post_tree_node['local_file_path']), null); + if(isset($filetype['type'])) { + $filetype = $filetype['type']; + } + } + $this->entities[] = new WP_Imported_Entity( + 'post', + array( + 'post_id' => $post_tree_node['post_id'], + 'post_title' => sanitize_file_name(basename($post_tree_node['local_file_path'])), + 'post_status' => 'inherit', + 'post_content' => '', + 'post_mime_type' => $filetype, + 'post_type' => 'attachment', + 'post_parent' => $post_tree_node['parent_id'], + 'guid' => $post_tree_node['local_file_path'], + // The importer will use the same Filesystem instance to + // source the attachment. + 'attachment_url' => 'file://' . $post_tree_node['local_file_path'], + ) + ); + // We're done emiting the entity. + // wp_generate_attachment_metadata() et al. will be called by the + // importer at the database insertion step. + continue 2; } if( false === $converter->convert() ) { diff --git a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_To_Post_Tree.php b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_To_Post_Tree.php index dde52c8671..930cb3c601 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_To_Post_Tree.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_To_Post_Tree.php @@ -3,6 +3,7 @@ /** */ class WP_Filesystem_To_Post_Tree { + private $fs; private $file_visitor; private $current_node; @@ -23,8 +24,19 @@ public static function create( $options ) { if ( ! isset( $options['first_post_id'] ) ) { - _doing_it_wrong( __FUNCTION__, 'Missing required options: first_post_id', '1.0.0' ); - return false; + $options['first_post_id'] = 2; + if(function_exists('get_posts')) { + $max_id = get_posts([ + 'post_type' => 'any', + 'posts_per_page' => 1, + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'DESC', + ]); + if(!empty($max_id)) { + $options['first_post_id'] = $max_id[0] + 1; + } + } } if ( 1 === $options['first_post_id'] ) { _doing_it_wrong( __FUNCTION__, 'First node ID must be greater than 1', '1.0.0' ); @@ -45,11 +57,15 @@ private function __construct( \WordPress\Filesystem\WP_Abstract_Filesystem $filesystem, $options ) { + $this->fs = $filesystem; $this->file_visitor = new WordPress\Filesystem\WP_Filesystem_Visitor( $filesystem ); $this->create_index_pages = $options['create_index_pages'] ?? true; $this->next_post_id = $options['first_post_id']; $this->filter_pattern = $options['filter_pattern']; $this->index_file_pattern = $options['index_file_pattern']; + if(isset($options['root_parent_id'])) { + $this->parent_ids[-1] = $options['root_parent_id']; + } } public function get_current_node() { @@ -66,7 +82,6 @@ public function next_node() { $dir = $this->file_visitor->get_event()->dir; $depth = $this->file_visitor->get_current_depth(); $parent_id = $this->parent_ids[ $depth - 1 ] ?? null; - if ( null === $parent_id && $depth > 1 ) { // There's no parent ID even though we're a few levels deep. // This is a scenario where `next_file()` skipped a few levels @@ -125,7 +140,6 @@ public function next_node() { } return true; } - while ( count( $this->pending_files ) ) { $parent_id = $this->parent_ids[ $this->file_visitor->get_current_depth() ] ?? null; $file_path = array_shift( $this->pending_files ); @@ -177,7 +191,20 @@ private function next_file() { foreach ( $event->files as $filename ) { $abs_paths[] = wp_join_paths( $event->dir, $filename ); } - $this->pending_files = $this->choose_relevant_files( $abs_paths ); + $this->pending_files = []; + foreach($abs_paths as $path) { + // Add all the subdirectory into the pending files list – there's + // a chance the directory wouldn't match the filter pattern, but + // a descendant file might. + if($this->fs->is_dir($path)) { + $this->pending_files[] = $path; + } + + // Only add the files that match the filter pattern. + if ( $this->fs->is_file($path) && preg_match($this->filter_pattern, $path) ) { + $this->pending_files[] = $path; + } + } if ( ! count( $this->pending_files ) ) { // Only consider directories with relevant files in them. // Otherwise we'll create fake pages for media directories @@ -226,13 +253,4 @@ protected function looks_like_directory_index( $path ) { return preg_match( $this->index_file_pattern, basename( $path ) ); } - protected function choose_relevant_files( $paths ) { - $filtered_paths = array(); - foreach ( $paths as $path ) { - if ( preg_match( $this->filter_pattern, $path ) ) { - $filtered_paths[] = $path; - } - } - return $filtered_paths; - } } diff --git a/packages/playground/data-liberation/src/functions.php b/packages/playground/data-liberation/src/functions.php index 55feb69709..5aa45997b7 100644 --- a/packages/playground/data-liberation/src/functions.php +++ b/packages/playground/data-liberation/src/functions.php @@ -130,69 +130,6 @@ function urldecode_n( $input, $decode_n ) { return $result; } -/** - * A generator that recursively list files in a directory. - * - * Example: - * - * ```php - * foreach(wp_list_files_recursive('./docs') as $event) { - * - * echo $event->type . " " . ($event->isFile ? 'file' : 'directory') . ' ' . $event->path . "\n"; - * } - * // Output: - * // entering directory ./docs - * // listing file ./docs/file1.txt - * // listing file ./docs/file2.txt - * // entering directory ./docs/subdir - * // listing file ./docs/subdir/file3.txt - * // exiting directory ./docs/subdir - * // exiting directory ./docs - * ``` - * - * @param string $dir - * @param integer $depth - * @yield WP_File_Visitor_Event - * @return Iterator - */ -function wp_visit_file_tree( $dir ) { - $directories = array(); - $files = array(); - $dh = opendir( $dir ); - while ( true ) { - $file = readdir( $dh ); - if ( $file === false ) { - break; - } - if ( '.' === $file || '..' === $file ) { - continue; - } - $file_path = $dir . '/' . $file; - if ( is_dir( $file_path ) ) { - $directories[] = $file_path; - continue; - } - - $files[] = new SplFileInfo( $file_path ); - } - closedir( $dh ); - - yield new WP_File_Visitor_Event( - WP_File_Visitor_Event::EVENT_ENTER, - new SplFileInfo( $dir ), - $files - ); - - foreach ( $directories as $directory ) { - yield from wp_visit_file_tree( $directory ); - } - - yield new WP_File_Visitor_Event( - WP_File_Visitor_Event::EVENT_EXIT, - new SplFileInfo( $dir ) - ); -} - /** * Import a WXR file. Used by the CLI. * @@ -301,3 +238,31 @@ function wp_join_paths() { return preg_replace( '#/+#', '/', $path ); } + +function wp_canonicalize_path($path) { + // Convert to absolute path + if (!str_starts_with($path, '/')) { + $path = '/' . $path; + } + + // Resolve . and .. + $parts = explode('/', $path); + $normalized = []; + foreach ($parts as $part) { + if ($part === '.' || $part === '') { + continue; + } + if ($part === '..') { + array_pop($normalized); + continue; + } + $normalized[] = $part; + } + + // Reconstruct path + $result = '/' . implode('/', $normalized); + if($result === '/.') { + $result = '/'; + } + return $result === '' ? '/' : $result; +} diff --git a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php index c8b468fc9b..a4a66e46f8 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Filesystem.php @@ -13,6 +13,7 @@ class WP_Git_Filesystem extends WP_Abstract_Filesystem { private $root; private $auto_push; private $client; + private $write_stream; public function __construct( WP_Git_Repository $repo, @@ -52,7 +53,7 @@ public function is_file($path) { return WP_Git_Pack_Processor::OBJECT_TYPE_BLOB === $this->repo->get_type(); } - public function open_file_stream($path) { + public function open_read_stream($path) { return $this->repo->read_by_path($path); } @@ -64,13 +65,13 @@ public function get_file_chunk() { return $this->repo->get_body_chunk(); } - public function get_error_message() { + public function get_last_error() { // @TODO: Manage our own errors in addition to passing // through the underlying repo's errors. return $this->repo->get_last_error(); } - public function close_file_stream() { + public function close_read_stream() { // @TODO: Implement this } @@ -121,8 +122,12 @@ public function rename($old_path, $new_path) { } public function mkdir($path) { - // Git doesn't support empty directories, let's not do anything. - return true; + // Git doesn't support empty directories so we must create an empty file. + return $this->commit([ + 'updates' => [ + $this->resolve_path($path) . '/.gitkeep' => '', + ], + ]); } public function rm($path) { @@ -157,17 +162,49 @@ public function rmdir($path, $options = []) { ); } - public function put_contents($path, $data, $options=[]) { - return $this->commit( - [ - 'updates' => [ - $this->resolve_path($path) => $data, - ], - 'amend' => isset($options['amend']) && $options['amend'], - 'message' => isset($options['message']) ? $options['message'] : null, - ] - ); - } + public function open_write_stream($path) { + if($this->write_stream) { + _doing_it_wrong(__METHOD__, 'Cannot open a new write stream while another write stream is open.', '1.0.0'); + return false; + } + $temp_file = tempnam(sys_get_temp_dir(), 'git_write_stream'); + if(false === $temp_file) { + return false; + } + $this->write_stream = [ + 'repo_path' => $this->resolve_path($path), + 'local_path' => $temp_file, + 'fp' => fopen($temp_file, 'wb'), + ]; + return true; + } + + public function append_bytes($data) { + if(!$this->write_stream) { + return false; + } + fwrite($this->write_stream['fp'], $data); + return true; + } + + public function close_write_stream($options = []) { + if(!$this->write_stream) { + return false; + } + fclose($this->write_stream['fp']); + $repo_path = $this->write_stream['repo_path']; + $local_path = $this->write_stream['local_path']; + unset($this->write_stream); + // Flush changes to the repo + return $this->commit([ + 'updates' => [ + // @TODO: Stream instead of file_get_contents + $repo_path => file_get_contents($local_path), + ], + 'amend' => isset($options['amend']) && $options['amend'], + 'message' => isset($options['message']) ? $options['message'] : null, + ]); + } private function commit($options) { if(false === $this->repo->commit($options)) { diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 305e80194e..d9e1150f6d 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -280,7 +280,7 @@ private function open_object_stream() { $this->last_error = 'Failed to initialize inflate handle'; return false; } - if(!$this->fs->open_file_stream($this->get_object_path($this->oid))) { + if(!$this->fs->open_read_stream($this->get_object_path($this->oid))) { return false; } return true; @@ -292,7 +292,7 @@ public function next_body_chunk() { return true; } if(false === $this->fs->next_file_chunk()) { - $this->last_error = $this->fs->get_error_message(); + $this->last_error = $this->fs->get_last_error(); return false; } $chunk = $this->fs->get_file_chunk(); @@ -314,7 +314,7 @@ public function get_body_chunk() { } private function close_object_stream() { - $this->fs->close_file_stream(); + $this->fs->close_read_stream(); $this->content_inflate_handle = null; return true; } diff --git a/packages/playground/data-liberation/src/import/WP_Attachment_Downloader.php b/packages/playground/data-liberation/src/import/WP_Attachment_Downloader.php index f16306275c..810c4b9509 100644 --- a/packages/playground/data-liberation/src/import/WP_Attachment_Downloader.php +++ b/packages/playground/data-liberation/src/import/WP_Attachment_Downloader.php @@ -8,15 +8,17 @@ class WP_Attachment_Downloader { private $fps = array(); private $output_root; private $output_paths = array(); + private $source_from_filesystem; private $current_event; private $pending_events = array(); private $enqueued_url; private $progress = array(); - public function __construct( $output_root ) { + public function __construct( $output_root, $options = array() ) { $this->client = new Client(); $this->output_root = $output_root; + $this->source_from_filesystem = $options['source_from_filesystem'] ?? null; } public function get_progress() { @@ -38,15 +40,15 @@ public function has_pending_requests() { public function enqueue_if_not_exists( $url, $output_relative_path ) { $this->enqueued_url = $url; - $output_relative_path = $this->output_root . '/' . ltrim( $output_relative_path, '/' ); - if ( file_exists( $output_relative_path ) ) { + $output_path = wp_join_paths($this->output_root, $output_relative_path); + if ( file_exists( $output_path ) ) { $this->pending_events[] = new WP_Attachment_Downloader_Event( $this->enqueued_url, WP_Attachment_Downloader_Event::ALREADY_EXISTS ); return true; } - if ( file_exists( $output_relative_path . '.partial' ) ) { + if ( file_exists( $output_path . '.partial' ) ) { $this->pending_events[] = new WP_Attachment_Downloader_Event( $this->enqueued_url, WP_Attachment_Downloader_Event::IN_PROGRESS @@ -54,7 +56,7 @@ public function enqueue_if_not_exists( $url, $output_relative_path ) { return true; } - $output_dir = dirname( $output_relative_path ); + $output_dir = dirname( $output_path ); if ( ! file_exists( $output_dir ) ) { // @TODO: think through the chmod of the created directory. mkdir( $output_dir, 0777, true ); @@ -67,15 +69,32 @@ public function enqueue_if_not_exists( $url, $output_relative_path ) { switch ( $protocol ) { case 'file': - $local_path = parse_url( $url, PHP_URL_PATH ); - if ( false === $local_path ) { + if(!$this->source_from_filesystem) { + _doing_it_wrong( __METHOD__, 'Cannot process file:// URLs without a source filesystem instance. Use the source_from_filesystem option to pass in a filesystem instance to WP_Attachment_Downloader.', '1.0' ); + return false; + } + $source_path = parse_url( $url, PHP_URL_PATH ); + if ( false === $source_path ) { return false; } // Just copy the file over. // @TODO: think through the chmod of the created file. - - $success = copy( $local_path, $output_relative_path ); + $success = $this->source_from_filesystem->open_read_stream($source_path); + if($success) { + $fp = fopen($output_path, 'wb'); + // @TODO: Filesystem instance error handling. + while($this->source_from_filesystem->next_file_chunk()) { + $chunk = $this->source_from_filesystem->get_file_chunk(); + fwrite($fp, $chunk); + } + $this->source_from_filesystem->close_read_stream(); + fclose($fp); + if($this->source_from_filesystem->get_last_error()) { + $success = false; + } + } + $this->pending_events[] = $success ? new WP_Attachment_Downloader_Event( $this->enqueued_url, @@ -90,7 +109,7 @@ public function enqueue_if_not_exists( $url, $output_relative_path ) { case 'http': case 'https': // Create a placeholder file to indicate that the download is in progress. - touch( $output_relative_path . '.partial' ); + touch( $output_path . '.partial' ); $request = new Request( $url ); $this->output_paths[ $request->id ] = $output_relative_path; $this->progress[ $this->enqueued_url ] = array( diff --git a/packages/playground/data-liberation/src/import/WP_Entity_Importer.php b/packages/playground/data-liberation/src/import/WP_Entity_Importer.php index 2a399b06a4..23fd1f1756 100644 --- a/packages/playground/data-liberation/src/import/WP_Entity_Importer.php +++ b/packages/playground/data-liberation/src/import/WP_Entity_Importer.php @@ -77,7 +77,6 @@ class=[\'"].*?\b(wp-image-\d+|attachment-[\w\-]+)\b * @var bool $prefill_existing_comments Should we prefill `comment_exists` calls? (True prefills and uses more memory, false checks once per imported comment and takes longer. Default is true.) * @var bool $prefill_existing_terms Should we prefill `term_exists` calls? (True prefills and uses more memory, false checks once per imported term and takes longer. Default is true.) * @var bool $update_attachment_guids Should attachment GUIDs be updated to the new URL? (True updates the GUID, which keeps compatibility with v1, false doesn't update, and allows deduplication and reimporting. Default is false.) - * @var bool $fetch_attachments Fetch attachments from the remote server. (True fetches and creates attachment posts, false skips attachments. Default is false.) * @var int $default_author User ID to use if author is missing or invalid. (Default is null, which leaves posts unassigned.) * } */ @@ -104,7 +103,6 @@ public function __construct( $options = array() ) { 'prefill_existing_comments' => true, 'prefill_existing_terms' => true, 'update_attachment_guids' => false, - 'fetch_attachments' => false, 'default_author' => null, ) ); @@ -541,6 +539,7 @@ public function import_post( $data ) { 'menu_order' => true, 'post_type' => true, 'post_password' => true, + 'local_file_path' => true, ); foreach ( $data as $key => $value ) { if ( ! isset( $allowed[ $key ] ) ) { @@ -553,27 +552,7 @@ public function import_post( $data ) { $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $data ); if ( isset( $postdata['post_type'] ) && 'attachment' === $postdata['post_type'] ) { - // @TODO: Do not download any attachments here. We're just inserting the data - // at this point. All the downloads have already been processed by now. - if ( ! $this->options['fetch_attachments'] ) { - $this->logger->notice( - sprintf( - /* translators: %s: post title */ - __( 'Skipping attachment "%s", fetching attachments disabled' ), - $data['post_title'] - ) - ); - /** - * Post processing skipped. - * - * @param array $data Raw data imported for the post. - * @param array $meta Raw meta data, already processed by {@see process_post_meta}. - */ - do_action( 'wxr_importer_process_skipped_post', $data ); - return false; - } - $remote_url = ! empty( $data['attachment_url'] ) ? $data['attachment_url'] : $data['guid']; - $post_id = $this->process_attachment( $postdata, $meta, $remote_url ); + $post_id = $this->process_attachment( $postdata, $meta ); } else { $post_id = wp_insert_post( $postdata, true ); do_action( 'wp_import_insert_post', $post_id, $original_id, $postdata, $data ); @@ -756,7 +735,11 @@ protected function process_menu_item_meta( $post_id, $data, $meta ) { * @param string $url URL to fetch attachment from * @return int|WP_Error Post ID on success, WP_Error otherwise */ - protected function process_attachment( $post, $meta, $remote_url ) { + protected function process_attachment( $post, $meta ) { + if ( ! isset( $post['local_file_path'] ) || ! file_exists( $post['local_file_path'] ) ) { + return new WP_Error( 'attachment_processing_error', __( 'File does not exist', 'wordpress-importer' ) ); + } + // try to use _wp_attached file for upload folder placement to ensure the same location as the export site // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload() $post['upload_date'] = $post['post_date']; @@ -771,46 +754,25 @@ protected function process_attachment( $post, $meta, $remote_url ) { break; } - // if the URL is absolute, but does not contain address, then upload it assuming base_site_url - if ( preg_match( '|^/[\w\W]+$|', $remote_url ) ) { - $remote_url = rtrim( $this->base_url, '/' ) . $remote_url; - } - - $upload = $this->fetch_remote_file( $remote_url, $post ); - if ( is_wp_error( $upload ) ) { - return $upload; - } - - $info = wp_check_filetype( $upload['file'] ); + $info = wp_check_filetype( $post['local_file_path'] ); if ( ! $info ) { return new WP_Error( 'attachment_processing_error', __( 'Invalid file type', 'wordpress-importer' ) ); } $post['post_mime_type'] = $info['type']; - // WP really likes using the GUID for display. Allow updating it. - // See https://core.trac.wordpress.org/ticket/33386 - if ( $this->options['update_attachment_guids'] ) { - $post['guid'] = $upload['url']; - } - // as per wp-admin/includes/upload.php - $post_id = wp_insert_attachment( $post, $upload['file'] ); + $post_id = wp_insert_attachment( $post, $post['local_file_path'] ); if ( is_wp_error( $post_id ) ) { return $post_id; } - $attachment_metadata = wp_generate_attachment_metadata( $post_id, $upload['file'] ); - wp_update_attachment_metadata( $post_id, $attachment_metadata ); + if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { + include( ABSPATH . 'wp-admin/includes/image.php' ); + } - // Map this image URL later if we need to - $this->url_remap[ $remote_url ] = $upload['url']; - - // If we have a HTTPS URL, ensure the HTTP URL gets replaced too - if ( substr( $remote_url, 0, 8 ) === 'https://' ) { - $insecure_url = 'http' . substr( $remote_url, 5 ); - $this->url_remap[ $insecure_url ] = $upload['url']; - } + $attachment_metadata = wp_generate_attachment_metadata( $post_id, $post['local_file_path'] ); + wp_update_attachment_metadata( $post_id, $attachment_metadata ); return $post_id; } @@ -820,6 +782,7 @@ protected function process_attachment( $post, $meta, $remote_url ) { * @TODO: Explore other interfaces for attachment import. */ public function import_attachment( $filepath, $post_id ) { + $filename = basename( $filepath ); // Check if attachment with this guid already exists $existing_attachment = get_posts( diff --git a/packages/playground/data-liberation/src/import/WP_File_Visitor.php b/packages/playground/data-liberation/src/import/WP_File_Visitor.php deleted file mode 100644 index 31f8d6564f..0000000000 --- a/packages/playground/data-liberation/src/import/WP_File_Visitor.php +++ /dev/null @@ -1,96 +0,0 @@ -dir = $dir; - $this->iterator_stack[] = $this->create_iterator( $dir ); - } - - public function get_current_depth() { - return $this->depth; - } - - public function get_root_dir() { - return $this->dir; - } - - private function create_iterator( $dir ) { - $this->directories = array(); - $this->files = array(); - - $dh = opendir( $dir ); - if ( $dh === false ) { - return new ArrayIterator( array() ); - } - - while ( true ) { - $file = readdir( $dh ); - if ( $file === false ) { - break; - } - if ( '.' === $file || '..' === $file ) { - continue; - } - $file_path = $dir . '/' . $file; - if ( is_dir( $file_path ) ) { - $this->directories[] = $file_path; - continue; - } - $this->files[] = new SplFileInfo( $file_path ); - } - closedir( $dh ); - - $events = array( - new WP_File_Visitor_Event( WP_File_Visitor_Event::EVENT_ENTER, new SplFileInfo( $dir ), $this->files ), - ); - - foreach ( $this->directories as $directory ) { - $events[] = $directory; // Placeholder for recursion - } - - $events[] = new WP_File_Visitor_Event( WP_File_Visitor_Event::EVENT_EXIT, new SplFileInfo( $dir ) ); - - return new ArrayIterator( $events ); - } - - public function next() { - while ( ! empty( $this->iterator_stack ) ) { - $this->current_iterator = end( $this->iterator_stack ); - - if ( $this->current_iterator->valid() ) { - $current = $this->current_iterator->current(); - $this->current_iterator->next(); - - if ( $current instanceof WP_File_Visitor_Event ) { - if ( $current->is_entering() ) { - ++$this->depth; - } - $this->current_event = $current; - if ( $current->is_exiting() ) { - --$this->depth; - } - return true; - } else { - // It's a directory path, push a new iterator onto the stack - $this->iterator_stack[] = $this->create_iterator( $current ); - } - } else { - array_pop( $this->iterator_stack ); - } - } - - return false; - } - - public function get_event() { - return $this->current_event; - } -} diff --git a/packages/playground/data-liberation/src/import/WP_Stream_Importer.php b/packages/playground/data-liberation/src/import/WP_Stream_Importer.php index 03e41536db..52135c9e88 100644 --- a/packages/playground/data-liberation/src/import/WP_Stream_Importer.php +++ b/packages/playground/data-liberation/src/import/WP_Stream_Importer.php @@ -418,7 +418,9 @@ protected function index_next_entities( $count = 10000 ) { } } // @TODO: Consider using sha1 hashes to prevent huge URLs from blowing up the memory. - $this->indexed_assets_urls[ $data['attachment_url'] ] = true; + if( isset($data['attachment_url']) ) { + $this->indexed_assets_urls[ $data['attachment_url'] ] = true; + } } elseif ( isset( $data['post_content'] ) ) { $post = $data; $p = WP_Block_Markup_Url_Processor::create_from_html( $post['post_content'], $this->source_site_url ); @@ -522,7 +524,10 @@ protected function frontload_next_entity() { if ( null === $this->next_stage ) { $this->entity_iterator->set_entities_iterator( $this->create_entity_iterator() ); } - $this->downloader = new WP_Attachment_Downloader( $this->options['uploads_path'] ); + $this->downloader = new WP_Attachment_Downloader( + $this->options['uploads_path'], + $this->options['attachment_downloader_options'] ?? [], + ); } // Clear the frontloading events from the previous pass. @@ -585,7 +590,12 @@ protected function frontload_next_entity() { break; case 'post': if ( isset( $data['post_type'] ) && $data['post_type'] === 'attachment' ) { - $this->enqueue_attachment_download( $data['attachment_url'] ); + if( isset($data['attachment_url']) ) { + $this->enqueue_attachment_download( $data['attachment_url'] ); + } else { + // @TODO: Emit warning / error event + _doing_it_wrong( __METHOD__, 'No attachment URL or file path found in the post entity.', '1.0' ); + } } elseif ( isset( $data['post_content'] ) ) { $post = $data; $p = WP_Block_Markup_Url_Processor::create_from_html( $post['post_content'], $this->source_site_url ); @@ -643,57 +653,72 @@ protected function import_next_entity() { // Rewrite the URLs in the post. switch ( $entity->get_type() ) { case 'post': - $data = $entity->get_data(); - foreach ( array( 'guid', 'post_content', 'post_excerpt' ) as $key ) { - if ( ! isset( $data[ $key ] ) ) { - continue; - } - $p = WP_Block_Markup_Url_Processor::create_from_html( $data[ $key ], $this->source_site_url ); - while ( $p->next_url() ) { - // Relative URLs are okay at this stage. - if ( ! $p->get_raw_url() ) { - continue; - } - - /** - * Any URL that has a corresponding frontloaded file is an asset URL. - */ - $asset_filename = $this->new_asset_filename( - $p->get_raw_url(), - $data['local_file_path'] ?? $data['slug'] ?? null - ); - if ( file_exists( $this->options['uploads_path'] . '/' . $asset_filename ) ) { - $p->set_raw_url( - $this->options['uploads_url'] . '/' . $asset_filename - ); - /** - * @TODO: How would we know a specific image block refers to a specific - * attachment? We need to cross-correlate that to rewrite the URL. - * The image block could have query parameters, too, but presumably the - * path would be the same at least? What if the same file is referred - * to by two different URLs? e.g. assets.site.com and site.com/assets/ ? - * A few ideas: GUID, block attributes, fuzzy matching. Maybe a configurable - * strategy? And the API consumer would make the decision? - */ - continue; - } - - // Absolute URLs are required at this stage. - if ( ! $p->get_parsed_url() ) { - continue; - } - - $target_base_url = $this->get_url_mapping_target( $p->get_parsed_url() ); - if ( false !== $target_base_url ) { - $p->replace_base_url( $target_base_url ); - continue; - } - } - $data[ $key ] = $p->get_updated_html(); - } - $entity->set_data( $data ); + $data = $entity->get_data(); + if ( isset( $data['post_type'] ) && $data['post_type'] === 'attachment' ) { + if( ! isset($data['attachment_url']) ) { + // @TODO: Emit warning / error event + _doing_it_wrong( __METHOD__, 'No attachment URL or file path found in the post entity.', '1.0' ); + break; + } + $asset_filename = $this->new_asset_filename( + $data['attachment_url'], + $data['local_file_path'] ?? $data['slug'] ?? null + ); + unset($data['attachment_url']); + $data['local_file_path'] = $this->options['uploads_path'] . '/' . $asset_filename; + } else { + foreach ( array( 'guid', 'post_content', 'post_excerpt' ) as $key ) { + if ( ! isset( $data[ $key ] ) ) { + continue; + } + $p = WP_Block_Markup_Url_Processor::create_from_html( $data[ $key ], $this->source_site_url ); + while ( $p->next_url() ) { + // Relative URLs are okay at this stage. + if ( ! $p->get_raw_url() ) { + continue; + } + + /** + * Any URL that has a corresponding frontloaded file is an asset URL. + */ + $asset_filename = $this->new_asset_filename( + $p->get_raw_url(), + $data['local_file_path'] ?? $data['slug'] ?? null + ); + if ( file_exists( $this->options['uploads_path'] . '/' . $asset_filename ) ) { + $p->set_raw_url( + $this->options['uploads_url'] . '/' . $asset_filename + ); + /** + * @TODO: How would we know a specific image block refers to a specific + * attachment? We need to cross-correlate that to rewrite the URL. + * The image block could have query parameters, too, but presumably the + * path would be the same at least? What if the same file is referred + * to by two different URLs? e.g. assets.site.com and site.com/assets/ ? + * A few ideas: GUID, block attributes, fuzzy matching. Maybe a configurable + * strategy? And the API consumer would make the decision? + */ + continue; + } + + // Absolute URLs are required at this stage. + if ( ! $p->get_parsed_url() ) { + continue; + } + + $target_base_url = $this->get_url_mapping_target( $p->get_parsed_url() ); + if ( false !== $target_base_url ) { + $p->replace_base_url( $target_base_url ); + continue; + } + } + $data[ $key ] = $p->get_updated_html(); + } + } + $entity->set_data( $data ); break; } + $post_id = $this->importer->import_entity( $entity ); if ( false !== $post_id ) { From b2755d7ef3741389f5c145b06536e42ae8deccd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 00:54:40 +0100 Subject: [PATCH 43/71] Suport UTF-8 filenames in download_file_endpoint --- .../playground/data-liberation-static-files-editor/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index dc64c63f1a..657f41b486 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -391,7 +391,7 @@ static public function download_file_endpoint($request) { // Set headers for file download header('Content-Type: application/octet-stream'); - header('Content-Disposition: attachment; filename="' . $filename . '"'); + header("Content-Disposition: attachment; filename=UTF-8''" . urlencode($filename)); header('Content-Length: ' . $filesize); header('Cache-Control: no-cache'); From a9202bb4bc635c66a07996873ab2d43148719416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 01:33:55 +0100 Subject: [PATCH 44/71] Rewrite WordPress URLs to relative URLs when serializing to markdown --- .../src/WP_Blocks_To_Markdown.php | 7 ++ .../plugin.php | 61 +++++++++++++++-- .../ui/src/index.tsx | 67 +++++++++---------- .../WP_Block_Markup_Url_Processor.php | 3 + 4 files changed, 95 insertions(+), 43 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 95206f36d7..276ba1f754 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -75,6 +75,13 @@ private function block_to_markdown($block) { return "{$fence}{$language}\n{$code}\n{$fence}\n\n"; case 'core/image': + if(!isset($attributes['url'])) { + $processor = WP_Data_Liberation_HTML_Processor::create_fragment($inner_html); + if($processor->next_tag('img')) { + $attributes['url'] = $processor->get_attribute('src'); + $attributes['alt'] = $processor->get_attribute('alt'); + } + } return "![" . ($attributes['alt'] ?? '') . "](" . ($attributes['url'] ?? '') . ")\n\n"; case 'core/heading': diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 657f41b486..d6f318c82a 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -23,8 +23,12 @@ use WordPress\Filesystem\WP_Filesystem_Visitor; use WordPress\Filesystem\WP_Uploaded_Directory_Tree_Filesystem; -if ( ! defined( 'WP_STATIC_CONTENT_DIR' ) ) { - define( 'WP_STATIC_CONTENT_DIR', WP_CONTENT_DIR . '/uploads/static-pages' ); +if ( ! defined( 'WP_STATIC_PAGES_DIR' ) ) { + define( 'WP_STATIC_PAGES_DIR', WP_CONTENT_DIR . '/uploads/static-pages' ); +} + +if ( ! defined( 'WP_STATIC_MEDIA_DIR' ) ) { + define( 'WP_STATIC_MEDIA_DIR', WP_STATIC_PAGES_DIR . '/media' ); } if( ! defined( 'WP_LOCAL_FILE_POST_TYPE' )) { @@ -52,11 +56,10 @@ class WP_Static_Files_Editor_Plugin { static private function get_fs() { if(!self::$fs) { - $dot_git_path = WP_CONTENT_DIR . '/.static-pages.git'; - if(!is_dir($dot_git_path)) { - mkdir($dot_git_path, 0777, true); + if(!is_dir(WP_STATIC_PAGES_DIR)) { + mkdir(WP_STATIC_PAGES_DIR, 0777, true); } - $local_fs = new WP_Local_Filesystem($dot_git_path); + $local_fs = new WP_Local_Filesystem(WP_STATIC_PAGES_DIR); $repo = new WP_Git_Repository($local_fs); $repo->add_remote('origin', GIT_REPO_URL); $repo->set_ref_head('HEAD', 'refs/heads/' . GIT_BRANCH); @@ -517,6 +520,8 @@ static private function convert_post_to_string($path, $post) { // ones explicitly set by the user in the editor? $content = get_post_field('post_content', $post_id); + $content = self::unwordpressify_static_assets_urls($content); + switch($extension) { // @TODO: Add support for HTML and XHTML case 'html': @@ -530,6 +535,48 @@ static private function convert_post_to_string($path, $post) { return $converter->get_result(); } + /** + * Convert references to files served via download_file_endpoint + * to an absolute path referring to the corresponding static files + * in the local filesystem. + */ + static private function unwordpressify_static_assets_urls($content) { + $site_url = WP_URL::parse(get_site_url()); + $expected_endpoint_path = '/wp-json/static-files-editor/v1/download-file'; + $p = WP_Block_Markup_Url_Processor::create_from_html($content, $site_url); + while($p->next_url()) { + $url = $p->get_parsed_url(); + if(!is_child_url_of($url, get_site_url())) { + continue; + } + + // Account for sites with no nice permalink structure + if($url->searchParams->has('rest_route')) { + $url = WP_URL::parse($url->searchParams->get('rest_route'), $site_url); + } + + // Naively check for the endpoint that serves the file. + // WordPress can use a custom REST API prefix which this + // check doesn't account for. It assumes the endpoint path + // is unique enough to not conflict with other paths. + // + // It may need to be revisited if any conflicts arise in + // the future. + if(!str_ends_with($url->pathname, $expected_endpoint_path)) { + continue; + } + + // At this point we're certain the URL intends to download + // a static file managed by this plugin. + + // Let's replace the URL in the content with the relative URL. + $original_url = $url->searchParams->get('path'); + $p->set_raw_url($original_url); + } + + return $p->get_updated_html(); + } + static public function get_local_files_tree($subdirectory = '') { $tree = []; $fs = self::get_fs(); @@ -608,7 +655,7 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path * @TODO: Error handling */ static public function import_static_pages() { - if ( ! is_dir( WP_STATIC_CONTENT_DIR ) ) { + if ( ! is_dir( WP_STATIC_PAGES_DIR ) ) { return; } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 8429f50d7d..f15503daaa 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -13,11 +13,10 @@ import { import apiFetch from '@wordpress/api-fetch'; import { addComponentToEditorContentArea, - addLoadingOverlay, addLocalFilesTab, } from './add-local-files-tab'; import { store as blockEditorStore } from '@wordpress/block-editor'; -import { Spinner, Button } from '@wordpress/components'; +import { Spinner } from '@wordpress/components'; import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import css from './style.module.css'; import { FileTree } from 'components/FilePickerTree/types'; @@ -79,42 +78,39 @@ function ConnectedFilePickerTree() { // Get the current post's file path from meta const [meta] = useEntityProp('postType', WP_LOCAL_FILE_POST_TYPE, 'meta'); - const [selectedPath, setSelectedPath] = useState( - meta?.local_file_path || '/' + + const initialPostId = useSelect( + (select) => select(editorStore).getCurrentPostId(), + [] ); + const [selectedNode, setSelectedNode] = useState({ + path: meta?.local_file_path || '/', + postId: initialPostId, + type: 'folder' as 'file' | 'folder', + }); + useEffect(() => { async function refreshPostId() { - console.log( - 'refreshPostId', - selectedPath, - isStaticAssetPath(selectedPath) - ); - if (isStaticAssetPath(selectedPath)) { + if (isStaticAssetPath(selectedNode.path)) { setPostLoading(false); - setSelectedPostId(null); - setPreviewPath(selectedPath); - } else { + setSelectedNode((prev) => ({ ...prev, postId: null })); + setPreviewPath(selectedNode.path); + } else if (selectedNode.type === 'file') { setPostLoading(true); - if (!selectedPostId) { + if (!selectedNode.postId) { const { post_id } = (await apiFetch({ path: '/static-files-editor/v1/get-or-create-post-for-file', method: 'POST', - data: { path: selectedPath }, + data: { path: selectedNode.path }, })) as { post_id: string }; - setSelectedPostId(post_id); + setSelectedNode((prev) => ({ ...prev, postId: post_id })); } setPreviewPath(null); } } refreshPostId(); - }, [selectedPath]); - - const initialPostId = useSelect( - (select) => select(editorStore).getCurrentPostId(), - [] - ); - const [selectedPostId, setSelectedPostId] = useState(initialPostId); + }, [selectedNode.path]); const { post, hasLoadedPost, onNavigateToEntityRecord } = useSelect( (select) => { @@ -127,16 +123,16 @@ function ConnectedFilePickerTree() { post: getEntityRecord( 'postType', WP_LOCAL_FILE_POST_TYPE, - selectedPostId + selectedNode.postId ), hasLoadedPost: hasFinishedResolution('getEntityRecord', [ 'postType', WP_LOCAL_FILE_POST_TYPE, - selectedPostId, + selectedNode.postId, ]), }; }, - [selectedPostId] + [selectedNode.postId] ); const { setPostLoading, setPreviewPath } = useDispatch(STORE_NAME); @@ -145,16 +141,16 @@ function ConnectedFilePickerTree() { // Only navigate once the post has been loaded. Otherwise the editor // will disappear for a second – the component renders its // children conditionally on having the post available. - if (selectedPostId) { + if (selectedNode.postId) { setPostLoading(!hasLoadedPost); if (hasLoadedPost && post) { onNavigateToEntityRecord({ - postId: selectedPostId, + postId: selectedNode.postId, postType: WP_LOCAL_FILE_POST_TYPE, }); } } - }, [hasLoadedPost, post, setPostLoading, selectedPostId]); + }, [hasLoadedPost, post, setPostLoading, selectedNode.postId]); const refreshFileTree = useCallback(async () => { fileTreePromise = apiFetch({ @@ -190,12 +186,11 @@ function ConnectedFilePickerTree() { }; const handleFileClick = async (filePath: string, node: FileNode) => { - setSelectedPath(filePath); - if (node.post_id && !isStaticAssetPath(filePath)) { - setSelectedPostId(node.post_id); - } else { - setSelectedPostId(null); - } + setSelectedNode({ + path: filePath, + postId: node.post_id, + type: node.type, + }); }; const handleNodesCreated = async (tree: FileTree) => { @@ -305,7 +300,7 @@ function ConnectedFilePickerTree() { base_url_string = $base_url_string; From 802a6f069fdc9d2399620a85aa03904fc6329e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 02:49:58 +0100 Subject: [PATCH 45/71] Preserve images as blocks --- .../src/WP_Blocks_To_Markdown.php | 9 +- .../src/WP_Markdown_To_Blocks.php | 23 ++- .../plugin.php | 131 +++++++++++------- .../src/components/FilePickerTree/index.tsx | 43 +++--- .../ui/src/index.tsx | 41 +++++- .../WP_Filesystem_Entity_Reader.php | 8 ++ .../src/import/WP_Entity_Importer.php | 11 +- 7 files changed, 177 insertions(+), 89 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 276ba1f754..634a9498d4 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -82,7 +82,14 @@ private function block_to_markdown($block) { $attributes['alt'] = $processor->get_attribute('alt'); } } - return "![" . ($attributes['alt'] ?? '') . "](" . ($attributes['url'] ?? '') . ")\n\n"; + $escaped_url = $attributes['url'] ?? ''; + // @TODO: Figure out the correct markdown escaping for these things + $escaped_url = str_replace(' ', '%20', $escaped_url); + $escaped_url = str_replace(')', '%29', $escaped_url); + + $escaped_alt = $attributes['alt'] ?? ''; + $escaped_alt = str_replace(['[', ']'], '', $escaped_alt); + return "![" . $escaped_alt . "](" . $escaped_url . ")\n\n"; case 'core/heading': $level = $attributes['level'] ?? null; diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index 29efe4cca4..4aae88545d 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -221,7 +221,7 @@ private function convert_markdown_to_blocks() { $html = new WP_HTML_Tag_Processor( '' ); $html->next_tag(); if ( $node->getUrl() ) { - $html->set_attribute( 'src', $node->getUrl() ); + $html->set_attribute( 'src', urldecode($node->getUrl()) ); } if ( $node->getTitle() ) { $html->set_attribute( 'title', $node->getTitle() ); @@ -235,7 +235,26 @@ private function convert_markdown_to_blocks() { $children[0]->setLiteral( '' ); } - $this->append_content( $html->get_updated_html() ); + $image_tag = $html->get_updated_html(); + // @TODO: Decide between inline image and the image block + $in_paragraph = $this->current_block()->block_name === 'paragraph'; + if ( $in_paragraph ) { + $this->append_content( '

' ); + $this->pop_block(); + } + // @TODO: Find a way to plug in the attachment ID here + $image_block = << +
+ $image_tag +
+ +BLOCK; + $this->append_content( $image_block ); + if ( $in_paragraph ) { + $this->push_block('paragraph'); + $this->append_content( '

' ); + } break; case ExtensionInline\Link::class: diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index d6f318c82a..e7d4bbfbad 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -3,8 +3,6 @@ * Plugin Name: Data Liberation – WordPress Static files editor * * @TODO: Page metadata editor in Gutenberg - * @TODO: A special "filename" field in wp-admin and in Gutenberg. Either source from the page title or - * pin it to a specific, user-defined value. * @TODO: Choose the local file storage format (MD, HTML, etc.) in Gutenberg page options. * @TODO: HTML, XHTML, and Blocks renderers * @TODO: Integrity check – is the database still in sync with the files? @@ -12,11 +10,6 @@ * * Overwrite the database with the local files? This is a local files editor after all. * * Display a warning in wp-admin and let the user decide what to do? * @TODO: Consider tricky scenarios – moving a parent to trash and then restoring it. - * @TODO: Consider using hierarchical taxonomy to model the directory/file structure – instead of - * using the post_parent field. Could be more flexible (no need for index.md files) and require - * less complex operations in the code (no need to update a subtree of posts when moving a post, - * no need to periodically "flatten" the parent directory). - * @TODO: Maybe use Playground's FilePickerTree React component? Or re-implement it with interactivity API? */ use WordPress\Filesystem\WP_Local_Filesystem; @@ -143,6 +136,7 @@ static public function initialize() { // Register hooks register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); + add_action('init', function() { self::get_fs(); self::register_post_type(); @@ -254,7 +248,11 @@ function() { 'methods' => 'GET', 'callback' => array(self::class, 'download_file_endpoint'), 'permission_callback' => function() { - return current_user_can('edit_posts'); + // @TODO: Restrict access to this endpoint to editors, but + // don't require a nonce. Nonces are troublesome for + // static assets that don't have a dynamic URL. + // return current_user_can('edit_posts'); + return true; }, 'args' => array( 'path' => array( @@ -478,9 +476,10 @@ static private function refresh_post_from_local_file($post) { $converter = new WP_HTML_To_Blocks( WP_HTML_Processor::create_fragment( $content ) ); break; case 'md': - default: $converter = new WP_Markdown_To_Blocks( $content ); break; + default: + return false; } $converter->convert(); @@ -489,6 +488,7 @@ static private function refresh_post_from_local_file($post) { $metadata[$key] = $value[0]; } $new_content = $converter->get_block_markup(); + $new_content = self::wordpressify_static_assets_urls($new_content); $updated = wp_update_post(array( 'ID' => $post_id, @@ -527,9 +527,10 @@ static private function convert_post_to_string($path, $post) { case 'html': case 'xhtml': case 'md': - default: $converter = new WP_Blocks_To_Markdown( $content, $metadata ); break; + default: + return ''; } $converter->convert(); return $converter->get_result(); @@ -577,6 +578,35 @@ static private function unwordpressify_static_assets_urls($content) { return $p->get_updated_html(); } + /** + * Convert references to files served via path to the + * corresponding download_file_endpoint references. + */ + static private function wordpressify_static_assets_urls($content) { + $site_url = WP_URL::parse(get_site_url()); + $expected_endpoint_path = '/wp-json/static-files-editor/v1/download-file'; + $p = WP_Block_Markup_Url_Processor::create_from_html($content, $site_url); + while($p->next_url()) { + $url = $p->get_parsed_url(); + if(!is_child_url_of($url, get_site_url())) { + continue; + } + + // @TODO: Also work with tags, account + // for .md and directory links etc. + if($p->get_tag() !== 'IMG') { + continue; + } + + $new_url = WP_URL::parse($url->pathname, $site_url); + $new_url->pathname = $expected_endpoint_path; + $new_url->searchParams->set('path', $p->get_raw_url()); + $p->set_raw_url($new_url->__toString()); + } + + return $p->get_updated_html(); + } + static public function get_local_files_tree($subdirectory = '') { $tree = []; $fs = self::get_fs(); @@ -589,21 +619,33 @@ static public function get_local_files_tree($subdirectory = '') { 'fields' => 'id=>meta' )); - $path_to_post_id = array(); + $path_to_post = array(); foreach($file_posts as $post) { $file_path = get_post_meta($post->ID, 'local_file_path', true); if ($file_path) { - $path_to_post_id[$file_path] = $post->ID; + $path_to_post[$file_path] = $post; + } + } + + $attachments = get_posts(array( + 'post_type' => 'attachment', + 'posts_per_page' => -1, + 'meta_key' => 'local_file_path', + )); + foreach($attachments as $attachment) { + $attachment_path = get_post_meta($attachment->ID, 'local_file_path', true); + if ($attachment_path) { + $path_to_post[$attachment_path] = $attachment; } } $base_dir = $subdirectory ? $subdirectory : '/'; - self::build_local_file_tree_recursive($fs, $base_dir, $tree, $path_to_post_id); + self::build_local_file_tree_recursive($fs, $base_dir, $tree, $path_to_post); return $tree; } - static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path_to_post_id) { + static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path_to_post) { $items = $fs->ls($dir); if ($items === false) { return; @@ -633,15 +675,16 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path // Recursively build children $last_index = count($tree) - 1; - self::build_local_file_tree_recursive($fs, $path, $tree[$last_index]['children'], $path_to_post_id); + self::build_local_file_tree_recursive($fs, $path, $tree[$last_index]['children'], $path_to_post); } else { $node = array( 'type' => 'file', 'name' => $item, ); - if (isset($path_to_post_id[$path])) { - $node['post_id'] = $path_to_post_id[$path]; + if (isset($path_to_post[$path])) { + $node['post_id'] = $path_to_post[$path]->ID; + $node['post_type'] = $path_to_post[$path]->post_type; } $tree[] = $node; @@ -655,10 +698,6 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path * @TODO: Error handling */ static public function import_static_pages() { - if ( ! is_dir( WP_STATIC_PAGES_DIR ) ) { - return; - } - if ( defined('WP_IMPORTING') && WP_IMPORTING ) { return; } @@ -671,20 +710,26 @@ static public function import_static_pages() { // Prevent ID conflicts self::reset_db_data(); - self::do_import_static_pages(); + return self::do_import_static_pages(); } static private function do_import_static_pages($options = array()) { + $fs = $options['filesystem'] ?? self::get_fs(); $importer = WP_Stream_Importer::create( - function () use ($options) { + function () use ($fs, $options) { return new WP_Filesystem_Entity_Reader( - self::get_fs(), + $fs, array( 'post_type' => WP_LOCAL_FILE_POST_TYPE, 'post_tree_options' => $options['post_tree_options'] ?? array(), ) ); - } + }, + array( + 'attachment_downloader_options' => array( + 'source_from_filesystem' => $fs, + ), + ) ); $import_session = WP_Import_Session::create( @@ -693,7 +738,7 @@ function () use ($options) { ) ); - data_liberation_import_step( $import_session, $importer ); + return data_liberation_import_step( $import_session, $importer ); } static private function register_post_type() { @@ -910,33 +955,13 @@ static public function create_files_endpoint($request) { } } - $importer = WP_Stream_Importer::create( - function () use ($parent_id, $uploaded_fs) { - return new WP_Filesystem_Entity_Reader( - $uploaded_fs, - array( - 'post_type' => WP_LOCAL_FILE_POST_TYPE, - 'post_tree_options' => array( - 'root_parent_id' => $parent_id, - 'create_index_pages' => false, - ), - ) - ); - }, - array( - 'attachment_downloader_options' => array( - 'source_from_filesystem' => $uploaded_fs, - ), - ) - ); - - $import_session = WP_Import_Session::create( - array ( - 'data_source' => 'static_pages' - ) - ); - - $result = data_liberation_import_step( $import_session, $importer ); + $result = self::do_import_static_pages(array( + 'filesystem' => $uploaded_fs, + 'post_tree_options' => array( + 'root_parent_id' => $parent_id, + 'create_index_pages' => false, + ), + )); if(is_wp_error($result)) { return $result; } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index dbde7e7f65..35a3f3ff3a 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -97,17 +97,9 @@ type FilePickerContextType = { onEditedNodeCancel: () => void; onNodeDeleted: (path: string) => void; startRenaming: (path: string) => void; - onDragStart: ( - e: React.DragEvent, - path: string, - type: 'file' | 'folder' - ) => void; - onDragOver: ( - e: React.DragEvent, - path: string, - type: 'file' | 'folder' - ) => void; - onDrop: (e: React.DragEvent, path: string, type: 'file' | 'folder') => void; + onDragStart: (e: React.DragEvent, path: string, node: FileNode) => void; + onDragOver: (e: React.DragEvent, path: string, node: FileNode) => void; + onDrop: (e: React.DragEvent, path: string, node: FileNode) => void; onDragEnd: () => void; dragState: DragState | null; }; @@ -301,6 +293,7 @@ export const FilePickerTree: React.FC = ({ path, hoverPath: null, hoverType: null, + isExternal: false, }); onDragStart?.(e, path, type); @@ -313,7 +306,7 @@ export const FilePickerTree: React.FC = ({ const handleDragOver = ( e: React.DragEvent, path: string, - type: 'file' | 'folder' + node: FileNode ) => { e.preventDefault(); @@ -324,7 +317,7 @@ export const FilePickerTree: React.FC = ({ path: '', isExternal: true, hoverPath: path, - hoverType: type, + hoverType: node.type, }); return; } @@ -340,7 +333,7 @@ export const FilePickerTree: React.FC = ({ setDragState({ ...dragState, hoverPath: path, - hoverType: type, + hoverType: node.type, }); } }; @@ -348,7 +341,7 @@ export const FilePickerTree: React.FC = ({ const handleDrop = async ( e: React.DragEvent, targetPath: string, - targetType: 'file' | 'folder' + targetNode: FileNode ) => { e.preventDefault(); // Prevent a parent element event handler from handling the drop @@ -363,7 +356,7 @@ export const FilePickerTree: React.FC = ({ // Prevent dropping a folder into its own descendant if ( dragState.path && - targetType === 'folder' && + targetNode.type === 'folder' && isDescendantPath(dragState.path, targetPath) ) { return; @@ -372,7 +365,7 @@ export const FilePickerTree: React.FC = ({ const fromPath = dragState.path.replace(/^\/+/, ''); const targetParentPath = - targetType === 'file' + targetNode.type === 'file' ? targetPath.split('/').slice(0, -1).join('/') : targetPath; @@ -396,7 +389,7 @@ export const FilePickerTree: React.FC = ({ // Drag&Drop from desktop into the FilePickerTree if (e.dataTransfer.items.length > 0) { const targetFolder = - targetType === 'folder' + targetNode.type === 'folder' ? targetPath : targetPath.split('/').slice(0, -1).join('/'); const items = Array.from(e.dataTransfer.items); @@ -571,7 +564,11 @@ export const FilePickerTree: React.FC = ({ ref={thisContainerRef} className={className} onDrop={(e) => { - handleDrop?.(e, '/', 'folder'); + handleDrop?.(e, '/', { + name: '', + type: 'folder', + children: [], + }); }} >

- onDragStart?.(e, path, node.type) + onDragStart?.(e, path, node) } onDragOver={(e) => - onDragOver?.(e, path, node.type) - } - onDrop={(e) => - onDrop?.(e, path, node.type) + onDragOver?.(e, path, node) } + onDrop={(e) => onDrop?.(e, path, node)} onDragEnd={onDragEnd} style={{ opacity: isBeingDragged ? 0.5 : 1, diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index f15503daaa..6ebb292a45 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -269,10 +269,10 @@ function ConnectedFilePickerTree() { const handleDragStart = ( e: React.DragEvent, path: string, - type: 'file' | 'folder' + node: FileNode ) => { // Directory downloads are not supported yet. - if (type === 'file') { + if (node.type === 'file') { const url = `${window.wpApiSettings.root}static-files-editor/v1/download-file?path=${path}&_wpnonce=${window.wpApiSettings.nonce}`; const filename = path.split('/').pop(); // For dragging & dropping to desktop @@ -280,11 +280,38 @@ function ConnectedFilePickerTree() { 'DownloadURL', `text/plain:${filename}:${url}` ); - // For dragging & dropping into the editor canvas - e.dataTransfer.setData( - 'text/html', - `${filename}` - ); + if ('post_type' in node && node.post_type === 'attachment') { + // Create DOM elements to safely construct HTML + + const figure = document.createElement('figure'); + figure.className = 'wp-block-image size-full'; + + const img = document.createElement('img'); + img.src = url; + img.alt = ''; + img.className = `wp-image-${node.post_id}`; + + figure.appendChild(img); + + // Wrap in WordPress block comments + // For dragging & dropping into the editor canvas + e.dataTransfer.setData( + 'text/html', + `', + '' + )},"sizeSlug":"full","linkDestination":"none"} --> +${figure.outerHTML} +` + ); + } else if (isStaticAssetPath(path)) { + const img = document.createElement('img'); + img.src = url; + img.alt = filename; + e.dataTransfer.setData('text/html', img.outerHTML); + } } }; diff --git a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php index 4fe8860e16..50c10799c0 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_Filesystem_Entity_Reader.php @@ -97,6 +97,14 @@ public function next_entity(): bool { 'attachment_url' => 'file://' . $post_tree_node['local_file_path'], ) ); + $this->entities[] = new WP_Imported_Entity( + 'post_meta', + array( + 'post_id' => $post_tree_node['post_id'], + 'key' => 'local_file_path', + 'value' => $post_tree_node['local_file_path'], + ) + ); // We're done emiting the entity. // wp_generate_attachment_metadata() et al. will be called by the // importer at the database insertion step. diff --git a/packages/playground/data-liberation/src/import/WP_Entity_Importer.php b/packages/playground/data-liberation/src/import/WP_Entity_Importer.php index 23fd1f1756..649b777323 100644 --- a/packages/playground/data-liberation/src/import/WP_Entity_Importer.php +++ b/packages/playground/data-liberation/src/import/WP_Entity_Importer.php @@ -451,6 +451,8 @@ public function import_post( $data ) { return false; } + $meta = array(); + $original_id = isset( $data['post_id'] ) ? (int) $data['post_id'] : 0; $parent_id = isset( $data['post_parent'] ) ? (int) $data['post_parent'] : 0; @@ -548,6 +550,12 @@ public function import_post( $data ) { $postdata[ $key ] = $data[ $key ]; } + if(!isset($postdata['post_date'])) { + $postdata['post_date'] = date('Y-m-d H:i:s'); + } + if(!isset($postdata['post_date_gmt'])) { + $postdata['post_date_gmt'] = date('Y-m-d H:i:s'); + } $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $data ); @@ -782,7 +790,6 @@ protected function process_attachment( $post, $meta ) { * @TODO: Explore other interfaces for attachment import. */ public function import_attachment( $filepath, $post_id ) { - $filename = basename( $filepath ); // Check if attachment with this guid already exists $existing_attachment = get_posts( @@ -863,7 +870,7 @@ public function import_post_meta( $meta_item, $post_id ) { $value = maybe_unserialize( $meta_item['value'] ); } - add_post_meta( $post_id, $key, $value ); + update_post_meta( $post_id, $key, $value ); do_action( 'import_post_meta', $post_id, $key, $value ); // if the post has a featured image, take note of this in case of remap From 875c89a197cc817a823011b03ce93f7e5a3e12d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 16:58:18 +0100 Subject: [PATCH 46/71] Upload media files to the static files filesystem --- .../src/WP_Markdown_To_Blocks.php | 1 - .../plugin.php | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index 4aae88545d..a3c609f9dc 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -242,7 +242,6 @@ private function convert_markdown_to_blocks() { $this->append_content( '

' ); $this->pop_block(); } - // @TODO: Find a way to plug in the attachment ID here $image_block = <<
diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index e7d4bbfbad..374e12c2d2 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -21,7 +21,7 @@ } if ( ! defined( 'WP_STATIC_MEDIA_DIR' ) ) { - define( 'WP_STATIC_MEDIA_DIR', WP_STATIC_PAGES_DIR . '/media' ); + define( 'WP_STATIC_MEDIA_DIR', 'media' ); } if( ! defined( 'WP_LOCAL_FILE_POST_TYPE' )) { @@ -136,7 +136,6 @@ static public function initialize() { // Register hooks register_activation_hook( __FILE__, array(self::class, 'import_static_pages') ); - add_action('init', function() { self::get_fs(); self::register_post_type(); @@ -148,6 +147,36 @@ static public function initialize() { } }); + // Handle media uploads + add_filter('wp_handle_upload', function($upload) { + try { + if(!self::acquire_synchronization_lock()) { + return $upload; + } + + $file = $upload['file']; + + $main_fs = self::get_fs(); + $local_fs = new WP_Local_Filesystem(dirname($file)); + + $filename = basename($file); + $target_path = wp_join_paths(WP_STATIC_MEDIA_DIR, $filename); + $local_fs->copy($filename, $target_path, [ + 'to_fs' => $main_fs, + ]); + + // Update attachment URL to point to static path + $upload['url'] = rest_url('static-files-editor/v1/download-file?path=' . urlencode($target_path)); + + // Set local_file_path metadata for the attachment + update_post_meta($upload['attachment_id'], 'local_file_path', $target_path); + + return $upload; + } finally { + self::release_synchronization_lock(); + } + }); + // Register the admin page add_action('admin_menu', function() { add_menu_page( @@ -581,6 +610,8 @@ static private function unwordpressify_static_assets_urls($content) { /** * Convert references to files served via path to the * corresponding download_file_endpoint references. + * + * @TODO: Plug in the attachment IDs into image blocks */ static private function wordpressify_static_assets_urls($content) { $site_url = WP_URL::parse(get_site_url()); From 007765977ba7003fcbc0fe3d859ea9dbc9209dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 17:31:04 +0100 Subject: [PATCH 47/71] Use the correct paths in wp_handle_upload --- .../plugin.php | 81 +++++++++++++++---- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 374e12c2d2..e21f80c9d7 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -3,13 +3,14 @@ * Plugin Name: Data Liberation – WordPress Static files editor * * @TODO: Page metadata editor in Gutenberg - * @TODO: Choose the local file storage format (MD, HTML, etc.) in Gutenberg page options. * @TODO: HTML, XHTML, and Blocks renderers * @TODO: Integrity check – is the database still in sync with the files? * If not, what should we do? * * Overwrite the database with the local files? This is a local files editor after all. * * Display a warning in wp-admin and let the user decide what to do? * @TODO: Consider tricky scenarios – moving a parent to trash and then restoring it. + * @TODO: Call resize_to_max_dimensions_if_files_is_an_image for the images dragged directly + * into the file picker */ use WordPress\Filesystem\WP_Local_Filesystem; @@ -32,6 +33,10 @@ define( 'WP_AUTOSAVES_DIRECTORY', '.autosaves' ); } +if( ! defined( 'WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION' )) { + define( 'WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION', 1280 ); +} + if(isset($_GET['dump'])) { add_action('init', function() { WP_Static_Files_Editor_Plugin::import_static_pages(); @@ -153,18 +158,24 @@ static public function initialize() { if(!self::acquire_synchronization_lock()) { return $upload; } - - $file = $upload['file']; + $file_path= $upload['file']; $main_fs = self::get_fs(); - $local_fs = new WP_Local_Filesystem(dirname($file)); + $local_fs = new WP_Local_Filesystem(dirname($file_path)); - $filename = basename($file); - $target_path = wp_join_paths(WP_STATIC_MEDIA_DIR, $filename); - $local_fs->copy($filename, $target_path, [ + $file_name = basename($file_path); + $target_path = wp_join_paths(WP_STATIC_MEDIA_DIR, $file_name); + + $file_path = self::resize_to_max_dimensions_if_files_is_an_image($file_path); + + // Copy the file to the static media directory + $local_fs->copy($file_name, $target_path, [ 'to_fs' => $main_fs, ]); + // Force pull after every write operation + self::$client->force_pull(); + // Update attachment URL to point to static path $upload['url'] = rest_url('static-files-editor/v1/download-file?path=' . urlencode($target_path)); @@ -407,6 +418,46 @@ function() { }, 10, 1); } + /** + * Resize image to a maximum width and height. + * + * @param string $image_path The path to the image file + * @return string The path to the resized image file + */ + static public function resize_to_max_dimensions_if_files_is_an_image($image_path) { + // Only resize if this is an image file + // getimagesize() returns false for non-images (and + // also image formats it can't handle) + $image_size = @getimagesize($image_path); + if ($image_size === false) { + return $image_path; + } + + if ($image_size[0] > WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION || $image_size[1] > WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION) { + $editor = wp_get_image_editor($image_path); + if (is_wp_error($editor)) { + return $image_path; + } + + $editor->resize(WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION, WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION, false); + + // Try saving to original path first + $result = $editor->save($image_path); + + // If saving fails (read-only), save to temp file + if (is_wp_error($result)) { + $temp_path = wp_tempnam(basename($image_path)); + $result = $editor->save($temp_path); + if (is_wp_error($result)) { + return $image_path; + } + $image_path = $temp_path; + } + } + + return $image_path; + } + static public function download_file_endpoint($request) { $path = $request->get_param('path'); $fs = self::get_fs(); @@ -445,7 +496,7 @@ static public function download_file_endpoint($request) { return new WP_Error('file_error', 'Could not read file'); } - static private $synchronizing = false; + static private $synchronizing = 0; static private function acquire_synchronization_lock() { // Skip if in maintenance mode if (wp_is_maintenance_mode()) { @@ -457,15 +508,15 @@ static private function acquire_synchronization_lock() { } // @TODO: Synchronize between threads - if(self::$synchronizing) { + if(self::$synchronizing > 0) { return false; } - self::$synchronizing = true; + ++self::$synchronizing; return true; } static private function release_synchronization_lock() { - self::$synchronizing = false; + self::$synchronizing = max(0, self::$synchronizing - 1); } static private function refresh_post_from_local_file($post) { @@ -741,11 +792,13 @@ static public function import_static_pages() { // Prevent ID conflicts self::reset_db_data(); - return self::do_import_static_pages(); + return self::do_import_static_pages([ + 'from_filesystem' => self::get_fs(), + ]); } static private function do_import_static_pages($options = array()) { - $fs = $options['filesystem'] ?? self::get_fs(); + $fs = $options['from_filesystem']; $importer = WP_Stream_Importer::create( function () use ($fs, $options) { return new WP_Filesystem_Entity_Reader( @@ -987,7 +1040,7 @@ static public function create_files_endpoint($request) { } $result = self::do_import_static_pages(array( - 'filesystem' => $uploaded_fs, + 'from_filesystem' => $uploaded_fs, 'post_tree_options' => array( 'root_parent_id' => $parent_id, 'create_index_pages' => false, From 11638c6f3475167d31a87c40918a2b4d42fab6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 17:37:01 +0100 Subject: [PATCH 48/71] Delete files when backspace is pressed --- .../ui/src/components/FilePickerTree/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 35a3f3ff3a..bb9bae1914 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -655,7 +655,11 @@ const NodeRow: React.FC<{ const toggleOpen = () => expandNode(path, !isExpanded); const handleKeyDown = (event: any) => { - if (event.key === 'ArrowLeft') { + if (event.key === 'Backspace') { + onNodeDeleted(path); + event.preventDefault(); + event.stopPropagation(); + } else if (event.key === 'ArrowLeft') { if (isExpanded) { toggleOpen(); } else { From a11d5201149667411d66b9da28febf412bcb9a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 18:43:41 +0100 Subject: [PATCH 49/71] Store updated media files when they're, e.g., rotated --- .../plugin.php | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index e21f80c9d7..f21bc970fd 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -153,13 +153,17 @@ static public function initialize() { }); // Handle media uploads - add_filter('wp_handle_upload', function($upload) { + add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) { try { if(!self::acquire_synchronization_lock()) { - return $upload; + return $metadata; } - $file_path= $upload['file']; - + + $file_path = get_attached_file($attachment_id); + if(!$file_path) { + return $metadata; + } + $main_fs = self::get_fs(); $local_fs = new WP_Local_Filesystem(dirname($file_path)); @@ -168,6 +172,9 @@ static public function initialize() { $file_path = self::resize_to_max_dimensions_if_files_is_an_image($file_path); + // Set local_file_path metadata for the attachment + update_post_meta($attachment_id, 'local_file_path', $target_path); + // Copy the file to the static media directory $local_fs->copy($file_name, $target_path, [ 'to_fs' => $main_fs, @@ -176,17 +183,55 @@ static public function initialize() { // Force pull after every write operation self::$client->force_pull(); - // Update attachment URL to point to static path - $upload['url'] = rest_url('static-files-editor/v1/download-file?path=' . urlencode($target_path)); + return $metadata; + } finally { + self::release_synchronization_lock(); + } + }, 10, 2); - // Set local_file_path metadata for the attachment - update_post_meta($upload['attachment_id'], 'local_file_path', $target_path); + // Handle attachment updates (e.g. image rotations) + add_action('wp_update_attachment_metadata', function($metadata, $attachment_id) { + try { + if(!self::acquire_synchronization_lock()) { + return $metadata; + } + + $file_path = get_attached_file($attachment_id); + if(!$file_path) { + return $metadata; + } - return $upload; + $local_file_path = get_post_meta($attachment_id, 'local_file_path', true); + if(!$local_file_path) { + return $metadata; + } + + $main_fs = self::get_fs(); + $local_fs = new WP_Local_Filesystem(dirname($file_path)); + + $file_name = basename($file_path); + // Copy the updated file to the static media directory + $local_fs->copy($file_name, $local_file_path, [ + 'to_fs' => $main_fs, + ]); + + // Force pull after every write operation + self::$client->force_pull(); + + return $metadata; } finally { self::release_synchronization_lock(); } - }); + }, 10, 2); + + // Rewrite attachment URLs to use the static files download endpoint + add_filter('wp_get_attachment_url', function($url, $attachment_id) { + $local_file_path = get_post_meta($attachment_id, 'local_file_path', true); + if ($local_file_path) { + return rest_url('static-files-editor/v1/download-file?path=' . urlencode($local_file_path)); + } + return $url; + }, 10, 2); // Register the admin page add_action('admin_menu', function() { From 34e08ff5b8b1c5ea3357e1edeff5183b97aeeedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 20:50:52 +0100 Subject: [PATCH 50/71] Media files preview --- .../components/src/FilePickerTree/index.tsx | 16 ++++- .../src/FilePickerTree/style.module.css | 5 ++ .../plugin.php | 60 +++++++++++++------ .../src/components/FilePickerTree/index.tsx | 25 ++++++++ .../FilePickerTree/style.module.css | 16 ++++- .../data-liberation/blueprints-library | 2 +- .../src/git/WP_Git_Filesystem.php | 25 ++++++++ 7 files changed, 129 insertions(+), 20 deletions(-) diff --git a/packages/playground/components/src/FilePickerTree/index.tsx b/packages/playground/components/src/FilePickerTree/index.tsx index 8d77a72cde..d140ecd77c 100644 --- a/packages/playground/components/src/FilePickerTree/index.tsx +++ b/packages/playground/components/src/FilePickerTree/index.tsx @@ -292,10 +292,18 @@ const NodeRow: React.FC<{ isOpen={node.type === 'folder' && isExpanded} level={level} /> + {/* @TODO: Source the path from the server to reduce code duplication and + also account for varying site path, no permalinks etc. */} + {isImage && ( + + )} )} - + {() => ( {node.name} + {node.post_type === 'attachment' && ( + + )} ); }; diff --git a/packages/playground/components/src/FilePickerTree/style.module.css b/packages/playground/components/src/FilePickerTree/style.module.css index 7bfa7b81d0..049ee45a50 100644 --- a/packages/playground/components/src/FilePickerTree/style.module.css +++ b/packages/playground/components/src/FilePickerTree/style.module.css @@ -31,6 +31,7 @@ box-sizing: border-box; padding: 6px 12px; border-radius: 2px; + text-overflow: ellipsis; width: 100%; white-space: nowrap; @@ -52,6 +53,10 @@ } } +.file-node-actions { + flex-shrink: 0; +} + .file-name { margin-left: 10px; } diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index f21bc970fd..579c9b9023 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -152,6 +152,10 @@ static public function initialize() { } }); + add_filter( 'big_image_size_threshold', function($threshold) { + return WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION; + }); + // Handle media uploads add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) { try { @@ -159,18 +163,30 @@ static public function initialize() { return $metadata; } - $file_path = get_attached_file($attachment_id); + // Don't process thumbnails, only original images + $file_path = wp_get_original_image_path($attachment_id); if(!$file_path) { return $metadata; } + // Skip if the file was already processed + $local_file_path = get_post_meta($attachment_id, 'local_file_path', true); + if($local_file_path) { + return $metadata; + } + + $file_path = self::resize_to_max_dimensions_if_files_is_an_image($file_path); + $main_fs = self::get_fs(); $local_fs = new WP_Local_Filesystem(dirname($file_path)); $file_name = basename($file_path); $target_path = wp_join_paths(WP_STATIC_MEDIA_DIR, $file_name); - $file_path = self::resize_to_max_dimensions_if_files_is_an_image($file_path); + // Skip if the file was already processed + if($main_fs->is_file($target_path)) { + return $metadata; + } // Set local_file_path metadata for the attachment update_post_meta($attachment_id, 'local_file_path', $target_path); @@ -196,20 +212,31 @@ static public function initialize() { return $metadata; } - $file_path = get_attached_file($attachment_id); + // Don't process thumbnails, only original images + $file_path = wp_get_original_image_path($attachment_id); if(!$file_path) { return $metadata; } + // Skip if the file isn't synchronized with the local filesystem $local_file_path = get_post_meta($attachment_id, 'local_file_path', true); if(!$local_file_path) { return $metadata; } + $file_path = self::resize_to_max_dimensions_if_files_is_an_image($file_path); + $main_fs = self::get_fs(); $local_fs = new WP_Local_Filesystem(dirname($file_path)); $file_name = basename($file_path); + $target_path = wp_join_paths(WP_STATIC_MEDIA_DIR, $file_name); + + // Skip if the file was already processed + if($main_fs->is_file($target_path)) { + return $metadata; + } + // Copy the updated file to the static media directory $local_fs->copy($file_name, $local_file_path, [ 'to_fs' => $main_fs, @@ -224,6 +251,11 @@ static public function initialize() { } }, 10, 2); + // Disable thumbnail generation for local file attachments + add_filter('intermediate_image_sizes_advanced', function($sizes, $metadata) { + return array(); + }, 10, 2); + // Rewrite attachment URLs to use the static files download endpoint add_filter('wp_get_attachment_url', function($url, $attachment_id) { $local_file_path = get_post_meta($attachment_id, 'local_file_path', true); @@ -388,20 +420,6 @@ function() { return $response; }, 10, 3); - // Delete the associated file when a post is deleted - // @TODO: Rethink this. We have a separate endpoint for deleting an entire path. - // Do we need a separate hook at all? - // add_action('before_delete_post', function($post_id) { - // $post = get_post($post_id); - // if ($post && $post->post_type === WP_LOCAL_FILE_POST_TYPE) { - // $fs = self::get_fs(); - // $path = get_post_meta($post_id, 'local_file_path', true); - // if ($path && $fs->is_file($path)) { - // $fs->rm($path); - // } - // } - // }); - // Update the file after post is saved add_action('save_post_' . WP_LOCAL_FILE_POST_TYPE, function($post_id, $post, $update) { try { @@ -485,6 +503,14 @@ static public function resize_to_max_dimensions_if_files_is_an_image($image_path } $editor->resize(WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION, WP_STATIC_FILES_EDITOR_IMAGE_MAX_DIMENSION, false); + + // Rotate the image if needed + if(function_exists('exif_read_data')) { + $exif = @exif_read_data($image_path); + if($exif && isset($exif['Orientation'])) { + $editor->rotate(exif_imagetype($image_path)); + } + } // Try saving to original path first $result = $editor->save($image_path); diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index bb9bae1914..019472c1e7 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -706,6 +706,25 @@ const NodeRow: React.FC<{ return a.type === 'folder' ? -1 : 1; }) : []; + + // @TODO: Make the server return a reliable information such as + // "type" => "image" or "type" => "document" instead of + // infering things on the frontend. + const isImage = + node.post_type === 'attachment' || + node.name.endsWith('.jpg') || + node.name.endsWith('.jpeg') || + node.name.endsWith('.png') || + node.name.endsWith('.gif') || + node.name.endsWith('.bmp') || + node.name.endsWith('.tiff') || + node.name.endsWith('.ico') || + node.name.endsWith('.webp'); + + console.log({ + node, + isImage, + }); return ( <> {!isRoot && ( @@ -787,6 +806,12 @@ const NodeRow: React.FC<{ } level={level} /> + {isImage && ( + + )}
repo->commit($options)) { return false; } + /** + * Auto push if enabled + * + * This is a risky, best-effort PoC for automatic synchronization + * of changes with the remote repository. There's no conflict + * resolution here, only force overwriting of changes both locally + * and in the remote repository. + * + * Let's re-work this once the notes management prototype is more mature. + */ if($this->auto_push) { + if($this->client->force_push_one_commit()) { + return true; + } + + // If push failed, force pull and retry + if(false === $this->client->force_pull()) { + // If this failed, we're out of luck + return false; + } + + // If pull succeeded, try committing and pushing again + if(false === $this->repo->commit($options)) { + return false; + } + if(false === $this->client->force_push_one_commit()) { return false; } From 86ab57d07c521b211da47598cf48759a7765f172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 22:30:40 +0100 Subject: [PATCH 51/71] Derive WP_Block_Markup_Processor from WP_HTML_Tag_Processor, not WP_HTML_Processor, to avoid bailing on unsupported structures --- .../src/block-markup/WP_Block_Markup_Processor.php | 2 +- .../src/block-markup/WP_Block_Markup_Url_Processor.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Processor.php b/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Processor.php index c9fd47a02a..0640fd17f9 100644 --- a/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Processor.php +++ b/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Processor.php @@ -11,7 +11,7 @@ * If the post cannot fit into memory, WordPress won't be able to render it * anyway. */ -class WP_Block_Markup_Processor extends WP_HTML_Processor { +class WP_Block_Markup_Processor extends WP_HTML_Tag_Processor { private $block_name; protected $block_attributes; diff --git a/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Url_Processor.php b/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Url_Processor.php index 8081a23613..194964f2e4 100644 --- a/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Url_Processor.php +++ b/packages/playground/data-liberation/src/block-markup/WP_Block_Markup_Url_Processor.php @@ -22,7 +22,7 @@ class WP_Block_Markup_Url_Processor extends WP_Block_Markup_Processor { * @return WP_Block_Markup_Url_Processor */ public static function create_from_html( $html, $base_url_string = null ) { - $processor = static::create_fragment( $html ); + $processor = new static( $html ); $processor->base_url_string = $base_url_string; $processor->base_url_object = $base_url_string ? WP_URL::parse( $base_url_string ) : null; return $processor; From 6be33e30b7d4f0405308d674b82ad629a001b8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jan 2025 00:39:22 +0100 Subject: [PATCH 52/71] Preserve the correct last href in Blocks -> Markdown conversion --- .../src/WP_Blocks_To_Markdown.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 634a9498d4..0ec631599e 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -82,10 +82,10 @@ private function block_to_markdown($block) { $attributes['alt'] = $processor->get_attribute('alt'); } } - $escaped_url = $attributes['url'] ?? ''; - // @TODO: Figure out the correct markdown escaping for these things - $escaped_url = str_replace(' ', '%20', $escaped_url); - $escaped_url = str_replace(')', '%29', $escaped_url); + + $escaped_url = self::escape_url( + $attributes['url'] ?? '' + ); $escaped_alt = $attributes['alt'] ?? ''; $escaped_alt = str_replace(['[', ']'], '', $escaped_alt); @@ -261,6 +261,7 @@ private function html_to_markdown($html, $parents = []) { $processor = WP_Data_Liberation_HTML_Processor::create_fragment($html); $markdown = ''; + $last_href = null; while ($processor->next_token()) { if ($processor->get_token_type() === '#text') { $markdown .= $processor->get_modifiable_text(); @@ -269,7 +270,6 @@ private function html_to_markdown($html, $parents = []) { continue; } - $last_href = null; $tag_name = $processor->get_tag(); $sign = $processor->is_tag_closer() ? '-' : ( $processor->expects_closer() ? '+' : '' @@ -303,12 +303,15 @@ private function html_to_markdown($html, $parents = []) { break; case '+A': - $last_href = $processor->get_attribute('href') ?? ''; + $last_href = self::escape_url( + $processor->get_attribute('href') ?? '' + ); $markdown .= '['; break; case '-A': $markdown .= "]($last_href)"; + $last_href = null; break; case 'BR': @@ -326,6 +329,13 @@ private function html_to_markdown($html, $parents = []) { return $markdown; } + // @TODO: Figure out the correct markdown escaping for URLs + static private function escape_url($url) { + $escaped_url = str_replace(' ', '%20', $url); + $escaped_url = str_replace(')', '%29', $escaped_url); + return $escaped_url; + } + private function has_parent($parent) { return in_array($parent, $this->parents, true); } From 1f27630b50d189a03b105e0ada1d873ab2bd0217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jan 2025 02:03:22 +0100 Subject: [PATCH 53/71] Don't explicitly force pull after writes --- .../plugin.php | 18 ------------------ .../data-liberation/blueprints-library | 2 +- .../playground/data-liberation/bootstrap.php | 1 + 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 579c9b9023..533307748d 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -196,9 +196,6 @@ static public function initialize() { 'to_fs' => $main_fs, ]); - // Force pull after every write operation - self::$client->force_pull(); - return $metadata; } finally { self::release_synchronization_lock(); @@ -242,9 +239,6 @@ static public function initialize() { 'to_fs' => $main_fs, ]); - // Force pull after every write operation - self::$client->force_pull(); - return $metadata; } finally { self::release_synchronization_lock(); @@ -1059,10 +1053,6 @@ static public function move_file_endpoint($request) { )); } - // Pull new changes from the remote repository after - // performing a write operation. - self::$client->force_pull(); - return array('success' => true); } finally { self::release_synchronization_lock(); @@ -1149,10 +1139,6 @@ static public function create_files_endpoint($request) { } } - // Pull new changes from the remote repository after - // performing a write operation. - self::$client->force_pull(); - return array( 'created_files' => $created_files ); @@ -1195,10 +1181,6 @@ static public function delete_path_endpoint($request) { } } - // Pull new changes from the remote repository after - // performing a write operation. - self::$client->force_pull(); - return array('success' => true); } finally { self::release_synchronization_lock(); diff --git a/packages/playground/data-liberation/blueprints-library b/packages/playground/data-liberation/blueprints-library index c0ea73c245..b35b3e416d 160000 --- a/packages/playground/data-liberation/blueprints-library +++ b/packages/playground/data-liberation/blueprints-library @@ -1 +1 @@ -Subproject commit c0ea73c2450ef8cf4644fcc475f2e85324c75a57 +Subproject commit b35b3e416d022fa2a3e31d3a43754a867c4291a3 diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 9aac3947c4..a9c6cbacd4 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -18,6 +18,7 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_File_Visitor_Event.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Visitor.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Chroot.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Google_Drive_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Uploaded_Directory_Tree_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_Byte_Reader.php'; From 2ad968b3165406bbeb2de375bc4365c26c04159d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jan 2025 18:02:25 +0100 Subject: [PATCH 54/71] Rudimentary commit diffing and text diffing --- .../playground/data-liberation/bootstrap.php | 1 + .../src/git/WP_Git_Diff_Engine.php | 158 ++++++++++++++++++ .../src/git/WP_Git_Repository.php | 122 +++++++++++++- 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index a9c6cbacd4..5d5419f9b8 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -92,6 +92,7 @@ require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; require_once __DIR__ . '/src/git/WP_Git_Repository.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; +require_once __DIR__ . '/src/git/WP_Git_Diff_Engine.php'; require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; require_once __DIR__ . '/src/utf8_decoder.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php b/packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php new file mode 100644 index 0000000000..07be4e3d4a --- /dev/null +++ b/packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php @@ -0,0 +1,158 @@ +calculateLCS($oldLines, $newLines); + + $oldIndex = 0; + $newIndex = 0; + $changes = []; + + foreach ($lcs as $match) { + while ($oldIndex < $match['oldIndex'] || $newIndex < $match['newIndex']) { + if ($oldIndex < $match['oldIndex']) { + $changes[] = ['type' => '-', 'line' => $oldLines[$oldIndex], 'oldIndex' => $oldIndex, 'newIndex' => null]; + $oldIndex++; + } + if ($newIndex < $match['newIndex']) { + $changes[] = ['type' => '+', 'line' => $newLines[$newIndex], 'oldIndex' => null, 'newIndex' => $newIndex]; + $newIndex++; + } + } + + // Add matching line as context + if ($oldIndex < count($oldLines) && $newIndex < count($newLines)) { + $changes[] = ['type' => ' ', 'line' => $oldLines[$oldIndex], 'oldIndex' => $oldIndex, 'newIndex' => $newIndex]; + $oldIndex++; + $newIndex++; + } + } + + // Add remaining lines + while ($oldIndex < count($oldLines)) { + $changes[] = ['type' => '-', 'line' => $oldLines[$oldIndex], 'oldIndex' => $oldIndex, 'newIndex' => null]; + $oldIndex++; + } + while ($newIndex < count($newLines)) { + $changes[] = ['type' => '+', 'line' => $newLines[$newIndex], 'oldIndex' => null, 'newIndex' => $newIndex]; + $newIndex++; + } + + return $changes; + } + + public function formatAsGit($changes, $options = []) { + $options['contextLines'] ??= 3; + $options['a_source'] ??= 'a/string'; + $options['b_source'] ??= 'b/string'; + + // Format the diff to Git-style with context + $formattedDiff = "diff --git " . $options['a_source'] . " " . $options['b_source'] . "\n"; + $formattedDiff .= "--- " . $options['a_source'] . "\n"; + $formattedDiff .= "+++ " . $options['b_source'] . "\n"; + + $changeBlocks = []; + $currentBlock = []; + + $last_changed_lineno = null; + foreach ($changes as $lineno => $change) { + if ($change['type'] === ' ') { + if(empty($currentBlock)) { + continue; + } + if($lineno - $last_changed_lineno > $options['contextLines']) { + $changeBlocks[] = $currentBlock; + $currentBlock = []; + continue; + } + } else if(empty($currentBlock)) { + $offset = max(0, $lineno - $options['contextLines'] - 1); + $length = min($options['contextLines'], count($changes) - $offset) - 1; + $currentBlock = array_slice($changes, $offset, $length); + } + + $currentBlock[] = $change; + + if($change['type'] !== ' ') { + $last_changed_lineno = $lineno; + } + } + + if(!empty($currentBlock)) { + $changeBlocks[] = $currentBlock; + } + + foreach ($changeBlocks as $changes) { + $block = ''; + $oldStart = null; + $newStart = null; + $oldCount = 0; + $newCount = 0; + + foreach ($changes as $change) { + if ($change['type'] !== '+') { + if ($oldStart === null) $oldStart = $change['oldIndex']; + $oldCount++; + } + if ($change['type'] !== '-') { + if ($newStart === null) $newStart = $change['newIndex']; + $newCount++; + } + } + + $oldStart = $oldStart !== null ? $oldStart + 1 : 0; + $newStart = $newStart !== null ? $newStart + 1 : 0; + + $block .= sprintf("@@ -%d,%d +%d,%d @@", $oldStart, $oldCount, $newStart, $newCount); + + foreach ($changes as $change) { + $block .= $change['type'] . ' ' . $change['line'] . "\n"; + } + + $formattedDiff .= $block; + } + + return $formattedDiff; + } + + private function calculateLCS($oldLines, $newLines) { + $oldLen = count($oldLines); + $newLen = count($newLines); + $lcsMatrix = array_fill(0, $oldLen + 1, array_fill(0, $newLen + 1, 0)); + + // Build the LCS matrix + for ($i = 1; $i <= $oldLen; $i++) { + for ($j = 1; $j <= $newLen; $j++) { + if ($oldLines[$i - 1] === $newLines[$j - 1]) { + $lcsMatrix[$i][$j] = $lcsMatrix[$i - 1][$j - 1] + 1; + } else { + $lcsMatrix[$i][$j] = max($lcsMatrix[$i - 1][$j], $lcsMatrix[$i][$j - 1]); + } + } + } + + // Backtrack to find the LCS + $lcs = []; + $i = $oldLen; + $j = $newLen; + while ($i > 0 && $j > 0) { + if ($oldLines[$i - 1] === $newLines[$j - 1]) { + $lcs[] = ['oldIndex' => $i - 1, 'newIndex' => $j - 1]; + $i--; + $j--; + } elseif ($lcsMatrix[$i - 1][$j] >= $lcsMatrix[$i][$j - 1]) { + $i--; + } else { + $j--; + } + } + + return array_reverse($lcs); + } + +} + diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index d9e1150f6d..9a45ad5ee8 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -117,13 +117,20 @@ class WP_Git_Repository { private $buffered_object_content; private $last_error; + /** + * @var WP_Git_Diff_Engine + */ + private $diff_engine; + private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; private const NULL_OID = '0000000000000000000000000000000000000000'; public function __construct( - WP_Abstract_Filesystem $fs + WP_Abstract_Filesystem $fs, + $options = [] ) { $this->fs = $fs; + $this->diff_engine = $options['diff_engine'] ?? new WP_Git_Diff_Engine(); $this->initialize_filesystem(); } @@ -687,6 +694,119 @@ public function commit($options=[]) { return $commit_oid; } + public function diff_commits($current_oid, $previous_oid) { + if(false === $this->read_object($current_oid)) { + return false; + } + $current_commit = $this->get_parsed_commit(); + $current_tree_oid = $current_commit['tree']; + + if(false === $this->read_object($previous_oid)) { + return false; + } + $previous_commit = $this->get_parsed_commit(); + $previous_tree_oid = $previous_commit['tree']; + + return $this->diff_trees($current_tree_oid, $previous_tree_oid); + } + + public function diff_trees($current_oid, $previous_oid) { + if(false === $this->read_object($current_oid)) { + return false; + } + $current_tree = $this->get_parsed_tree(); + + if(false === $this->read_object($previous_oid)) { + return false; + } + $previous_tree = $this->get_parsed_tree(); + + $diff = []; + foreach($current_tree as $name => $current_entry) { + if(!isset($previous_tree[$name])) { + $diff[$name] = $current_entry; + continue; + } + $previous_entry = $previous_tree[$name]; + if($current_entry['sha1'] === $previous_entry['sha1']) { + continue; + } + + if($current_entry['mode'] !== $previous_entry['mode']) { + /* + * @TODO: Account for a scenario when just one text file changes and + * also the mode changed from executable to non-executable. + * We could do a text diff in that case. + */ + $diff[$name] = $current_entry; + continue; + } + + $diff[$name] = [ + 'name' => $name, + 'mode' => 'diff', + 'sha1' => $current_entry['sha1'], + ]; + + if($current_entry['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { + $diff[$name]['diff'] = $this->diff_trees($current_entry['sha1'], $previous_entry['sha1']); + } else { + $diff[$name]['diff'] = $this->diff_blobs( + $current_entry, + $previous_entry + ); + } + } + + foreach($previous_tree as $name => $previous_entry) { + if(!isset($current_tree[$name])) { + $diff[$name] = self::DELETE_PLACEHOLDER; + } + } + return $diff; + } + + public function diff_blobs($current_blob_entry, $previous_blob_entry) { + if(false === $this->read_object($current_blob_entry['sha1'])) { + return false; + } + // @TODO: Support streaming diffs for large files + $current_blob = $this->read_entire_object_contents(); + $current_blob_is_binary = $this->guess_if_binary_blob($current_blob_entry, $current_blob); + + if(false === $this->read_object($previous_blob_entry['sha1'])) { + return false; + } + $previous_blob = $this->read_entire_object_contents(); + $previous_blob_is_binary = $this->guess_if_binary_blob($previous_blob_entry, $previous_blob); + + if($current_blob_is_binary && $previous_blob_is_binary) { + return ['type' => 'binary']; + } else if($current_blob_is_binary ^ $previous_blob_is_binary) { + return ['type' => 'completely_new_blob']; + } else { + return [ + 'type' => 'text_diff', + 'diff' => $this->diff_engine->diff($current_blob, $previous_blob) + ]; + } + } + + static private function guess_if_binary_blob($blob_entry, $blob_contents) { + $name = $blob_entry['name']; + $extension = pathinfo($name, PATHINFO_EXTENSION); + if(in_array($extension, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'tiff', 'tif', 'raw', 'heic', 'heif', 'avif'])) { + return true; + } + + // Naively assume null bytes only occur in binary files + if(strpos($blob_contents, "\0") !== false) { + return true; + } + + return false; + } + public function squash($squash_into_commit_oid, $squash_until_ancestor_oid) { // Find the parent of the squashed range $this->read_object($squash_until_ancestor_oid); From 12c3fb238248a0b068c9b71292b211b210cad8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jan 2025 18:04:12 +0100 Subject: [PATCH 55/71] Clarify a TODO comment --- .../playground/data-liberation/src/git/WP_Git_Repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 9a45ad5ee8..31b3e89763 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -734,7 +734,7 @@ public function diff_trees($current_oid, $previous_oid) { if($current_entry['mode'] !== $previous_entry['mode']) { /* - * @TODO: Account for a scenario when just one text file changes and + * @TODO: Account for a scenario when just one text line changes and * also the mode changed from executable to non-executable. * We could do a text diff in that case. */ From b13b8337c7078e587b239a85716f187d2983c6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 3 Jan 2025 18:34:53 +0100 Subject: [PATCH 56/71] Implement three way merge --- .../playground/data-liberation/bootstrap.php | 2 +- .../src/git/WP_Git_Diff_Engine.php | 158 ------------ .../src/git/WP_Git_Merge_Engine.php | 227 ++++++++++++++++++ .../src/git/WP_Git_Repository.php | 2 +- 4 files changed, 229 insertions(+), 160 deletions(-) delete mode 100644 packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php create mode 100644 packages/playground/data-liberation/src/git/WP_Git_Merge_Engine.php diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 5d5419f9b8..a6dcfb8a07 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -92,7 +92,7 @@ require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; require_once __DIR__ . '/src/git/WP_Git_Repository.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; -require_once __DIR__ . '/src/git/WP_Git_Diff_Engine.php'; +require_once __DIR__ . '/src/git/WP_Git_Merge_Engine.php'; require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; require_once __DIR__ . '/src/utf8_decoder.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php b/packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php deleted file mode 100644 index 07be4e3d4a..0000000000 --- a/packages/playground/data-liberation/src/git/WP_Git_Diff_Engine.php +++ /dev/null @@ -1,158 +0,0 @@ -calculateLCS($oldLines, $newLines); - - $oldIndex = 0; - $newIndex = 0; - $changes = []; - - foreach ($lcs as $match) { - while ($oldIndex < $match['oldIndex'] || $newIndex < $match['newIndex']) { - if ($oldIndex < $match['oldIndex']) { - $changes[] = ['type' => '-', 'line' => $oldLines[$oldIndex], 'oldIndex' => $oldIndex, 'newIndex' => null]; - $oldIndex++; - } - if ($newIndex < $match['newIndex']) { - $changes[] = ['type' => '+', 'line' => $newLines[$newIndex], 'oldIndex' => null, 'newIndex' => $newIndex]; - $newIndex++; - } - } - - // Add matching line as context - if ($oldIndex < count($oldLines) && $newIndex < count($newLines)) { - $changes[] = ['type' => ' ', 'line' => $oldLines[$oldIndex], 'oldIndex' => $oldIndex, 'newIndex' => $newIndex]; - $oldIndex++; - $newIndex++; - } - } - - // Add remaining lines - while ($oldIndex < count($oldLines)) { - $changes[] = ['type' => '-', 'line' => $oldLines[$oldIndex], 'oldIndex' => $oldIndex, 'newIndex' => null]; - $oldIndex++; - } - while ($newIndex < count($newLines)) { - $changes[] = ['type' => '+', 'line' => $newLines[$newIndex], 'oldIndex' => null, 'newIndex' => $newIndex]; - $newIndex++; - } - - return $changes; - } - - public function formatAsGit($changes, $options = []) { - $options['contextLines'] ??= 3; - $options['a_source'] ??= 'a/string'; - $options['b_source'] ??= 'b/string'; - - // Format the diff to Git-style with context - $formattedDiff = "diff --git " . $options['a_source'] . " " . $options['b_source'] . "\n"; - $formattedDiff .= "--- " . $options['a_source'] . "\n"; - $formattedDiff .= "+++ " . $options['b_source'] . "\n"; - - $changeBlocks = []; - $currentBlock = []; - - $last_changed_lineno = null; - foreach ($changes as $lineno => $change) { - if ($change['type'] === ' ') { - if(empty($currentBlock)) { - continue; - } - if($lineno - $last_changed_lineno > $options['contextLines']) { - $changeBlocks[] = $currentBlock; - $currentBlock = []; - continue; - } - } else if(empty($currentBlock)) { - $offset = max(0, $lineno - $options['contextLines'] - 1); - $length = min($options['contextLines'], count($changes) - $offset) - 1; - $currentBlock = array_slice($changes, $offset, $length); - } - - $currentBlock[] = $change; - - if($change['type'] !== ' ') { - $last_changed_lineno = $lineno; - } - } - - if(!empty($currentBlock)) { - $changeBlocks[] = $currentBlock; - } - - foreach ($changeBlocks as $changes) { - $block = ''; - $oldStart = null; - $newStart = null; - $oldCount = 0; - $newCount = 0; - - foreach ($changes as $change) { - if ($change['type'] !== '+') { - if ($oldStart === null) $oldStart = $change['oldIndex']; - $oldCount++; - } - if ($change['type'] !== '-') { - if ($newStart === null) $newStart = $change['newIndex']; - $newCount++; - } - } - - $oldStart = $oldStart !== null ? $oldStart + 1 : 0; - $newStart = $newStart !== null ? $newStart + 1 : 0; - - $block .= sprintf("@@ -%d,%d +%d,%d @@", $oldStart, $oldCount, $newStart, $newCount); - - foreach ($changes as $change) { - $block .= $change['type'] . ' ' . $change['line'] . "\n"; - } - - $formattedDiff .= $block; - } - - return $formattedDiff; - } - - private function calculateLCS($oldLines, $newLines) { - $oldLen = count($oldLines); - $newLen = count($newLines); - $lcsMatrix = array_fill(0, $oldLen + 1, array_fill(0, $newLen + 1, 0)); - - // Build the LCS matrix - for ($i = 1; $i <= $oldLen; $i++) { - for ($j = 1; $j <= $newLen; $j++) { - if ($oldLines[$i - 1] === $newLines[$j - 1]) { - $lcsMatrix[$i][$j] = $lcsMatrix[$i - 1][$j - 1] + 1; - } else { - $lcsMatrix[$i][$j] = max($lcsMatrix[$i - 1][$j], $lcsMatrix[$i][$j - 1]); - } - } - } - - // Backtrack to find the LCS - $lcs = []; - $i = $oldLen; - $j = $newLen; - while ($i > 0 && $j > 0) { - if ($oldLines[$i - 1] === $newLines[$j - 1]) { - $lcs[] = ['oldIndex' => $i - 1, 'newIndex' => $j - 1]; - $i--; - $j--; - } elseif ($lcsMatrix[$i - 1][$j] >= $lcsMatrix[$i][$j - 1]) { - $i--; - } else { - $j--; - } - } - - return array_reverse($lcs); - } - -} - diff --git a/packages/playground/data-liberation/src/git/WP_Git_Merge_Engine.php b/packages/playground/data-liberation/src/git/WP_Git_Merge_Engine.php new file mode 100644 index 0000000000..003242eb4b --- /dev/null +++ b/packages/playground/data-liberation/src/git/WP_Git_Merge_Engine.php @@ -0,0 +1,227 @@ + '!', 'line1' => $change1['line'], 'line2' => $change2['line']]; + $index1++; + $index2++; + } else { + $merged[] = $change1; + $index1++; + $index2++; + } + } elseif ($change1['type'] === '-' && $change2['type'] === '-') { + $merged[] = $change1; + $index1++; + $index2++; + } elseif ($change1['type'] === '-' || $change2['type'] === '-') { + // Line removed in one diff but not the other + if ($change1['type'] === '-') { + $merged[] = $change1; + } + if ($change2['type'] === '-') { + $merged[] = $change2; + } + $index1++; + $index2++; + } elseif ($change1['type'] === '+') { + $merged[] = $change1; + $index1++; + } elseif ($change2['type'] === '+') { + $merged[] = $change2; + $index2++; + } else { + $conflicts[] = ['type' => '!', 'line1' => $change1['line'], 'line2' => $change2['line']]; + $index1++; + $index2++; + } + } elseif ($index1 < count($diff1)) { + $merged[] = $diff1[$index1]; + $index1++; + } elseif ($index2 < count($diff2)) { + $merged[] = $diff2[$index2]; + $index2++; + } + } + + if (!empty($conflicts)) { + foreach ($conflicts as $conflict) { + $merged[] = ['type' => '!', 'line' => "CONFLICT: " . $conflict['line1'] . " | " . $conflict['line2']]; + } + } + + return $merged; + } + + public function diff($old_string, $new_string) { + $old_lines = explode("\n", $old_string); + $new_lines = explode("\n", $new_string); + + $lcs = $this->calculate_longest_common_subsequence($old_lines, $new_lines); + + $old_index = 0; + $new_index = 0; + $changes = []; + + foreach ($lcs as $match) { + while ($old_index < $match['old_index'] || $new_index < $match['new_index']) { + if ($old_index < $match['old_index']) { + $changes[] = ['type' => '-', 'line' => $old_lines[$old_index], 'old_index' => $old_index, 'new_index' => null]; + $old_index++; + } + if ($new_index < $match['new_index']) { + $changes[] = ['type' => '+', 'line' => $new_lines[$new_index], 'old_index' => null, 'new_index' => $new_index]; + $new_index++; + } + } + + // Add matching line as context + if ($old_index < count($old_lines) && $new_index < count($new_lines)) { + $changes[] = ['type' => ' ', 'line' => $old_lines[$old_index], 'old_index' => $old_index, 'new_index' => $new_index]; + $old_index++; + $new_index++; + } + } + + // Add remaining lines + while ($old_index < count($old_lines)) { + $changes[] = ['type' => '-', 'line' => $old_lines[$old_index], 'old_index' => $old_index, 'new_index' => null]; + $old_index++; + } + while ($new_index < count($new_lines)) { + $changes[] = ['type' => '+', 'line' => $new_lines[$new_index], 'old_index' => null, 'new_index' => $new_index]; + $new_index++; + } + + return $changes; + } + + public function format_as_git($changes, $options = []) { + $options['contextLines'] ??= 3; + $options['a_source'] ??= 'a/string'; + $options['b_source'] ??= 'b/string'; + + // Format the diff to Git-style with context + $formatted_diff = "diff --git " . $options['a_source'] . " " . $options['b_source'] . "\n"; + $formatted_diff .= "--- " . $options['a_source'] . "\n"; + $formatted_diff .= "+++ " . $options['b_source'] . "\n"; + + $changed_blocks = []; + $current_block = []; + + $last_changed_lineno = null; + foreach ($changes as $lineno => $change) { + if ($change['type'] === ' ') { + if(empty($current_block)) { + continue; + } + if($lineno - $last_changed_lineno > $options['contextLines']) { + $changed_blocks[] = $current_block; + $current_block = []; + continue; + } + } else if(empty($current_block)) { + $offset = max(0, $lineno - $options['contextLines'] - 1); + $length = min($options['contextLines'], count($changes) - $offset) - 1; + $current_block = array_slice($changes, $offset, $length); + } + + $current_block[] = $change; + + if($change['type'] !== ' ') { + $last_changed_lineno = $lineno; + } + } + + if(!empty($current_block)) { + $changed_blocks[] = $current_block; + } + + foreach ($changed_blocks as $changes) { + $block = ''; + $old_start = null; + $new_start = null; + $oldCount = 0; + $newCount = 0; + + foreach ($changes as $change) { + if ($change['type'] !== '+') { + if ($old_start === null) $old_start = $change['old_index']; + $oldCount++; + } + if ($change['type'] !== '-') { + if ($new_start === null) $new_start = $change['new_index']; + $newCount++; + } + } + + $old_start = $old_start !== null ? $old_start + 1 : 0; + $new_start = $new_start !== null ? $new_start + 1 : 0; + + $block .= sprintf("@@ -%d,%d +%d,%d @@", $old_start, $oldCount, $new_start, $newCount); + + foreach ($changes as $change) { + $block .= $change['type'] . ' ' . $change['line'] . "\n"; + } + + $formatted_diff .= $block; + } + + return $formatted_diff; + } + + private function calculate_longest_common_subsequence($old_lines, $new_lines) { + $old_len = count($old_lines); + $new_len = count($new_lines); + $lcsMatrix = array_fill(0, $old_len + 1, array_fill(0, $new_len + 1, 0)); + + // Build the LCS matrix + for ($i = 1; $i <= $old_len; $i++) { + for ($j = 1; $j <= $new_len; $j++) { + if ($old_lines[$i - 1] === $new_lines[$j - 1]) { + $lcsMatrix[$i][$j] = $lcsMatrix[$i - 1][$j - 1] + 1; + } else { + $lcsMatrix[$i][$j] = max($lcsMatrix[$i - 1][$j], $lcsMatrix[$i][$j - 1]); + } + } + } + + // Backtrack to find the LCS + $lcs = []; + $i = $old_len; + $j = $new_len; + while ($i > 0 && $j > 0) { + if ($old_lines[$i - 1] === $new_lines[$j - 1]) { + $lcs[] = ['old_index' => $i - 1, 'new_index' => $j - 1]; + $i--; + $j--; + } elseif ($lcsMatrix[$i - 1][$j] >= $lcsMatrix[$i][$j - 1]) { + $i--; + } else { + $j--; + } + } + + return array_reverse($lcs); + } + +} + diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 31b3e89763..cfcfa0ac7d 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -130,7 +130,7 @@ public function __construct( $options = [] ) { $this->fs = $fs; - $this->diff_engine = $options['diff_engine'] ?? new WP_Git_Diff_Engine(); + $this->diff_engine = $options['diff_engine'] ?? new WP_Git_Merge_Engine(); $this->initialize_filesystem(); } From 65c73bbd77ffeea31ba85eb544d65c0168e79294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 4 Jan 2025 18:54:14 +0100 Subject: [PATCH 57/71] Support for the HTML format and changing file extensions --- .../src/WP_Blocks_To_Markdown.php | 7 +- .../src/WP_Markdown_To_Blocks.php | 10 +- .../plugin.php | 154 ++++++++++++------ .../src/components/FilePickerTree/index.tsx | 4 - .../ui/src/index.tsx | 8 +- .../playground/data-liberation/bootstrap.php | 2 + .../block-markup/WP_Block_HTML_Serializer.php | 34 ++++ .../WP_HTML_With_Blocks_to_Blocks.php | 67 ++++++++ .../entity-readers/WP_EPub_Entity_Reader.php | 9 +- .../entity-readers/WP_HTML_Entity_Reader.php | 22 +-- .../tests/WPHTMLEntityReaderTests.php | 10 +- 11 files changed, 254 insertions(+), 73 deletions(-) create mode 100644 packages/playground/data-liberation/src/block-markup/WP_Block_HTML_Serializer.php create mode 100644 packages/playground/data-liberation/src/block-markup/WP_HTML_With_Blocks_to_Blocks.php diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 0ec631599e..1b1a12e114 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -8,6 +8,7 @@ class WP_Blocks_To_Markdown { private $state; private $parents = []; private $metadata; + private $markdown; public function __construct($block_markup, $metadata = []) { $this->block_markup = $block_markup; @@ -18,12 +19,14 @@ public function __construct($block_markup, $metadata = []) { $this->metadata = $metadata; } - private $markdown; - public function convert() { + if(null !== $this->markdown) { + return false; + } $this->markdown = ''; $this->markdown .= $this->frontmatter(); $this->markdown .= $this->blocks_to_markdown(parse_blocks($this->block_markup)); + return true; } public function get_result() { diff --git a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php index a3c609f9dc..5752d52b6b 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php +++ b/packages/playground/data-liberation-markdown/src/WP_Markdown_To_Blocks.php @@ -45,8 +45,14 @@ public function convert() { return true; } - public function get_all_metadata() { - return $this->frontmatter; + public function get_all_metadata($options=[]) { + $metadata = $this->frontmatter; + if(isset($options['first_value_only']) && $options['first_value_only']) { + $metadata = array_map(function($value) { + return $value[0]; + }, $metadata); + } + return $metadata; } public function get_meta_value( $key ) { diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 533307748d..51627f0eeb 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -429,10 +429,10 @@ function() { return; } - $path = get_post_meta($post_id, 'local_file_path', true); - $content = self::convert_post_to_string($path, $post); + $content = self::convert_post_to_string($post); $fs = self::get_fs(); + $path = get_post_meta($post_id, 'local_file_path', true); $fs->put_contents($path, $content, [ 'message' => 'User saved ' . $post->post_title, ]); @@ -460,12 +460,13 @@ function() { return; } + $content = self::convert_post_to_string($autosave); + $path = wp_join_paths( '/' . WP_AUTOSAVES_DIRECTORY . '/', get_post_meta($parent_post->ID, 'local_file_path', true) ); $fs = self::get_fs(); - $content = self::convert_post_to_string($path, $autosave); $fs->put_contents($path, $content, [ 'amend' => true, ]); @@ -524,7 +525,7 @@ static public function resize_to_max_dimensions_if_files_is_an_image($image_path } static public function download_file_endpoint($request) { - $path = $request->get_param('path'); + $path = wp_canonicalize_path($request->get_param('path')); $fs = self::get_fs(); if($fs->is_dir($path)) { @@ -613,27 +614,15 @@ static private function refresh_post_from_local_file($post) { return false; } $extension = pathinfo($path, PATHINFO_EXTENSION); - switch($extension) { - case 'xhtml': - $converter = new WP_HTML_To_Blocks( WP_XML_Processor::create_from_string( $content ) ); - break; - case 'html': - $converter = new WP_HTML_To_Blocks( WP_HTML_Processor::create_fragment( $content ) ); - break; - case 'md': - $converter = new WP_Markdown_To_Blocks( $content ); - break; - default: - return false; + $converter = self::parse_local_file($content, $extension); + if(!$converter) { + return false; } - $converter->convert(); - $metadata = []; - foreach($converter->get_all_metadata() as $key => $value) { - $metadata[$key] = $value[0]; - } - $new_content = $converter->get_block_markup(); - $new_content = self::wordpressify_static_assets_urls($new_content); + $new_content = self::wordpressify_static_assets_urls( + $converter->get_block_markup() + ); + $metadata = $converter->get_all_metadata(['first_value_only' => true]); $updated = wp_update_post(array( 'ID' => $post_id, @@ -653,10 +642,38 @@ static private function refresh_post_from_local_file($post) { } } - static private function convert_post_to_string($path, $post) { - $post_id = $post->ID; + static private function parse_local_file($content, $format) { + switch($format) { + case 'xhtml': + $converter = new WP_HTML_To_Blocks( WP_XML_Processor::create_from_string( $content ) ); + break; + case 'html': + $converter = new WP_HTML_With_Blocks_to_Blocks( $content ); + break; + case 'md': + $converter = new WP_Markdown_To_Blocks( $content ); + break; + default: + return false; + } + if(!$converter->convert()) { + throw new Exception('Failed to convert post to string'); + return false; + } + return $converter; + } + + static private function convert_post_to_string($post, $format=null) { + if($format === null) { + $path = get_post_meta($post->ID, 'local_file_path', true); + if($path) { + $format = pathinfo($path, PATHINFO_EXTENSION); + } + } + if($format === null) { + $format = 'html'; + } - $extension = pathinfo($path, PATHINFO_EXTENSION); $metadata = []; foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { $metadata[$key] = get_post_field($key, $post->ID); @@ -664,20 +681,34 @@ static private function convert_post_to_string($path, $post) { // @TODO: Also include actual post_meta. Which ones? All? The // ones explicitly set by the user in the editor? - $content = get_post_field('post_content', $post_id); - $content = self::unwordpressify_static_assets_urls($content); + return self::convert_post_data_to_string( + $post->post_content, + $metadata, + $format + ); + } - switch($extension) { - // @TODO: Add support for HTML and XHTML - case 'html': - case 'xhtml': + static private function convert_post_data_to_string($block_markup, $metadata, $format) { + $content = self::unwordpressify_static_assets_urls( + $block_markup + ); + + switch($format) { case 'md': $converter = new WP_Blocks_To_Markdown( $content, $metadata ); break; + case 'xhtml': + // @TODO: Add proper support for XHTML – perhaps via the serialize() method? + throw new Exception('Serializing to XHTML is not supported yet'); + case 'html': default: - return ''; + $converter = new WP_Block_HTML_Serializer( $content, $metadata ); + break; + } + if(!$converter->convert()) { + throw new Exception('Failed to convert post to string'); + return false; } - $converter->convert(); return $converter->get_result(); } @@ -953,8 +984,7 @@ static public function get_or_create_post_for_file($request) { return new WP_Error('synchronization_lock_failed', 'Failed to acquire synchronization lock'); } - $file_path = $request->get_param('path'); - $file_path = '/' . ltrim($file_path, '/'); + $file_path = wp_canonicalize_path($request->get_param('path')); $create_file = $request->get_param('create_file'); if (!$file_path) { @@ -1025,32 +1055,60 @@ static public function move_file_endpoint($request) { if(!self::acquire_synchronization_lock()) { return; } - $from_path = $request->get_param('fromPath'); - $to_path = $request->get_param('toPath'); + $from_path = wp_canonicalize_path($request->get_param('fromPath')); + $to_path = wp_canonicalize_path($request->get_param('toPath')); if (!$from_path || !$to_path) { return new WP_Error('missing_path', 'Both source and target paths are required'); } + $fs = self::get_fs(); + if(!$fs->is_file($from_path)) { + return new WP_Error('move_failed', sprintf('Failed to move file – source path is not a file (%s)', $from_path)); + } + // Find and update associated post + $previous_content = $fs->get_contents($from_path); $existing_posts = get_posts(array( 'post_type' => WP_LOCAL_FILE_POST_TYPE, 'meta_key' => 'local_file_path', 'meta_value' => $from_path, 'posts_per_page' => 1 )); + $existing_post = count($existing_posts) > 0 ? $existing_posts[0] : null; + + $moved = false; + + if ($existing_post) { + // Regenerate the content from scratch if we're changing the file format. + $previous_extension = pathinfo($from_path, PATHINFO_EXTENSION); + $new_extension = pathinfo($to_path, PATHINFO_EXTENSION); + if($existing_post->post_type === WP_LOCAL_FILE_POST_TYPE && $previous_extension !== $new_extension) { + $converter = self::parse_local_file( + $previous_content, + $previous_extension + ); + $new_content = self::convert_post_data_to_string( + $converter->get_block_markup(), + $converter->get_all_metadata(['first_value_only' => true]), + $new_extension + ); + $fs->put_contents( + $to_path, + $new_content + ); + $fs->rm($from_path); + $moved = true; + } - $fs = self::get_fs(); - if (!$fs->rename($from_path, $to_path)) { - return new WP_Error('move_failed', 'Failed to move file'); + // Update the local file path + if(false === update_post_meta($existing_post->ID, 'local_file_path', $to_path)) { + throw new Exception('Failed to update local file path'); + } } - if (!empty($existing_posts)) { - update_post_meta($existing_posts[0]->ID, 'local_file_path', $to_path); - wp_update_post(array( - 'ID' => $existing_posts[0]->ID, - 'post_title' => basename($to_path) - )); + if(!$moved) { + $fs->rename($from_path, $to_path); } return array('success' => true); @@ -1148,7 +1206,7 @@ static public function create_files_endpoint($request) { } static public function delete_path_endpoint($request) { - $path = $request->get_param('path'); + $path = wp_canonicalize_path($request->get_param('path')); if (!$path) { return new WP_Error('missing_path', 'File path is required'); } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx index 019472c1e7..369e7375ec 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/components/FilePickerTree/index.tsx @@ -721,10 +721,6 @@ const NodeRow: React.FC<{ node.name.endsWith('.ico') || node.name.endsWith('.webp'); - console.log({ - node, - isImage, - }); return ( <> {!isRoot && ( diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 6ebb292a45..12f54ce977 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -150,7 +150,13 @@ function ConnectedFilePickerTree() { }); } } - }, [hasLoadedPost, post, setPostLoading, selectedNode.postId]); + }, [ + hasLoadedPost, + post, + setPostLoading, + selectedNode.postId, + selectedNode.path, + ]); const refreshFileTree = useCallback(async () => { fileTreePromise = apiFetch({ diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index a6dcfb8a07..8088b3a5d0 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -64,6 +64,8 @@ require_once __DIR__ . '/src/block-markup/WP_URL_In_Text_Processor.php'; require_once __DIR__ . '/src/block-markup/WP_URL.php'; require_once __DIR__ . '/src/block-markup/WP_HTML_To_Blocks.php'; +require_once __DIR__ . '/src/block-markup/WP_HTML_With_Blocks_to_Blocks.php'; +require_once __DIR__ . '/src/block-markup/WP_Block_HTML_Serializer.php'; require_once __DIR__ . '/src/entity-readers/WP_Entity_Reader.php'; require_once __DIR__ . '/src/entity-readers/WP_HTML_Entity_Reader.php'; diff --git a/packages/playground/data-liberation/src/block-markup/WP_Block_HTML_Serializer.php b/packages/playground/data-liberation/src/block-markup/WP_Block_HTML_Serializer.php new file mode 100644 index 0000000000..a7c5f7d760 --- /dev/null +++ b/packages/playground/data-liberation/src/block-markup/WP_Block_HTML_Serializer.php @@ -0,0 +1,34 @@ +block_markup = $block_markup; + $this->metadata = $metadata; + } + + public function convert() { + foreach($this->metadata as $key => $value) { + $p = new WP_HTML_Tag_Processor(''); + $p->next_tag(); + $p->set_attribute('name', $key); + if(is_array($value) || is_object($value)) { + $value = json_encode($value); + } + $p->set_attribute('content', $value); + $this->result .= $p->get_updated_html() . "\n"; + } + $this->result .= $this->block_markup; + return true; + } + + public function get_result() { + return $this->result; + } +} diff --git a/packages/playground/data-liberation/src/block-markup/WP_HTML_With_Blocks_to_Blocks.php b/packages/playground/data-liberation/src/block-markup/WP_HTML_With_Blocks_to_Blocks.php new file mode 100644 index 0000000000..8dd1d36548 --- /dev/null +++ b/packages/playground/data-liberation/src/block-markup/WP_HTML_With_Blocks_to_Blocks.php @@ -0,0 +1,67 @@ +original_html = $original_html; + } + + public function convert() { + if ( self::STATE_READY !== $this->state ) { + return false; + } + + if( null === $this->parsed_blocks ) { + $this->parsed_blocks = parse_blocks( $this->original_html ); + } + + foreach($this->parsed_blocks as $block) { + if($block['blockName'] === NULL) { + $html_converter = new WP_HTML_To_Blocks(WP_HTML_Processor::create_fragment($block['innerHTML'])); + $html_converter->convert(); + $this->block_markup .= $html_converter->get_block_markup() . "\n"; + $this->metadata = array_merge($this->metadata, $html_converter->get_all_metadata()); + } else { + $this->block_markup .= serialize_block($block) . "\n"; + } + } + + $this->state = self::STATE_COMPLETE; + + return true; + } + + public function get_meta_value( $key ) { + if ( ! array_key_exists( $key, $this->metadata ) ) { + return null; + } + return $this->metadata[ $key ][0]; + } + + public function get_all_metadata($options=[]) { + $metadata = $this->metadata; + if(isset($options['first_value_only']) && $options['first_value_only']) { + $metadata = array_map(function($value) { + return $value[0]; + }, $metadata); + } + return $metadata; + } + + public function get_block_markup() { + return $this->block_markup; + } + + public function get_last_error() { + return $this->last_error; + } +} diff --git a/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php index ec6427afcd..5f8d4e299c 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_EPub_Entity_Reader.php @@ -95,8 +95,15 @@ public function next_entity() { $html_file = array_shift( $this->remaining_html_files ); $html = $this->zip->get_contents( $html_file ); + $converter = new WP_HTML_To_Blocks( WP_XML_Processor::create_from_string( $html ) ); + if ( false === $converter->convert() ) { + $this->last_error = $converter->get_last_error(); + return false; + } + $this->current_html_reader = new WP_HTML_Entity_Reader( - WP_XML_Processor::create_from_string( $html ), + $converter->get_block_markup(), + $converter->get_all_metadata(), $this->current_post_id ); if ( $this->current_html_reader->get_last_error() ) { diff --git a/packages/playground/data-liberation/src/entity-readers/WP_HTML_Entity_Reader.php b/packages/playground/data-liberation/src/entity-readers/WP_HTML_Entity_Reader.php index aef6041666..46c249ad5b 100644 --- a/packages/playground/data-liberation/src/entity-readers/WP_HTML_Entity_Reader.php +++ b/packages/playground/data-liberation/src/entity-readers/WP_HTML_Entity_Reader.php @@ -7,15 +7,17 @@ */ class WP_HTML_Entity_Reader extends WP_Entity_Reader { - protected $html_processor; + protected $block_markup; + protected $metadata; protected $entities; protected $finished = false; protected $post_id; protected $last_error; - public function __construct( $html_processor, $post_id ) { - $this->html_processor = $html_processor; - $this->post_id = $post_id; + public function __construct( $block_markup, $metadata, $post_id ) { + $this->block_markup = $block_markup; + $this->metadata = $metadata; + $this->post_id = $post_id; } public function next_entity() { @@ -34,17 +36,9 @@ public function next_entity() { return true; } - // We did not read any entities yet. Let's convert the HTML document into entities. - $converter = new WP_HTML_To_Blocks( $this->html_processor ); - if ( false === $converter->convert() ) { - $this->last_error = $converter->get_last_error(); - return false; - } - - $all_metadata = $converter->get_all_metadata(); $post_fields = array(); $other_metadata = array(); - foreach ( $all_metadata as $key => $values ) { + foreach ( $this->metadata as $key => $values ) { if ( in_array( $key, WP_Imported_Entity::POST_FIELDS, true ) ) { $post_fields[ $key ] = $values[0]; } else { @@ -59,7 +53,7 @@ public function next_entity() { $post_fields, array( 'post_id' => $this->post_id, - 'content' => $converter->get_block_markup(), + 'content' => $this->block_markup, ) ) ); diff --git a/packages/playground/data-liberation/tests/WPHTMLEntityReaderTests.php b/packages/playground/data-liberation/tests/WPHTMLEntityReaderTests.php index 0dccac219e..1a5f6092e7 100644 --- a/packages/playground/data-liberation/tests/WPHTMLEntityReaderTests.php +++ b/packages/playground/data-liberation/tests/WPHTMLEntityReaderTests.php @@ -13,7 +13,15 @@ public function test_entity_reader() {

It is our pleasure to announce that WordPress 6.8 was released

Last week, WordPress 6.8 was released.

HTML; - $reader = new WP_HTML_Entity_Reader( WP_HTML_Processor::create_fragment( $html ), 1 ); + $html_processor = WP_HTML_Processor::create_fragment( $html ); + $converter = new WP_HTML_With_Blocks_to_Blocks( $html_processor ); + $this->assertTrue( $converter->convert() ); + + $reader = new WP_HTML_Entity_Reader( + $converter->get_block_markup(), + $converter->get_all_metadata(), + 1 + ); $entities = []; while ( $reader->next_entity() ) { $data = $reader->get_entity()->get_data(); From 6357abd8e8a412e45c5c2ca34ab84714d0c7f242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 5 Jan 2025 21:49:15 +0100 Subject: [PATCH 58/71] Start implementing a git server --- .../playground/data-liberation/bootstrap.php | 1 + .../playground/data-liberation/phpunit.xml | 1 + .../src/git/WP_Git_Pack_Processor.php | 27 ++++ .../src/git/WP_Git_Repository.php | 44 ++++- .../data-liberation/src/git/WP_Git_Server.php | 151 ++++++++++++++++++ 5 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 packages/playground/data-liberation/src/git/WP_Git_Server.php diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 8088b3a5d0..f13a00e582 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -94,6 +94,7 @@ require_once __DIR__ . '/src/git/WP_Git_Pack_Processor.php'; require_once __DIR__ . '/src/git/WP_Git_Repository.php'; require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; +require_once __DIR__ . '/src/git/WP_Git_Server.php'; require_once __DIR__ . '/src/git/WP_Git_Merge_Engine.php'; require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; diff --git a/packages/playground/data-liberation/phpunit.xml b/packages/playground/data-liberation/phpunit.xml index 8b656b80fb..b929d24e56 100644 --- a/packages/playground/data-liberation/phpunit.xml +++ b/packages/playground/data-liberation/phpunit.xml @@ -24,6 +24,7 @@ tests/WPXMLProcessorTests.php tests/UrldecodeNTests.php tests/WPStreamImporterTests.php + tests/WPGitServerTests.php diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 404b300a84..a413b3b7ce 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -222,6 +222,33 @@ static public function decode($pack_bytes, $pack_index=null) { return $pack_index; } + static public function decode_next_packet_line($pack_bytes, &$offset) { + $packet_length_bytes = substr($pack_bytes, $offset, 4); + $offset += 4; + if( + strlen($packet_length_bytes) !== 4 || + !preg_match('/^[0-9a-f]{4}$/', $packet_length_bytes) + ) { + return false; + } + switch($packet_length_bytes) { + case '0000': + return ['type' => '#flush']; + case '0001': + return ['type' => '#delimiter']; + case '0002': + return ['type' => '#response-end']; + default: + $length = intval($packet_length_bytes, 16); + $payload = substr($pack_bytes, $offset, $length); + if(str_ends_with($payload, "\n")) { + $payload = substr($payload, 0, -1); + } + $offset += $length; + return ['type' => '#packet', 'payload' => $payload]; + } + } + static private function wrap_git_object($type, $object) { $length = strlen($object); $type_name = self::OBJECT_NAMES[$type]; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index cfcfa0ac7d..6b466b9d5b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -982,5 +982,47 @@ private function commit_tree($path, $changed_trees) { ); } + public function list_refs($prefix = '') { + $refs = []; -} + /** + * Only allow listing refs in the refs/ directory to avoid + * accidentally working with, say, the main .git directory. + * + * This is a starter implementation. We may need to revisit this + * for full compliance with Git. + */ + $path = ltrim(wp_canonicalize_path($prefix), '/'); + $first_path = $this->fs->is_dir($path) ? $path : dirname($path); + $stack = []; + if(str_starts_with($first_path, 'refs/')) { + $stack = [$first_path]; + } + if($prefix === '') { + $stack = ['refs/heads/']; + } + while(!empty($stack)) { + $path = array_shift($stack); + if ($this->fs->is_dir($path)) { + $ref_files = $this->fs->ls($path); + foreach ($ref_files as $ref_file) { + $full_path = wp_join_paths($path, $ref_file); + array_push($stack, $full_path); + } + } else if(str_starts_with($path, $prefix)) { + $hash = trim($this->fs->get_contents($path)); + if ($hash) { + $ref_name = trim($path, '/'); + $refs[$ref_name] = $hash; + } + } + } + + if($prefix === '' || str_starts_with('HEAD', $prefix)) { + $refs['HEAD'] = $this->get_ref_head('HEAD'); + } + + return $refs; + } + +} \ No newline at end of file diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php new file mode 100644 index 0000000000..bc22ba663a --- /dev/null +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -0,0 +1,151 @@ +repository = $repository; + } + + /** + * Handle Git protocol v2 ls-refs command + * + * @param array $request The parsed request data + * @return string The response in Git protocol v2 format + */ + public function handle_ls_refs_request($request) { + $parsed = $this->parse_ls_refs_request($request); + if(!$parsed) { + return false; + } + $prefix = $parsed['arguments']['ref-prefix'] ?? ''; + + $refs = $this->repository->list_refs($prefix); + $response = ''; + foreach ($refs as $ref_name => $ref_hash) { + // Format: \n + $response .= WP_Git_Pack_Processor::encode_packet_line( + $ref_hash . ' ' . $ref_name . "\n" + ); + } + + // End the response with 0000 + return $response . "0000"; + } + + /** + * Capability Advertisement + * + * A server which decides to communicate (based on a request from a client) using + * protocol version 2, notifies the client by sending a version string in its initial + * response followed by an advertisement of its capabilities. Each capability is a key + * with an optional value. Clients must ignore all unknown keys. Semantics of unknown + * values are left to the definition of each key. Some capabilities will describe + * command which can be requested to be executed by the client. + * + * capability-advertisement = protocol-version + * capability-list + * flush-pkt + * + * protocol-version = PKT-LINE("version 2" LF) + * capability-list = *capability + * capability = PKT-LINE(key[=value] LF) + * + * key = 1*(ALPHA | DIGIT | "-_") + * value = 1*(ALPHA | DIGIT | " -_.,?\/{}[]()<>!@#$%^&*+=:;") + * flush-pkt = PKT-LINE("0000" LF) + * + * @see https://git-scm.com/docs/protocol-v2#_capability_advertisement + * @return string The capability advertisement in Git protocol v2 format + */ + public function capability_advertise() { + return "version 2\n" . + "agent=git/2.37.3\n" . + "0000"; + } + + /** + * ls-refs is the command used to request a reference advertisement in v2. + * Unlike the current reference advertisement, ls-refs takes in arguments + * which can be used to limit the refs sent from the server. + * + * Additional features not supported in the base command will be advertised as + * the value of the command in the capability advertisement in the form of a space + * separated list of features: "= " + * + * ls-refs takes in the following arguments: + * + * symrefs + * In addition to the object pointed by it, show the underlying ref + * pointed by it when showing a symbolic ref. + * + * peel + * Show peeled tags. + * + * ref-prefix + * When specified, only references having a prefix matching one of + * the provided prefixes are displayed. Multiple instances may be + * given, in which case references matching any prefix will be + * shown. Note that this is purely for optimization; a server MAY + * show refs not matching the prefix if it chooses, and clients + * should filter the result themselves. + * + * unborn + * The server will send information about HEAD even if it is a symref + * pointing to an unborn branch in the form "unborn HEAD symref-target:". + * + * @see https://git-scm.com/docs/protocol-v2#_ls_refs + * + * @param string $request_bytes Raw request bytes + * @return array Parsed request data + */ + public function parse_ls_refs_request($request_bytes) { + $parsed = [ + 'capabilities' => [], + 'arguments' => [], + ]; + + // Parse the capability advertisement part + $offset = 0; + while (true) { + $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); + if ($line === false || $line['type'] !== '#packet') { + break; + } + + list($key, $value) = explode('=', $line['payload']); + $parsed['capabilities'][$key] = $value; + } + + // Parse the optional arguments part + while (true) { + $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); + if ($line === false || $line['type'] !== '#packet') { + break; + } + + $space_at = strpos($line['payload'], ' '); + if($space_at === false) { + $parsed['arguments'][$line['payload']] = true; + continue; + } + $key = substr($line['payload'], 0, $space_at); + $value = substr($line['payload'], $space_at + 1); + $parsed['arguments'][$key] = $value; + } + + return $parsed; + } + +} From dd73f79305ab01f11b241524ea97334f11bfb61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 5 Jan 2025 22:19:07 +0100 Subject: [PATCH 59/71] Parse Git Client messages --- .../src/git/WP_Git_Pack_Processor.php | 14 +- .../data-liberation/src/git/WP_Git_Server.php | 176 ++++++++++++------ 2 files changed, 135 insertions(+), 55 deletions(-) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index a413b3b7ce..33f7667db3 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -152,6 +152,18 @@ static private function wrap_object($type, $object) { return "$type_name $length\x00" . $object; } + static public function encode_packet_lines(array $payloads): string { + $lines = []; + foreach($payloads as $payload) { + if($payload === '0000' || $payload === '0001' || $payload === '0002') { + $lines[] = $payload; + } else { + $lines[] = self::encode_packet_line($payload); + } + } + return implode('', $lines); + } + static public function encode_packet_line(string $payload): string { $length = strlen($payload) + 4; return sprintf("%04x", $length) . $payload; @@ -239,7 +251,7 @@ static public function decode_next_packet_line($pack_bytes, &$offset) { case '0002': return ['type' => '#response-end']; default: - $length = intval($packet_length_bytes, 16); + $length = intval($packet_length_bytes, 16) - 4 ; $payload = substr($pack_bytes, $offset, $length); if(str_ends_with($payload, "\n")) { $payload = substr($payload, 0, -1); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index bc22ba663a..9fa80d2c10 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -21,15 +21,45 @@ public function __construct(WP_Git_Repository $repository) { /** * Handle Git protocol v2 ls-refs command * + * ls-refs is the command used to request a reference advertisement in v2. + * Unlike the current reference advertisement, ls-refs takes in arguments + * which can be used to limit the refs sent from the server. + * + * Additional features not supported in the base command will be advertised as + * the value of the command in the capability advertisement in the form of a space + * separated list of features: "= " + * + * ls-refs takes in the following arguments: + * + * symrefs + * In addition to the object pointed by it, show the underlying ref + * pointed by it when showing a symbolic ref. + * + * peel + * Show peeled tags. + * + * ref-prefix + * When specified, only references having a prefix matching one of + * the provided prefixes are displayed. Multiple instances may be + * given, in which case references matching any prefix will be + * shown. Note that this is purely for optimization; a server MAY + * show refs not matching the prefix if it chooses, and clients + * should filter the result themselves. + * + * unborn + * The server will send information about HEAD even if it is a symref + * pointing to an unborn branch in the form "unborn HEAD symref-target:". + * + * @see https://git-scm.com/docs/protocol-v2#_ls_refs * @param array $request The parsed request data * @return string The response in Git protocol v2 format */ public function handle_ls_refs_request($request) { - $parsed = $this->parse_ls_refs_request($request); + $parsed = $this->parse_message($request); if(!$parsed) { return false; } - $prefix = $parsed['arguments']['ref-prefix'] ?? ''; + $prefix = $parsed['arguments']['ref-prefix'][0] ?? ''; $refs = $this->repository->list_refs($prefix); $response = ''; @@ -75,60 +105,29 @@ public function capability_advertise() { "0000"; } - /** - * ls-refs is the command used to request a reference advertisement in v2. - * Unlike the current reference advertisement, ls-refs takes in arguments - * which can be used to limit the refs sent from the server. - * - * Additional features not supported in the base command will be advertised as - * the value of the command in the capability advertisement in the form of a space - * separated list of features: "= " - * - * ls-refs takes in the following arguments: - * - * symrefs - * In addition to the object pointed by it, show the underlying ref - * pointed by it when showing a symbolic ref. - * - * peel - * Show peeled tags. - * - * ref-prefix - * When specified, only references having a prefix matching one of - * the provided prefixes are displayed. Multiple instances may be - * given, in which case references matching any prefix will be - * shown. Note that this is purely for optimization; a server MAY - * show refs not matching the prefix if it chooses, and clients - * should filter the result themselves. - * - * unborn - * The server will send information about HEAD even if it is a symref - * pointing to an unborn branch in the form "unborn HEAD symref-target:". - * - * @see https://git-scm.com/docs/protocol-v2#_ls_refs - * - * @param string $request_bytes Raw request bytes - * @return array Parsed request data - */ - public function parse_ls_refs_request($request_bytes) { - $parsed = [ - 'capabilities' => [], - 'arguments' => [], - ]; - - // Parse the capability advertisement part + public function parse_message($request_bytes) { $offset = 0; + return [ + 'capabilities' => $this->parse_capabilities($request_bytes, $offset), + 'arguments' => $this->parse_arguments($request_bytes, $offset), + ]; + } + + private function parse_capabilities($request_bytes, &$offset=0) { + $capabilities = []; while (true) { $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); if ($line === false || $line['type'] !== '#packet') { break; } - list($key, $value) = explode('=', $line['payload']); - $parsed['capabilities'][$key] = $value; + $capabilities[$key] = $value; } + return $capabilities; + } - // Parse the optional arguments part + private function parse_arguments($request_bytes, &$offset=0) { + $arguments = []; while (true) { $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); if ($line === false || $line['type'] !== '#packet') { @@ -137,15 +136,84 @@ public function parse_ls_refs_request($request_bytes) { $space_at = strpos($line['payload'], ' '); if($space_at === false) { - $parsed['arguments'][$line['payload']] = true; - continue; + $key = $line['payload']; + $value = true; + } else { + $key = substr($line['payload'], 0, $space_at); + $value = substr($line['payload'], $space_at + 1); } - $key = substr($line['payload'], 0, $space_at); - $value = substr($line['payload'], $space_at + 1); - $parsed['arguments'][$key] = $value; - } - return $parsed; + if(!array_key_exists($key, $arguments)) { + $arguments[$key] = []; + } + $arguments[$key][] = $value; + } + return $arguments; } + /** + * Handle Git protocol v2 fetch command with "want" packets + * + * @param array $request The parsed request data + * @return string The response in Git protocol v2 format containing the pack data + */ + public function handle_fetch_request($request) { + $parsed = $this->parse_message($request); + if (!$parsed || empty($parsed['arguments']['want'])) { + return false; + } + + $objects_to_send = []; + foreach ($parsed['arguments']['want'] as $want_hash) { + // For each wanted commit, find objects not present in any of the have commits + $new_objects = $this->repository->find_objects_added_in( + $want_hash, + $parsed['arguments']['have'] ?? ['0000000000000000000000000000000000000000'] + ); + $objects_to_send = array_merge($objects_to_send, $new_objects); + } + $objects_to_send = array_unique($objects_to_send); + + // Pack the objects + $pack_objects = []; + foreach ($objects_to_send as $oid) { + $this->repository->read_object($oid); + + // Apply blob filters if specified + if ($this->repository->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_BLOB) { + $filter = $parsed['arguments']['filter'] ?? null; + if ($filter) { + if ($filter['type'] === 'none') { + continue; // Skip all blobs + } else if ($filter['type'] === 'limit') { + $content = $this->repository->read_entire_object_contents(); + if (strlen($content) > $filter['size']) { + continue; // Skip large blobs + } + } + } + } + + $pack_objects[] = [ + 'type' => $this->repository->get_type(), + 'content' => $this->repository->read_entire_object_contents(), + ]; + } + + // Handle deepen if specified + if (isset($parsed['arguments']['deepen'])) { + // @TODO: Implement history truncation based on deepen value + // This would involve walking the commit history and including + // only commits within the specified depth + } + + // Encode the pack + $pack_data = WP_Git_Pack_Processor::encode($pack_objects); + + // Format the response according to protocol v2 + return + WP_Git_Pack_Processor::encode_packet_line("packfile\n") . + WP_Git_Pack_Processor::encode_packet_line("\x01" . $pack_data) . // side-band channel 1 + "0000"; + } } From bb3928edf4282f7366173bc735ed48f2ca55e097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 5 Jan 2025 22:23:40 +0100 Subject: [PATCH 60/71] Add unit tests --- .../tests/WPGitServerTests.php | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 packages/playground/data-liberation/tests/WPGitServerTests.php diff --git a/packages/playground/data-liberation/tests/WPGitServerTests.php b/packages/playground/data-liberation/tests/WPGitServerTests.php new file mode 100644 index 0000000000..030bf56d92 --- /dev/null +++ b/packages/playground/data-liberation/tests/WPGitServerTests.php @@ -0,0 +1,258 @@ +repository = new WP_Git_Repository( + new WP_In_Memory_Filesystem() + ); + $this->server = new WP_Git_Server($this->repository); + $this->repository->set_ref_head('HEAD', 'ref: refs/heads/main'); + $this->main_branch_oid = $this->repository->commit([ + 'updates' => [ + 'README.md' => 'Hello, world!', + ], + ]); + + $this->repository->set_ref_head('refs/heads/main', $this->main_branch_oid); + $this->repository->set_ref_head('refs/heads/twin', $this->main_branch_oid); + $this->repository->set_ref_head('refs/heads/main-backup', $this->main_branch_oid); + $this->repository->set_ref_head('refs/heads/dev', $this->main_branch_oid); + $this->repository->set_ref_head('HEAD', 'ref: refs/heads/dev'); + + $this->dev_branch_oid = $this->repository->commit([ + 'updates' => [ + 'DEV.md' => 'Another file!', + ], + ]); + } + + /** + * @dataProvider provideRequestData + */ + public function test_parse_message($request, $expected) { + $result = $this->server->parse_message($request); + $this->assertEquals($expected, $result); + } + + public function provideRequestData() { + return [ + 'basic ls-refs request' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=ls-refs\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'ls-refs', + ], + 'arguments' => [], + ] + ], + 'request with multiple capabilities' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=ls-refs\n", + "agent=git/2.37.3\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'ls-refs', + 'agent' => 'git/2.37.3' + ], + 'arguments' => [], + ] + ], + 'request with multiple arguments' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=ls-refs\n", + "0001", + "peel\n", + "ref-prefix HEAD\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'ls-refs', + ], + 'arguments' => [ + 'peel' => [true], + 'ref-prefix' => ['HEAD'] + ], + ] + ], + 'basic want request' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "agent=git/2.37.3\n", + "object-format=sha1\n", + "0000", + "want e0d02a851d0c461a7c725dc69eb2d53f57f666a6\n", + "done\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'fetch', + 'agent' => 'git/2.37.3', + 'object-format' => 'sha1' + ], + 'arguments' => [ + 'want' => ['e0d02a851d0c461a7c725dc69eb2d53f57f666a6'], + 'done' => [true] + ] + ] + ], + 'want with have and filter' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "agent=git/2.37.3\n", + "object-format=sha1\n", + "0000", + "want e0d02a851d0c461a7c725dc69eb2d53f57f666a6\n", + "have f5b97d7b9af357c81b5df5773329d50f764c2992\n", + "filter blob:none\n", + "done\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'fetch', + 'agent' => 'git/2.37.3', + 'object-format' => 'sha1' + ], + 'arguments' => [ + 'want' => ['e0d02a851d0c461a7c725dc69eb2d53f57f666a6'], + 'have' => ['f5b97d7b9af357c81b5df5773329d50f764c2992'], + 'filter' => ['blob:none'], + 'done' => [true] + ] + ] + ], + 'want with deepen and blob size limit' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "agent=git/2.37.3\n", + "object-format=sha1\n", + "0000", + "want e0d02a851d0c461a7c725dc69eb2d53f57f666a6\n", + "filter blob:limit=1000\n", + "deepen 10\n", + "done\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'fetch', + 'agent' => 'git/2.37.3', + 'object-format' => 'sha1' + ], + 'arguments' => [ + 'want' => ['e0d02a851d0c461a7c725dc69eb2d53f57f666a6'], + 'filter' => ['blob:limit=1000'], + 'deepen' => ['10'], + 'done' => [true] + ] + ] + ], + 'multiple want and have' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "0000", + "want e0d02a851d0c461a7c725dc69eb2d53f57f666a6\n", + "want f10e2821bbbea527ea02200352313bc059445190\n", + "have f5b97d7b9af357c81b5df5773329d50f764c2992\n", + "have 0e747aaa0f03a7b7bb9a964f47fe7c508be7b086\n", + "done\n", + "0000", + ]), + [ + 'capabilities' => [ + 'command' => 'fetch', + ], + 'arguments' => [ + 'want' => [ + 'e0d02a851d0c461a7c725dc69eb2d53f57f666a6', + 'f10e2821bbbea527ea02200352313bc059445190' + ], + 'have' => [ + 'f5b97d7b9af357c81b5df5773329d50f764c2992', + '0e747aaa0f03a7b7bb9a964f47fe7c508be7b086' + ], + 'done' => [true], + ] + ] + ] + ]; + } + + /** + * @dataProvider provideRefRequests + */ + public function test_handle_ls_refs_returns_matching_refs($request, $expected_response) { + // Replace placeholders with actual values in the test as $this->main_branch_oid and + // $this->dev_branch_oid are not available in the data provider. + $expected_response = str_replace( + array('{main_branch_oid}', '{dev_branch_oid}'), + array($this->main_branch_oid, $this->dev_branch_oid), + $expected_response + ); + $response = $this->server->handle_ls_refs_request($request); + $this->assertEquals($expected_response, $response); + } + + public function provideRefRequests() { + return [ + 'all refs under heads' => [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=ls-refs\n", + "0000", + ]), + << [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=ls-refs\n", + "0001", + "peel\n", + "ref-prefix refs/heads/main\n", + "0000", + ]), + << [ + WP_Git_Pack_Processor::encode_packet_lines([ + "command=ls-refs\n", + "0001", + "peel\n", + "ref-prefix HEAD\n", + "0000", + ]), + << Date: Mon, 6 Jan 2025 03:14:04 +0100 Subject: [PATCH 61/71] Functional fetch support --- .../playground/data-liberation/bootstrap.php | 3 + .../data-liberation/src/git/WP_Git_Client.php | 8 +- .../src/git/WP_Git_Pack_Processor.php | 104 +++++---- .../src/git/WP_Git_Repository.php | 17 +- .../data-liberation/src/git/WP_Git_Server.php | 213 +++++++++++++++--- .../tests/WPGitServerTests.php | 167 ++++++++++++++ 6 files changed, 417 insertions(+), 95 deletions(-) diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index f13a00e582..51c2543dee 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -10,6 +10,9 @@ require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/HttpError.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/Connection.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/Client.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/ResponseWriter/ResponseWriter.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/ResponseWriter/StreamingResponseWriter.php'; +require_once __DIR__ . '/blueprints-library/src/WordPress/AsyncHttp/ResponseWriter/BufferingResponseWriter.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Abstract_Filesystem.php'; require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Local_Filesystem.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index af527f580b..3a80c1323b 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -147,7 +147,7 @@ public function list_objects($ref_hash) { 'Content-Type: application/x-git-upload-pack-request', ]); - $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); + $pack_data = self::accumulate_pack_data_from_multiplexed_chunks($response); return WP_Git_Pack_Processor::decode($pack_data); } @@ -208,7 +208,7 @@ public function fetchObjects($refs) { 'Accept: application/x-git-upload-pack-advertisement', 'Content-Type: application/x-git-upload-pack-request', ]); - $pack_data = $this->accumulate_pack_data_from_multiplexed_chunks($response); + $pack_data = self::accumulate_pack_data_from_multiplexed_chunks($response); WP_Git_Pack_Processor::decode($pack_data, $this->index); return true; } @@ -256,9 +256,9 @@ private function encode_packet_line($data) { return str_pad(dechex($length), 4, '0', STR_PAD_LEFT) . $data; } - private function accumulate_pack_data_from_multiplexed_chunks($raw_response) { + static public function accumulate_pack_data_from_multiplexed_chunks($raw_response) { $parsed_pack_data = []; - $parsed_chunks = $this->parse_multiplexed_pack_data($raw_response); + $parsed_chunks = self::parse_multiplexed_pack_data($raw_response); foreach($parsed_chunks as $chunk) { if($chunk['type'] !== 'side-band') { continue; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 33f7667db3..b9e008c768 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -155,16 +155,15 @@ static private function wrap_object($type, $object) { static public function encode_packet_lines(array $payloads): string { $lines = []; foreach($payloads as $payload) { - if($payload === '0000' || $payload === '0001' || $payload === '0002') { - $lines[] = $payload; - } else { - $lines[] = self::encode_packet_line($payload); - } + $lines[] = self::encode_packet_line($payload); } return implode('', $lines); } static public function encode_packet_line(string $payload): string { + if($payload === '0000' || $payload === '0001' || $payload === '0002') { + return $payload; + } $length = strlen($payload) + 4; return sprintf("%04x", $length) . $payload; } @@ -179,55 +178,9 @@ static public function decode($pack_bytes, $pack_index=null) { } $parsed_pack = self::parse_pack_data($pack_bytes); - $objects = $parsed_pack['objects']; - - $by_oid = []; - $by_offset = []; - $resolved_objects = 0; - // Index entities and resolve deltas - // Run until all objects are resolved - while($resolved_objects < count($objects)) { - $resolved_in_this_iteration = 0; - for($i = 0; $i < count($objects); $i++) { - // Skip already processed objects - if( - isset($by_offset[$objects[$i]['header_offset']]) && - isset($by_oid[$objects[$i]['oid']]) - ) { - continue; - } - - if($objects[$i]['type'] === self::OBJECT_TYPE_OFS_DELTA) { - $target_offset = $objects[$i]['header_offset'] - $objects[$i]['ofs']; - if(!isset($by_offset[$target_offset])) { - continue; - } - // TODO: Make sure the base object will never be another delta. - $base = $objects[$by_offset[$target_offset]]; - $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); - $objects[$i]['type'] = $base['type']; - } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { - if(!isset($by_oid[$objects[$i]['reference']])) { - continue; - } - $base = $objects[$by_oid[$objects[$i]['reference']]]; - $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); - $objects[$i]['type'] = $base['type']; - } - $oid = sha1(self::wrap_git_object($objects[$i]['type'], $objects[$i]['content'])); - $objects[$i]['oid'] = $oid; - $by_oid[$oid] = $i; - $by_offset[$objects[$i]['header_offset']] = $i; - ++$resolved_in_this_iteration; - ++$resolved_objects; - } - if($resolved_in_this_iteration === 0) { - throw new Exception('Could not resolve objects'); - } - } // Resolve trees - foreach($objects as $object) { + foreach($parsed_pack['objects'] as $object) { $pack_index->add_object($object['type'], $object['content']); } @@ -402,7 +355,7 @@ static private function readVariableLength($data, &$offset) { return $result; } - static private function parse_pack_data($packData) { + static public function parse_pack_data($packData) { $offset = 0; // Basic sanity checks @@ -435,6 +388,51 @@ static private function parse_pack_data($packData) { $object['content'] = self::inflate_object($packData, $offset, $object['uncompressed_length']); $objects[] = $object; } + + $by_oid = []; + $by_offset = []; + $resolved_objects = 0; + // Index entities and resolve deltas + // Run until all objects are resolved + while($resolved_objects < count($objects)) { + $resolved_in_this_iteration = 0; + for($i = 0; $i < count($objects); $i++) { + // Skip already processed objects + if( + isset($by_offset[$objects[$i]['header_offset']]) && + isset($by_oid[$objects[$i]['oid']]) + ) { + continue; + } + + if($objects[$i]['type'] === self::OBJECT_TYPE_OFS_DELTA) { + $target_offset = $objects[$i]['header_offset'] - $objects[$i]['ofs']; + if(!isset($by_offset[$target_offset])) { + continue; + } + // TODO: Make sure the base object will never be another delta. + $base = $objects[$by_offset[$target_offset]]; + $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['type'] = $base['type']; + } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { + if(!isset($by_oid[$objects[$i]['reference']])) { + continue; + } + $base = $objects[$by_oid[$objects[$i]['reference']]]; + $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); + $objects[$i]['type'] = $base['type']; + } + $oid = sha1(self::wrap_git_object($objects[$i]['type'], $objects[$i]['content'])); + $objects[$i]['oid'] = $oid; + $by_oid[$oid] = $i; + $by_offset[$objects[$i]['header_offset']] = $i; + ++$resolved_in_this_iteration; + ++$resolved_objects; + } + if($resolved_in_this_iteration === 0) { + throw new Exception('Could not resolve objects'); + } + } return [ 'objects' => $objects, 'total_objects' => $objectCount, diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 6b466b9d5b..940cb6d977 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -123,7 +123,7 @@ class WP_Git_Repository { private $diff_engine; private const DELETE_PLACEHOLDER = 'DELETE_PLACEHOLDER'; - private const NULL_OID = '0000000000000000000000000000000000000000'; + public const NULL_OID = '0000000000000000000000000000000000000000'; public function __construct( WP_Abstract_Filesystem $fs, @@ -372,7 +372,7 @@ public function oid_exists($oid) { public function read_by_path($path, $root_tree_oid=null) { if($root_tree_oid === null) { $head_oid = $this->get_ref_head('HEAD'); - if(false === $this->read_object($head_oid)) { + if(!$head_oid || false === $this->read_object($head_oid)) { return false; } $root_tree_oid = $this->get_parsed_commit()['tree'] ?? null; @@ -433,7 +433,7 @@ public function find_path_descendants($path) { return $oids; } - public function find_objects_added_in($new_tree_oid, $old_tree_oid=null, $options=[]) { + public function find_objects_added_in($new_tree_oid, $old_tree_oid=WP_Git_Repository::NULL_OID, $options=[]) { $old_tree_index = $options['old_tree_index'] ?? $this; if($old_tree_index === null) { $old_tree_index = $this; @@ -452,7 +452,7 @@ public function find_objects_added_in($new_tree_oid, $old_tree_oid=null, $option } // Resolve the actual tree oid if $old_tree_oid is a commit - if($old_tree_oid) { + if(!$this->is_null_oid($old_tree_oid)) { if(false === $old_tree_index->read_object($old_tree_oid)) { $this->last_error = 'Failed to read object: ' . $old_tree_oid; return false; @@ -475,6 +475,9 @@ public function find_objects_added_in($new_tree_oid, $old_tree_oid=null, $option if($current_new_oid === $current_old_oid) { continue; } + if($this->is_null_oid($current_new_oid)) { + continue; + } if(false === $this->read_object($current_new_oid)) { $this->last_error = 'Failed to read object: ' . $current_new_oid; @@ -492,7 +495,7 @@ public function find_objects_added_in($new_tree_oid, $old_tree_oid=null, $option yield $this->get_oid(); $old_tree = []; - if($current_old_oid) { + if(!$this->is_null_oid($current_old_oid)) { if(false === $old_tree_index->read_object($current_old_oid)) { $this->last_error = 'Failed to read object: ' . $current_old_oid; return false; @@ -506,6 +509,10 @@ public function find_objects_added_in($new_tree_oid, $old_tree_oid=null, $option } } + private function is_null_oid($oid) { + return $oid === null || $oid === WP_Git_Repository::NULL_OID; + } + public function set_ref_head($ref, $oid) { $path = $this->resolve_ref_file_path($ref); if(!$path) { diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index 9fa80d2c10..382b4f8a0e 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -1,8 +1,6 @@ repository = $repository; } + public function handle_request($path, $request_bytes, $response) { + switch($path) { + case '/HEAD': + $response->write(WP_Git_Pack_Processor::encode_packet_line(sha1("a") . " HEAD\n")); + $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); + $response->end(); + break; + // @TODO handle service=git-upload-pack + case '/info/refs?service=git-upload-pack': + $parsed = $this->parse_message($request_bytes); + $response->send_header( + 'Content-Type', + 'application/x-git-upload-pack-advertisement' + ); + $response->send_header( + 'Cache-Control', + 'no-cache' + ); + $response->send_header( + 'Git-Protocol', + 'version=2' + ); + $response->write(WP_Git_Pack_Processor::encode_packet_lines([ + "# service=git-upload-pack\n", + "0000", + "version 2\n", + "agent=git/github-395dce4f6ecf\n", + "ls-refs=unborn\n", + "fetch=shallow wait-for-done filter\n", + "server-option\n", + "object-format=sha1\n", + "0000" + ])); + flush(); + $response->end(); + break; + case '/git-upload-pack': + $parsed = $this->parse_message($request_bytes); + switch($parsed['capabilities']['command']) { + case 'ls-refs': + $this->handle_ls_refs_request($request_bytes, $response); + break; + case 'fetch': + $this->handle_fetch_request($request_bytes, $response); + break; + default: + throw new Exception('Unknown command: ' . $parsed['capabilities']['command']); + } + break; + case '/git-receive-pack': + throw new Exception('Not implemented yet'); + default: + throw new Exception('Unknown path: ' . $path); + } + } + /** * Handle Git protocol v2 ls-refs command * @@ -51,27 +105,46 @@ public function __construct(WP_Git_Repository $repository) { * pointing to an unborn branch in the form "unborn HEAD symref-target:". * * @see https://git-scm.com/docs/protocol-v2#_ls_refs - * @param array $request The parsed request data + * @param array $request_bytes The parsed request data * @return string The response in Git protocol v2 format */ - public function handle_ls_refs_request($request) { - $parsed = $this->parse_message($request); + public function handle_ls_refs_request($request_bytes, ResponseWriter $response) { + $response->send_header( + 'Content-Type', + 'application/x-git-upload-pack-advertisement' + ); + $response->send_header( + 'Cache-Control', + 'no-cache' + ); + $response->send_header( + 'Git-Protocol', + 'version=2' + ); + + $parsed = $this->parse_message($request_bytes); if(!$parsed) { - return false; + // return false; } $prefix = $parsed['arguments']['ref-prefix'][0] ?? ''; $refs = $this->repository->list_refs($prefix); - $response = ''; + $first_ref = array_key_first($refs); foreach ($refs as $ref_name => $ref_hash) { + $line = $ref_hash . ' ' . $ref_name; + if($ref_name === $first_ref) { + $line .= "\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/trunk filter object-format=sha1 agent=git/github-395dce4f6ecf"; + } // Format: \n - $response .= WP_Git_Pack_Processor::encode_packet_line( - $ref_hash . ' ' . $ref_name . "\n" + $response->write( + WP_Git_Pack_Processor::encode_packet_line( + $line . "\n" + ) ); } // End the response with 0000 - return $response . "0000"; + $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); } /** @@ -105,18 +178,18 @@ public function capability_advertise() { "0000"; } - public function parse_message($request_bytes) { + public function parse_message($request_bytes_bytes) { $offset = 0; return [ - 'capabilities' => $this->parse_capabilities($request_bytes, $offset), - 'arguments' => $this->parse_arguments($request_bytes, $offset), + 'capabilities' => $this->parse_capabilities($request_bytes_bytes, $offset), + 'arguments' => $this->parse_arguments($request_bytes_bytes, $offset), ]; } - private function parse_capabilities($request_bytes, &$offset=0) { + private function parse_capabilities($request_bytes_bytes, &$offset=0) { $capabilities = []; while (true) { - $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); + $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes_bytes, $offset); if ($line === false || $line['type'] !== '#packet') { break; } @@ -126,10 +199,10 @@ private function parse_capabilities($request_bytes, &$offset=0) { return $capabilities; } - private function parse_arguments($request_bytes, &$offset=0) { + private function parse_arguments($request_bytes_bytes, &$offset=0) { $arguments = []; while (true) { - $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); + $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes_bytes, $offset); if ($line === false || $line['type'] !== '#packet') { break; } @@ -154,38 +227,98 @@ private function parse_arguments($request_bytes, &$offset=0) { /** * Handle Git protocol v2 fetch command with "want" packets * - * @param array $request The parsed request data + * @param array $request_bytes The parsed request data * @return string The response in Git protocol v2 format containing the pack data */ - public function handle_fetch_request($request) { - $parsed = $this->parse_message($request); + public function handle_fetch_request($request_bytes, $response) { + $parsed = $this->parse_message($request_bytes); if (!$parsed || empty($parsed['arguments']['want'])) { return false; } + $filter_raw = $parsed['arguments']['filter'][0] ?? null; + $filter = $this->parse_filter($filter_raw); + if($filter === false) { + throw new Exception('Invalid filter: ' . $filter_raw); + } + + $have_oids = [ + WP_Git_Repository::NULL_OID => true, + ]; + if(isset($parsed['arguments']['have'])) { + foreach($parsed['arguments']['have'] as $have_hash) { + $have_oids[$have_hash] = true; + } + } + $objects_to_send = []; + $acks = []; foreach ($parsed['arguments']['want'] as $want_hash) { + // For all the requested non-shallow commits, find + // most recent parent commit the client we have in + // common with the client. + $common_parent_hash = WP_Git_Repository::NULL_OID; + $commit_hash = $want_hash; + while($this->repository->read_object($commit_hash)) { + $objects_to_send[] = $commit_hash; + if($this->repository->get_type() !== WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { + // Just send non-commit objects as they are. It would be lovely to + // delta-compress them in the future. + continue 2; + } + + $parsed_commit = $this->repository->get_parsed_commit(); + if(!isset($parsed_commit['parent'])) { + break; + } + + $commit_hash = $parsed_commit['parent']; + if(isset($have_oids[$commit_hash])) { + break; + } + } + $common_parent_hash = $commit_hash; + // For each wanted commit, find objects not present in any of the have commits $new_objects = $this->repository->find_objects_added_in( $want_hash, - $parsed['arguments']['have'] ?? ['0000000000000000000000000000000000000000'] + $common_parent_hash ); - $objects_to_send = array_merge($objects_to_send, $new_objects); + $objects_to_send = array_merge( + $objects_to_send, + iterator_to_array($new_objects) + ); + if($common_parent_hash !== WP_Git_Repository::NULL_OID) { + $acks[] = $common_parent_hash; + } } - $objects_to_send = array_unique($objects_to_send); + $acks = array_unique($acks); + if(count($parsed['arguments']['have']) > 0) { + $response->write(WP_Git_Pack_Processor::encode_packet_line("acknowledgments\n")); + if(count($acks) > 0) { + foreach($acks as $ack) { + $response->write(WP_Git_Pack_Processor::encode_packet_line("ACK $ack\n")); + } + } else { + $response->write(WP_Git_Pack_Processor::encode_packet_line("NAK\n")); + } + $response->write(WP_Git_Pack_Processor::encode_packet_line("ready\n")); + $response->write(WP_Git_Pack_Processor::encode_packet_line("0001")); + } + $response->write(WP_Git_Pack_Processor::encode_packet_line("packfile\n")); // Pack the objects + $objects_to_send = array_unique($objects_to_send); $pack_objects = []; foreach ($objects_to_send as $oid) { $this->repository->read_object($oid); // Apply blob filters if specified if ($this->repository->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_BLOB) { - $filter = $parsed['arguments']['filter'] ?? null; - if ($filter) { - if ($filter['type'] === 'none') { + if ($filter['type'] === 'blob') { + if ($filter['filter'] === 'none') { continue; // Skip all blobs - } else if ($filter['type'] === 'limit') { + } else if ($filter['filter'] === 'limit') { $content = $this->repository->read_entire_object_contents(); if (strlen($content) > $filter['size']) { continue; // Skip large blobs @@ -205,15 +338,29 @@ public function handle_fetch_request($request) { // @TODO: Implement history truncation based on deepen value // This would involve walking the commit history and including // only commits within the specified depth + throw new Exception('Deepen is not implemented yet'); } // Encode the pack + // @TODO: Stream the pack data instead of buffering it $pack_data = WP_Git_Pack_Processor::encode($pack_objects); - // Format the response according to protocol v2 - return - WP_Git_Pack_Processor::encode_packet_line("packfile\n") . - WP_Git_Pack_Processor::encode_packet_line("\x01" . $pack_data) . // side-band channel 1 - "0000"; + $response->write(WP_Git_Pack_Processor::encode_packet_line("\x01" . $pack_data)); + $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); + $response->end(); + return true; + } + + private function parse_filter($filter) { + if($filter === null) { + return ['type' => 'none']; + } else if($filter === 'blob:none') { + return ['type' => 'blob', 'filter' => 'none']; + } else if(str_starts_with($filter, 'blob:limit=')) { + $limit = substr($filter, strlen('blob:limit=')); + return ['type' => 'blob', 'filter' => 'limit', 'size' => intval($limit)]; + } + return false; } } + diff --git a/packages/playground/data-liberation/tests/WPGitServerTests.php b/packages/playground/data-liberation/tests/WPGitServerTests.php index 030bf56d92..413f99836a 100644 --- a/packages/playground/data-liberation/tests/WPGitServerTests.php +++ b/packages/playground/data-liberation/tests/WPGitServerTests.php @@ -2,6 +2,7 @@ use PHPUnit\Framework\TestCase; use WordPress\Filesystem\WP_In_Memory_Filesystem; +use WordPress\AsyncHttp\ResponseWriter\BufferingResponseWriter; class WPGitServerTests extends TestCase { @@ -255,4 +256,170 @@ public function provideRefRequests() { ]; } + public function test_handle_fetch_request_returns_packfile() { + // Create a more complex repository structure for testing + $readme_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, + "# Hello World" + ); + $large_file_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, + str_repeat('x', 2000) // 2KB file + ); + + $tree_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_TREE, + WP_Git_Pack_Processor::encode_tree_bytes([ + [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'name' => 'README.md', + 'sha1' => $readme_oid + ], + [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'name' => 'large.txt', + 'sha1' => $large_file_oid + ] + ]) + ); + + $commit_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, + "tree $tree_oid\nparent 0000000000000000000000000000000000000000\nauthor Test 1234567890 +0000\ncommitter Test 1234567890 +0000\n\nInitial commit\n" + ); + + $test_cases = [ + 'basic fetch' => [ + 'request' => WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "0000", + "want $commit_oid\n", + "done\n", + "0000", + ]), + 'expected_oids' => [ + $commit_oid, + $tree_oid, + $readme_oid, + $large_file_oid, + ], + ], + 'fetch with blob:none filter' => [ + 'request' => WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "0000", + "want $commit_oid\n", + "filter blob:none\n", + "done\n", + "0000", + ]), + 'expected_oids' => [ + $commit_oid, + $tree_oid, + ], + ], + 'fetch with blob size limit' => [ + 'request' => WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "0000", + "want $commit_oid\n", + "filter blob:limit=1000\n", + "done\n", + "0000", + ]), + 'expected_oids' => [ + $commit_oid, + $tree_oid, + $readme_oid, + ], + ], + 'fetch with multiple wants' => [ + 'request' => WP_Git_Pack_Processor::encode_packet_lines([ + "command=fetch\n", + "0000", + "want $commit_oid\n", + "want $tree_oid\n", + "done\n", + "0000", + ]), + // same objects, just different entry point + 'expected_oids' => [ + $commit_oid, + $tree_oid, + $readme_oid, + $large_file_oid, + ], + ] + ]; + + foreach ($test_cases as $name => $test) { + /** @var BufferingResponseWriter */ + $response = $this->getMockBuilder(BufferingResponseWriter::class) + ->onlyMethods(['end']) + ->getMock(); + $this->server->handle_fetch_request($test['request'], $response); + + // Verify response format + $response = $response->get_buffered_body(); + $expected_response_start = WP_Git_Pack_Processor::encode_packet_lines([ + "acknowledgments\n", + "NACK\n", + "ready\n", + "0001", + "packfile\n", + ]); + $actual_response_start = substr($response, 0, strlen($expected_response_start)); + $this->assertEquals( + $expected_response_start, + $actual_response_start, + "$name: Response should start with packfile header" + ); + + $rest_of_response = substr($response, strlen($expected_response_start)); + $pack_data = WP_Git_Client::accumulate_pack_data_from_multiplexed_chunks( + $rest_of_response + ); + $pack = WP_Git_Pack_Processor::parse_pack_data($pack_data); + + $this->assertCount( + count($test['expected_oids']), + $pack['objects'], + "$name: Pack should contain expected number of objects" + ); + foreach($pack['objects'] as $object) { + $this->assertContains($object['oid'], $test['expected_oids']); + } + } + } + + // public function test_handle_fetch_request_validates_filter() { + // $this->expectException(Exception::class); + // $this->expectExceptionMessage('Invalid filter: invalid:filter'); + + // $request = WP_Git_Pack_Processor::encode_packet_lines([ + // "command=fetch\n", + // "0000", + // "want " . $this->main_branch_oid . "\n", + // "filter invalid:filter\n", + // "done\n", + // "0000", + // ]); + + // $this->server->handle_fetch_request($request); + // } + + // public function test_handle_fetch_request_requires_want() { + // $request = WP_Git_Pack_Processor::encode_packet_lines([ + // "command=fetch\n", + // "0000", + // "done\n", + // "0000", + // ]); + + // $this->assertFalse( + // $this->server->handle_fetch_request($request), + // "Fetch request without want should return false" + // ); + // } + } \ No newline at end of file From 4d90846be68f7f9ef865b426a2d5c2c8e41b6800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Jan 2025 19:25:45 +0100 Subject: [PATCH 62/71] Implement git push in WP_Git_Server --- .../plugin.php | 2 +- .../src/git/WP_Git_Pack_Processor.php | 46 ++-- .../src/git/WP_Git_Repository.php | 50 ++-- .../data-liberation/src/git/WP_Git_Server.php | 256 +++++++++++++++--- .../tests/WPGitServerTests.php | 120 ++++++++ 5 files changed, 403 insertions(+), 71 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 51627f0eeb..5432eef8fe 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -302,7 +302,7 @@ function() { array('wp-components', 'wp-block-editor', 'wp-edit-post'), '1.0.0' ); - + wp_enqueue_script('static-files-editor'); wp_enqueue_style('static-files-editor'); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index b9e008c768..9076c70b7e 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -36,6 +36,10 @@ class WP_Git_Pack_Processor { self::FILE_MODE_COMMIT => 'commit', ]; + const CHANNEL_PACK = "\x01"; + const CHANNEL_PROGRESS = "\x02"; + const CHANNEL_ERROR = "\x03"; + // Helper: Build a barebones pack with the given objects (no compression). // Objects must be in an order that satisfies dependencies if you skip deltas. static public function encode(array $objects): string { @@ -160,12 +164,14 @@ static public function encode_packet_lines(array $payloads): string { return implode('', $lines); } - static public function encode_packet_line(string $payload): string { - if($payload === '0000' || $payload === '0001' || $payload === '0002') { - return $payload; + static public function encode_packet_line(string $payload, $channel=''): string { + $payload = $channel . $payload; + if($payload !== '0000' && $payload !== '0001' && $payload !== '0002') { + $length = sprintf("%04x", strlen($payload) + 4); + } else { + $length = ''; } - $length = strlen($payload) + 4; - return sprintf("%04x", $length) . $payload; + return $length . $payload; } /** @@ -177,7 +183,7 @@ static public function decode($pack_bytes, $pack_index=null) { $pack_index = new WP_Git_Repository(new WP_In_Memory_Filesystem()); } - $parsed_pack = self::parse_pack_data($pack_bytes); + $parsed_pack = self::parse_pack_data($pack_bytes, $pack_index); // Resolve trees foreach($parsed_pack['objects'] as $object) { @@ -292,15 +298,13 @@ static public function parse_tree_bytes($treeContent) { while ($offset < strlen($treeContent)) { if ($offset >= strlen($treeContent)) { - var_dump('uninitialized string offset'); - break; // Prevent uninitialized string offset + throw new Exception('uninitialized string offset'); } // Read file mode $modeEnd = strpos($treeContent, ' ', $offset); if ($modeEnd === false || $modeEnd >= strlen($treeContent)) { - var_dump('invalid mode'); - break; // Invalid mode + throw new Exception('invalid mode'); } $mode = substr($treeContent, $offset, $modeEnd - $offset); $offset = $modeEnd + 1; @@ -320,16 +324,14 @@ static public function parse_tree_bytes($treeContent) { // Read file name $nameEnd = strpos($treeContent, "\0", $offset); if ($nameEnd === false || $nameEnd >= strlen($treeContent)) { - var_dump('invalid name'); - break; // Invalid name + throw new Exception('invalid name'); } $name = substr($treeContent, $offset, $nameEnd - $offset); $offset = $nameEnd + 1; // Read SHA1 if ($offset + 20 > strlen($treeContent)) { - var_dump('invalid sha1'); - break; // Prevent out-of-bounds access + throw new Exception('invalid sha1'); } $sha1 = bin2hex(substr($treeContent, $offset, 20)); $offset += 20; @@ -355,7 +357,7 @@ static private function readVariableLength($data, &$offset) { return $result; } - static public function parse_pack_data($packData) { + static public function parse_pack_data($packData, $pack_index=null) { $offset = 0; // Basic sanity checks @@ -415,10 +417,19 @@ static public function parse_pack_data($packData) { $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); $objects[$i]['type'] = $base['type']; } else if($objects[$i]['type'] === self::OBJECT_TYPE_REF_DELTA) { - if(!isset($by_oid[$objects[$i]['reference']])) { + if(isset($by_oid[$objects[$i]['reference']])) { + $base = $objects[$by_oid[$objects[$i]['reference']]]; + } else if($pack_index) { + if(false === $pack_index->read_object($objects[$i]['reference'])) { + throw new Exception('Failed to read object: ' . $objects[$i]['reference']); + } + $base = [ + 'type' => $pack_index->get_type(), + 'content' => $pack_index->read_entire_object_contents(), + ]; + } else { continue; } - $base = $objects[$by_oid[$objects[$i]['reference']]]; $objects[$i]['content'] = self::applyDelta($base['content'], $objects[$i]['content']); $objects[$i]['type'] = $base['type']; } @@ -522,6 +533,7 @@ static private function parse_pack_header(string $packData, int &$offset) { } } else if ($type === self::OBJECT_TYPE_REF_DELTA) { $reference = substr($packData, $offset, 20); + $reference = bin2hex($reference); $offset += 20; } return [ diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 940cb6d977..9aa5748ac1 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -521,6 +521,14 @@ public function set_ref_head($ref, $oid) { return $this->fs->put_contents($path, $oid); } + public function delete_ref($ref) { + $path = $this->resolve_ref_file_path($ref); + if(!$path) { + return false; + } + return $this->fs->rm($path); + } + public function get_ref_head($ref='HEAD', $options=[]) { if($this->oid_exists($ref)) { return $ref; @@ -989,7 +997,7 @@ private function commit_tree($path, $changed_trees) { ); } - public function list_refs($prefix = '') { + public function list_refs($prefixes = ['']) { $refs = []; /** @@ -999,15 +1007,15 @@ public function list_refs($prefix = '') { * This is a starter implementation. We may need to revisit this * for full compliance with Git. */ - $path = ltrim(wp_canonicalize_path($prefix), '/'); - $first_path = $this->fs->is_dir($path) ? $path : dirname($path); - $stack = []; - if(str_starts_with($first_path, 'refs/')) { - $stack = [$first_path]; - } - if($prefix === '') { - $stack = ['refs/heads/']; + $stack = ['refs/heads/']; + foreach ($prefixes as $prefix) { + $path = ltrim(wp_canonicalize_path($prefix), '/'); + $first_path = $this->fs->is_dir($path) ? $path : dirname($path); + if(str_starts_with($first_path, 'refs/')) { + $stack[] = $first_path; + } } + while(!empty($stack)) { $path = array_shift($stack); if ($this->fs->is_dir($path)) { @@ -1016,17 +1024,27 @@ public function list_refs($prefix = '') { $full_path = wp_join_paths($path, $ref_file); array_push($stack, $full_path); } - } else if(str_starts_with($path, $prefix)) { - $hash = trim($this->fs->get_contents($path)); - if ($hash) { - $ref_name = trim($path, '/'); - $refs[$ref_name] = $hash; + } else if($this->fs->is_file($path)) { + // Check if path matches any of the prefixes + foreach ($prefixes as $prefix) { + if(str_starts_with($path, $prefix)) { + $hash = trim($this->fs->get_contents($path)); + if ($hash) { + $ref_name = trim($path, '/'); + $refs[$ref_name] = $hash; + } + break; + } } } } - if($prefix === '' || str_starts_with('HEAD', $prefix)) { - $refs['HEAD'] = $this->get_ref_head('HEAD'); + // Check if we should include HEAD + foreach ($prefixes as $prefix) { + if($prefix === '' || str_starts_with('HEAD', $prefix)) { + $refs['HEAD'] = $this->get_ref_head('HEAD'); + break; + } } return $refs; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index 382b4f8a0e..3a703c5966 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -17,27 +17,16 @@ public function __construct(WP_Git_Repository $repository) { } public function handle_request($path, $request_bytes, $response) { + error_log("Request: " . $path); + switch($path) { case '/HEAD': $response->write(WP_Git_Pack_Processor::encode_packet_line(sha1("a") . " HEAD\n")); $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); - $response->end(); break; // @TODO handle service=git-upload-pack case '/info/refs?service=git-upload-pack': - $parsed = $this->parse_message($request_bytes); - $response->send_header( - 'Content-Type', - 'application/x-git-upload-pack-advertisement' - ); - $response->send_header( - 'Cache-Control', - 'no-cache' - ); - $response->send_header( - 'Git-Protocol', - 'version=2' - ); + $this->send_protocol_v2_headers($response, 'git-upload-pack'); $response->write(WP_Git_Pack_Processor::encode_packet_lines([ "# service=git-upload-pack\n", "0000", @@ -49,10 +38,20 @@ public function handle_request($path, $request_bytes, $response) { "object-format=sha1\n", "0000" ])); - flush(); - $response->end(); + break; + case '/info/refs?service=git-receive-pack': + $this->send_protocol_v2_headers($response, 'git-receive-pack'); + $response->write(WP_Git_Pack_Processor::encode_packet_lines([ + "# service=git-receive-pack\n", + "0000", + ])); + $this->respond_with_ls_refs($response, [ + 'capabilities' => 'report-status report-status-v2 delete-refs side-band-64k ofs-delta atomic object-format=sha1 quiet agent=github/spokes-receive-pack-bff11521ff0f3fc96efd2ba7a18ecebb89dc6949 session-id=26DD:527D3:3A481E46:3BF47E4D:677BF4BA push-options', + ]); + $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); break; case '/git-upload-pack': + $this->send_protocol_v2_headers($response, 'git-upload-pack'); $parsed = $this->parse_message($request_bytes); switch($parsed['capabilities']['command']) { case 'ls-refs': @@ -66,12 +65,29 @@ public function handle_request($path, $request_bytes, $response) { } break; case '/git-receive-pack': - throw new Exception('Not implemented yet'); + $this->send_protocol_v2_headers($response, 'git-receive-pack'); + $this->handle_push_request($request_bytes, $response); + break; default: throw new Exception('Unknown path: ' . $path); } } + private function send_protocol_v2_headers(ResponseWriter $response, $service) { + $response->send_header( + 'Content-Type', + 'application/x-' . $service . '-advertisement' + ); + $response->send_header( + 'Cache-Control', + 'no-cache' + ); + $response->send_header( + 'Git-Protocol', + 'version=2' + ); + } + /** * Handle Git protocol v2 ls-refs command * @@ -109,31 +125,30 @@ public function handle_request($path, $request_bytes, $response) { * @return string The response in Git protocol v2 format */ public function handle_ls_refs_request($request_bytes, ResponseWriter $response) { - $response->send_header( - 'Content-Type', - 'application/x-git-upload-pack-advertisement' - ); - $response->send_header( - 'Cache-Control', - 'no-cache' - ); - $response->send_header( - 'Git-Protocol', - 'version=2' - ); - $parsed = $this->parse_message($request_bytes); if(!$parsed) { // return false; } - $prefix = $parsed['arguments']['ref-prefix'][0] ?? ''; - $refs = $this->repository->list_refs($prefix); + $this->respond_with_ls_refs($response, [ + 'ref-prefix' => $parsed['arguments']['ref-prefix'] ?? [''], + 'capabilities' => 'multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/trunk filter object-format=sha1 agent=git/github-395dce4f6ecf', + ]); + + // End the response with 0000 + $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); + } + + private function respond_with_ls_refs($response, $options) { + $ref_prefixes = $options['ref-prefix'] ?? ['']; + $capabilities_to_advertise = $options['capabilities']; + + $refs = $this->repository->list_refs($ref_prefixes); $first_ref = array_key_first($refs); foreach ($refs as $ref_name => $ref_hash) { $line = $ref_hash . ' ' . $ref_name; if($ref_name === $first_ref) { - $line .= "\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/trunk filter object-format=sha1 agent=git/github-395dce4f6ecf"; + $line .= "\0$capabilities_to_advertise"; } // Format: \n $response->write( @@ -142,9 +157,6 @@ public function handle_ls_refs_request($request_bytes, ResponseWriter $response) ) ); } - - // End the response with 0000 - $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); } /** @@ -347,10 +359,180 @@ public function handle_fetch_request($request_bytes, $response) { $response->write(WP_Git_Pack_Processor::encode_packet_line("\x01" . $pack_data)); $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); - $response->end(); return true; } + /** + * Handle Git protocol v2 push command + * + * @param string $request_bytes Raw request bytes + * @param ResponseWriter $response Response writer + * @return bool Success status + */ + public function handle_push_request($request_bytes, ResponseWriter $response) { + /* + 16:13:09.493439 http.c:637 <= Recv header: + 16:13:09.493829 pkt-line.c:80 packet: sideband< \2\0 + 16:13:09.493888 http.c:678 == Info: Connection #0 to host github.com left intact + 16:13:09.493998 pkt-line.c:80 packet: sideband< \1000eunpack ok + 16:13:09.494062 pkt-line.c:80 packet: sideband< \10017ok refs/heads/test + 16:13:09.494104 pkt-line.c:80 packet: sideband< \2Create a pull request for 'test' on GitHub by visiting: https://github.com/adamziel/playground-docs-workflow/pull/new/test + remote: + 16:13:09.494196 pkt-line.c:80 packet: git< unpack ok + remote: Create a pull request for 'test' on GitHub by visiting: + remote: https://github.com/adamziel/playground-docs-workflow/pull/new/test + remote: + 16:13:09.494261 pkt-line.c:80 packet: sideband< \10000 + 16:13:09.494257 pkt-line.c:80 packet: git< ok refs/heads/test + 16:13:09.494277 pkt-line.c:80 packet: sideband< 0000 + 16:13:09.494328 pkt-line.c:80 packet: git< 0000 + 16:13:09.494343 pkt-line.c:80 packet: git> 0000 + 000016:13:09.494727 trace.c:411 performance: 0.799128000 s: git command: /usr/local/Cellar/git/2.37.3/libexec/git-core/git send-pack --stateless-rpc --helper-status --thin --progress https://github.com/adamziel/playground-docs-workflow.git/ --stdin + To https://github.com/adamziel/playground-docs-workflow.git + * [new branch] test -> test + branch 'test' set up to track 'docs/test'. + 16:13:09.523605 trace.c:411 performance: 1.993747000 s: git command: /usr/local/Cellar/git/2.37.3/libexec/git-core/git remote-https docs https://github.com/adamziel/playground-docs-workflow.git + 16:13:09.526981 trace.c:411 performance: 2.015966000 s: git command: /usr/local/bin/git push -u docs test + */ + $parsed = $this->parse_push_request($request_bytes); + if (!$parsed || empty($parsed['new_oid'])) { + return false; + } + + $response->send_header('Content-Type', 'application/x-git-receive-pack-result'); + $response->send_header('Cache-Control', 'no-cache'); + + $old_oid = $parsed['old_oid']; + $new_oid = $parsed['new_oid']; + $ref_name = $parsed['ref_name']; + + // Validate ref name + if (!preg_match('|^refs/|', $ref_name)) { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "error invalid ref name: $ref_name\n", + WP_Git_Pack_Processor::CHANNEL_ERROR + )); + $response->write("0000", WP_Git_Pack_Processor::CHANNEL_ERROR); + // @TODO: Throw / catch? + return false; + } + + // Handle deletion + if ($new_oid === WP_Git_Repository::NULL_OID) { + if ($this->repository->delete_ref($ref_name)) { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "ok $ref_name\n", + WP_Git_Pack_Processor::CHANNEL_PACK + )); + } else { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "error $ref_name delete failed\n", + WP_Git_Pack_Processor::CHANNEL_ERROR + )); + $response->write("0000", WP_Git_Pack_Processor::CHANNEL_ERROR); + } + return false; + } + + // Unpack objects if provided + if (isset($parsed['pack_data'])) { + $success = WP_Git_Pack_Processor::decode( + $parsed['pack_data'], + $this->repository + ); + if($success) { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "000eunpack ok\n", + WP_Git_Pack_Processor::CHANNEL_PACK + )); + } else { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "error unpack failed\n", + WP_Git_Pack_Processor::CHANNEL_ERROR + )); + $response->write("0000", WP_Git_Pack_Processor::CHANNEL_ERROR); + // @TODO: Throw? + return false; + } + } + + // Verify we have the object + if (!$this->repository->read_object($new_oid)) { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "error missing object: $new_oid\n", + WP_Git_Pack_Processor::CHANNEL_ERROR + )); + $response->write("0000", WP_Git_Pack_Processor::CHANNEL_ERROR); + // @TODO: Throw? + return false; + } + + // Update ref + if ($this->repository->set_ref_head($ref_name, $new_oid)) { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "0017ok $ref_name\n", + WP_Git_Pack_Processor::CHANNEL_PACK + )); + } else { + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "error $ref_name update failed\n", + WP_Git_Pack_Processor::CHANNEL_ERROR + )); + $response->write("0000", WP_Git_Pack_Processor::CHANNEL_ERROR); + // @TODO: Throw? + return false; + } + + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "0000", + WP_Git_Pack_Processor::CHANNEL_PACK + )); + $response->write(WP_Git_Pack_Processor::encode_packet_line( + "0000" + )); + return true; + } + + /** + * Parse a push request according to Git protocol v2 + * + * @param string $request_bytes Raw request bytes + * @return array|false Parsed request data or false on error + */ + private function parse_push_request($request_bytes) { + $parsed = [ + 'old_oid' => null, + 'new_oid' => null, + 'ref_name' => null, + 'capabilities' => [], + 'pack_data' => '', + ]; + + $offset = 0; + while (true) { + $line = WP_Git_Pack_Processor::decode_next_packet_line($request_bytes, $offset); + if ($line === false) { + break; + } + + if ($line['type'] === '#packet') { + if (preg_match('/^(?:([0-9a-f]{40}) )?([0-9a-f]{40}) (.+?)\0(.+)$/', $line['payload'], $matches)) { + $parsed['old_oid'] = $matches[1]; + $parsed['new_oid'] = $matches[2]; + $parsed['ref_name'] = $matches[3]; + $parsed['capabilities'] = explode(' ', trim($matches[4])); + } else { + throw new Exception('Invalid push request'); + } + } else if($line['type'] === '#flush') { + $parsed['pack_data'] = substr($request_bytes, $offset, strlen($request_bytes) - $offset); + break; + } + } + + return $parsed; + } + private function parse_filter($filter) { if($filter === null) { return ['type' => 'none']; diff --git a/packages/playground/data-liberation/tests/WPGitServerTests.php b/packages/playground/data-liberation/tests/WPGitServerTests.php index 413f99836a..3d4a81c4c0 100644 --- a/packages/playground/data-liberation/tests/WPGitServerTests.php +++ b/packages/playground/data-liberation/tests/WPGitServerTests.php @@ -422,4 +422,124 @@ public function test_handle_fetch_request_returns_packfile() { // ); // } + public function test_handle_push_request() { + // Create test objects + $readme_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, + "# New Content" + ); + + $tree_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_TREE, + WP_Git_Pack_Processor::encode_tree_bytes([ + [ + 'mode' => WP_Git_Pack_Processor::FILE_MODE_REGULAR_NON_EXECUTABLE, + 'name' => 'README.md', + 'sha1' => $readme_oid + ] + ]) + ); + + $commit_oid = $this->repository->add_object( + WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT, + "tree $tree_oid\nparent 0000000000000000000000000000000000000000\nauthor Test 1234567890 +0000\ncommitter Test 1234567890 +0000\n\nPush test\n" + ); + + $test_cases = [ + 'basic push' => [ + 'request' => WP_Git_Pack_Processor::encode_packet_lines([ + "command=push\n", + "0000", + "0000000000000000000000000000000000000000 $commit_oid refs/heads/main\n", + "0000" + ]), + 'expected_ref' => 'refs/heads/main', + 'expected_oid' => $commit_oid + ], + 'delete ref' => [ + 'request' => WP_Git_Pack_Processor::encode_packet_lines([ + "command=push\n", + "0000", + "$commit_oid 0000000000000000000000000000000000000000 refs/heads/main\n", + "0000" + ]), + 'expected_ref' => 'refs/heads/main', + 'expected_oid' => null + ] + ]; + + foreach ($test_cases as $name => $test) { + /** @var BufferingResponseWriter */ + $response = $this->getMockBuilder(BufferingResponseWriter::class) + ->onlyMethods(['end']) + ->getMock(); + + $this->server->handle_push_request($test['request'], $response); + + $response_body = $response->get_buffered_body(); + + if ($test['expected_oid'] === null) { + // Should be deleted + $this->assertFalse( + $this->repository->get_ref_head($test['expected_ref']), + "$name: Ref should be deleted" + ); + } else { + // Should be updated + $this->assertEquals( + $test['expected_oid'], + $this->repository->get_ref_head($test['expected_ref']), + "$name: Ref should be updated to new commit" + ); + } + + // Should contain "ok" response + $this->assertStringContainsString( + "ok " . $test['expected_ref'] . "\n", + $response_body, + "$name: Response should contain success message" + ); + } + } + + public function test_handle_push_request_with_packfile() { + // Create a packfile with new objects + $readme_content = "# Pushed Content"; + $pack_data = WP_Git_Pack_Processor::encode([ + [ + 'type' => WP_Git_Pack_Processor::OBJECT_TYPE_BLOB, + 'content' => $readme_content + ] + ]); + + $readme_oid = sha1("blob " . strlen($readme_content) . "\0" . $readme_content); + + $request = WP_Git_Pack_Processor::encode_packet_lines([ + "command=push\n", + "0000", + "0000000000000000000000000000000000000000 $readme_oid refs/heads/test\n", + "packfile\n" + ]) . $pack_data . "0000"; + + /** @var BufferingResponseWriter */ + $response = $this->getMockBuilder(BufferingResponseWriter::class) + ->onlyMethods(['end']) + ->getMock(); + + $this->server->handle_push_request($request, $response); + + // Verify the object was stored + $this->assertTrue( + $this->repository->read_object($readme_oid), + "Object should be stored in repository" + ); + + // Verify the ref was updated + $this->assertEquals( + $readme_oid, + $this->repository->get_ref_head('refs/heads/test'), + "Ref should be updated to new object" + ); + } + } \ No newline at end of file From 32af23d61c4bad45a798eaf466c0ca7e3d8d7b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Jan 2025 19:26:45 +0100 Subject: [PATCH 63/71] Remove debug comment --- .../data-liberation/src/git/WP_Git_Server.php | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index 3a703c5966..b88dcf79d0 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -370,30 +370,6 @@ public function handle_fetch_request($request_bytes, $response) { * @return bool Success status */ public function handle_push_request($request_bytes, ResponseWriter $response) { - /* - 16:13:09.493439 http.c:637 <= Recv header: - 16:13:09.493829 pkt-line.c:80 packet: sideband< \2\0 - 16:13:09.493888 http.c:678 == Info: Connection #0 to host github.com left intact - 16:13:09.493998 pkt-line.c:80 packet: sideband< \1000eunpack ok - 16:13:09.494062 pkt-line.c:80 packet: sideband< \10017ok refs/heads/test - 16:13:09.494104 pkt-line.c:80 packet: sideband< \2Create a pull request for 'test' on GitHub by visiting: https://github.com/adamziel/playground-docs-workflow/pull/new/test - remote: - 16:13:09.494196 pkt-line.c:80 packet: git< unpack ok - remote: Create a pull request for 'test' on GitHub by visiting: - remote: https://github.com/adamziel/playground-docs-workflow/pull/new/test - remote: - 16:13:09.494261 pkt-line.c:80 packet: sideband< \10000 - 16:13:09.494257 pkt-line.c:80 packet: git< ok refs/heads/test - 16:13:09.494277 pkt-line.c:80 packet: sideband< 0000 - 16:13:09.494328 pkt-line.c:80 packet: git< 0000 - 16:13:09.494343 pkt-line.c:80 packet: git> 0000 - 000016:13:09.494727 trace.c:411 performance: 0.799128000 s: git command: /usr/local/Cellar/git/2.37.3/libexec/git-core/git send-pack --stateless-rpc --helper-status --thin --progress https://github.com/adamziel/playground-docs-workflow.git/ --stdin - To https://github.com/adamziel/playground-docs-workflow.git - * [new branch] test -> test - branch 'test' set up to track 'docs/test'. - 16:13:09.523605 trace.c:411 performance: 1.993747000 s: git command: /usr/local/Cellar/git/2.37.3/libexec/git-core/git remote-https docs https://github.com/adamziel/playground-docs-workflow.git - 16:13:09.526981 trace.c:411 performance: 2.015966000 s: git command: /usr/local/bin/git push -u docs test - */ $parsed = $this->parse_push_request($request_bytes); if (!$parsed || empty($parsed['new_oid'])) { return false; From 122fe94e235ef0950aeb09867ed5591d6f42a5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Jan 2025 19:27:30 +0100 Subject: [PATCH 64/71] Add TODO --- packages/playground/data-liberation/src/git/WP_Git_Server.php | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index b88dcf79d0..a5fb52ab05 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -379,6 +379,7 @@ public function handle_push_request($request_bytes, ResponseWriter $response) { $response->send_header('Cache-Control', 'no-cache'); $old_oid = $parsed['old_oid']; + // @TODO: Verify the old_oid is the ref_name tip $new_oid = $parsed['new_oid']; $ref_name = $parsed['ref_name']; From 149f8b45a16d8be32af142309863b17df7fc14f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Jan 2025 20:02:12 +0100 Subject: [PATCH 65/71] git fetch WordPress pages --- .../git-repo/index.php | 87 +++++++++++++++++++ .../data-liberation/src/git/WP_Git_Server.php | 3 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/playground/data-liberation-static-files-editor/git-repo/index.php diff --git a/packages/playground/data-liberation-static-files-editor/git-repo/index.php b/packages/playground/data-liberation-static-files-editor/git-repo/index.php new file mode 100644 index 0000000000..da71e943b3 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/git-repo/index.php @@ -0,0 +1,87 @@ +get_ref_head('HEAD')) { + $server = new WP_Git_Server($repository); + $repository->set_ref_head('HEAD', 'ref: refs/heads/main'); + $repository->set_ref_head('refs/heads/main', '0000000000000000000000000000000000000000'); + $main_branch_oid = $repository->commit([ + '.gitkeep' => '!', + ]); +} + +$server = new WP_Git_Server( + $repository, + [ + 'root' => GIT_DIRECTORY_ROOT, + ] +); + +$request_bytes = file_get_contents('php://input'); +$response = new BufferingResponseWriter(); + +$query_string = $_SERVER['REQUEST_URI'] ?? ""; +$path = substr($query_string, strlen($_SERVER['PHP_SELF'])); +if($path[0] === '?') { + $path = substr($path, 1); + $path = preg_replace('/&(amp;)?/', '?', $path, 1); +} + +// Before handling the request, commit all the pages to the git repo +switch($path) { + // ls refs – protocol discovery + case '/info/refs?service=git-upload-pack': + // ls refs or fetch – smart protocol + case '/git-upload-pack': + $post_types = [ + 'page', + 'post', + 'local_file', + ]; + + // @TODO: Don't brute-force delete everything, only the + // delta. + // @TODO: Do streaming and amend the commit every few changes + // @TODO: Use the streaming exporter instead of the ad-hoc loop below + $diff = [ + // Delete all the pages + 'updates' => [], + ]; + foreach($post_types as $post_type) { + $pages = get_posts([ + 'post_type' => $post_type, + 'post_status' => 'publish', + ]); + foreach($pages as $page) { + $file_path = $post_type . '/' . $page->post_name . '.html'; + // @TODO: Run the Markdown or block markup exporter + $diff['updates'][$file_path] = $page->post_content; + } + } + if(!$repository->commit($diff)) { + throw new Exception('Failed to commit changes'); + } + break; +} + +$server->handle_request($path, $request_bytes, $response); + +// @TODO: If we just pushed, run importer from the pushed branch diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index a5fb52ab05..dfbf47532d 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -71,6 +71,7 @@ public function handle_request($path, $request_bytes, $response) { default: throw new Exception('Unknown path: ' . $path); } + $response->end(); } private function send_protocol_v2_headers(ResponseWriter $response, $service) { @@ -305,7 +306,7 @@ public function handle_fetch_request($request_bytes, $response) { } } $acks = array_unique($acks); - if(count($parsed['arguments']['have']) > 0) { + if(isset($parsed['arguments']['have']) && count($parsed['arguments']['have']) > 0) { $response->write(WP_Git_Pack_Processor::encode_packet_line("acknowledgments\n")); if(count($acks) > 0) { foreach($acks as $ack) { From 75f7601102e10da7c4319bae1316b1caf8640a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Jan 2025 20:05:46 +0100 Subject: [PATCH 66/71] Add readme for the git repo plugin --- .../git-repo/README.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/playground/data-liberation-static-files-editor/git-repo/README.md diff --git a/packages/playground/data-liberation-static-files-editor/git-repo/README.md b/packages/playground/data-liberation-static-files-editor/git-repo/README.md new file mode 100644 index 0000000000..a3f8456019 --- /dev/null +++ b/packages/playground/data-liberation-static-files-editor/git-repo/README.md @@ -0,0 +1,25 @@ +## Git API for WordPress + +This is a simple git server that allows you to push and pull your content to a WordPress site as if it was a git repository. Because now it is. + +Basic usage with the dirty dev scripts shipped in this PR: + +Start WordPress server with this plugin enabled: + +``` +cd packages/playground/data-liberation-static-files-editor/ +bash run.sh +``` + +Cool! Now you can use it as a git repo: + +``` +cd my-git-repo-dir +git init +git remote add wp http://localhost:9400/wp-content/plugins/z-data-liberation-static-files-editor/git-repo/index.php\? +git pull wp main +``` + +All your pages and posts should now be available for editing in the local directory and versioned in your local git repo (and the one maintained by the plugin). + +Pushing changes is not supported yet, but it wouldn't be too difficult to implement. From 9b09454489146dd61645ab26011e59f68e8d6e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Jan 2025 20:08:26 +0100 Subject: [PATCH 67/71] Add a todo --- .../playground/data-liberation/src/git/WP_Git_Repository.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 9aa5748ac1..bd692804c8 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -620,6 +620,10 @@ static private function wrap_git_object($type, $object) { return "$type_name $length\x00" . $object; } + /** + * @TODO: Don't commit without a "force" option if the + * changeset didn't actually change the root tree oid. + */ public function commit($options=[]) { // First process all blob updates $updates = $options['updates'] ?? []; From 8dd84b3a1ffdb0a0566854f8d750264dee544ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 7 Jan 2025 00:24:28 +0100 Subject: [PATCH 68/71] git push to update content --- .../git-repo/index.php | 91 +++++++++++++++++-- .../src/git/WP_Git_Repository.php | 11 +++ 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/git-repo/index.php b/packages/playground/data-liberation-static-files-editor/git-repo/index.php index da71e943b3..e272d89761 100644 --- a/packages/playground/data-liberation-static-files-editor/git-repo/index.php +++ b/packages/playground/data-liberation-static-files-editor/git-repo/index.php @@ -17,6 +17,7 @@ } $fs = new WP_Local_Filesystem($git_repo_path); $repository = new WP_Git_Repository($fs); +$git_fs = new WP_Git_Filesystem($repository); // Ensure the root commit exists if(!$repository->get_ref_head('HEAD')) { @@ -46,16 +47,16 @@ } // Before handling the request, commit all the pages to the git repo +$synced_post_types = [ + 'page', + 'post', + 'local_file', +]; switch($path) { // ls refs – protocol discovery case '/info/refs?service=git-upload-pack': // ls refs or fetch – smart protocol case '/git-upload-pack': - $post_types = [ - 'page', - 'post', - 'local_file', - ]; // @TODO: Don't brute-force delete everything, only the // delta. @@ -65,15 +66,24 @@ // Delete all the pages 'updates' => [], ]; - foreach($post_types as $post_type) { + foreach($synced_post_types as $post_type) { $pages = get_posts([ 'post_type' => $post_type, 'post_status' => 'publish', ]); foreach($pages as $page) { $file_path = $post_type . '/' . $page->post_name . '.html'; + $metadata = []; + foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { + $metadata[$key] = get_post_field($key, $page->ID); + } + + $converter = new WP_Block_HTML_Serializer( $page->post_content, $metadata ); + if(false === $converter->convert()) { + throw new Exception('Failed to convert the post to HTML'); + } // @TODO: Run the Markdown or block markup exporter - $diff['updates'][$file_path] = $page->post_content; + $diff['updates'][$file_path] = $converter->get_result(); } } if(!$repository->commit($diff)) { @@ -84,4 +94,69 @@ $server->handle_request($path, $request_bytes, $response); -// @TODO: If we just pushed, run importer from the pushed branch +// @TODO: Support the use-case below in the streaming importer +// @TODO: When a page is moved, don't delete the old page and create a new one but +// rather update the existing page. +if($path === '/git-receive-pack') { + foreach($synced_post_types as $post_type) { + $updated_ids = []; + foreach($git_fs->ls($post_type) as $file_name) { + $file_path = $post_type . '/' . $file_name; + $converter = new WP_HTML_With_Blocks_to_Blocks( + $git_fs->get_contents($file_path) + ); + if(false === $converter->convert()) { + throw new Exception('Failed to convert the post to HTML'); + } + + $existing_posts = get_posts([ + 'post_type' => $post_type, + 'meta_key' => 'local_file_path', + 'meta_value' => $file_path, + ]); + + $filename_without_extension = pathinfo($file_name, PATHINFO_FILENAME); + + if($existing_posts) { + $post_id = $existing_posts[0]->ID; + } else { + $post_id = wp_insert_post([ + 'post_type' => $post_type, + 'post_status' => 'publish', + 'post_title' => $filename_without_extension, + 'meta_input' => [ + 'local_file_path' => $file_path, + ], + ]); + } + $updated_ids[] = $post_id; + + $metadata = $converter->get_all_metadata(['first_value_only' => true]); + $updated = wp_update_post(array( + 'ID' => $post_id, + 'post_name' => $filename_without_extension, + 'post_content' => $converter->get_block_markup(), + 'post_title' => $metadata['post_title'] ?? '', + 'post_date_gmt' => $metadata['post_date_gmt'] ?? '', + 'menu_order' => $metadata['menu_order'] ?? '', + 'meta_input' => $metadata, + )); + if(is_wp_error($updated)) { + throw new Exception('Failed to update post'); + } + } + + // Delete posts that were not updated (i.e. files were deleted) + $posts_to_delete = get_posts([ + 'post_type' => $post_type, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'post__not_in' => $updated_ids, + 'fields' => 'ids' + ]); + + foreach($posts_to_delete as $post_id) { + wp_delete_post($post_id, true); + } + } +} diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index bd692804c8..687efdd1dd 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -679,9 +679,20 @@ public function commit($options=[]) { $is_amend = isset($options['amend']) && $options['amend']; + $this->read_object($this->get_ref_head('refs/heads/main')); + $old_tree_oid = $this->get_parsed_commit()['tree']; + // Process trees bottom-up recursively $root_tree_oid = $this->commit_tree('/', $changed_trees); + if( + $root_tree_oid === $old_tree_oid && + !$is_amend + ) { + // Nothing has changed, skip creating a new empty commit. + return $this->oid; + } + // Create a new commit object $options['tree'] = $root_tree_oid; if($this->get_ref_head('HEAD')) { From 2d5da28ded1cf4a80d943ef4f41f258180687c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 7 Jan 2025 14:31:25 +0100 Subject: [PATCH 69/71] When fetching, fetch all the relevant objects without leaving out any --- .../playground/data-liberation/bootstrap.php | 1 + .../data-liberation/src/git/WP_Git_Client.php | 5 +- .../src/git/WP_Git_Pack_Processor.php | 21 +++- .../src/git/WP_Git_Repository.php | 110 +++++++----------- .../data-liberation/src/git/WP_Git_Server.php | 15 ++- .../data-liberation/src/git/functions.php | 41 +++++++ 6 files changed, 109 insertions(+), 84 deletions(-) create mode 100644 packages/playground/data-liberation/src/git/functions.php diff --git a/packages/playground/data-liberation/bootstrap.php b/packages/playground/data-liberation/bootstrap.php index 51c2543dee..e5c6b32ede 100644 --- a/packages/playground/data-liberation/bootstrap.php +++ b/packages/playground/data-liberation/bootstrap.php @@ -99,6 +99,7 @@ require_once __DIR__ . '/src/git/WP_Git_Filesystem.php'; require_once __DIR__ . '/src/git/WP_Git_Server.php'; require_once __DIR__ . '/src/git/WP_Git_Merge_Engine.php'; +require_once __DIR__ . '/src/git/functions.php'; require_once __DIR__ . '/src/WP_Data_Liberation_HTML_Processor.php'; require_once __DIR__ . '/src/utf8_decoder.php'; diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 3a80c1323b..6c85d63456 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -91,6 +91,7 @@ public function force_push_one_commit() { $pack_objects = []; foreach($delta as $oid) { // @TODO: just stream the saved object instead of re-reading and re-encoding it. + $this->index->read_object($oid); $body = ''; do { $body .= $this->index->get_body_chunk(); @@ -179,8 +180,8 @@ public function force_pull($branch_name=null, $path = '/') { $all_path_related_oids = array_flip($all_path_related_oids); // @TODO: Support "want" and "have" here - $new_oids = $remote_index->find_objects_added_in($remote_head, $local_ref, [ - 'old_tree_index' => $local_index, + $new_oids = $remote_index->find_objects_added_in($remote_head, $local_ref ?: null, [ + 'old_commit_repository' => $local_index, ]); $objects_to_fetch = []; foreach($new_oids as $oid) { diff --git a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php index 9076c70b7e..4255e606a7 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Pack_Processor.php @@ -165,13 +165,22 @@ static public function encode_packet_lines(array $payloads): string { } static public function encode_packet_line(string $payload, $channel=''): string { - $payload = $channel . $payload; - if($payload !== '0000' && $payload !== '0001' && $payload !== '0002') { - $length = sprintf("%04x", strlen($payload) + 4); - } else { - $length = ''; + // @TODO: Stream instead of buffering + if($payload === '0000' || $payload === '0001' || $payload === '0002') { + $payload = $channel . $payload; + return $payload; } - return $length . $payload; + + $chunk_size = 8000; + $offset = 0; + $lines = []; + while($offset < strlen($payload)) { + $chunk = $channel . substr($payload, $offset, $chunk_size); + $length = sprintf("%04x", strlen($chunk) + 4); + $lines[] = $length . $chunk; + $offset += $chunk_size; + } + return implode('', $lines); } /** diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 687efdd1dd..28a7fa1707 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -330,6 +330,7 @@ public function get_parsed_commit() { if(null === $this->parsed_commit && $this->oid) { $commit_body = $this->read_entire_object_contents(); $this->parsed_commit = WP_Git_Pack_Processor::parse_commit_body($commit_body); + $this->parsed_commit['oid'] = $this->oid; if(!$this->parsed_commit) { $this->last_error = 'Failed to parse commit'; $this->parsed_commit = []; @@ -433,84 +434,53 @@ public function find_path_descendants($path) { return $oids; } - public function find_objects_added_in($new_tree_oid, $old_tree_oid=WP_Git_Repository::NULL_OID, $options=[]) { - $old_tree_index = $options['old_tree_index'] ?? $this; - if($old_tree_index === null) { - $old_tree_index = $this; - } - - // Resolve the actual tree oid if $new_tree_oid is a commit - if(false === $this->read_object($new_tree_oid)) { - $this->last_error = 'Failed to read object: ' . $new_tree_oid; - return false; - } - if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { - // yield the commit object itself - $parsed_commit = $this->get_parsed_commit(); - $new_tree_oid = $parsed_commit['tree']; - yield $this->oid; - } - - // Resolve the actual tree oid if $old_tree_oid is a commit - if(!$this->is_null_oid($old_tree_oid)) { - if(false === $old_tree_index->read_object($old_tree_oid)) { - $this->last_error = 'Failed to read object: ' . $old_tree_oid; - return false; + public function find_objects_added_in($new_commit_hash, $old_commit_hash=WP_Git_Repository::NULL_OID, $options=[]) { + $new_commit = wp_git_get_parsed_commit($this, $new_commit_hash); + if(!$new_commit) { + throw new Exception('Failed to read new commit object: ' . $new_commit_hash); + } + // Resolve the actual tree oid if $old_commit_hash is a commit + $old_tree_hash = WP_Git_Repository::NULL_OID; + $old_objects_oids = []; + if(!wp_git_is_null_oid($old_commit_hash)) { + $old_commit_repository = $options['old_commit_repository'] ?? $this; + if(false === $old_commit_repository->read_object($old_commit_hash)) { + throw new Exception('Failed to read old commit object: ' . $old_commit_hash); } - if($old_tree_index->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { - $old_tree_oid = $old_tree_index->get_parsed_commit()['tree']; + if($old_commit_repository->get_type() !== WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { + throw new Exception('Object was not a commit in find_objects_added_in: ' . $old_commit_repository->get_type()); } + $old_tree_hash = $old_commit_repository->get_parsed_commit()['tree']; + $old_objects_oids = array_flip( + wp_git_get_all_descendant_oids_in_tree($old_commit_repository, $old_tree_hash) + ); + $old_objects_oids[$old_commit_hash] = true; } - if($new_tree_oid === $old_tree_oid) { - return false; - } - - $stack = [[$new_tree_oid, $old_tree_oid]]; - - while(!empty($stack)) { - list($current_new_oid, $current_old_oid) = array_pop($stack); + $new_objects_oids = []; + // Optimization – don't process the same tree more than once. + $processed_trees = []; - // Object is unchanged - if($current_new_oid === $current_old_oid) { - continue; + while($new_commit_hash !== $old_commit_hash && !wp_git_is_null_oid($new_commit_hash)) { + if(false === $this->read_object($new_commit_hash)) { + throw new Exception('Failed to read new commit object: ' . $new_commit_hash); } - if($this->is_null_oid($current_new_oid)) { - continue; - } - - if(false === $this->read_object($current_new_oid)) { - $this->last_error = 'Failed to read object: ' . $current_new_oid; - return false; - } - if($this->get_type() === WP_Git_Pack_Processor::OBJECT_TYPE_BLOB) { - yield $this->get_oid(); - continue; - } else if($this->get_type() !== WP_Git_Pack_Processor::OBJECT_TYPE_TREE) { - _doing_it_wrong(__METHOD__, 'Invalid object type in find_objects_added_in: ' . $this->get_type(), '1.0.0'); - return false; - } - - $new_tree = $this->get_parsed_tree(); - yield $this->get_oid(); - - $old_tree = []; - if(!$this->is_null_oid($current_old_oid)) { - if(false === $old_tree_index->read_object($current_old_oid)) { - $this->last_error = 'Failed to read object: ' . $current_old_oid; - return false; + $new_objects_oids[$new_commit_hash] = true; + $parsed_commit = $this->get_parsed_commit(); + $tree_oid = $parsed_commit['tree']; + $new_objects_oids[$tree_oid] = true; + if(!isset($processed_trees[$tree_oid])) { + $descendants = wp_git_get_all_descendant_oids_in_tree($this, $tree_oid); + foreach($descendants as $descendant) { + $new_objects_oids[$descendant] = true; } - $old_tree = $old_tree_index->get_parsed_tree(); - } - - foreach($new_tree as $name => $object) { - $stack[] = [$object['sha1'], $old_tree[$name]['sha1'] ?? null]; } + $processed_trees[$tree_oid] = true; + $new_commit_hash = $parsed_commit['parent'] ?? WP_Git_Repository::NULL_OID; } - } - private function is_null_oid($oid) { - return $oid === null || $oid === WP_Git_Repository::NULL_OID; + $diff = array_diff_key($new_objects_oids, $old_objects_oids); + return array_keys($diff); } public function set_ref_head($ref, $oid) { @@ -680,13 +650,13 @@ public function commit($options=[]) { $is_amend = isset($options['amend']) && $options['amend']; $this->read_object($this->get_ref_head('refs/heads/main')); - $old_tree_oid = $this->get_parsed_commit()['tree']; + $old_commit_hash = $this->get_parsed_commit()['tree']; // Process trees bottom-up recursively $root_tree_oid = $this->commit_tree('/', $changed_trees); if( - $root_tree_oid === $old_tree_oid && + $root_tree_oid === $old_commit_hash && !$is_amend ) { // Nothing has changed, skip creating a new empty commit. diff --git a/packages/playground/data-liberation/src/git/WP_Git_Server.php b/packages/playground/data-liberation/src/git/WP_Git_Server.php index dfbf47532d..82f6903166 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Server.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Server.php @@ -282,25 +282,28 @@ public function handle_fetch_request($request_bytes, $response) { $parsed_commit = $this->repository->get_parsed_commit(); if(!isset($parsed_commit['parent'])) { + $common_parent_hash = WP_Git_Repository::NULL_OID; break; } $commit_hash = $parsed_commit['parent']; if(isset($have_oids[$commit_hash])) { + $common_parent_hash = $commit_hash; break; } } - $common_parent_hash = $commit_hash; // For each wanted commit, find objects not present in any of the have commits $new_objects = $this->repository->find_objects_added_in( $want_hash, $common_parent_hash ); - $objects_to_send = array_merge( - $objects_to_send, - iterator_to_array($new_objects) - ); + if(false !== $new_objects) { + $objects_to_send = array_merge( + $objects_to_send, + $new_objects + ); + } if($common_parent_hash !== WP_Git_Repository::NULL_OID) { $acks[] = $common_parent_hash; } @@ -358,7 +361,7 @@ public function handle_fetch_request($request_bytes, $response) { // @TODO: Stream the pack data instead of buffering it $pack_data = WP_Git_Pack_Processor::encode($pack_objects); - $response->write(WP_Git_Pack_Processor::encode_packet_line("\x01" . $pack_data)); + $response->write(WP_Git_Pack_Processor::encode_packet_line($pack_data, "\x01")); $response->write(WP_Git_Pack_Processor::encode_packet_line("0000")); return true; } diff --git a/packages/playground/data-liberation/src/git/functions.php b/packages/playground/data-liberation/src/git/functions.php new file mode 100644 index 0000000000..801008bc95 --- /dev/null +++ b/packages/playground/data-liberation/src/git/functions.php @@ -0,0 +1,41 @@ +read_object($tree_oid)) { + return false; + } + $oids = [$tree_oid]; + $trees = [$tree_oid]; + + while (!empty($trees)) { + $tree_hash = array_pop($trees); + if (!$repository->read_object($tree_hash)) { + _doing_it_wrong('wp_git_get_all_descendant_oids_in_tree', 'Failed to read object: ' . $tree_hash, '1.0.0'); + return false; + } + $tree = $repository->get_parsed_tree(); + foreach ($tree as $object) { + $oids[] = $object['sha1']; + if ($object['mode'] === WP_Git_Pack_Processor::FILE_MODE_DIRECTORY) { + $trees[] = $object['sha1']; + } + } + } + return $oids; +} + +function wp_git_get_parsed_commit(WP_Git_Repository $repository, $commit_oid) { + if(false === $repository->read_object($commit_oid)) { + _doing_it_wrong('wp_git_get_parsed_commit', 'Failed to read object: ' . $commit_oid, '1.0.0'); + return false; + } + if($repository->get_type() !== WP_Git_Pack_Processor::OBJECT_TYPE_COMMIT) { + _doing_it_wrong('wp_git_get_parsed_commit', 'Object was not a commit in find_objects_added_in: ' . $repository->get_type(), '1.0.0'); + return false; + } + return $repository->get_parsed_commit(); +} + +function wp_git_is_null_oid($oid) { + return $oid === null || $oid === WP_Git_Repository::NULL_OID; +} From d4afa36e11104f830970f4554b03b5f031a283d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 7 Jan 2025 15:07:37 +0100 Subject: [PATCH 70/71] Comment out the failure in find_objects_added_in --- .../data-liberation-static-files-editor/plugin.php | 1 + .../playground/data-liberation/src/git/WP_Git_Client.php | 2 +- .../data-liberation/src/git/WP_Git_Repository.php | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 5432eef8fe..3681888d2a 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -57,6 +57,7 @@ static private function get_fs() { if(!is_dir(WP_STATIC_PAGES_DIR)) { mkdir(WP_STATIC_PAGES_DIR, 0777, true); } + // $local_fs = new WP_Local_Filesystem(WP_STATIC_PAGES_DIR); $local_fs = new WP_Local_Filesystem(WP_STATIC_PAGES_DIR); $repo = new WP_Git_Repository($local_fs); $repo->add_remote('origin', GIT_REPO_URL); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Client.php b/packages/playground/data-liberation/src/git/WP_Git_Client.php index 6c85d63456..e9c6358d7d 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Client.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Client.php @@ -160,6 +160,7 @@ public function force_pull($branch_name=null, $path = '/') { $path = '/' . ltrim($path, '/'); $remote_refs = $this->fetchRefs('refs/heads/' . $branch_name); $remote_head = $remote_refs['refs/heads/' . $branch_name]; + // @TODO: Support "want" and "have" here $remote_index = $this->list_objects($remote_head); $remote_branch_ref = 'refs/heads/' . $branch_name; @@ -179,7 +180,6 @@ public function force_pull($branch_name=null, $path = '/') { $all_path_related_oids[] = $remote_head; $all_path_related_oids = array_flip($all_path_related_oids); - // @TODO: Support "want" and "have" here $new_oids = $remote_index->find_objects_added_in($remote_head, $local_ref ?: null, [ 'old_commit_repository' => $local_index, ]); diff --git a/packages/playground/data-liberation/src/git/WP_Git_Repository.php b/packages/playground/data-liberation/src/git/WP_Git_Repository.php index 28a7fa1707..f6da16bba4 100644 --- a/packages/playground/data-liberation/src/git/WP_Git_Repository.php +++ b/packages/playground/data-liberation/src/git/WP_Git_Repository.php @@ -463,7 +463,12 @@ public function find_objects_added_in($new_commit_hash, $old_commit_hash=WP_Git_ while($new_commit_hash !== $old_commit_hash && !wp_git_is_null_oid($new_commit_hash)) { if(false === $this->read_object($new_commit_hash)) { - throw new Exception('Failed to read new commit object: ' . $new_commit_hash); + // @TODO: This is a fatal failure since we're not able to + // establish a path between the old and new commit, + // but it's commented out for now to record a demo. + // Let's restore the failure here. + // throw new Exception('Failed to read new commit object: ' . $new_commit_hash); + break; } $new_objects_oids[$new_commit_hash] = true; $parsed_commit = $this->get_parsed_commit(); From bbea7aeb1bb64515f385368e17b287704bb7d8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 8 Jan 2025 18:11:40 +0100 Subject: [PATCH 71/71] Handle content deletions in the git endpoint --- .../git-repo/index.php | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/playground/data-liberation-static-files-editor/git-repo/index.php b/packages/playground/data-liberation-static-files-editor/git-repo/index.php index e272d89761..a886cea688 100644 --- a/packages/playground/data-liberation-static-files-editor/git-repo/index.php +++ b/packages/playground/data-liberation-static-files-editor/git-repo/index.php @@ -10,6 +10,8 @@ use WordPress\Filesystem\WP_Local_Filesystem; use WordPress\AsyncHttp\ResponseWriter\StreamingResponseWriter; use WordPress\AsyncHttp\ResponseWriter\BufferingResponseWriter; +use WordPress\Filesystem\WP_Filesystem_Visitor; +use WordPress\Filesystem\WP_Filesystem_Chroot; $git_repo_path = __DIR__ . '/git-test-server-data'; if(!is_dir($git_repo_path)) { @@ -40,10 +42,10 @@ $response = new BufferingResponseWriter(); $query_string = $_SERVER['REQUEST_URI'] ?? ""; -$path = substr($query_string, strlen($_SERVER['PHP_SELF'])); -if($path[0] === '?') { - $path = substr($path, 1); - $path = preg_replace('/&(amp;)?/', '?', $path, 1); +$request_path = substr($query_string, strlen($_SERVER['PHP_SELF'])); +if($request_path[0] === '?') { + $request_path = substr($request_path, 1); + $request_path = preg_replace('/&(amp;)?/', '?', $request_path, 1); } // Before handling the request, commit all the pages to the git repo @@ -52,19 +54,16 @@ 'post', 'local_file', ]; -switch($path) { +switch($request_path) { // ls refs – protocol discovery case '/info/refs?service=git-upload-pack': // ls refs or fetch – smart protocol case '/git-upload-pack': - - // @TODO: Don't brute-force delete everything, only the - // delta. // @TODO: Do streaming and amend the commit every few changes // @TODO: Use the streaming exporter instead of the ad-hoc loop below $diff = [ - // Delete all the pages 'updates' => [], + 'deletes' => [], ]; foreach($synced_post_types as $post_type) { $pages = get_posts([ @@ -72,7 +71,7 @@ 'post_status' => 'publish', ]); foreach($pages as $page) { - $file_path = $post_type . '/' . $page->post_name . '.html'; + $file_path = wp_canonicalize_path($post_type . '/' . $page->post_name . '.html'); $metadata = []; foreach(['post_date_gmt', 'post_title', 'menu_order'] as $key) { $metadata[$key] = get_post_field($key, $page->ID); @@ -85,6 +84,20 @@ // @TODO: Run the Markdown or block markup exporter $diff['updates'][$file_path] = $converter->get_result(); } + $visitor = new WP_Filesystem_Visitor( + new WP_Filesystem_Chroot($git_fs, $post_type) + ); + while($visitor->next()) { + $event = $visitor->get_event(); + if($event->is_entering()) { + foreach($event->files as $file_name) { + $path = wp_canonicalize_path($post_type . '/' . $event->dir . '/' . $file_name); + if(!isset($diff['updates'][$path])) { + $diff['deletes'][] = $path; + } + } + } + } } if(!$repository->commit($diff)) { throw new Exception('Failed to commit changes'); @@ -92,12 +105,12 @@ break; } -$server->handle_request($path, $request_bytes, $response); +$server->handle_request($request_path, $request_bytes, $response); // @TODO: Support the use-case below in the streaming importer // @TODO: When a page is moved, don't delete the old page and create a new one but // rather update the existing page. -if($path === '/git-receive-pack') { +if($request_path === '/git-receive-pack') { foreach($synced_post_types as $post_type) { $updated_ids = []; foreach($git_fs->ls($post_type) as $file_name) {