feat(endo): Add command line
This change provides a preliminary command line for `endo`, mostly because it is convenient for ad-hoc testing, though it will likely grow into something else.
ADD
packages/endo/bin/endo
+endo.js
\ No newline at end of file

ADD
packages/endo/bin/endo.js
+#!/usr/bin/env node
+import fs from "fs";
+import { main } from "../src/cli.js";
+
+main(process, { fs: fs.promises });

MODIFY
packages/endo/package.json
packages/endo/package.json
"require": "./dist/endo.cjs",
"browser": "./dist/endo.umd.js"
},
+ "bin": {
+ "endo": "./bin/endo.js"
+ },
"scripts": {
"build": "rollup --config rollup.config.js",
"clean": "rm -rf dist",

ADD
packages/endo/src/cli.js
+/* eslint no-shadow: [0] */
+import "./lockdown.js";
+import { writeArchive } from "./main.js";
+import { search } from "./search.js";
+import { compartmentMapForNodeModules } from "./compartmap.js";
+
+function usage(message) {
+ console.error(message);
+ return 1;
+}
+
+async function noEntryUsage() {
+ return usage(`expected path to program`);
+}
+
+async function noArchiveUsage() {
+ return usage(`expected path for archive`);
+}
+
+async function subcommand([arg, ...rest], handlers) {
+ const keys = Object.keys(handlers);
+ if (arg === undefined || !keys.includes(arg)) {
+ return usage(`expected one of ${keys.join(", ")}`);
+ }
+ return handlers[arg](rest);
+}
+
+async function parameter(args, handle, usage) {
+ const [arg, ...rest] = args;
+ if (arg === undefined) {
+ return usage(`expected an argument`);
+ }
+ if (arg.startsWith("-")) {
+ return usage(`unexpected flag: ${arg}`);
+ }
+ return handle(arg, rest);
+}
+
+async function run(args, { cwd, read, write, stdout }) {
+ async function compartmap(args) {
+ async function handleEntry(applicationPath, args) {
+ if (args.length) {
+ return usage(`unexpected arguments: ${JSON.stringify(args)}`);
+ }
+ const currentLocation = new URL(`${cwd()}/`, "file:///");
+ const applicationLocation = new URL(applicationPath, currentLocation);
+ const { packageLocation } = await search(read, applicationLocation);
+ const compartmentMap = await compartmentMapForNodeModules(
+ read,
+ packageLocation
+ );
+ stdout.write(`${JSON.stringify(compartmentMap, null, 2)}\n`);
+ return 0;
+ }
+ return parameter(args, handleEntry, noEntryUsage);
+ }
+
+ async function archive(args) {
+ async function handleArchive(archivePath, args) {
+ async function handleEntry(applicationPath, args) {
+ if (args.length) {
+ return usage(`unexpected arguments: ${JSON.stringify(args)}`);
+ }
+ const currentLocation = new URL(`${cwd()}/`, "file:///");
+ const archiveLocation = new URL(archivePath, currentLocation);
+ const applicationLocation = new URL(applicationPath, currentLocation);
+ await writeArchive(write, read, archiveLocation, applicationLocation);
+ return 0;
+ }
+ return parameter(args, handleEntry, noEntryUsage);
+ }
+ return parameter(args, handleArchive, noArchiveUsage);
+ }
+
+ return subcommand(args, { compartmap, archive });
+}
+
+export async function main(process, modules) {
+ const { fs } = modules;
+ const { cwd, stdout } = process;
+
+ // Filesystem errors often don't have stacks:
+
+ async function read(location) {
+ try {
+ return await fs.readFile(new URL(location).pathname);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ }
+
+ async function write(location, content) {
+ try {
+ return await fs.writeFile(new URL(location).pathname, content);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ }
+
+ try {
+ process.exitCode = await run(process.argv.slice(2), {
+ read,
+ write,
+ cwd,
+ stdout
+ });
+ } catch (error) {
+ process.exitCode = usage(error.stack || error.message);
+ }
+}

MODIFY
packages/endo/src/compartmap.js
packages/endo/src/compartmap.js
@@ -102,7 +102,7 @@ const inferParsers = (type, location) => {
// that the package exports.
const graphPackage = async (
- name,
+ name = "",
readDescriptor,
graph,
{ packageLocation, packageDescriptor },

MODIFY
packages/endo/src/import-archive.js
packages/endo/src/import-archive.js
@@ -22,8 +22,8 @@ const makeArchiveImportHookMaker = archive => {
return makeImportHook;
};
-export const parseArchive = async archiveBytes => {
- const archive = await readZip(archiveBytes);
+export const parseArchive = async (archiveBytes, archiveLocation) => {
+ const archive = await readZip(archiveBytes, archiveLocation);
const compartmentMapBytes = await archive.read("compartmap.json");
const compartmentMapText = decoder.decode(compartmentMapBytes);
@@ -47,12 +47,17 @@ export const parseArchive = async archiveBytes => {
return { execute };
};
-export const loadArchive = async (read, archivePath) => {
- const archiveBytes = await read(archivePath);
- return parseArchive(archiveBytes);
+export const loadArchive = async (read, archiveLocation) => {
+ const archiveBytes = await read(archiveLocation);
+ return parseArchive(archiveBytes, archiveLocation);
};
-export const importArchive = async (read, archivePath, endowments, modules) => {
- const archive = await loadArchive(read, archivePath);
+export const importArchive = async (
+ read,
+ archiveLocation,
+ endowments,
+ modules
+) => {
+ const archive = await loadArchive(read, archiveLocation);
return archive.execute(endowments, modules);
};

MODIFY
packages/endo/src/zip.js
packages/endo/src/zip.js
import JSZip from "jszip";
-export const readZip = async data => {
+export const readZip = async (data, location) => {
const zip = new JSZip();
await zip.loadAsync(data);
- const read = async path => zip.file(path).async("uint8array");
+ const read = async path => {
+ const file = zip.file(path);
+ if (file === undefined) {
+ throw new Error(
+ `Cannot find file to read ${path} in archive ${location}`
+ );
+ }
+ return file.async("uint8array");
+ };
return { read };
};

feat(endo): Add command line (#398)

This change provides a preliminary command line for `endo`, mostly because it is convenient for ad-hoc testing, though it will likely grow into something else.