diff --git a/packages/cli/package.json b/packages/cli/package.json index 34248ce88..4a6c8317b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,8 +49,8 @@ "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", "@metamask/utils": "^11.9.0", + "@ocap/repo-tools": "workspace:^", "@types/node": "^22.13.1", - "acorn": "^8.15.0", "chokidar": "^4.0.1", "glob": "^11.0.0", "libp2p": "2.10.0", @@ -64,7 +64,6 @@ "@metamask/eslint-config": "^15.0.0", "@metamask/eslint-config-nodejs": "^15.0.0", "@metamask/eslint-config-typescript": "^15.0.0", - "@ocap/repo-tools": "workspace:^", "@ts-bridge/cli": "^0.6.3", "@ts-bridge/shims": "^0.1.1", "@types/serve-handler": "^6", diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index 348c3cb18..d07731bac 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -25,7 +25,7 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock('../vite/vat-bundler.ts', () => ({ +vi.mock('@ocap/repo-tools/vite-plugins', () => ({ bundleVat: mocks.bundleVat, })); diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index ebbc5d451..883c28134 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -1,11 +1,12 @@ +import type { VatBundle } from '@metamask/kernel-utils'; import type { Logger } from '@metamask/logger'; +import { bundleVat } from '@ocap/repo-tools/vite-plugins'; import { glob } from 'glob'; import { writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { isDirectory } from '../file.ts'; import { resolveBundlePath } from '../path.ts'; -import { bundleVat } from '../vite/vat-bundler.ts'; type BundleFileOptions = { logger: Logger; @@ -29,7 +30,8 @@ export async function bundleFile( const { logger, targetPath } = options; const sourceFullPath = resolve(sourcePath); const bundlePath = targetPath ?? resolveBundlePath(sourceFullPath); - const bundle = await bundleVat(sourceFullPath); + // Type annotation ensures repo-tools VatBundle stays compatible with kernel-utils VatBundle. + const bundle: VatBundle = await bundleVat(sourceFullPath); const bundleContent = JSON.stringify(bundle); await writeFile(bundlePath, bundleContent); logger.info(`Wrote ${bundlePath}: ${new Blob([bundleContent]).size} bytes`); diff --git a/packages/extension/scripts/start.sh b/packages/extension/scripts/start.sh index 644845168..7b7d41d9b 100755 --- a/packages/extension/scripts/start.sh +++ b/packages/extension/scripts/start.sh @@ -7,17 +7,11 @@ set -o pipefail yarn ocap relay & RELAY_PID=$! -yarn ocap start "../kernel-test/src/vats/default" & -OCAP_PID=$! - function cleanup() { # Kill the build if it's still running if kill -0 $RELAY_PID 2>/dev/null; then kill $RELAY_PID fi - if kill -0 $OCAP_PID 2>/dev/null; then - kill $OCAP_PID - fi } # Ensure we always close the ocap cli diff --git a/packages/extension/scripts/test-e2e-ci.sh b/packages/extension/scripts/test-e2e-ci.sh index 225d61f64..c59a55b87 100755 --- a/packages/extension/scripts/test-e2e-ci.sh +++ b/packages/extension/scripts/test-e2e-ci.sh @@ -1,22 +1,20 @@ #!/usr/bin/env bash - set -x set -e set -o pipefail -yarn ocap bundle "../kernel-test/src/vats/default" +yarn build -# Start the server in background and capture its PID -yarn ocap serve "../kernel-test/src/vats/default" & +# Bundle and serve test vats (e.g., empty-vat used by minimal-cluster.json) +yarn ocap bundle "../kernel-test/src/vats/default" +yarn ocap serve "../kernel-test/src/vats/default" & SERVER_PID=$! function cleanup() { - # Kill the server if it's still running if kill -0 $SERVER_PID 2>/dev/null; then kill $SERVER_PID fi } -# Ensure we always close the server trap cleanup EXIT yarn test:e2e "$@" diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 2893e3062..158c21e13 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -8,12 +8,13 @@ import { handleConsoleForwardMessage, } from '@metamask/kernel-browser-runtime'; import type { CapTPMessage } from '@metamask/kernel-browser-runtime'; -import defaultSubcluster from '@metamask/kernel-browser-runtime/default-cluster'; import { delay, isJsonRpcMessage, stringify } from '@metamask/kernel-utils'; import type { JsonRpcMessage } from '@metamask/kernel-utils'; import { Logger } from '@metamask/logger'; import { ChromeRuntimeDuplexStream } from '@metamask/streams/browser'; +import defaultSubcluster from './default-cluster.json'; + defineGlobals(); const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; diff --git a/packages/kernel-browser-runtime/src/default-cluster.json b/packages/extension/src/default-cluster.json similarity index 65% rename from packages/kernel-browser-runtime/src/default-cluster.json rename to packages/extension/src/default-cluster.json index 39acb7ded..6ce2aad3c 100644 --- a/packages/kernel-browser-runtime/src/default-cluster.json +++ b/packages/extension/src/default-cluster.json @@ -4,19 +4,19 @@ "services": ["ocapURLIssuerService", "ocapURLRedemptionService"], "vats": { "alice": { - "bundleSpec": "http://localhost:3000/sample-vat.bundle", + "bundleSpec": "sample-vat.bundle", "parameters": { "name": "Alice" } }, "bob": { - "bundleSpec": "http://localhost:3000/sample-vat.bundle", + "bundleSpec": "sample-vat.bundle", "parameters": { "name": "Bob" } }, "carol": { - "bundleSpec": "http://localhost:3000/sample-vat.bundle", + "bundleSpec": "sample-vat.bundle", "parameters": { "name": "Carol" } diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index 91ed7d421..4b78bdd06 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -6,6 +6,7 @@ import { getPackageDevAliases, } from '@ocap/repo-tools/build-utils/vite'; import { + bundleVats, deduplicateAssets, extensionDev, htmlTrustedPrelude, @@ -90,6 +91,17 @@ export default defineConfig(({ mode }) => { }, }, plugins: [ + bundleVats({ + vats: [ + { + source: path.resolve( + rootDir, + 'packages/kernel-test/src/vats/default/sample-vat.js', + ), + output: 'sample-vat.bundle', + }, + ], + }), react(), htmlTrustedPrelude(), jsTrustedPrelude({ trustedPreludes }), diff --git a/packages/kernel-browser-runtime/default-cluster.js b/packages/kernel-browser-runtime/default-cluster.js deleted file mode 100644 index fd60eaef0..000000000 --- a/packages/kernel-browser-runtime/default-cluster.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable import-x/unambiguous */ -// Re-exported for compatibility with Browserify. -module.exports = require('./dist/default-cluster.json'); diff --git a/packages/kernel-browser-runtime/package.json b/packages/kernel-browser-runtime/package.json index 69a6ed015..d6fd624cc 100644 --- a/packages/kernel-browser-runtime/package.json +++ b/packages/kernel-browser-runtime/package.json @@ -29,15 +29,13 @@ "default": "./dist/index.cjs" } }, - "./package.json": "./package.json", - "./default-cluster": "./dist/default-cluster.json" + "./package.json": "./package.json" }, "module": "./dist/index.mjs", "main": "./dist/index.cjs", "types": "./dist/index.d.cts", "files": [ - "dist/", - "default-cluster.js" + "dist/" ], "scripts": { "build": "yarn build:lib && yarn build:vite", diff --git a/packages/kernel-browser-runtime/tsconfig.build.json b/packages/kernel-browser-runtime/tsconfig.build.json index 27749e5ed..16ff6b008 100644 --- a/packages/kernel-browser-runtime/tsconfig.build.json +++ b/packages/kernel-browser-runtime/tsconfig.build.json @@ -16,7 +16,6 @@ { "path": "../ocap-kernel/tsconfig.build.json" }, { "path": "../streams/tsconfig.build.json" } ], - "files": ["./src/default-cluster.json"], "include": ["./src"], "exclude": [ "**/vite.config.ts", diff --git a/packages/kernel-ui/src/hooks/useKernelActions.test.ts b/packages/kernel-ui/src/hooks/useKernelActions.test.ts index 16a3f5db6..f38da8853 100644 --- a/packages/kernel-ui/src/hooks/useKernelActions.test.ts +++ b/packages/kernel-ui/src/hooks/useKernelActions.test.ts @@ -1,4 +1,3 @@ -import clusterConfig from '@metamask/kernel-browser-runtime/default-cluster' assert { type: 'json' }; import { waitFor, renderHook } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -6,6 +5,15 @@ vi.mock('../context/PanelContext.tsx', () => ({ usePanelContext: vi.fn(), })); +const clusterConfig = { + bootstrap: 'alice', + forceReset: true, + services: [], + vats: { + alice: { bundleSpec: 'sample-vat.bundle', parameters: { name: 'Alice' } }, + }, +}; + vi.mock('@metamask/kernel-utils', async (importOriginal) => ({ ...(await importOriginal()), stringify: JSON.stringify, diff --git a/packages/kernel-ui/src/hooks/useStatusPolling.test.ts b/packages/kernel-ui/src/hooks/useStatusPolling.test.ts index e645270e8..3bec3b5de 100644 --- a/packages/kernel-ui/src/hooks/useStatusPolling.test.ts +++ b/packages/kernel-ui/src/hooks/useStatusPolling.test.ts @@ -1,4 +1,3 @@ -import clusterConfig from '@metamask/kernel-browser-runtime/default-cluster' assert { type: 'json' }; import { waitFor, renderHook } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; @@ -8,6 +7,15 @@ vi.mock('../services/logger.ts', () => ({ }, })); +const clusterConfig = { + bootstrap: 'alice', + forceReset: true, + services: [], + vats: { + alice: { bundleSpec: 'sample-vat.bundle', parameters: { name: 'Alice' } }, + }, +}; + describe('useStatusPolling', () => { const mockSendMessage = vi.fn(); const mockInterval = 100; diff --git a/packages/omnium-gatherum/package.json b/packages/omnium-gatherum/package.json index e40947a64..8635e1d6b 100644 --- a/packages/omnium-gatherum/package.json +++ b/packages/omnium-gatherum/package.json @@ -16,11 +16,10 @@ "dist/" ], "scripts": { - "build": "yarn build:caplets && yarn build:vite && yarn test:build", + "build": "yarn build:vite && yarn test:build", "build:dev": "yarn build:vite --mode development", "build:watch": "yarn build:dev --watch", "build:browser": "OPEN_BROWSER=true yarn build:dev --watch", - "build:caplets": "ocap bundle src/caplets/echo && ocap bundle src/vats/controller-vat.ts", "build:vite": "vite build --configLoader runner --config vite.config.ts", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/omnium-gatherum", "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", @@ -39,7 +38,7 @@ "test:verbose": "yarn test --reporter verbose", "test:watch": "vitest --config vitest.config.ts", "test:e2e": "yarn playwright test", - "test:e2e:ci": "./scripts/test-e2e-ci.sh", + "test:e2e:ci": "yarn test:e2e", "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent" diff --git a/packages/omnium-gatherum/scripts/start.sh b/packages/omnium-gatherum/scripts/start.sh index 644845168..7b7d41d9b 100755 --- a/packages/omnium-gatherum/scripts/start.sh +++ b/packages/omnium-gatherum/scripts/start.sh @@ -7,17 +7,11 @@ set -o pipefail yarn ocap relay & RELAY_PID=$! -yarn ocap start "../kernel-test/src/vats/default" & -OCAP_PID=$! - function cleanup() { # Kill the build if it's still running if kill -0 $RELAY_PID 2>/dev/null; then kill $RELAY_PID fi - if kill -0 $OCAP_PID 2>/dev/null; then - kill $OCAP_PID - fi } # Ensure we always close the ocap cli diff --git a/packages/omnium-gatherum/scripts/test-e2e-ci.sh b/packages/omnium-gatherum/scripts/test-e2e-ci.sh deleted file mode 100755 index 225d61f64..000000000 --- a/packages/omnium-gatherum/scripts/test-e2e-ci.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -x -set -e -set -o pipefail - -yarn ocap bundle "../kernel-test/src/vats/default" - -# Start the server in background and capture its PID -yarn ocap serve "../kernel-test/src/vats/default" & -SERVER_PID=$! - -function cleanup() { - # Kill the server if it's still running - if kill -0 $SERVER_PID 2>/dev/null; then - kill $SERVER_PID - fi -} -# Ensure we always close the server -trap cleanup EXIT - -yarn test:e2e "$@" diff --git a/packages/omnium-gatherum/vite.config.ts b/packages/omnium-gatherum/vite.config.ts index 97c8fdd3f..61d32372e 100644 --- a/packages/omnium-gatherum/vite.config.ts +++ b/packages/omnium-gatherum/vite.config.ts @@ -6,6 +6,7 @@ import { getPackageDevAliases, } from '@ocap/repo-tools/build-utils/vite'; import { + bundleVats, deduplicateAssets, extensionDev, htmlTrustedPrelude, @@ -38,15 +39,9 @@ const staticCopyTargets: readonly (string | Target)[] = [ 'packages/omnium-gatherum/src/manifest.json', // Trusted prelude-related 'packages/kernel-shims/dist/endoify.js', - // Controller vat bundle (system vat for kernel services) - { - src: 'packages/omnium-gatherum/src/vats/controller-vat.bundle', - dest: './', - rename: 'controller-vat-bundle.json', - }, // Caplets (add new caplet entries here) { - src: 'packages/omnium-gatherum/src/caplets/echo/{manifest.json,*.bundle}', + src: 'packages/omnium-gatherum/src/caplets/echo/manifest.json', dest: 'echo/', }, ]; @@ -113,6 +108,18 @@ export default defineConfig(({ mode }) => { }, }, plugins: [ + bundleVats({ + vats: [ + { + source: path.resolve(dirname, 'src/caplets/echo/echo-caplet.js'), + output: 'echo/echo-caplet.bundle', + }, + { + source: path.resolve(dirname, 'src/vats/controller-vat.ts'), + output: 'controller-vat-bundle.json', + }, + ], + }), react(), htmlTrustedPrelude(), jsTrustedPrelude({ trustedPreludes }), diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index a8457870a..276b8c415 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -53,6 +53,7 @@ "@typescript-eslint/utils": "^8.29.0", "@vitest/eslint-plugin": "^1.6.5", "@vitest/runner": "4.0.16", + "acorn": "^8.15.0", "cheerio": "^1.0.0", "depcheck": "^1.4.7", "eslint": "^9.23.0", diff --git a/packages/cli/src/vite/vat-bundler.ts b/packages/repo-tools/src/vite-plugins/bundle-vats/bundle-vat.ts similarity index 84% rename from packages/cli/src/vite/vat-bundler.ts rename to packages/repo-tools/src/vite-plugins/bundle-vats/bundle-vat.ts index e35cc174a..c10bfc4b5 100644 --- a/packages/cli/src/vite/vat-bundler.ts +++ b/packages/repo-tools/src/vite-plugins/bundle-vats/bundle-vat.ts @@ -1,10 +1,22 @@ -import type { VatBundle } from '@metamask/kernel-utils'; import { build } from 'vite'; import type { Rollup } from 'vite'; import { exportMetadataPlugin } from './export-metadata-plugin.ts'; import { stripCommentsPlugin } from './strip-comments-plugin.ts'; +/** + * A bundle produced by the vat bundler. + * + * Contains the bundled code as an IIFE that assigns exports to `__vatExports__`, + * along with metadata about the bundle's exports and external dependencies. + */ +export type VatBundle = { + moduleFormat: 'iife'; + code: string; + exports: string[]; + external: string[]; +}; + /** * Bundle a vat source file using vite. * diff --git a/packages/cli/src/vite/export-metadata-plugin.ts b/packages/repo-tools/src/vite-plugins/bundle-vats/export-metadata-plugin.ts similarity index 100% rename from packages/cli/src/vite/export-metadata-plugin.ts rename to packages/repo-tools/src/vite-plugins/bundle-vats/export-metadata-plugin.ts diff --git a/packages/repo-tools/src/vite-plugins/bundle-vats/index.test.ts b/packages/repo-tools/src/vite-plugins/bundle-vats/index.test.ts new file mode 100644 index 000000000..b3cc193e0 --- /dev/null +++ b/packages/repo-tools/src/vite-plugins/bundle-vats/index.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import type { VatBundle } from './bundle-vat.ts'; +import { bundleVats } from './index.ts'; + +vi.mock('./bundle-vat.ts', () => ({ + bundleVat: vi.fn(), +})); + +const mockBundle: VatBundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: ['default'], + external: [], +}; + +describe('bundleVats', () => { + const vats = [ + { + source: '/absolute/path/to/echo-caplet.js', + output: 'echo/echo-caplet.bundle', + }, + { source: '/absolute/path/to/sample-vat.js', output: 'sample-vat.bundle' }, + ]; + + beforeEach(async () => { + const { bundleVat } = await import('./bundle-vat.ts'); + vi.mocked(bundleVat).mockResolvedValue(mockBundle); + }); + + it('registers vat sources for watch mode in buildStart', () => { + const plugin = bundleVats({ vats }); + const context = { addWatchFile: vi.fn() }; + (plugin.buildStart as (this: typeof context) => void).call(context); + + expect(context.addWatchFile).toHaveBeenCalledTimes(2); + expect(context.addWatchFile).toHaveBeenCalledWith(vats[0].source); + expect(context.addWatchFile).toHaveBeenCalledWith(vats[1].source); + }); + + it('bundles vats and emits assets in generateBundle', async () => { + const { bundleVat } = await import('./bundle-vat.ts'); + const plugin = bundleVats({ vats }); + const context = { emitFile: vi.fn() }; + await ( + plugin.generateBundle as (this: typeof context) => Promise + ).call(context); + + expect(bundleVat).toHaveBeenCalledTimes(2); + expect(bundleVat).toHaveBeenCalledWith(vats[0].source); + expect(bundleVat).toHaveBeenCalledWith(vats[1].source); + + expect(context.emitFile).toHaveBeenCalledTimes(2); + expect(context.emitFile).toHaveBeenCalledWith({ + type: 'asset', + fileName: 'echo/echo-caplet.bundle', + source: JSON.stringify(mockBundle), + }); + expect(context.emitFile).toHaveBeenCalledWith({ + type: 'asset', + fileName: 'sample-vat.bundle', + source: JSON.stringify(mockBundle), + }); + }); +}); diff --git a/packages/repo-tools/src/vite-plugins/bundle-vats/index.ts b/packages/repo-tools/src/vite-plugins/bundle-vats/index.ts new file mode 100644 index 000000000..3315aa002 --- /dev/null +++ b/packages/repo-tools/src/vite-plugins/bundle-vats/index.ts @@ -0,0 +1,55 @@ +import type { Plugin } from 'vite'; + +import { bundleVat } from './bundle-vat.ts'; + +export { bundleVat } from './bundle-vat.ts'; +export type { VatBundle } from './bundle-vat.ts'; + +type VatEntry = { + /** Absolute path to the vat source file */ + source: string; + /** Output path relative to build outDir (e.g., 'echo/echo-caplet.bundle') */ + output: string; +}; + +type BundleVatsOptions = { + vats: VatEntry[]; +}; + +/** + * Vite plugin that bundles vat source files as part of the build pipeline. + * + * Registers vat sources for watch mode and emits bundled assets during + * `generateBundle`. + * + * @param options - Plugin options specifying which vats to bundle. + * @returns A Vite plugin. + */ +export function bundleVats(options: BundleVatsOptions): Plugin { + return { + name: 'ocap-kernel:bundle-vats', + + buildStart() { + for (const vat of options.vats) { + this.addWatchFile(vat.source); + } + }, + + async generateBundle() { + const results = await Promise.all( + options.vats.map(async (vat) => { + const bundle = await bundleVat(vat.source); + return { fileName: vat.output, bundle }; + }), + ); + + for (const { fileName, bundle } of results) { + this.emitFile({ + type: 'asset', + fileName, + source: JSON.stringify(bundle), + }); + } + }, + }; +} diff --git a/packages/cli/src/vite/strip-comments-plugin.test.ts b/packages/repo-tools/src/vite-plugins/bundle-vats/strip-comments-plugin.test.ts similarity index 100% rename from packages/cli/src/vite/strip-comments-plugin.test.ts rename to packages/repo-tools/src/vite-plugins/bundle-vats/strip-comments-plugin.test.ts diff --git a/packages/cli/src/vite/strip-comments-plugin.ts b/packages/repo-tools/src/vite-plugins/bundle-vats/strip-comments-plugin.ts similarity index 100% rename from packages/cli/src/vite/strip-comments-plugin.ts rename to packages/repo-tools/src/vite-plugins/bundle-vats/strip-comments-plugin.ts diff --git a/packages/repo-tools/src/vite-plugins/index.test.ts b/packages/repo-tools/src/vite-plugins/index.test.ts index a2d14a3b4..cc609b542 100644 --- a/packages/repo-tools/src/vite-plugins/index.test.ts +++ b/packages/repo-tools/src/vite-plugins/index.test.ts @@ -4,7 +4,10 @@ import * as indexModule from './index.ts'; describe('index', () => { it('has the expected exports', () => { + // VatBundle is a type-only export, not visible at runtime expect(Object.keys(indexModule).sort()).toStrictEqual([ + 'bundleVat', + 'bundleVats', 'deduplicateAssets', 'extensionDev', 'htmlTrustedPrelude', diff --git a/packages/repo-tools/src/vite-plugins/index.ts b/packages/repo-tools/src/vite-plugins/index.ts index 0c409d2a0..e26af73f1 100644 --- a/packages/repo-tools/src/vite-plugins/index.ts +++ b/packages/repo-tools/src/vite-plugins/index.ts @@ -1,3 +1,5 @@ +export { bundleVat, bundleVats } from './bundle-vats/index.ts'; +export type { VatBundle } from './bundle-vats/index.ts'; export * from './deduplicate-assets.ts'; export * from './extension-dev.ts'; export * from './html-trusted-prelude.ts'; diff --git a/yarn.lock b/yarn.lock index 61c9f72d5..e3b364372 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3385,7 +3385,6 @@ __metadata: "@typescript-eslint/parser": "npm:^8.29.0" "@typescript-eslint/utils": "npm:^8.29.0" "@vitest/eslint-plugin": "npm:^1.6.5" - acorn: "npm:^8.15.0" chokidar: "npm:^4.0.1" depcheck: "npm:^1.4.7" eslint: "npm:^9.23.0" @@ -4041,6 +4040,7 @@ __metadata: "@typescript-eslint/utils": "npm:^8.29.0" "@vitest/eslint-plugin": "npm:^1.6.5" "@vitest/runner": "npm:4.0.16" + acorn: "npm:^8.15.0" cheerio: "npm:^1.0.0" depcheck: "npm:^1.4.7" eslint: "npm:^9.23.0"