From 23f7d5013822c2aede90b32b1cf6ed542471c7dc Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:49:54 -0600 Subject: [PATCH 01/19] add ocap cli with bundle and serve commands --- package.json | 3 + packages/cli/README.md | 7 + packages/cli/package.json | 88 +++++++++ packages/cli/src/app.ts | 101 ++++++++++ packages/cli/src/commands/bundle.test.ts | 93 +++++++++ packages/cli/src/commands/bundle.ts | 46 +++++ packages/cli/src/commands/serve.test.ts | 111 +++++++++++ packages/cli/src/commands/serve.ts | 127 ++++++++++++ packages/cli/src/config.ts | 12 ++ packages/cli/src/meta.test.ts | 19 ++ packages/cli/test/bundles.ts | 33 ++++ .../cli/test/bundles/sample-vat-esp.expected | 1 + packages/cli/test/bundles/sample-vat-esp.js | 14 ++ packages/cli/test/bundles/sample-vat.expected | 1 + packages/cli/test/bundles/sample-vat.js | 14 ++ packages/cli/test/file.ts | 22 +++ packages/cli/test/test.bundle | 1 + packages/cli/tsconfig.build.json | 13 ++ packages/cli/tsconfig.json | 16 ++ packages/cli/tsconfig.lint.json | 10 + packages/cli/vitest.config.ts | 24 +++ packages/extension/src/manifest.json | 4 + yarn.lock | 180 +++++++++++++++++- 23 files changed, 930 insertions(+), 10 deletions(-) create mode 100644 packages/cli/README.md create mode 100644 packages/cli/package.json create mode 100755 packages/cli/src/app.ts create mode 100644 packages/cli/src/commands/bundle.test.ts create mode 100644 packages/cli/src/commands/bundle.ts create mode 100644 packages/cli/src/commands/serve.test.ts create mode 100644 packages/cli/src/commands/serve.ts create mode 100644 packages/cli/src/config.ts create mode 100644 packages/cli/src/meta.test.ts create mode 100644 packages/cli/test/bundles.ts create mode 100644 packages/cli/test/bundles/sample-vat-esp.expected create mode 100644 packages/cli/test/bundles/sample-vat-esp.js create mode 100644 packages/cli/test/bundles/sample-vat.expected create mode 100644 packages/cli/test/bundles/sample-vat.js create mode 100644 packages/cli/test/file.ts create mode 100644 packages/cli/test/test.bundle create mode 100644 packages/cli/tsconfig.build.json create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsconfig.lint.json create mode 100644 packages/cli/vitest.config.ts diff --git a/package.json b/package.json index be450f6e5..159d5265e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "workspaces": [ "packages/*" ], + "bin": { + "ocap": "packages/cli/dist/app.mjs" + }, "scripts": { "build": "yarn run build:source", "build:clean": "yarn clean && yarn build", diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000..86239786c --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,7 @@ +# `cli` + +Ocap Kernel cli. + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..69f2cf13d --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,88 @@ +{ + "name": "@ocap/cli", + "version": "0.0.0", + "private": true, + "description": "Ocap Kernel cli", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/ocap-kernel.git" + }, + "type": "module", + "bin": { + "ocap": "./dist/app.mjs" + }, + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --clean", + "build:docs": "typedoc", + "build:app": "vite build", + "bundle": "tsx src/bundle.ts", + "changelog:validate": "../../scripts/validate-changelog.sh utils", + "clean": "rimraf --glob ./dist './*.tsbuildinfo'", + "lint": "yarn lint:ts && yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", + "lint:dependencies": "depcheck", + "lint:eslint": "eslint . --cache", + "lint:fix": "yarn lint:ts && yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies", + "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore", + "lint:ts": "tsc --project tsconfig.lint.json", + "publish:preview": "yarn npm publish --tag preview", + "test": "vitest run --config vitest.config.ts", + "test:clean": "yarn test --no-cache --coverage.clean", + "test:dev": "yarn test --coverage false", + "test:verbose": "yarn test --reporter verbose", + "test:watch": "vitest --config vitest.config.ts" + }, + "dependencies": { + "@endo/bundle-source": "^3.5.0", + "@endo/init": "^1.1.6", + "@metamask/snaps-utils": "^8.3.0", + "@ocap/shims": "workspace:^", + "@ocap/utils": "workspace:^", + "@types/node": "^18.18.14", + "glob": "^11.0.0", + "serve-handler": "^6.1.6", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.16.4", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eslint-config": "^14.0.0", + "@metamask/eslint-config-nodejs": "^14.0.0", + "@metamask/eslint-config-typescript": "^14.0.0", + "@ts-bridge/cli": "^0.5.1", + "@ts-bridge/shims": "^0.1.1", + "@types/serve-handler": "^6", + "@types/yargs": "^17.0.33", + "@typescript-eslint/eslint-plugin": "^8.8.1", + "@typescript-eslint/parser": "^8.8.1", + "@typescript-eslint/utils": "^8.8.1", + "@vitest/eslint-plugin": "^1.1.7", + "depcheck": "^1.4.7", + "eslint": "^9.12.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import-x": "^4.3.1", + "eslint-plugin-jsdoc": "^50.3.1", + "eslint-plugin-n": "^17.11.1", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-promise": "^7.1.0", + "jsdom": "^24.1.1", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "ses": "^1.9.0", + "typedoc": "^0.26.8", + "typescript": "~5.5.4", + "typescript-eslint": "^8.8.1", + "vite": "^5.3.5", + "vite-plugin-node": "^4.0.0", + "vitest": "^2.1.2" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "exports": { + "./package.json": "./package.json" + } +} diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts new file mode 100755 index 000000000..feb7237a0 --- /dev/null +++ b/packages/cli/src/app.ts @@ -0,0 +1,101 @@ +import { lstat } from 'fs/promises'; +import { resolve } from 'path'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { createBundle, createBundleDir } from './commands/bundle.js'; +import { getServer } from './commands/serve.js'; +import { defaultConfig } from './config.js'; +import type { Config } from './config.js'; + +const demandOneOfOption = + (...options: string[]) => + (argv: { [prop: string]: unknown }) => { + const count = options.filter((option) => argv[option]).length; + const lastOption = options.pop(); + + if (count === 0) { + throw new Error( + `Exactly one of the arguments ${options.join(', ')} and ${lastOption} is required`, + ); + } else if (count > 1) { + throw new Error( + `Arguments ${options.join(', ')} and ${lastOption} are mutually exclusive`, + ); + } + + return true; + }; + +await yargs(hideBin(process.argv)) + .usage('$0 [options]') + .command( + 'bundle [target] [-f files..]', + 'Bundle user code to be used in a vat', + (_yargs) => + _yargs + .option('target', { + type: 'string', + file: true, + dir: true, + describe: 'The file or directory of files to bundle', + }) + .option('files', { + alias: 'f', + type: 'array', + string: true, + file: true, + describe: 'The file(s) to bundle', + }) + .check(demandOneOfOption('target', 'files')), + async (args) => { + const resolvePath = (path: string): string => + // eslint-disable-next-line n/no-process-env + resolve(process.env.INIT_CWD ?? '.', path); + if (args.files) { + await Promise.all( + args.files.map(async (file) => await createBundle(resolvePath(file))), + ); + return; + } + if (args.target) { + if ((await lstat(args.target)).isDirectory()) { + await createBundleDir(resolvePath(args.target)); + } else { + await createBundle(resolvePath(args.target)); + } + } + }, + ) + .command( + 'serve ', + 'Serve bundled user code by filename', + (_yargs) => + _yargs + .option('port', { + alias: 'p', + type: 'number', + default: defaultConfig.server.port, + }) + .option('dir', { + alias: 'd', + type: 'string', + dir: true, + required: true, + describe: 'A directory of files to bundle', + }), + async (args) => { + console.info(`serving ${args.dir} on localhost:${args.port}`); + const config: Config = { + server: { + port: args.port, + }, + dir: args.dir, + }; + const server = getServer(config); + await server.listen(); + }, + ) + .help('h') + .alias('h', 'help') + .parse(); diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts new file mode 100644 index 000000000..551833e57 --- /dev/null +++ b/packages/cli/src/commands/bundle.test.ts @@ -0,0 +1,93 @@ +import { createHash } from 'crypto'; +import { readFile, rm } from 'fs/promises'; +import { glob } from 'glob'; +import { join, basename } from 'path'; +import { + describe, + it, + expect, + vi, + beforeAll, + beforeEach, + afterEach, +} from 'vitest'; + +import { createBundle, createBundleDir } from './bundle.js'; +import { getTestBundles } from '../../test/bundles.js'; +import { exists } from '../../test/file.js'; + +describe('bundle', async () => { + beforeEach(() => { + vi.resetModules(); + }); + + const { testBundleRoot, testBundleNames, testBundleSpecs } = + await getTestBundles(); + + const deleteTestBundles = async (): Promise => + await Promise.all( + testBundleSpecs.map(async ({ bundle }) => rm(bundle, { force: true })), + ).then(() => undefined); + + beforeAll(deleteTestBundles); + afterEach(deleteTestBundles); + + describe('createBundle', () => { + it.for(testBundleSpecs)( + 'bundles a single file: $name', + async ({ script, expected, bundle }, ctx) => { + if (!(await exists(expected))) { + // this test case has no expected bundle + // reporting handled in `describe('[meta]'` above + ctx.skip(); + } + ctx.expect(await exists(bundle)).toBe(false); + + await createBundle(script); + + ctx.expect(await exists(bundle)).toBe(true); + + const expectedBundleContent = await readFile(expected); + const bundleContent = await readFile(bundle); + const expectedBundleHash = createHash('sha256') + .update(expectedBundleContent) + .digest(); + const bundleHash = createHash('sha256').update(bundleContent).digest(); + + ctx + .expect(bundleHash.toString('hex')) + .toStrictEqual(expectedBundleHash.toString('hex')); + }, + ); + + it('throws an error if supplied path is a directory', async () => { + await expect(createBundle(testBundleRoot)).rejects.toThrow( + /cannot be called on directory/u, + ); + }); + }); + + describe('createBundleDir', () => { + it('bundles a directory', async () => { + expect( + (await glob(join(testBundleRoot, '*.bundle'))).map((filepath) => + basename(filepath, '.bundle'), + ), + ).toStrictEqual([]); + + await createBundleDir(testBundleRoot); + + expect( + (await glob(join(testBundleRoot, '*.bundle'))).map((filepath) => + basename(filepath, '.bundle'), + ), + ).toStrictEqual(testBundleNames); + }); + + it('throws an error if supplied path is not a directory', async () => { + await expect( + createBundleDir(testBundleSpecs[0]?.script as string), + ).rejects.toThrow(/must be called on directory/u); + }); + }); +}); diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts new file mode 100644 index 000000000..9a29a3f87 --- /dev/null +++ b/packages/cli/src/commands/bundle.ts @@ -0,0 +1,46 @@ +import '@endo/init'; +import bundleSource from '@endo/bundle-source'; +import { glob } from 'glob'; +import { lstat, writeFile } from 'node:fs/promises'; +import { resolve, parse, format, join } from 'node:path'; + +/** + * Create a bundle given path to an entry point. + * + * @param sourcePath - Path to the source file that is the root of the bundle. + * @returns A promise that resolves when the bundle has been written. + */ +export async function createBundle(sourcePath: string): Promise { + if ((await lstat(sourcePath)).isDirectory()) { + throw new Error('createBundle cannot be called on directory', { + cause: { sourcePath }, + }); + } + + const sourceFullPath = resolve(sourcePath); + console.log(sourceFullPath); + const { dir, name } = parse(sourceFullPath); + const bundlePath = format({ dir, name, ext: '.bundle' }); + const bundle = await bundleSource(sourceFullPath); + const bundleString = JSON.stringify(bundle); + await writeFile(bundlePath, bundleString); + console.log(`wrote ${bundlePath}: ${bundleString.length} bytes`); +} + +/** + * Create a bundle given path to an entry point. + * + * @param sourceDir - Path to a directory of source files to bundle. + * @returns A promise that resolves when the bundles have been written. + */ +export async function createBundleDir(sourceDir: string): Promise { + if (!(await lstat(sourceDir)).isDirectory()) { + throw new Error('createBundleDir must be called on directory', { + cause: { sourceDir }, + }); + } + console.log('bundling dir', sourceDir); + for (const source of await glob(join(sourceDir, '**/*.js'))) { + await createBundle(source); + } +} diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts new file mode 100644 index 000000000..4f9702a80 --- /dev/null +++ b/packages/cli/src/commands/serve.test.ts @@ -0,0 +1,111 @@ +import '@ocap/shims/endoify'; +import { makeCounter } from '@ocap/utils'; +import { createHash } from 'crypto'; +import { readFile } from 'fs/promises'; +import { join, resolve } from 'path'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { getServer } from './serve.js'; +import { getTestBundles } from '../../test/bundles.js'; + +describe('serve', async () => { + beforeEach(() => { + vi.resetModules(); + }); + + const { testBundleRoot, testBundleSpecs } = await getTestBundles(); + + describe('getServer', () => { + it('returns an object with a listen property', () => { + const server = getServer({ + server: { + port: 3000, + }, + dir: testBundleRoot, + }); + + expect(server).toHaveProperty('listen'); + }); + + it(`throws if 'dir' is not specified`, () => { + expect(() => getServer({ server: { port: 3000 } })).toThrow(/dir/u); + }); + }); + + describe('server', () => { + const getServerPort = makeCounter(3000); + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const makeServer = (root: string = testBundleRoot) => { + const port = getServerPort(); + const { listen } = getServer({ + server: { + port, + }, + dir: root, + }); + const requestBundle = async ( + path: string, + ): Promise<{ content: string }> => + // TODO: mock + await fetch(`http://localhost:${port}/${path}`).then(async (resp) => { + if (resp.ok) { + return resp.json(); + } + throw new Error(resp.statusText, { cause: resp.status }); + }); + return { + listen, + requestBundle, + }; + }; + + it.sequential('serves bundles', async () => { + const bundleName = 'test.bundle'; + const bundleRoot = join(testBundleRoot, '..'); + const bundlePath = join(bundleRoot, bundleName); + const { listen, requestBundle } = makeServer(bundleRoot); + + const { close } = await listen(); + + try { + const expectedBundleHash = await readFile(bundlePath) + .then((content) => JSON.parse(content.toString())) + .then(({ content }) => createHash('sha256').update(content).digest()); + + const receivedBundleHash = await requestBundle(bundleName).then( + ({ content }) => + createHash('sha256').update(Buffer.from(content)).digest(), + ); + + expect(receivedBundleHash.toString('hex')).toStrictEqual( + expectedBundleHash.toString('hex'), + ); + } finally { + await close(); + } + }); + + it('only serves *.bundle files', async () => { + const { listen, requestBundle } = makeServer(); + + const script = testBundleSpecs[0]?.script as string; + + const { close } = await listen(); + await expect(requestBundle(script)) + .rejects.toMatchObject({ cause: 404 }) + .finally(async () => await close()); + }); + + it('only serves files in the target dir', async () => { + const { listen, requestBundle } = makeServer(); + + const extraneousBundle = resolve(testBundleRoot, '../test.bundle'); + + const { close } = await listen(); + await expect(requestBundle(extraneousBundle)) + .rejects.toMatchObject({ cause: 404 }) + .finally(async () => await close()); + }); + }); +}); diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts new file mode 100644 index 000000000..5d0281a5b --- /dev/null +++ b/packages/cli/src/commands/serve.ts @@ -0,0 +1,127 @@ +import { logError } from '@metamask/snaps-utils/node'; +import type { IncomingMessage, Server, ServerResponse } from 'http'; +import { createServer } from 'http'; +import type { AddressInfo } from 'net'; +import { resolve as resolvePath } from 'path'; +import serveMiddleware from 'serve-handler'; + +import type { Config } from '../config.js'; + +/** + * Get a static server for development purposes. + * + * Note: We're intentionally not using `webpack-dev-server` here because it + * adds a lot of extra stuff to the output that we don't need, and it's + * difficult to customize. + * + * @param config - The config object. + * @returns An object with a `listen` method that returns a promise that + * resolves when the server is listening. + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function getServer(config: Config) { + if (typeof config.dir === 'undefined' || !config.dir) { + throw new Error(`Config option 'dir' must be specified.`); + } + const bundleRoot = resolvePath(config.dir); + // Only serve .bundle files + const isAllowedPath = (path?: string): boolean => + typeof path === 'string' && path.endsWith('.bundle'); + + /** + * Get the response for a request. This is extracted into a function so that + * we can easily catch errors and send a 500 response. + * + * @param request - The request. + * @param response - The response. + * @returns A promise that resolves when the response is sent. + */ + async function getResponse( + request: IncomingMessage, + response: ServerResponse, + ): Promise { + const pathname = + request.url && + request.headers.host && + new URL(request.url, `http://${request.headers.host}`).pathname; + const path = pathname?.slice(1); + + if (!isAllowedPath(path)) { + response.statusCode = 404; + response.end(); + return; + } + + await serveMiddleware(request, response, { + public: bundleRoot, + directoryListing: false, + headers: [ + { + source: '**/*', + headers: [ + { + key: 'Cache-Control', + value: 'no-cache', + }, + { + key: 'Access-Control-Allow-Origin', + value: '*', + }, + ], + }, + ], + }); + } + + const server = createServer((request, response) => { + getResponse(request, response).catch( + /* istanbul ignore next */ + (error) => { + logError(error); + response.statusCode = 500; + response.end(); + }, + ); + }); + + /** + * Start the server on the port specified in the config. + * + * @param port - The port to listen on. + * @returns A promise that resolves when the server is listening. The promise + * resolves to an object with the port and the server instance. Note that if + * the `config.server.port` is `0`, the OS will choose a random port for us, + * so we need to get the port from the server after it starts. + */ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const listen = async (port = config.server.port) => { + return new Promise<{ + port: number; + server: Server; + close: () => Promise; + }>((resolve, reject) => { + try { + server.listen(port, () => { + const close = async (): Promise => { + await new Promise((_resolve, _reject) => { + server.close((closeError) => { + if (closeError) { + return _reject(closeError); + } + + return _resolve(); + }); + }); + }; + + const address = server.address() as AddressInfo; + resolve({ port: address.port, server, close }); + }); + } catch (listenError) { + reject(listenError as Error); + } + }); + }; + + return { listen, getResponse }; +} diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts new file mode 100644 index 000000000..fc24834c5 --- /dev/null +++ b/packages/cli/src/config.ts @@ -0,0 +1,12 @@ +export type Config = { + server: { + port: number; + }; + dir?: string; +}; + +export const defaultConfig: Config = { + server: { + port: 3000, + }, +}; diff --git a/packages/cli/src/meta.test.ts b/packages/cli/src/meta.test.ts new file mode 100644 index 000000000..43ddde62b --- /dev/null +++ b/packages/cli/src/meta.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; + +import { getTestBundles } from '../test/bundles.js'; +import { exists } from '../test/file.js'; + +describe('[meta]', async () => { + const { testBundleNames, testBundleSpecs } = await getTestBundles(); + + it('at least one test bundle is configured', () => { + expect(testBundleNames.length).toBeGreaterThan(0); + }); + + it.each(testBundleSpecs)( + 'test bundles have expectations: $script', + async ({ expected }) => { + expect(await exists(expected)).toBe(true); + }, + ); +}); diff --git a/packages/cli/test/bundles.ts b/packages/cli/test/bundles.ts new file mode 100644 index 000000000..8693a371a --- /dev/null +++ b/packages/cli/test/bundles.ts @@ -0,0 +1,33 @@ +import { glob } from 'glob'; +import { resolve, join, basename } from 'path'; + +const testBundleRoot = resolve( + import.meta.url.split(':')[1] as string, + '../bundles', +); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const getTestBundleNames = async (bundleRoot: string) => + (await glob(join(bundleRoot, '*.js'))).map((filepath) => + basename(filepath, '.js'), + ); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const getTestBundleSpecs = (bundleRoot: string, bundleNames: string[]) => + bundleNames.map((bundleName) => ({ + name: bundleName, + script: join(bundleRoot, `${bundleName}.js`), + expected: join(bundleRoot, `${bundleName}.expected`), + bundle: join(bundleRoot, `${bundleName}.bundle`), + })); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const getTestBundles = async () => { + const testBundleNames = await getTestBundleNames(testBundleRoot); + const testBundleSpecs = getTestBundleSpecs(testBundleRoot, testBundleNames); + return { + testBundleRoot, + testBundleNames, + testBundleSpecs, + }; +}; diff --git a/packages/cli/test/bundles/sample-vat-esp.expected b/packages/cli/test/bundles/sample-vat-esp.expected new file mode 100644 index 000000000..0df012325 --- /dev/null +++ b/packages/cli/test/bundles/sample-vat-esp.expected @@ -0,0 +1 @@ +{"moduleFormat":"endoZipBase64","endoZipBase64":"UEsDBAoAAAAAAAAAAACzfS0JXQIAAF0CAAAUAAAAY29tcGFydG1lbnQtbWFwLmpzb257CiAgInRhZ3MiOiBbXSwKICAiZW50cnkiOiB7CiAgICAiY29tcGFydG1lbnQiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAibW9kdWxlIjogIi4vdGVzdC9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzIgogIH0sCiAgImNvbXBhcnRtZW50cyI6IHsKICAgICJAb2NhcC9jbGktdjAuMC4wIjogewogICAgICAibmFtZSI6ICJAb2NhcC9jbGkiLAogICAgICAibGFiZWwiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAgICJsb2NhdGlvbiI6ICJAb2NhcC9jbGktdjAuMC4wIiwKICAgICAgIm1vZHVsZXMiOiB7CiAgICAgICAgIi4vdGVzdC9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzIjogewogICAgICAgICAgImxvY2F0aW9uIjogInRlc3QvYnVuZGxlcy9zYW1wbGUtdmF0LWVzcC5qcyIsCiAgICAgICAgICAicGFyc2VyIjogInByZS1tanMtanNvbiIsCiAgICAgICAgICAic2hhNTEyIjogIjU4MDc5MGZjMGZjZmNmOTAyNzBkMTliOTIyY2FkYzRiMGIzMWRlYmYzYzU2ZjQ4ODJmYmViZDkxYWVjMDk5MTMyN2Q4ZGY3ZGZlMDFiNWU3OWIzNWE2MzE1ODRjYTFiMDE2ZjRkMTY2MzI4OWU4OTE0OGNlYmQ5ZjMxN2YxNTg1IgogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KfVBLAwQKAAAAAAAAAAAAs+4sO00DAABNAwAALwAAAEBvY2FwL2NsaS12MC4wLjAvdGVzdC9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzeyJpbXBvcnRzIjpbXSwiZXhwb3J0cyI6WyJzdGFydCJdLCJyZWV4cG9ydHMiOltdLCJfX3N5bmNNb2R1bGVQcm9ncmFtX18iOiIoeyAgIGltcG9ydHM6ICRozY9faW1wb3J0cywgICBsaXZlVmFyOiAkaM2PX2xpdmUsICAgb25jZVZhcjogJGjNj19vbmNlLCAgIGltcG9ydE1ldGE6ICRozY9fX19fbWV0YSwgfSkgPT4gKGZ1bmN0aW9uICgpIHsgJ3VzZSBzdHJpY3QnOyAgICRozY9faW1wb3J0cyhbXSk7T2JqZWN0LmRlZmluZVByb3BlcnR5KHN0YXJ0LCAnbmFtZScsIHt2YWx1ZTogXCJzdGFydFwifSk7JGjNj19vbmNlLnN0YXJ0KHN0YXJ0KTsgICAvKipcbiAqIFN0YXJ0IGZ1bmN0aW9uIGZvciBnZW5lcmljIHRlc3QgdmF0LlxuICpcbiAqIEBwYXJhbSB7dW5rbm93bn0gcGFyYW1ldGVycyAtIEluaXRpYWxpemF0aW9uIHBhcmFtZXRlcnMgZnJvbSB0aGUgdmF0J3MgY29uZmlnIG9iamVjdC5cbiAqIEByZXR1cm5zIHt1bmtub3dufSBUaGUgcm9vdCBvYmplY3QgZm9yIHRoZSBuZXcgdmF0LlxuICovXG5mdW5jdGlvbiAgICAgICAgc3RhcnQocGFyYW1ldGVycyl7XG5jb25zdCBuYW1lPXBhcmFtZXRlcnM/Lm5hbWU/Pydhbm9ueW1vdXMnO1xuY29uc29sZS5sb2coIGBlbXBlY2UgdmF0IHJvb3Qgb2JqZWN0byBcIiR7bmFtZX1cImApO1xucmV0dXJue1xubmFtZSxcbnN0dWZmOiBgc2UgaW5pY2nDsyBwb3IgJHtKU09OLnN0cmluZ2lmeShwYXJhbWV0ZXJzKX1gfTtcblxuIH1cbn0pKClcbiIsIl9fbGl2ZUV4cG9ydE1hcF9fIjp7fSwiX19yZWV4cG9ydE1hcF9fIjp7fSwiX19maXhlZEV4cG9ydE1hcF9fIjp7InN0YXJ0IjpbInN0YXJ0Il19LCJfX25lZWRzSW1wb3J0TWV0YV9fIjpmYWxzZX1QSwECHgMKAAAAAAAAAAAAs30tCV0CAABdAgAAFAAAAAAAAAAAAAAApIEAAAAAY29tcGFydG1lbnQtbWFwLmpzb25QSwECHgMKAAAAAAAAAAAAs+4sO00DAABNAwAALwAAAAAAAAAAAAAApIGPAgAAQG9jYXAvY2xpLXYwLjAuMC90ZXN0L2J1bmRsZXMvc2FtcGxlLXZhdC1lc3AuanNQSwUGAAAAAAIAAgCfAAAAKQYAAAAA","endoZipBase64Sha512":"7c137760832a5b520d187759baf7318f5e77470dd4b86febfc7e2ad0685a5475d6e8938e1a66dfb6802aeac9649de8ea1f67b2bbe87cce2273438649ebcf9536"} \ No newline at end of file diff --git a/packages/cli/test/bundles/sample-vat-esp.js b/packages/cli/test/bundles/sample-vat-esp.js new file mode 100644 index 000000000..787c9025e --- /dev/null +++ b/packages/cli/test/bundles/sample-vat-esp.js @@ -0,0 +1,14 @@ +/** + * Start function for generic test vat. + * + * @param {unknown} parameters - Initialization parameters from the vat's config object. + * @returns {unknown} The root object for the new vat. + */ +export function start(parameters) { + const name = parameters?.name ?? 'anonymous'; + console.log(`empece vat root objecto "${name}"`); + return { + name, + stuff: `se inició por ${JSON.stringify(parameters)}`, + }; +} diff --git a/packages/cli/test/bundles/sample-vat.expected b/packages/cli/test/bundles/sample-vat.expected new file mode 100644 index 000000000..a0961fd80 --- /dev/null +++ b/packages/cli/test/bundles/sample-vat.expected @@ -0,0 +1 @@ +{"moduleFormat":"endoZipBase64","endoZipBase64":"UEsDBAoAAAAAAAAAAADuwyPVUQIAAFECAAAUAAAAY29tcGFydG1lbnQtbWFwLmpzb257CiAgInRhZ3MiOiBbXSwKICAiZW50cnkiOiB7CiAgICAiY29tcGFydG1lbnQiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAibW9kdWxlIjogIi4vdGVzdC9idW5kbGVzL3NhbXBsZS12YXQuanMiCiAgfSwKICAiY29tcGFydG1lbnRzIjogewogICAgIkBvY2FwL2NsaS12MC4wLjAiOiB7CiAgICAgICJuYW1lIjogIkBvY2FwL2NsaSIsCiAgICAgICJsYWJlbCI6ICJAb2NhcC9jbGktdjAuMC4wIiwKICAgICAgImxvY2F0aW9uIjogIkBvY2FwL2NsaS12MC4wLjAiLAogICAgICAibW9kdWxlcyI6IHsKICAgICAgICAiLi90ZXN0L2J1bmRsZXMvc2FtcGxlLXZhdC5qcyI6IHsKICAgICAgICAgICJsb2NhdGlvbiI6ICJ0ZXN0L2J1bmRsZXMvc2FtcGxlLXZhdC5qcyIsCiAgICAgICAgICAicGFyc2VyIjogInByZS1tanMtanNvbiIsCiAgICAgICAgICAic2hhNTEyIjogIjA0ZmI2Y2EwMTk0Mzc2ZjIwMzE5ZGEwMjQwYWM5OTg0NDM2MTQ5MTU0MDgzZjQ4ZDIzMWFjNTI5ZTEyZmNlZGRjYmU0OTZlNzViYTg2ZGFjYTcyY2Q5OTVlZDNkNzQ5NWY5NmNmODM1OWY1ZGRiYjk2MmM4YmY3ZTMyYjg3YWYyIgogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KfVBLAwQKAAAAAAAAAAAAPl/YgU0DAABNAwAAKwAAAEBvY2FwL2NsaS12MC4wLjAvdGVzdC9idW5kbGVzL3NhbXBsZS12YXQuanN7ImltcG9ydHMiOltdLCJleHBvcnRzIjpbInN0YXJ0Il0sInJlZXhwb3J0cyI6W10sIl9fc3luY01vZHVsZVByb2dyYW1fXyI6Iih7ICAgaW1wb3J0czogJGjNj19pbXBvcnRzLCAgIGxpdmVWYXI6ICRozY9fbGl2ZSwgICBvbmNlVmFyOiAkaM2PX29uY2UsICAgaW1wb3J0TWV0YTogJGjNj19fX19tZXRhLCB9KSA9PiAoZnVuY3Rpb24gKCkgeyAndXNlIHN0cmljdCc7ICAgJGjNj19pbXBvcnRzKFtdKTtPYmplY3QuZGVmaW5lUHJvcGVydHkoc3RhcnQsICduYW1lJywge3ZhbHVlOiBcInN0YXJ0XCJ9KTskaM2PX29uY2Uuc3RhcnQoc3RhcnQpOyAgIC8qKlxuICogU3RhcnQgZnVuY3Rpb24gZm9yIGdlbmVyaWMgdGVzdCB2YXQuXG4gKlxuICogQHBhcmFtIHt1bmtub3dufSBwYXJhbWV0ZXJzIC0gSW5pdGlhbGl6YXRpb24gcGFyYW1ldGVycyBmcm9tIHRoZSB2YXQncyBjb25maWcgb2JqZWN0LlxuICogQHJldHVybnMge3Vua25vd259IFRoZSByb290IG9iamVjdCBmb3IgdGhlIG5ldyB2YXQuXG4gKi9cbmZ1bmN0aW9uICAgICAgICBzdGFydChwYXJhbWV0ZXJzKXtcbmNvbnN0IG5hbWU9cGFyYW1ldGVycz8ubmFtZT8/J2Fub255bW91cyc7XG5jb25zb2xlLmxvZyggYHN0YXJ0IHZhdCByb290IG9iamVjdCBcIiR7bmFtZX1cImApO1xucmV0dXJue1xubmFtZSxcbnN0dWZmOiBgaW5pdGlhbGl6ZWQgd2l0aCAke0pTT04uc3RyaW5naWZ5KHBhcmFtZXRlcnMpfWB9O1xuXG4gfVxufSkoKVxuIiwiX19saXZlRXhwb3J0TWFwX18iOnt9LCJfX3JlZXhwb3J0TWFwX18iOnt9LCJfX2ZpeGVkRXhwb3J0TWFwX18iOnsic3RhcnQiOlsic3RhcnQiXX0sIl9fbmVlZHNJbXBvcnRNZXRhX18iOmZhbHNlfVBLAQIeAwoAAAAAAAAAAADuwyPVUQIAAFECAAAUAAAAAAAAAAAAAACkgQAAAABjb21wYXJ0bWVudC1tYXAuanNvblBLAQIeAwoAAAAAAAAAAAA+X9iBTQMAAE0DAAArAAAAAAAAAAAAAACkgYMCAABAb2NhcC9jbGktdjAuMC4wL3Rlc3QvYnVuZGxlcy9zYW1wbGUtdmF0LmpzUEsFBgAAAAACAAIAmwAAABkGAAAAAA==","endoZipBase64Sha512":"ef6309a11e421df02258b108152d039b9e748750ff3eba173c6738efe0ca81bb0f482e1e3d7695f06eb614aef9a8ea9836856f66ecbe2f3ae6b883da73406d51"} \ No newline at end of file diff --git a/packages/cli/test/bundles/sample-vat.js b/packages/cli/test/bundles/sample-vat.js new file mode 100644 index 000000000..118704e89 --- /dev/null +++ b/packages/cli/test/bundles/sample-vat.js @@ -0,0 +1,14 @@ +/** + * Start function for generic test vat. + * + * @param {unknown} parameters - Initialization parameters from the vat's config object. + * @returns {unknown} The root object for the new vat. + */ +export function start(parameters) { + const name = parameters?.name ?? 'anonymous'; + console.log(`start vat root object "${name}"`); + return { + name, + stuff: `initialized with ${JSON.stringify(parameters)}`, + }; +} diff --git a/packages/cli/test/file.ts b/packages/cli/test/file.ts new file mode 100644 index 000000000..1bafdaeb1 --- /dev/null +++ b/packages/cli/test/file.ts @@ -0,0 +1,22 @@ +import { open } from 'fs/promises'; + +/** + * Asynchronously check if a file exists. + * + * @param path - The path to check + * @returns A promise that resolves to true iff a file exists at the given path + */ +export async function exists(path: string): Promise { + return open(path, 'wx') + .then(async (file) => { + // if the file opens, it didn't exist yet + await file.close(); + return false; + }) + .catch((error) => { + if (error.code === 'EEXIST') { + return true; + } + throw error; + }); +} diff --git a/packages/cli/test/test.bundle b/packages/cli/test/test.bundle new file mode 100644 index 000000000..6996a21ba --- /dev/null +++ b/packages/cli/test/test.bundle @@ -0,0 +1 @@ +{"content":"This is merely a test bundle!"} \ No newline at end of file diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json new file mode 100644 index 000000000..f40fb5434 --- /dev/null +++ b/packages/cli/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["DOM", "ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "types": ["ses", "node"] + }, + "references": [], + "files": [], + "include": ["./src"] +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000..9c3390863 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["DOM", "ES2022"], + "types": ["ses", "vitest", "node"] + }, + "references": [{ "path": "../test-utils" }, { "path": "../utils" }], + "include": [ + "../../vitest.config.ts", + "./src", + "./test", + "./vite.config.ts", + "./vitest.config.ts" + ] +} diff --git a/packages/cli/tsconfig.lint.json b/packages/cli/tsconfig.lint.json new file mode 100644 index 000000000..8d3e49eb8 --- /dev/null +++ b/packages/cli/tsconfig.lint.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["./src"], + "exclude": [] +} diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts new file mode 100644 index 000000000..9eec777c0 --- /dev/null +++ b/packages/cli/vitest.config.ts @@ -0,0 +1,24 @@ +import { defineProject, mergeConfig } from 'vitest/config'; + +import defaultConfig from '../../vitest.config.js'; + +const config = mergeConfig( + defaultConfig, + defineProject({ + build: { + ssr: true, + rollupOptions: { + output: { + esModule: true, + }, + }, + }, + test: { + name: 'cli', + }, + }), +); + +config.test.coverage.thresholds = true; + +export default config; diff --git a/packages/extension/src/manifest.json b/packages/extension/src/manifest.json index 7c7e6b5a2..ee0a78eb3 100644 --- a/packages/extension/src/manifest.json +++ b/packages/extension/src/manifest.json @@ -17,5 +17,9 @@ "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-ancestors 'none';", "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'none'; default-src 'none'; connect-src *;" + }, + "vats": { + "bundleRoot": "./vats", + "bundles": ["sample-vat.bundle", "sample-vat-esp.bundle"] } } diff --git a/yarn.lock b/yarn.lock index 5db07b591..a248fec5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1599,6 +1599,56 @@ __metadata: languageName: node linkType: hard +"@ocap/cli@workspace:packages/cli": + version: 0.0.0-use.local + resolution: "@ocap/cli@workspace:packages/cli" + dependencies: + "@arethetypeswrong/cli": "npm:^0.16.4" + "@endo/bundle-source": "npm:^3.5.0" + "@endo/init": "npm:^1.1.6" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/eslint-config": "npm:^14.0.0" + "@metamask/eslint-config-nodejs": "npm:^14.0.0" + "@metamask/eslint-config-typescript": "npm:^14.0.0" + "@metamask/snaps-utils": "npm:^8.3.0" + "@ocap/shims": "workspace:^" + "@ocap/utils": "workspace:^" + "@ts-bridge/cli": "npm:^0.5.1" + "@ts-bridge/shims": "npm:^0.1.1" + "@types/node": "npm:^18.18.14" + "@types/serve-handler": "npm:^6" + "@types/yargs": "npm:^17.0.33" + "@typescript-eslint/eslint-plugin": "npm:^8.8.1" + "@typescript-eslint/parser": "npm:^8.8.1" + "@typescript-eslint/utils": "npm:^8.8.1" + "@vitest/eslint-plugin": "npm:^1.1.7" + depcheck: "npm:^1.4.7" + eslint: "npm:^9.12.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-import-resolver-typescript: "npm:^3.6.3" + eslint-plugin-import-x: "npm:^4.3.1" + eslint-plugin-jsdoc: "npm:^50.3.1" + eslint-plugin-n: "npm:^17.11.1" + eslint-plugin-prettier: "npm:^5.2.1" + eslint-plugin-promise: "npm:^7.1.0" + glob: "npm:^11.0.0" + jsdom: "npm:^24.1.1" + prettier: "npm:^3.3.3" + rimraf: "npm:^6.0.1" + serve-handler: "npm:^6.1.6" + ses: "npm:^1.9.0" + typedoc: "npm:^0.26.8" + typescript: "npm:~5.5.4" + typescript-eslint: "npm:^8.8.1" + vite: "npm:^5.3.5" + vite-plugin-node: "npm:^4.0.0" + vitest: "npm:^2.1.2" + yargs: "npm:^17.7.2" + bin: + ocap: ./dist/app.mjs + languageName: unknown + linkType: soft + "@ocap/errors@workspace:^, @ocap/errors@workspace:packages/errors": version: 0.0.0-use.local resolution: "@ocap/errors@workspace:packages/errors" @@ -1785,6 +1835,8 @@ __metadata: vite-tsconfig-paths: "npm:^4.3.2" vitest: "npm:^2.1.2" webextension-polyfill: "npm:^0.12.0" + bin: + ocap: packages/cli/dist/app.mjs languageName: unknown linkType: soft @@ -2052,7 +2104,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.0.0": +"@rollup/pluginutils@npm:^4.0.0, @rollup/pluginutils@npm:^4.1.1": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" dependencies: @@ -2527,6 +2579,15 @@ __metadata: languageName: node linkType: hard +"@types/serve-handler@npm:^6": + version: 6.1.4 + resolution: "@types/serve-handler@npm:6.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/c92ae204605659b37202af97cfcc7690be43b9290692c1d6c3c93805b399044fd67573af4eb2e7b1fd975451db6d0d5c6cd2f09b20997209fa3341f345f661e4 + languageName: node + linkType: hard + "@types/statuses@npm:^2.0.4": version: 2.0.5 resolution: "@types/statuses@npm:2.0.5" @@ -2555,6 +2616,22 @@ __metadata: languageName: node linkType: hard +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: 10/a794eb750e8ebc6273a51b12a0002de41343ffe46befef460bdbb57262d187fdf608bc6615b7b11c462c63c3ceb70abe2564c8dd8ee0f7628f38a314f74a9b9b + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.33": + version: 17.0.33 + resolution: "@types/yargs@npm:17.0.33" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10/16f6681bf4d99fb671bf56029141ed01db2862e3db9df7fc92d8bea494359ac96a1b4b1c35a836d1e95e665fb18ad753ab2015fc0db663454e8fd4e5d5e2ef91 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:8.9.0, @typescript-eslint/eslint-plugin@npm:^8.8.1": version: 8.9.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.9.0" @@ -3380,6 +3457,13 @@ __metadata: languageName: node linkType: hard +"bytes@npm:3.0.0": + version: 3.0.0 + resolution: "bytes@npm:3.0.0" + checksum: 10/a2b386dd8188849a5325f58eef69c3b73c51801c08ffc6963eddc9be244089ba32d19347caf6d145c86f315ae1b1fc7061a32b0c1aa6379e6a719090287ed101 + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -3823,6 +3907,13 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:0.5.2": + version: 0.5.2 + resolution: "content-disposition@npm:0.5.2" + checksum: 10/97c5e7c8c72a0524c5d92866ecd3da28d4596925321aa3252d7ce3122d354b099d73cc1981fec8f24848d74314089928f626af8f9d7b51c3bc625d47f11e1d90 + languageName: node + linkType: hard + "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -6492,6 +6583,22 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:~1.33.0": + version: 1.33.0 + resolution: "mime-db@npm:1.33.0" + checksum: 10/b3b89cff1d3569d02280f8d5b3b6e3c6df4dd340647b48228b2624293a73da0a7c784712aec8eac0aaccd353ac04b4d50309ab9f6a87d7ee79b4dca0ebb70ed8 + languageName: node + linkType: hard + +"mime-types@npm:2.1.18": + version: 2.1.18 + resolution: "mime-types@npm:2.1.18" + dependencies: + mime-db: "npm:~1.33.0" + checksum: 10/65d69085abda6732d4372e9874018fbe491894ff25f7329b8c8815fe40989599488567e08dcee39f1bb54729c4311fb660195ab551603d1cb97d7f2bf33ca8a2 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -6522,6 +6629,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:3.1.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 + languageName: node + linkType: hard + "minimatch@npm:^10.0.0": version: 10.0.1 resolution: "minimatch@npm:10.0.1" @@ -6531,15 +6647,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 - languageName: node - linkType: hard - "minimatch@npm:^7.4.6": version: 7.4.6 resolution: "minimatch@npm:7.4.6" @@ -7183,6 +7290,13 @@ __metadata: languageName: node linkType: hard +"path-is-inside@npm:1.0.2": + version: 1.0.2 + resolution: "path-is-inside@npm:1.0.2" + checksum: 10/0b5b6c92d3018b82afb1f74fe6de6338c4c654de4a96123cb343f2b747d5606590ac0c890f956ed38220a4ab59baddfd7b713d78a62d240b20b14ab801fa02cb + languageName: node + linkType: hard + "path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -7224,6 +7338,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:3.3.0": + version: 3.3.0 + resolution: "path-to-regexp@npm:3.3.0" + checksum: 10/8d256383af8db66233ee9027cfcbf8f5a68155efbb4f55e784279d3ab206dcaee554ddb72ff0dae97dd2882af9f7fa802634bb7cffa2e796927977e31b829259 + languageName: node + linkType: hard + "path-to-regexp@npm:^6.3.0": version: 6.3.0 resolution: "path-to-regexp@npm:6.3.0" @@ -7505,6 +7626,13 @@ __metadata: languageName: node linkType: hard +"range-parser@npm:1.2.0": + version: 1.2.0 + resolution: "range-parser@npm:1.2.0" + checksum: 10/1a561fef1feae1cee3a3cb2440d4d9d3ab96cf2eebaf0d3a5cf06aecf91bc869f273ca0e2f05f73a4c530e751e4af0ed2723b7b86aeef296e3eaea7cfd0a5bfb + languageName: node + linkType: hard + "react-is@npm:^17.0.1": version: 17.0.2 resolution: "react-is@npm:17.0.2" @@ -7857,6 +7985,21 @@ __metadata: languageName: node linkType: hard +"serve-handler@npm:^6.1.6": + version: 6.1.6 + resolution: "serve-handler@npm:6.1.6" + dependencies: + bytes: "npm:3.0.0" + content-disposition: "npm:0.5.2" + mime-types: "npm:2.1.18" + minimatch: "npm:3.1.2" + path-is-inside: "npm:1.0.2" + path-to-regexp: "npm:3.3.0" + range-parser: "npm:1.2.0" + checksum: 10/7e7d93eb7e69fcd9f9c5afc2ef2b46cb0072b4af13cbabef9bca725afb350ddae6857d8c8be2c256f7ce1f7677c20347801399c11caa5805c0090339f894e8f2 + languageName: node + linkType: hard + "ses@npm:^1.1.0, ses@npm:^1.10.0, ses@npm:^1.9.0, ses@npm:^1.9.1": version: 1.10.0 resolution: "ses@npm:1.10.0" @@ -8960,6 +9103,23 @@ __metadata: languageName: node linkType: hard +"vite-plugin-node@npm:^4.0.0": + version: 4.0.0 + resolution: "vite-plugin-node@npm:4.0.0" + dependencies: + "@rollup/pluginutils": "npm:^4.1.1" + chalk: "npm:^4.1.2" + debug: "npm:^4.3.2" + peerDependencies: + "@swc/core": ^1.7.26 + vite: ^5.0.0 + peerDependenciesMeta: + "@swc/core": + optional: true + checksum: 10/56705f40d3577b6f639d4524713b63664a5a11f6dbb76456d0b260fff28d996d8ba9a97665248aa71a7e0373aa20161b925c13e4824676ad5ed148b682dbabf9 + languageName: node + linkType: hard + "vite-plugin-static-copy@npm:^1.0.6": version: 1.0.6 resolution: "vite-plugin-static-copy@npm:1.0.6" From 745dffb3314e99347fa5a471d7942c0462c95041 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 2 Dec 2024 07:45:11 -0600 Subject: [PATCH 02/19] test uses node-fetch to target server on localhost --- packages/cli/package.json | 3 +- packages/cli/src/commands/serve.test.ts | 57 ++++++++++++------- packages/cli/src/commands/serve.ts | 2 +- packages/cli/vitest.config.ts | 8 +++ yarn.lock | 73 ++++++++++++++++++------- 5 files changed, 103 insertions(+), 40 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 69f2cf13d..c2d9bde21 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "@metamask/eslint-config": "^14.0.0", "@metamask/eslint-config-nodejs": "^14.0.0", "@metamask/eslint-config-typescript": "^14.0.0", + "@metamask/utils": "^9.3.0", "@ts-bridge/cli": "^0.5.1", "@ts-bridge/shims": "^0.1.1", "@types/serve-handler": "^6", @@ -69,6 +70,7 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^7.1.0", "jsdom": "^24.1.1", + "node-fetch": "^3.3.2", "prettier": "^3.3.3", "rimraf": "^6.0.1", "ses": "^1.9.0", @@ -76,7 +78,6 @@ "typescript": "~5.5.4", "typescript-eslint": "^8.8.1", "vite": "^5.3.5", - "vite-plugin-node": "^4.0.0", "vitest": "^2.1.2" }, "engines": { diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts index 4f9702a80..2e2cca566 100644 --- a/packages/cli/src/commands/serve.test.ts +++ b/packages/cli/src/commands/serve.test.ts @@ -1,7 +1,9 @@ import '@ocap/shims/endoify'; -import { makeCounter } from '@ocap/utils'; +import { isObject, hasProperty } from '@metamask/utils'; +import { makeCounter, stringify } from '@ocap/utils'; import { createHash } from 'crypto'; import { readFile } from 'fs/promises'; +import nodeFetch from 'node-fetch'; import { join, resolve } from 'path'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -44,16 +46,15 @@ describe('serve', async () => { }, dir: root, }); - const requestBundle = async ( - path: string, - ): Promise<{ content: string }> => - // TODO: mock - await fetch(`http://localhost:${port}/${path}`).then(async (resp) => { - if (resp.ok) { - return resp.json(); - } - throw new Error(resp.statusText, { cause: resp.status }); - }); + const requestBundle = async (path: string): Promise => + await nodeFetch(`http://localhost:${port}/${path}`).then( + async (resp) => { + if (resp.ok) { + return resp.json(); + } + throw new Error(resp.statusText, { cause: resp.status }); + }, + ); return { listen, requestBundle, @@ -74,8 +75,18 @@ describe('serve', async () => { .then(({ content }) => createHash('sha256').update(content).digest()); const receivedBundleHash = await requestBundle(bundleName).then( - ({ content }) => - createHash('sha256').update(Buffer.from(content)).digest(), + (json) => { + if ( + !isObject(json) || + !hasProperty(json, 'content') || + typeof json.content !== 'string' + ) { + return `Received unexpected response from server: ${stringify(json)}`; + } + return createHash('sha256') + .update(Buffer.from(json.content)) + .digest(); + }, ); expect(receivedBundleHash.toString('hex')).toStrictEqual( @@ -92,9 +103,13 @@ describe('serve', async () => { const script = testBundleSpecs[0]?.script as string; const { close } = await listen(); - await expect(requestBundle(script)) - .rejects.toMatchObject({ cause: 404 }) - .finally(async () => await close()); + try { + await expect(requestBundle(script)).rejects.toMatchObject({ + cause: 404, + }); + } finally { + await close(); + } }); it('only serves files in the target dir', async () => { @@ -103,9 +118,13 @@ describe('serve', async () => { const extraneousBundle = resolve(testBundleRoot, '../test.bundle'); const { close } = await listen(); - await expect(requestBundle(extraneousBundle)) - .rejects.toMatchObject({ cause: 404 }) - .finally(async () => await close()); + try { + await expect(requestBundle(extraneousBundle)).rejects.toMatchObject({ + cause: 404, + }); + } finally { + await close(); + } }); }); }); diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 5d0281a5b..80f84ec2a 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -123,5 +123,5 @@ export function getServer(config: Config) { }); }; - return { listen, getResponse }; + return { listen }; } diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index 9eec777c0..6236edfda 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -1,3 +1,4 @@ +import path from 'path'; import { defineProject, mergeConfig } from 'vitest/config'; import defaultConfig from '../../vitest.config.js'; @@ -15,6 +16,13 @@ const config = mergeConfig( }, test: { name: 'cli', + alias: [ + { + find: '@ocap/shims/endoify', + replacement: path.resolve('../shims/src/endoify.js'), + customResolver: (id) => ({ external: true, id }), + }, + ], }, }), ); diff --git a/yarn.lock b/yarn.lock index a248fec5e..08138f28f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1611,6 +1611,7 @@ __metadata: "@metamask/eslint-config-nodejs": "npm:^14.0.0" "@metamask/eslint-config-typescript": "npm:^14.0.0" "@metamask/snaps-utils": "npm:^8.3.0" + "@metamask/utils": "npm:^9.3.0" "@ocap/shims": "workspace:^" "@ocap/utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" @@ -1633,6 +1634,7 @@ __metadata: eslint-plugin-promise: "npm:^7.1.0" glob: "npm:^11.0.0" jsdom: "npm:^24.1.1" + node-fetch: "npm:^3.3.2" prettier: "npm:^3.3.3" rimraf: "npm:^6.0.1" serve-handler: "npm:^6.1.6" @@ -1641,7 +1643,6 @@ __metadata: typescript: "npm:~5.5.4" typescript-eslint: "npm:^8.8.1" vite: "npm:^5.3.5" - vite-plugin-node: "npm:^4.0.0" vitest: "npm:^2.1.2" yargs: "npm:^17.7.2" bin: @@ -2104,7 +2105,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.0.0, @rollup/pluginutils@npm:^4.1.1": +"@rollup/pluginutils@npm:^4.0.0": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" dependencies: @@ -4015,6 +4016,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c + languageName: node + linkType: hard + "data-urls@npm:^5.0.0": version: 5.0.0 resolution: "data-urls@npm:5.0.0" @@ -4997,6 +5005,16 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b + languageName: node + linkType: hard + "fflate@npm:^0.8.2": version: 0.8.2 resolution: "fflate@npm:0.8.2" @@ -5105,6 +5123,15 @@ __metadata: languageName: node linkType: hard +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f + languageName: node + linkType: hard + "fs-extra@npm:^11.1.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -6859,6 +6886,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + "node-emoji@npm:^2.1.3": version: 2.1.3 resolution: "node-emoji@npm:2.1.3" @@ -6885,6 +6919,17 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d + languageName: node + linkType: hard + "node-gyp-build@npm:^4.2.2": version: 4.8.3 resolution: "node-gyp-build@npm:4.8.3" @@ -9103,23 +9148,6 @@ __metadata: languageName: node linkType: hard -"vite-plugin-node@npm:^4.0.0": - version: 4.0.0 - resolution: "vite-plugin-node@npm:4.0.0" - dependencies: - "@rollup/pluginutils": "npm:^4.1.1" - chalk: "npm:^4.1.2" - debug: "npm:^4.3.2" - peerDependencies: - "@swc/core": ^1.7.26 - vite: ^5.0.0 - peerDependenciesMeta: - "@swc/core": - optional: true - checksum: 10/56705f40d3577b6f639d4524713b63664a5a11f6dbb76456d0b260fff28d996d8ba9a97665248aa71a7e0373aa20161b925c13e4824676ad5ed148b682dbabf9 - languageName: node - linkType: hard - "vite-plugin-static-copy@npm:^1.0.6": version: 1.0.6 resolution: "vite-plugin-static-copy@npm:1.0.6" @@ -9311,6 +9339,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 + languageName: node + linkType: hard + "webextension-polyfill@npm:^0.12.0": version: 0.12.0 resolution: "webextension-polyfill@npm:0.12.0" From 26b08e4dc25ff5910354aa0e1e0c3a58e26fda79 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:13:27 -0600 Subject: [PATCH 03/19] remove unused manifest entry from extension --- packages/extension/src/manifest.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/extension/src/manifest.json b/packages/extension/src/manifest.json index ee0a78eb3..7c7e6b5a2 100644 --- a/packages/extension/src/manifest.json +++ b/packages/extension/src/manifest.json @@ -17,9 +17,5 @@ "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-ancestors 'none';", "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'none'; default-src 'none'; connect-src *;" - }, - "vats": { - "bundleRoot": "./vats", - "bundles": ["sample-vat.bundle", "sample-vat-esp.bundle"] } } From afc1c755b5643678d49126cd77abdbe0d1099b74 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:46:51 -0500 Subject: [PATCH 04/19] convert then().catch() to try {} catch {} Co-authored-by: Dimitris Marlagkoutsos --- packages/cli/test/file.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/cli/test/file.ts b/packages/cli/test/file.ts index 1bafdaeb1..5d734f204 100644 --- a/packages/cli/test/file.ts +++ b/packages/cli/test/file.ts @@ -7,16 +7,15 @@ import { open } from 'fs/promises'; * @returns A promise that resolves to true iff a file exists at the given path */ export async function exists(path: string): Promise { - return open(path, 'wx') - .then(async (file) => { - // if the file opens, it didn't exist yet - await file.close(); - return false; - }) - .catch((error) => { - if (error.code === 'EEXIST') { - return true; - } - throw error; - }); + try { + const file = await open(path, 'wx'); + // if the file opens, it didn't exist yet + await file.close(); + return false; + } catch (error) { + if (error.code === 'EEXIST') { + return true; + } + throw error; + } } From 734e85504bbd607192842c0cf0276c3378e8afb5 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:00:35 -0600 Subject: [PATCH 05/19] typecheck error --- packages/cli/test/file.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/file.ts b/packages/cli/test/file.ts index 5d734f204..958ef6e45 100644 --- a/packages/cli/test/file.ts +++ b/packages/cli/test/file.ts @@ -1,3 +1,4 @@ +import { isObject } from '@metamask/utils'; import { open } from 'fs/promises'; /** @@ -7,13 +8,13 @@ import { open } from 'fs/promises'; * @returns A promise that resolves to true iff a file exists at the given path */ export async function exists(path: string): Promise { - try { + try { const file = await open(path, 'wx'); // if the file opens, it didn't exist yet await file.close(); return false; } catch (error) { - if (error.code === 'EEXIST') { + if (isObject(error) && error.code === 'EEXIST') { return true; } throw error; From 4c80ba7e6012f7ca6656db120ad1c7ee05acbeec Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:54:18 -0600 Subject: [PATCH 06/19] read hash from bundle file during tests --- packages/cli/src/commands/serve.test.ts | 45 +++++++++++++++++-------- packages/cli/test/test.bundle | 2 +- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts index 2e2cca566..a361eb96a 100644 --- a/packages/cli/src/commands/serve.test.ts +++ b/packages/cli/src/commands/serve.test.ts @@ -1,4 +1,5 @@ import '@ocap/shims/endoify'; +import type { BundleSourceResult } from '@endo/bundle-source'; import { isObject, hasProperty } from '@metamask/utils'; import { makeCounter, stringify } from '@ocap/utils'; import { createHash } from 'crypto'; @@ -10,6 +11,17 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getServer } from './serve.js'; import { getTestBundles } from '../../test/bundles.js'; +const isBundleSourceResult = ( + value: unknown, +): value is BundleSourceResult<'endoZipBase64'> => + isObject(value) && + hasProperty(value, 'moduleFormat') && + value.moduleFormat === 'endoZipBase64' && + hasProperty(value, 'endoZipBase64') && + typeof value.endoZipBase64 === 'string' && + hasProperty(value, 'endoZipBase64Sha512') && + typeof value.endoZipBase64Sha512 === 'string'; + describe('serve', async () => { beforeEach(() => { vi.resetModules(); @@ -70,28 +82,33 @@ describe('serve', async () => { const { close } = await listen(); try { - const expectedBundleHash = await readFile(bundlePath) - .then((content) => JSON.parse(content.toString())) - .then(({ content }) => createHash('sha256').update(content).digest()); + const expectedBundleHash = await readFile(bundlePath).then( + (content) => { + const json = JSON.parse(content.toString()); + if (!isBundleSourceResult(json)) { + throw new Error( + [ + `Could not read expected bundle ${bundlePath}`, + `Parsed JSON: ${stringify(json)}`, + ].join('\n'), + ); + } + return json.endoZipBase64Sha512; + }, + ); const receivedBundleHash = await requestBundle(bundleName).then( (json) => { - if ( - !isObject(json) || - !hasProperty(json, 'content') || - typeof json.content !== 'string' - ) { + if (!isBundleSourceResult(json)) { return `Received unexpected response from server: ${stringify(json)}`; } - return createHash('sha256') - .update(Buffer.from(json.content)) - .digest(); + return createHash('sha512') + .update(Buffer.from(json.endoZipBase64)) + .digest('hex'); }, ); - expect(receivedBundleHash.toString('hex')).toStrictEqual( - expectedBundleHash.toString('hex'), - ); + expect(receivedBundleHash).toStrictEqual(expectedBundleHash); } finally { await close(); } diff --git a/packages/cli/test/test.bundle b/packages/cli/test/test.bundle index 6996a21ba..cb26f3a08 100644 --- a/packages/cli/test/test.bundle +++ b/packages/cli/test/test.bundle @@ -1 +1 @@ -{"content":"This is merely a test bundle!"} \ No newline at end of file +{"moduleFormat":"endoZipBase64","endoZipBase64":"This is merely a test bundle!","endoZipBase64Sha512":"d0ef05812fb8a7cce57dcb8d6f6fbe5677f175e2ef966df1d81dec9868a8f56b0e89b691fdb3d43a3902962adcf37d1eee6902a113476cca85f8d76957bdf154"} \ No newline at end of file From e73d14677747def3598b4599f7874b4a34d0a596 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:52:02 -0600 Subject: [PATCH 07/19] clean up --- packages/cli/package.json | 2 - packages/cli/src/app.ts | 71 +++++------------------- packages/cli/src/commands/bundle.test.ts | 16 +++--- packages/cli/src/commands/bundle.ts | 41 ++++++++++++-- packages/cli/src/meta.test.ts | 19 ------- packages/cli/test/file.ts | 2 +- packages/cli/tsconfig.build.json | 9 ++- packages/cli/tsconfig.json | 6 +- packages/cli/tsconfig.lint.json | 2 +- packages/cli/typedoc.json | 7 +++ 10 files changed, 76 insertions(+), 99 deletions(-) delete mode 100644 packages/cli/src/meta.test.ts create mode 100644 packages/cli/typedoc.json diff --git a/packages/cli/package.json b/packages/cli/package.json index c2d9bde21..c22494e34 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,8 +17,6 @@ "scripts": { "build": "ts-bridge --project tsconfig.build.json --clean", "build:docs": "typedoc", - "build:app": "vite build", - "bundle": "tsx src/bundle.ts", "changelog:validate": "../../scripts/validate-changelog.sh utils", "clean": "rimraf --glob ./dist './*.tsbuildinfo'", "lint": "yarn lint:ts && yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts index feb7237a0..24d94bd25 100755 --- a/packages/cli/src/app.ts +++ b/packages/cli/src/app.ts @@ -1,70 +1,26 @@ -import { lstat } from 'fs/promises'; -import { resolve } from 'path'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { createBundle, createBundleDir } from './commands/bundle.js'; +import { createBundle } from './commands/bundle.js'; import { getServer } from './commands/serve.js'; import { defaultConfig } from './config.js'; -import type { Config } from './config.js'; - -const demandOneOfOption = - (...options: string[]) => - (argv: { [prop: string]: unknown }) => { - const count = options.filter((option) => argv[option]).length; - const lastOption = options.pop(); - - if (count === 0) { - throw new Error( - `Exactly one of the arguments ${options.join(', ')} and ${lastOption} is required`, - ); - } else if (count > 1) { - throw new Error( - `Arguments ${options.join(', ')} and ${lastOption} are mutually exclusive`, - ); - } - - return true; - }; await yargs(hideBin(process.argv)) .usage('$0 [options]') .command( - 'bundle [target] [-f files..]', + 'bundle [targets..]', 'Bundle user code to be used in a vat', (_yargs) => - _yargs - .option('target', { - type: 'string', - file: true, - dir: true, - describe: 'The file or directory of files to bundle', - }) - .option('files', { - alias: 'f', - type: 'array', - string: true, - file: true, - describe: 'The file(s) to bundle', - }) - .check(demandOneOfOption('target', 'files')), + _yargs.option('targets', { + type: 'string', + file: true, + dir: true, + array: true, + demandOption: true, + describe: 'The files or directories of files to bundle', + }), async (args) => { - const resolvePath = (path: string): string => - // eslint-disable-next-line n/no-process-env - resolve(process.env.INIT_CWD ?? '.', path); - if (args.files) { - await Promise.all( - args.files.map(async (file) => await createBundle(resolvePath(file))), - ); - return; - } - if (args.target) { - if ((await lstat(args.target)).isDirectory()) { - await createBundleDir(resolvePath(args.target)); - } else { - await createBundle(resolvePath(args.target)); - } - } + await Promise.all(args.targets.map(createBundle)); }, ) .command( @@ -86,13 +42,12 @@ await yargs(hideBin(process.argv)) }), async (args) => { console.info(`serving ${args.dir} on localhost:${args.port}`); - const config: Config = { + const server = getServer({ server: { port: args.port, }, dir: args.dir, - }; - const server = getServer(config); + }); await server.listen(); }, ) diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index 551833e57..11020a20f 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -12,9 +12,9 @@ import { afterEach, } from 'vitest'; -import { createBundle, createBundleDir } from './bundle.js'; +import { createBundleFile, createBundleDir } from './bundle.js'; import { getTestBundles } from '../../test/bundles.js'; -import { exists } from '../../test/file.js'; +import { fileExists } from '../../test/file.js'; describe('bundle', async () => { beforeEach(() => { @@ -32,20 +32,20 @@ describe('bundle', async () => { beforeAll(deleteTestBundles); afterEach(deleteTestBundles); - describe('createBundle', () => { + describe('createBundleFile', () => { it.for(testBundleSpecs)( 'bundles a single file: $name', async ({ script, expected, bundle }, ctx) => { - if (!(await exists(expected))) { + if (!(await fileExists(expected))) { // this test case has no expected bundle // reporting handled in `describe('[meta]'` above ctx.skip(); } - ctx.expect(await exists(bundle)).toBe(false); + ctx.expect(await fileExists(bundle)).toBe(false); - await createBundle(script); + await createBundleFile(script); - ctx.expect(await exists(bundle)).toBe(true); + ctx.expect(await fileExists(bundle)).toBe(true); const expectedBundleContent = await readFile(expected); const bundleContent = await readFile(bundle); @@ -61,7 +61,7 @@ describe('bundle', async () => { ); it('throws an error if supplied path is a directory', async () => { - await expect(createBundle(testBundleRoot)).rejects.toThrow( + await expect(createBundleFile(testBundleRoot)).rejects.toThrow( /cannot be called on directory/u, ); }); diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index 9a29a3f87..17e3c03ce 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -4,14 +4,28 @@ import { glob } from 'glob'; import { lstat, writeFile } from 'node:fs/promises'; import { resolve, parse, format, join } from 'node:path'; +/** + * Check if the target path is a directory. + * + * @param target The path to check. + * @returns A promise which resolves to true if the target path is a directory. + */ +async function isDirectory(target: string): Promise { + return (await lstat(target)).isDirectory(); +} + /** * Create a bundle given path to an entry point. * * @param sourcePath - Path to the source file that is the root of the bundle. + * @param check - Whether to check if the sourcePath is a directory. Defaults to true. * @returns A promise that resolves when the bundle has been written. */ -export async function createBundle(sourcePath: string): Promise { - if ((await lstat(sourcePath)).isDirectory()) { +export async function createBundleFile( + sourcePath: string, + check: boolean = true, +): Promise { + if (check && (await isDirectory(sourcePath))) { throw new Error('createBundle cannot be called on directory', { cause: { sourcePath }, }); @@ -31,16 +45,33 @@ export async function createBundle(sourcePath: string): Promise { * Create a bundle given path to an entry point. * * @param sourceDir - Path to a directory of source files to bundle. + * @param check - Whether to check if the sourceDir is a directory. Defaults to true. * @returns A promise that resolves when the bundles have been written. */ -export async function createBundleDir(sourceDir: string): Promise { - if (!(await lstat(sourceDir)).isDirectory()) { +export async function createBundleDir( + sourceDir: string, + check: boolean = true, +): Promise { + if (check && !(await isDirectory(sourceDir))) { throw new Error('createBundleDir must be called on directory', { cause: { sourceDir }, }); } console.log('bundling dir', sourceDir); for (const source of await glob(join(sourceDir, '**/*.js'))) { - await createBundle(source); + await createBundleFile(source); } } + +/** + * Bundle a target file or every file in the target directory. + * + * @param target The file or directory to apply the bundler to. + * @returns A promise that resolves when bundling is done. + */ +export async function createBundle(target: string): Promise { + await ((await isDirectory(target)) ? createBundleDir : createBundleFile)( + target, + false, + ); +} diff --git a/packages/cli/src/meta.test.ts b/packages/cli/src/meta.test.ts deleted file mode 100644 index 43ddde62b..000000000 --- a/packages/cli/src/meta.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { getTestBundles } from '../test/bundles.js'; -import { exists } from '../test/file.js'; - -describe('[meta]', async () => { - const { testBundleNames, testBundleSpecs } = await getTestBundles(); - - it('at least one test bundle is configured', () => { - expect(testBundleNames.length).toBeGreaterThan(0); - }); - - it.each(testBundleSpecs)( - 'test bundles have expectations: $script', - async ({ expected }) => { - expect(await exists(expected)).toBe(true); - }, - ); -}); diff --git a/packages/cli/test/file.ts b/packages/cli/test/file.ts index 958ef6e45..c8659db3b 100644 --- a/packages/cli/test/file.ts +++ b/packages/cli/test/file.ts @@ -7,7 +7,7 @@ import { open } from 'fs/promises'; * @param path - The path to check * @returns A promise that resolves to true iff a file exists at the given path */ -export async function exists(path: string): Promise { +export async function fileExists(path: string): Promise { try { const file = await open(path, 'wx'); // if the file opens, it didn't exist yet diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index f40fb5434..442776e8b 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -4,10 +4,15 @@ "baseUrl": "./", "lib": ["DOM", "ES2022"], "outDir": "./dist", + "emitDeclarationOnly": false, "rootDir": "./src", - "types": ["ses", "node"] + "types": ["ses", "node"], + "noEmit": true }, - "references": [], + "references": [ + { "path": "../utils/tsconfig.build.json" }, + { "path": "../shims/tsconfig.build.json" } + ], "files": [], "include": ["./src"] } diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 9c3390863..b4cc7f68c 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,14 +3,14 @@ "compilerOptions": { "baseUrl": "./", "lib": ["DOM", "ES2022"], - "types": ["ses", "vitest", "node"] + "types": ["ses", "vitest", "node"], + "noEmit": true }, "references": [{ "path": "../test-utils" }, { "path": "../utils" }], "include": [ "../../vitest.config.ts", - "./src", + "./src/**/*.ts", "./test", - "./vite.config.ts", "./vitest.config.ts" ] } diff --git a/packages/cli/tsconfig.lint.json b/packages/cli/tsconfig.lint.json index 8d3e49eb8..66aa16ef3 100644 --- a/packages/cli/tsconfig.lint.json +++ b/packages/cli/tsconfig.lint.json @@ -5,6 +5,6 @@ "noEmit": true, "skipLibCheck": true }, - "include": ["./src"], + "include": ["./src", "./scripts", "./test"], "exclude": [] } diff --git a/packages/cli/typedoc.json b/packages/cli/typedoc.json new file mode 100644 index 000000000..0ffe09d54 --- /dev/null +++ b/packages/cli/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/app.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} From 414311164c9bbbaeed21de2d66e1c0eaa0b9f032 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:59:30 -0600 Subject: [PATCH 08/19] improve cli usage feedback --- packages/cli/src/app.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts index 24d94bd25..02180bdc2 100755 --- a/packages/cli/src/app.ts +++ b/packages/cli/src/app.ts @@ -7,8 +7,10 @@ import { defaultConfig } from './config.js'; await yargs(hideBin(process.argv)) .usage('$0 [options]') + .demandCommand(1) + .strict() .command( - 'bundle [targets..]', + 'bundle ', 'Bundle user code to be used in a vat', (_yargs) => _yargs.option('targets', { @@ -24,7 +26,7 @@ await yargs(hideBin(process.argv)) }, ) .command( - 'serve ', + 'serve [-p port]', 'Serve bundled user code by filename', (_yargs) => _yargs From b85338b84a7eff0b6084da0f72975ef95be84e6f Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:02:26 -0600 Subject: [PATCH 09/19] update root tsconfig files --- tsconfig.build.json | 1 + tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/tsconfig.build.json b/tsconfig.build.json index 2f070b5d9..57892aef3 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,6 +3,7 @@ "include": [], "references": [ // We exclude the extension and shims here due to their special build processes. + { "path": "./packages/cli/tsconfig.build.json" }, { "path": "./packages/errors/tsconfig.build.json" }, { "path": "./packages/kernel/tsconfig.build.json" }, { "path": "./packages/streams/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 8630999cd..613fdac49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "files": [], "include": [], "references": [ + { "path": "./packages/cli" }, { "path": "./packages/errors" }, { "path": "./packages/extension" }, { "path": "./packages/kernel" }, From 70805019542b1694127cff2ad370fa1cb7b68043 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:44:21 -0600 Subject: [PATCH 10/19] use dedicated test/stage dir for testing --- .gitignore | 2 + packages/cli/src/commands/bundle.test.ts | 11 +++-- packages/cli/src/commands/bundle.ts | 12 +---- packages/cli/src/file.ts | 45 +++++++++++++++++++ packages/cli/test/bundles.ts | 35 ++++++++++++--- .../cli/test/bundles/sample-vat-esp.expected | 2 +- packages/cli/test/bundles/sample-vat.expected | 2 +- packages/cli/test/file.ts | 22 --------- 8 files changed, 88 insertions(+), 43 deletions(-) create mode 100644 packages/cli/src/file.ts delete mode 100644 packages/cli/test/file.ts diff --git a/.gitignore b/.gitignore index 152bf9cf1..47dda0844 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,8 @@ node_modules/ # Ignore all .bundle files in the extensions directory packages/extension/**/*.bundle +packages/cli/test/stage + # Ignore playwright reports playwright-report diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index 11020a20f..c12b3294a 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -14,7 +14,7 @@ import { import { createBundleFile, createBundleDir } from './bundle.js'; import { getTestBundles } from '../../test/bundles.js'; -import { fileExists } from '../../test/file.js'; +import { fileExists } from '../file.js'; describe('bundle', async () => { beforeEach(() => { @@ -85,9 +85,12 @@ describe('bundle', async () => { }); it('throws an error if supplied path is not a directory', async () => { - await expect( - createBundleDir(testBundleSpecs[0]?.script as string), - ).rejects.toThrow(/must be called on directory/u); + const script = testBundleSpecs[0]?.script; + expect(testBundleSpecs.length).toBeGreaterThan(0); + expect(script).toBeDefined(); + await expect(createBundleDir(script as string)).rejects.toThrow( + /must be called on directory/u, + ); }); }); }); diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index 17e3c03ce..157f9a4d7 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -1,18 +1,10 @@ import '@endo/init'; import bundleSource from '@endo/bundle-source'; import { glob } from 'glob'; -import { lstat, writeFile } from 'node:fs/promises'; +import { writeFile } from 'node:fs/promises'; import { resolve, parse, format, join } from 'node:path'; -/** - * Check if the target path is a directory. - * - * @param target The path to check. - * @returns A promise which resolves to true if the target path is a directory. - */ -async function isDirectory(target: string): Promise { - return (await lstat(target)).isDirectory(); -} +import { isDirectory } from '../file.js'; /** * Create a bundle given path to an entry point. diff --git a/packages/cli/src/file.ts b/packages/cli/src/file.ts new file mode 100644 index 000000000..1af55f910 --- /dev/null +++ b/packages/cli/src/file.ts @@ -0,0 +1,45 @@ +import { isObject } from '@metamask/utils'; +import { copyFile, lstat, access } from 'fs/promises'; + +/** + * Check if the target path is a directory. + * + * @param target The path to check. + * @returns A promise which resolves to true if the target path is a directory. + */ +export async function isDirectory(target: string): Promise { + return (await lstat(target)).isDirectory(); +} + +/** + * Asynchronously copy file(s) from source to destination. + * + * @param source - Where to copy file(s) from. + * @param destination - Where to copy file(s) to. + * @returns A promise that resolves when copying is complete. + */ +export async function cp(source: string, destination: string): Promise { + if (await isDirectory(source)) { + throw new Error('Directory cp not implemented.'); + } + await copyFile(source, destination); +} + +/** + * Asynchronously check if a file exists. + * + * @param path - The path to check + * @returns A promise that resolves to true iff a file exists at the given path + */ +export async function fileExists(path: string): Promise { + try { + // if the file can be accessed, it didn't exist yet + await access(path); + return false; + } catch (error) { + if (isObject(error) && error.code === 'EEXIST') { + return true; + } + throw error; + } +} diff --git a/packages/cli/test/bundles.ts b/packages/cli/test/bundles.ts index 8693a371a..eab287465 100644 --- a/packages/cli/test/bundles.ts +++ b/packages/cli/test/bundles.ts @@ -1,10 +1,34 @@ +import { mkdir, rm } from 'fs/promises'; import { glob } from 'glob'; -import { resolve, join, basename } from 'path'; +import { resolve, join, basename, format } from 'path'; -const testBundleRoot = resolve( - import.meta.url.split(':')[1] as string, - '../bundles', -); +import { cp } from '../src/file.js'; + +const makeTestBundleRoot = async (): Promise => { + const testRoot = resolve(import.meta.url.split(':')[1] as string, '..'); + + // clean and remake test staging area + const stage = resolve(testRoot, 'stage'); + await rm(stage, { recursive: true, force: true }); + await mkdir(stage); + + // copy bundle targets to staging area + const testBundleRoot = resolve(testRoot, 'bundles'); + const stageBundleRoot = resolve(stage, 'bundles'); + await mkdir(stageBundleRoot); + for (const ext of ['.js', '.expected']) { + await Promise.all( + (await glob(join(testBundleRoot, `*${ext}`))).map(async (filePath) => { + const name = basename(filePath, ext); + await cp(filePath, format({ dir: stageBundleRoot, name, ext })); + }), + ); + } + await cp(join(testRoot, 'test.bundle'), join(stage, 'test.bundle')); + + // return the staging area, ready for testing + return stageBundleRoot; +}; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getTestBundleNames = async (bundleRoot: string) => @@ -23,6 +47,7 @@ const getTestBundleSpecs = (bundleRoot: string, bundleNames: string[]) => // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const getTestBundles = async () => { + const testBundleRoot = await makeTestBundleRoot(); const testBundleNames = await getTestBundleNames(testBundleRoot); const testBundleSpecs = getTestBundleSpecs(testBundleRoot, testBundleNames); return { diff --git a/packages/cli/test/bundles/sample-vat-esp.expected b/packages/cli/test/bundles/sample-vat-esp.expected index 0df012325..df049ce35 100644 --- a/packages/cli/test/bundles/sample-vat-esp.expected +++ b/packages/cli/test/bundles/sample-vat-esp.expected @@ -1 +1 @@ -{"moduleFormat":"endoZipBase64","endoZipBase64":"UEsDBAoAAAAAAAAAAACzfS0JXQIAAF0CAAAUAAAAY29tcGFydG1lbnQtbWFwLmpzb257CiAgInRhZ3MiOiBbXSwKICAiZW50cnkiOiB7CiAgICAiY29tcGFydG1lbnQiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAibW9kdWxlIjogIi4vdGVzdC9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzIgogIH0sCiAgImNvbXBhcnRtZW50cyI6IHsKICAgICJAb2NhcC9jbGktdjAuMC4wIjogewogICAgICAibmFtZSI6ICJAb2NhcC9jbGkiLAogICAgICAibGFiZWwiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAgICJsb2NhdGlvbiI6ICJAb2NhcC9jbGktdjAuMC4wIiwKICAgICAgIm1vZHVsZXMiOiB7CiAgICAgICAgIi4vdGVzdC9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzIjogewogICAgICAgICAgImxvY2F0aW9uIjogInRlc3QvYnVuZGxlcy9zYW1wbGUtdmF0LWVzcC5qcyIsCiAgICAgICAgICAicGFyc2VyIjogInByZS1tanMtanNvbiIsCiAgICAgICAgICAic2hhNTEyIjogIjU4MDc5MGZjMGZjZmNmOTAyNzBkMTliOTIyY2FkYzRiMGIzMWRlYmYzYzU2ZjQ4ODJmYmViZDkxYWVjMDk5MTMyN2Q4ZGY3ZGZlMDFiNWU3OWIzNWE2MzE1ODRjYTFiMDE2ZjRkMTY2MzI4OWU4OTE0OGNlYmQ5ZjMxN2YxNTg1IgogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KfVBLAwQKAAAAAAAAAAAAs+4sO00DAABNAwAALwAAAEBvY2FwL2NsaS12MC4wLjAvdGVzdC9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzeyJpbXBvcnRzIjpbXSwiZXhwb3J0cyI6WyJzdGFydCJdLCJyZWV4cG9ydHMiOltdLCJfX3N5bmNNb2R1bGVQcm9ncmFtX18iOiIoeyAgIGltcG9ydHM6ICRozY9faW1wb3J0cywgICBsaXZlVmFyOiAkaM2PX2xpdmUsICAgb25jZVZhcjogJGjNj19vbmNlLCAgIGltcG9ydE1ldGE6ICRozY9fX19fbWV0YSwgfSkgPT4gKGZ1bmN0aW9uICgpIHsgJ3VzZSBzdHJpY3QnOyAgICRozY9faW1wb3J0cyhbXSk7T2JqZWN0LmRlZmluZVByb3BlcnR5KHN0YXJ0LCAnbmFtZScsIHt2YWx1ZTogXCJzdGFydFwifSk7JGjNj19vbmNlLnN0YXJ0KHN0YXJ0KTsgICAvKipcbiAqIFN0YXJ0IGZ1bmN0aW9uIGZvciBnZW5lcmljIHRlc3QgdmF0LlxuICpcbiAqIEBwYXJhbSB7dW5rbm93bn0gcGFyYW1ldGVycyAtIEluaXRpYWxpemF0aW9uIHBhcmFtZXRlcnMgZnJvbSB0aGUgdmF0J3MgY29uZmlnIG9iamVjdC5cbiAqIEByZXR1cm5zIHt1bmtub3dufSBUaGUgcm9vdCBvYmplY3QgZm9yIHRoZSBuZXcgdmF0LlxuICovXG5mdW5jdGlvbiAgICAgICAgc3RhcnQocGFyYW1ldGVycyl7XG5jb25zdCBuYW1lPXBhcmFtZXRlcnM/Lm5hbWU/Pydhbm9ueW1vdXMnO1xuY29uc29sZS5sb2coIGBlbXBlY2UgdmF0IHJvb3Qgb2JqZWN0byBcIiR7bmFtZX1cImApO1xucmV0dXJue1xubmFtZSxcbnN0dWZmOiBgc2UgaW5pY2nDsyBwb3IgJHtKU09OLnN0cmluZ2lmeShwYXJhbWV0ZXJzKX1gfTtcblxuIH1cbn0pKClcbiIsIl9fbGl2ZUV4cG9ydE1hcF9fIjp7fSwiX19yZWV4cG9ydE1hcF9fIjp7fSwiX19maXhlZEV4cG9ydE1hcF9fIjp7InN0YXJ0IjpbInN0YXJ0Il19LCJfX25lZWRzSW1wb3J0TWV0YV9fIjpmYWxzZX1QSwECHgMKAAAAAAAAAAAAs30tCV0CAABdAgAAFAAAAAAAAAAAAAAApIEAAAAAY29tcGFydG1lbnQtbWFwLmpzb25QSwECHgMKAAAAAAAAAAAAs+4sO00DAABNAwAALwAAAAAAAAAAAAAApIGPAgAAQG9jYXAvY2xpLXYwLjAuMC90ZXN0L2J1bmRsZXMvc2FtcGxlLXZhdC1lc3AuanNQSwUGAAAAAAIAAgCfAAAAKQYAAAAA","endoZipBase64Sha512":"7c137760832a5b520d187759baf7318f5e77470dd4b86febfc7e2ad0685a5475d6e8938e1a66dfb6802aeac9649de8ea1f67b2bbe87cce2273438649ebcf9536"} \ No newline at end of file +{"moduleFormat":"endoZipBase64","endoZipBase64":"UEsDBAoAAAAAAAAAAADqdnMTbwIAAG8CAAAUAAAAY29tcGFydG1lbnQtbWFwLmpzb257CiAgInRhZ3MiOiBbXSwKICAiZW50cnkiOiB7CiAgICAiY29tcGFydG1lbnQiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAibW9kdWxlIjogIi4vdGVzdC9zdGFnZS9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzIgogIH0sCiAgImNvbXBhcnRtZW50cyI6IHsKICAgICJAb2NhcC9jbGktdjAuMC4wIjogewogICAgICAibmFtZSI6ICJAb2NhcC9jbGkiLAogICAgICAibGFiZWwiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAgICJsb2NhdGlvbiI6ICJAb2NhcC9jbGktdjAuMC4wIiwKICAgICAgIm1vZHVsZXMiOiB7CiAgICAgICAgIi4vdGVzdC9zdGFnZS9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzIjogewogICAgICAgICAgImxvY2F0aW9uIjogInRlc3Qvc3RhZ2UvYnVuZGxlcy9zYW1wbGUtdmF0LWVzcC5qcyIsCiAgICAgICAgICAicGFyc2VyIjogInByZS1tanMtanNvbiIsCiAgICAgICAgICAic2hhNTEyIjogIjU4MDc5MGZjMGZjZmNmOTAyNzBkMTliOTIyY2FkYzRiMGIzMWRlYmYzYzU2ZjQ4ODJmYmViZDkxYWVjMDk5MTMyN2Q4ZGY3ZGZlMDFiNWU3OWIzNWE2MzE1ODRjYTFiMDE2ZjRkMTY2MzI4OWU4OTE0OGNlYmQ5ZjMxN2YxNTg1IgogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KfVBLAwQKAAAAAAAAAAAAs+4sO00DAABNAwAANQAAAEBvY2FwL2NsaS12MC4wLjAvdGVzdC9zdGFnZS9idW5kbGVzL3NhbXBsZS12YXQtZXNwLmpzeyJpbXBvcnRzIjpbXSwiZXhwb3J0cyI6WyJzdGFydCJdLCJyZWV4cG9ydHMiOltdLCJfX3N5bmNNb2R1bGVQcm9ncmFtX18iOiIoeyAgIGltcG9ydHM6ICRozY9faW1wb3J0cywgICBsaXZlVmFyOiAkaM2PX2xpdmUsICAgb25jZVZhcjogJGjNj19vbmNlLCAgIGltcG9ydE1ldGE6ICRozY9fX19fbWV0YSwgfSkgPT4gKGZ1bmN0aW9uICgpIHsgJ3VzZSBzdHJpY3QnOyAgICRozY9faW1wb3J0cyhbXSk7T2JqZWN0LmRlZmluZVByb3BlcnR5KHN0YXJ0LCAnbmFtZScsIHt2YWx1ZTogXCJzdGFydFwifSk7JGjNj19vbmNlLnN0YXJ0KHN0YXJ0KTsgICAvKipcbiAqIFN0YXJ0IGZ1bmN0aW9uIGZvciBnZW5lcmljIHRlc3QgdmF0LlxuICpcbiAqIEBwYXJhbSB7dW5rbm93bn0gcGFyYW1ldGVycyAtIEluaXRpYWxpemF0aW9uIHBhcmFtZXRlcnMgZnJvbSB0aGUgdmF0J3MgY29uZmlnIG9iamVjdC5cbiAqIEByZXR1cm5zIHt1bmtub3dufSBUaGUgcm9vdCBvYmplY3QgZm9yIHRoZSBuZXcgdmF0LlxuICovXG5mdW5jdGlvbiAgICAgICAgc3RhcnQocGFyYW1ldGVycyl7XG5jb25zdCBuYW1lPXBhcmFtZXRlcnM/Lm5hbWU/Pydhbm9ueW1vdXMnO1xuY29uc29sZS5sb2coIGBlbXBlY2UgdmF0IHJvb3Qgb2JqZWN0byBcIiR7bmFtZX1cImApO1xucmV0dXJue1xubmFtZSxcbnN0dWZmOiBgc2UgaW5pY2nDsyBwb3IgJHtKU09OLnN0cmluZ2lmeShwYXJhbWV0ZXJzKX1gfTtcblxuIH1cbn0pKClcbiIsIl9fbGl2ZUV4cG9ydE1hcF9fIjp7fSwiX19yZWV4cG9ydE1hcF9fIjp7fSwiX19maXhlZEV4cG9ydE1hcF9fIjp7InN0YXJ0IjpbInN0YXJ0Il19LCJfX25lZWRzSW1wb3J0TWV0YV9fIjpmYWxzZX1QSwECHgMKAAAAAAAAAAAA6nZzE28CAABvAgAAFAAAAAAAAAAAAAAApIEAAAAAY29tcGFydG1lbnQtbWFwLmpzb25QSwECHgMKAAAAAAAAAAAAs+4sO00DAABNAwAANQAAAAAAAAAAAAAApIGhAgAAQG9jYXAvY2xpLXYwLjAuMC90ZXN0L3N0YWdlL2J1bmRsZXMvc2FtcGxlLXZhdC1lc3AuanNQSwUGAAAAAAIAAgClAAAAQQYAAAAA","endoZipBase64Sha512":"5f5e720ff9793741f3026c6cc078d8395f411c8896129a6ba63698739dd71f13e6ff31682189ab596018091aa47595e28dee72ec937a6bb777c12e8d621e48ab"} \ No newline at end of file diff --git a/packages/cli/test/bundles/sample-vat.expected b/packages/cli/test/bundles/sample-vat.expected index a0961fd80..b964b43ec 100644 --- a/packages/cli/test/bundles/sample-vat.expected +++ b/packages/cli/test/bundles/sample-vat.expected @@ -1 +1 @@ -{"moduleFormat":"endoZipBase64","endoZipBase64":"UEsDBAoAAAAAAAAAAADuwyPVUQIAAFECAAAUAAAAY29tcGFydG1lbnQtbWFwLmpzb257CiAgInRhZ3MiOiBbXSwKICAiZW50cnkiOiB7CiAgICAiY29tcGFydG1lbnQiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAibW9kdWxlIjogIi4vdGVzdC9idW5kbGVzL3NhbXBsZS12YXQuanMiCiAgfSwKICAiY29tcGFydG1lbnRzIjogewogICAgIkBvY2FwL2NsaS12MC4wLjAiOiB7CiAgICAgICJuYW1lIjogIkBvY2FwL2NsaSIsCiAgICAgICJsYWJlbCI6ICJAb2NhcC9jbGktdjAuMC4wIiwKICAgICAgImxvY2F0aW9uIjogIkBvY2FwL2NsaS12MC4wLjAiLAogICAgICAibW9kdWxlcyI6IHsKICAgICAgICAiLi90ZXN0L2J1bmRsZXMvc2FtcGxlLXZhdC5qcyI6IHsKICAgICAgICAgICJsb2NhdGlvbiI6ICJ0ZXN0L2J1bmRsZXMvc2FtcGxlLXZhdC5qcyIsCiAgICAgICAgICAicGFyc2VyIjogInByZS1tanMtanNvbiIsCiAgICAgICAgICAic2hhNTEyIjogIjA0ZmI2Y2EwMTk0Mzc2ZjIwMzE5ZGEwMjQwYWM5OTg0NDM2MTQ5MTU0MDgzZjQ4ZDIzMWFjNTI5ZTEyZmNlZGRjYmU0OTZlNzViYTg2ZGFjYTcyY2Q5OTVlZDNkNzQ5NWY5NmNmODM1OWY1ZGRiYjk2MmM4YmY3ZTMyYjg3YWYyIgogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KfVBLAwQKAAAAAAAAAAAAPl/YgU0DAABNAwAAKwAAAEBvY2FwL2NsaS12MC4wLjAvdGVzdC9idW5kbGVzL3NhbXBsZS12YXQuanN7ImltcG9ydHMiOltdLCJleHBvcnRzIjpbInN0YXJ0Il0sInJlZXhwb3J0cyI6W10sIl9fc3luY01vZHVsZVByb2dyYW1fXyI6Iih7ICAgaW1wb3J0czogJGjNj19pbXBvcnRzLCAgIGxpdmVWYXI6ICRozY9fbGl2ZSwgICBvbmNlVmFyOiAkaM2PX29uY2UsICAgaW1wb3J0TWV0YTogJGjNj19fX19tZXRhLCB9KSA9PiAoZnVuY3Rpb24gKCkgeyAndXNlIHN0cmljdCc7ICAgJGjNj19pbXBvcnRzKFtdKTtPYmplY3QuZGVmaW5lUHJvcGVydHkoc3RhcnQsICduYW1lJywge3ZhbHVlOiBcInN0YXJ0XCJ9KTskaM2PX29uY2Uuc3RhcnQoc3RhcnQpOyAgIC8qKlxuICogU3RhcnQgZnVuY3Rpb24gZm9yIGdlbmVyaWMgdGVzdCB2YXQuXG4gKlxuICogQHBhcmFtIHt1bmtub3dufSBwYXJhbWV0ZXJzIC0gSW5pdGlhbGl6YXRpb24gcGFyYW1ldGVycyBmcm9tIHRoZSB2YXQncyBjb25maWcgb2JqZWN0LlxuICogQHJldHVybnMge3Vua25vd259IFRoZSByb290IG9iamVjdCBmb3IgdGhlIG5ldyB2YXQuXG4gKi9cbmZ1bmN0aW9uICAgICAgICBzdGFydChwYXJhbWV0ZXJzKXtcbmNvbnN0IG5hbWU9cGFyYW1ldGVycz8ubmFtZT8/J2Fub255bW91cyc7XG5jb25zb2xlLmxvZyggYHN0YXJ0IHZhdCByb290IG9iamVjdCBcIiR7bmFtZX1cImApO1xucmV0dXJue1xubmFtZSxcbnN0dWZmOiBgaW5pdGlhbGl6ZWQgd2l0aCAke0pTT04uc3RyaW5naWZ5KHBhcmFtZXRlcnMpfWB9O1xuXG4gfVxufSkoKVxuIiwiX19saXZlRXhwb3J0TWFwX18iOnt9LCJfX3JlZXhwb3J0TWFwX18iOnt9LCJfX2ZpeGVkRXhwb3J0TWFwX18iOnsic3RhcnQiOlsic3RhcnQiXX0sIl9fbmVlZHNJbXBvcnRNZXRhX18iOmZhbHNlfVBLAQIeAwoAAAAAAAAAAADuwyPVUQIAAFECAAAUAAAAAAAAAAAAAACkgQAAAABjb21wYXJ0bWVudC1tYXAuanNvblBLAQIeAwoAAAAAAAAAAAA+X9iBTQMAAE0DAAArAAAAAAAAAAAAAACkgYMCAABAb2NhcC9jbGktdjAuMC4wL3Rlc3QvYnVuZGxlcy9zYW1wbGUtdmF0LmpzUEsFBgAAAAACAAIAmwAAABkGAAAAAA==","endoZipBase64Sha512":"ef6309a11e421df02258b108152d039b9e748750ff3eba173c6738efe0ca81bb0f482e1e3d7695f06eb614aef9a8ea9836856f66ecbe2f3ae6b883da73406d51"} \ No newline at end of file +{"moduleFormat":"endoZipBase64","endoZipBase64":"UEsDBAoAAAAAAAAAAADY08YWYwIAAGMCAAAUAAAAY29tcGFydG1lbnQtbWFwLmpzb257CiAgInRhZ3MiOiBbXSwKICAiZW50cnkiOiB7CiAgICAiY29tcGFydG1lbnQiOiAiQG9jYXAvY2xpLXYwLjAuMCIsCiAgICAibW9kdWxlIjogIi4vdGVzdC9zdGFnZS9idW5kbGVzL3NhbXBsZS12YXQuanMiCiAgfSwKICAiY29tcGFydG1lbnRzIjogewogICAgIkBvY2FwL2NsaS12MC4wLjAiOiB7CiAgICAgICJuYW1lIjogIkBvY2FwL2NsaSIsCiAgICAgICJsYWJlbCI6ICJAb2NhcC9jbGktdjAuMC4wIiwKICAgICAgImxvY2F0aW9uIjogIkBvY2FwL2NsaS12MC4wLjAiLAogICAgICAibW9kdWxlcyI6IHsKICAgICAgICAiLi90ZXN0L3N0YWdlL2J1bmRsZXMvc2FtcGxlLXZhdC5qcyI6IHsKICAgICAgICAgICJsb2NhdGlvbiI6ICJ0ZXN0L3N0YWdlL2J1bmRsZXMvc2FtcGxlLXZhdC5qcyIsCiAgICAgICAgICAicGFyc2VyIjogInByZS1tanMtanNvbiIsCiAgICAgICAgICAic2hhNTEyIjogIjA0ZmI2Y2EwMTk0Mzc2ZjIwMzE5ZGEwMjQwYWM5OTg0NDM2MTQ5MTU0MDgzZjQ4ZDIzMWFjNTI5ZTEyZmNlZGRjYmU0OTZlNzViYTg2ZGFjYTcyY2Q5OTVlZDNkNzQ5NWY5NmNmODM1OWY1ZGRiYjk2MmM4YmY3ZTMyYjg3YWYyIgogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KfVBLAwQKAAAAAAAAAAAAPl/YgU0DAABNAwAAMQAAAEBvY2FwL2NsaS12MC4wLjAvdGVzdC9zdGFnZS9idW5kbGVzL3NhbXBsZS12YXQuanN7ImltcG9ydHMiOltdLCJleHBvcnRzIjpbInN0YXJ0Il0sInJlZXhwb3J0cyI6W10sIl9fc3luY01vZHVsZVByb2dyYW1fXyI6Iih7ICAgaW1wb3J0czogJGjNj19pbXBvcnRzLCAgIGxpdmVWYXI6ICRozY9fbGl2ZSwgICBvbmNlVmFyOiAkaM2PX29uY2UsICAgaW1wb3J0TWV0YTogJGjNj19fX19tZXRhLCB9KSA9PiAoZnVuY3Rpb24gKCkgeyAndXNlIHN0cmljdCc7ICAgJGjNj19pbXBvcnRzKFtdKTtPYmplY3QuZGVmaW5lUHJvcGVydHkoc3RhcnQsICduYW1lJywge3ZhbHVlOiBcInN0YXJ0XCJ9KTskaM2PX29uY2Uuc3RhcnQoc3RhcnQpOyAgIC8qKlxuICogU3RhcnQgZnVuY3Rpb24gZm9yIGdlbmVyaWMgdGVzdCB2YXQuXG4gKlxuICogQHBhcmFtIHt1bmtub3dufSBwYXJhbWV0ZXJzIC0gSW5pdGlhbGl6YXRpb24gcGFyYW1ldGVycyBmcm9tIHRoZSB2YXQncyBjb25maWcgb2JqZWN0LlxuICogQHJldHVybnMge3Vua25vd259IFRoZSByb290IG9iamVjdCBmb3IgdGhlIG5ldyB2YXQuXG4gKi9cbmZ1bmN0aW9uICAgICAgICBzdGFydChwYXJhbWV0ZXJzKXtcbmNvbnN0IG5hbWU9cGFyYW1ldGVycz8ubmFtZT8/J2Fub255bW91cyc7XG5jb25zb2xlLmxvZyggYHN0YXJ0IHZhdCByb290IG9iamVjdCBcIiR7bmFtZX1cImApO1xucmV0dXJue1xubmFtZSxcbnN0dWZmOiBgaW5pdGlhbGl6ZWQgd2l0aCAke0pTT04uc3RyaW5naWZ5KHBhcmFtZXRlcnMpfWB9O1xuXG4gfVxufSkoKVxuIiwiX19saXZlRXhwb3J0TWFwX18iOnt9LCJfX3JlZXhwb3J0TWFwX18iOnt9LCJfX2ZpeGVkRXhwb3J0TWFwX18iOnsic3RhcnQiOlsic3RhcnQiXX0sIl9fbmVlZHNJbXBvcnRNZXRhX18iOmZhbHNlfVBLAQIeAwoAAAAAAAAAAADY08YWYwIAAGMCAAAUAAAAAAAAAAAAAACkgQAAAABjb21wYXJ0bWVudC1tYXAuanNvblBLAQIeAwoAAAAAAAAAAAA+X9iBTQMAAE0DAAAxAAAAAAAAAAAAAACkgZUCAABAb2NhcC9jbGktdjAuMC4wL3Rlc3Qvc3RhZ2UvYnVuZGxlcy9zYW1wbGUtdmF0LmpzUEsFBgAAAAACAAIAoQAAADEGAAAAAA==","endoZipBase64Sha512":"bcefe2bfbe1f9730d46eccd9f93bcca779f223e2d0a79dbc25bac6b86478a66435bf2298cf0c3c4dc6fd5728e38962a534c66c45f21469cfaafd5c31e967713a"} \ No newline at end of file diff --git a/packages/cli/test/file.ts b/packages/cli/test/file.ts deleted file mode 100644 index c8659db3b..000000000 --- a/packages/cli/test/file.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isObject } from '@metamask/utils'; -import { open } from 'fs/promises'; - -/** - * Asynchronously check if a file exists. - * - * @param path - The path to check - * @returns A promise that resolves to true iff a file exists at the given path - */ -export async function fileExists(path: string): Promise { - try { - const file = await open(path, 'wx'); - // if the file opens, it didn't exist yet - await file.close(); - return false; - } catch (error) { - if (isObject(error) && error.code === 'EEXIST') { - return true; - } - throw error; - } -} From a209fc7b4141a5449fac2b57deea869f0e1075f8 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:55:50 -0600 Subject: [PATCH 11/19] remove noEmit from build --- packages/cli/tsconfig.build.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index 442776e8b..e7e91e4a3 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -6,8 +6,7 @@ "outDir": "./dist", "emitDeclarationOnly": false, "rootDir": "./src", - "types": ["ses", "node"], - "noEmit": true + "types": ["ses", "node"] }, "references": [ { "path": "../utils/tsconfig.build.json" }, From 920b08eade66f2057291678eed85d42aa6d54ec5 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:59:43 -0600 Subject: [PATCH 12/19] clean up --- packages/cli/src/commands/bundle.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index c12b3294a..39f53a923 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -86,8 +86,6 @@ describe('bundle', async () => { it('throws an error if supplied path is not a directory', async () => { const script = testBundleSpecs[0]?.script; - expect(testBundleSpecs.length).toBeGreaterThan(0); - expect(script).toBeDefined(); await expect(createBundleDir(script as string)).rejects.toThrow( /must be called on directory/u, ); From ae31068ce014b99d2d3088ea51c0e7c8d6b6770a Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 20:36:30 -0600 Subject: [PATCH 13/19] deflake server test --- packages/cli/src/commands/bundle.ts | 8 ++- packages/cli/src/commands/serve.test.ts | 80 ++++++++++++------------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index 157f9a4d7..b9038aa00 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -50,9 +50,11 @@ export async function createBundleDir( }); } console.log('bundling dir', sourceDir); - for (const source of await glob(join(sourceDir, '**/*.js'))) { - await createBundleFile(source); - } + await Promise.all( + (await glob(join(sourceDir, '*.js'))).map( + async (source) => await createBundleFile(source), + ), + ); } /** diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts index a361eb96a..7c665d160 100644 --- a/packages/cli/src/commands/serve.test.ts +++ b/packages/cli/src/commands/serve.test.ts @@ -10,6 +10,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getServer } from './serve.js'; import { getTestBundles } from '../../test/bundles.js'; +import { defaultConfig } from '../config.js'; const isBundleSourceResult = ( value: unknown, @@ -29,11 +30,13 @@ describe('serve', async () => { const { testBundleRoot, testBundleSpecs } = await getTestBundles(); + const getServerPort = makeCounter(defaultConfig.server.port); + describe('getServer', () => { it('returns an object with a listen property', () => { const server = getServer({ server: { - port: 3000, + port: getServerPort(), }, dir: testBundleRoot, }); @@ -42,13 +45,13 @@ describe('serve', async () => { }); it(`throws if 'dir' is not specified`, () => { - expect(() => getServer({ server: { port: 3000 } })).toThrow(/dir/u); + expect(() => getServer({ server: { port: getServerPort() } })).toThrow( + /dir/u, + ); }); }); describe('server', () => { - const getServerPort = makeCounter(3000); - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const makeServer = (root: string = testBundleRoot) => { const port = getServerPort(); @@ -58,22 +61,20 @@ describe('serve', async () => { }, dir: root, }); - const requestBundle = async (path: string): Promise => - await nodeFetch(`http://localhost:${port}/${path}`).then( - async (resp) => { - if (resp.ok) { - return resp.json(); - } - throw new Error(resp.statusText, { cause: resp.status }); - }, - ); + const requestBundle = async (path: string): Promise => { + const resp = await nodeFetch(`http://localhost:${port}/${path}`); + if (resp.ok) { + return resp.json(); + } + throw new Error(resp.statusText, { cause: resp.status }); + }; return { listen, requestBundle, }; }; - it.sequential('serves bundles', async () => { + it('serves bundles', async () => { const bundleName = 'test.bundle'; const bundleRoot = join(testBundleRoot, '..'); const bundlePath = join(bundleRoot, bundleName); @@ -82,35 +83,34 @@ describe('serve', async () => { const { close } = await listen(); try { - const expectedBundleHash = await readFile(bundlePath).then( - (content) => { - const json = JSON.parse(content.toString()); - if (!isBundleSourceResult(json)) { - throw new Error( - [ - `Could not read expected bundle ${bundlePath}`, - `Parsed JSON: ${stringify(json)}`, - ].join('\n'), - ); - } - return json.endoZipBase64Sha512; - }, - ); - - const receivedBundleHash = await requestBundle(bundleName).then( - (json) => { - if (!isBundleSourceResult(json)) { - return `Received unexpected response from server: ${stringify(json)}`; - } - return createHash('sha512') - .update(Buffer.from(json.endoZipBase64)) - .digest('hex'); - }, - ); + const bundleData = await readFile(bundlePath); + const expectedBundleContent = JSON.parse(bundleData.toString()); + if (!isBundleSourceResult(expectedBundleContent)) { + throw new Error( + [ + `Could not read expected bundle ${bundlePath}`, + `Parsed JSON: ${stringify(expectedBundleContent)}`, + ].join('\n'), + ); + } + const expectedBundleHash = expectedBundleContent.endoZipBase64Sha512; + + const receivedBundleContent = await requestBundle(bundleName); + if (!isBundleSourceResult(receivedBundleContent)) { + throw new Error( + `Received unexpected response from server: ${stringify(receivedBundleContent)}`, + ); + } + const receivedBundleHash = createHash('sha512') + .update(Buffer.from(receivedBundleContent.endoZipBase64)) + .digest('hex'); expect(receivedBundleHash).toStrictEqual(expectedBundleHash); } finally { - await close(); + await Promise.race([ + new Promise((_resolve) => setTimeout(_resolve, 400)), + close(), + ]); } }); From 78634d0f6d6efe172c9d76a3a6b67610ae70aed4 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Dec 2024 20:38:45 -0600 Subject: [PATCH 14/19] add coverage thresholds --- vitest.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index 531d0514a..d85d126e0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -34,6 +34,12 @@ export default defineConfig({ ], thresholds: { autoUpdate: true, + 'packages/cli/**': { + statements: 77.41, + functions: 76.19, + branches: 75, + lines: 77.41, + }, 'packages/errors/**': { statements: 100, functions: 100, From 7fefe35cdcd9969f05ad4ba8a4ff96a5e64c72b0 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:42:25 -0600 Subject: [PATCH 15/19] simplify serve.ts --- packages/cli/src/commands/serve.ts | 18 ++---------------- vitest.config.ts | 8 ++++---- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 80f84ec2a..5732dcf99 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -4,16 +4,13 @@ import { createServer } from 'http'; import type { AddressInfo } from 'net'; import { resolve as resolvePath } from 'path'; import serveMiddleware from 'serve-handler'; +import { promisify } from 'util'; import type { Config } from '../config.js'; /** * Get a static server for development purposes. * - * Note: We're intentionally not using `webpack-dev-server` here because it - * adds a lot of extra stuff to the output that we don't need, and it's - * difficult to customize. - * * @param config - The config object. * @returns An object with a `listen` method that returns a promise that * resolves when the server is listening. @@ -102,18 +99,7 @@ export function getServer(config: Config) { }>((resolve, reject) => { try { server.listen(port, () => { - const close = async (): Promise => { - await new Promise((_resolve, _reject) => { - server.close((closeError) => { - if (closeError) { - return _reject(closeError); - } - - return _resolve(); - }); - }); - }; - + const close = promisify(server.close.bind(server)); const address = server.address() as AddressInfo; resolve({ port: address.port, server, close }); }); diff --git a/vitest.config.ts b/vitest.config.ts index d85d126e0..454dca093 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -35,10 +35,10 @@ export default defineConfig({ thresholds: { autoUpdate: true, 'packages/cli/**': { - statements: 77.41, - functions: 76.19, - branches: 75, - lines: 77.41, + statements: 77.19, + functions: 72.22, + branches: 76.66, + lines: 77.19, }, 'packages/errors/**': { statements: 100, From 7ae5c03ada9b0673d1c3edda00f70c4967dc36c5 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:27:09 -0500 Subject: [PATCH 16/19] use Blob size to count bytes written Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com> --- packages/cli/src/commands/bundle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index b9038aa00..94bec0205 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -30,7 +30,7 @@ export async function createBundleFile( const bundle = await bundleSource(sourceFullPath); const bundleString = JSON.stringify(bundle); await writeFile(bundlePath, bundleString); - console.log(`wrote ${bundlePath}: ${bundleString.length} bytes`); + console.log(`wrote ${bundlePath}: ${new Blob([bundleString]).size} bytes`); } /** From 0d157229916b0af36c4e8344a0c7048c8049516e Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:28:13 -0500 Subject: [PATCH 17/19] Unredundify truthiness check Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com> --- packages/cli/src/commands/serve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 5732dcf99..aa4fc7472 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -17,7 +17,7 @@ import type { Config } from '../config.js'; */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function getServer(config: Config) { - if (typeof config.dir === 'undefined' || !config.dir) { + if (!config.dir) { throw new Error(`Config option 'dir' must be specified.`); } const bundleRoot = resolvePath(config.dir); From ed4062f41d71e5d4bc5755b10682f0e1500683a5 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:57:23 -0600 Subject: [PATCH 18/19] remove unused directory checks --- packages/cli/src/commands/bundle.ts | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index 94bec0205..6edfdd23b 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -10,19 +10,9 @@ import { isDirectory } from '../file.js'; * Create a bundle given path to an entry point. * * @param sourcePath - Path to the source file that is the root of the bundle. - * @param check - Whether to check if the sourcePath is a directory. Defaults to true. * @returns A promise that resolves when the bundle has been written. */ -export async function createBundleFile( - sourcePath: string, - check: boolean = true, -): Promise { - if (check && (await isDirectory(sourcePath))) { - throw new Error('createBundle cannot be called on directory', { - cause: { sourcePath }, - }); - } - +export async function createBundleFile(sourcePath: string): Promise { const sourceFullPath = resolve(sourcePath); console.log(sourceFullPath); const { dir, name } = parse(sourceFullPath); @@ -37,18 +27,9 @@ export async function createBundleFile( * Create a bundle given path to an entry point. * * @param sourceDir - Path to a directory of source files to bundle. - * @param check - Whether to check if the sourceDir is a directory. Defaults to true. * @returns A promise that resolves when the bundles have been written. */ -export async function createBundleDir( - sourceDir: string, - check: boolean = true, -): Promise { - if (check && !(await isDirectory(sourceDir))) { - throw new Error('createBundleDir must be called on directory', { - cause: { sourceDir }, - }); - } +export async function createBundleDir(sourceDir: string): Promise { console.log('bundling dir', sourceDir); await Promise.all( (await glob(join(sourceDir, '*.js'))).map( @@ -66,6 +47,5 @@ export async function createBundleDir( export async function createBundle(target: string): Promise { await ((await isDirectory(target)) ? createBundleDir : createBundleFile)( target, - false, ); } From 8f3dfc0657e17303677e9ecbe1c5b5cc5d499fb5 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:17:23 -0600 Subject: [PATCH 19/19] use tmpdir for testing --- .gitignore | 2 -- packages/cli/src/commands/bundle.test.ts | 27 ++++++++++++------------ packages/cli/test/bundles.ts | 17 +++++++-------- vitest.config.ts | 6 +++--- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 47dda0844..152bf9cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -80,8 +80,6 @@ node_modules/ # Ignore all .bundle files in the extensions directory packages/extension/**/*.bundle -packages/cli/test/stage - # Ignore playwright reports playwright-report diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index 39f53a923..dca91c8dc 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -1,3 +1,4 @@ +import bundleSourceImport from '@endo/bundle-source'; import { createHash } from 'crypto'; import { readFile, rm } from 'fs/promises'; import { glob } from 'glob'; @@ -16,6 +17,12 @@ import { createBundleFile, createBundleDir } from './bundle.js'; import { getTestBundles } from '../../test/bundles.js'; import { fileExists } from '../file.js'; +const bundleSource = bundleSourceImport as ReturnType; + +vi.mock('@endo/bundle-source', () => ({ + default: vi.fn(), +})); + describe('bundle', async () => { beforeEach(() => { vi.resetModules(); @@ -43,11 +50,14 @@ describe('bundle', async () => { } ctx.expect(await fileExists(bundle)).toBe(false); + const expectedBundleContent = await readFile(expected); + + bundleSource.mockImplementationOnce(() => expectedBundleContent); + await createBundleFile(script); ctx.expect(await fileExists(bundle)).toBe(true); - const expectedBundleContent = await readFile(expected); const bundleContent = await readFile(bundle); const expectedBundleHash = createHash('sha256') .update(expectedBundleContent) @@ -59,12 +69,6 @@ describe('bundle', async () => { .toStrictEqual(expectedBundleHash.toString('hex')); }, ); - - it('throws an error if supplied path is a directory', async () => { - await expect(createBundleFile(testBundleRoot)).rejects.toThrow( - /cannot be called on directory/u, - ); - }); }); describe('createBundleDir', () => { @@ -75,6 +79,8 @@ describe('bundle', async () => { ), ).toStrictEqual([]); + bundleSource.mockImplementation(() => 'test content'); + await createBundleDir(testBundleRoot); expect( @@ -83,12 +89,5 @@ describe('bundle', async () => { ), ).toStrictEqual(testBundleNames); }); - - it('throws an error if supplied path is not a directory', async () => { - const script = testBundleSpecs[0]?.script; - await expect(createBundleDir(script as string)).rejects.toThrow( - /must be called on directory/u, - ); - }); }); }); diff --git a/packages/cli/test/bundles.ts b/packages/cli/test/bundles.ts index eab287465..9cfaced4f 100644 --- a/packages/cli/test/bundles.ts +++ b/packages/cli/test/bundles.ts @@ -1,5 +1,6 @@ -import { mkdir, rm } from 'fs/promises'; +import { mkdir } from 'fs/promises'; import { glob } from 'glob'; +import { tmpdir } from 'os'; import { resolve, join, basename, format } from 'path'; import { cp } from '../src/file.js'; @@ -7,15 +8,10 @@ import { cp } from '../src/file.js'; const makeTestBundleRoot = async (): Promise => { const testRoot = resolve(import.meta.url.split(':')[1] as string, '..'); - // clean and remake test staging area - const stage = resolve(testRoot, 'stage'); - await rm(stage, { recursive: true, force: true }); - await mkdir(stage); - // copy bundle targets to staging area const testBundleRoot = resolve(testRoot, 'bundles'); - const stageBundleRoot = resolve(stage, 'bundles'); - await mkdir(stageBundleRoot); + const stageBundleRoot = resolve(tmpdir(), 'test/bundles'); + await mkdir(stageBundleRoot, { recursive: true }); for (const ext of ['.js', '.expected']) { await Promise.all( (await glob(join(testBundleRoot, `*${ext}`))).map(async (filePath) => { @@ -24,7 +20,10 @@ const makeTestBundleRoot = async (): Promise => { }), ); } - await cp(join(testRoot, 'test.bundle'), join(stage, 'test.bundle')); + await cp( + join(testRoot, 'test.bundle'), + join(stageBundleRoot, '../test.bundle'), + ); // return the staging area, ready for testing return stageBundleRoot; diff --git a/vitest.config.ts b/vitest.config.ts index 454dca093..9e825de6f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -35,10 +35,10 @@ export default defineConfig({ thresholds: { autoUpdate: true, 'packages/cli/**': { - statements: 77.19, + statements: 75.47, functions: 72.22, - branches: 76.66, - lines: 77.19, + branches: 61.11, + lines: 75.47, }, 'packages/errors/**': { statements: 100,