Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -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).
87 changes: 87 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"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",
"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",
"@metamask/utils": "^9.3.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",
"node-fetch": "^3.3.2",
"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",
"vitest": "^2.1.2"
},
"engines": {
"node": "^18.18 || >=20"
},
"exports": {
"./package.json": "./package.json"
}
}
58 changes: 58 additions & 0 deletions packages/cli/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import { createBundle } from './commands/bundle.js';
import { getServer } from './commands/serve.js';
import { defaultConfig } from './config.js';

await yargs(hideBin(process.argv))
.usage('$0 <command> [options]')
.demandCommand(1)
.strict()
.command(
'bundle <targets..>',
'Bundle user code to be used in a vat',
(_yargs) =>
_yargs.option('targets', {
type: 'string',
file: true,
dir: true,
array: true,
demandOption: true,
describe: 'The files or directories of files to bundle',
}),
async (args) => {
await Promise.all(args.targets.map(createBundle));
},
)
.command(
'serve <dir> [-p port]',
'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 server = getServer({
server: {
port: args.port,
},
dir: args.dir,
});
await server.listen();
},
)
.help('h')
.alias('h', 'help')
.parse();
93 changes: 93 additions & 0 deletions packages/cli/src/commands/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import bundleSourceImport from '@endo/bundle-source';
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 { createBundleFile, createBundleDir } from './bundle.js';
import { getTestBundles } from '../../test/bundles.js';
import { fileExists } from '../file.js';

const bundleSource = bundleSourceImport as ReturnType<typeof vi.fn>;

vi.mock('@endo/bundle-source', () => ({
default: vi.fn(),
}));

describe('bundle', async () => {
beforeEach(() => {
vi.resetModules();
});

const { testBundleRoot, testBundleNames, testBundleSpecs } =
await getTestBundles();

const deleteTestBundles = async (): Promise<void> =>
await Promise.all(
testBundleSpecs.map(async ({ bundle }) => rm(bundle, { force: true })),
).then(() => undefined);

beforeAll(deleteTestBundles);
afterEach(deleteTestBundles);

describe('createBundleFile', () => {
it.for(testBundleSpecs)(
'bundles a single file: $name',
async ({ script, expected, bundle }, ctx) => {
if (!(await fileExists(expected))) {
// this test case has no expected bundle
// reporting handled in `describe('[meta]'` above
ctx.skip();
}
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 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'));
},
);
});

describe('createBundleDir', () => {
it('bundles a directory', async () => {
expect(
(await glob(join(testBundleRoot, '*.bundle'))).map((filepath) =>
basename(filepath, '.bundle'),
),
).toStrictEqual([]);

bundleSource.mockImplementation(() => 'test content');

await createBundleDir(testBundleRoot);

expect(
(await glob(join(testBundleRoot, '*.bundle'))).map((filepath) =>
basename(filepath, '.bundle'),
),
).toStrictEqual(testBundleNames);
});
});
});
51 changes: 51 additions & 0 deletions packages/cli/src/commands/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import '@endo/init';
import bundleSource from '@endo/bundle-source';
import { glob } from 'glob';
import { writeFile } from 'node:fs/promises';
import { resolve, parse, format, join } from 'node:path';

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.
* @returns A promise that resolves when the bundle has been written.
*/
export async function createBundleFile(sourcePath: string): Promise<void> {
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}: ${new Blob([bundleString]).size} 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<void> {
console.log('bundling dir', sourceDir);
await Promise.all(
(await glob(join(sourceDir, '*.js'))).map(
async (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<void> {
await ((await isDirectory(target)) ? createBundleDir : createBundleFile)(
target,
);
}
Loading
Loading